From 61df3ba2d371ddff0c6187ece86f2881603b9bb9 Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Mon, 17 Nov 2025 17:06:08 +0100 Subject: [PATCH 01/12] DEV DocMarkdownPrinter can use now ObjectMarkdownPrinter --- .../MarkdownPrinters/DocMarkdownPrinter.php | 15 +++++++++++++-- .../MarkdownPrinters/ObjectMarkdownPrinter.php | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Facades/DocsFacade/MarkdownPrinters/DocMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/DocMarkdownPrinter.php index 8660d85ff..72d87ba65 100644 --- a/Facades/DocsFacade/MarkdownPrinters/DocMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/DocMarkdownPrinter.php @@ -135,7 +135,7 @@ public function docsExists(): bool public function getMarkdown(): string { - $markdown = $this->readFile($this->getAbsolutePath()); + $markdown = null; if(StringDataType::endsWith($this->uri->getPath(), 'UXON_prototypes.md')) { $query = $this->uri->getQuery(); $params = []; @@ -144,8 +144,19 @@ public function getMarkdown(): string $printer = new UxonPrototypeMarkdownPrinter($this->workbench, $selector); $markdown = $printer->getMarkdown(); } + + if(StringDataType::endsWith($this->uri->getPath(), 'Available_metaobjects.md')) { + $query = $this->uri->getQuery(); + $params = []; + parse_str($query, $params); + $selector = urldecode($params['selector']); + $printer = new ObjectMarkdownPrinter($this->workbench, $selector); + $markdown = $printer->getMarkdown(); + } - + if(!$markdown) { + $markdown = $this->readFile($this->getAbsolutePath()); + } return $this->rebaseRelativeLinks($markdown, $this->getAbsolutePath(), $this->getDirectoryPath(),0); diff --git a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php index e84934ec8..f7c92090c 100644 --- a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php @@ -29,7 +29,7 @@ class ObjectMarkdownPrinter //implements MarkdownPrinterInterface public function __construct(WorkbenchInterface $workbench, string $objectId, ObjectMarkdownPrinter $parent = null) { $this->workbench = $workbench; - $this->objectId = $objectId; + $this->objectId = $this->normalize($objectId); if($parent){ $this->parent = $parent; $this->currentDepth = $parent->getCurrentDepth() + 1; @@ -109,6 +109,20 @@ protected function escapeCell(string $value): string return $value; } + protected function normalize(string $raw): string + { + $decoded = urldecode($raw); + + $start = strpos($decoded, '['); + $end = strpos($decoded, ']'); + + if ($start === false || $end === false || $end <= $start) { + return ''; + } + + return substr($decoded, $start + 1, $end - $start - 1); + } + protected function addRelation(string $relation) : ObjectMarkdownPrinter { From a70623a03d132250fb51563589ea7f5c523fb634 Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Mon, 17 Nov 2025 17:06:59 +0100 Subject: [PATCH 02/12] DEV UxonPrototypeMarkdownPrinter add Types to the Markdown --- .../MarkdownPrinters/UxonPrototypeMarkdownPrinter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Facades/DocsFacade/MarkdownPrinters/UxonPrototypeMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/UxonPrototypeMarkdownPrinter.php index 2e1fae239..eaaa04a7c 100644 --- a/Facades/DocsFacade/MarkdownPrinters/UxonPrototypeMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/UxonPrototypeMarkdownPrinter.php @@ -178,9 +178,11 @@ protected function buildMarkdownTableRowForProperties(array $propertyRow, int $h } return <<buildMarkdownHeading('Property `' . $propertyRow['PROPERTY'] . '`', $headingLevel + 1)} -{$propertyRow['TITLE']}{$links} +`Type : {$propertyRow['TYPE']}` +{$propertyRow['TITLE']}{$links} {$propertyRow['DESCRIPTION']} MD; } From 064a9e982cc70dc6d79bfde96051be30196b96b0 Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Mon, 17 Nov 2025 18:05:18 +0100 Subject: [PATCH 03/12] FIX ObjectMarkdownPrinter fixed that the normalized function not more return "" --- Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php index f7c92090c..c9a8d17ed 100644 --- a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php @@ -117,7 +117,7 @@ protected function normalize(string $raw): string $end = strpos($decoded, ']'); if ($start === false || $end === false || $end <= $start) { - return ''; + return $raw; } return substr($decoded, $start + 1, $end - $start - 1); From 852a3f8a3b565b9f1afc633ac83461b87b2f5609 Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Mon, 17 Nov 2025 18:06:27 +0100 Subject: [PATCH 04/12] NEW LogEntryMarkdownPrinter currently ignores values with support --- .../LogEntryMarkdownPrinter.php | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Facades/DocsFacade/MarkdownPrinters/LogEntryMarkdownPrinter.php diff --git a/Facades/DocsFacade/MarkdownPrinters/LogEntryMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/LogEntryMarkdownPrinter.php new file mode 100644 index 000000000..09df388ab --- /dev/null +++ b/Facades/DocsFacade/MarkdownPrinters/LogEntryMarkdownPrinter.php @@ -0,0 +1,102 @@ +workbench = $workbench; + + + //Clean up Log Id + if (stripos($logId, 'log-') !== false) { + $logId = str_ireplace('log-', '', $logId); + $logId = strtoupper($logId); + } + + $this->logId = $logId; + + } + + public function getMarkdown() : string + { + $logId = $this->logId; + + + + $logFileSheet = DataSheetFactory::createFromObjectIdOrAlias($this->workbench, 'exface.Core.LOG'); + $logFileCol = $logFileSheet->getColumns()->addFromExpression('PATHNAME_RELATIVE'); + $logFileSheet->getFilters()->addConditionFromString('CONTENTS', $logId, ComparatorDataType::IS); + $logFileSheet->dataRead(); + + $logFile = $logFileCol->getValue(0); + + $logEntrySheet = DataSheetFactory::createFromObjectIdOrAlias($this->workbench, 'exface.Core.LOG_ENTRY'); + $logEntrySheet->getColumns()->addMultiple([ + 'id', 'levelname', 'message', 'filepath', 'context' , 'channel' + ]); + $logEntrySheet->getFilters()->addConditionFromString('id',$logId, ComparatorDataType::EQUALS); + $logEntrySheet->getFilters()->addConditionFromString('logfile', $logFile, ComparatorDataType::EQUALS); + $logEntrySheet->dataRead(); + $row = $logEntrySheet->getRow(0); + $detailsPath = $this->workbench->filemanager()->getPathToLogDetailsFolder(). '/' . $row['filepath'] . '.json'; + + $detailsJson = json_decode(file_get_contents($detailsPath), true); + + return $this->buildMarkdown($detailsJson, 1); + + } + + protected function buildMarkdown($json, int $headingLevel) : string + { + $markdown = ""; + + if (isset($json['caption'])) { + + $hide = isset($json['hide_caption']) ? (bool)$json['hide_caption'] : false; + + if (!$hide) { + $markdown .= $this->buildMarkdownHeader($json['caption'], $headingLevel) . "\n"; + } + } + + if (isset($json['widgets']) && is_array($json['widgets'])) { + foreach ($json['widgets'] as $widget) { + $markdown .= $this->buildMarkdown($widget, $headingLevel + 1); + } + } + + if (isset($json['value'])) { + + // future toggle to allow filtering of forbidden words + $showContactSupport = true; + + if ( + $showContactSupport + && stripos(strtolower($json['value']), 'support') === false + ) { + $markdown .= $json['value']; + } + } + + + return $markdown; + } + + protected function buildMarkdownHeader(string $content, int $headingLevel) : string + { + $prefix = str_repeat('#', $headingLevel); + return $prefix . ' ' . $content; + } + + +} \ No newline at end of file From afcfb7fa51dcb25d3bb9cfa6a513b12d60f419f9 Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Tue, 25 Nov 2025 14:34:42 +0100 Subject: [PATCH 05/12] DEV MetaObjectPrinter function normalize add Description --- .../MarkdownPrinters/ObjectMarkdownPrinter.php | 13 +++++++++++++ .../Middleware/MetaObjectPrinterMiddleware.php | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php index c9a8d17ed..a0e143425 100644 --- a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php @@ -109,6 +109,18 @@ protected function escapeCell(string $value): string return $value; } + /** + * Normalizes a raw link selector by extracting the object ID or alias. + * + * The function looks for a pattern like: + * objectName [idOrAlias] + * and returns only the part inside the brackets. + * + * Example: + * "AI agent" [axenox.GenAI.AI_AGENT] → axenox.GenAI.AI_AGENT + * + * If the selector does not follow this pattern, the original raw string is returned unchanged. + */ protected function normalize(string $raw): string { $decoded = urldecode($raw); @@ -124,6 +136,7 @@ protected function normalize(string $raw): string } + protected function addRelation(string $relation) : ObjectMarkdownPrinter { if(!in_array($relation, $this->getFinishedRelations(), true) diff --git a/Facades/DocsFacade/Middleware/MetaObjectPrinterMiddleware.php b/Facades/DocsFacade/Middleware/MetaObjectPrinterMiddleware.php index cd5a97962..95450b7a2 100644 --- a/Facades/DocsFacade/Middleware/MetaObjectPrinterMiddleware.php +++ b/Facades/DocsFacade/Middleware/MetaObjectPrinterMiddleware.php @@ -75,6 +75,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $response; } + /** + * Normalizes a raw link selector by extracting the object ID or alias. + * + * The function looks for a pattern like: + * objectName [idOrAlias] + * and returns only the part inside the brackets. + * + * Example: + * "AI agent" [axenox.GenAI.AI_AGENT] → axenox.GenAI.AI_AGENT + * + * If the selector does not follow this pattern, the original raw string is returned unchanged. + */ protected function normalize(string $raw): string { $decoded = urldecode($raw); @@ -90,6 +102,7 @@ protected function normalize(string $raw): string } + protected function getWorkbench(): WorkbenchInterface { return $this->facade->getWorkbench(); From c6c6c0aac629028a7a8f7ee4c19174f25be58f7f Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Tue, 25 Nov 2025 15:41:03 +0100 Subject: [PATCH 06/12] DEV MarkdownDataType NEW functions buildMarkdownHeader & convertHeaderLevels --- DataTypes/MarkdownDataType.php | 79 +++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/DataTypes/MarkdownDataType.php b/DataTypes/MarkdownDataType.php index 1fd8bedff..24bff8f37 100644 --- a/DataTypes/MarkdownDataType.php +++ b/DataTypes/MarkdownDataType.php @@ -110,7 +110,26 @@ public static function buildMarkdownTableFromArray(array $rows, array $headings return $md; } - + + /** + * Builds a Markdown header string with the given heading level. + * + * Example: + * buildMarkdownHeader("Title", 3) + * returns: "### Title" + * + * @param string $content The text of the header + * @param int $headingLevel The header level (1 to 6) + * + * @return string A Markdown formatted header line + */ + public static function buildMarkdownHeader(string $content, int $headingLevel): string + { + $prefix = str_repeat('#', $headingLevel); + return $prefix . ' ' . $content; + } + + /** * * @param string $markdown @@ -122,6 +141,64 @@ public static function convertMarkdownToHtml(string $markdown) : string return $parser->parse($markdown); } + /** + * Adjusts all Markdown header levels so that the highest level header + * (the one with the fewest number of # characters) is shifted to a + * specified target level. + * + * Example: + * If the highest header in the input is "#" and $highestLevel = 2, + * all headers are shifted by +1, so: + * # Title -> ## Title + * ## Section -> ### Section + * + * If the highest header already equals $highestLevel, the input + * markdown is returned unchanged. + * + * Headers are always clamped between level 1 and 6. + * + * @param string $markdown The full markdown content + * @param int $highestLevel The desired level for the highest header (default 2) + * + * @return string The markdown content with adjusted header levels + */ + public static function convertHeaderLevels(string $markdown, int $highestLevel = 2): string + { + if (!preg_match_all('/^(#{1,6})\s+.+$/m', $markdown, $matches)) { + return $markdown; + } + + $levels = array_map('strlen', $matches[1]); + $minLevel = min($levels); + + if ($minLevel === $highestLevel) { + return $markdown; + } + + $offset = $highestLevel - $minLevel; + + $converted = preg_replace_callback( + '/^(#{1,6})\s+(.+)$/m', + function (array $m) use ($offset) { + $currentLevel = strlen($m[1]); + $newLevel = $currentLevel + $offset; + + if ($newLevel < 1) { + $newLevel = 1; + } elseif ($newLevel > 6) { + $newLevel = 6; + } + + return str_repeat('#', $newLevel) . ' ' . $m[2]; + }, + $markdown + ); + + return $converted ?? $markdown; + } + + + /** * * @see exface\Core\Interfaces\DataTypes\HtmlCompatibleDataTypeInterface:toHtml() From 7005bdc8aed07d705dc1159b4b119d4793f0dd87 Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Tue, 25 Nov 2025 15:44:07 +0100 Subject: [PATCH 07/12] DEV ObjectMarkdownPrinter add to everything description --- .../ObjectMarkdownPrinter.php | 140 ++++++++++++++++-- 1 file changed, 125 insertions(+), 15 deletions(-) diff --git a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php index a0e143425..be040e717 100644 --- a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php @@ -3,29 +3,68 @@ namespace exface\Core\Facades\DocsFacade\MarkdownPrinters; +use exface\Core\DataTypes\MarkdownDataType; use exface\Core\Factories\MetaObjectFactory; use exface\Core\Interfaces\Model\MetaObjectInterface; use exface\Core\Interfaces\WorkbenchInterface; +/** + * Builds a Markdown documentation view for a meta object and its related objects. + * + * The printer renders a table of attributes for the given meta object and can + * optionally walk through relation attributes to print child objects up to a + * configurable depth. + */ class ObjectMarkdownPrinter //implements MarkdownPrinterInterface { protected WorkbenchInterface $workbench; private string $objectId; - + + /** + * Maximum depth of recursive relation traversal. + * + * Depth 0 would print only the root object, higher values include related objects. + */ private int $depth = 3; - + + /** + * Current recursion depth for this printer instance. + */ private int $currentDepth = 0; - + + /** + * Parent printer that created this instance while walking relations. + * + * Null for the root printer. + */ private ?ObjectMarkdownPrinter $parent = null; - + + /** + * Queue of relation target object identifiers that still need to be processed. + * + * @var string[] + */ private array $relations = []; - + + /** + * List of relation target object identifiers that have already been processed. + * + * This is kept only on the root printer and shared through the tree in order + * to avoid infinite loops when relations form cycles. + * + * @var string[] + */ private array $finishedRelations = []; - + /** + * Creates a new object markdown printer for the given meta object identifier. + * + * When called from a parent printer the depth and current depth are inherited + * so that the whole tree respects the same maximum depth. + */ public function __construct(WorkbenchInterface $workbench, string $objectId, ObjectMarkdownPrinter $parent = null) { $this->workbench = $workbench; @@ -36,7 +75,11 @@ public function __construct(WorkbenchInterface $workbench, string $objectId, Obj $this->depth = $parent->getDepth(); } } - + + /** + * Builds and returns the complete Markdown for the current object + * and all related objects up to the configured depth. + */ public function getMarkdown(): string { if(!$this->objectId) return ''; @@ -89,19 +132,45 @@ protected function getAttributes(MetaObjectInterface $metaObject): array if ($attribute->isRelation()) { $relationObject = $attribute->getRelation(); $rightObject = $relationObject->getRightObject(); - $relation = $this->escapeCell($rightObject->getAlias()); - $link = '"'. $rightObject->getName() .'"['. $rightObject->getNameSpace() ."." .$rightObject->getAlias() ."]"; + $this->addRelation($rightObject->getId()); - $relation = "[" . $relation . "]"; - $relation .= "(Available_metaobjects.md?selector=". urlencode($link) .")"; + $relation = $this->createLink($rightObject); } + $list[] = "| {$name} | {$alias} | {$dataType} | {$required} | {$relation} |"; } return $list; } + /** + * Creates a Markdown link to the target meta object. + * + * The output format is: + * [Alias](Available_metaobjects.md?selector="ObjectName"[Namespace.Alias]) + * + * @param MetaObjectInterface $metaObject The target object on the right side of the relation + * @return string The formatted Markdown link + */ + protected function createLink(MetaObjectInterface $metaObject): string + { + $alias = $this->escapeCell($metaObject->getAlias()); + + $link = '"' . $metaObject->getName() . '"[' + . $metaObject->getNameSpace() . '.' + . $metaObject->getAlias() . ']'; + + return '[' . $alias . ']' . + '(Available_metaobjects.md?selector=' . urlencode($link) . ')'; + } + + + /** + * Escapes a value so that it can be safely used inside a Markdown table cell. + * + * Line breaks are converted to HTML line breaks and pipe characters are escaped. + */ protected function escapeCell(string $value): string { $value = str_replace(["\r\n", "\r", "\n"], '
', $value); @@ -136,7 +205,12 @@ protected function normalize(string $raw): string } - + /** + * Adds a relation target object identifier to the processing queue if it is not + * already in the queue and has not been processed before. + * + * @return ObjectMarkdownPrinter Provides fluent interface. + */ protected function addRelation(string $relation) : ObjectMarkdownPrinter { if(!in_array($relation, $this->getFinishedRelations(), true) @@ -145,7 +219,15 @@ protected function addRelation(string $relation) : ObjectMarkdownPrinter } return $this; } - + + /** + * Marks a relation target object identifier as processed. + * + * For child printers this call is delegated to the root printer so that + * the finished list is shared for the whole recursion tree. + * + * @return ObjectMarkdownPrinter Provides fluent interface on the root printer. + */ public function addFinishedRelation(string $relation) : ObjectMarkdownPrinter { if($this->parent){ @@ -157,7 +239,13 @@ public function addFinishedRelation(string $relation) : ObjectMarkdownPrinter return $this; } } - + + /** + * Marks multiple relation target object identifiers as processed. + * + * @param string[] $relations + * @return ObjectMarkdownPrinter + */ protected function addFinishedRelations(array $relations) : ObjectMarkdownPrinter { foreach ($relations as $relation){ @@ -177,26 +265,48 @@ public function setObjectId(?string $objectId): ObjectMarkdownPrinter return $this; } + /** + * Returns the maximum traversal depth configured for this printer tree. + */ public function getDepth(): int { return $this->depth; } + /** + * Returns the current recursion depth of this printer instance. + */ public function getCurrentDepth(): int { return $this->currentDepth; } + /** + * Returns the parent printer or null if this is the root printer. + */ public function getParent(): ?ObjectMarkdownPrinter { return $this->parent; } + /** + * Returns the queue of relation target object identifiers that still need processing. + * + * @return string[] + */ public function getRelations(): array { return $this->relations; } - + + /** + * Returns the list of relation target object identifiers that have been processed. + * + * For child printers this list is always read from the root printer so that + * all printers share the same finished set. + * + * @return string[] + */ public function getFinishedRelations(): array { if($this->parent){ From d297e858ec104af3ba4c0eddf4edd690eff7fe42 Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Tue, 25 Nov 2025 15:51:51 +0100 Subject: [PATCH 08/12] FIX LogEntryMarkdownPrinter and added all Description --- .../LogEntryMarkdownPrinter.php | 76 +++++++++++++------ 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/Facades/DocsFacade/MarkdownPrinters/LogEntryMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/LogEntryMarkdownPrinter.php index 09df388ab..f65e3e57a 100644 --- a/Facades/DocsFacade/MarkdownPrinters/LogEntryMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/LogEntryMarkdownPrinter.php @@ -3,35 +3,58 @@ namespace exface\Core\Facades\DocsFacade\MarkdownPrinters; use exface\Core\DataTypes\ComparatorDataType; +use exface\Core\DataTypes\MarkdownDataType; use exface\Core\Factories\DataSheetFactory; use exface\Core\Interfaces\WorkbenchInterface; +/** + * Builds a Markdown representation of a single log entry. + * + * The printer resolves the log file and log entry from the database, + * loads the corresponding JSON details file and renders it as Markdown. + * The JSON structure is processed recursively so that nested widgets + * and captions are converted into a hierarchical Markdown document. + */ class LogEntryMarkdownPrinter { + protected WorkbenchInterface $workbench; + /** + * Normalized identifier of the log entry that should be rendered. + * + * The constructor strips a leading "log-" prefix in a case insensitive way + * and stores the cleaned value in upper case. + */ protected string $logId; + public function __construct(WorkbenchInterface $workbench, string $logId, int $depth = 0) { $this->workbench = $workbench; - - //Clean up Log Id + // Clean up log id if (stripos($logId, 'log-') !== false) { $logId = str_ireplace('log-', '', $logId); $logId = strtoupper($logId); } $this->logId = $logId; - } + /** + * Generates the complete Markdown output for the configured log entry. + * + * The method resolves the log file that contains the entry, loads the + * corresponding details JSON from the file system and passes it to + * buildMarkdown for recursive rendering. + * + * @return string Markdown representation of the log entry or an empty string + * if no details can be found. + */ public function getMarkdown() : string { $logId = $this->logId; - - $logFileSheet = DataSheetFactory::createFromObjectIdOrAlias($this->workbench, 'exface.Core.LOG'); $logFileCol = $logFileSheet->getColumns()->addFromExpression('PATHNAME_RELATIVE'); @@ -44,18 +67,35 @@ public function getMarkdown() : string $logEntrySheet->getColumns()->addMultiple([ 'id', 'levelname', 'message', 'filepath', 'context' , 'channel' ]); - $logEntrySheet->getFilters()->addConditionFromString('id',$logId, ComparatorDataType::EQUALS); + $logEntrySheet->getFilters()->addConditionFromString('id', $logId, ComparatorDataType::EQUALS); $logEntrySheet->getFilters()->addConditionFromString('logfile', $logFile, ComparatorDataType::EQUALS); $logEntrySheet->dataRead(); + $row = $logEntrySheet->getRow(0); $detailsPath = $this->workbench->filemanager()->getPathToLogDetailsFolder(). '/' . $row['filepath'] . '.json'; - $detailsJson = json_decode(file_get_contents($detailsPath), true); + $detailsJson = json_decode(file_get_contents($detailsPath), true); return $this->buildMarkdown($detailsJson, 1); - } + /** + * Recursively converts a JSON structure of log details into Markdown. + * + * The method walks through the JSON node and renders: + * - caption: as a Markdown heading with the given heading level + * - widgets: each widget is processed recursively with an increased level + * - value: rendered as a heading as well, optionally filtered + * + * This recursive approach allows nested widgets and sections in the + * log details file to be mapped to a nested Markdown document with + * multiple levels of headings. + * + * @param array $json Decoded JSON node describing a part of the log details. + * @param int $headingLevel Current heading level that should be used for this node. + * + * @return string Generated Markdown segment for the given JSON node. + */ protected function buildMarkdown($json, int $headingLevel) : string { $markdown = ""; @@ -65,38 +105,30 @@ protected function buildMarkdown($json, int $headingLevel) : string $hide = isset($json['hide_caption']) ? (bool)$json['hide_caption'] : false; if (!$hide) { - $markdown .= $this->buildMarkdownHeader($json['caption'], $headingLevel) . "\n"; + $markdown .= MarkdownDataType::convertHeaderLevels($json['caption'], $headingLevel) . "\n"; } } if (isset($json['widgets']) && is_array($json['widgets'])) { foreach ($json['widgets'] as $widget) { + // Recursive call for each nested widget, using a deeper heading level $markdown .= $this->buildMarkdown($widget, $headingLevel + 1); } } if (isset($json['value'])) { - // future toggle to allow filtering of forbidden words + // Future toggle to allow filtering of forbidden words $showContactSupport = true; if ( $showContactSupport && stripos(strtolower($json['value']), 'support') === false ) { - $markdown .= $json['value']; + $markdown .= MarkdownDataType::convertHeaderLevels($json['value'], $headingLevel + 1); } } - - return $markdown; + return $markdown . "\n"; } - - protected function buildMarkdownHeader(string $content, int $headingLevel) : string - { - $prefix = str_repeat('#', $headingLevel); - return $prefix . ' ' . $content; - } - - -} \ No newline at end of file +} From 8c0471c30251f605cde5e32319132b2fdad929f6 Mon Sep 17 00:00:00 2001 From: Frxnklyn Date: Thu, 27 Nov 2025 10:12:22 +0100 Subject: [PATCH 09/12] FIX Docsface and NEW MarkdownPrinterMiddlewareInterface to use the new Method getDocsMarkdown --- Facades/DocsFacade.php | 45 ++- .../MarkdownPrinters/DocMarkdownPrinter.php | 316 +++++++++++++----- .../MetaObjectPrinterMiddleware.php | 33 +- .../UxonPrototypePrinterMiddleware.php | 32 +- .../MarkdownPrinterMiddlewareInterface.php | 14 + 5 files changed, 336 insertions(+), 104 deletions(-) create mode 100644 Interfaces/Facades/MarkdownPrinterMiddlewareInterface.php diff --git a/Facades/DocsFacade.php b/Facades/DocsFacade.php index c61e5611f..2f5d0141e 100644 --- a/Facades/DocsFacade.php +++ b/Facades/DocsFacade.php @@ -1,11 +1,14 @@ getDocsHandler($baseUrl, $reader); $matcher = $this->getUrlMatcher(); $handler->add(new FileRouteMiddleware($matcher, $this->getWorkbench()->filemanager()->getPathToVendorFolder(), $reader, $template)); - $response = $handler->handle($request); - return $response->getBody()->__toString(); + + $markdown = null; + + foreach ($this->getMarkdownPrinterMiddlewares($baseUrl,$reader) as $printer) { + if($printer instanceof MarkdownPrinterMiddlewareInterface) { + if(!$printer->shouldSkip($request)) { + $markdown = $printer->getMarkdown($request); + } + } + } + + if(!$markdown) { + $printer = new DocMarkdownPrinter($this->getWorkbench(), $path); + $markdown = $printer->getMarkdown(); + } + return $markdown; } protected function getUrlMatcher() : callable @@ -178,7 +195,24 @@ protected function getFileReader() : FileReaderInterface { return new MarkdownDocsReader($this->getWorkbench()); } - + + /** + * Builds and returns a list of middlewares that implement + * MarkdownPrinterMiddlewareInterface. + + * + * @return MarkdownPrinterMiddlewareInterface[] + */ + protected function getMarkdownPrinterMiddlewares(string $baseUrl, FileReaderInterface $reader) : array + { + return [ + new UxonPrototypePrinterMiddleware($this, $baseUrl, 'UXON/UXON_prototypes.md', $reader), + new MetaObjectPrinterMiddleware($this, $baseUrl, 'creating_metamodels/Available_metaobjects.md', $reader) + ]; + } + + + protected function getDocsHandler(string $baseUrl, FileReaderInterface $reader) : HttpMiddlewareBusInterface { $handler = new HttpRequestHandler(new NotFoundHandler()); @@ -187,8 +221,9 @@ protected function getDocsHandler(string $baseUrl, FileReaderInterface $reader) $urlRewriter = new AppUrlRewriterMiddleware($this); $handler->add($urlRewriter); - $handler->add(new UxonPrototypePrinterMiddleware($this, $baseUrl,'UXON/UXON_prototypes.md', $reader)); - $handler->add(new MetaObjectPrinterMiddleware($this, $baseUrl, 'creating_metamodels/Available_metaobjects.md', $reader)); + foreach ($this->getMarkdownPrinterMiddlewares($baseUrl, $reader) as $middleware) { + $handler->add($middleware); + } return $handler; } diff --git a/Facades/DocsFacade/MarkdownPrinters/DocMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/DocMarkdownPrinter.php index 72d87ba65..ba38c05f2 100644 --- a/Facades/DocsFacade/MarkdownPrinters/DocMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/DocMarkdownPrinter.php @@ -8,41 +8,87 @@ use GuzzleHttp\Psr7\Uri; use exface\Core\Interfaces\WorkbenchInterface; - +/** + * DocMarkdownPrinter loads a markdown file from an app Docs folder + * and rewrites internal links so they point to the api docs entry points. + * + * It can be constructed from an incoming request path or configured later + * via app alias and docs path. + */ class DocMarkdownPrinter { - private WorkbenchInterface $workbench; - + + /** + * Original Uri that was used to construct this printer. + */ private Uri $uri; - + + /** + * Normalized file path from the incoming request or constructor. + */ private ?string $filePath = null; - - private ?AppInterface $app = null; - - private ?string $docsPath = null; - + + private ?AppInterface $app = null; + + /** + * Path to the document inside the Docs folder of the app. + */ + private ?string $docsPath = null; + + /** + * Creates a new printer for the given workbench and optional request path. + * + * If a file path is provided it is normalized, the app alias is extracted + * from the api docs segment and the Docs sub path is derived from it. + * + * @param WorkbenchInterface $workbench + * @param string|null $filePath Optional incoming request path or url + */ public function __construct(WorkbenchInterface $workbench, string $filePath = null) { $this->workbench = $workbench; - - if($filePath){ + + if ($filePath) { $this->uri = new Uri($filePath); $this->filePath = $this->normalizePath(rawurldecode($this->uri->getPath())); $appAlias = $this->extractApp($this->filePath); $this->app = $this->workbench->getApp($appAlias); $this->docsPath = $this->extractDocPath($this->filePath); - } - } + /** + * Reads the markdown document and returns its content + * with rebased relative links for the api docs context. + * + * @return string Rewritten markdown content + */ + public function getMarkdown(): string + { + $markdown = $this->readFile($this->getAbsolutePath()); + + return $this->rebaseRelativeLinks( + $markdown, + $this->getAbsolutePath(), + $this->getDirectoryPath(), + 0 + ); + } + + /** + * Normalizes a file path to use a consistent directory separator. + * + * All slash variants are converted to the system directory separator + * and duplicate separators are collapsed. + * + * @param string $path Raw path + * @return string Normalized path + */ protected function normalizePath(string $path): string { $path = str_replace(['\/', '\\'], '/', $path); - $path = preg_replace('#/+#', '/', $path); - $path = str_replace('/', DIRECTORY_SEPARATOR, $path); $pattern = '#'.preg_quote(DIRECTORY_SEPARATOR).'+' . '#'; @@ -51,23 +97,39 @@ protected function normalizePath(string $path): string return $path; } - - + /** + * Extracts the app alias from an api docs path. + * + * Expected pattern: + * api/docs/exface/Core/Docs/... + * which is converted to lower case "exface.core". + * + * @param string $link Normalized path + * @return string Extracted app alias or empty string if none could be found + */ protected function extractApp(string $link): string { $link = $this->normalizePath($link); $ds = preg_quote(DIRECTORY_SEPARATOR, '#'); $pattern = "#api{$ds}docs{$ds}([^{$ds}]+){$ds}([^{$ds}]+){$ds}#i"; - + if (preg_match($pattern, $link, $m)) { return strtolower($m[1] . "." . $m[2]); } - + return ""; } - + /** + * Extracts the relative document path inside the Docs folder. + * + * Example: + * .../Docs/Section/file.md → Section/file.md + * + * @param string $link Normalized path + * @return string Relative Docs path or empty string + */ protected function extractDocPath(string $link): string { $link = $this->normalizePath($link); @@ -82,28 +144,59 @@ protected function extractDocPath(string $link): string return ""; } - + /** + * Sets the app alias manually and resolves the app instance + * from the workbench. + * + * @param string $appAlias App alias to use + * @return DocMarkdownPrinter Fluent interface + */ public function setAppAlias(string $appAlias): DocMarkdownPrinter { $this->app = $this->workbench->getApp($appAlias); return $this; } + /** + * Returns the resolved app or null if none is set. + * + * @return AppInterface|null Current app instance + */ public function getApp(): ?AppInterface { return $this->app; } - + + /** + * Returns the alias of the current app. + * + * @return string|null App alias or null if no app is set + */ public function getAppAlias(): ?string { return $this->app->getAlias(); } + /** + * Returns the Docs relative path prefixed with the Docs folder name. + * + * @return string|null Docs path including "Docs" prefix + */ public function getDocsPath(): ?string { - return "Docs" . DIRECTORY_SEPARATOR . $this->docsPath; + return "Docs" . DIRECTORY_SEPARATOR . $this->docsPath; } - + + /** + * Sets the Docs path from a url or path string. + * + * The given value is interpreted as Uri, normalized and then the + * Docs sub path is extracted. If no Docs segment is found the + * normalized path is used as is. + * + * @param string $docsPath Url or path to a docs file + * @return DocMarkdownPrinter Fluent interface + */ public function setDocsPath(string $docsPath): DocMarkdownPrinter { $this->uri = new Uri($docsPath); @@ -112,61 +205,52 @@ public function setDocsPath(string $docsPath): DocMarkdownPrinter $this->docsPath = $path !== "" ? $path : $docsPath; return $this; } - + + /** + * Returns the absolute file system path to the current Docs file. + * + * @return string|null Absolute path to the markdown file + */ public function getAbsolutePath(): ?string { return $this->app->getDirectoryAbsolutePath() . DIRECTORY_SEPARATOR . $this->getDocsPath(); } - + + /** + * Returns the base directory of the current app. + * + * @return string App directory path + */ public function getDirectoryPath(): string { return $this->app->getDirectory(); } - + + /** + * Checks whether a Docs folder exists in the app directory. + * + * @return bool True if the Docs folder exists + */ public function docsExists(): bool { return file_exists($this->app->getDirectoryAbsolutePath() . DIRECTORY_SEPARATOR . "Docs"); } - - - - - - - public function getMarkdown(): string - { - $markdown = null; - if(StringDataType::endsWith($this->uri->getPath(), 'UXON_prototypes.md')) { - $query = $this->uri->getQuery(); - $params = []; - parse_str($query, $params); - $selector = urldecode($params['selector']); - $printer = new UxonPrototypeMarkdownPrinter($this->workbench, $selector); - $markdown = $printer->getMarkdown(); - } - - if(StringDataType::endsWith($this->uri->getPath(), 'Available_metaobjects.md')) { - $query = $this->uri->getQuery(); - $params = []; - parse_str($query, $params); - $selector = urldecode($params['selector']); - $printer = new ObjectMarkdownPrinter($this->workbench, $selector); - $markdown = $printer->getMarkdown(); - } - - if(!$markdown) { - $markdown = $this->readFile($this->getAbsolutePath()); - } - - - return $this->rebaseRelativeLinks($markdown, $this->getAbsolutePath(), $this->getDirectoryPath(),0); - } - - - - - //functions from LinkRebaser + /** + * Builds a table of contents from a markdown document. + * + * The method parses all markdown links, emits formatted entries and, + * for each linked markdown file, loads it and calls itself recursively + * until the given depth is reached. Already processed links are skipped + * to avoid infinite recursion. + * + * @param string $content Markdown content to scan + * @param string $filePath Path to the current file + * @param string $basePath Base app docs path used in api docs links + * @param int $depth Maximum recursion depth + * @param int $currentDepth Current heading depth for nested entries + * @return string Markdown list that represents the table of contents + */ public function getTableOfContents(string $content, string $filePath, string $basePath, int $depth, int $currentDepth = 2): string { if ($depth < 0) { @@ -202,27 +286,48 @@ public function getTableOfContents(string $content, string $filePath, string $ba if ($fullPath && pathinfo($fullPath, PATHINFO_EXTENSION) === 'md') { $newContent = $this->readFile($fullPath); - $output .= $this->getTableOfContents($newContent, $fullPath, dirname($fullPath), $depth - 1, $currentDepth + 1); + $output .= $this->getTableOfContents( + $newContent, + $fullPath, + dirname($fullPath), + $depth - 1, + $currentDepth + 1 + ); } } return $output; } + /** + * Rewrites relative markdown links so they point to the api docs base path. + * + * For each markdown link or image link this method computes a new relative + * path based on the current file and the app docs base directory. + * External links, pure anchors and data urls are not changed. + * + * The method walks the content once and uses a callback to rebuild each + * markdown link in place. + * + * @param string $content Original markdown content + * @param string $filePath Path of the current file + * @param string $basePath App docs base path + * @param int $depth Unused here but kept for a compatible signature + * @param int $currentDepth Unused in the rewrite but part of the signature + * @return string Markdown content with rebased links + */ public function rebaseRelativeLinks(string $content, string $filePath, string $basePath, int $depth, int $currentDepth = 2): string { - if ($depth < 0) { return ""; } - $pattern = '/ - (?P!)? # optional ! fuer Bilder - \[(?P[^\]]*)\] # Linktext + (?P!)? # optional ! for images + \[(?P[^\]]*)\] # link text \( - \s*(?P[^)\s]+) # Ziel ohne Leerzeichen bis Klammer - (?:\s+"(?P[^"]*)")? # optionaler Title + \s*(?P<url>[^)\s]+) # target url + (?:\s+"(?P<title>[^"]*)")? # optional title \) /x'; @@ -237,22 +342,18 @@ public function rebaseRelativeLinks(string $content, string $filePath, string $b return $m[0]; } - if ($this->isExternalLink($url) || $this->isPureAnchor($url) || $this->isDataLike($url)) { return $m[0]; } - $fragment = ''; if (strpos($url, '#') !== false) { [$url, $frag] = explode('#', $url, 2); $fragment = '#'.$frag; } - $rebased = $this->getRelativePath($filePath, $url, $basePath); - if (!$rebased || $rebased === $url) { $candidate = realpath($dirOfFile . DIRECTORY_SEPARATOR . $url); if ($candidate !== false) { @@ -277,31 +378,77 @@ public function rebaseRelativeLinks(string $content, string $filePath, string $b return preg_replace_callback($pattern, $cb, $content); } + /** + * Checks whether a link is external and should not be rewritten. + * + * @param string $link Url or path from markdown + * @return bool True if the link starts with http + */ protected function isExternalLink(string $link) : bool { return str_starts_with($link, 'http'); } + /** + * Detects markdown link texts that represent keyboard shortcuts. + * + * These links are left untouched when rebasing. + * + * @param string $text Link text + * @return bool True if the text starts with a kbd tag + */ protected function isKeyboardShortcut(string $text) : bool { return str_starts_with($text, '<kbd>'); } + /** + * Checks whether the url is a pure anchor without a path. + * + * @param string $url Url or fragment + * @return bool True if it starts with a hash + */ private function isPureAnchor(string $url): bool { return $url !== '' && $url[0] === '#'; } + /** + * Checks whether the url is a data or about url that should not be changed. + * + * @param string $url Url string + * @return bool True if it looks like a data or about url + */ private function isDataLike(string $url): bool { return (bool)preg_match('#^(data:|about:)#i', $url); } + /** + * Formats a link as a markdown heading entry for the table of contents. + * + * @param string $text Link text + * @param string $link Rebases link path + * @param int $depth Heading depth used as number of hash characters + * @return string Formatted markdown line + */ protected function formatLink(string $text, string $link, int $depth) : string { return str_repeat("#", $depth) . "- " . $text . " (" . $link . ")\n"; } + /** + * Computes a relative path for a linked file inside the api docs tree. + * + * The method resolves the real path of the target file and then + * rebuilds a virtual path that starts at api/docs and points + * into the Docs folder of the app. + * + * @param string $filePath Path to the current markdown file + * @param string $linkedFile Linked relative file path + * @param string $basePath App docs base path + * @return string Rebases link path or the original linked file if Docs could not be found + */ protected function getRelativePath(string $filePath, string $linkedFile, string $basePath) : string { $normalizedPath = dirname($filePath) . DIRECTORY_SEPARATOR . $linkedFile; @@ -309,15 +456,22 @@ protected function getRelativePath(string $filePath, string $linkedFile, string $base = 'api'. DIRECTORY_SEPARATOR . 'docs' . DIRECTORY_SEPARATOR . $basePath . '\\'; return strstr($fullPath, 'Docs\\') ? $base . strstr($fullPath, 'Docs\\') : $linkedFile; } - - // from File Reader - public function readFile(string $filePath): string { + /** + * Reads the content of a markdown file from disk. + * + * If the file does not exist an error message is returned instead. + * + * @param string $filePath Absolute path to the markdown file + * @return string File content or error message + */ + public function readFile(string $filePath): string + { if (! file_exists($filePath)) { return 'ERROR: file not found!'; } $md = file_get_contents($filePath); return $md; } - + } \ No newline at end of file diff --git a/Facades/DocsFacade/Middleware/MetaObjectPrinterMiddleware.php b/Facades/DocsFacade/Middleware/MetaObjectPrinterMiddleware.php index 95450b7a2..b8d7a4d37 100644 --- a/Facades/DocsFacade/Middleware/MetaObjectPrinterMiddleware.php +++ b/Facades/DocsFacade/Middleware/MetaObjectPrinterMiddleware.php @@ -5,6 +5,7 @@ use exface\Core\Facades\DocsFacade\MarkdownContent; use exface\Core\Facades\DocsFacade\MarkdownPrinters\ObjectMarkdownPrinter; use exface\Core\Facades\DocsFacade\MarkdownPrinters\UxonPrototypeMarkdownPrinter; +use exface\Core\Interfaces\Facades\MarkdownPrinterMiddlewareInterface; use exface\Core\Interfaces\WorkbenchInterface; use GuzzleHttp\Psr7\Response; use kabachello\FileRoute\Interfaces\FileReaderInterface; @@ -25,7 +26,7 @@ * @author Andrej Kabachnik * */ -class MetaObjectPrinterMiddleware implements MiddlewareInterface +class MetaObjectPrinterMiddleware implements MarkdownPrinterMiddlewareInterface { private $workbench = null; @@ -50,16 +51,12 @@ public function __construct(HttpFacadeInterface $facade, string $baseUrl, string */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (! StringDataType::endsWith($request->getUri()->getPath(), $this->fileUrl)) { + if ($this->shouldSkip($request)) { return $handler->handle($request); } - $query = $request->getUri()->getQuery(); - $params = []; - parse_str($query, $params); - $selector = $this->normalize($params['selector']); - $printer = new ObjectMarkdownPrinter($this->getWorkbench(), $selector); - $markdown = $printer->getMarkdown(); + $markdown = $this->getMarkdown($request); + $templatePath = Filemanager::pathJoin([$this->facade->getApp()->getDirectoryAbsolutePath(), 'Facades/DocsFacade/template.html']); $template = new PlaceholderFileTemplate($templatePath, $this->baseUrl . '/' . $this->facade->buildUrlToFacade(true)); @@ -75,6 +72,25 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $response; } + public function shouldSkip(ServerRequestInterface $request): bool + { + return ! StringDataType::endsWith( + $request->getUri()->getPath(), + $this->fileUrl + ); + } + + + public function getMarkdown(ServerRequestInterface $request): string + { + $query = $request->getUri()->getQuery(); + $params = []; + parse_str($query, $params); + $selector = $this->normalize($params['selector']); + $printer = new ObjectMarkdownPrinter($this->getWorkbench(), $selector); + return $printer->getMarkdown(); + } + /** * Normalizes a raw link selector by extracting the object ID or alias. * @@ -102,7 +118,6 @@ protected function normalize(string $raw): string } - protected function getWorkbench(): WorkbenchInterface { return $this->facade->getWorkbench(); diff --git a/Facades/DocsFacade/Middleware/UxonPrototypePrinterMiddleware.php b/Facades/DocsFacade/Middleware/UxonPrototypePrinterMiddleware.php index 808f19eb3..9cb0e1d40 100644 --- a/Facades/DocsFacade/Middleware/UxonPrototypePrinterMiddleware.php +++ b/Facades/DocsFacade/Middleware/UxonPrototypePrinterMiddleware.php @@ -4,6 +4,7 @@ use exface\Core\CommonLogic\Filemanager; use exface\Core\Facades\DocsFacade\MarkdownContent; use exface\Core\Facades\DocsFacade\MarkdownPrinters\UxonPrototypeMarkdownPrinter; +use exface\Core\Interfaces\Facades\MarkdownPrinterMiddlewareInterface; use exface\Core\Interfaces\WorkbenchInterface; use GuzzleHttp\Psr7\Response; use kabachello\FileRoute\Interfaces\FileReaderInterface; @@ -24,7 +25,7 @@ * @author Andrej Kabachnik * */ -class UxonPrototypePrinterMiddleware implements MiddlewareInterface +class UxonPrototypePrinterMiddleware implements MarkdownPrinterMiddlewareInterface { private $workbench = null; @@ -49,16 +50,11 @@ public function __construct(HttpFacadeInterface $facade, string $baseUrl, string */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (! StringDataType::endsWith($request->getUri()->getPath(), $this->fileUrl)) { - return $handler->handle($request); + if ($this->shouldSkip($request)) { + return $handler->handle($request); } - $query = $request->getUri()->getQuery(); - $params = []; - parse_str($query, $params); - $selector = urldecode($params['selector']); - $printer = new UxonPrototypeMarkdownPrinter($this->getWorkbench(), $selector); - $markdown = $printer->getMarkdown(); + $markdown = $this->getMarkdown($request); $templatePath = Filemanager::pathJoin([$this->facade->getApp()->getDirectoryAbsolutePath(), 'Facades/DocsFacade/template.html']); $template = new PlaceholderFileTemplate($templatePath, $this->baseUrl . '/' . $this->facade->buildUrlToFacade(true)); @@ -74,6 +70,24 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $response; } + public function getMarkdown(ServerRequestInterface $request) : string + { + $query = $request->getUri()->getQuery(); + $params = []; + parse_str($query, $params); + $selector = urldecode($params['selector']); + $printer = new UxonPrototypeMarkdownPrinter($this->getWorkbench(), $selector); + return $printer->getMarkdown(); + } + + public function shouldSkip(ServerRequestInterface $request): bool + { + return ! StringDataType::endsWith( + $request->getUri()->getPath(), + $this->fileUrl + ); + } + protected function getWorkbench(): WorkbenchInterface { return $this->facade->getWorkbench(); diff --git a/Interfaces/Facades/MarkdownPrinterMiddlewareInterface.php b/Interfaces/Facades/MarkdownPrinterMiddlewareInterface.php new file mode 100644 index 000000000..651dab9c9 --- /dev/null +++ b/Interfaces/Facades/MarkdownPrinterMiddlewareInterface.php @@ -0,0 +1,14 @@ +<?php + +namespace exface\Core\Interfaces\Facades; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; + +interface MarkdownPrinterMiddlewareInterface extends MiddlewareInterface +{ + public function getMarkdown(ServerRequestInterface $request) : string; + + + public function shouldSkip(ServerRequestInterface $request) : bool; +} \ No newline at end of file From 766bc7fe9a125c7bcaa5471f9b45552731ab5e08 Mon Sep 17 00:00:00 2001 From: Frxnklyn <brooklynfraenzschky@gmail.com> Date: Tue, 2 Dec 2025 11:34:21 +0100 Subject: [PATCH 10/12] NEW CodeMarkdownPrinter --- .../MarkdownPrinters/CodeMarkdownPrinter.php | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 Facades/DocsFacade/MarkdownPrinters/CodeMarkdownPrinter.php diff --git a/Facades/DocsFacade/MarkdownPrinters/CodeMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/CodeMarkdownPrinter.php new file mode 100644 index 000000000..83c579700 --- /dev/null +++ b/Facades/DocsFacade/MarkdownPrinters/CodeMarkdownPrinter.php @@ -0,0 +1,170 @@ +<?php + +namespace exface\Core\Facades\DocsFacade\MarkdownPrinters; + +use exface\Core\DataTypes\StringDataType; +use exface\Core\Interfaces\AppInterface; +use GuzzleHttp\Psr7\Uri; +use exface\Core\Interfaces\WorkbenchInterface; + +/** + * CodeMarkdownPrinter loads a markdown file from an php file + * + * Expected request pattern: + * api/docs/Vendor/App/Some/Path/File.php + * + * The path part after Vendor and App is used directly as relative file path + * inside the app directory. + */ +class CodeMarkdownPrinter +{ + private WorkbenchInterface $workbench; + + private ?AppInterface $app = null; + + private ?string $docsPath = null; + + public function __construct(WorkbenchInterface $workbench, string $filePath = null) + { + $this->workbench = $workbench; + + if ($filePath) { + + $normalizedPath = $this->normalizePath(rawurldecode($filePath)); + + [$appAlias, $relativePath] = $this->extractAppAndRelativePath($normalizedPath); + + if ($appAlias !== '') { + $this->app = $this->workbench->getApp($appAlias); + } + + if ($relativePath !== '') { + $this->docsPath = $relativePath; + } + } + } + + /** + * Reads the markdown document and returns its content. + * * + * @return string Markdown content + */ + public function getMarkdown(): string + { + $path = $this->getAbsolutePath(); + if(!$path){ + return 'ERROR: file not found!'; + } + if(!StringDataType::endsWith($path, '.php')) { + return "ERROR: This path does not refer to a PHP file. Therefore, the file could not be read."; + } + + return $this->rebasePaths($this->readFile($path)); + } + + /** + * Normalizes a file path to use a consistent directory separator. + * + * All slash variants are converted to the system directory separator + * and duplicate separators are collapsed. + * + * @param string $path Raw path + * @return string Normalized path + */ + protected function normalizePath(string $path): string + { + $path = str_replace(['\/', '\\'], '/', $path); + $path = preg_replace('#/+#', '/', $path); + $path = str_replace('/', DIRECTORY_SEPARATOR, $path); + + $pattern = '#'.preg_quote(DIRECTORY_SEPARATOR).'+' . '#'; + $path = preg_replace($pattern, DIRECTORY_SEPARATOR, $path); + + return $path; + } + + /** + * Extracts app alias and relative path from an api docs path. + * + * Expected pattern: + * api/docs/Vendor/App/some/path/file.php + * becomes: + * alias: "vendor.app" + * relativePath: "some/path/file.php" + * + * @param string $link Normalized path + * @return array{0:string,1:string} [appAlias, relativePath] + */ + protected function extractAppAndRelativePath(string $link): array + { + $link = $this->normalizePath($link); + + $ds = preg_quote(DIRECTORY_SEPARATOR, '#'); + $pattern = "#api{$ds}docs{$ds}([^{$ds}]+){$ds}([^{$ds}]+){$ds}(.+)$#i"; + + if (preg_match($pattern, $link, $m)) { + $appAlias = strtolower($m[1] . "." . $m[2]); + $relativePath = $m[3]; + return [$appAlias, $relativePath]; + } + + return ['', '']; + } + + /** + * Returns the absolute file system path to the current markdown file. + * + * @return string Absolute path to the markdown file + */ + public function getAbsolutePath(): ?string + { + if (! $this->app || ! $this->docsPath) { + return null; + } + + return $this->app->getDirectoryAbsolutePath() + . DIRECTORY_SEPARATOR + . $this->docsPath; + } + + + + /** + * Reads the content of a markdown file from disk. + * + * If the file does not exist an error message is returned instead. + * + * @param string $filePath Absolute path to the markdown file + * @return string File content or error message + */ + protected function readFile(string $filePath): string + { + if (! file_exists($filePath)) { + return 'ERROR: file not found!'; + } + + $md = file_get_contents($filePath); + return $md === false ? '' : $md; + } + + /** + * Rewrites specific PHP use statements by inserting the api docs prefix. + * + * The raw input contains PHP code as a string. All matching use statements + * beginning with "use exface\" are rewritten so they begin with + * "use api\docs\exface\" instead. Other lines remain unchanged. + */ + protected function rebasePaths(string $raw) : string + { + return preg_replace( + '/^use\s+exface\\\\(.*);/m', + 'use api\\docs\\exface\\\\$1;', + $raw + ); + } + + + + + +} \ No newline at end of file From 98598ec7e1d001eefaabbc21ed456bc041c3e330 Mon Sep 17 00:00:00 2001 From: Frxnklyn <brooklynfraenzschky@gmail.com> Date: Fri, 5 Dec 2025 10:30:34 +0100 Subject: [PATCH 11/12] FIX AbstractMarkdownPrinterMiddleware change shouldskip from protectded to public --- .../DocsFacade/Middleware/AbstractMarkdownPrinterMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Facades/DocsFacade/Middleware/AbstractMarkdownPrinterMiddleware.php b/Facades/DocsFacade/Middleware/AbstractMarkdownPrinterMiddleware.php index 574156f34..e4c0db16a 100644 --- a/Facades/DocsFacade/Middleware/AbstractMarkdownPrinterMiddleware.php +++ b/Facades/DocsFacade/Middleware/AbstractMarkdownPrinterMiddleware.php @@ -84,7 +84,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $response; } - protected function shouldSkip(ServerRequestInterface $request): bool + public function shouldSkip(ServerRequestInterface $request): bool { return ! StringDataType::endsWith( $request->getUri()->getPath(), From 496d6e77748f7f88afbb2d80b6bdc76047f7aff1 Mon Sep 17 00:00:00 2001 From: Frxnklyn <brooklynfraenzschky@gmail.com> Date: Mon, 26 Jan 2026 09:55:47 +0100 Subject: [PATCH 12/12] DEV ObjectMarkdownPrinter now with Actions & new ActionMarkdownPrinter --- .../ActionMarkdownPrinter.php | 82 +++++++++++++++++++ .../ObjectMarkdownPrinter.php | 25 +++++- 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 Facades/DocsFacade/MarkdownPrinters/ActionMarkdownPrinter.php diff --git a/Facades/DocsFacade/MarkdownPrinters/ActionMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/ActionMarkdownPrinter.php new file mode 100644 index 000000000..d8d6814f2 --- /dev/null +++ b/Facades/DocsFacade/MarkdownPrinters/ActionMarkdownPrinter.php @@ -0,0 +1,82 @@ +<?php + +namespace exface\Core\Facades\DocsFacade\MarkdownPrinters; + + +use exface\Core\CommonLogic\UxonObject; +use exface\Core\DataTypes\MarkdownDataType; +use exface\Core\DataTypes\PhpClassDataType; +use exface\Core\DataTypes\RelationTypeDataType; +use exface\Core\Exceptions\Model\MetaRelationBrokenError; +use exface\Core\Facades\DocsFacade; +use exface\Core\Factories\MetaObjectFactory; +use exface\Core\Factories\QueryBuilderFactory; +use exface\Core\Interfaces\Actions\ActionInterface; +use exface\Core\Interfaces\Model\BehaviorInterface; +use exface\Core\Interfaces\Model\MetaAttributeInterface; +use exface\Core\Interfaces\Model\MetaAttributeListInterface; +use exface\Core\Interfaces\Model\MetaObjectInterface; +use exface\Core\Interfaces\Model\MetaRelationInterface; +use exface\Core\Interfaces\WorkbenchInterface; + +/** + * Builds a Markdown documentation view for a meta object and its related objects. + * + * The printer renders a table of attributes for the given meta object and can + * optionally walk through relation attributes to print child objects up to a + * configurable depth. + */ +class ActionMarkdownPrinter //implements MarkdownPrinterInterface +{ + protected WorkbenchInterface $workbench; + + private ActionInterface $action; + private int $headingLevel = 1; + + /** + * Maximum depth of recursive relation traversal. + * + * Depth 0 would print only the root object, higher values include related objects. + */ + private int $relationDepth = 0; + private ?string $relationType = RelationTypeDataType::REGULAR; + + + + /** + * Creates a new object markdown printer for the given action. + * + */ + public function __construct(WorkbenchInterface $workbench, ActionInterface $action, int $headingLevel = 1) + { + $this->workbench = $workbench; + $this->action = $action; + $this->headingLevel = $headingLevel; + } + + /** + * Builds and returns the complete Markdown for the current action + */ + public function getMarkdown(): string + { + $heading = MarkdownDataType::buildMarkdownHeader($this->action->getName(), $this->headingLevel); + + $uxon = $this->action->exportUxonObject(); + + $uxon = $uxon->toJson(); + //Uxon + + //TODO Einbauen Uxon im Codeblock, mögliche Description + + return <<<MD +{$heading} + +Uxon: +´´´ +{$uxon} +´´´ +MD; + } + + +} \ No newline at end of file diff --git a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php index 303135e7c..a913fd3ff 100644 --- a/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php +++ b/Facades/DocsFacade/MarkdownPrinters/ObjectMarkdownPrinter.php @@ -11,12 +11,14 @@ use exface\Core\Facades\DocsFacade; use exface\Core\Factories\MetaObjectFactory; use exface\Core\Factories\QueryBuilderFactory; +use exface\Core\Interfaces\Actions\ActionInterface; use exface\Core\Interfaces\Model\BehaviorInterface; use exface\Core\Interfaces\Model\MetaAttributeInterface; use exface\Core\Interfaces\Model\MetaAttributeListInterface; use exface\Core\Interfaces\Model\MetaObjectInterface; use exface\Core\Interfaces\Model\MetaRelationInterface; use exface\Core\Interfaces\WorkbenchInterface; +use Respect\Validation\Rules\Length; /** * Builds a Markdown documentation view for a meta object and its related objects. @@ -87,7 +89,10 @@ public function getMarkdown(): string $importantAttributes = trim($importantAttributes); $attributesHeading = MarkdownDataType::buildMarkdownHeader("Attributes of \"{$metaObject->getName()}\"", $headingLevel + 1); - + + $actionHeading = MarkdownDataType::buildMarkdownHeader("Actions of \"{$metaObject->getName()}\"", $headingLevel + 1); + + $markdown = <<<MD {$heading} @@ -104,6 +109,8 @@ public function getMarkdown(): string {$this->buildMdAttributesSections($metaObject, $headingLevel+2)} +{$this->buildMdActionSection($actionHeading, $metaObject, $headingLevel+2 )} + {$this->buildMdBehaviorsSections($metaObject, 'Behaviors of "' . $metaObject->getName() . '"', $headingLevel+1)} {$this->buildMdRelatedObjects($metaObject->getRelations(), 'Related objects', $headingLevel)} @@ -172,6 +179,22 @@ protected function buildMarkDownAttributeSection(MetaAttributeInterface $attr, i {$this->buildMdUxonCodeblock($attr->getCustomDataTypeUxon(), 'Configuration of data type [' . $dataType->getAliasWithNamespace() . '](' . $dataTypeLink . '):')} +MD; + + } + + protected function buildMdActionSection(string $header, MetaObjectInterface $obj, int $headingLevel = 3) : string + { + + $markdown = ''; + foreach ($obj->getActions() as $act) { + $actionPrinter = new ActionMarkdownPrinter($this->workbench, $act, $headingLevel); + $markdown .= $actionPrinter->getMarkdown(); + } + return <<<MD +{$header} + +{$markdown} MD; }