diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 09fc97f9..74474069 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,5 +14,5 @@ to this repository directly but always to the mono-repository linked above. # Make changes (create pull requests) -See the `Contribution chapter `__ in the -`Documentation` `__. +See the `Contribution chapter `__ in the +`Documentation` `__. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 799a7918..63feae67 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -19,5 +19,5 @@ Create Issues Make changes (create pull requests) =================================== -See the `Contribution chapter `__ in the -`Documentation` `__. +See the `Contribution chapter `__ in the +`Documentation` `__. diff --git a/README.rst b/README.rst index 53603bf7..3f2d4332 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,13 @@ -.. image:: http://poser.pugx.org/phpdocumentor/guides/require/php +.. image:: https://poser.pugx.org/phpdocumentor/guides/require/php :alt: PHP Version Require :target: https://packagist.org/packages/phpdocumentor/guides -.. image:: http://poser.pugx.org/phpdocumentor/guides/v/unstable +.. image:: https://poser.pugx.org/phpdocumentor/guides/v/stable + :alt: Latest Stable Version + :target: https://packagist.org/packages/phpdocumentor/guides + +.. image:: https://poser.pugx.org/phpdocumentor/guides/v/unstable :alt: Latest Unstable Version :target: https://packagist.org/packages/phpdocumentor/guides @@ -27,7 +31,7 @@ The package `phpdocumentor/guides set(InternalMenuEntryNodeTransformer::class) ->tag('phpdoc.guides.compiler.nodeTransformers') + ->set(RawNodeEscapeTransformer::class) + ->arg('$escapeRawNodes', param('phpdoc.guides.raw_node.escape')) + ->arg('$htmlSanitizerConfig', service('phpdoc.guides.raw_node.sanitizer.default')) ->set(AbsoluteUrlGenerator::class) ->set(RelativeUrlGenerator::class) @@ -143,6 +152,8 @@ ->set(AnchorReferenceResolver::class) + ->set(TitleReferenceResolver::class) + ->set(InternalReferenceResolver::class) ->set(DocReferenceResolver::class) @@ -191,6 +202,8 @@ ->set(ReferenceResolverPreRender::class) ->tag('phpdoc.guides.prerenderer') + ->set(ImageReferenceResolverPreRender::class) + ->tag('phpdoc.guides.prerenderer') ->set(InMemoryRendererFactory::class) ->arg('$renderSets', tagged_iterator('phpdoc.renderer.typerenderer', 'format')) @@ -207,6 +220,11 @@ ->tag('twig.extension') ->autowire() + ->set(GlobalMenuExtension::class) + ->arg('$nodeRenderer', service('phpdoc.guides.output_node_renderer')) + ->tag('twig.extension') + ->autowire() + ->set(ThemeManager::class) ->arg('$filesystemLoader', service(FilesystemLoader::class)) ->arg( @@ -214,11 +232,12 @@ param('phpdoc.guides.base_template_paths'), ) - ->set(FilesystemLoader::class) + ->set(TrimFilesystemLoader::class) ->arg( '$paths', param('phpdoc.guides.base_template_paths'), ) + ->alias(FilesystemLoader::class, TrimFilesystemLoader::class) ->set(LoadSettingsFromComposer::class) ->tag('event_listener', ['event' => PostProjectNodeCreated::class]) @@ -230,5 +249,8 @@ ->arg('$themeManager', service(ThemeManager::class)) ->set(TemplateRenderer::class, TwigTemplateRenderer::class) - ->arg('$environmentBuilder', new Reference(EnvironmentBuilder::class)); + ->arg('$environmentBuilder', new Reference(EnvironmentBuilder::class)) + + ->set('phpdoc.guides.raw_node.sanitizer.default', HtmlSanitizerConfig::class) + ->call('allowSafeElements', [], true); }; diff --git a/resources/template/html/body/admonition.html.twig b/resources/template/html/body/admonition.html.twig index f1865a14..3efc77f6 100644 --- a/resources/template/html/body/admonition.html.twig +++ b/resources/template/html/body/admonition.html.twig @@ -1,3 +1,4 @@ +
{% if title and isTitled %}

{{ renderNode(title) }}

{% endif %} {% if title and not isTitled %}

{{ renderNode(title) }}

