diff --git a/Classes/Common/Indexer.php b/Classes/Common/Indexer.php
index 582bb9f310..3f859fbf71 100644
--- a/Classes/Common/Indexer.php
+++ b/Classes/Common/Indexer.php
@@ -74,6 +74,13 @@ class Indexer
*/
protected static array $processedDocs = [];
+ /**
+ * @access protected
+ * @static
+ * @var array List of already extracted structure nodes for structure path
+ */
+ protected static array $extractedStructurePathNodes = [];
+
/**
* @access protected
* @static
@@ -319,6 +326,10 @@ protected static function processLogical(Document $document, array $logicalUnit)
$solrDoc->setField('toplevel', $logicalUnit['id'] == $doc->toplevelId ? true : false);
$solrDoc->setField('title', $metadata['title'][0], self::$fields['fieldboost']['title']);
$solrDoc->setField('volume', $metadata['volume'][0], self::$fields['fieldboost']['volume']);
+ // extract structure path
+ self::$extractedStructurePathNodes[$logicalUnit['id']] = self::extractStructurePathNodes($doc->tableOfContents, $logicalUnit['id']);
+ $processedStructurePath = self::buildStructurePathData(self::$extractedStructurePathNodes[$logicalUnit['id']], $document->getCurrentDocument()->toplevelId);
+ $solrDoc->setField('structure_path', json_encode($processedStructurePath, JSON_UNESCAPED_UNICODE));
// verify date formatting
if(strtotime($metadata['date'][0])) {
$solrDoc->setField('date', self::getFormattedDate($metadata['date'][0]));
@@ -404,6 +415,21 @@ protected static function processPhysical(Document $document, int $page, array $
$solrDoc->setField('type', $physicalUnit['type'], self::$fields['fieldboost']['type']);
$solrDoc->setField('collection', $doc->metadataArray[$doc->toplevelId]['collection']);
$solrDoc->setField('location', $document->getLocation());
+ // pick only the deepest structure paths
+ $associatedPaths = [];
+ foreach ($doc->smLinks['p2l'][$physicalUnit['id']] as $logicalId) {
+ $path = self::$extractedStructurePathNodes[$logicalId] ?? [];
+ if (!empty($path)) {
+ $associatedPaths[$logicalId] = $path;
+ }
+ }
+ $deepestPaths = self::filterDeepestStructurePaths($associatedPaths);
+ $processedStructurePath = [];
+ foreach ($deepestPaths as $path) {
+ $segments = self::buildStructurePathData($path, $document->getCurrentDocument()->toplevelId);
+ $processedStructurePath[] = json_encode($segments, JSON_UNESCAPED_UNICODE);
+ }
+ $solrDoc->setField('structure_path', $processedStructurePath);
$solrDoc->setField('fulltext', $fullText);
if (is_array($doc->metadataArray[$doc->toplevelId])) {
@@ -639,6 +665,147 @@ private static function removeAppendsFromAuthor($authors)
return $authors;
}
+ /**
+ * Extract nodes alongside the structure map in direct line to the target id and return them as flattened array.
+ *
+ * @access private
+ *
+ * @static
+ *
+ * @param array $nodes Tree or Sub-Tree, where the target id should be extracted from if present
+ * @param string $targetId The ID of the logical structure element to be found
+ * @param array $path An intermediate array that keeps track of the current branch that is being looked up
+ *
+ * @return array
+ */
+ private static function extractStructurePathNodes(array $nodes, string $targetId, array $path = []): array
+ {
+ foreach ($nodes as $node) {
+ // remember where we came from
+ $currentPath = array_merge($path, [$node]);
+ if ($node['id'] == $targetId) {
+ return $currentPath;
+ }
+ if (!empty($node['children'])) {
+ $result = self::extractStructurePathNodes($node['children'], $targetId, $currentPath);
+ if ($result) {
+ return $result;
+ }
+ }
+ }
+ return [];
+ }
+
+ /**
+ * Filters those structure path nodes that are the descending into the structure tree the most and removes any that resemble a "prefix" of another.
+ *
+ * @access private
+ *
+ * @static
+ *
+ * @param array $paths The array containing all structure path nodes associated with a physical page
+ *
+ * @return array
+ */
+ private static function filterDeepestStructurePaths(array $paths): array
+ {
+ if (count($paths) <= 1) {
+ return $paths;
+ }
+
+ $deepestPath = [];
+ foreach ($paths as $currentLogicalId => $currentPath) {
+ $currentIds = array_column($currentPath, 'id');
+ $isPrefix = false;
+
+ foreach ($paths as $comparisonLogicalId => $comparisonPath) {
+ if ($currentLogicalId === $comparisonLogicalId) {
+ continue;
+ }
+ $comparisonIds = array_column($comparisonPath, 'id');
+ // check if structure path is part/prefix of another structure path
+ if (
+ count($currentIds) < count($comparisonIds)
+ && array_slice($comparisonIds, 0, count($currentIds)) === $currentIds
+ ) {
+ $isPrefix = true;
+ break;
+ }
+ }
+
+ if (!$isPrefix) {
+ $deepestPath[$currentLogicalId] = $currentPath;
+ }
+ }
+ return $deepestPath;
+ }
+
+ /**
+ * Create the actual array with the required data for the structure path that will be JSON encoded and indexed.
+ *
+ * @access private
+ *
+ * @static
+ *
+ * @param array $path The structure path nodes that shall be processed
+ * @param string $cutoffId The logical id at which ancestors and itself will not be part of the structure path data
+ *
+ * @return array
+ */
+ private static function buildStructurePathData(array $path, string $cutoffId): array
+ {
+ $cutoffIndex = array_search($cutoffId, array_column($path, 'id'));
+ if ($cutoffIndex !== false) {
+ $path = array_slice($path, $cutoffIndex + 1);
+ }
+
+ $segments = [];
+ foreach ($path as $node) {
+ $segments[] = self::buildStructurePathSegments($node);
+ }
+ return $segments;
+ }
+
+ /**
+ * Gets the label or type of a structure path node with corresponding tag
+ *
+ * @access private
+ *
+ * @static
+ *
+ * @param array $node The current node that should be processed
+ *
+ * @return array
+ */
+ private static function buildStructurePathSegments(array $node): array
+ {
+ if (!empty($node['label'])) {
+ return [
+ 'label' => $node['label'],
+ ];
+ }
+ if (!empty($node['orderlabel'])) {
+ return [
+ 'label' => $node['orderlabel'],
+ ];
+ }
+ if (!empty($node['volume'])) {
+ $value = !empty($node['year'])
+ ? $node['volume'] . ' ' . $node['year']
+ : $node['volume'];
+
+ return [
+ 'label' => $value,
+ ];
+ }
+ if (!empty($node['type'])) {
+ return [
+ 'type' => $node['type'],
+ ];
+ }
+ return ['label' => ''];
+ }
+
/**
* Handle exception.
*
diff --git a/Classes/Common/Solr/SearchResult/ResultDocument.php b/Classes/Common/Solr/SearchResult/ResultDocument.php
index 764070aef1..ddbc8e6c47 100644
--- a/Classes/Common/Solr/SearchResult/ResultDocument.php
+++ b/Classes/Common/Solr/SearchResult/ResultDocument.php
@@ -73,6 +73,12 @@ class ResultDocument
*/
private ?string $type;
+ /**
+ * @access private
+ * @var array The JSON encoded structure path(s)
+ */
+ private array $structurePath = [];
+
/**
* @access private
* @var Page[] All pages in which search phrase was found
@@ -117,6 +123,7 @@ public function __construct(Document $record, array $highlighting, array $fields
$this->title = $record[$fields['title']];
$this->toplevel = $record[$fields['toplevel']] ?? false;
$this->type = $record[$fields['type']];
+ $this->structurePath = $record[$fields['structure_path']] ?? [];
if (!empty($highlighting[$this->id])) {
$highlightingForRecord = $highlighting[$this->id][$fields['fulltext']];
@@ -225,6 +232,18 @@ public function getType(): ?string
return $this->type;
}
+ /**
+ * Get the structure path(s)
+ *
+ * @access public
+ *
+ * @return array
+ */
+ public function getStructurePath(): array
+ {
+ return $this->structurePath;
+ }
+
/**
* Get all result's pages which contain search phrase.
*
diff --git a/Classes/Common/Solr/Solr.php b/Classes/Common/Solr/Solr.php
index c6eea82e45..9786b7b58f 100644
--- a/Classes/Common/Solr/Solr.php
+++ b/Classes/Common/Solr/Solr.php
@@ -256,6 +256,7 @@ public static function getFields(): array
self::$fields['type'] = $solrFields['type'];
self::$fields['title'] = $solrFields['title'];
self::$fields['volume'] = $solrFields['volume'];
+ self::$fields['structure_path'] = $solrFields['structurePath'];
self::$fields['date'] = $solrFields['date'];
self::$fields['thumbnail'] = $solrFields['thumbnail'];
self::$fields['default'] = $solrFields['default'];
diff --git a/Classes/Common/Solr/SolrSearch.php b/Classes/Common/Solr/SolrSearch.php
index a21cc3a8b1..829b3cecd0 100644
--- a/Classes/Common/Solr/SolrSearch.php
+++ b/Classes/Common/Solr/SolrSearch.php
@@ -443,7 +443,7 @@ public function prepare()
$params['listMetadataRecords'] = [];
// Restrict the fields to the required ones.
- $params['fields'] = 'uid,id,page,title,thumbnail,partof,toplevel,type';
+ $params['fields'] = 'uid,id,page,title,thumbnail,partof,toplevel,type,structure_path';
if ($this->listedMetadata) {
foreach ($this->listedMetadata as $metadata) {
@@ -525,6 +525,31 @@ public function submit($start, $rows, $processResults = true)
$searchResult['page'] = $doc['page'];
$searchResult['thumbnail'] = $doc['thumbnail'];
$searchResult['structure'] = $doc['type'];
+ // create string(s) from structure path(s)
+ $encodedStructurePaths = $doc['structure_path'] ?? [];
+ if (!is_array($encodedStructurePaths)) {
+ $encodedStructurePaths = [$encodedStructurePaths];
+ }
+ $structurePathStrings = [];
+ foreach ($encodedStructurePaths as $jsonString) {
+ if (!is_string($jsonString) || $jsonString === '') {
+ continue;
+ }
+ $segments = json_decode($jsonString, true);
+ if ($segments === null && json_last_error() !== JSON_ERROR_NONE) {
+ continue;
+ }
+ $structurePathLabels = [];
+ foreach ($segments as $currentSegment) {
+ if (isset($currentSegment['type'])) {
+ $structurePathLabels[] = Helper::translate($currentSegment['type'], 'tx_dlf_structures', $this->settings['storagePid']);
+ } elseif (!empty($currentSegment['label'])) {
+ $structurePathLabels[] = $currentSegment['label'];
+ }
+ }
+ $structurePathStrings[] = implode(' → ', $structurePathLabels);
+ }
+ $searchResult['structure_path'] = $structurePathStrings;
$searchResult['title'] = $doc['title'];
foreach ($params['listMetadataRecords'] as $indexName => $solrField) {
if (isset($doc['metadata'][$indexName])) {
@@ -826,6 +851,7 @@ private function getDocument(Document $record, array $highlighting, array $field
'title' => $resultDocument->getTitle(),
'toplevel' => $resultDocument->getToplevel(),
'type' => $resultDocument->getType(),
+ 'structure_path' => $resultDocument->getStructurePath(),
'uid' => !empty($resultDocument->getUid()) ? $resultDocument->getUid() : $parameters['uid'],
'highlight' => $resultDocument->getHighlightsIds(),
];
diff --git a/Configuration/FlexForms/ListView.xml b/Configuration/FlexForms/ListView.xml
index 69439b9c5c..2c8783d41d 100644
--- a/Configuration/FlexForms/ListView.xml
+++ b/Configuration/FlexForms/ListView.xml
@@ -76,6 +76,14 @@
+
+ 1
+
+
+ check
+ 0
+
+
reload
diff --git a/Resources/Private/Language/de.locallang_be.xlf b/Resources/Private/Language/de.locallang_be.xlf
index 3923d04225..82c91e6b7c 100644
--- a/Resources/Private/Language/de.locallang_be.xlf
+++ b/Resources/Private/Language/de.locallang_be.xlf
@@ -14,6 +14,10 @@
+
+
+
+
diff --git a/Resources/Private/Language/de.locallang_labels.xlf b/Resources/Private/Language/de.locallang_labels.xlf
index b9101521ec..b3fc37796e 100644
--- a/Resources/Private/Language/de.locallang_labels.xlf
+++ b/Resources/Private/Language/de.locallang_labels.xlf
@@ -776,6 +776,10 @@
Solr-Schema-Feld "volume" : Volume field is mandatory for identifying documents (Standard ist "volume")
Solr Schema Field "volume" : Volume field is mandatory for identifying documents (default is "volume")
+
+
+ Solr-Schema-Feld "structure_path" : Field providing context about the location of a resource in the structure map (Standard ist "structure_path")
+ Solr Schema Field "structure_path" : Field providing context about the location of a resource in the structure map (default is "structure_path")
Solr Schema Field "date" : The date a resource was issued or created. Used for datesearch (Standard ist "date")
diff --git a/Resources/Private/Language/de.locallang_metadata.xlf b/Resources/Private/Language/de.locallang_metadata.xlf
index 84af587407..a49a040bf7 100644
--- a/Resources/Private/Language/de.locallang_metadata.xlf
+++ b/Resources/Private/Language/de.locallang_metadata.xlf
@@ -89,6 +89,10 @@
+
+
+
+
diff --git a/Resources/Private/Language/locallang_be.xlf b/Resources/Private/Language/locallang_be.xlf
index ab3c0d3985..9a0d80ae45 100644
--- a/Resources/Private/Language/locallang_be.xlf
+++ b/Resources/Private/Language/locallang_be.xlf
@@ -14,6 +14,9 @@
+
+
+
diff --git a/Resources/Private/Language/locallang_labels.xlf b/Resources/Private/Language/locallang_labels.xlf
index b36e6b23bd..e4c4def65f 100644
--- a/Resources/Private/Language/locallang_labels.xlf
+++ b/Resources/Private/Language/locallang_labels.xlf
@@ -587,6 +587,9 @@
Solr Schema Field "volume" : Volume field is mandatory for identifying documents (default is "volume")
+
+ Solr Schema Field "structure_path" : Field providing context about the location of a resource in the structure map (default is "structure_path")
+
Solr Schema Field "date" : The date a resource was issued or created. Used for datesearch (default is "date")
diff --git a/Resources/Private/Language/locallang_metadata.xlf b/Resources/Private/Language/locallang_metadata.xlf
index 70034a09df..02165d5fa6 100644
--- a/Resources/Private/Language/locallang_metadata.xlf
+++ b/Resources/Private/Language/locallang_metadata.xlf
@@ -68,6 +68,9 @@
+
+
+
diff --git a/ext_conf_template.txt b/ext_conf_template.txt
index b2e51147a8..559d6ee498 100644
--- a/ext_conf_template.txt
+++ b/ext_conf_template.txt
@@ -62,6 +62,8 @@ solr.fields.partof = partof
solr.fields.root = root
# cat=Solr; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.solr.fields.sid
solr.fields.sid = sid
+# cat=Solr; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.solr.fields.structurePath
+solr.fields.structurePath = structure_path
# cat=Solr; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.solr.fields.toplevel
solr.fields.toplevel = toplevel
# cat=Solr; type=string; label=LLL:EXT:dlf/Resources/Private/Language/locallang_labels.xlf:config.solr.fields.type