diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index 2e77204b9..60c0c7130 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -17,6 +17,7 @@ jobs: uses: "phpDocumentor/.github/.github/workflows/code-coverage.yml@v0.8" with: php-version: "8.2" + php-extensions: "none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, fileinfo, iconv" coding-standards: name: "Coding Standards" @@ -29,6 +30,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" + extensions: "none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, fileinfo, iconv" php-version: "8.2" tools: "cs2pr" @@ -45,6 +47,7 @@ jobs: uses: "phpDocumentor/.github/.github/workflows/lint.yml@main" with: composer-options: "--no-check-publish --ansi" + php-extensions: "none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, fileinfo, iconv" php-version: "8.2" static-analysis: @@ -52,6 +55,7 @@ jobs: uses: "phpDocumentor/.github/.github/workflows/static-analysis.yml@v0.8" with: php-version: "8.2" + php-extensions: "none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, fileinfo, iconv" composer-root-version: "2.x-dev" architecture: @@ -64,6 +68,7 @@ jobs: with: coverage: "none" php-version: "8.2" + extensions: "none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, fileinfo, iconv" - name: "Install dependencies with Composer" uses: "ramsey/composer-install@v3" @@ -79,6 +84,7 @@ jobs: uses: "phpDocumentor/.github/.github/workflows/continuous-integration.yml@v0.8" with: composer-root-version: "2.x-dev" + php-extensions: "none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, fileinfo, iconv" functional-tests: name: "Functional test" @@ -86,6 +92,7 @@ jobs: needs: "unit-tests" with: test-suite: "functional" + php-extensions: "none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, fileinfo, iconv" composer-root-version: "2.x-dev" integration-tests: @@ -94,6 +101,7 @@ jobs: needs: "unit-tests" with: test-suite: "integration" + php-extensions: "none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, fileinfo, iconv" composer-root-version: "2.x-dev" xml-lint: diff --git a/composer.json b/composer.json index cdc1e0db8..729458de4 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ }, "require": { "php": "^8.1", + "ext-iconv": "*", "ext-json": "*", "ext-mbstring": "*", "doctrine/deprecations": "^1.1", diff --git a/composer.lock b/composer.lock index a98c5cbbe..244711238 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "420872b359a64babd842e98ef1572ec4", + "content-hash": "af22dade10c460040d600f3e32132c34", "packages": [ { "name": "dflydev/dot-access-data", @@ -1597,9 +1597,10 @@ "dist": { "type": "path", "url": "./packages/guides-theme-rst", - "reference": "70ef007a53221f1c725d02d6cc71adb4e7648177" + "reference": "2e5308d46fd6d65e1ea788c7c4a872d2d5008de0" }, "require": { + "ext-iconv": "*", "php": "^8.1", "phpdocumentor/guides": "^1.0 || ^2.0" }, @@ -7197,6 +7198,7 @@ "prefer-lowest": false, "platform": { "php": "^8.1", + "ext-iconv": "*", "ext-json": "*", "ext-mbstring": "*" }, diff --git a/packages/guides-theme-rst/composer.json b/packages/guides-theme-rst/composer.json index 7a3681a80..add642c53 100644 --- a/packages/guides-theme-rst/composer.json +++ b/packages/guides-theme-rst/composer.json @@ -22,6 +22,7 @@ "minimum-stability": "stable", "require": { "php": "^8.1", + "ext-iconv": "*", "phpdocumentor/guides": "^1.0 || ^2.0" }, "extra": { diff --git a/packages/guides-theme-rst/resources/config/guides-theme-rst.php b/packages/guides-theme-rst/resources/config/guides-theme-rst.php index 34b3fe095..291b4f635 100644 --- a/packages/guides-theme-rst/resources/config/guides-theme-rst.php +++ b/packages/guides-theme-rst/resources/config/guides-theme-rst.php @@ -6,6 +6,8 @@ use phpDocumentor\Guides\RstTheme\Twig\RstExtension; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; + return static function (ContainerConfigurator $container): void { $container->services() ->defaults() @@ -29,6 +31,7 @@ ) ->set(RstExtension::class) + ->arg('$nodeRenderer', service('phpdoc.guides.output_node_renderer')) ->tag('twig.extension') ->autowire(); }; diff --git a/packages/guides-theme-rst/resources/template/rst/body/table.rst.twig b/packages/guides-theme-rst/resources/template/rst/body/table.rst.twig new file mode 100644 index 000000000..a794551ae --- /dev/null +++ b/packages/guides-theme-rst/resources/template/rst/body/table.rst.twig @@ -0,0 +1,2 @@ + +{{ renderRstTable(node) | raw }} diff --git a/packages/guides-theme-rst/resources/template/rst/template.php b/packages/guides-theme-rst/resources/template/rst/template.php index 4b150dd8a..fab6307ac 100644 --- a/packages/guides-theme-rst/resources/template/rst/template.php +++ b/packages/guides-theme-rst/resources/template/rst/template.php @@ -51,6 +51,7 @@ use phpDocumentor\Guides\Nodes\QuoteNode; use phpDocumentor\Guides\Nodes\SectionNode; use phpDocumentor\Guides\Nodes\SeparatorNode; +use phpDocumentor\Guides\Nodes\TableNode; use phpDocumentor\Guides\Nodes\TitleNode; return [ @@ -74,6 +75,7 @@ CitationNode::class => 'body/citation.rst.twig', FootnoteNode::class => 'body/footnote.rst.twig', AnnotationListNode::class => 'body/annotation-list.rst.twig', + TableNode::class => 'body/table.rst.twig', // Inline ImageInlineNode::class => 'inline/image.rst.twig', AbbreviationInlineNode::class => 'inline/textroles/abbreviation.rst.twig', diff --git a/packages/guides-theme-rst/src/RstTheme/Twig/RstExtension.php b/packages/guides-theme-rst/src/RstTheme/Twig/RstExtension.php index 9443b82cc..c0b697f29 100644 --- a/packages/guides-theme-rst/src/RstTheme/Twig/RstExtension.php +++ b/packages/guides-theme-rst/src/RstTheme/Twig/RstExtension.php @@ -13,7 +13,12 @@ namespace phpDocumentor\Guides\RstTheme\Twig; +use phpDocumentor\Guides\NodeRenderers\NodeRenderer; +use phpDocumentor\Guides\Nodes\Table\TableColumn; +use phpDocumentor\Guides\Nodes\Table\TableRow; +use phpDocumentor\Guides\Nodes\TableNode; use phpDocumentor\Guides\Nodes\TitleNode; +use phpDocumentor\Guides\RenderContext; use phpDocumentor\Guides\RstTheme\Configuration\HeaderSyntax; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; @@ -22,6 +27,9 @@ use function array_map; use function explode; use function implode; +use function max; +use function mb_str_pad; +use function mb_strlen; use function min; use function preg_replace; use function rtrim; @@ -30,11 +38,17 @@ final class RstExtension extends AbstractExtension { + public function __construct( + private NodeRenderer $nodeRenderer, + ) { + } + /** @return TwigFunction[] */ public function getFunctions(): array { return [ new TwigFunction('renderRstTitle', $this->renderRstTitle(...), ['is_safe' => ['rst'], 'needs_context' => false]), + new TwigFunction('renderRstTable', $this->renderRstTable(...), ['is_safe' => ['rst'], 'needs_context' => true]), new TwigFunction('renderRstIndent', $this->renderRstIndent(...), ['is_safe' => ['rst'], 'needs_context' => false]), ]; } @@ -75,6 +89,75 @@ public function renderRstTitle(TitleNode $node, string $content): string $ret .= $content . "\n" . str_repeat($headerSyntax->delimiter(), strlen($content)); + return $ret . "\n"; + } + + /** @param array{env: RenderContext} $context */ + public function renderRstTable(array $context, TableNode $node): string + { + $columnWidths = []; + + $this->determineMaxLenght($node->getHeaders(), $context['env'], $columnWidths); + $this->determineMaxLenght($node->getData(), $context['env'], $columnWidths); + + $ret = $this->renderTableRowEnd($columnWidths); + $ret .= $this->renderRows($node->getHeaders(), $context['env'], $columnWidths, '='); + $ret .= $this->renderRows($node->getData(), $context['env'], $columnWidths); + + return $ret . "\n"; + } + + private function renderCellContent(RenderContext $env, TableColumn $column): string + { + return implode('', array_map(fn ($node) => $this->nodeRenderer->render($node, $env), $column->getValue())); + } + + /** + * @param TableRow[] $rows + * @param int[] &$columnWidths + */ + private function determineMaxLenght(array $rows, RenderContext $env, array &$columnWidths): void + { + foreach ($rows as $row) { + foreach ($row->getColumns() as $index => $column) { + $content = $this->renderCellContent($env, $column); + + $columnWidths[$index] = max(mb_strlen($content) + 2, $columnWidths[$index] ?? 0); + } + } + } + + /** + * @param TableRow[] $rows + * @param int[] $columnWidths + */ + private function renderRows(array $rows, RenderContext $env, array $columnWidths, string $separator = '-'): string + { + $ret = ''; + foreach ($rows as $row) { + $ret .= '|'; + foreach ($row->getColumns() as $index => $column) { + $content = $this->renderCellContent($env, $column); + + $ret .= ' ' . mb_str_pad($content, $columnWidths[$index] - 2) . ' |'; + } + + $ret .= "\n" . $this->renderTableRowEnd($columnWidths, $separator); + } + + return $ret; + } + + /** @param int[] $columnWidths */ + private function renderTableRowEnd(array $columnWidths, string $char = '-'): string + { + $ret = ''; + foreach ($columnWidths as $width) { + $ret .= '+' . str_repeat($char, $width); + } + + $ret .= '+' . "\n"; + return $ret; } } diff --git a/tests/Integration/tests-full/md-to-rst/table-md-to-rst/expected/index.rst b/tests/Integration/tests-full/md-to-rst/table-md-to-rst/expected/index.rst new file mode 100644 index 000000000..1ea6d02e3 --- /dev/null +++ b/tests/Integration/tests-full/md-to-rst/table-md-to-rst/expected/index.rst @@ -0,0 +1,56 @@ +=============== +Markdown Tables +=============== + +Simple Table +============ + ++------------+-----+---------------+ +| Name | Age | City | ++============+=====+===============+ +| John Doe | 29 | New York | ++------------+-----+---------------+ +| Jane Smith | 34 | San Francisco | ++------------+-----+---------------+ +| Sam Green | 22 | Boston | ++------------+-----+---------------+ + +Table 1 +======= + ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| Method name | Description | Parameters | Default | ++==============================================+===================================================================================================================================================================+=======================================================+============================================================================================+ +| `setIcon` | icon file, or existing icon identifier | `string $icon` | `'EXT:container/Resources/Public/Icons/Extension.svg'` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| `setBackendTemplate` | Template for backend view | `string $backendTemplate` | `null'` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| `setGridTemplate` | Template for grid | `string $gridTemplate` | `'EXT:container/Resources/Private/Templates/Container.html'` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| `setGridPartialPaths` / `addGridPartialPath` | Partial root paths for grid | `array $gridPartialPaths` / `string $gridPartialPath` | `['EXT:backend/Resources/Private/Partials/', 'EXT:container/Resources/Private/Partials/']` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| `setGridLayoutPaths` | Layout root paths for grid | `array $gridLayoutPaths` | `[]` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| `setSaveAndCloseInNewContentElementWizard` | saveAndClose for new content element wizard | `bool $saveAndCloseInNewContentElementWizard` | `true` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| `setRegisterInNewContentElementWizard` | register in new content element wizard | `bool $registerInNewContentElementWizard` | `true` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| `setGroup` | Custom Group (used as optgroup for CType select, and as tab in New Content Element Wizard). If empty "container" is used as tab and no optgroup in CType is used. | `string $group` | `'container'` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +| `setDefaultValues` | Default values for the newContentElement.wizardItems | `array $defaultValues` | `[]` | ++----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------+--------------------------------------------------------------------------------------------+ + +Table 2 +======= + ++-----------------------------+---------------------------------------------------------------------------------------------------------+------------------------------------------------------------+-----------+ +| Option | Description | Default | Parameter | ++=============================+=========================================================================================================+============================================================+===========+ +| `contentId` | id of container to to process | current uid of content element `$cObj->data['uid']` | `?int` | ++-----------------------------+---------------------------------------------------------------------------------------------------------+------------------------------------------------------------+-----------+ +| `colPos` | colPos of children to to process | empty, all children are processed (as `children_`) | `?int` | ++-----------------------------+---------------------------------------------------------------------------------------------------------+------------------------------------------------------------+-----------+ +| `as` | variable to use for proceesedData (only if `colPos` is set) | `children` | `?string` | ++-----------------------------+---------------------------------------------------------------------------------------------------------+------------------------------------------------------------+-----------+ +| `skipRenderingChildContent` | do not call `ContentObjectRenderer->render()` for children, (`renderedContent` in child will not exist) | empty | `?int` | ++-----------------------------+---------------------------------------------------------------------------------------------------------+------------------------------------------------------------+-----------+ diff --git a/tests/Integration/tests-full/md-to-rst/table-md-to-rst/input/guides.xml b/tests/Integration/tests-full/md-to-rst/table-md-to-rst/input/guides.xml new file mode 100644 index 000000000..a4d11241d --- /dev/null +++ b/tests/Integration/tests-full/md-to-rst/table-md-to-rst/input/guides.xml @@ -0,0 +1,11 @@ + + + + + rst + diff --git a/tests/Integration/tests-full/md-to-rst/table-md-to-rst/input/index.md b/tests/Integration/tests-full/md-to-rst/table-md-to-rst/input/index.md new file mode 100644 index 000000000..d52650a3a --- /dev/null +++ b/tests/Integration/tests-full/md-to-rst/table-md-to-rst/input/index.md @@ -0,0 +1,32 @@ +# Markdown Tables + +## Simple Table + +| Name | Age | City | +|------------|-----|--------------| +| John Doe | 29 | New York | +| Jane Smith | 34 | San Francisco| +| Sam Green | 22 | Boston | + +## Table 1 + +| Method name | Description | Parameters | Default | +| ----------- | ----------- | ---------- | ---------- | +| `setIcon` | icon file, or existing icon identifier | `string $icon` | `'EXT:container/Resources/Public/Icons/Extension.svg'` | +| `setBackendTemplate` | Template for backend view| `string $backendTemplate` | `null'` | +| `setGridTemplate` | Template for grid | `string $gridTemplate` | `'EXT:container/Resources/Private/Templates/Container.html'` | +| `setGridPartialPaths` / `addGridPartialPath` | Partial root paths for grid | `array $gridPartialPaths` / `string $gridPartialPath` | `['EXT:backend/Resources/Private/Partials/', 'EXT:container/Resources/Private/Partials/']` | +| `setGridLayoutPaths` | Layout root paths for grid | `array $gridLayoutPaths` | `[]` | +| `setSaveAndCloseInNewContentElementWizard` | saveAndClose for new content element wizard | `bool $saveAndCloseInNewContentElementWizard` | `true` | +| `setRegisterInNewContentElementWizard` |register in new content element wizard | `bool $registerInNewContentElementWizard` | `true` | +| `setGroup` | Custom Group (used as optgroup for CType select, and as tab in New Content Element Wizard). If empty "container" is used as tab and no optgroup in CType is used. | `string $group` | `'container'` | +| `setDefaultValues` | Default values for the newContentElement.wizardItems | `array $defaultValues` | `[]` | + +## Table 2 + +| Option | Description | Default | Parameter | +|-----------------------------|------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------|-------------| +| `contentId` | id of container to to process | current uid of content element ``$cObj->data['uid']`` | ``?int`` | +| `colPos` | colPos of children to to process | empty, all children are processed (as ``children_``) | ``?int`` | +| `as` | variable to use for proceesedData (only if ``colPos`` is set) | ``children`` | ``?string`` | +| `skipRenderingChildContent` | do not call ``ContentObjectRenderer->render()`` for children, (``renderedContent`` in child will not exist) | empty | ``?int`` |