{% endif %} diff --git a/resources/template/html/body/code.html.twig b/resources/template/html/body/code.html.twig index 31c1665d..c5efcaa7 100644 --- a/resources/template/html/body/code.html.twig +++ b/resources/template/html/body/code.html.twig @@ -12,4 +12,4 @@ {%- if node.emphasizeLines %} data-emphasize-lines="{{ node.emphasizeLines }}"{% endif -%}> {%- include "body/code/highlighted-code.html.twig" -%} -{%- endif -%} +{%~ endif -%} diff --git a/resources/template/html/body/container.html.twig b/resources/template/html/body/container.html.twig index 1a94629f..4cf5df71 100644 --- a/resources/template/html/body/container.html.twig +++ b/resources/template/html/body/container.html.twig @@ -1,3 +1,4 @@
{{ renderNode(node) }}
+{# force a new line at the end of the file #} diff --git a/resources/template/html/body/definition-list.html.twig b/resources/template/html/body/definition-list.html.twig index 0279042d..d6dcd4e0 100644 --- a/resources/template/html/body/definition-list.html.twig +++ b/resources/template/html/body/definition-list.html.twig @@ -16,7 +16,7 @@ {% if definitionListTerm.children %} {%- for definition in definitionListTerm.children -%}
{{ renderNode(definition) }}
- {%- endfor -%} + {%- endfor ~%} {% endif %} {% endfor %} diff --git a/resources/template/html/body/image.html.twig b/resources/template/html/body/image.html.twig index 9bc9eeb0..9564e81c 100644 --- a/resources/template/html/body/image.html.twig +++ b/resources/template/html/body/image.html.twig @@ -1,3 +1,4 @@ +{% if node.target %}{% endif %} +{% if node.target %}{% endif %} diff --git a/resources/template/html/body/list/list-item.html.twig b/resources/template/html/body/list/list-item.html.twig index 1d76fb0c..c0b845fc 100644 --- a/resources/template/html/body/list/list-item.html.twig +++ b/resources/template/html/body/list/list-item.html.twig @@ -1,4 +1,5 @@ - + {%- for child in node.children -%} {{ renderNode(child) }} {%- endfor -%} diff --git a/resources/template/html/body/list/list.html.twig b/resources/template/html/body/list/list.html.twig index cedaa815..2f9d9965 100644 --- a/resources/template/html/body/list/list.html.twig +++ b/resources/template/html/body/list/list.html.twig @@ -4,8 +4,11 @@ {% set keyword = 'ol' %} {% endif %} -<{{ keyword }}{% if node.classes %} class="{{ node.classesString }}"{% endif %}> +<{{ keyword }}{% if node.classes %} class="{{ node.classesString }}"{% endif %} +{%- if node.start %} start="{{ node.start }}"{% endif -%} +{%- if node.orderingType %} type="{{ renderOrderedListType(node.orderingType) }}"{% endif -%}> {% for child in node.children %} {{ renderNode(child) }} {% endfor %} +{# force a new line at the end of the file #} diff --git a/resources/template/html/body/math.html.twig b/resources/template/html/body/math.html.twig new file mode 100644 index 00000000..3bdefb35 --- /dev/null +++ b/resources/template/html/body/math.html.twig @@ -0,0 +1 @@ +{{ node.value|raw }} diff --git a/resources/template/html/body/menu/menu-item.html.twig b/resources/template/html/body/menu/menu-item.html.twig index 42829f82..9133dd2c 100644 --- a/resources/template/html/body/menu/menu-item.html.twig +++ b/resources/template/html/body/menu/menu-item.html.twig @@ -1,16 +1,17 @@ -
  • {{ node.value.toString }} - {%- if node.children|length %} +
  • + {{ node.value.toString }} + {%~ if node.children|length %} - {%- endif -%} - {%- if node.sections|length %} + {%- endif ~%} + {%~ if node.sections|length %}
      {% for subsection in node.sections -%} {{ renderNode(subsection) }} {% endfor %}
    - {%- endif -%} + {%- endif ~%}
  • diff --git a/resources/template/html/body/menu/menu-level.html.twig b/resources/template/html/body/menu/menu-level.html.twig index d0c96bcc..48c8ecb8 100644 --- a/resources/template/html/body/menu/menu-level.html.twig +++ b/resources/template/html/body/menu/menu-level.html.twig @@ -3,3 +3,4 @@ {{ renderNode(entry) }} {% endfor %} +{# force a new line at the end of the file #} diff --git a/resources/template/html/body/paragraph.html.twig b/resources/template/html/body/paragraph.html.twig index 245fd6f2..cb10d5fc 100644 --- a/resources/template/html/body/paragraph.html.twig +++ b/resources/template/html/body/paragraph.html.twig @@ -1,7 +1,5 @@ -{% apply spaceless %} - {% set text = renderNode(node.value) %} +{% set text = renderNode(node.value) %} - {% if text %} - {{ text|raw }}

    - {% endif %} -{% endapply %} +{% if text %} + {{ text|raw }}

    +{% endif %} diff --git a/resources/template/html/body/quote.html.twig b/resources/template/html/body/quote.html.twig index 3f8942e7..8c6752e1 100644 --- a/resources/template/html/body/quote.html.twig +++ b/resources/template/html/body/quote.html.twig @@ -1 +1 @@ -{{ renderNode(node.value) }} +{{- renderNode(node.value) -}} diff --git a/resources/template/html/body/table/table-body.html.twig b/resources/template/html/body/table/table-body.html.twig index 769aca83..78179565 100644 --- a/resources/template/html/body/table/table-body.html.twig +++ b/resources/template/html/body/table/table-body.html.twig @@ -3,3 +3,4 @@ {% include "body/table/table-row.html.twig" %} {% endfor %} +{# force a new line at the end of the file #} diff --git a/resources/template/html/body/table/table-cell.html.twig b/resources/template/html/body/table/table-cell.html.twig index 373906a5..5a9e6029 100644 --- a/resources/template/html/body/table/table-cell.html.twig +++ b/resources/template/html/body/table/table-cell.html.twig @@ -2,3 +2,4 @@ {%- for child in column.children -%} {{- renderNode(child) -}} {%- else %} {% endfor %} +{# force a new line at the end of the file #} diff --git a/resources/template/html/body/table/table-row.html.twig b/resources/template/html/body/table/table-row.html.twig index 2040ca88..3fa50150 100644 --- a/resources/template/html/body/table/table-row.html.twig +++ b/resources/template/html/body/table/table-row.html.twig @@ -3,3 +3,4 @@ {% include "body/table/table-cell.html.twig" %} {% endfor %} +{# force a new line at the end of the file #} diff --git a/resources/template/html/inline/anchor.html.twig b/resources/template/html/inline/anchor.html.twig index 72ebecdc..f95c8dfe 100644 --- a/resources/template/html/inline/anchor.html.twig +++ b/resources/template/html/inline/anchor.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} -{% endapply %} diff --git a/resources/template/html/inline/doc.html.twig b/resources/template/html/inline/doc.html.twig index 09b89757..04701ab3 100644 --- a/resources/template/html/inline/doc.html.twig +++ b/resources/template/html/inline/doc.html.twig @@ -1 +1 @@ -{% apply spaceless %}{{ include('inline/link.html.twig') }}{% endapply %} +{{- include('inline/link.html.twig') -}} diff --git a/resources/template/html/inline/emphasis.html.twig b/resources/template/html/inline/emphasis.html.twig index 6dddff1f..1708213e 100644 --- a/resources/template/html/inline/emphasis.html.twig +++ b/resources/template/html/inline/emphasis.html.twig @@ -1,3 +1,5 @@ -{% apply spaceless %} -{{- node.value -}} -{% endapply %} + + {%- for child in node.children -%} + {{- renderNode(child) -}} + {%- endfor -%} + diff --git a/resources/template/html/inline/image.html.twig b/resources/template/html/inline/image.html.twig index 0923a3eb..3da6db2e 100644 --- a/resources/template/html/inline/image.html.twig +++ b/resources/template/html/inline/image.html.twig @@ -1,2 +1,2 @@ -{% apply spaceless %}{{- node.altText -}} -{% endapply %} +{{- node.altText -}} diff --git a/resources/template/html/inline/link.html.twig b/resources/template/html/inline/link.html.twig index 9c9160ee..a6db95ca 100644 --- a/resources/template/html/inline/link.html.twig +++ b/resources/template/html/inline/link.html.twig @@ -1,7 +1,14 @@ {%- if node.url -%} - - {{- node.value -}} + + {%- for child in node.children -%} + {{- renderNode(child) -}} + {%- endfor -%} {%- else -%} - {{- node.value -}} + {%- for child in node.children -%} + {{- renderNode(child) -}} + {%- endfor -%} {%- endif -%} diff --git a/resources/template/html/inline/literal.html.twig b/resources/template/html/inline/literal.html.twig index 905abfb1..d5b5dea4 100644 --- a/resources/template/html/inline/literal.html.twig +++ b/resources/template/html/inline/literal.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{- node.value -}} -{% endapply %} diff --git a/resources/template/html/inline/nbsp.html.twig b/resources/template/html/inline/nbsp.html.twig index 5a7ba0e7..c0c0ffe1 100644 --- a/resources/template/html/inline/nbsp.html.twig +++ b/resources/template/html/inline/nbsp.html.twig @@ -1 +1 @@ -{% apply spaceless %} {% endapply %} +  diff --git a/resources/template/html/inline/newline.html.twig b/resources/template/html/inline/newline.html.twig index 399784e8..0ca25d9f 100644 --- a/resources/template/html/inline/newline.html.twig +++ b/resources/template/html/inline/newline.html.twig @@ -1 +1 @@ -{% apply spaceless %}
    {% endapply %} +
    diff --git a/resources/template/html/inline/ref.html.twig b/resources/template/html/inline/ref.html.twig index 09b89757..04701ab3 100644 --- a/resources/template/html/inline/ref.html.twig +++ b/resources/template/html/inline/ref.html.twig @@ -1 +1 @@ -{% apply spaceless %}{{ include('inline/link.html.twig') }}{% endapply %} +{{- include('inline/link.html.twig') -}} diff --git a/resources/template/html/inline/strong.html.twig b/resources/template/html/inline/strong.html.twig index 1c543c18..77a6bc00 100644 --- a/resources/template/html/inline/strong.html.twig +++ b/resources/template/html/inline/strong.html.twig @@ -1,2 +1,5 @@ -{% apply spaceless %}{{- node.value -}} -{% endapply %} + +{%- for child in node.children -%} + {{- renderNode(child) -}} +{%- endfor -%} + diff --git a/resources/template/html/inline/textroles/abbreviation.html.twig b/resources/template/html/inline/textroles/abbreviation.html.twig index 57b4cbf6..0d900f0b 100644 --- a/resources/template/html/inline/textroles/abbreviation.html.twig +++ b/resources/template/html/inline/textroles/abbreviation.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.term }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/aspect.html.twig b/resources/template/html/inline/textroles/aspect.html.twig index 7f6c3a0a..ea89e807 100644 --- a/resources/template/html/inline/textroles/aspect.html.twig +++ b/resources/template/html/inline/textroles/aspect.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/br.html.twig b/resources/template/html/inline/textroles/br.html.twig index cf16fe37..1915ec14 100644 --- a/resources/template/html/inline/textroles/br.html.twig +++ b/resources/template/html/inline/textroles/br.html.twig @@ -1 +1 @@ -{%- apply spaceless %}
    {% endapply -%} +
    diff --git a/resources/template/html/inline/textroles/code.html.twig b/resources/template/html/inline/textroles/code.html.twig index f4fa91ff..e9e2930a 100644 --- a/resources/template/html/inline/textroles/code.html.twig +++ b/resources/template/html/inline/textroles/code.html.twig @@ -1,4 +1 @@ -{% apply spaceless %} {{ node.value }} - -{% endapply %} diff --git a/resources/template/html/inline/textroles/command.html.twig b/resources/template/html/inline/textroles/command.html.twig index 1ed7a24b..98321e15 100644 --- a/resources/template/html/inline/textroles/command.html.twig +++ b/resources/template/html/inline/textroles/command.html.twig @@ -1,4 +1 @@ -{% apply spaceless %} {{ node.value }} - -{% endapply %} diff --git a/resources/template/html/inline/textroles/dfn.html.twig b/resources/template/html/inline/textroles/dfn.html.twig index fca41efa..18c575cb 100644 --- a/resources/template/html/inline/textroles/dfn.html.twig +++ b/resources/template/html/inline/textroles/dfn.html.twig @@ -1,4 +1 @@ -{% apply spaceless %} {{ node.value }} - -{% endapply %} diff --git a/resources/template/html/inline/textroles/emphasis.html.twig b/resources/template/html/inline/textroles/emphasis.html.twig index d3c2d9be..c4e714eb 100644 --- a/resources/template/html/inline/textroles/emphasis.html.twig +++ b/resources/template/html/inline/textroles/emphasis.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/file.html.twig b/resources/template/html/inline/textroles/file.html.twig index a2c7b735..61a234f9 100644 --- a/resources/template/html/inline/textroles/file.html.twig +++ b/resources/template/html/inline/textroles/file.html.twig @@ -1,4 +1 @@ -{% apply spaceless %} {{ node.value }} - -{% endapply %} diff --git a/resources/template/html/inline/textroles/guilabel.html.twig b/resources/template/html/inline/textroles/guilabel.html.twig index 0416d4b9..d71b7926 100644 --- a/resources/template/html/inline/textroles/guilabel.html.twig +++ b/resources/template/html/inline/textroles/guilabel.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/kbd.html.twig b/resources/template/html/inline/textroles/kbd.html.twig index cacd6cfb..d12f64a0 100644 --- a/resources/template/html/inline/textroles/kbd.html.twig +++ b/resources/template/html/inline/textroles/kbd.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/literal.html.twig b/resources/template/html/inline/textroles/literal.html.twig index a3d14c72..e9e2930a 100644 --- a/resources/template/html/inline/textroles/literal.html.twig +++ b/resources/template/html/inline/textroles/literal.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/mailheader.html.twig b/resources/template/html/inline/textroles/mailheader.html.twig index 713547cc..b3eaf3b8 100644 --- a/resources/template/html/inline/textroles/mailheader.html.twig +++ b/resources/template/html/inline/textroles/mailheader.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/math.html.twig b/resources/template/html/inline/textroles/math.html.twig index 3ee9551b..9cfd6cdd 100644 --- a/resources/template/html/inline/textroles/math.html.twig +++ b/resources/template/html/inline/textroles/math.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/span.html.twig b/resources/template/html/inline/textroles/span.html.twig index da3c85d9..5a1008f6 100644 --- a/resources/template/html/inline/textroles/span.html.twig +++ b/resources/template/html/inline/textroles/span.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{- node.value -}} -{% endapply %} diff --git a/resources/template/html/inline/textroles/strong.html.twig b/resources/template/html/inline/textroles/strong.html.twig index c79e8248..6bfb54e7 100644 --- a/resources/template/html/inline/textroles/strong.html.twig +++ b/resources/template/html/inline/textroles/strong.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/sub.html.twig b/resources/template/html/inline/textroles/sub.html.twig index d6887d95..5e03d447 100644 --- a/resources/template/html/inline/textroles/sub.html.twig +++ b/resources/template/html/inline/textroles/sub.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/subscript.html.twig b/resources/template/html/inline/textroles/subscript.html.twig index d6887d95..5e03d447 100644 --- a/resources/template/html/inline/textroles/subscript.html.twig +++ b/resources/template/html/inline/textroles/subscript.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/sup.html.twig b/resources/template/html/inline/textroles/sup.html.twig index 83ecfd28..feb157f8 100644 --- a/resources/template/html/inline/textroles/sup.html.twig +++ b/resources/template/html/inline/textroles/sup.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/superscript.html.twig b/resources/template/html/inline/textroles/superscript.html.twig index 83ecfd28..feb157f8 100644 --- a/resources/template/html/inline/textroles/superscript.html.twig +++ b/resources/template/html/inline/textroles/superscript.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/t.html.twig b/resources/template/html/inline/textroles/t.html.twig index 300329c9..6616ef56 100644 --- a/resources/template/html/inline/textroles/t.html.twig +++ b/resources/template/html/inline/textroles/t.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/title-reference.html.twig b/resources/template/html/inline/textroles/title-reference.html.twig index 300329c9..6616ef56 100644 --- a/resources/template/html/inline/textroles/title-reference.html.twig +++ b/resources/template/html/inline/textroles/title-reference.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/title.html.twig b/resources/template/html/inline/textroles/title.html.twig index 300329c9..6616ef56 100644 --- a/resources/template/html/inline/textroles/title.html.twig +++ b/resources/template/html/inline/textroles/title.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/textroles/unknown.html.twig b/resources/template/html/inline/textroles/unknown.html.twig index bca55e70..c09d70d9 100644 --- a/resources/template/html/inline/textroles/unknown.html.twig +++ b/resources/template/html/inline/textroles/unknown.html.twig @@ -1,3 +1 @@ -{% apply spaceless %} {{ node.value }} -{% endapply %} diff --git a/resources/template/html/inline/variable.html.twig b/resources/template/html/inline/variable.html.twig index 71cb90e3..1f89324f 100644 --- a/resources/template/html/inline/variable.html.twig +++ b/resources/template/html/inline/variable.html.twig @@ -1,2 +1 @@ -{% apply spaceless %}{{ renderNode(node.child) }} -{% endapply %} +{{- renderNode(node.child) -}} diff --git a/resources/template/html/structure/header-title.html.twig b/resources/template/html/structure/header-title.html.twig index 8ca02538..a2775cb5 100644 --- a/resources/template/html/structure/header-title.html.twig +++ b/resources/template/html/structure/header-title.html.twig @@ -1 +1 @@ -{{ renderNode(node.value) }} +{{ renderNode(node.value) }} diff --git a/resources/template/html/template.php b/resources/template/html/template.php index 67194cf6..89da801e 100644 --- a/resources/template/html/template.php +++ b/resources/template/html/template.php @@ -34,6 +34,7 @@ use phpDocumentor\Guides\Nodes\ListItemNode; use phpDocumentor\Guides\Nodes\ListNode; use phpDocumentor\Guides\Nodes\LiteralBlockNode; +use phpDocumentor\Guides\Nodes\MathNode; use phpDocumentor\Guides\Nodes\Metadata\AddressNode; use phpDocumentor\Guides\Nodes\Metadata\AuthorNode; use phpDocumentor\Guides\Nodes\Metadata\AuthorsNode; @@ -75,13 +76,13 @@ ListNode::class => 'body/list/list.html.twig', ListItemNode::class => 'body/list/list-item.html.twig', LiteralBlockNode::class => 'body/literal-block.html.twig', + MathNode::class => 'body/math.html.twig', CitationNode::class => 'body/citation.html.twig', FootnoteNode::class => 'body/footnote.html.twig', AnnotationListNode::class => 'body/annotation-list.html.twig', EmbeddedFrame::class => 'body/embedded-frame.html.twig', // Inline ImageInlineNode::class => 'inline/image.html.twig', - InlineCompoundNode::class => 'inline/inline-node.html.twig', AbbreviationInlineNode::class => 'inline/textroles/abbreviation.html.twig', CitationInlineNode::class => 'inline/citation.html.twig', DocReferenceNode::class => 'inline/doc.html.twig', @@ -96,6 +97,8 @@ StrongInlineNode::class => 'inline/strong.html.twig', VariableInlineNode::class => 'inline/variable.html.twig', GenericTextRoleInlineNode::class => 'inline/textroles/generic.html.twig', + InlineCompoundNode::class => 'inline/inline-node.html.twig', + // Output as Metatags AuthorNode::class => 'structure/header/author.html.twig', CopyrightNode::class => 'structure/header/copyright.html.twig', diff --git a/resources/template/tex/body/paragraph.tex.twig b/resources/template/tex/body/paragraph.tex.twig index 41bdff70..2ffeb891 100644 --- a/resources/template/tex/body/paragraph.tex.twig +++ b/resources/template/tex/body/paragraph.tex.twig @@ -1,8 +1,5 @@ -{% apply spaceless %} -{% set text = renderNode(node.value) %} +{%- set text = renderNode(node.value) -%} -{% if text|trim %} -{{ text|raw }} - -{% endif %} -{% endapply %} +{%- if text|trim %} + {{- text|raw -}} +{% endif -%} diff --git a/resources/template/tex/inline/literal.tex.twig b/resources/template/tex/inline/literal.tex.twig index 68547a46..78e810b6 100644 --- a/resources/template/tex/inline/literal.tex.twig +++ b/resources/template/tex/inline/literal.tex.twig @@ -1,3 +1 @@ -{% apply spaceless %} \texttt{{ '{' }}{{- node.value -}}{{ '}' }} -{% endapply %} diff --git a/resources/template/tex/inline/textroles/generic.tex.twig b/resources/template/tex/inline/textroles/generic.tex.twig index a0d7329e..709a5124 100644 --- a/resources/template/tex/inline/textroles/generic.tex.twig +++ b/resources/template/tex/inline/textroles/generic.tex.twig @@ -1,4 +1,4 @@ {%- include [ ('inline/textroles/' ~ node.type ~ '.tex.twig'), - 'inline/textroles/unkown.tex.twig' + 'inline/textroles/unknown.tex.twig' ] -%} diff --git a/resources/template/tex/inline/textroles/unkown.tex.twig b/resources/template/tex/inline/textroles/unknown.tex.twig similarity index 54% rename from resources/template/tex/inline/textroles/unkown.tex.twig rename to resources/template/tex/inline/textroles/unknown.tex.twig index 68547a46..78e810b6 100644 --- a/resources/template/tex/inline/textroles/unkown.tex.twig +++ b/resources/template/tex/inline/textroles/unknown.tex.twig @@ -1,3 +1 @@ -{% apply spaceless %} \texttt{{ '{' }}{{- node.value -}}{{ '}' }} -{% endapply %} diff --git a/resources/template/tex/structure/header-title.tex.twig b/resources/template/tex/structure/header-title.tex.twig index af78ebf0..7ed46794 100644 --- a/resources/template/tex/structure/header-title.tex.twig +++ b/resources/template/tex/structure/header-title.tex.twig @@ -1,6 +1,4 @@ -{%- apply spaceless -%} {%- set headingLevel = node.level -%} \{% if headingLevel == 1 %}section{% elseif headingLevel == 2 %}subsection{% elseif headingLevel == 3 %}subsubsection{% elseif headingLevel == 4 %}paragraph{% elseif headingLevel == 5 %}subparagraph{% elseif headingLevel == 6 %}subparagraph{% endif %}{ {{- renderNode(node.value) -}} } -{%- endapply -%} diff --git a/src/Compiler/CompilerContext.php b/src/Compiler/CompilerContext.php index e7531723..45a1c40f 100644 --- a/src/Compiler/CompilerContext.php +++ b/src/Compiler/CompilerContext.php @@ -13,13 +13,25 @@ namespace phpDocumentor\Guides\Compiler; +use Doctrine\Deprecations\Deprecation; use Exception; use phpDocumentor\Guides\Compiler\ShadowTree\TreeNode; use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\Nodes\ProjectNode; -final class CompilerContext +/** + * Context class used in compiler passes to store the state of the nodes. + * + * The {@see Compiler} is making changes to the nodes in a {@see DocumentNode} as the nodes are immutable cannot + * do this directly. This class helps to modify the nodes in the {@see DocumentNode} by creating a shadow tree. + * + * The class is final and should not be extended, if you need to provide more information to the compiler pass + * you can use the {@see CompilerContextInterface} and decorate this class. + * + * @final + */ +class CompilerContext implements CompilerContextInterface { /** @var TreeNode */ private TreeNode $shadowTree; @@ -27,6 +39,15 @@ final class CompilerContext public function __construct( private readonly ProjectNode $projectNode, ) { + if (self::class === static::class) { + return; + } + + Deprecation::trigger( + 'phpdocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues/971', + 'Extending CompilerContext is deprecated, please use the CompilerContextInterface instead.', + ); } public function getProjectNode(): ProjectNode diff --git a/src/Compiler/CompilerContextInterface.php b/src/Compiler/CompilerContextInterface.php new file mode 100644 index 00000000..0168da4d --- /dev/null +++ b/src/Compiler/CompilerContextInterface.php @@ -0,0 +1,37 @@ + $shadowTree */ + public function withShadowTree(TreeNode $shadowTree): self; + + /** @return TreeNode */ + public function getShadowTree(): TreeNode; + + /** @return array */ + public function getLoggerInformation(): array; +} diff --git a/src/Compiler/NodeTransformers/CitationInlineNodeTransformer.php b/src/Compiler/NodeTransformers/CitationInlineNodeTransformer.php index d41ccf37..61083574 100644 --- a/src/Compiler/NodeTransformers/CitationInlineNodeTransformer.php +++ b/src/Compiler/NodeTransformers/CitationInlineNodeTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\Inline\CitationInlineNode; use phpDocumentor\Guides\Nodes\Node; @@ -21,7 +21,7 @@ /** @implements NodeTransformer */ final class CitationInlineNodeTransformer implements NodeTransformer { - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { if ($node instanceof CitationInlineNode) { $internalTarget = $compilerContext->getProjectNode()->getCitationTarget($node->getName()); @@ -31,7 +31,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { return $node; } diff --git a/src/Compiler/NodeTransformers/CitationTargetTransformer.php b/src/Compiler/NodeTransformers/CitationTargetTransformer.php index 92621674..1fa6033d 100644 --- a/src/Compiler/NodeTransformers/CitationTargetTransformer.php +++ b/src/Compiler/NodeTransformers/CitationTargetTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Meta\CitationTarget; use phpDocumentor\Guides\Nodes\CitationNode; @@ -22,7 +22,7 @@ /** @implements NodeTransformer */ final class CitationTargetTransformer implements NodeTransformer { - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { if ($node instanceof CitationNode) { $compilerContext->getProjectNode()->addCitationTarget( @@ -37,7 +37,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { return $node; } diff --git a/src/Compiler/NodeTransformers/ClassNodeTransformer.php b/src/Compiler/NodeTransformers/ClassNodeTransformer.php index 22fb83fb..0e0be95a 100644 --- a/src/Compiler/NodeTransformers/ClassNodeTransformer.php +++ b/src/Compiler/NodeTransformers/ClassNodeTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\ClassNode; use phpDocumentor\Guides\Nodes\DocumentNode; @@ -32,7 +32,7 @@ final class ClassNodeTransformer implements NodeTransformer /** @var string[] */ private array $classes = []; - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { if ($node instanceof DocumentNode) { // unset classes when entering the next document @@ -52,7 +52,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { if ($node instanceof ClassNode) { //Remove the class node from the tree. diff --git a/src/Compiler/NodeTransformers/CollectLinkTargetsTransformer.php b/src/Compiler/NodeTransformers/CollectLinkTargetsTransformer.php index a0dd7af0..90c17e37 100644 --- a/src/Compiler/NodeTransformers/CollectLinkTargetsTransformer.php +++ b/src/Compiler/NodeTransformers/CollectLinkTargetsTransformer.php @@ -13,19 +13,25 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; +use phpDocumentor\Guides\Exception\DuplicateLinkAnchorException; use phpDocumentor\Guides\Meta\InternalTarget; use phpDocumentor\Guides\Nodes\AnchorNode; use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\LinkTargetNode; use phpDocumentor\Guides\Nodes\MultipleLinkTargetsNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\OptionalLinkTargetsNode; +use phpDocumentor\Guides\Nodes\PrefixedLinkTargetNode; use phpDocumentor\Guides\Nodes\SectionNode; use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer; +use Psr\Log\LoggerInterface; use SplStack; use Webmozart\Assert\Assert; +use function sprintf; + /** @implements NodeTransformer */ final class CollectLinkTargetsTransformer implements NodeTransformer { @@ -34,6 +40,7 @@ final class CollectLinkTargetsTransformer implements NodeTransformer public function __construct( private readonly AnchorNormalizer $anchorReducer, + private LoggerInterface|null $logger = null, ) { /* * TODO: remove stack here, as we should not have sub documents in this way, sub documents are @@ -43,11 +50,15 @@ public function __construct( $this->documentStack = new SplStack(); } - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { if ($node instanceof DocumentNode) { $this->documentStack->push($node); - } elseif ($node instanceof AnchorNode) { + + return $node; + } + + if ($node instanceof AnchorNode) { $currentDocument = $compilerContext->getDocumentNode(); $parentSection = $compilerContext->getShadowTree()->getParent()?->getNode(); $title = null; @@ -56,34 +67,81 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node } $anchorName = $this->anchorReducer->reduceAnchor($node->toString()); - $compilerContext->getProjectNode()->addLinkTarget( - $anchorName, - new InternalTarget( - $currentDocument->getFilePath(), - $node->toString(), - $title, - ), - ); - } elseif ($node instanceof LinkTargetNode) { + try { + $compilerContext->getProjectNode()->addLinkTarget( + $anchorName, + new InternalTarget( + $currentDocument->getFilePath(), + $node->toString(), + $title, + ), + ); + } catch (DuplicateLinkAnchorException $exception) { + $this->logger?->warning($exception->getMessage(), $compilerContext->getLoggerInformation()); + } + + return $node; + } + + if ($node instanceof SectionNode) { $currentDocument = $this->documentStack->top(); Assert::notNull($currentDocument); - $anchor = $node->getId(); - $compilerContext->getProjectNode()->addLinkTarget( - $anchor, + $anchorName = $node->getId(); + foreach ($node->getChildren() as $childNode) { + if ($childNode instanceof AnchorNode) { + $anchorName = $childNode->getValue(); + break; + } + } + + try { + $compilerContext->getProjectNode()->addLinkTarget( + $node->getId(), + new InternalTarget( + $currentDocument->getFilePath(), + $anchorName, + $node->getLinkText(), + SectionNode::STD_TITLE, + ), + ); + } catch (DuplicateLinkAnchorException $exception) { + $this->logger?->warning($exception->getMessage(), $compilerContext->getLoggerInformation()); + } + + return $node; + } + + if ($node instanceof LinkTargetNode) { + if ($node instanceof OptionalLinkTargetsNode && $node->isNoindex()) { + return $node; + } + + $currentDocument = $this->documentStack->top(); + Assert::notNull($currentDocument); + $anchor = $this->anchorReducer->reduceAnchor($node->getId()); + $prefix = ''; + if ($node instanceof PrefixedLinkTargetNode) { + $prefix = $node->getPrefix(); + } + + $this->addLinkTargetToProject( + $compilerContext, new InternalTarget( $currentDocument->getFilePath(), $anchor, $node->getLinkText(), $node->getLinkType(), + $prefix, ), ); if ($node instanceof MultipleLinkTargetsNode) { foreach ($node->getAdditionalIds() as $id) { - $compilerContext->getProjectNode()->addLinkTarget( - $id, + $anchor = $this->anchorReducer->reduceAnchor($id); + $this->addLinkTargetToProject( + $compilerContext, new InternalTarget( $currentDocument->getFilePath(), - $id, + $anchor, $node->getLinkText(), $node->getLinkType(), ), @@ -95,7 +153,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { if ($node instanceof DocumentNode) { $this->documentStack->pop(); @@ -114,4 +172,32 @@ public function getPriority(): int // After MetasPass return 5000; } + + private function addLinkTargetToProject(CompilerContextInterface $compilerContext, InternalTarget $internalTarget): void + { + if ($compilerContext->getProjectNode()->hasInternalTarget($internalTarget->getAnchor(), $internalTarget->getLinkType())) { + $otherLink = $compilerContext->getProjectNode()->getInternalTarget($internalTarget->getAnchor(), $internalTarget->getLinkType()); + $this->logger?->warning( + sprintf( + 'Duplicate anchor "%s" for link type "%s" in document "%s". The anchor is already used at "%s"', + $internalTarget->getAnchor(), + $internalTarget->getLinkType(), + $compilerContext->getDocumentNode()->getFilePath(), + $otherLink?->getDocumentPath(), + ), + $compilerContext->getLoggerInformation(), + ); + + return; + } + + try { + $compilerContext->getProjectNode()->addLinkTarget( + $internalTarget->getAnchor(), + $internalTarget, + ); + } catch (DuplicateLinkAnchorException $exception) { + $this->logger?->warning($exception->getMessage(), $compilerContext->getLoggerInformation()); + } + } } diff --git a/src/Compiler/NodeTransformers/DocumentBlockNodeTransformer.php b/src/Compiler/NodeTransformers/DocumentBlockNodeTransformer.php index 4936ec85..01b88c7c 100644 --- a/src/Compiler/NodeTransformers/DocumentBlockNodeTransformer.php +++ b/src/Compiler/NodeTransformers/DocumentBlockNodeTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\DocumentBlockNode; use phpDocumentor\Guides\Nodes\Menu\TocNode; @@ -27,12 +27,12 @@ */ final class DocumentBlockNodeTransformer implements NodeTransformer { - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { if ($node instanceof DocumentBlockNode) { $children = []; diff --git a/src/Compiler/NodeTransformers/DocumentEntryRegistrationTransformer.php b/src/Compiler/NodeTransformers/DocumentEntryRegistrationTransformer.php index 1a01611e..16ef5fed 100644 --- a/src/Compiler/NodeTransformers/DocumentEntryRegistrationTransformer.php +++ b/src/Compiler/NodeTransformers/DocumentEntryRegistrationTransformer.php @@ -13,28 +13,34 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; +use phpDocumentor\Guides\Event\ModifyDocumentEntryAdditionalData; use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\Nodes\TitleNode; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; +use function assert; +use function is_string; + /** @implements NodeTransformer */ final class DocumentEntryRegistrationTransformer implements NodeTransformer { public function __construct( private readonly LoggerInterface $logger, + private readonly EventDispatcherInterface|null $eventDispatcher = null, ) { } - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { if (!$node instanceof DocumentNode) { return $node; @@ -44,7 +50,23 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu $this->logger->warning('Document has no title', $compilerContext->getLoggerInformation()); } - $entry = new DocumentEntryNode($node->getFilePath(), $node->getTitle() ?? TitleNode::emptyNode(), $node->isRoot()); + $additionalData = []; + if (is_string($node->getNavigationTitle())) { + $additionalData['navigationTitle'] = TitleNode::fromString($node->getNavigationTitle()); + } + + if ($this->eventDispatcher !== null) { + $event = $this->eventDispatcher->dispatch(new ModifyDocumentEntryAdditionalData($additionalData, $node, $compilerContext)); + assert($event instanceof ModifyDocumentEntryAdditionalData); + $additionalData = $event->getAdditionalData(); + } + + $entry = new DocumentEntryNode( + $node->getFilePath(), + $node->getTitle() ?? TitleNode::emptyNode(), + $node->isRoot(), + $additionalData, + ); $compilerContext->getProjectNode()->addDocumentEntry($entry); return $node->setDocumentEntry($entry); diff --git a/src/Compiler/NodeTransformers/FootNodeNamedTransformer.php b/src/Compiler/NodeTransformers/FootNodeNamedTransformer.php index 43b96a39..de29f9e6 100644 --- a/src/Compiler/NodeTransformers/FootNodeNamedTransformer.php +++ b/src/Compiler/NodeTransformers/FootNodeNamedTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Meta\FootnoteTarget; use phpDocumentor\Guides\Nodes\FootnoteNode; @@ -22,7 +22,7 @@ /** @implements NodeTransformer */ final class FootNodeNamedTransformer implements NodeTransformer { - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { if ($node instanceof FootnoteNode && $this->supports($node)) { $number = $compilerContext->getDocumentNode()->addFootnoteTarget(new FootnoteTarget( @@ -37,7 +37,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { return $node; } diff --git a/src/Compiler/NodeTransformers/FootNodeNumberedTransformer.php b/src/Compiler/NodeTransformers/FootNodeNumberedTransformer.php index ac7903cb..29cbaf2b 100644 --- a/src/Compiler/NodeTransformers/FootNodeNumberedTransformer.php +++ b/src/Compiler/NodeTransformers/FootNodeNumberedTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Meta\FootnoteTarget; use phpDocumentor\Guides\Nodes\FootnoteNode; @@ -22,7 +22,7 @@ /** @implements NodeTransformer */ final class FootNodeNumberedTransformer implements NodeTransformer { - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { if ($node instanceof FootnoteNode && $this->supports($node)) { $compilerContext->getDocumentNode()->addFootnoteTarget(new FootnoteTarget( @@ -36,7 +36,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { return $node; } diff --git a/src/Compiler/NodeTransformers/FootnoteInlineNodeTransformer.php b/src/Compiler/NodeTransformers/FootnoteInlineNodeTransformer.php index 844726b6..03d35003 100644 --- a/src/Compiler/NodeTransformers/FootnoteInlineNodeTransformer.php +++ b/src/Compiler/NodeTransformers/FootnoteInlineNodeTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\Inline\FootnoteInlineNode; use phpDocumentor\Guides\Nodes\Node; @@ -21,7 +21,7 @@ /** @implements NodeTransformer */ final class FootnoteInlineNodeTransformer implements NodeTransformer { - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { if ($node instanceof FootnoteInlineNode) { if ($node->getNumber() > 0) { @@ -38,7 +38,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { return $node; } diff --git a/src/Compiler/NodeTransformers/ListNodeTransformer.php b/src/Compiler/NodeTransformers/ListNodeTransformer.php new file mode 100644 index 00000000..a4dbd866 --- /dev/null +++ b/src/Compiler/NodeTransformers/ListNodeTransformer.php @@ -0,0 +1,62 @@ + */ +final class ListNodeTransformer implements NodeTransformer +{ + public function __construct( + private readonly LoggerInterface $logger, + ) { + } + + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node + { + return $node; + } + + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null + { + assert($node instanceof ListNode); + foreach ($node->getChildren() as $listItemNode) { + assert($listItemNode instanceof ListItemNode); + if (!empty($listItemNode->getChildren())) { + continue; + } + + $this->logger->warning('List item without content', $compilerContext->getLoggerInformation()); + } + + return $node; + } + + public function supports(Node $node): bool + { + return $node instanceof ListNode; + } + + public function getPriority(): int + { + return 1000; + } +} diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/AbstractMenuEntryNodeTransformer.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/AbstractMenuEntryNodeTransformer.php index 6cf4166f..df8ea46d 100644 --- a/src/Compiler/NodeTransformers/MenuNodeTransformers/AbstractMenuEntryNodeTransformer.php +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/AbstractMenuEntryNodeTransformer.php @@ -14,7 +14,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers; use Exception; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuNode; @@ -32,13 +32,13 @@ public function __construct( ) { } - final public function enterNode(Node $node, CompilerContext $compilerContext): MenuEntryNode + final public function enterNode(Node $node, CompilerContextInterface $compilerContext): MenuEntryNode { return $node; } /** @param MenuEntryNode $node */ - final public function leaveNode(Node $node, CompilerContext $compilerContext): MenuEntryNode|null + final public function leaveNode(Node $node, CompilerContextInterface $compilerContext): MenuEntryNode|null { assert($node instanceof MenuEntryNode); $currentMenuShaddow = $compilerContext->getShadowTree()->getParent(); @@ -70,5 +70,5 @@ final public function leaveNode(Node $node, CompilerContext $compilerContext): M } /** @return list */ - abstract protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContext $compilerContext): array; + abstract protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array; } diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/ContentsMenuEntryNodeTransformer.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/ContentsMenuEntryNodeTransformer.php index 07953f71..7883f838 100644 --- a/src/Compiler/NodeTransformers/MenuNodeTransformers/ContentsMenuEntryNodeTransformer.php +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/ContentsMenuEntryNodeTransformer.php @@ -13,12 +13,14 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; +use phpDocumentor\Guides\Nodes\DocumentTree\SectionEntryNode; use phpDocumentor\Guides\Nodes\Menu\ContentMenuNode; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuNode; use phpDocumentor\Guides\Nodes\Menu\SectionMenuEntryNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\SectionNode; use function assert; @@ -36,7 +38,7 @@ public function supports(Node $node): bool } /** @return list */ - protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContext $compilerContext): array + protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array { if (!$currentMenu instanceof ContentMenuNode) { return [$entryNode]; @@ -45,12 +47,36 @@ protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNo assert($entryNode instanceof SectionMenuEntryNode); $depth = (int) $currentMenu->getOption('depth', self::DEFAULT_MAX_LEVELS - 1) + 1; $documentEntry = $compilerContext->getDocumentNode()->getDocumentEntry(); - $newEntryNode = new SectionMenuEntryNode( - $documentEntry->getFile(), - $entryNode->getValue() ?? $documentEntry->getTitle(), - 1, - ); - $this->addSubSectionsToMenuEntries($documentEntry, $newEntryNode, $depth); + if ($currentMenu->isLocal()) { + $sectionNode = $compilerContext->getShadowTree()->getParent()?->getParent()?->getNode(); + if (!$sectionNode instanceof SectionNode) { + $this->logger->error('Section of contents directive not found. ', $compilerContext->getLoggerInformation()); + + return []; + } + + $sectionEntry = $documentEntry->findSectionEntry($sectionNode); + if (!$sectionEntry instanceof SectionEntryNode) { + $this->logger->error('Section of contents directive not found. ', $compilerContext->getLoggerInformation()); + + return []; + } + + $newEntryNode = new SectionMenuEntryNode( + $documentEntry->getFile(), + $entryNode->getValue() ?? $sectionEntry->getTitle(), + 1, + $sectionEntry->getId(), + ); + $this->addSubSections($newEntryNode, $sectionEntry, $documentEntry, 1, $depth); + } else { + $newEntryNode = new SectionMenuEntryNode( + $documentEntry->getFile(), + $entryNode->getValue() ?? $documentEntry->getTitle(), + 1, + ); + $this->addSubSectionsToMenuEntries($documentEntry, $newEntryNode, $depth); + } return $newEntryNode->getSections(); } diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/ExternalMenuEntryNodeTransformer.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/ExternalMenuEntryNodeTransformer.php new file mode 100644 index 00000000..2efd0b16 --- /dev/null +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/ExternalMenuEntryNodeTransformer.php @@ -0,0 +1,66 @@ + */ + protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array + { + assert($entryNode instanceof ExternalMenuEntryNode); + + $newEntryNode = new ExternalEntryNode( + $entryNode->getUrl(), + ($entryNode->getValue() ?? TitleNode::emptyNode())->toString(), + ); + + if ($currentMenu instanceof TocNode) { + $this->attachDocumentEntriesToParents([$newEntryNode], $compilerContext, ''); + } + + return [$entryNode]; + } + + public function getPriority(): int + { + // After DocumentEntryTransformer + return 4500; + } +} diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/GlobMenuEntryNodeTransformer.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/GlobMenuEntryNodeTransformer.php index b9c4e404..5ed5fec5 100644 --- a/src/Compiler/NodeTransformers/MenuNodeTransformers/GlobMenuEntryNodeTransformer.php +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/GlobMenuEntryNodeTransformer.php @@ -13,21 +13,23 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Nodes\Menu\GlobMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuNode; use phpDocumentor\Guides\Nodes\Menu\TocNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\TitleNode; use function array_pop; use function assert; use function explode; use function implode; use function in_array; +use function is_string; use function preg_match; -use function str_replace; +use function preg_replace; final class GlobMenuEntryNodeTransformer extends AbstractMenuEntryNodeTransformer { @@ -38,7 +40,7 @@ final class GlobMenuEntryNodeTransformer extends AbstractMenuEntryNodeTransforme private const DEFAULT_MAX_LEVELS = 10; /** @return list */ - protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContext $compilerContext): array + protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array { assert($entryNode instanceof GlobMenuEntryNode); $maxDepth = (int) $currentMenu->getOption('maxdepth', self::DEFAULT_MAX_LEVELS); @@ -65,10 +67,16 @@ protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNo } } + $titleNode = $documentEntry->getTitle(); + $navigationTitle = $documentEntry->getAdditionalData('navigationTitle'); + if ($navigationTitle instanceof TitleNode) { + $titleNode = $navigationTitle; + } + $documentEntriesInTree[] = $documentEntry; $newEntryNode = new InternalMenuEntryNode( $documentEntry->getFile(), - $documentEntry->getTitle(), + $titleNode, [], false, 1, @@ -130,7 +138,9 @@ private static function matches(string $actualFile, GlobMenuEntryNode $parsedMen private static function isGlob(string $documentEntryFile, string $currentPath, string $file, string $prefix, array $globExclude): bool { if (!in_array($documentEntryFile, $globExclude, true)) { - $file = str_replace('*', '[^\/]*', $file); + $file = preg_replace('/(? 0; diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/InternalMenuEntryNodeTransformer.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/InternalMenuEntryNodeTransformer.php index 82245cc9..6e928381 100644 --- a/src/Compiler/NodeTransformers/MenuNodeTransformers/InternalMenuEntryNodeTransformer.php +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/InternalMenuEntryNodeTransformer.php @@ -13,12 +13,13 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuNode; use phpDocumentor\Guides\Nodes\Menu\TocNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\TitleNode; use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface; use Psr\Log\LoggerInterface; @@ -49,7 +50,7 @@ public function supports(Node $node): bool } /** @return list */ - protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContext $compilerContext): array + protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array { assert($entryNode instanceof InternalMenuEntryNode); $documentEntries = $compilerContext->getProjectNode()->getAllDocumentEntries(); @@ -63,10 +64,20 @@ protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNo continue; } + $titleNode = $documentEntry->getTitle(); + $navigationTitle = $documentEntry->getAdditionalData('navigationTitle'); + if ($navigationTitle instanceof TitleNode) { + $titleNode = $navigationTitle; + } + + if ($entryNode->getValue() instanceof TitleNode) { + $titleNode = $entryNode->getValue(); + } + $documentEntriesInTree[] = $documentEntry; $newEntryNode = new InternalMenuEntryNode( $documentEntry->getFile(), - $entryNode->getValue() ?? $documentEntry->getTitle(), + $titleNode, [], false, 1, diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/MenuEntryManagement.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/MenuEntryManagement.php index c2a9262a..73b918e9 100644 --- a/src/Compiler/NodeTransformers/MenuNodeTransformers/MenuEntryManagement.php +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/MenuEntryManagement.php @@ -13,42 +13,45 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode; use function sprintf; use function str_starts_with; trait MenuEntryManagement { - /** @param DocumentEntryNode[] $documentEntriesInTree */ + /** @param array $entryNodes */ private function attachDocumentEntriesToParents( - array $documentEntriesInTree, - CompilerContext $compilerContext, + array $entryNodes, + CompilerContextInterface $compilerContext, string $currentPath, ): void { - foreach ($documentEntriesInTree as $documentEntryInToc) { - if ($documentEntryInToc->isRoot() || $currentPath === $documentEntryInToc->getFile()) { - // The root page may not be attached to any other - continue; - } + foreach ($entryNodes as $entryNode) { + if ($entryNode instanceof DocumentEntryNode) { + if (($entryNode->isRoot() || $currentPath === $entryNode->getFile())) { + // The root page may not be attached to any other + continue; + } - if ($documentEntryInToc->getParent() !== null && $documentEntryInToc->getParent() !== $compilerContext->getDocumentNode()->getDocumentEntry()) { - $this->logger->warning(sprintf( - 'Document %s has been added to parents %s and %s. The `toctree` directive changes the ' - . 'position of documents in the document tree. Use the `menu` directive to only display a menu without changing the document tree.', - $documentEntryInToc->getFile(), - $documentEntryInToc->getParent()->getFile(), - $compilerContext->getDocumentNode()->getDocumentEntry()->getFile(), - ), $compilerContext->getLoggerInformation()); - } + if ($entryNode->getParent() !== null && $entryNode->getParent() !== $compilerContext->getDocumentNode()->getDocumentEntry()) { + $this->logger->warning(sprintf( + 'Document %s has been added to parents %s and %s. The `toctree` directive changes the ' + . 'position of documents in the document tree. Use the `menu` directive to only display a menu without changing the document tree.', + $entryNode->getFile(), + $entryNode->getParent()->getFile(), + $compilerContext->getDocumentNode()->getDocumentEntry()->getFile(), + ), $compilerContext->getLoggerInformation()); + } - if ($documentEntryInToc->getParent() !== null) { - continue; + if ($entryNode->getParent() !== null) { + continue; + } } - $documentEntryInToc->setParent($compilerContext->getDocumentNode()->getDocumentEntry()); - $compilerContext->getDocumentNode()->getDocumentEntry()->addChild($documentEntryInToc); + $entryNode->setParent($compilerContext->getDocumentNode()->getDocumentEntry()); + $compilerContext->getDocumentNode()->getDocumentEntry()->addChild($entryNode); } } diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/SubInternalMenuEntryNodeTransformer.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/SubInternalMenuEntryNodeTransformer.php index d0555af7..92c13363 100644 --- a/src/Compiler/NodeTransformers/MenuNodeTransformers/SubInternalMenuEntryNodeTransformer.php +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/SubInternalMenuEntryNodeTransformer.php @@ -13,13 +13,16 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Exception\DocumentEntryNotFound; use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode; +use phpDocumentor\Guides\Nodes\Menu\ExternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\TitleNode; use function assert; use function sprintf; @@ -38,7 +41,7 @@ public function supports(Node $node): bool } /** @return list */ - protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContext $compilerContext): array + protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array { assert($entryNode instanceof InternalMenuEntryNode); $maxDepth = (int) $currentMenu->getOption('maxdepth', self::DEFAULT_MAX_LEVELS); @@ -50,7 +53,6 @@ protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNo return []; } - $documentEntryOfMenuEntry = $compilerContext->getProjectNode()->getDocumentEntry($entryNode->getUrl()); $this->addSubEntries($currentMenu, $compilerContext, $entryNode, $documentEntryOfMenuEntry, $entryNode->getLevel() + 1, $maxDepth); return [$entryNode]; @@ -64,7 +66,7 @@ public function getPriority(): int private function addSubEntries( MenuNode $currentMenu, - CompilerContext $compilerContext, + CompilerContextInterface $compilerContext, InternalMenuEntryNode $sectionMenuEntry, DocumentEntryNode $documentEntry, int $currentLevel, @@ -74,24 +76,44 @@ private function addSubEntries( return; } - foreach ($documentEntry->getChildren() as $subDocumentEntryNode) { - $subMenuEntry = new InternalMenuEntryNode( - $subDocumentEntryNode->getFile(), - $subDocumentEntryNode->getTitle(), - [], - false, - $currentLevel, - '', - self::isInRootline($subDocumentEntryNode, $compilerContext->getDocumentNode()->getDocumentEntry()), - self::isCurrent($subDocumentEntryNode, $compilerContext->getDocumentNode()->getFilePath()), - ); + foreach ($documentEntry->getMenuEntries() as $subEntryNode) { + if ($subEntryNode instanceof DocumentEntryNode) { + $titleNode = $subEntryNode->getTitle(); + $navigationTitle = $subEntryNode->getAdditionalData('navigationTitle'); + if ($navigationTitle instanceof TitleNode) { + $titleNode = $navigationTitle; + } + + $subMenuEntry = new InternalMenuEntryNode( + $subEntryNode->getFile(), + $titleNode, + [], + false, + $currentLevel, + '', + self::isInRootline($subEntryNode, $compilerContext->getDocumentNode()->getDocumentEntry()), + self::isCurrent($subEntryNode, $compilerContext->getDocumentNode()->getFilePath()), + ); + + if (!$currentMenu->hasOption('titlesonly') && $maxDepth - $currentLevel + 1 > 1) { + $this->addSubSectionsToMenuEntries($subEntryNode, $subMenuEntry, $maxDepth - $currentLevel + 2); + } + + $sectionMenuEntry->addMenuEntry($subMenuEntry); + $this->addSubEntries($currentMenu, $compilerContext, $subMenuEntry, $subEntryNode, $currentLevel + 1, $maxDepth); + continue; + } - if (!$currentMenu->hasOption('titlesonly') && $maxDepth - $currentLevel + 1 > 1) { - $this->addSubSectionsToMenuEntries($subDocumentEntryNode, $subMenuEntry, $maxDepth - $currentLevel + 2); + if (!($subEntryNode instanceof ExternalEntryNode)) { + continue; } + $subMenuEntry = new ExternalMenuEntryNode( + $subEntryNode->getValue(), + TitleNode::fromString($subEntryNode->getTitle()), + $currentLevel, + ); $sectionMenuEntry->addMenuEntry($subMenuEntry); - $this->addSubEntries($currentMenu, $compilerContext, $subMenuEntry, $subDocumentEntryNode, $currentLevel + 1, $maxDepth); } } } diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/TocNodeReplacementTransformer.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/TocNodeReplacementTransformer.php new file mode 100644 index 00000000..0063b211 --- /dev/null +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/TocNodeReplacementTransformer.php @@ -0,0 +1,72 @@ + */ +final class TocNodeReplacementTransformer implements NodeTransformer +{ + public function __construct( + private readonly LoggerInterface $logger, + private readonly SettingsManager $settingsManager, + ) { + } + + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node + { + return $node; + } + + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null + { + if (!$node instanceof TocNode) { + return $node; + } + + if (!$this->settingsManager->getProjectSettings()->isAutomaticMenu()) { + return $node; + } + + if ($node->hasOption('hidden')) { + $this->logger->warning('The `.. toctree::` directive with option `:hidden:` is not supported in automatic-menu mode. ', $compilerContext->getLoggerInformation()); + + return null; + } + + $this->logger->warning('The `.. toctree::` directive is not supported in automatic-menu mode. Use `.. menu::` instead. ', $compilerContext->getLoggerInformation()); + $menuNode = new NavMenuNode($node->getMenuEntries()); + $menuNode = $menuNode->withOptions($node->getOptions()); + $menuNode = $menuNode->withCaption($node->getCaption()); + + return $menuNode; + } + + public function supports(Node $node): bool + { + return $node instanceof TocNode; + } + + public function getPriority(): int + { + return 20_000; + } +} diff --git a/src/Compiler/NodeTransformers/MenuNodeTransformers/TocNodeTransformer.php b/src/Compiler/NodeTransformers/MenuNodeTransformers/TocNodeTransformer.php index 590c7c77..f69643e4 100644 --- a/src/Compiler/NodeTransformers/MenuNodeTransformers/TocNodeTransformer.php +++ b/src/Compiler/NodeTransformers/MenuNodeTransformers/TocNodeTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\Menu\TocNode; use phpDocumentor\Guides\Nodes\Node; @@ -23,7 +23,7 @@ /** @implements NodeTransformer */ final class TocNodeTransformer implements NodeTransformer { - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { assert($node instanceof TocNode); $compilerContext->getDocumentNode()->addTocNode($node); @@ -31,7 +31,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { return $node; } diff --git a/src/Compiler/NodeTransformers/MoveAnchorTransformer.php b/src/Compiler/NodeTransformers/MoveAnchorTransformer.php index 6c3158f2..9b6ef3c8 100644 --- a/src/Compiler/NodeTransformers/MoveAnchorTransformer.php +++ b/src/Compiler/NodeTransformers/MoveAnchorTransformer.php @@ -15,7 +15,7 @@ use ArrayIterator; use LogicException; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Compiler\ShadowTree\TreeNode; use phpDocumentor\Guides\Nodes\AnchorNode; @@ -34,12 +34,12 @@ public function __construct() $this->seen = new WeakMap(); } - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { //When exists in seen, it means that the node has already been processed. Ignore it. if (isset($this->seen[$node])) { diff --git a/src/Compiler/NodeTransformers/RawNodeEscapeTransformer.php b/src/Compiler/NodeTransformers/RawNodeEscapeTransformer.php new file mode 100644 index 00000000..a04f5d05 --- /dev/null +++ b/src/Compiler/NodeTransformers/RawNodeEscapeTransformer.php @@ -0,0 +1,72 @@ + */ +final class RawNodeEscapeTransformer implements NodeTransformer +{ + private HtmlSanitizer $htmlSanitizer; + + public function __construct( + private readonly bool $escapeRawNodes, + private readonly LoggerInterface $logger, + HtmlSanitizerConfig $htmlSanitizerConfig, + ) { + $this->htmlSanitizer = new HtmlSanitizer($htmlSanitizerConfig); + } + + public function enterNode(Node $node, CompilerContext $compilerContext): Node + { + return $node; + } + + public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + { + assert($node instanceof RawNode); + if ($this->escapeRawNodes) { + $this->logger->warning('We do not support plain HTML for security reasons. Escaping all HTML '); + + return new ParagraphNode([new InlineCompoundNode([new PlainTextInlineNode($node->getValue())])]); + } + + if ($node->getOption('format', 'html') === 'html') { + return new RawNode($this->htmlSanitizer->sanitize($node->getValue())); + } + + return $node; + } + + public function supports(Node $node): bool + { + return $node instanceof RawNode; + } + + public function getPriority(): int + { + return 1000; + } +} diff --git a/src/Compiler/NodeTransformers/SectionCreationTransformer.php b/src/Compiler/NodeTransformers/SectionCreationTransformer.php index 00bcf056..4542f3cc 100644 --- a/src/Compiler/NodeTransformers/SectionCreationTransformer.php +++ b/src/Compiler/NodeTransformers/SectionCreationTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\Node; @@ -31,9 +31,15 @@ final class SectionCreationTransformer implements NodeTransformer { /** @var SectionNode[] $sectionStack */ private array $sectionStack = []; + private int $firstLevel = 1; - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { + if ($node instanceof DocumentNode) { + $this->firstLevel = 1; + $this->sectionStack = []; + } + if (!$compilerContext->getShadowTree()->getParent()?->getNode() instanceof DocumentNode) { return $node; } @@ -48,7 +54,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { if (!$compilerContext->getShadowTree()->getParent()?->getNode() instanceof DocumentNode) { return $node; @@ -64,7 +70,7 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu if (count($this->sectionStack) > 0 && $compilerContext->getShadowTree()->isLastChildOfParent()) { $lastSection = end($this->sectionStack); - while ($lastSection?->getTitle()->getLevel() > 1) { + while ($lastSection?->getTitle()->getLevel() > $this->firstLevel) { $lastSection = array_pop($this->sectionStack); } @@ -88,9 +94,9 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu end($this->sectionStack)->addChildNode($newSection); } - $this->sectionStack[] = $newSection; + $this->pushNewSectionToStack($newSection); - return $lastSection?->getTitle()->getLevel() === 1 ? $lastSection : null; + return $lastSection?->getTitle()->getLevel() <= $this->firstLevel ? $lastSection : null; } $newSection = new SectionNode($node); @@ -98,7 +104,7 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu $lastSection->addChildNode($newSection); } - $this->sectionStack[] = $newSection; + $this->pushNewSectionToStack($newSection); return null; } @@ -113,4 +119,21 @@ public function getPriority(): int // Should run as first transformer return PHP_INT_MAX; } + + /** + * Pushes the new section to the stack. + * + * The stack is used to track the current level of nodes and adding child + * nodes to the section. As not all documentation formats are using the + * correct level of title nodes we need to track the level of the first + * title node to determine the correct level of the section. + */ + private function pushNewSectionToStack(SectionNode $newSection): void + { + if (count($this->sectionStack) === 0) { + $this->firstLevel = $newSection->getTitle()->getLevel(); + } + + $this->sectionStack[] = $newSection; + } } diff --git a/src/Compiler/NodeTransformers/SectionEntryRegistrationTransformer.php b/src/Compiler/NodeTransformers/SectionEntryRegistrationTransformer.php index 136964ec..de7e2cc0 100644 --- a/src/Compiler/NodeTransformers/SectionEntryRegistrationTransformer.php +++ b/src/Compiler/NodeTransformers/SectionEntryRegistrationTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\DocumentTree\SectionEntryNode; use phpDocumentor\Guides\Nodes\Node; @@ -30,7 +30,7 @@ final class SectionEntryRegistrationTransformer implements NodeTransformer /** @var SectionEntryNode[] $sectionStack */ private array $sectionStack = []; - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { if (!$node instanceof SectionNode) { return $node; @@ -50,7 +50,7 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { if (!$node instanceof SectionNode) { return $node; diff --git a/src/Compiler/NodeTransformers/VariableInlineNodeTransformer.php b/src/Compiler/NodeTransformers/VariableInlineNodeTransformer.php index 7d53eac8..7d2a0c6b 100644 --- a/src/Compiler/NodeTransformers/VariableInlineNodeTransformer.php +++ b/src/Compiler/NodeTransformers/VariableInlineNodeTransformer.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\NodeTransformers; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\NodeTransformer; use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode; use phpDocumentor\Guides\Nodes\Inline\VariableInlineNode; @@ -33,12 +33,12 @@ public function __construct( ) { } - public function enterNode(Node $node, CompilerContext $compilerContext): Node + public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node { return $node; } - public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null { if (!$node instanceof VariableInlineNode) { return $node; diff --git a/src/Compiler/Passes/AutomaticMenuPass.php b/src/Compiler/Passes/AutomaticMenuPass.php new file mode 100644 index 00000000..07757862 --- /dev/null +++ b/src/Compiler/Passes/AutomaticMenuPass.php @@ -0,0 +1,112 @@ +settingsManager->getProjectSettings()->isAutomaticMenu()) { + return $documents; + } + + $projectNode = $compilerContext->getProjectNode(); + $rootDocumentEntry = $projectNode->getRootDocumentEntry(); + $indexNames = explode(',', $this->settingsManager->getProjectSettings()->getIndexName()); + foreach ($documents as $documentNode) { + if ($documentNode->isOrphan()) { + // Do not add orphans to the automatic menu + continue; + } + + if ($documentNode->isRoot()) { + continue; + } + + $filePath = $documentNode->getFilePath(); + $pathParts = explode('/', $filePath); + $documentEntry = $projectNode->getDocumentEntry($filePath); + if (count($pathParts) === 1 || count($pathParts) === 2 && in_array($pathParts[1], $indexNames, true)) { + $documentEntry->setParent($rootDocumentEntry); + $rootDocumentEntry->addChild($documentEntry); + continue; + } + + $fileName = array_pop($pathParts); + $path = implode('/', $pathParts); + if (in_array($fileName, $indexNames, true)) { + array_pop($pathParts); + $path = implode('/', $pathParts); + } + + $parentFound = false; + foreach ($indexNames as $indexName) { + $indexFile = $path . '/' . $indexName; + $parentEntry = $projectNode->findDocumentEntry($indexFile); + if ($parentEntry === null) { + continue; + } + + $documentEntry->setParent($parentEntry); + $parentEntry->addChild($documentEntry); + $parentFound = true; + break; + } + + if ($parentFound) { + continue; + } + + $parentEntry = $projectNode->findDocumentEntry($path); + if ($parentEntry === null) { + $this->logger?->warning(sprintf('No parent found for file "%s/%s" attaching it to the document root instead. ', $path, $fileName)); + continue; + } + + $documentEntry->setParent($parentEntry); + $parentEntry->addChild($documentEntry); + } + + return $documents; + } +} diff --git a/src/Compiler/Passes/GlobalMenuPass.php b/src/Compiler/Passes/GlobalMenuPass.php index 11d6c88c..bdbabff9 100644 --- a/src/Compiler/Passes/GlobalMenuPass.php +++ b/src/Compiler/Passes/GlobalMenuPass.php @@ -13,19 +13,24 @@ namespace phpDocumentor\Guides\Compiler\Passes; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\CompilerPass; use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\EntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode; +use phpDocumentor\Guides\Nodes\Menu\ExternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\NavMenuNode; use phpDocumentor\Guides\Nodes\Menu\TocNode; +use phpDocumentor\Guides\Nodes\TitleNode; use phpDocumentor\Guides\Settings\SettingsManager; use Throwable; use function array_map; use function assert; +use function count; use const PHP_INT_MAX; @@ -46,13 +51,13 @@ public function getPriority(): int * * @return DocumentNode[] */ - public function run(array $documents, CompilerContext $compilerContext): array + public function run(array $documents, CompilerContextInterface $compilerContext): array { $projectNode = $compilerContext->getProjectNode(); try { $rootDocumentEntry = $projectNode->getRootDocumentEntry(); } catch (Throwable) { - // Todo: Functional tests have not root document entry + // Todo: Functional tests have no root document entry return $documents; } @@ -74,12 +79,37 @@ public function run(array $documents, CompilerContext $compilerContext): array $menuNodes[] = $menuNode->withCaption($tocNode->getCaption()); } + if ($this->settingsManager->getProjectSettings()->isAutomaticMenu() && count($menuNodes) === 0) { + $menuNodes[] = $this->getNavMenuNodeFromDocumentEntries($compilerContext); + } + $projectNode->setGlobalMenues($menuNodes); return $documents; } - private function getNavMenuNodefromTocNode(CompilerContext $compilerContext, TocNode $tocNode, string|null $menuType = null): NavMenuNode + private function getNavMenuNodeFromDocumentEntries(CompilerContextInterface $compilerContext): NavMenuNode + { + $rootDocumentEntry = $compilerContext->getProjectNode()->getRootDocumentEntry(); + $menuEntries = $this->getMenuEntriesFromDocumentEntries($rootDocumentEntry); + + return new NavMenuNode($menuEntries); + } + + /** @return InternalMenuEntryNode[] */ + public function getMenuEntriesFromDocumentEntries(DocumentEntryNode $rootDocumentEntry): array + { + $menuEntries = []; + foreach ($rootDocumentEntry->getChildren() as $documentEntryNode) { + $children = $this->getMenuEntriesFromDocumentEntries($documentEntryNode); + $newMenuEntry = new InternalMenuEntryNode($documentEntryNode->getFile(), $documentEntryNode->getTitle(), $children, false, 1); + $menuEntries[] = $newMenuEntry; + } + + return $menuEntries; + } + + private function getNavMenuNodefromTocNode(CompilerContextInterface $compilerContext, TocNode $tocNode, string|null $menuType = null): NavMenuNode { $self = $this; $menuEntries = array_map(static function (MenuEntryNode $tocEntry) use ($compilerContext, $self) { @@ -100,7 +130,7 @@ private function getNavMenuNodefromTocNode(CompilerContext $compilerContext, Toc return $node; } - private function getMenuEntryWithChildren(CompilerContext $compilerContext, MenuEntryNode $menuEntry): MenuEntryNode + private function getMenuEntryWithChildren(CompilerContextInterface $compilerContext, MenuEntryNode $menuEntry): MenuEntryNode { if (!$menuEntry instanceof InternalMenuEntryNode) { return $menuEntry; @@ -115,10 +145,11 @@ private function getMenuEntryWithChildren(CompilerContext $compilerContext, Menu return $newMenuEntry; } + /** @param EntryNode|ExternalEntryNode $entryNode */ private function addSubEntries( - CompilerContext $compilerContext, + CompilerContextInterface $compilerContext, MenuEntryNode $sectionMenuEntry, - DocumentEntryNode $documentEntry, + EntryNode $entryNode, int $currentLevel, int $maxDepth, ): void { @@ -130,17 +161,51 @@ private function addSubEntries( return; } - foreach ($documentEntry->getChildren() as $subDocumentEntryNode) { - $subMenuEntry = new InternalMenuEntryNode( - $subDocumentEntryNode->getFile(), - $subDocumentEntryNode->getTitle(), - [], - false, - $currentLevel, - '', - ); + if (!$entryNode instanceof DocumentEntryNode) { + return; + } + + foreach ($entryNode->getMenuEntries() as $subEntryNode) { + $subMenuEntry = match ($subEntryNode::class) { + DocumentEntryNode::class => $this->createInternalMenuEntry($subEntryNode, $currentLevel), + ExternalEntryNode::class => $this->createExternalMenuEntry($subEntryNode, $currentLevel), + }; + $sectionMenuEntry->addMenuEntry($subMenuEntry); - $this->addSubEntries($compilerContext, $subMenuEntry, $subDocumentEntryNode, $currentLevel + 1, $maxDepth); + $this->addSubEntries( + $compilerContext, + $subMenuEntry, + $subEntryNode, + $currentLevel + 1, + $maxDepth, + ); + } + } + + private function createInternalMenuEntry(DocumentEntryNode $subEntryNode, int $currentLevel): InternalMenuEntryNode + { + $titleNode = $subEntryNode->getTitle(); + $navigationTitle = $subEntryNode->getAdditionalData('navigationTitle'); + if ($navigationTitle instanceof TitleNode) { + $titleNode = $navigationTitle; } + + return new InternalMenuEntryNode( + $subEntryNode->getFile(), + $titleNode, + [], + false, + $currentLevel, + '', + ); + } + + private function createExternalMenuEntry(ExternalEntryNode $subEntryNode, int $currentLevel): ExternalMenuEntryNode + { + return new ExternalMenuEntryNode( + $subEntryNode->getValue(), + TitleNode::fromString($subEntryNode->getTitle()), + $currentLevel, + ); } } diff --git a/src/Compiler/Passes/ImplicitHyperlinkTargetPass.php b/src/Compiler/Passes/ImplicitHyperlinkTargetPass.php index 260bab19..6c7fcbd5 100644 --- a/src/Compiler/Passes/ImplicitHyperlinkTargetPass.php +++ b/src/Compiler/Passes/ImplicitHyperlinkTargetPass.php @@ -13,7 +13,7 @@ namespace phpDocumentor\Guides\Compiler\Passes; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\CompilerPass; use phpDocumentor\Guides\Nodes\AnchorNode; use phpDocumentor\Guides\Nodes\CompoundNode; @@ -43,7 +43,7 @@ public function getPriority(): int } /** {@inheritDoc} */ - public function run(array $documents, CompilerContext $compilerContext): array + public function run(array $documents, CompilerContextInterface $compilerContext): array { return array_map(function (DocumentNode $document): DocumentNode { // implicit references must not conflict with explicit ones diff --git a/src/Compiler/Passes/ToctreeValidationPass.php b/src/Compiler/Passes/ToctreeValidationPass.php index f4bdf880..75a69bb9 100644 --- a/src/Compiler/Passes/ToctreeValidationPass.php +++ b/src/Compiler/Passes/ToctreeValidationPass.php @@ -13,18 +13,25 @@ namespace phpDocumentor\Guides\Compiler\Passes; -use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\CompilerContextInterface; use phpDocumentor\Guides\Compiler\CompilerPass; use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; use phpDocumentor\Guides\Nodes\ProjectNode; +use phpDocumentor\Guides\Settings\ProjectSettings; +use phpDocumentor\Guides\Settings\SettingsManager; use Psr\Log\LoggerInterface; final class ToctreeValidationPass implements CompilerPass { + private SettingsManager $settingsManager; + public function __construct( private readonly LoggerInterface $logger, + SettingsManager|null $settingsManager = null, ) { + // if for backward compatibility reasons no settings manager was passed, use the defaults + $this->settingsManager = $settingsManager ?? new SettingsManager(new ProjectSettings()); } public function getPriority(): int @@ -37,8 +44,12 @@ public function getPriority(): int * * @return DocumentNode[] */ - public function run(array $documents, CompilerContext $compilerContext): array + public function run(array $documents, CompilerContextInterface $compilerContext): array { + if ($this->settingsManager->getProjectSettings()->isAutomaticMenu()) { + return $documents; + } + $projectNode = $compilerContext->getProjectNode(); foreach ($documents as $document) { diff --git a/src/DependencyInjection/GuidesExtension.php b/src/DependencyInjection/GuidesExtension.php index 89904c0d..52e61a8b 100644 --- a/src/DependencyInjection/GuidesExtension.php +++ b/src/DependencyInjection/GuidesExtension.php @@ -13,6 +13,7 @@ namespace phpDocumentor\Guides\DependencyInjection; +use phpDocumentor\Guides\Compiler\NodeTransformers\RawNodeEscapeTransformer; use phpDocumentor\Guides\DependencyInjection\Compiler\NodeRendererPass; use phpDocumentor\Guides\DependencyInjection\Compiler\ParserRulesPass; use phpDocumentor\Guides\DependencyInjection\Compiler\RendererPass; @@ -31,6 +32,8 @@ use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; use function array_keys; use function array_map; @@ -42,6 +45,7 @@ use function is_int; use function is_string; use function pathinfo; +use function trim; use function var_export; final class GuidesExtension extends Extension implements CompilerPassInterface, ConfigurationInterface, PrependExtensionInterface @@ -68,12 +72,33 @@ static function ($value) { return var_export($value, true); } + if (is_string($value)) { + return trim($value, "'"); + } + + return $value; + }, + ) + ->end() + ->end() + ->scalarNode('release') + ->beforeNormalization() + ->always( + // We need to revert the phpize call in XmlUtils. Version is always a string! + static function ($value) { + if (!is_int($value) && !is_string($value)) { + return var_export($value, true); + } + + if (is_string($value)) { + return trim($value, "'"); + } + return $value; }, ) ->end() ->end() - ->scalarNode('release')->end() ->scalarNode('copyright')->end() ->end() ->end() @@ -92,6 +117,7 @@ static function ($value) { ->scalarNode('theme')->end() ->scalarNode('input')->end() ->scalarNode('input_file')->end() + ->scalarNode('index_name')->end() ->scalarNode('output')->end() ->scalarNode('input_format')->end() ->arrayNode('output_format') @@ -120,6 +146,7 @@ static function ($value) { ->scalarNode('show_progress')->end() ->scalarNode('links_are_relative')->end() ->scalarNode('max_menu_depth')->end() + ->scalarNode('automatic_menu')->end() ->arrayNode('base_template_paths') ->defaultValue([]) ->scalarPrototype()->end() @@ -139,6 +166,33 @@ static function ($value) { ->end() ->end() ->end() + ->arrayNode('raw_node') + ->fixXmlConfig('sanitizer') + ->children() + ->booleanNode('escape')->defaultValue(false)->end() + ->scalarNode('sanitizer_name')->end() + ->arrayNode('sanitizers') + ->defaultValue([]) + ->arrayPrototype() + ->fixXmlConfig('allow_element') + ->fixXmlConfig('drop_element') + ->fixXmlConfig('block_element') + ->fixXmlConfig('allow_attribute') + ->fixXmlConfig('drop_attribute') + ->children() + ->scalarNode('name')->isRequired()->end() + ->booleanNode('allow_safe_elements')->defaultValue(true)->end() + ->booleanNode('allow_static_elements')->defaultValue(true)->end() + ->arrayNode('allow_elements')->scalarPrototype()->end()->end() + ->arrayNode('block_elements')->scalarPrototype()->end()->end() + ->arrayNode('drop_elements')->scalarPrototype()->end()->end() + ->arrayNode('allow_attributes')->scalarPrototype()->end()->end() + ->arrayNode('drop_attributes')->scalarPrototype()->end()->end() + ->end() + ->end() + ->end() + ->end() + ->end() ->scalarNode('default_code_language')->defaultValue('')->end() ->arrayNode('themes') ->defaultValue([]) @@ -215,6 +269,10 @@ public function load(array $configs, ContainerBuilder $container): void } } + if (isset($config['index_name']) && $config['index_name'] !== '') { + $projectSettings->setIndexName((string) $config['index_name']); + } + if (isset($config['output'])) { $projectSettings->setOutput((string) $config['output']); } @@ -251,6 +309,10 @@ public function load(array $configs, ContainerBuilder $container): void $projectSettings->setMaxMenuDepth((int) $config['max_menu_depth']); } + if (isset($config['automatic_menu'])) { + $projectSettings->setAutomaticMenu((bool) $config['automatic_menu']); + } + if (isset($config['default_code_language'])) { $projectSettings->setDefaultCodeLanguage((string) $config['default_code_language']); } @@ -263,6 +325,11 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('phpdoc.guides.base_template_paths', $config['base_template_paths']); $container->setParameter('phpdoc.guides.node_templates', $config['templates']); $container->setParameter('phpdoc.guides.inventories', $config['inventories']); + $container->setParameter('phpdoc.guides.raw_node.escape', $config['raw_node']['escape'] ?? false); + + if ($config['raw_node'] ?? false) { + $this->configureSanitizers($config['raw_node'], $container); + } foreach ($config['themes'] as $themeName => $themeConfig) { $container->getDefinition(ThemeManager::class) @@ -301,6 +368,54 @@ public function prepend(ContainerBuilder $container): void ], ); } + + /** @param array $rawNodeConfig */ + private function configureSanitizers(array $rawNodeConfig, ContainerBuilder $container): void + { + if ($rawNodeConfig['sanitizer_name'] ?? false) { + $container->getDefinition(RawNodeEscapeTransformer::class) + ->setArgument('$htmlSanitizerConfig', new Reference('phpdoc.guides.raw_node.sanitizer.' . $rawNodeConfig['sanitizer_name'])); + } + + if (!is_array($rawNodeConfig['sanitizers'] ?? false)) { + return; + } + + foreach ($rawNodeConfig['sanitizers'] as $sanitizerConfig) { + $def = $container->register('phpdoc.guides.raw_node.sanitizer.' . $sanitizerConfig['name'], HtmlSanitizerConfig::class); + + // Base + if ($sanitizerConfig['allow_safe_elements']) { + $def->addMethodCall('allowSafeElements', [], true); + } + + if ($sanitizerConfig['allow_static_elements']) { + $def->addMethodCall('allowStaticElements', [], true); + } + + // Configures elements + foreach ($sanitizerConfig['allow_elements'] as $element => $attributes) { + $def->addMethodCall('allowElement', [$element, $attributes], true); + } + + foreach ($sanitizerConfig['block_elements'] as $element) { + $def->addMethodCall('blockElement', [$element], true); + } + + foreach ($sanitizerConfig['drop_elements'] as $element) { + $def->addMethodCall('dropElement', [$element], true); + } + + // Configures attributes + foreach ($sanitizerConfig['allow_attributes'] as $attribute => $elements) { + $def->addMethodCall('allowAttribute', [$attribute, $elements], true); + } + + foreach ($sanitizerConfig['drop_attributes'] as $attribute => $elements) { + $def->addMethodCall('dropAttribute', [$attribute, $elements], true); + } + } + } } /** diff --git a/src/Event/ModifyDocumentEntryAdditionalData.php b/src/Event/ModifyDocumentEntryAdditionalData.php new file mode 100644 index 00000000..6d87ff1a --- /dev/null +++ b/src/Event/ModifyDocumentEntryAdditionalData.php @@ -0,0 +1,53 @@ + $additionalData */ + public function __construct( + private array $additionalData, + private readonly DocumentNode $documentNode, + private readonly CompilerContextInterface $compilerContext, + ) { + } + + /** @return array */ + public function getAdditionalData(): array + { + return $this->additionalData; + } + + /** @param array $additionalData */ + public function setAdditionalData(array $additionalData): ModifyDocumentEntryAdditionalData + { + $this->additionalData = $additionalData; + + return $this; + } + + public function getDocumentNode(): DocumentNode + { + return $this->documentNode; + } + + public function getCompilerContext(): CompilerContextInterface + { + return $this->compilerContext; + } +} diff --git a/src/Event/PostParseDocument.php b/src/Event/PostParseDocument.php index f252473e..017b8511 100644 --- a/src/Event/PostParseDocument.php +++ b/src/Event/PostParseDocument.php @@ -22,8 +22,11 @@ */ final class PostParseDocument { - public function __construct(private readonly string $fileName, private readonly DocumentNode|null $documentNode) - { + public function __construct( + private readonly string $fileName, + private DocumentNode|null $documentNode, + private readonly string $originalFile, + ) { } public function getDocumentNode(): DocumentNode|null @@ -31,8 +34,18 @@ public function getDocumentNode(): DocumentNode|null return $this->documentNode; } + public function setDocumentNode(DocumentNode|null $documentNode): void + { + $this->documentNode = $documentNode; + } + public function getFileName(): string { return $this->fileName; } + + public function getOriginalFileName(): string + { + return $this->originalFile; + } } diff --git a/src/Exception/DuplicateLinkAnchorException.php b/src/Exception/DuplicateLinkAnchorException.php new file mode 100644 index 00000000..d4ebabb1 --- /dev/null +++ b/src/Exception/DuplicateLinkAnchorException.php @@ -0,0 +1,20 @@ +> $files */ - $files = $filesystem->find( - new AndSpecification(new InPath(new Path($directory)), new HasExtension([$extension])), - ); + $files = $filesystem->find($specification); // completely populate the splFileInfos property $this->fileInfos = []; diff --git a/src/Handlers/ParseDirectoryCommand.php b/src/Handlers/ParseDirectoryCommand.php index 55a037cd..87a8c63c 100644 --- a/src/Handlers/ParseDirectoryCommand.php +++ b/src/Handlers/ParseDirectoryCommand.php @@ -13,6 +13,7 @@ namespace phpDocumentor\Guides\Handlers; +use Flyfinder\Specification\SpecificationInterface; use League\Flysystem\FilesystemInterface; use phpDocumentor\Guides\Nodes\ProjectNode; @@ -23,6 +24,7 @@ public function __construct( private readonly string $directory, private readonly string $inputFormat, private readonly ProjectNode $projectNode, + private readonly SpecificationInterface|null $excludedSpecification = null, ) { } @@ -45,4 +47,9 @@ public function getProjectNode(): ProjectNode { return $this->projectNode; } + + public function getExcludedSpecification(): SpecificationInterface|null + { + return $this->excludedSpecification; + } } diff --git a/src/Handlers/ParseDirectoryHandler.php b/src/Handlers/ParseDirectoryHandler.php index 087b2a3d..8851e4b8 100644 --- a/src/Handlers/ParseDirectoryHandler.php +++ b/src/Handlers/ParseDirectoryHandler.php @@ -21,20 +21,28 @@ use phpDocumentor\Guides\Event\PreParseProcess; use phpDocumentor\Guides\FileCollector; use phpDocumentor\Guides\Nodes\DocumentNode; +use phpDocumentor\Guides\Settings\ProjectSettings; +use phpDocumentor\Guides\Settings\SettingsManager; use Psr\EventDispatcher\EventDispatcherInterface; +use function array_map; use function assert; +use function explode; +use function implode; use function sprintf; final class ParseDirectoryHandler { - private const INDEX_FILE_NAMES = ['index', 'Index']; + private SettingsManager $settingsManager; public function __construct( private readonly FileCollector $fileCollector, private readonly CommandBus $commandBus, private readonly EventDispatcherInterface $eventDispatcher, + SettingsManager|null $settingsManager = null, ) { + // if for backward compatibility reasons no settings manager was passed, use the defaults + $this->settingsManager = $settingsManager ?? new SettingsManager(new ProjectSettings()); } /** @return DocumentNode[] */ @@ -56,7 +64,7 @@ public function handle(ParseDirectoryCommand $command): array $extension, ); - $files = $this->fileCollector->collect($origin, $currentDirectory, $extension); + $files = $this->fileCollector->collect($origin, $currentDirectory, $extension, $command->getExcludedSpecification()); $postCollectFilesForParsingEvent = $this->eventDispatcher->dispatch( new PostCollectFilesForParsingEvent($command, $files), @@ -98,17 +106,20 @@ private function getDirectoryIndexFile( $hashedContentFromFilesystem[$itemFromFilesystem['basename']] = true; } - foreach (self::INDEX_FILE_NAMES as $indexName) { - $indexFilename = sprintf('%s.%s', $indexName, $extension); - if (isset($hashedContentFromFilesystem[$indexFilename])) { + $indexFileNames = array_map('trim', explode(',', $this->settingsManager->getProjectSettings()->getIndexName())); + + $indexNamesNotFound = []; + foreach ($indexFileNames as $indexName) { + $fullIndexFilename = sprintf('%s.%s', $indexName, $extension); + if (isset($hashedContentFromFilesystem[$fullIndexFilename])) { return $indexName; } - } - $indexFilename = sprintf('%s.%s', self::INDEX_FILE_NAMES[0], $extension); + $indexNamesNotFound[] = $fullIndexFilename; + } throw new InvalidArgumentException( - sprintf('Could not find index file "%s" in "%s"', $indexFilename, $directory), + sprintf('Could not find an index file "%s", expected file names: %s', $directory, implode(', ', $indexNamesNotFound)), ); } } diff --git a/src/Handlers/ParseFileHandler.php b/src/Handlers/ParseFileHandler.php index 548f119b..f68262ca 100644 --- a/src/Handlers/ParseFileHandler.php +++ b/src/Handlers/ParseFileHandler.php @@ -103,7 +103,7 @@ private function createDocument( ); } - $event = $this->eventDispatcher->dispatch(new PostParseDocument($fileName, $document)); + $event = $this->eventDispatcher->dispatch(new PostParseDocument($fileName, $document, $path)); assert($event instanceof PostParseDocument); return $event->getDocumentNode(); diff --git a/src/Meta/InternalTarget.php b/src/Meta/InternalTarget.php index 70481516..bc238a93 100644 --- a/src/Meta/InternalTarget.php +++ b/src/Meta/InternalTarget.php @@ -24,6 +24,7 @@ public function __construct( protected string $anchorName, private readonly string|null $title = null, private readonly string $linkType = SectionNode::STD_LABEL, + private readonly string $prefix = '', ) { } @@ -63,4 +64,9 @@ public function getLinkType(): string { return $this->linkType; } + + public function getPrefix(): string + { + return $this->prefix; + } } diff --git a/src/NodeRenderers/Html/AdmonitionNodeRenderer.php b/src/NodeRenderers/Html/AdmonitionNodeRenderer.php new file mode 100644 index 00000000..4deb5bbd --- /dev/null +++ b/src/NodeRenderers/Html/AdmonitionNodeRenderer.php @@ -0,0 +1,59 @@ + */ +class AdmonitionNodeRenderer implements NodeRenderer +{ + public function __construct(private readonly TemplateRenderer $renderer) + { + } + + public function supports(string $nodeFqcn): bool + { + return $nodeFqcn === AdmonitionNode::class || is_a($nodeFqcn, AdmonitionNode::class, true); + } + + public function render(Node $node, RenderContext $renderContext): string + { + if ($node instanceof AdmonitionNode === false) { + throw new InvalidArgumentException('Node must be an instance of ' . AdmonitionNode::class); + } + + $classes = $node->getClasses(); + + return $this->renderer->renderTemplate( + $renderContext, + 'body/admonition.html.twig', + [ + 'name' => $node->getName(), + 'text' => $node->getText(), + 'title' => $node->getTitle(), + 'isTitled' => $node->isTitled(), + 'class' => implode(' ', $classes), + 'node' => $node->getValue(), + ], + ); + } +} diff --git a/src/NodeRenderers/Html/BreadCrumbNodeRenderer.php b/src/NodeRenderers/Html/BreadCrumbNodeRenderer.php index 8c4d28da..24c07e38 100644 --- a/src/NodeRenderers/Html/BreadCrumbNodeRenderer.php +++ b/src/NodeRenderers/Html/BreadCrumbNodeRenderer.php @@ -19,6 +19,7 @@ use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\TitleNode; use phpDocumentor\Guides\RenderContext; use phpDocumentor\Guides\TemplateRenderer; @@ -107,9 +108,15 @@ private function buildBreadcrumb( int $level, bool $isCurrent, ): array { + $title = $documentEntry->getTitle(); + $navigationTitle = $documentEntry->getAdditionalData('navigationTitle'); + if ($navigationTitle instanceof TitleNode) { + $title = $navigationTitle; + } + $entry = new InternalMenuEntryNode( $documentEntry->getFile(), - $documentEntry->getTitle(), + $title, [], false, $level, diff --git a/src/Nodes/AdmonitionNode.php b/src/Nodes/AdmonitionNode.php new file mode 100644 index 00000000..26e03fe2 --- /dev/null +++ b/src/Nodes/AdmonitionNode.php @@ -0,0 +1,44 @@ + */ +class AdmonitionNode extends CompoundNode +{ + /** @param Node[] $value */ + public function __construct(private readonly string $name, private readonly InlineCompoundNode|null $title, private readonly string $text, array $value, private readonly bool $isTitled = false) + { + parent::__construct($value); + } + + public function getName(): string + { + return $this->name; + } + + public function getTitle(): InlineCompoundNode|null + { + return $this->title; + } + + public function getText(): string + { + return $this->text; + } + + public function isTitled(): bool + { + return $this->isTitled; + } +} diff --git a/src/Nodes/DocumentNode.php b/src/Nodes/DocumentNode.php index 1acead6b..fad13d23 100644 --- a/src/Nodes/DocumentNode.php +++ b/src/Nodes/DocumentNode.php @@ -19,6 +19,7 @@ use phpDocumentor\Guides\Nodes\DocumentTree\SectionEntryNode; use phpDocumentor\Guides\Nodes\Menu\TocNode; use phpDocumentor\Guides\Nodes\Metadata\MetadataNode; +use phpDocumentor\Guides\Nodes\Metadata\NavigationTitleNode; use function array_filter; use function max; @@ -52,6 +53,8 @@ final class DocumentNode extends CompoundNode private int $maxFootnoteNumber = 0; private int $lastReturnedAnonymousFootnoteNumber = -1; + private string|null $navigationTitle = null; + /** * Variables are replacements in a document or project. * @@ -97,6 +100,11 @@ public function getNodes(string $nodeType = Node::class): array return array_filter($this->value, static fn ($node): bool => $node instanceof $nodeType); } + public function getNavigationTitle(): string|null + { + return $this->navigationTitle; + } + public function getPageTitle(): string|null { if ($this->metaTitle !== null) { @@ -113,7 +121,7 @@ public function getPageTitle(): string|null public function getTitle(): TitleNode|null { foreach ($this->value as $node) { - if ($node instanceof SectionNode && $node->getTitle()->getLevel() === 1) { + if ($node instanceof SectionNode) { return $node->getTitle(); } @@ -132,6 +140,10 @@ public function setMetaTitle(string $metaTitle): void public function addHeaderNode(MetadataNode $node): void { + if ($node instanceof NavigationTitleNode) { + $this->navigationTitle = $node->getValue(); + } + $this->headerNodes[] = $node; } diff --git a/src/Nodes/DocumentTree/DocumentEntryNode.php b/src/Nodes/DocumentTree/DocumentEntryNode.php index baf51a78..6dfac5ad 100644 --- a/src/Nodes/DocumentTree/DocumentEntryNode.php +++ b/src/Nodes/DocumentTree/DocumentEntryNode.php @@ -13,23 +13,27 @@ namespace phpDocumentor\Guides\Nodes\DocumentTree; -use phpDocumentor\Guides\Nodes\AbstractNode; -use phpDocumentor\Guides\Nodes\DocumentNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\SectionNode; use phpDocumentor\Guides\Nodes\TitleNode; -/** @extends AbstractNode */ -final class DocumentEntryNode extends AbstractNode +use function array_filter; +use function array_values; + +/** @extends EntryNode */ +final class DocumentEntryNode extends EntryNode { - /** @var DocumentEntryNode[] */ + /** @var array */ private array $entries = []; /** @var SectionEntryNode[] */ private array $sections = []; - private DocumentEntryNode|null $parent = null; + /** @param array $additionalData */ public function __construct( private readonly string $file, private readonly TitleNode $titleNode, private readonly bool $isRoot = false, + private array $additionalData = [], ) { } @@ -38,25 +42,27 @@ public function getTitle(): TitleNode return $this->titleNode; } - public function addChild(DocumentEntryNode $child): void + public function addChild(DocumentEntryNode|ExternalEntryNode $child): void { $this->entries[] = $child; } - /** @return DocumentEntryNode[] */ + /** @return array */ public function getChildren(): array { - return $this->entries; - } + // Filter the entries array to only include DocumentEntryNode instances + $documentEntries = array_filter($this->entries, static function ($entry) { + return $entry instanceof DocumentEntryNode; + }); - public function getParent(): DocumentEntryNode|null - { - return $this->parent; + // Re-index the array to maintain numeric keys + return array_values($documentEntries); } - public function setParent(DocumentEntryNode|null $parent): void + /** @return array */ + public function getMenuEntries(): array { - $this->parent = $parent; + return $this->entries; } /** @return SectionEntryNode[] */ @@ -79,4 +85,32 @@ public function isRoot(): bool { return $this->isRoot; } + + public function findSectionEntry(SectionNode $sectionNode): SectionEntryNode|null + { + foreach ($this->sections as $sectionEntryNode) { + if ($sectionNode->getId() === $sectionEntryNode->getId()) { + return $sectionEntryNode; + } + } + + foreach ($this->sections as $sectionEntryNode) { + $subsection = $sectionEntryNode->findSectionEntry($sectionNode); + if ($subsection !== null) { + return $subsection; + } + } + + return null; + } + + public function getAdditionalData(string $key): Node|null + { + return $this->additionalData[$key] ?? null; + } + + public function addAdditionalData(string $key, Node $value): void + { + $this->additionalData[$key] = $value; + } } diff --git a/src/Nodes/DocumentTree/EntryNode.php b/src/Nodes/DocumentTree/EntryNode.php new file mode 100644 index 00000000..2ae0daad --- /dev/null +++ b/src/Nodes/DocumentTree/EntryNode.php @@ -0,0 +1,35 @@ + + */ +abstract class EntryNode extends AbstractNode +{ + private DocumentEntryNode|null $parent = null; + + public function getParent(): DocumentEntryNode|null + { + return $this->parent; + } + + public function setParent(DocumentEntryNode|null $parent): void + { + $this->parent = $parent; + } +} diff --git a/src/Nodes/DocumentTree/ExternalEntryNode.php b/src/Nodes/DocumentTree/ExternalEntryNode.php new file mode 100644 index 00000000..cc807688 --- /dev/null +++ b/src/Nodes/DocumentTree/ExternalEntryNode.php @@ -0,0 +1,28 @@ + */ +final class ExternalEntryNode extends EntryNode +{ + public function __construct(string $value, public readonly string $title) + { + $this->value = $value; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/src/Nodes/DocumentTree/SectionEntryNode.php b/src/Nodes/DocumentTree/SectionEntryNode.php index ebc16223..5de4a0be 100644 --- a/src/Nodes/DocumentTree/SectionEntryNode.php +++ b/src/Nodes/DocumentTree/SectionEntryNode.php @@ -13,12 +13,12 @@ namespace phpDocumentor\Guides\Nodes\DocumentTree; -use phpDocumentor\Guides\Nodes\AbstractNode; use phpDocumentor\Guides\Nodes\DocumentNode; +use phpDocumentor\Guides\Nodes\SectionNode; use phpDocumentor\Guides\Nodes\TitleNode; -/** @extends AbstractNode */ -final class SectionEntryNode extends AbstractNode +/** @extends EntryNode */ +final class SectionEntryNode extends EntryNode { /** @var SectionEntryNode[] */ private array $children = []; @@ -47,4 +47,22 @@ public function getChildren(): array { return $this->children; } + + public function findSectionEntry(SectionNode $sectionNode): SectionEntryNode|null + { + foreach ($this->children as $sectionEntryNode) { + if ($sectionNode->getId() === $sectionEntryNode->getId()) { + return $sectionEntryNode; + } + } + + foreach ($this->children as $sectionEntryNode) { + $subsection = $sectionEntryNode->findSectionEntry($sectionNode); + if ($subsection !== null) { + return $subsection; + } + } + + return null; + } } diff --git a/src/Nodes/ImageNode.php b/src/Nodes/ImageNode.php index cb4f1525..14cbd9a5 100644 --- a/src/Nodes/ImageNode.php +++ b/src/Nodes/ImageNode.php @@ -13,6 +13,19 @@ namespace phpDocumentor\Guides\Nodes; +use phpDocumentor\Guides\Nodes\Inline\LinkInlineNode; + final class ImageNode extends TextNode { + public LinkInlineNode|null $target = null; + + public function getTarget(): LinkInlineNode|null + { + return $this->target; + } + + public function setTarget(LinkInlineNode|null $target): void + { + $this->target = $target; + } } diff --git a/src/Nodes/Inline/AbstractLinkInlineNode.php b/src/Nodes/Inline/AbstractLinkInlineNode.php index 3e1aa3dd..5732d4ec 100644 --- a/src/Nodes/Inline/AbstractLinkInlineNode.php +++ b/src/Nodes/Inline/AbstractLinkInlineNode.php @@ -13,13 +13,32 @@ namespace phpDocumentor\Guides\Nodes\Inline; -abstract class AbstractLinkInlineNode extends InlineNode implements LinkInlineNode +use Doctrine\Deprecations\Deprecation; +use phpDocumentor\Guides\Nodes\InlineCompoundNode; + +abstract class AbstractLinkInlineNode extends InlineCompoundNode implements LinkInlineNode { + use BCInlineNodeBehavior; + private string $url = ''; - public function __construct(string $type, private readonly string $targetReference, string $value = '') - { - parent::__construct($type, $value); + /** @param InlineNodeInterface[] $children */ + public function __construct( + private readonly string $type, + private readonly string $targetReference, + string $value = '', + array $children = [], + ) { + if (empty($children)) { + Deprecation::trigger( + 'phpdocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues/1161', + 'Please provide the children as an array of InlineNodeInterface instances instead of a string.', + ); + $children = [new PlainTextInlineNode($value)]; + } + + parent::__construct($children); } public function getTargetReference(): string @@ -46,4 +65,9 @@ public function getDebugInformation(): array 'value' => $this->getValue(), ]; } + + public function getType(): string + { + return $this->type; + } } diff --git a/src/Nodes/Inline/BCInlineNodeBehavior.php b/src/Nodes/Inline/BCInlineNodeBehavior.php new file mode 100644 index 00000000..e34ca853 --- /dev/null +++ b/src/Nodes/Inline/BCInlineNodeBehavior.php @@ -0,0 +1,50 @@ +toString(); + } + + /** @param InlineNodeInterface[]|string $value */ + public function setValue(mixed $value): void + { + if (is_string($value)) { + $value = [new PlainTextInlineNode($value)]; + + Deprecation::trigger( + 'phpdocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues/1161', + 'Please provide the children as an array of InlineNodeInterface instances instead of a string.', + ); + } + + parent::setValue($value); + } + + abstract public function toString(): string; +} diff --git a/src/Nodes/Inline/EmphasisInlineNode.php b/src/Nodes/Inline/EmphasisInlineNode.php index ecb80900..1d4371aa 100644 --- a/src/Nodes/Inline/EmphasisInlineNode.php +++ b/src/Nodes/Inline/EmphasisInlineNode.php @@ -13,12 +13,32 @@ namespace phpDocumentor\Guides\Nodes\Inline; -final class EmphasisInlineNode extends InlineNode +use Doctrine\Deprecations\Deprecation; +use phpDocumentor\Guides\Nodes\InlineCompoundNode; + +final class EmphasisInlineNode extends InlineCompoundNode { + use BCInlineNodeBehavior; + public const TYPE = 'emphasis'; - public function __construct(string $value) + /** @param InlineNodeInterface[] $children */ + public function __construct(string $value, array $children = []) + { + if (empty($children)) { + $children = [new PlainTextInlineNode($value)]; + Deprecation::trigger( + 'phpdocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues/1161', + 'Please provide the children as an array of InlineNodeInterface instances instead of a string.', + ); + } + + parent::__construct($children); + } + + public function getType(): string { - parent::__construct(self::TYPE, $value); + return self::TYPE; } } diff --git a/src/Nodes/Inline/HyperLinkNode.php b/src/Nodes/Inline/HyperLinkNode.php index 5ea04e8f..89eef6f5 100644 --- a/src/Nodes/Inline/HyperLinkNode.php +++ b/src/Nodes/Inline/HyperLinkNode.php @@ -18,8 +18,9 @@ */ final class HyperLinkNode extends AbstractLinkInlineNode { - public function __construct(string $value, string $targetReference) + /** @param InlineNodeInterface[] $children */ + public function __construct(string $value, string $targetReference, array $children = []) { - parent::__construct('link', $targetReference, $value); + parent::__construct('link', $targetReference, $value, $children); } } diff --git a/src/Nodes/Inline/ImageInlineNode.php b/src/Nodes/Inline/ImageInlineNode.php index ffd0c783..e44550bb 100644 --- a/src/Nodes/Inline/ImageInlineNode.php +++ b/src/Nodes/Inline/ImageInlineNode.php @@ -20,8 +20,11 @@ final class ImageInlineNode extends InlineNode { public const TYPE = 'image'; - public function __construct(private readonly string $url, private readonly string $altText) - { + public function __construct( + private readonly string $url, + private readonly string $altText, + private readonly string|null $title = null, + ) { parent::__construct(self::TYPE, $url); } @@ -34,4 +37,9 @@ public function getAltText(): string { return $this->altText; } + + public function getTitle(): string + { + return $this->title ?? ''; + } } diff --git a/src/Nodes/Inline/InlineNode.php b/src/Nodes/Inline/InlineNode.php index 3f8414d6..59bc5f15 100644 --- a/src/Nodes/Inline/InlineNode.php +++ b/src/Nodes/Inline/InlineNode.php @@ -16,7 +16,7 @@ use phpDocumentor\Guides\Nodes\AbstractNode; /** @extends AbstractNode */ -abstract class InlineNode extends AbstractNode +abstract class InlineNode extends AbstractNode implements InlineNodeInterface { public function __construct(private readonly string $type, string $value = '') { diff --git a/src/Nodes/Inline/InlineNodeInterface.php b/src/Nodes/Inline/InlineNodeInterface.php new file mode 100644 index 00000000..50e67be6 --- /dev/null +++ b/src/Nodes/Inline/InlineNodeInterface.php @@ -0,0 +1,23 @@ +linkType; + } + + public function getPrefix(): string + { + return $this->prefix; } } diff --git a/src/Nodes/Inline/StrongInlineNode.php b/src/Nodes/Inline/StrongInlineNode.php index 8bd6dd52..1a3d3a26 100644 --- a/src/Nodes/Inline/StrongInlineNode.php +++ b/src/Nodes/Inline/StrongInlineNode.php @@ -13,12 +13,32 @@ namespace phpDocumentor\Guides\Nodes\Inline; -final class StrongInlineNode extends InlineNode +use Doctrine\Deprecations\Deprecation; +use phpDocumentor\Guides\Nodes\InlineCompoundNode; + +final class StrongInlineNode extends InlineCompoundNode { + use BCInlineNodeBehavior; + public const TYPE = 'strong'; - public function __construct(string $value) + /** @param InlineNodeInterface[] $children */ + public function __construct(string $value, array $children = []) + { + if (empty($children)) { + $children = [new PlainTextInlineNode($value)]; + Deprecation::trigger( + 'phpdocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues/1161', + 'Please provide the children as an array of InlineNodeInterface instances instead of a string.', + ); + } + + parent::__construct($children); + } + + public function getType(): string { - parent::__construct(self::TYPE, $value); + return self::TYPE; } } diff --git a/src/Nodes/InlineCompoundNode.php b/src/Nodes/InlineCompoundNode.php index 497f487a..02a045e9 100644 --- a/src/Nodes/InlineCompoundNode.php +++ b/src/Nodes/InlineCompoundNode.php @@ -13,11 +13,11 @@ namespace phpDocumentor\Guides\Nodes; -use phpDocumentor\Guides\Nodes\Inline\InlineNode; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode; -/** @extends CompoundNode */ -final class InlineCompoundNode extends CompoundNode +/** @extends CompoundNode */ +class InlineCompoundNode extends CompoundNode implements InlineNodeInterface { public function toString(): string { @@ -33,4 +33,9 @@ public static function getPlainTextInlineNode(string $content): self { return new InlineCompoundNode([new PlainTextInlineNode($content)]); } + + public function getType(): string + { + return 'compound'; + } } diff --git a/src/Nodes/ListItemNode.php b/src/Nodes/ListItemNode.php index 44ec8553..3cf9d11a 100644 --- a/src/Nodes/ListItemNode.php +++ b/src/Nodes/ListItemNode.php @@ -25,6 +25,8 @@ public function __construct( private readonly string $prefix, private readonly bool $ordered, array $contents, + private readonly string|null $orderNumber = null, + private readonly string|null $orderType = null, ) { parent::__construct($contents); } @@ -38,4 +40,14 @@ public function isOrdered(): bool { return $this->ordered; } + + public function getOrderNumber(): string|null + { + return $this->orderNumber; + } + + public function getOrderType(): string|null + { + return $this->orderType; + } } diff --git a/src/Nodes/ListNode.php b/src/Nodes/ListNode.php index ef9b9b1b..4cc76f5a 100644 --- a/src/Nodes/ListNode.php +++ b/src/Nodes/ListNode.php @@ -17,11 +17,39 @@ final class ListNode extends CompoundNode { /** @param ListItemNode[] $items */ - public function __construct(array $items, private readonly bool $ordered = false) - { + public function __construct( + array $items, + private readonly bool $ordered = false, + private string|null $start = null, + private string|null $orderingType = null, + ) { parent::__construct($items); } + public function getStart(): string|null + { + return $this->start; + } + + public function setStart(string|null $start): ListNode + { + $this->start = $start; + + return $this; + } + + public function getOrderingType(): string|null + { + return $this->orderingType; + } + + public function setOrderingType(string|null $orderingType): ListNode + { + $this->orderingType = $orderingType; + + return $this; + } + public function isOrdered(): bool { return $this->ordered; diff --git a/src/Nodes/MathNode.php b/src/Nodes/MathNode.php new file mode 100644 index 00000000..be291304 --- /dev/null +++ b/src/Nodes/MathNode.php @@ -0,0 +1,25 @@ +hasOption('depth') && is_scalar($this->getOption('depth'))) { @@ -31,4 +33,17 @@ public function isPageLevelOnly(): bool { return false; } + + public function isLocal(): bool + { + return $this->local; + } + + public function withLocal(bool $local): ContentMenuNode + { + $that = clone $this; + $that->local = $local; + + return $that; + } } diff --git a/src/Nodes/Menu/InternalMenuEntryNode.php b/src/Nodes/Menu/InternalMenuEntryNode.php index b81eba96..dededf72 100644 --- a/src/Nodes/Menu/InternalMenuEntryNode.php +++ b/src/Nodes/Menu/InternalMenuEntryNode.php @@ -57,7 +57,7 @@ public function getEntries(): array return $this->children; } - public function addMenuEntry(InternalMenuEntryNode $menuEntryNode): void + public function addMenuEntry(MenuEntryNode $menuEntryNode): void { $this->children[] = $menuEntryNode; } diff --git a/src/Nodes/Metadata/NavigationTitleNode.php b/src/Nodes/Metadata/NavigationTitleNode.php new file mode 100644 index 00000000..95cb0b65 --- /dev/null +++ b/src/Nodes/Metadata/NavigationTitleNode.php @@ -0,0 +1,26 @@ +citationTargets[$name] ?? null; } + /** @throws DuplicateLinkAnchorException */ public function addLinkTarget(string $anchorName, InternalTarget $target): void { - if (!isset($this->internalLinkTargets[$target->getLinkType()])) { - $this->internalLinkTargets[$target->getLinkType()] = []; + $linkType = $target->getLinkType(); + if (!isset($this->internalLinkTargets[$linkType])) { + $this->internalLinkTargets[$linkType] = []; } - $this->internalLinkTargets[$target->getLinkType()][$anchorName] = $target; + if (isset($this->internalLinkTargets[$linkType][$anchorName]) && $linkType !== 'std:title') { + throw new DuplicateLinkAnchorException(sprintf('Duplicate anchor "%s". There is already another anchor of that name in document "%s"', $anchorName, $this->internalLinkTargets[$linkType][$anchorName]->getDocumentPath())); + } + + $this->internalLinkTargets[$linkType][$anchorName] = $target; + } + + public function hasInternalTarget(string $anchorName, string $linkType = SectionNode::STD_LABEL): bool + { + return isset($this->internalLinkTargets[$linkType][$anchorName]); } public function getInternalTarget(string $anchorName, string $linkType = SectionNode::STD_LABEL): InternalTarget|null diff --git a/src/Nodes/RawNode.php b/src/Nodes/RawNode.php index c55a4c17..74fc82e9 100644 --- a/src/Nodes/RawNode.php +++ b/src/Nodes/RawNode.php @@ -15,4 +15,10 @@ final class RawNode extends TextNode { + public function __construct(string $contents, string $format = 'html') + { + parent::__construct($contents); + + $this->options['format'] = $format; + } } diff --git a/src/Nodes/SectionNode.php b/src/Nodes/SectionNode.php index 49050270..0efcb48f 100644 --- a/src/Nodes/SectionNode.php +++ b/src/Nodes/SectionNode.php @@ -19,6 +19,7 @@ final class SectionNode extends CompoundNode implements LinkTargetNode { public const STD_LABEL = 'std:label'; + public const STD_TITLE = 'std:title'; public function __construct(private readonly TitleNode $title) { diff --git a/src/Nodes/Table/TableColumn.php b/src/Nodes/Table/TableColumn.php index 34afc2fb..35970b47 100644 --- a/src/Nodes/Table/TableColumn.php +++ b/src/Nodes/Table/TableColumn.php @@ -54,7 +54,7 @@ public function getRowSpan(): int public function addContent(string $content): void { - $this->content = trim($this->content . $content); + $this->content .= $content; } public function incrementRowSpan(): void diff --git a/src/Nodes/Table/TableRow.php b/src/Nodes/Table/TableRow.php index 81e00448..8cc37bbc 100644 --- a/src/Nodes/Table/TableRow.php +++ b/src/Nodes/Table/TableRow.php @@ -23,8 +23,10 @@ final class TableRow { - /** @var TableColumn[] */ - private array $columns = []; + /** @param TableColumn[] $columns */ + public function __construct(private array $columns = []) + { + } public function addColumn(TableColumn $tableColumn): void { diff --git a/src/Nodes/TitleNode.php b/src/Nodes/TitleNode.php index 628f0599..e9f59cd7 100644 --- a/src/Nodes/TitleNode.php +++ b/src/Nodes/TitleNode.php @@ -40,6 +40,13 @@ public function getLevel(): int return $this->level; } + public function setLevel(int $level): TitleNode + { + $this->level = $level; + + return $this; + } + public function setTarget(string $target): void { $this->target = $target; diff --git a/src/ReferenceResolvers/AnchorHyperlinkResolver.php b/src/ReferenceResolvers/AnchorHyperlinkResolver.php index c2a23fbc..ed8757d3 100644 --- a/src/ReferenceResolvers/AnchorHyperlinkResolver.php +++ b/src/ReferenceResolvers/AnchorHyperlinkResolver.php @@ -15,6 +15,7 @@ use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode; use phpDocumentor\Guides\Nodes\Inline\LinkInlineNode; +use phpDocumentor\Guides\Nodes\SectionNode; use phpDocumentor\Guides\RenderContext; use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface; @@ -43,7 +44,10 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess $target = $renderContext->getProjectNode()->getInternalTarget($reducedAnchor); if ($target === null) { - return false; + $target = $renderContext->getProjectNode()->getInternalTarget($reducedAnchor, SectionNode::STD_TITLE); + if ($target === null) { + return false; + } } $node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $target->getDocumentPath(), $target->getAnchor())); diff --git a/src/ReferenceResolvers/AnchorReferenceResolver.php b/src/ReferenceResolvers/AnchorReferenceResolver.php index 6d099fb7..4bead238 100644 --- a/src/ReferenceResolvers/AnchorReferenceResolver.php +++ b/src/ReferenceResolvers/AnchorReferenceResolver.php @@ -46,7 +46,7 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess return false; } - $node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $target->getDocumentPath(), $target->getAnchor())); + $node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $target->getDocumentPath(), $target->getPrefix() . $target->getAnchor())); if ($node->getValue() === '') { $node->setValue($target->getTitle() ?? ''); } diff --git a/src/ReferenceResolvers/DocReferenceResolver.php b/src/ReferenceResolvers/DocReferenceResolver.php index 7176d4d1..0de9e76b 100644 --- a/src/ReferenceResolvers/DocReferenceResolver.php +++ b/src/ReferenceResolvers/DocReferenceResolver.php @@ -18,7 +18,9 @@ use phpDocumentor\Guides\RenderContext; use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface; +use function explode; use function sprintf; +use function str_contains; final class DocReferenceResolver implements ReferenceResolver { @@ -40,7 +42,15 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess return false; } - $canonicalDocumentName = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $node->getTargetReference()); + $targetReference = $node->getTargetReference(); + $anchor = ''; + if (str_contains($targetReference, '#')) { + $exploded = explode('#', $targetReference, 2); + $targetReference = $exploded[0]; + $anchor = '#' . $exploded[1]; + } + + $canonicalDocumentName = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $targetReference); $document = $renderContext->getProjectNode()->findDocumentEntry($canonicalDocumentName); if ($document === null) { @@ -53,7 +63,7 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess return false; } - $node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $document->getFile())); + $node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $document->getFile()) . $anchor); if ($node->getValue() === '') { $node->setValue($document->getTitle()->toString()); } diff --git a/src/ReferenceResolvers/ImageReferenceResolverPreRender.php b/src/ReferenceResolvers/ImageReferenceResolverPreRender.php new file mode 100644 index 00000000..bb211a42 --- /dev/null +++ b/src/ReferenceResolvers/ImageReferenceResolverPreRender.php @@ -0,0 +1,62 @@ +getTarget() === null) { + return $node; + } + + $referenceLinkNode = $node->getTarget(); + $messages = new Messages(); + $resolved = $this->referenceResolver->resolve($referenceLinkNode, $renderContext, $messages); + if (!$resolved) { + $this->logger->warning( + $messages->getLastWarning()?->getMessage() ?? sprintf( + 'Target %s of image could not be resolved in %s', + $referenceLinkNode->getTargetReference(), + $renderContext->getCurrentFileName(), + ), + array_merge($renderContext->getLoggerInformation(), $messages->getLastWarning()?->getDebugInfo() ?? []), + ); + } + + return $node; + } +} diff --git a/src/ReferenceResolvers/Interlink/InventoryGroup.php b/src/ReferenceResolvers/Interlink/InventoryGroup.php index aa1ed907..38f32140 100644 --- a/src/ReferenceResolvers/Interlink/InventoryGroup.php +++ b/src/ReferenceResolvers/Interlink/InventoryGroup.php @@ -14,6 +14,7 @@ namespace phpDocumentor\Guides\ReferenceResolvers\Interlink; use phpDocumentor\Guides\Nodes\Inline\CrossReferenceNode; +use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode; use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer; use phpDocumentor\Guides\ReferenceResolvers\Message; use phpDocumentor\Guides\ReferenceResolvers\Messages; @@ -21,7 +22,9 @@ use function array_key_exists; use function array_merge; +use function explode; use function sprintf; +use function str_contains; final class InventoryGroup { @@ -47,7 +50,15 @@ public function hasLink(string $key): bool public function getLink(CrossReferenceNode $node, RenderContext $renderContext, Messages $messages): InventoryLink|null { - $reducedKey = $this->anchorNormalizer->reduceAnchor($node->getTargetReference()); + $targetReference = $node->getTargetReference(); + $anchor = ''; + if ($node instanceof DocReferenceNode && str_contains($targetReference, '#')) { + $exploded = explode('#', $targetReference, 2); + $targetReference = $exploded[0]; + $anchor = '#' . $exploded[1]; + } + + $reducedKey = $this->anchorNormalizer->reduceAnchor($targetReference); if (!array_key_exists($reducedKey, $this->links)) { $messages->addWarning( new Message( @@ -64,6 +75,11 @@ public function getLink(CrossReferenceNode $node, RenderContext $renderContext, return null; } - return $this->links[$reducedKey]; + $link = $this->links[$reducedKey]; + if ($anchor !== '') { + $link = $link->withPath($link->getPath() . $anchor); + } + + return $link; } } diff --git a/src/ReferenceResolvers/Interlink/InventoryLink.php b/src/ReferenceResolvers/Interlink/InventoryLink.php index ab3851f4..98672355 100644 --- a/src/ReferenceResolvers/Interlink/InventoryLink.php +++ b/src/ReferenceResolvers/Interlink/InventoryLink.php @@ -22,10 +22,10 @@ final class InventoryLink public function __construct( private readonly string $project, private readonly string $version, - private readonly string $path, + private string $path, private readonly string $title, ) { - if (preg_match('/^([a-zA-Z0-9-_.]+\/)*([a-zA-Z0-9-_.])+\.html(#[^#]*)?$/', $path) < 1) { + if (preg_match('/^([a-zA-Z0-9-_.]+\/)*([a-zA-Z0-9-_.]+)(\.html)?(#[^#]*)?$/', $path) < 1) { throw new InvalidInventoryLink('Inventory link "' . $path . '" has an invalid scheme. ', 1_671_398_986); } } @@ -49,4 +49,12 @@ public function getTitle(): string { return $this->title; } + + public function withPath(string $path): InventoryLink + { + $that = clone$this; + $that->path = $path; + + return $that; + } } diff --git a/src/ReferenceResolvers/TitleReferenceResolver.php b/src/ReferenceResolvers/TitleReferenceResolver.php new file mode 100644 index 00000000..05e5d728 --- /dev/null +++ b/src/ReferenceResolvers/TitleReferenceResolver.php @@ -0,0 +1,62 @@ +getInterlinkDomain() !== '') { + return false; + } + + $reducedAnchor = $this->anchorReducer->reduceAnchor($node->getTargetReference()); + $target = $renderContext->getProjectNode()->getInternalTarget($reducedAnchor, SectionNode::STD_TITLE); + + if ($target === null) { + return false; + } + + $node->setUrl($this->urlGenerator->generateCanonicalOutputUrl($renderContext, $target->getDocumentPath(), $target->getPrefix() . $target->getAnchor())); + if ($node->getValue() === '') { + $node->setValue($target->getTitle() ?? ''); + } + + return true; + } + + public static function getPriority(): int + { + return self::PRIORITY; + } +} diff --git a/src/RenderContext.php b/src/RenderContext.php index 1db91fe6..813100ad 100644 --- a/src/RenderContext.php +++ b/src/RenderContext.php @@ -82,6 +82,17 @@ public function withDocument(DocumentNode $documentNode): self )->withIterator($this->getIterator()); } + public function getDocument(): DocumentNode + { + return $this->document; + } + + /** @return DocumentNode[] */ + public function getAllDocuments(): array + { + return $this->allDocuments; + } + public function withIterator(Renderer\DocumentListIterator $iterator): self { $that = clone $this; @@ -142,7 +153,7 @@ public function getDirName(): string return $dirname; } - + public function hasCurrentFileName(): bool { return $this->currentFileName !== null; diff --git a/src/Renderer/DocumentTreeIterator.php b/src/Renderer/DocumentTreeIterator.php index aa46bae6..30266415 100644 --- a/src/Renderer/DocumentTreeIterator.php +++ b/src/Renderer/DocumentTreeIterator.php @@ -77,6 +77,8 @@ public function hasChildren(): bool public function getChildren(): self|null { - return new self($this->levelNodes[$this->position]->getChildren(), $this->documents); + $children = $this->levelNodes[$this->position]->getChildren(); + + return new self($children, $this->documents); } } diff --git a/src/Renderer/InterlinkObjectsRenderer.php b/src/Renderer/InterlinkObjectsRenderer.php index 189e4823..fbef3372 100644 --- a/src/Renderer/InterlinkObjectsRenderer.php +++ b/src/Renderer/InterlinkObjectsRenderer.php @@ -14,6 +14,7 @@ namespace phpDocumentor\Guides\Renderer; use phpDocumentor\Guides\Handlers\RenderCommand; +use phpDocumentor\Guides\Meta\InternalTarget; use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface; use phpDocumentor\Guides\RenderContext; use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface; @@ -73,6 +74,13 @@ public function render(RenderCommand $renderCommand): void '', $this->urlGenerator->createFileUrl($context, $internalTarget->getDocumentPath(), $internalTarget->getAnchor()), ); + if ($internalTarget instanceof InternalTarget) { + $url = $this->documentNameResolver->canonicalUrl( + '', + $this->urlGenerator->createFileUrl($context, $internalTarget->getDocumentPath(), $internalTarget->getPrefix() . $internalTarget->getAnchor()), + ); + } + $inventory[$linkType][$key] = [ $projectNode->getTitle() ?? '-', $projectNode->getVersion() ?? '-', diff --git a/src/Settings/ProjectSettings.php b/src/Settings/ProjectSettings.php index 84b5272a..08bdfd3e 100644 --- a/src/Settings/ProjectSettings.php +++ b/src/Settings/ProjectSettings.php @@ -24,6 +24,7 @@ final class ProjectSettings private string $theme = 'default'; private string $input = 'docs'; private string $inputFile = ''; + private string $indexName = 'index,Index'; private string $output = 'output'; private string $inputFormat = 'rst'; /** @var string[] */ @@ -34,6 +35,7 @@ final class ProjectSettings private bool $linksRelative = false; private string $defaultCodeLanguage = ''; private int $maxMenuDepth = 0; + private bool $automaticMenu = false; /** @var string[] */ private array $ignoredDomains = []; @@ -230,4 +232,28 @@ public function setIgnoredDomains(array $ignoredDomains): void { $this->ignoredDomains = $ignoredDomains; } + + public function getIndexName(): string + { + return $this->indexName; + } + + public function setIndexName(string $indexName): ProjectSettings + { + $this->indexName = $indexName; + + return $this; + } + + public function isAutomaticMenu(): bool + { + return $this->automaticMenu; + } + + public function setAutomaticMenu(bool $automaticMenu): ProjectSettings + { + $this->automaticMenu = $automaticMenu; + + return $this; + } } diff --git a/src/Twig/AssetsExtension.php b/src/Twig/AssetsExtension.php index 35944740..1bcb3fee 100644 --- a/src/Twig/AssetsExtension.php +++ b/src/Twig/AssetsExtension.php @@ -21,7 +21,6 @@ use phpDocumentor\Guides\Meta\Target; use phpDocumentor\Guides\NodeRenderers\NodeRenderer; use phpDocumentor\Guides\Nodes\BreadCrumbNode; -use phpDocumentor\Guides\Nodes\CollectionNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface; use phpDocumentor\Guides\RenderContext; @@ -29,7 +28,6 @@ use Psr\Log\LoggerInterface; use RuntimeException; use Stringable; -use Throwable; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; use Twig\TwigTest; @@ -39,6 +37,8 @@ final class AssetsExtension extends AbstractExtension { + private GlobalMenuExtension $menuExtension; + /** @param NodeRenderer $nodeRenderer */ public function __construct( private readonly LoggerInterface $logger, @@ -46,6 +46,7 @@ public function __construct( private readonly DocumentNameResolverInterface $documentNameResolver, private readonly UrlGeneratorInterface $urlGenerator, ) { + $this->menuExtension = new GlobalMenuExtension($this->nodeRenderer); } /** @return TwigFunction[] */ @@ -56,8 +57,9 @@ public function getFunctions(): array new TwigFunction('renderNode', $this->renderNode(...), ['is_safe' => ['html'], 'needs_context' => true]), new TwigFunction('renderLink', $this->renderLink(...), ['is_safe' => ['html'], 'needs_context' => true]), new TwigFunction('renderBreadcrumb', $this->renderBreadcrumb(...), ['is_safe' => ['html'], 'needs_context' => true]), - new TwigFunction('renderMenu', $this->renderMenu(...), ['is_safe' => ['html'], 'needs_context' => true]), + new TwigFunction('renderMenu', $this->renderMenu(...), ['is_safe' => ['html'], 'needs_context' => true, 'deprecated' => true]), new TwigFunction('renderTarget', $this->renderTarget(...), ['is_safe' => ['html'], 'needs_context' => true]), + new TwigFunction('renderOrderedListType', $this->renderOrderedListType(...), ['is_safe' => ['html'], 'needs_context' => false]), ]; } @@ -136,22 +138,7 @@ public function renderBreadcrumb(array $context): string /** @param array{env: RenderContext} $context */ public function renderMenu(array $context, string $menuType, int $maxMenuCount = 0): string { - $renderContext = $this->getRenderContext($context); - $globalMenues = $renderContext->getProjectNode()->getGlobalMenues(); - $menues = []; - foreach ($globalMenues as $menu) { - $menu = $menu->withOptions(['menu' => $menuType]); - try { - $menu = $menu->withCurrentPath($renderContext->getCurrentFileName()); - $menu = $menu->withRootlinePaths($renderContext->getCurrentFileRootline()); - } catch (Throwable) { - // do nothing, we are in a context without active menu like single page or functional test - } - - $menues[] = $menu; - } - - return $this->nodeRenderer->render(new CollectionNode($menues), $renderContext); + return $this->menuExtension->renderMenu($context, $menuType); } /** @param array{env: RenderContext} $context */ @@ -169,25 +156,27 @@ private function copyAsset( } $canonicalUrl = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $sourcePath); + $normalizedSourcePath = $this->documentNameResolver->canonicalUrl($renderContext->getDirName(), $sourcePath); + $outputPath = $this->documentNameResolver->absoluteUrl( $renderContext->getDestinationPath(), $canonicalUrl, ); try { - if ($renderContext->getOrigin()->has($sourcePath) === false) { + if ($renderContext->getOrigin()->has($normalizedSourcePath) === false) { $this->logger->error( - sprintf('Image reference not found "%s"', $sourcePath), + sprintf('Image reference not found "%s"', $normalizedSourcePath), $renderContext->getLoggerInformation(), ); return $outputPath; } - $fileContents = $renderContext->getOrigin()->read($sourcePath); + $fileContents = $renderContext->getOrigin()->read($normalizedSourcePath); if ($fileContents === false) { $this->logger->error( - sprintf('Could not read image file "%s"', $sourcePath), + sprintf('Could not read image file "%s"', $normalizedSourcePath), $renderContext->getLoggerInformation(), ); @@ -221,4 +210,22 @@ private function getRenderContext(array $context): RenderContext return $renderContext; } + + public function renderOrderedListType(string $listType): string + { + switch ($listType) { + case 'numberdot': + case 'numberparentheses': + case 'numberright-parenthesis': + return '1'; + + case 'romandot': + case 'romanparentheses': + case 'romanright-parenthesis': + return 'i'; + + default: + return 'a'; + } + } } diff --git a/src/Twig/GlobalMenuExtension.php b/src/Twig/GlobalMenuExtension.php new file mode 100644 index 00000000..6cf4f5b5 --- /dev/null +++ b/src/Twig/GlobalMenuExtension.php @@ -0,0 +1,83 @@ + + */ + private array $menuCache = []; + + /** @param NodeRenderer $nodeRenderer */ + public function __construct( + private readonly NodeRenderer $nodeRenderer, + ) { + } + + /** @return TwigFunction[] */ + public function getFunctions(): array + { + return [ + new TwigFunction('renderMenu', $this->renderMenu(...), ['is_safe' => ['html'], 'needs_context' => true]), + ]; + } + + /** @param array{env: RenderContext} $context */ + public function renderMenu(array $context, string $menuType): string + { + $renderContext = $this->getRenderContext($context); + $globalMenues = $renderContext->getProjectNode()->getGlobalMenues(); + if (isset($this->menuCache[$renderContext->getCurrentFileName() . '::' . $menuType])) { + return $this->menuCache[$renderContext->getCurrentFileName() . '::' . $menuType]; + } + + $menues = []; + foreach ($globalMenues as $menu) { + $menu = $menu->withOptions(['menu' => $menuType]); + try { + $menu = $menu->withCurrentPath($renderContext->getCurrentFileName()); + $menu = $menu->withRootlinePaths($renderContext->getCurrentFileRootline()); + } catch (Throwable) { + // do nothing, we are in a context without active menu like single page or functional test + } + + $menues[] = $menu; + } + + return $this->menuCache[$renderContext->getCurrentFileName() . '::' . $menuType] = $this->nodeRenderer->render(new CollectionNode($menues), $renderContext); + } + + /** @param array{env: RenderContext} $context */ + private function getRenderContext(array $context): RenderContext + { + $renderContext = $context['env'] ?? null; + if (!$renderContext instanceof RenderContext) { + throw new RuntimeException('Render context must be set in the twig global state to render nodes'); + } + + return $renderContext; + } +} diff --git a/src/Twig/TrimFilesystemLoader.php b/src/Twig/TrimFilesystemLoader.php new file mode 100644 index 00000000..f0e93b1e --- /dev/null +++ b/src/Twig/TrimFilesystemLoader.php @@ -0,0 +1,41 @@ +getCode()) ?? $source->getCode(), + $source->getName(), + $source->getPath(), + ); + } +} diff --git a/tests/unit/Compiler/CompilerContextTest.php b/tests/unit/Compiler/CompilerContextTest.php new file mode 100644 index 00000000..075bee7a --- /dev/null +++ b/tests/unit/Compiler/CompilerContextTest.php @@ -0,0 +1,36 @@ +expectDeprecationWithIdentifier('https://github.com/phpDocumentor/guides/issues/971'); + $context = new class (new ProjectNode()) extends CompilerContext{ + }; + } + + public function testNoDeprecationOnNormalConstruct(): void + { + $this->expectNoDeprecationWithIdentifier('https://github.com/phpDocumentor/guides/issues/971'); + $context = new CompilerContext(new ProjectNode()); + } +} diff --git a/tests/unit/Interlink/InventoryGroupTest.php b/tests/unit/Interlink/InventoryGroupTest.php new file mode 100644 index 00000000..371cbebf --- /dev/null +++ b/tests/unit/Interlink/InventoryGroupTest.php @@ -0,0 +1,68 @@ +inventoryGroup = new InventoryGroup(new NullAnchorNormalizer()); + $this->renderContext = $this->createMock(RenderContext::class); + } + + #[DataProvider('linkProvider')] + public function testGetLinkFromInterlinkGroup(string $expected, string $input, string $path): void + { + $this->inventoryGroup->addLink($path, new InventoryLink('', '', $path . '.html', '')); + $messages = new Messages(); + $link = $this->inventoryGroup->getLink( + new DocReferenceNode($input, '', 'interlink'), + $this->renderContext, + $messages, + ); + self::assertEmpty($messages->getWarnings()); + self::assertEquals($expected, $link?->getPath()); + } + + /** @return string[][] */ + public static function linkProvider(): array + { + return [ + 'plain' => [ + 'expected' => 'some-document.html', + 'input' => 'some-document', + 'path' => 'some-document', + ], + 'withAnchor' => [ + 'expected' => 'some-document.html#anchor', + 'input' => 'some-document#anchor', + 'path' => 'some-document', + ], + ]; + } +} diff --git a/tests/unit/Interlink/InventoryLinkTest.php b/tests/unit/Interlink/InventoryLinkTest.php index a858cafa..689aebd9 100644 --- a/tests/unit/Interlink/InventoryLinkTest.php +++ b/tests/unit/Interlink/InventoryLinkTest.php @@ -54,13 +54,6 @@ public function testLinkMayContaintDot(): void self::assertEquals($inventoryLink->getPath(), $link); } - public function testPhpLinkThrowsError(): void - { - $link = 'Some/Path/SomeThing.php#anchor'; - $this->expectException(InvalidInventoryLink::class); - new InventoryLink('', '', $link, ''); - } - public function testJavaScriptLinkThrowsError(): void { $link = 'javascript:alert()'; diff --git a/tests/unit/ReferenceResolvers/DocReferenceResolverTest.php b/tests/unit/ReferenceResolvers/DocReferenceResolverTest.php new file mode 100644 index 00000000..d55af447 --- /dev/null +++ b/tests/unit/ReferenceResolvers/DocReferenceResolverTest.php @@ -0,0 +1,74 @@ +projectNode = new ProjectNode('some-name'); + $this->projectNode->addDocumentEntry($documentEntry); + $this->renderContext = $this->createMock(RenderContext::class); + $this->renderContext->expects(self::once())->method('getProjectNode')->willReturn($this->projectNode); + $this->documentNameResolver = self::createMock(DocumentNameResolverInterface::class); + $this->urlGenerator = self::createMock(UrlGeneratorInterface::class); + $this->subject = new DocReferenceResolver($this->urlGenerator, $this->documentNameResolver); + } + + #[DataProvider('pathProvider')] + public function testDocumentReducer(string $expected, string $input, string $path): void + { + $this->documentNameResolver->expects(self::once())->method('canonicalUrl')->with('', $path)->willReturn($path); + $input = new DocReferenceNode($input); + $this->urlGenerator->expects(self::once())->method('generateCanonicalOutputUrl')->willReturn($path); + $messages = new Messages(); + self::assertTrue($this->subject->resolve($input, $this->renderContext, $messages)); + self::assertEmpty($messages->getWarnings()); + self::assertEquals($expected, $input->getUrl()); + } + + /** @return string[][] */ + public static function pathProvider(): array + { + return [ + 'plain' => [ + 'expected' => 'some-document', + 'input' => 'some-document', + 'path' => 'some-document', + ], + 'withAnchor' => [ + 'expected' => 'some-document#anchor', + 'input' => 'some-document#anchor', + 'path' => 'some-document', + ], + ]; + } +} diff --git a/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php b/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php new file mode 100644 index 00000000..60e16547 --- /dev/null +++ b/tests/unit/ReferenceResolvers/InterlinkReferenceResolverTest.php @@ -0,0 +1,70 @@ +renderContext = $this->createMock(RenderContext::class); + $this->inventoryRepository = $this->createMock(InventoryRepository::class); + $this->anchorNormalizer = new NullAnchorNormalizer(); + $this->subject = new InterlinkReferenceResolver($this->inventoryRepository); + } + + #[DataProvider('pathProvider')] + public function testDocumentReducer(string $expected, string $input, string $path): void + { + $input = new DocReferenceNode($input, '', 'interlink-target'); + $inventoryLink = new InventoryLink('project', '1.0', $path, ''); + $inventory = new Inventory('base-url/', $this->anchorNormalizer); + $this->inventoryRepository->expects(self::once())->method('getInventory')->willReturn($inventory); + $this->inventoryRepository->expects(self::once())->method('getLink')->willReturn($inventoryLink); + $messages = new Messages(); + self::assertTrue($this->subject->resolve($input, $this->renderContext, $messages)); + self::assertEmpty($messages->getWarnings()); + self::assertEquals($expected, $input->getUrl()); + } + + /** @return string[][] */ + public static function pathProvider(): array + { + return [ + 'plain' => [ + 'expected' => 'base-url/some-document.html', + 'input' => 'some-document', + 'path' => 'some-document.html', + ], + 'withAnchor' => [ + 'expected' => 'base-url/some-document.html#anchor', + 'input' => 'some-document#anchor', + 'path' => 'some-document.html#anchor', + ], + ]; + } +}