Skip to content

Commit 522f540

Browse files
wouterjjaapio
authored andcommitted
Centralize reference resolving in the rendering phase
1 parent 140e3e3 commit 522f540

26 files changed

+382
-160
lines changed

packages/guides-restructured-text/src/RestructuredText/Parser/InlineLexer.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99

1010
use function array_column;
1111
use function array_flip;
12+
use function parse_url;
1213
use function preg_match;
1314

15+
use const PHP_URL_SCHEME;
16+
1417
/** @extends AbstractLexer<int, string> */
1518
final class InlineLexer extends AbstractLexer
1619
{
@@ -40,6 +43,8 @@ final class InlineLexer extends AbstractLexer
4043
public const VARIABLE_DELIMITER = 24;
4144
public const ESCAPED_SIGN = 25;
4245

46+
public const SUPPORTED_TLDS = '(?:aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ar|ark|at|attachment|aw|barion|bb|beshare|bitcoin|bitcoincash|blob|bolo|browserext|cabal|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap+tcp|coap+ws|coaps|coaps+tcp|coaps+ws|com-eventbrite-attendee|content|content-type|crid|cstr|cvs|dab|dat|data|dav|dhttp|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|doi|dpp|drm|drop|dtmi|dtn|dvb|dvx|dweb|ed2k|eid|elsi|embedded|ens|ethereum|example|facetime|fax|feed|feedready|fido|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gitoid|gizmoproject|go|gopher|graph|grd|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|hyper|iax|icap|icon|im|imap|info|iotdisco|ipfs|ipn|ipns|ipp|ipps|irc|irc6|ircs|iris|iris\.beep|iris\.lwz|iris\.xpc|iris\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|lbry|ldap|ldaps|leaptofrogans|lorawan|lpa|lvlt|magnet|mailserver|mailto|maps|market|matrix|message|microsoft\.windows\.camera|microsoft\.windows\.camera\.multipicker|microsoft\.windows\.camera\.picker|mid|mms|modem|mongodb|moz|ms-access|ms-appinstaller|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-launchremotedesktop|ms-lockscreencomponent-config|ms-media-stream-id|ms-meetnow|ms-mixedrealitycapture|ms-mobileplans|ms-newsandinterests|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-remotedesktop|ms-remotedesktop-launch|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-stickers|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mt|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|num|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|otpauth|p1|pack|palm|paparazzi|payment|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|quic-transport|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|sarif|secondlife|secret-token|service|session|sftp|sgn|shc|shttp (OBSOLETE)|sieve|simpleledger|simplex|sip|sips|skype|smb|smp|sms|smtp|snews|snmp|soap\.beep|soap\.beeps|soldat|spiffe|spotify|ssb|ssh|starknet|steam|stun|stuns|submit|svn|swh|swid|swidpath|tag|taler|teamspeak|tel|teliaeid|telnet|tftp|things|thismessage|tip|tn3270|tool|turn|turns|tv|udp|unreal|upt|urn|ut2004|uuid-in-package|v-event|vemmi|ventrilo|ves|videotex|vnc|view-source|vscode|vscode-insiders|vsls|w3|wais|web3|wcr|webcal|web+ap|wifi|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc\.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s)';
47+
4348
/**
4449
* Map between string position and position in token list.
4550
*
@@ -55,7 +60,7 @@ protected function getCatchablePatterns(): array
5560
return [
5661
'\\\\``', // must be a separate case, as the next pattern would split in "\`" + "`", causing it to become a intepreted text
5762
'\\\\[\s\S]', // Escaping hell... needs escaped slash in regex, but also in php.
58-
'https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)',
63+
self::SUPPORTED_TLDS . ':[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*[-a-zA-Z0-9()@%_\\+~#&\\/=]', // standalone hyperlinks
5964
'\\S+@\\S+\\.\\S+',
6065
'[a-z0-9-]+_{2}', //Inline href.
6166
'[a-z0-9-]+_{1}(?=[\s\.+]|$)', //Inline href.
@@ -114,7 +119,7 @@ protected function getType(string &$value)
114119
return self::LITERAL;
115120
}
116121

117-
if (preg_match('/https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)/i', $value)) {
122+
if (preg_match('/' . self::SUPPORTED_TLDS . ':[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*[-a-zA-Z0-9()@%_\\+~#&\\/=]/', $value) && parse_url($value, PHP_URL_SCHEME) !== null) {
118123
return self::HYPERLINK;
119124
}
120125

packages/guides-restructured-text/src/RestructuredText/Parser/Productions/InlineRules/AnonymousPhraseRule.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;
1010

1111
/**
12-
* Rule to parse for simple anonymous references, such as `myref__`
12+
* Rule to parse for anonymous references
13+
*
14+
* Syntax example:
15+
*
16+
* `Example anonymous reference`__
17+
* `Example reference <http://phpdoc.org>`__
18+
*
19+
* @see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#anonymous-hyperlinks
1320
*/
1421
class AnonymousPhraseRule extends ReferenceRule
1522
{
@@ -21,19 +28,19 @@ public function applies(InlineLexer $lexer): bool
2128
public function apply(ParserContext $parserContext, InlineLexer $lexer): HyperLinkNode|null
2229
{
2330
$text = '';
24-
$url = null;
31+
$embeddedUrl = null;
2532
$initialPosition = $lexer->token?->position;
2633
$lexer->moveNext();
2734
while ($lexer->token !== null) {
2835
switch ($lexer->token->type) {
2936
case InlineLexer::PHRASE_ANONYMOUS_END:
3037
$lexer->moveNext();
3138

32-
return $this->createReference($parserContext, $text, $url, false);
39+
return $this->createAnonymousReference($parserContext, $text, $embeddedUrl);
3340

3441
case InlineLexer::EMBEDED_URL_START:
35-
$url = $this->parseEmbeddedUrl($lexer);
36-
if ($url === null) {
42+
$embeddedUrl = $this->parseEmbeddedUrl($lexer);
43+
if ($embeddedUrl === null) {
3744
$text .= '<';
3845
}
3946

@@ -50,6 +57,15 @@ public function apply(ParserContext $parserContext, InlineLexer $lexer): HyperLi
5057
return null;
5158
}
5259

60+
private function createAnonymousReference(ParserContext $parserContext, string $link, string|null $embeddedUrl): HyperLinkNode
61+
{
62+
$parserContext->resetAnonymousStack();
63+
$node = $this->createReference($parserContext, $link, $embeddedUrl, false);
64+
$parserContext->pushAnonymous($link);
65+
66+
return $node;
67+
}
68+
5369
public function getPriority(): int
5470
{
5571
return 1000;

packages/guides-restructured-text/src/RestructuredText/Parser/Productions/InlineRules/AnonymousReferenceRule.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@
1111
use function trim;
1212

1313
/**
14-
* Rule to parse for simple anonymous references, such as `myref__`
14+
* Rule to parse for simple anonymous references
15+
*
16+
* Syntax example:
17+
*
18+
* Example reference__
19+
*
20+
* @see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#anonymous-hyperlinks
1521
*/
1622
class AnonymousReferenceRule extends ReferenceRule
1723
{
@@ -34,7 +40,7 @@ public function apply(ParserContext $parserContext, InlineLexer $lexer): HyperLi
3440
private function createAnonymousReference(ParserContext $parserContext, string $link): HyperLinkNode
3541
{
3642
$parserContext->resetAnonymousStack();
37-
$node = $this->createReference($parserContext, $link);
43+
$node = $this->createReference($parserContext, $link, null, false);
3844
$parserContext->pushAnonymous($link);
3945

4046
return $node;

packages/guides-restructured-text/src/RestructuredText/Parser/Productions/InlineRules/NamedPhraseRule.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;
1010

1111
/**
12-
* Rule to parse for simple named references, such as `myref_`
12+
* Rule to parse for named references
13+
*
14+
* Syntax examples:
15+
*
16+
* `Sample reference`_
17+
* `Another example <https://phpdoc.org>`_
18+
*
19+
* @see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#hyperlink-references
1320
*/
1421
class NamedPhraseRule extends ReferenceRule
1522
{
@@ -21,25 +28,29 @@ public function applies(InlineLexer $lexer): bool
2128
public function apply(ParserContext $parserContext, InlineLexer $lexer): InlineNode|null
2229
{
2330
$text = '';
24-
$url = null;
31+
$embeddedUrl = null;
2532
$initialPosition = $lexer->token?->position;
2633
$lexer->moveNext();
2734
while ($lexer->token !== null) {
2835
switch ($lexer->token->type) {
2936
case InlineLexer::NAMED_REFERENCE_END:
3037
$lexer->moveNext();
3138
if ($text === '') {
32-
$text = $url ?? '';
39+
$text = $embeddedUrl ?? '';
3340
}
3441

35-
return $this->createReference($parserContext, $text, $url);
42+
return $this->createReference($parserContext, $text, $embeddedUrl);
3643

3744
case InlineLexer::EMBEDED_URL_START:
38-
$url = $this->parseEmbeddedUrl($lexer);
39-
if ($url === null) {
45+
$embeddedUrl = $this->parseEmbeddedUrl($lexer);
46+
if ($embeddedUrl === null) {
4047
$text .= '<';
4148
}
4249

50+
break;
51+
case InlineLexer::WHITESPACE:
52+
$text .= ' ';
53+
4354
break;
4455
default:
4556
$text .= $lexer->token->value;

packages/guides-restructured-text/src/RestructuredText/Parser/Productions/InlineRules/NamedReferenceRule.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@
88
use phpDocumentor\Guides\ParserContext;
99
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;
1010

11-
use function trim;
11+
use function rtrim;
1212

1313
/**
14-
* Rule to parse for simple named references, such as `myref_`
14+
* Rule to parse for simple named references
15+
*
16+
* Syntax examples:
17+
*
18+
* Sample reference_
19+
*
20+
* @see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#hyperlink-references
1521
*/
1622
class NamedReferenceRule extends ReferenceRule
1723
{
@@ -22,7 +28,9 @@ public function applies(InlineLexer $lexer): bool
2228

2329
public function apply(ParserContext $parserContext, InlineLexer $lexer): InlineNode|null
2430
{
25-
$node = $this->createReference($parserContext, trim($lexer->token?->value ?? '', '_'));
31+
$value = rtrim($lexer->token?->value ?? '', '_');
32+
$node = $this->createReference($parserContext, $value);
33+
2634
$lexer->moveNext();
2735

2836
return $node;

packages/guides-restructured-text/src/RestructuredText/Parser/Productions/InlineRules/ReferenceRule.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@
1414

1515
abstract class ReferenceRule extends AbstractInlineRule
1616
{
17-
protected function createReference(ParserContext $parserContext, string $link, string|null $url = null, bool $registerLink = true): HyperLinkNode
17+
protected function createReference(ParserContext $parserContext, string $link, string|null $embeddedUrl = null, bool $registerLink = true): HyperLinkNode
1818
{
1919
// the link may have a new line in it, so we need to strip it
2020
// before setting the link and adding a token to be replaced
2121
$link = str_replace("\n", ' ', $link);
2222
$link = trim(preg_replace('/\s+/', ' ', $link) ?? '');
2323

24-
if ($registerLink && $url !== null) {
25-
$parserContext->setLink($link, $url);
24+
if ($registerLink && $embeddedUrl !== null) {
25+
$parserContext->setLink($link, $embeddedUrl);
2626
}
2727

28-
return new HyperLinkNode($link, $url);
28+
return new HyperLinkNode($link, $embeddedUrl ?? $link);
2929
}
3030

3131
protected function parseEmbeddedUrl(InlineLexer $lexer): string|null

packages/guides-restructured-text/src/RestructuredText/Parser/Productions/InlineRules/StandaloneEmailRule.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;
1010

1111
/**
12-
* Rule to parse for simple anonymous references, such as `myref__`
12+
* Rule for standalone hyperlinks
13+
*
14+
* Syntax example:
15+
*
16+
17+
*
18+
* @see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#standalone-hyperlinks
1319
*/
1420
class StandaloneEmailRule extends ReferenceRule
1521
{
@@ -20,12 +26,9 @@ public function applies(InlineLexer $lexer): bool
2026

2127
public function apply(ParserContext $parserContext, InlineLexer $lexer): HyperLinkNode|null
2228
{
23-
$node = $this->createReference(
24-
$parserContext,
25-
$lexer->token?->value ?? '',
26-
'mailto:' . $lexer->token?->value,
27-
false,
28-
);
29+
$value = $lexer->token?->value ?? '';
30+
$node = $this->createReference($parserContext, $value, $value, false);
31+
2932
$lexer->moveNext();
3033

3134
return $node;

packages/guides-restructured-text/src/RestructuredText/Parser/Productions/InlineRules/StandaloneHyperlinkRule.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
use phpDocumentor\Guides\RestructuredText\Parser\InlineLexer;
1010

1111
/**
12-
* Rule to parse for simple anonymous references, such as `myref__`
12+
* Rule for standalone hyperlinks
13+
*
14+
* Syntax example:
15+
*
16+
* https://phpdoc.org/
17+
*
18+
* @see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#standalone-hyperlinks
1319
*/
1420
class StandaloneHyperlinkRule extends ReferenceRule
1521
{
@@ -20,12 +26,9 @@ public function applies(InlineLexer $lexer): bool
2026

2127
public function apply(ParserContext $parserContext, InlineLexer $lexer): HyperLinkNode|null
2228
{
23-
$node = $this->createReference(
24-
$parserContext,
25-
$lexer->token?->value ?? '',
26-
null,
27-
false,
28-
);
29+
$value = $lexer->token?->value ?? '';
30+
$node = $this->createReference($parserContext, $value, $value, false);
31+
2932
$lexer->moveNext();
3033

3134
return $node;

packages/guides-restructured-text/tests/unit/Parser/InlineTokenParserTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,37 +104,37 @@ public static function inlineNodeProvider(): array
104104
],
105105
'Named Reference' => [
106106
'myref_',
107-
new InlineCompoundNode([new HyperLinkNode('myref')]),
107+
new InlineCompoundNode([new HyperLinkNode('myref', 'myref')]),
108108
],
109109
'Named Reference in string' => [
110110
'abc: myref_ xyz',
111111
new InlineCompoundNode([
112112
new PlainTextInlineNode('abc: '),
113-
new HyperLinkNode('myref'),
113+
new HyperLinkNode('myref', 'myref'),
114114
new PlainTextInlineNode(' xyz'),
115115
]),
116116
],
117117
'Anonymous Reference' => [
118118
'myref__',
119-
new InlineCompoundNode([new HyperLinkNode('myref')]),
119+
new InlineCompoundNode([new HyperLinkNode('myref', 'myref')]),
120120
],
121121
'Anonymous Reference in string' => [
122122
'abc: myref__ xyz',
123123
new InlineCompoundNode([
124124
new PlainTextInlineNode('abc: '),
125-
new HyperLinkNode('myref'),
125+
new HyperLinkNode('myref', 'myref'),
126126
new PlainTextInlineNode(' xyz'),
127127
]),
128128
],
129129
'Internal Reference' => [
130130
'_`myref`',
131-
new InlineCompoundNode([new HyperLinkNode('myref')]),
131+
new InlineCompoundNode([new HyperLinkNode('myref', 'myref')]),
132132
],
133133
'Internal Reference in string' => [
134134
'abc: _`myref` xyz',
135135
new InlineCompoundNode([
136136
new PlainTextInlineNode('abc: '),
137-
new HyperLinkNode('myref'),
137+
new HyperLinkNode('myref', 'myref'),
138138
new PlainTextInlineNode(' xyz'),
139139
]),
140140
],

packages/guides/resources/config/guides.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@
1818
use phpDocumentor\Guides\NodeRenderers\InMemoryNodeRendererFactory;
1919
use phpDocumentor\Guides\NodeRenderers\NodeRendererFactory;
2020
use phpDocumentor\Guides\NodeRenderers\NodeRendererFactoryAware;
21+
use phpDocumentor\Guides\NodeRenderers\ReferenceResolvingNodeRendererFactory;
2122
use phpDocumentor\Guides\Parser;
23+
use phpDocumentor\Guides\ReferenceResolvers\DelegatingReferenceResolver;
24+
use phpDocumentor\Guides\ReferenceResolvers\DocReferenceResolver;
25+
use phpDocumentor\Guides\ReferenceResolvers\ExternalReferenceResolver;
26+
use phpDocumentor\Guides\ReferenceResolvers\InternalReferenceResolver;
27+
use phpDocumentor\Guides\ReferenceResolvers\ReferenceResolver;
2228
use phpDocumentor\Guides\Renderer\HtmlRenderer;
2329
use phpDocumentor\Guides\Renderer\InMemoryRendererFactory;
2430
use phpDocumentor\Guides\Renderer\IntersphinxRenderer;
@@ -58,6 +64,9 @@
5864
->instanceof(NodeTransformer::class)
5965
->tag('phpdoc.guides.compiler.nodeTransformers')
6066

67+
->instanceof(ReferenceResolver::class)
68+
->tag('phpdoc.guides.reference_resolver')
69+
6170
->load(
6271
'phpDocumentor\\Guides\\Compiler\\NodeTransformers\\',
6372
'%vendor_dir%/phpdocumentor/guides/src/Compiler/NodeTransformers/*Transformer.php',
@@ -88,9 +97,15 @@
8897

8998
->set(DocumentNodeTraverser::class)
9099

91-
92100
->set(UrlGenerator::class)
93101

102+
->set(ExternalReferenceResolver::class)
103+
->set(InternalReferenceResolver::class)
104+
->set(DocReferenceResolver::class)
105+
106+
->set(DelegatingReferenceResolver::class)
107+
->arg('$resolvers', tagged_iterator('phpdoc.guides.reference_resolver', defaultPriorityMethod: 'getPriority'))
108+
94109
->set(HtmlRenderer::class)
95110
->tag('phpdoc.renderer.typerenderer')
96111
->args(
@@ -121,7 +136,12 @@
121136
])
122137
->alias(NodeRendererFactory::class, InMemoryNodeRendererFactory::class)
123138

124-
->set(InMemoryRendererFactory::class)
139+
->set(ReferenceResolvingNodeRendererFactory::class)
140+
->decorate(NodeRendererFactory::class)
141+
->arg('$innerFactory', service('.inner'))
142+
->arg('$referenceResolver', service(DelegatingReferenceResolver::class))
143+
144+
->set(InMemoryRendererFactory::class)
125145
->arg('$renderSets', tagged_iterator('phpdoc.renderer.typerenderer'))
126146
->alias(TypeRendererFactory::class, InMemoryRendererFactory::class)
127147

0 commit comments

Comments
 (0)