Skip to content

Commit 0af0c7d

Browse files
authored
Merge pull request #547 from phpDocumentor/feature-breadcrumb
[FEATURE] Introduce Breadcrumb
2 parents 02d6364 + ea166c2 commit 0af0c7d

File tree

33 files changed

+556
-16
lines changed

33 files changed

+556
-16
lines changed

packages/guides-restructured-text/resources/config/guides-restructured-text.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use phpDocumentor\Guides\RestructuredText\Directives\AdmonitionDirective;
88
use phpDocumentor\Guides\RestructuredText\Directives\AttentionDirective;
99
use phpDocumentor\Guides\RestructuredText\Directives\BaseDirective;
10+
use phpDocumentor\Guides\RestructuredText\Directives\BreadcrumbDirective;
1011
use phpDocumentor\Guides\RestructuredText\Directives\CautionDirective;
1112
use phpDocumentor\Guides\RestructuredText\Directives\ClassDirective;
1213
use phpDocumentor\Guides\RestructuredText\Directives\CodeBlockDirective;
@@ -143,6 +144,7 @@
143144

144145
->set(AdmonitionDirective::class)
145146
->set(AttentionDirective::class)
147+
->set(BreadcrumbDirective::class)
146148
->set(CautionDirective::class)
147149
->set(ClassDirective::class)
148150
->set(CodeBlockDirective::class)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\RestructuredText\Directives;
6+
7+
use phpDocumentor\Guides\Nodes\BreadCrumbNode;
8+
use phpDocumentor\Guides\Nodes\Node;
9+
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
10+
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
11+
12+
/**
13+
* The "breadcrumb" directive displays a breadcrumb of the current. It does not exist in Sphinx or the
14+
* reST standard yet.
15+
*
16+
* It takes neither arguments nor content.
17+
*
18+
* Usage:
19+
*
20+
* ```
21+
* .. breadcrumb::
22+
* ```
23+
*/
24+
class BreadcrumbDirective extends BaseDirective
25+
{
26+
public function getName(): string
27+
{
28+
return 'breadcrumb';
29+
}
30+
31+
public function processNode(
32+
BlockContext $blockContext,
33+
Directive $directive,
34+
): Node {
35+
return new BreadCrumbNode();
36+
}
37+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
<nav aria-label="breadcrumb">
3+
<ol class="breadcrumb">
4+
{% for entry in rootline -%}
5+
<li class="breadcrumb-item"><a href="{{ renderLink(entry.url) }}">{{ entry.value.toString }}</a></li>
6+
{% endfor %}
7+
</ol>
8+
</nav>

packages/guides-theme-bootstrap/resources/template/structure/document.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525

2626
{% block breadcrumb %}
27-
{% include "structure/navigation/breadcrumb.html.twig" %}
27+
{{ renderBreadcrumb() }}
2828
{% endblock %}
2929

3030
{% block content %}

packages/guides-theme-bootstrap/resources/template/structure/navigation/breadcrumb.html.twig

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/guides/resources/config/guides.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use phpDocumentor\Guides\Intersphinx\JsonLoader;
1515
use phpDocumentor\Guides\NodeRenderers\DefaultNodeRenderer;
1616
use phpDocumentor\Guides\NodeRenderers\DelegatingNodeRenderer;
17+
use phpDocumentor\Guides\NodeRenderers\Html\BreadCrumbNodeRenderer;
1718
use phpDocumentor\Guides\NodeRenderers\Html\DocumentNodeRenderer;
1819
use phpDocumentor\Guides\NodeRenderers\Html\MenuEntryRenderer;
1920
use phpDocumentor\Guides\NodeRenderers\Html\MenuNodeRenderer;
@@ -152,6 +153,8 @@
152153
->tag('phpdoc.guides.noderenderer.html')
153154
->set(MenuEntryRenderer::class)
154155
->tag('phpdoc.guides.noderenderer.html')
156+
->set(BreadCrumbNodeRenderer::class)
157+
->tag('phpdoc.guides.noderenderer.html')
155158

156159
->set(DefaultNodeRenderer::class)
157160

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<nav aria-label="breadcrumb">
2+
{% for entry in rootline -%}
3+
{%- if entry.current %}
4+
<span class="breadcrumb active level-{{ entry.level }}">{{ entry.value.toString }}</span>
5+
{% else -%}
6+
<a href="{{ renderLink(entry.url) }}"
7+
class="breadcrumb level-{{ entry.level }}">{{ entry.value.toString }}</a> &gt;
8+
{% endif -%}
9+
{% endfor %}
10+
</nav>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\NodeRenderers\Html;
6+
7+
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
8+
use phpDocumentor\Guides\Nodes\BreadCrumbNode;
9+
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
10+
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
11+
use phpDocumentor\Guides\Nodes\Node;
12+
use phpDocumentor\Guides\RenderContext;
13+
use phpDocumentor\Guides\TemplateRenderer;
14+
15+
use function array_reverse;
16+
use function assert;
17+
18+
/**
19+
* @template T as Node
20+
* @implements NodeRenderer<BreadCrumbNode>
21+
*/
22+
class BreadCrumbNodeRenderer implements NodeRenderer
23+
{
24+
private string $template = 'body/menu/breadcrumb.html.twig';
25+
26+
public function __construct(
27+
private readonly TemplateRenderer $renderer,
28+
) {
29+
}
30+
31+
public function supports(Node $node): bool
32+
{
33+
return $node instanceof BreadCrumbNode;
34+
}
35+
36+
/** @param T $node */
37+
public function render(Node $node, RenderContext $renderContext): string
38+
{
39+
assert($node instanceof BreadCrumbNode);
40+
$documentEntry = $renderContext->getCurrentDocumentEntry();
41+
$data = [
42+
'node' => $node,
43+
'rootline' => $documentEntry === null ? [] :
44+
$this->buildBreadcrumb(
45+
$node,
46+
$renderContext,
47+
$documentEntry,
48+
[],
49+
$this->getBreadcrumbMaxLevel($node, $renderContext, $documentEntry, 0),
50+
true,
51+
),
52+
];
53+
54+
return $this->renderer->renderTemplate(
55+
$renderContext,
56+
$this->template,
57+
$data,
58+
);
59+
}
60+
61+
private function getBreadcrumbMaxLevel(
62+
BreadCrumbNode $node,
63+
RenderContext $renderContext,
64+
DocumentEntryNode $documentEntry,
65+
int $level,
66+
): int {
67+
if ($documentEntry->getParent() === null) {
68+
if ($documentEntry->isRoot()) {
69+
return $level;
70+
}
71+
72+
// Current document has no parent but is not the root, add the overall root to the breadcrumb
73+
$entries = $renderContext->getProjectNode()->getAllDocumentEntries();
74+
foreach ($entries as $entry) {
75+
if ($entry->isRoot() && $entry->getParent() === null) {
76+
return $this->getBreadcrumbMaxLevel($node, $renderContext, $entry, ++$level);
77+
}
78+
}
79+
80+
return $level;
81+
}
82+
83+
return $this->getBreadcrumbMaxLevel($node, $renderContext, $documentEntry->getParent(), ++$level);
84+
}
85+
86+
/**
87+
* @param MenuEntryNode[] $currentBreadcrumb
88+
*
89+
* @return MenuEntryNode[]
90+
*/
91+
private function buildBreadcrumb(
92+
BreadCrumbNode $node,
93+
RenderContext $renderContext,
94+
DocumentEntryNode $documentEntry,
95+
array $currentBreadcrumb,
96+
int $level,
97+
bool $isCurrent,
98+
): array {
99+
$entry = new MenuEntryNode(
100+
$documentEntry->getFile(),
101+
$documentEntry->getTitle(),
102+
[],
103+
false,
104+
$level,
105+
'',
106+
true,
107+
$isCurrent,
108+
);
109+
$currentBreadcrumb[] = $entry;
110+
if ($documentEntry->getParent() === null) {
111+
if ($documentEntry->isRoot()) {
112+
return array_reverse($currentBreadcrumb);
113+
}
114+
115+
// Current document has no parent but is not the root, add the overall root to the breadcrumb
116+
$entries = $renderContext->getProjectNode()->getAllDocumentEntries();
117+
foreach ($entries as $entry) {
118+
if ($entry->isRoot() && $entry->getParent() === null) {
119+
return $this->buildBreadcrumb($node, $renderContext, $entry, $currentBreadcrumb, --$level, false);
120+
}
121+
}
122+
123+
return array_reverse($currentBreadcrumb);
124+
}
125+
126+
return $this->buildBreadcrumb($node, $renderContext, $documentEntry->getParent(), $currentBreadcrumb, --$level, false);
127+
}
128+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\Nodes;
6+
7+
/** @extends AbstractNode<string> */
8+
final class BreadCrumbNode extends AbstractNode
9+
{
10+
public function __construct(string $value = '')
11+
{
12+
$this->value = $value;
13+
}
14+
}

packages/guides/src/Twig/AssetsExtension.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use phpDocumentor\Guides\Meta\InternalTarget;
1919
use phpDocumentor\Guides\Meta\Target;
2020
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
21+
use phpDocumentor\Guides\Nodes\BreadCrumbNode;
2122
use phpDocumentor\Guides\Nodes\Node;
2223
use phpDocumentor\Guides\RenderContext;
2324
use phpDocumentor\Guides\UrlGeneratorInterface;
@@ -48,6 +49,7 @@ public function getFunctions(): array
4849
new TwigFunction('asset', $this->asset(...), ['is_safe' => ['html'], 'needs_context' => true]),
4950
new TwigFunction('renderNode', $this->renderNode(...), ['is_safe' => ['html'], 'needs_context' => true]),
5051
new TwigFunction('renderLink', $this->renderLink(...), ['is_safe' => ['html'], 'needs_context' => true]),
52+
new TwigFunction('renderBreadcrumb', $this->renderBreadcrumb(...), ['is_safe' => ['html'], 'needs_context' => true]),
5153
new TwigFunction('renderTarget', $this->renderTarget(...), ['is_safe' => ['html'], 'needs_context' => true]),
5254
];
5355
}
@@ -91,10 +93,7 @@ public function renderNode(array $context, Node|array|null $node): string
9193
return '';
9294
}
9395

