diff --git a/Classes/Command/IndexCommand.php b/Classes/Command/IndexCommand.php
new file mode 100644
index 0000000..e8be191
--- /dev/null
+++ b/Classes/Command/IndexCommand.php
@@ -0,0 +1,163 @@
+setDescription('Create elasticsearch index from zotero bibliography');
+ }
+
+ protected function initialize(InputInterface $input, OutputInterface $output) {
+ $this->extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('liszt_bibliography');
+ $this->client = ElasticClientBuilder::getClient();
+ $this->bibApi = new ZoteroApi($this->extConf['zoteroApiKey']);
+ $this->localeApi = new ZoteroApi($this->extConf['zoteroApiKey']);
+ $this->io = new SymfonyStyle($input, $output);
+ $this->io->title($this->getDescription());
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->io->section('Fetching Bibliography Data');
+ $this->fetchBibliography();
+ $this->io->section('Committing Bibliography Data');
+ $this->commitBibliography();
+ $this->io->section('Committing Locale Data');
+ $this->commitLocales();
+ return 0;
+ }
+
+ protected function fetchBibliography(): void
+ {
+ // fetch locales
+ $response = $this->localeApi->
+ raw('https://api.zotero.org/schema?format=json')->
+ send();
+ $this->locales = $response->getBody()['locales'];
+
+ // get bulk size and total size
+ $bulkSize = (int) $this->extConf['zoteroBulkSize'];
+ $response = $this->bibApi->
+ group($this->extConf['zoteroGroupId'])->
+ items()->
+ top()->
+ limit(1)->
+ send();
+ $total = (int) $response->getHeaders()['Total-Results'][0];
+
+ // fetch bibliography items bulkwise
+ $this->io->progressStart($total);
+ $collection = new Collection($response->getBody());
+ $this->bibliographyItems = $collection->pluck('data');
+
+ $cursor = $bulkSize;
+ while ($cursor < $total) {
+ $this->io->progressAdvance($bulkSize);
+ $response = $this->bibApi->
+ group($this->extConf['zoteroGroupId'])->
+ items()->
+ top()->
+ start($cursor)->
+ limit($bulkSize)->
+ send();
+ $collection = new Collection($response->getBody());
+ $this->bibliographyItems = $this->bibliographyItems->
+ concat($collection->pluck('data'));
+ $cursor += $bulkSize;
+ }
+ $this->io->progressFinish();
+ }
+
+ protected function commitBibliography(): void
+ {
+ $index = $this->extConf['elasticIndexName'];
+ $this->io->text('Committing the ' . $index . ' index');
+
+ $this->io->progressStart(count($this->bibliographyItems));
+ if ($this->client->indices()->exists(['index' => $index])) {
+ $this->client->indices()->delete(['index' => $index]);
+ $this->client->indices()->create(['index' => $index]);
+ }
+
+ $params = [ 'body' => [] ];
+ $bulkCount = 0;
+ foreach ($this->bibliographyItems as $document) {
+ $this->io->progressAdvance();
+ $params['body'][] = [ 'index' =>
+ [
+ '_index' => $index,
+ '_id' => $document['key']
+ ]
+ ];
+ $params['body'][] = json_encode($document);
+
+ if (!(++$bulkCount % $this->extConf['elasticBulkSize'])) {
+ $this->client->bulk($params);
+ $params = [ 'body' => [] ];
+ }
+ }
+ $this->io->progressFinish();
+ $this->client->bulk($params);
+
+ $this->io->text('done');
+ }
+
+ protected function commitLocales(): void
+ {
+ $localeIndex = $this->extConf['elasticLocaleIndexName'];
+ $this->io->text('Committing the ' . $localeIndex . ' index');
+
+ if ($this->client->indices()->exists(['index' => $localeIndex])) {
+ $this->client->indices()->delete(['index' => $localeIndex]);
+ $this->client->indices()->create(['index' => $localeIndex]);
+ }
+
+ $params = [ 'body' => [] ];
+ foreach ($this->locales as $key => $locale) {
+ $params['body'][] = [ 'index' =>
+ [
+ '_index' => $localeIndex,
+ '_id' => $key
+ ]
+ ];
+ $params['body'][] = json_encode($locale);
+
+ }
+ $this->client->bulk($params);
+
+ $this->io->text('done');
+ }
+}
diff --git a/Classes/Controller/BibliographyController.php b/Classes/Controller/BibliographyController.php
new file mode 100644
index 0000000..2e0511a
--- /dev/null
+++ b/Classes/Controller/BibliographyController.php
@@ -0,0 +1,93 @@
+responseFactory = $responseFactory;
+ $this->streamFactory = $streamFactory;
+ }
+
+ public function initializeAction(): void
+ {
+ $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('liszt_bibliography');
+ $this->bibIndex = $extConf['elasticIndexName'];
+ $this->localeIndex = $extConf['elasticLocaleIndexName'];
+ }
+
+ public function indexAction(): ResponseInterface
+ {
+ $this->createJsCall();
+ $this->wrapTargetDiv();
+ $contentStream = $this->
+ streamFactory->
+ createStream(
+ $this->div .
+ $this->jsCall
+ );
+
+ return $this->
+ responseFactory->
+ createResponse()->
+ withBody($contentStream);
+ }
+
+ private function wrapTargetDiv(): void
+ {
+ $sideCol = '
';
+ $mainCol = '';
+ $this->div = '' .
+ $sideCol . $mainCol . '
';
+ }
+
+ private function createJsCall(): void
+ {
+ $this->jsCall =
+ '';
+ }
+}
diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml
new file mode 100644
index 0000000..b621b14
--- /dev/null
+++ b/Configuration/Services.yaml
@@ -0,0 +1,18 @@
+services:
+ _defaults:
+ autowire: true
+ autoconfigure: true
+ public: false
+ Slub\LisztBibliography\:
+ resource: '../Classes/*'
+ exclude: '../Classes/Domain/Model/*'
+ Slub\LisztBibliography\Classes\Controller\BibliographyController:
+ tags:
+ - backend.controller
+ Slub\LisztBibliography\Command\IndexCommand:
+ tags:
+ -
+ name: console.command
+ command: 'liszt-bibliography:index'
+ description: 'Create elasticsearch index from zotero bibliography'
+ schedulable: true
diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php
new file mode 100644
index 0000000..4c454d7
--- /dev/null
+++ b/Configuration/TCA/Overrides/tt_content.php
@@ -0,0 +1,45 @@
+ '
+ --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
+ --palette--;;general,
+ header; Title,
+ bodytext;LLL:EXT:core/Resources/Private/Language/Form/locallang_ttc.xlf:bodytext_formlabel,
+ --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
+ --palette--;;hidden,
+ --palette--;;acces,
+ ',
+ 'columnsOverrides' => [
+ 'bodytext' => [
+ 'config' => [
+ 'enableRichtext' => true,
+ 'richtextConfiguration' => 'default'
+ ]
+ ]
+ ]
+];
+
diff --git a/Configuration/TsConfig/Page/Mod/Wizards/Listing.tsconfig b/Configuration/TsConfig/Page/Mod/Wizards/Listing.tsconfig
new file mode 100644
index 0000000..1ea5e22
--- /dev/null
+++ b/Configuration/TsConfig/Page/Mod/Wizards/Listing.tsconfig
@@ -0,0 +1,28 @@
+mod.wizards.newContentElement.wizardItems {
+ plugins {
+ elements {
+ lisztbibliography_bibliographylisting {
+ iconIdentifier = content-text
+ title = LLL:EXT:liszt_bibliography/Resources/Private/Language/locallang.xlf:listing_title
+ description = LLL:EXT:liszt_bibliography/Resources/Private/Language/locallang.xlf:listing_description
+ tt_content_defValues {
+ CType = list
+ list_type = lisztbibliography_bibliographylisting
+ }
+ }
+ }
+ }
+ common {
+ elements {
+ lisztbibliography_bibliographylisting {
+ iconIdentifier = content-text
+ title = LLL:EXT:liszt_bibliography/Resources/Private/Language/locallang.xlf:listing_title
+ description = LLL:EXT:liszt_bibliography/Resources/Private/Language/locallang.xlf:listing_description
+ tt_content_defValues {
+ CType = lisztbibliography_listing
+ }
+ }
+ }
+ show := addToList(lisztbibliography_listing)
+ }
+}
diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript
new file mode 100644
index 0000000..d6de7b9
--- /dev/null
+++ b/Configuration/TypoScript/setup.typoscript
@@ -0,0 +1,25 @@
+######################
+#### DEPENDENCIES ####
+######################
+
+
+lib.contentElement {
+ #layoutRootPaths {
+ #200 = EXT:liszt_bibliography:Resources/Private/Layouts
+ #}
+ #partialRootPaths {
+ #200 = EXT:liszt_bibliography:Resources/Private/Partial
+ #}
+ templateRootPaths {
+ 200 = EXT:liszt_bibliography/Resources/Private/Templates
+ }
+}
+
+tt_content {
+ lisztbibliography_listing =< lib.contentElement
+ lisztbibliography_listing {
+ templateName = BibliographyListing
+ }
+}
+
+page.includeJSFooter.BibliographyController = EXT:liszt_bibliography/Resources/Public/JavaScript/Src/BibliographyController.js
diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf
new file mode 100644
index 0000000..ac8e907
--- /dev/null
+++ b/Resources/Private/Language/locallang.xlf
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/Public/JavaScript/Src/BibliographyController.js b/Resources/Public/JavaScript/Src/BibliographyController.js
new file mode 100644
index 0000000..a21419f
--- /dev/null
+++ b/Resources/Public/JavaScript/Src/BibliographyController.js
@@ -0,0 +1,155 @@
+/** todos
+ * fehlermeldungen
+ * ad hoc funktionen benennen
+ * facette entstehungsdatum
+ * t3-buildmechanismus für js
+ * facettenregistratur
+ * urlparameter auf highlighting abbilden
+ */
+
+const LISZT_BIB_DEFAULT_SIZE = 20;
+const LISZT_BIB_DEFAULT_TRIGGER_POSITION = 200;
+const LISZT_BIB_DEFAULT_BIBLIST_ID = 'bib-list';
+const LISZT_BIB_DEFAULT_SIDELIST_ID = 'bib-list-side';
+const LISZT_BIB_DEFAULT_BIBINDEX = 'zotero';
+const LISZT_BIB_DEFAULT_LOCALEINDEX = 'zotero';
+
+class BibliographyController {
+
+ #urlManager = null;
+ #target = '';
+ #size = LISZT_BIB_DEFAULT_SIZE;
+ #triggerPosition = LISZT_BIB_DEFAULT_TRIGGER_POSITION;
+ #bibListId = LISZT_BIB_DEFAULT_BIBLIST_ID;
+ #url = new URL(location);
+ #body = {};
+
+ constructor (config) {
+ this.#target = config.target;
+ this.#size = config.size ?? this.#size;
+ this.#triggerPosition = config.triggerPosition ?? this.#triggerPosition;
+ this.#bibListId = config.bibListId ?? this.#bibListId;
+
+ this.init();
+ }
+
+ init() {
+ this.#body = {};
+ this.#urlManager = new UrlManager();
+ this.#urlManager.registerMapping({itemTypes: 'query.match.itemType'});
+ this.#body = this.#urlManager.body;
+
+ this.client = new elasticsearch.Client({
+ host: 'https://ddev-liszt-portal.ddev.site:9201'
+ });
+ this.from = 0;
+ this.docs = this.client.search({ index: 'zotero', size: this.#size, body: this.#body });
+ this.docs.then(docs => this.renderDocs(docs, this.#target));
+
+ const types = this.client.search({
+ index:'zotero',
+ size:0,
+ body:{
+ aggs:{
+ types:{
+ categorize_text:{
+ field:'itemType'
+ }
+ }
+ }
+ }
+ });
+ types.then(r => this.renderTypes(r.aggregations.types.buckets));
+
+ let allowed = true;
+ $(window).scroll(_ => {
+ const bottomPosition = $(document).scrollTop() + $(window).height()
+ if (bottomPosition > $(document).height() - this.#triggerPosition && allowed) {
+ allowed = false;
+ this.from += this.#size;
+ const newDocs = this.client.search({index:'zotero',size:this.#size,from:this.from,body:this.#body});
+ newDocs.then(docs => {
+ this.appendDocs(docs, this.#target);
+ allowed = true;
+ });
+ }
+ });
+ }
+
+ renderTypes(buckets) {
+ const render = buckets => {
+ const renderedBuckets = buckets
+ .sort((a, b) => (b.doc_count - a.doc_count))
+ .map(bucket => this.renderType(bucket, this.locales))
+ .join('');
+ const buttonGroup = `${renderedBuckets}
`;
+ $(`#bib-list-side`).append(`${this.locales.fields.itemType}
`);
+ $(`#bib-list-side #item-type`).append(buttonGroup);
+ $(`#bib-list-side .list-group-item-action`).click(d => {
+ d.preventDefault();
+ this.from = 0;
+ $(d.target).toggleClass('active');
+ const matches = $(`#bib-list-side .list-group-item.active`).map((d,i) => $(i).attr('data'));
+ const itemTypes = matches.toArray().join(' ');
+
+ this.#urlManager.setParam('itemTypes', itemTypes);
+ this.#body = this.#urlManager.body;
+ const docs = this.client.search({index:'zotero',size:this.#size,from:this.from,body:this.#body});
+ docs.then(docs => {
+ this.renderDocs(docs, this.#target);
+ });
+ });
+ }
+ if (!this.locales) {
+ this.client.get({index:'zoterolocales',id:'de'}).then(locales => {
+ this.locales = locales._source;
+ render(buckets);
+ });
+ } else {
+ render(buckets);
+ }
+ }
+
+ renderType(bucket, locales) {
+ const key = locales.itemTypes[bucket.key];
+ return ` ${key} (${bucket.doc_count}) `;
+ }
+
+ appendDocs(docs, target) {
+ const hits = docs.hits.hits.map(hit => hit._source);
+ const renderedDocs = hits.map(hit => BibliographyController.renderDoc(hit, this.locales)).join('');
+ $(renderedDocs).hide().appendTo(`#${target} #${this.#bibListId}`).fadeIn('slow');
+
+ }
+
+ renderDocs(docs, target) {
+ const render = (docs, target) => {
+ const hits = docs.hits.hits.map(hit => hit._source);
+ const renderedDocs = hits.map(hit => BibliographyController.renderDoc(hit, this.locales)).join('');
+ $(`#${target}`).html(``);
+ }
+ if (!this.locales) {
+ this.client.get({index:'zoterolocales',id:'de'}).then(locales => {
+ this.locales = locales._source;
+ render(docs, target)
+ });
+ } else {
+ render(docs, target);
+ }
+ }
+
+ static renderDoc(doc, locales) {
+ const itemType = locales.itemTypes[doc.itemType];
+ const renderedCreators = doc.creators ? BibliographyController.renderCreators(doc.creators, locales) : '';
+ return ` ${doc.title} ${itemType}
${renderedCreators} `;
+ }
+
+ static renderCreators(creators, locales) {
+ return creators.map(creator => BibliographyController.renderCreator(creator, locales)).join(', ');
+ }
+
+ static renderCreator(creator, locales) {
+ const creatorType = locales.creatorTypes[creator.creatorType];
+ return `${creator.firstName} ${creator.lastName} (${creatorType})`;
+ }
+}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..f2acc24
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "slub/liszt-bibliography",
+ "description": "Manages an elasticsearch index obtained from a zotero library and other data sources, displays a bibliography listing",
+ "type": "typo3-cms-extension",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "require": {
+ "typo3/cms-core": "^11.5",
+ "typo3/cms-fluid-styled-content": "^11.5",
+ "slub/liszt-common": "dev-main",
+ "dikastes/zotero-api": "dev-master",
+ "illuminate/collections": "^8"
+ },
+ "autoload": {
+ "psr-4": {
+ "Slub\\LisztBibliography\\": "Classes/"
+ }
+ },
+ "extra": {
+ "typo3/cms": {
+ "extension-key": "liszt_bibliography"
+ }
+ }
+}
diff --git a/ext_conf_template.txt b/ext_conf_template.txt
new file mode 100644
index 0000000..c6bb055
--- /dev/null
+++ b/ext_conf_template.txt
@@ -0,0 +1,14 @@
+# cat=Elasticsearch; type=string; label=LLL:EXT:publisher_db/Resources/Private/Language/Labels.xml:config.elasticIndexName
+elasticIndexName = zotero
+# cat=Elasticsearch; type=string; label=LLL:EXT:publisher_db/Resources/Private/Language/Labels.xml:config.elasticLocaleIndexName
+elasticLocaleIndexName = zoterolocales
+# cat=Elasticsearch; type=int; label=LLL:EXT:publisher_db/Resources/Private/Language/Labels.xml:config.elasticBulkSize
+elasticBulkSize = 20
+# cat=Zotero; type=string; label=LLL:EXT:liszt_zotero/Resources/Private/Language/Labels.xml:config.zoteroApiKey
+zoteroApiKey = xgC1nXqyHO2eYOq2M7Dj7V5q
+# cat=Zotero; type=int; label=LLL:EXT:liszt_zotero/Resources/Private/Language/Labels.xml:config.zoteroGroupId
+zoteroGroupId = 5080468
+# cat=Zotero; type=int; label=LLL:EXT:liszt_zotero/Resources/Private/Language/Labels.xml:config.zoteroUserId
+zoteroUserId = 7071210
+# cat=Zotero; type=int; label=LLL:EXT:liszt_zotero/Resources/Private/Language/Labels.xml:config.zoteroBulkSize
+zoteroBulkSize = 50
diff --git a/ext_localconf.php b/ext_localconf.php
new file mode 100644
index 0000000..e0cc1d0
--- /dev/null
+++ b/ext_localconf.php
@@ -0,0 +1,20 @@
+ 'index' ],
+ [ BibliographyController::class => 'index' ]
+);
+
+ExtensionManagementUtility::addPageTSConfig(
+ ''
+);