94-
$renderContext = $context['env'] ?? null;
95-
if (!$renderContext instanceof RenderContext) {
96-
throw new RuntimeException('Render context must be set in the twig global state to render nodes');
97-
}
96+
$renderContext = $this->getRenderContext($context);
9897

9998
if ($node instanceof Node) {
10099
return $this->nodeRenderer->render($node, $renderContext);
@@ -112,16 +111,22 @@ public function renderNode(array $context, Node|array|null $node): string
112111
public function renderTarget(array $context, Target $target): string
113112
{
114113
if ($target instanceof InternalTarget) {
115-
return $context['env']->relativeDocUrl($target->getDocumentPath(), $target->getAnchor());
114+
return $this->getRenderContext($context)->relativeDocUrl($target->getDocumentPath(), $target->getAnchor());
116115
}
117116

118117
return $target->getUrl();
119118
}
120119

120+
/** @param array{env: RenderContext} $context */
121+
public function renderBreadcrumb(array $context): string
122+
{
123+
return $this->nodeRenderer->render(new BreadCrumbNode(), $this->getRenderContext($context));
124+
}
125+
121126
/** @param array{env: RenderContext} $context */
122127
public function renderLink(array $context, string $url, string|null $anchor = null): string
123128
{
124-
return $context['env']->relativeDocUrl($url, $anchor);
129+
return $this->getRenderContext($context)->relativeDocUrl($url, $anchor);
125130
}
126131

127132
private function copyAsset(
@@ -175,4 +180,15 @@ private function copyAsset(
175180

176181
return $outputPath;
177182
}
183+
184+
/** @param array{env: RenderContext} $context */
185+
private function getRenderContext(array $context): RenderContext
186+
{
187+
$renderContext = $context['env'] ?? null;
188+
if (!$renderContext instanceof RenderContext) {
189+
throw new RuntimeException('Render context must be set in the twig global state to render nodes');
190+
}
191+
192+
return $renderContext;
193+
}
178194
}

tests/Integration/tests-bootstrap/admonition/expected/index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@
1818
<div class="col-lg-3">
1919
</div>
2020
<div class="col-lg-9">
21-
<div class="section" id="document-title">
21+
22+
<nav aria-label="breadcrumb">
23+
<ol class="breadcrumb">
24+
<li class="breadcrumb-item"><a href="/index.html">Document Title</a></li>
25+
</ol>
26+
</nav>
27+
28+
<div class="section" id="document-title">
2229
<h1>Document Title</h1>
2330

2431

tests/Integration/tests-bootstrap/index/expected/anotherPage.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,15 @@
6868

6969
</div>
7070
<div class="col-lg-9">
71-
71+
72+
<nav aria-label="breadcrumb">
73+
<ol class="breadcrumb">
74+
<li class="breadcrumb-item"><a href="/index.html">Document Title</a></li>
75+
<li class="breadcrumb-item"><a href="/anotherPage.html">Another Page</a></li>
76+
</ol>
77+
</nav>
78+
79+
7280

7381
<div class="section" id="another-page">
7482
<h1>Another Page</h1>

tests/Integration/tests-bootstrap/index/expected/index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,14 @@
6868

6969
</div>
7070
<div class="col-lg-9">
71-
71+
72+
<nav aria-label="breadcrumb">
73+
<ol class="breadcrumb">
74+
<li class="breadcrumb-item"><a href="/index.html">Document Title</a></li>
75+
</ol>
76+
</nav>
77+
78+
7279

7380
<div class="section" id="document-title">
7481
<h1>Document Title</h1>

tests/Integration/tests-bootstrap/index/expected/somePage.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,15 @@
6868

6969
</div>
7070
<div class="col-lg-9">
71-
71+
72+
<nav aria-label="breadcrumb">
73+
<ol class="breadcrumb">
74+
<li class="breadcrumb-item"><a href="/index.html">Document Title</a></li>
75+
<li class="breadcrumb-item"><a href="/somePage.html">Some Page</a></li>
76+
</ol>
77+
</nav>
78+
79+
7280

7381
<div class="section" id="some-page">
7482
<h1>Some Page</h1>

0 commit comments

Comments
 (0)