' . $parsed . ''; - } -} diff --git a/src/Markdown/CodeBlocks/InlineCodeBlockRenderer.php b/src/Markdown/CodeBlocks/InlineCodeBlockRenderer.php deleted file mode 100644 index 1faecd34..00000000 --- a/src/Markdown/CodeBlocks/InlineCodeBlockRenderer.php +++ /dev/null @@ -1,35 +0,0 @@ -[\w]+)}(?
.*)/', $node->getLiteral(), $match);
-
- $language = $match['match'] ?? 'php';
- $code = $match['code'] ?? $node->getLiteral();
-
- return '' . $this->highlighter->parse($code, $language) . '';
- }
-}
diff --git a/src/Markdown/CodeGroups/CodeGroupBlock.php b/src/Markdown/CodeGroups/CodeGroupBlock.php
deleted file mode 100644
index 759379b5..00000000
--- a/src/Markdown/CodeGroups/CodeGroupBlock.php
+++ /dev/null
@@ -1,9 +0,0 @@
-block = new CodeGroupBlock();
- }
-
- #[Override]
- public function addLine(string $line): void {}
-
- #[Override]
- public function getBlock(): AbstractBlock
- {
- return $this->block;
- }
-
- #[Override]
- public function isContainer(): bool
- {
- return true;
- }
-
- #[Override]
- public function canContain(AbstractBlock $child_block): bool
- {
- return true;
- }
-
- #[Override]
- public function canHaveLazyContinuationLines(): bool
- {
- return false;
- }
-
- #[Override]
- public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active_block_parser): ?BlockContinue
- {
- if ($cursor->isIndented()) {
- return BlockContinue::at($cursor);
- }
-
- $match = RegexHelper::matchFirst('/^:::$/', $cursor->getLine());
-
- if ($match !== null) {
- $this->finished = true;
-
- return BlockContinue::finished();
- }
-
- return BlockContinue::at($cursor);
- }
-
- #[Override]
- public function closeBlock(): void {}
-
- public function isFinished(): bool
- {
- return $this->finished;
- }
-}
diff --git a/src/Markdown/CodeGroups/CodeGroupBlockRenderer.php b/src/Markdown/CodeGroups/CodeGroupBlockRenderer.php
deleted file mode 100644
index 1b017916..00000000
--- a/src/Markdown/CodeGroups/CodeGroupBlockRenderer.php
+++ /dev/null
@@ -1,97 +0,0 @@
-children() as $child) {
- if (! $child instanceof FencedCode) {
- continue;
- }
-
- $info_words = $this->getInfoWords($child);
- $filename = $info_words[1] ?? "Tab {$index}";
-
- $is_active = $index === 0;
- $tab_id = 'tab-' . md5($filename . $index);
- $panel_id = 'panel-' . md5($filename . $index);
-
- $tabs[] = new HtmlElement(
- tagName: 'button',
- attributes: [
- 'class' => 'code-group-tab' . ($is_active ? ' active' : ''),
- 'role' => 'tab',
- 'aria-selected' => $is_active ? 'true' : 'false',
- 'aria-controls' => $panel_id,
- 'id' => $tab_id,
- 'data-panel' => $panel_id,
- ],
- contents: $filename,
- );
-
- $rendered_code = $this->code_block_renderer->render($child, $child_renderer);
-
- $panels[] = new HtmlElement(
- tagName: 'div',
- attributes: array_filter([
- 'class' => 'code-group-panel' . ($is_active ? ' active' : ''),
- 'role' => 'tabpanel',
- 'aria-labelledby' => $tab_id,
- 'id' => $panel_id,
- 'hidden' => $is_active ? null : 'hidden',
- ]),
- contents: $rendered_code,
- );
-
- $index++;
- }
-
- if ($tabs === []) {
- return '';
- }
-
- $tab_list = new HtmlElement(
- tagName: 'div',
- attributes: [
- 'class' => 'code-group-tabs',
- 'role' => 'tablist',
- ],
- contents: $tabs,
- );
-
- return new HtmlElement(
- tagName: 'div',
- attributes: ['class' => 'code-group'],
- contents: [$tab_list, ...$panels],
- );
- }
-}
diff --git a/src/Markdown/CodeGroups/CodeGroupBlockStartParser.php b/src/Markdown/CodeGroups/CodeGroupBlockStartParser.php
deleted file mode 100644
index 1b3913e7..00000000
--- a/src/Markdown/CodeGroups/CodeGroupBlockStartParser.php
+++ /dev/null
@@ -1,33 +0,0 @@
-isIndented()) {
- return BlockStart::none();
- }
-
- $match = RegexHelper::matchFirst('/^:::code-group$/', $cursor->getLine());
-
- if ($match === null) {
- return BlockStart::none();
- }
-
- $cursor->advanceToEnd();
-
- return BlockStart::of(new CodeGroupBlockParser())->at($cursor);
- }
-}
diff --git a/src/Markdown/CodeGroups/CodeGroupExtension.php b/src/Markdown/CodeGroups/CodeGroupExtension.php
deleted file mode 100644
index 7f1fd102..00000000
--- a/src/Markdown/CodeGroups/CodeGroupExtension.php
+++ /dev/null
@@ -1,18 +0,0 @@
-addBlockStartParser(new CodeGroupBlockStartParser());
- }
-}
diff --git a/src/Markdown/CodeGroups/code-groups.entrypoint.ts b/src/Markdown/CodeGroups/code-groups.entrypoint.ts
deleted file mode 100644
index 001c4c2f..00000000
--- a/src/Markdown/CodeGroups/code-groups.entrypoint.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
- document.querySelectorAll('.code-group').forEach((codeGroup) => {
- const tabs = codeGroup.querySelectorAll('.code-group-tab')
- const panels = codeGroup.querySelectorAll('.code-group-panel')
-
- tabs.forEach((tab) => {
- tab.addEventListener('click', () => {
- if (!tab.dataset.panel) {
- return
- }
-
- tabs.forEach((tab) => {
- tab.classList.remove('active')
- tab.setAttribute('aria-selected', 'false')
- })
-
- panels.forEach((panel) => {
- panel.classList.remove('active')
- panel.setAttribute('hidden', 'hidden')
- })
-
- tab.classList.add('active')
- tab.setAttribute('aria-selected', 'true')
-
- const targetPanel = document.getElementById(tab.dataset.panel)
- if (targetPanel) {
- targetPanel.classList.add('active')
- targetPanel.removeAttribute('hidden')
- }
- })
- })
- })
-})
diff --git a/src/Markdown/Extensions/GitHubLink/GitHubLinkRule.php b/src/Markdown/Extensions/GitHubLink/GitHubLinkRule.php
new file mode 100644
index 00000000..4c5cda91
--- /dev/null
+++ b/src/Markdown/Extensions/GitHubLink/GitHubLinkRule.php
@@ -0,0 +1,33 @@
+comesNext('{b`', length: 3)
+ || $parser->comesNext('{`', length: 2);
+ }
+
+ public function parse(Parser $parser): ?Token
+ {
+ $parser->consumeIncluding('`');
+ $content = $parser->consumeUntil('`');
+ $parser->consumeIncluding('}');
+
+ return new GitHubLinkToken($this->version, $content);
+ }
+}
\ No newline at end of file
diff --git a/src/Markdown/Extensions/GitHubLink/GitHubLinkToken.php b/src/Markdown/Extensions/GitHubLink/GitHubLinkToken.php
new file mode 100644
index 00000000..d7fcf715
--- /dev/null
+++ b/src/Markdown/Extensions/GitHubLink/GitHubLinkToken.php
@@ -0,0 +1,52 @@
+content)
+ ->stripStart('#[')
+ ->stripEnd(']')
+ ->stripStart(['\\Tempest\\', 'Tempest\\'])
+ ->replaceRegex("/^(\w+)/", static fn (array $matches) => sprintf('packages/%s/src', to_kebab_case($matches[0])))
+ ->replaceEvery(['date-time' => 'datetime'])
+ ->replace('\\', '/')
+ ->prepend('https://github.com/tempestphp/tempest-framework/blob/' . $this->version->getBranch() . '/')
+ ->append('.php')
+ ->toString();
+
+ if (str_starts_with($this->content, '#[')) {
+ $text = str($this->content)
+ ->stripStart('#[')
+ ->stripEnd(']')
+ ->stripStart('\\')
+ ->classBasename()
+ ->wrap('#[', ']');
+ } else {
+ $text = str($this->content)
+ ->stripStart('\\')
+ ->classBasename()
+ ->wrap('', '')
+ ->toString();
+ }
+
+ return sprintf(
+ '%s',
+ $uri,
+ $text,
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Markdown/Extensions/Heading/HeadingRule.php b/src/Markdown/Extensions/Heading/HeadingRule.php
new file mode 100644
index 00000000..44811cbd
--- /dev/null
+++ b/src/Markdown/Extensions/Heading/HeadingRule.php
@@ -0,0 +1,27 @@
+comesNext('#', 1);
+ }
+
+ public function parse(Parser $parser): Token
+ {
+ $buffer = $parser->consumeUntil(Parser::NEW_LINE);
+
+ $level = strspn($buffer, '#');
+
+ return new HeadingToken(substr($buffer, $level) |> trim(...), $level);
+ }
+}
diff --git a/src/Markdown/Extensions/Heading/HeadingToken.php b/src/Markdown/Extensions/Heading/HeadingToken.php
new file mode 100644
index 00000000..bd20cdf2
--- /dev/null
+++ b/src/Markdown/Extensions/Heading/HeadingToken.php
@@ -0,0 +1,50 @@
+level}";
+
+ $slug = $this->content |> trim(...) |> strtolower(...) |> (fn (string $x) => str_replace(' ', '-', $x));
+
+ $id = " id=\"{$slug}\"";
+
+ $content = $parser
+ ->forToken($this, [
+ new BoldAndItalicRule(),
+ new BoldRule(),
+ new ItalicRule(),
+ new StrikethroughRule(),
+ new LinkRule(),
+ new CodeRule(),
+ new TextRule(),
+ ])
+ ->parse($this->content);
+
+ if ($this->level === 2 || $this->level === 3) {
+ $svg = '';
+
+ return "<{$tag}{$id}>{$svg} {$content}{$tag}>";
+ }
+
+ return "<{$tag}{$id}>{$content}{$tag}>";
+ }
+}
diff --git a/src/Markdown/Extensions/Link/LinkRule.php b/src/Markdown/Extensions/Link/LinkRule.php
new file mode 100644
index 00000000..b3893e5e
--- /dev/null
+++ b/src/Markdown/Extensions/Link/LinkRule.php
@@ -0,0 +1,59 @@
+comesNext('[', 1);
+ }
+
+ public function parse(Parser $parser): Token
+ {
+ $parser->consumeIncluding('[');
+ $content = $this->consumeContent($parser);
+ $parser->consumeIncluding(']');
+
+ $href = null;
+
+ if ($parser->comesNext('(', 1)) {
+ $parser->consumeIncluding('(');
+ $href = $parser->consumeUntil(')');
+ $parser->consumeIncluding(')');
+ }
+
+ return new LinkToken($content, $href);
+ }
+
+ private function consumeContent(Parser $parser): string
+ {
+ $content = '';
+ $bracketDepth = 0;
+
+ while ($parser->current !== null) {
+ if ($parser->comesNext(']') && $bracketDepth === 0) {
+ break;
+ }
+
+ if ($parser->comesNext('[')) {
+ $bracketDepth += 1;
+ } elseif ($parser->comesNext(']')) {
+ $bracketDepth -= 1;
+ }
+
+ $content .= $parser->consume();
+ }
+
+ return $content;
+ }
+}
diff --git a/src/Markdown/Extensions/Link/LinkToken.php b/src/Markdown/Extensions/Link/LinkToken.php
new file mode 100644
index 00000000..2addf8f8
--- /dev/null
+++ b/src/Markdown/Extensions/Link/LinkToken.php
@@ -0,0 +1,47 @@
+forToken($this, [
+ new BoldAndItalicRule(),
+ new BoldRule(),
+ new ItalicRule(),
+ new StrikethroughRule(),
+ new ImageRule(),
+ new TextRule(),
+ ])
+ ->parse($this->content);
+
+ $href = $this->href ?? '';
+ $blank = '';
+
+ if (str_starts_with($href, '*')) {
+ $href = substr($href, 1);
+ $blank = ' target="_blank" rel="noopener noreferrer"';
+ }
+
+ $href= preg_replace('/\.md((?=[\/#?])|$)/', '', $href);
+
+ return "{$content}";
+ }
+}
\ No newline at end of file
diff --git a/src/Markdown/Extensions/Lists/ListRule.php b/src/Markdown/Extensions/Lists/ListRule.php
new file mode 100644
index 00000000..f21d207e
--- /dev/null
+++ b/src/Markdown/Extensions/Lists/ListRule.php
@@ -0,0 +1,57 @@
+comesNext('- ', 2);
+ }
+
+ public function parse(Parser $parser): ?Token
+ {
+ $parser->consumeIncluding('- ');
+ $content = trim($parser->consumeUntil(Parser::NEW_LINE));
+ $parser->consumeWhile(Parser::NEW_LINE);
+
+ $childContent = '';
+ $indent = strspn($parser->content, ' ', $parser->position);
+
+ while ($indent >= 2 && $parser->current !== null) {
+ if (strspn($parser->content, ' ', $parser->position) < $indent) {
+ break;
+ }
+
+ $parser->consume($indent);
+ $childContent .= $parser->consumeUntil(Parser::NEW_LINE) . PHP_EOL;
+ $parser->consumeWhile(Parser::NEW_LINE);
+ }
+
+ $children = $childContent !== ''
+ ? $parser->withRules(new ListRule($this->version))->lex($childContent)[0]
+ : null;
+
+ $item = new ListItem($content, $children);
+
+ if ($parser->lastToken instanceof ListToken) {
+ $parser->lastToken->items[] = $item;
+ return null;
+ }
+
+ return new ListToken($this->version, [$item]);
+ }
+}
\ No newline at end of file
diff --git a/src/Markdown/Extensions/Lists/ListToken.php b/src/Markdown/Extensions/Lists/ListToken.php
new file mode 100644
index 00000000..51253266
--- /dev/null
+++ b/src/Markdown/Extensions/Lists/ListToken.php
@@ -0,0 +1,50 @@
+forToken($this, [
+ new GitHubLinkRule($this->version),
+ new BoldAndItalicRule(),
+ new BoldRule(),
+ new ItalicRule(),
+ new LinkRule(),
+ new ImageRule(),
+ new CodeRule(),
+ new TextRule(),
+ ]);
+
+ $list = '';
+
+ foreach ($this->items as $item) {
+ $content = $parser->parse($item->content);
+ $children = $item->children?->parse($parser) ?? '';
+ $list .= "- {$content}{$children}
";
+ }
+
+ $list .= '
';
+
+ return $list;
+ }
+}
diff --git a/src/Markdown/Extensions/Paragraph/ParagraphRule.php b/src/Markdown/Extensions/Paragraph/ParagraphRule.php
new file mode 100644
index 00000000..f8a4ed57
--- /dev/null
+++ b/src/Markdown/Extensions/Paragraph/ParagraphRule.php
@@ -0,0 +1,43 @@
+current !== null) {
+ $content .= $parser->consumeUntil(Parser::NEW_LINE);
+
+ if ($parser->current === null) {
+ break;
+ }
+
+ // A blank line (two consecutive newlines) ends the paragraph
+ if ($parser->comesNext("\n\n", 2) || $parser->comesNext("\r\n\r\n", 4) || $parser->comesNext("\n\r\n", 3) || $parser->comesNext("\r\n\n", 3)) {
+ break;
+ }
+
+ // Single newline — consume it and continue to the next line
+ $content .= $parser->consumeWhile(Parser::NEW_LINE);
+ }
+
+ return new ParagraphToken($this->version, $content);
+ }
+}
\ No newline at end of file
diff --git a/src/Markdown/Extensions/Paragraph/ParagraphToken.php b/src/Markdown/Extensions/Paragraph/ParagraphToken.php
new file mode 100644
index 00000000..d74d5325
--- /dev/null
+++ b/src/Markdown/Extensions/Paragraph/ParagraphToken.php
@@ -0,0 +1,43 @@
+forToken($this, [
+ new GitHubLinkRule($this->version),
+ new BoldAndItalicRule(),
+ new BoldRule(),
+ new ItalicRule(),
+ new StrikethroughRule(),
+ new LinkRule(),
+ new ImageRule(),
+ new CodeRule(),
+ new TextRule(),
+ ]);
+
+ $content = $parser->parse($this->content);
+
+ return "{$content}
";
+ }
+}
\ No newline at end of file
diff --git a/src/Markdown/HeadingRenderer.php b/src/Markdown/HeadingRenderer.php
deleted file mode 100644
index 72310dbd..00000000
--- a/src/Markdown/HeadingRenderer.php
+++ /dev/null
@@ -1,51 +0,0 @@
-getLevel();
- $attrs = $node->data->get('attributes');
- $slug = new ImmutableString($childRenderer->renderNodes($node->children()))
- ->stripTags()
- ->kebab()
- ->toString();
-
- $svg = '';
-
- return new HtmlElement(
- tagName: $tag,
- attributes: [
- ...$attrs,
- 'id' => $slug,
- ],
- contents: new HtmlElement(
- tagName: 'a',
- attributes: ['href' => '#' . $slug, 'class' => 'heading-permalink'],
- contents: [
- new HtmlElement('span', contents: $svg),
- $childRenderer->renderNodes($node->children()),
- ],
- ),
- );
- }
-}
diff --git a/src/Markdown/LinkRenderer.php b/src/Markdown/LinkRenderer.php
deleted file mode 100644
index b31e40ba..00000000
--- a/src/Markdown/LinkRenderer.php
+++ /dev/null
@@ -1,67 +0,0 @@
-setUrl(
- replace($node->getUrl(), '/\.md((?=[\/#?])|$)/', ''),
- );
-
- $renderer = new InlineLinkRenderer();
- $renderer->setConfiguration($this->config);
-
- return $renderer->render($node, $childRenderer);
- }
-
- #[Override]
- public function setConfiguration(ConfigurationInterface $configuration): void
- {
- $this->config = $configuration;
- }
-
- #[Override]
- public function getXmlTagName(Node $node): string
- {
- return 'link';
- }
-
- #[Override]
- public function getXmlAttributes(Node $node): array
- {
- if (! $node instanceof Link) {
- throw new InvalidArgumentException('Node must be instance of ' . Link::class);
- }
-
- return [
- 'destination' => $node->getUrl(),
- 'title' => $node->getTitle() ?? '',
- ];
- }
-}
diff --git a/src/Markdown/MarkdownInitializer.php b/src/Markdown/MarkdownInitializer.php
index 72899b8f..a1c86418 100644
--- a/src/Markdown/MarkdownInitializer.php
+++ b/src/Markdown/MarkdownInitializer.php
@@ -1,66 +1,38 @@
get(Highlighter::class, tag: 'project');
-
- $codeBlockRenderer = new CodeBlockRenderer($highlighter);
- $version = $container->get(Version::class);
-
- $environment
- ->addExtension(new CommonMarkCoreExtension())
- ->addExtension(new FrontMatterExtension())
- ->addExtension(new AttributesExtension())
- ->addExtension(new AlertExtension())
- ->addExtension(new CodeGroupExtension())
- ->addExtension(new TableExtension())
- ->addInlineParser(new TempestPackageParser($version))
- ->addInlineParser(new FqcnParser($version))
- ->addInlineParser(new AttributeParser($version))
- ->addInlineParser(new FunctionParser($version))
- ->addInlineParser(new HandleParser())
- ->addRenderer(FencedCode::class, $codeBlockRenderer)
- ->addRenderer(Code::class, new InlineCodeBlockRenderer($highlighter))
- ->addRenderer(Link::class, new LinkRenderer())
- ->addRenderer(Heading::class, new HeadingRenderer())
- ->addRenderer(CodeGroupBlock::class, new CodeGroupBlockRenderer($codeBlockRenderer));
-
- return new MarkdownConverter($environment);
+ return new Markdown(
+ highlighter: $container->get(Highlighter::class, tag: 'project'),
+ )
+ ->removeRules(
+ TempestParagraphRule::class,
+ TempestListRule::class,
+ TempestHeadingRule::class,
+ )
+ ->prependRules(
+ $container->get(ListRule::class),
+ $container->get(HeadingRule::class),
+ )
+ ->appendRules(
+ $container->get(ParagraphRule::class),
+ );
}
-}
+}
\ No newline at end of file
diff --git a/src/Markdown/ResolvesInfoWords.php b/src/Markdown/ResolvesInfoWords.php
deleted file mode 100644
index 0e26de52..00000000
--- a/src/Markdown/ResolvesInfoWords.php
+++ /dev/null
@@ -1,28 +0,0 @@
-getInfo() === null || $code->getInfo() === '') {
- return [];
- }
-
- $words = [];
- $pattern = '/"([^"]*)"|(\S+)/';
-
- \preg_match_all($pattern, $code->getInfo(), $matches, PREG_SET_ORDER);
-
- foreach ($matches as $match) {
- $words[] = $match[1] !== '' ? $match[1] : $match[2];
- }
-
- return $words;
- }
-}
diff --git a/src/Markdown/Symbols/AttributeParser.php b/src/Markdown/Symbols/AttributeParser.php
deleted file mode 100644
index 492a0f55..00000000
--- a/src/Markdown/Symbols/AttributeParser.php
+++ /dev/null
@@ -1,64 +0,0 @@
-getCursor();
- $previousChar = $cursor->peek(-1);
-
- if ($previousChar !== null && $previousChar !== ' ') {
- return false;
- }
-
- $cursor->advanceBy($inlineContext->getFullMatchLength());
-
- [$flag, $fqcn] = $inlineContext->getSubMatches();
- $url = str($fqcn)
- ->stripStart(['\\Tempest\\', 'Tempest\\'])
- ->replaceRegex("/^(\w+)/", static fn (array $matches) => sprintf('packages/%s/src', to_kebab_case($matches[0])))
- ->replaceEvery(['date-time' => 'datetime'])
- ->replace('\\', '/')
- ->prepend('https://github.com/tempestphp/tempest-framework/blob/' . $this->version->getBranch() . '/')
- ->append('.php')
- ->toString();
-
- $attribute = str($fqcn)
- ->stripStart('\\')
- ->when($flag === 'b', static fn ($s) => $s->classBasename())
- ->wrap(before: '#[', after: ']')
- ->toString();
-
- $link = new Link($url);
- $link->appendChild(new Code($attribute));
- $inlineContext->getContainer()->appendChild($link);
-
- return true;
- }
-}
diff --git a/src/Markdown/Symbols/FqcnParser.php b/src/Markdown/Symbols/FqcnParser.php
deleted file mode 100644
index 68828c29..00000000
--- a/src/Markdown/Symbols/FqcnParser.php
+++ /dev/null
@@ -1,61 +0,0 @@
-getCursor();
- $previousChar = $cursor->peek(-1);
-
- if ($previousChar !== null && $previousChar !== ' ') {
- return false;
- }
-
- $cursor->advanceBy($inlineContext->getFullMatchLength());
-
- [$flag, $fqcn] = $inlineContext->getSubMatches();
- $url = str($fqcn)
- ->stripStart(['\\Tempest\\', 'Tempest\\'])
- ->replaceRegex("/^(\w+)/", static fn (array $matches) => sprintf('packages/%s/src', to_kebab_case($matches[0])))
- ->replaceEvery(['date-time' => 'datetime'])
- ->replace('\\', '/')
- ->prepend('https://github.com/tempestphp/tempest-framework/blob/' . $this->version->getBranch() . '/')
- ->append('.php')
- ->toString();
-
- $link = new Link($url);
- $link->appendChild(new Code($flag === 'b' ? class_basename($fqcn) : strip_start($fqcn, '\\')));
- $inlineContext->getContainer()->appendChild($link);
-
- return true;
- }
-}
diff --git a/src/Markdown/Symbols/FunctionParser.php b/src/Markdown/Symbols/FunctionParser.php
deleted file mode 100644
index bf33216b..00000000
--- a/src/Markdown/Symbols/FunctionParser.php
+++ /dev/null
@@ -1,80 +0,0 @@
-getCursor();
- $previousChar = $cursor->peek(-1);
-
- if ($previousChar !== null && $previousChar !== ' ') {
- return false;
- }
-
- $cursor->advanceBy($inlineContext->getFullMatchLength());
-
- [$flag, $fqf] = $inlineContext->getSubMatches();
-
- $reflection = $this->createReflectionFromFqf($fqf);
- $function = str($fqf)
- ->stripStart('\\')
- ->when($flag === 'b', static fn (ImmutableString $s) => $s->afterLast('\\'))
- ->append('()')
- ->toString();
-
- if (! $reflection) {
- $inlineContext->getContainer()->appendChild(new Code($function));
-
- return true;
- }
-
- $url = str($reflection->getFileName())
- ->afterLast('tempest/framework/')
- ->prepend('https://github.com/tempestphp/tempest-framework/blob/' . $this->version->getBranch() . '/')
- ->append("#L{$reflection->getStartLine()}-L{$reflection->getEndLine()}")
- ->toString();
-
- $link = new Link($url);
- $link->appendChild(new Code($function));
- $inlineContext->getContainer()->appendChild($link);
-
- return true;
- }
-
- private function createReflectionFromFqf(string $fqf): ?ReflectionFunction
- {
- try {
- return new ReflectionFunction($fqf);
- } catch (Throwable) {
- return null;
- }
- }
-}
diff --git a/src/Markdown/Symbols/HandleParser.php b/src/Markdown/Symbols/HandleParser.php
deleted file mode 100644
index 2bb37bd8..00000000
--- a/src/Markdown/Symbols/HandleParser.php
+++ /dev/null
@@ -1,51 +0,0 @@
-getCursor();
- $previousChar = $cursor->peek(-1);
-
- if ($previousChar !== null && $previousChar !== ' ') {
- return false;
- }
-
- $cursor->advanceBy($inlineContext->getFullMatchLength());
-
- [$platform, $handle, $text] = $inlineContext->getSubMatches() + [null, null, null];
-
- $url = match ($platform) {
- 'bluesky', 'bsky' => "https://bsky.app/profile/{$handle}",
- 'gh', 'github' => "https://github.com/{$handle}",
- 'x', 'twitter' => "https://x.com/{$handle}",
- default => throw new RuntimeException("Unknown platform: {$platform}"),
- };
-
- $inlineContext
- ->getContainer()
- ->appendChild(
- new Link($url, label: $text ?? '@' . $handle),
- );
-
- return true;
- }
-}
diff --git a/src/Web/Blog/BlogIndexer.php b/src/Web/Blog/BlogIndexer.php
index 56d22ce0..3727a061 100644
--- a/src/Web/Blog/BlogIndexer.php
+++ b/src/Web/Blog/BlogIndexer.php
@@ -7,10 +7,8 @@
use App\Web\CommandPalette\Command;
use App\Web\CommandPalette\Indexer;
use App\Web\CommandPalette\Type;
-use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
-use League\CommonMark\MarkdownConverter;
use Override;
-use RuntimeException;
+use Tempest\Markdown\Markdown;
use Tempest\Support\Arr\ImmutableArray;
use function Tempest\Router\uri;
@@ -21,7 +19,7 @@
final readonly class BlogIndexer implements Indexer
{
public function __construct(
- private MarkdownConverter $markdown,
+ private Markdown $markdown,
) {}
#[Override]
@@ -29,14 +27,10 @@ public function index(): ImmutableArray
{
return arr(glob(__DIR__ . '/articles/*.md'))
->map(function (string $path) {
- $markdown = $this->markdown->convert(file_get_contents($path));
+ $parsed = $this->markdown->parse(file_get_contents($path));
preg_match('/\d+-\d+-\d+-(?.*)\.md/', $path, $matches);
- if (! $markdown instanceof RenderedContentWithFrontMatter) {
- throw new RuntimeException(sprintf('Blog entry [%s] is missing a frontmatter.', $path));
- }
-
- $frontmatter = $markdown->getFrontMatter();
+ $frontmatter = $parsed->frontmatter;
$title = get_by_key($frontmatter, 'title');
$author = get_by_key($frontmatter, 'author');
$description = get_by_key($frontmatter, 'description');
@@ -44,14 +38,14 @@ public function index(): ImmutableArray
$tags = get_by_key($frontmatter, 'tag');
return new Command(
- type: Type::URI,
title: $title,
- uri: uri([BlogController::class, 'show'], slug: $matches['slug']),
+ type: Type::URI,
hierarchy: [
'Blog',
Author::tryFrom($author)?->getName() ?? 'Tempest',
$title,
],
+ uri: uri([BlogController::class, 'show'], slug: $matches['slug']),
fields: [
$author,
$description,
diff --git a/src/Web/Blog/BlogRepository.php b/src/Web/Blog/BlogRepository.php
index b3e32c88..0e382d1a 100644
--- a/src/Web/Blog/BlogRepository.php
+++ b/src/Web/Blog/BlogRepository.php
@@ -5,10 +5,9 @@
namespace App\Web\Blog;
use DateTimeImmutable;
-use Exception;
-use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
-use League\CommonMark\MarkdownConverter;
use Spatie\YamlFrontMatter\YamlFrontMatter;
+use Tempest\Markdown\Markdown;
+use Tempest\Markdown\ParsedMarkdown;
use Tempest\Support\Arr\ImmutableArray;
use function Tempest\Mapper\map;
@@ -17,7 +16,7 @@
final readonly class BlogRepository
{
public function __construct(
- private MarkdownConverter $markdown,
+ private Markdown $markdown,
) {}
/**
@@ -48,7 +47,7 @@ public function all(bool $loadContent = false): ImmutableArray
}
if ($loadContent) {
- $data['content'] = $this->parseContent($path)->getContent();
+ $data['content'] = $this->parseContent($path)->html;
}
return $data;
@@ -69,9 +68,9 @@ public function find(string $slug): ?BlogPost
$data = [
'slug' => $slug,
- 'content' => $content->getContent(),
+ 'content' => $content->html,
'createdAt' => $this->parseDate($path),
- ...$content->getFrontMatter(),
+ ...$content->frontmatter,
];
if (isset($data['tag'])) {
@@ -85,7 +84,7 @@ public function find(string $slug): ?BlogPost
return map($data)->to(BlogPost::class);
}
- private function parseContent(string $path): ?RenderedContentWithFrontMatter
+ private function parseContent(string $path): ?ParsedMarkdown
{
$content = @file_get_contents($path); // @mago-expect lint:no-error-control-operator
@@ -93,13 +92,7 @@ private function parseContent(string $path): ?RenderedContentWithFrontMatter
return null;
}
- $parsed = $this->markdown->convert($content);
-
- if (! $parsed instanceof RenderedContentWithFrontMatter) {
- throw new Exception("Missing frontmatter or content in {$path}");
- }
-
- return $parsed;
+ return $this->markdown->parse($content);
}
private function parseDate(string $path): DateTimeImmutable
diff --git a/src/Web/Challenges/parsing-100m-lines.html b/src/Web/Challenges/parsing-100m-lines.html
index 2c3bf7a4..9be9df4e 100644
--- a/src/Web/Challenges/parsing-100m-lines.html
+++ b/src/Web/Challenges/parsing-100m-lines.html
@@ -957,14 +957,14 @@
-
+
Tempest
100M Challenge
diff --git a/src/Web/Documentation/ChapterRepository.php b/src/Web/Documentation/ChapterRepository.php
index ca7498f1..c1e0e714 100644
--- a/src/Web/Documentation/ChapterRepository.php
+++ b/src/Web/Documentation/ChapterRepository.php
@@ -5,10 +5,9 @@
namespace App\Web\Documentation;
use App\Support\HasMemoization;
-use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
-use League\CommonMark\MarkdownConverter;
use RuntimeException;
use Spatie\YamlFrontMatter\YamlFrontMatter;
+use Tempest\Markdown\Markdown;
use Tempest\Support\Arr\ImmutableArray;
use function Tempest\root_path;
@@ -23,7 +22,7 @@ final class ChapterRepository
use HasMemoization;
public function __construct(
- private readonly MarkdownConverter $markdown,
+ private readonly Markdown $markdown,
) {}
public function find(Version $version, string $category, string $slug): ?Chapter
@@ -46,13 +45,9 @@ public function find(Version $version, string $category, string $slug): ?Chapter
}
$raw = file_get_contents($path);
- $markdown = $this->markdown->convert($raw);
+ $markdown = $this->markdown->parse($raw);
- if (! $markdown instanceof RenderedContentWithFrontMatter) {
- throw new RuntimeException(sprintf('Documentation entry [%s] is missing a frontmatter.', $path));
- }
-
- $frontmatter = $markdown->getFrontMatter();
+ $frontmatter = $markdown->frontmatter;
$title = get_by_key($frontmatter, 'title');
$description = get_by_key($frontmatter, 'description');
$hidden = get_by_key($frontmatter, 'hidden');
@@ -61,7 +56,7 @@ public function find(Version $version, string $category, string $slug): ?Chapter
version: $version,
category: $category,
slug: $slug,
- body: $markdown->getContent(),
+ body: $markdown->html,
raw: $raw,
title: $title,
path: to_relative_path(root_path(), $path),
diff --git a/src/Web/Documentation/DocumentationIndexer.php b/src/Web/Documentation/DocumentationIndexer.php
index 8a26e506..70773857 100644
--- a/src/Web/Documentation/DocumentationIndexer.php
+++ b/src/Web/Documentation/DocumentationIndexer.php
@@ -7,21 +7,16 @@
use App\Web\CommandPalette\Command;
use App\Web\CommandPalette\Indexer;
use App\Web\CommandPalette\Type;
-use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
-use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
-use League\CommonMark\MarkdownConverter;
-use League\CommonMark\Node\Inline\Text;
-use League\CommonMark\Node\Query;
use Override;
-use RuntimeException;
+use Tempest\Markdown\Markdown;
use Tempest\Support\Arr\ImmutableArray;
use Tempest\Support\Str\ImmutableString;
+use function Tempest\Support\str;
use function Tempest\Router\uri;
use function Tempest\Support\arr;
use function Tempest\Support\Arr\get_by_key;
use function Tempest\Support\Arr\wrap;
-use function Tempest\Support\Str\to_kebab_case;
use function Tempest\Support\Str\to_sentence_case;
/**
@@ -30,7 +25,7 @@
final readonly class DocumentationIndexer implements Indexer
{
public function __construct(
- private MarkdownConverter $markdown,
+ private Markdown $markdown,
) {}
/**
@@ -43,55 +38,48 @@ public function index(): ImmutableArray
return arr(glob(__DIR__ . "/content/{$version->value}/*/*.md"))
->flatMap(function (string $path) use ($version) {
- $markdown = $this->markdown->convert(file_get_contents($path));
-
- if (! $markdown instanceof RenderedContentWithFrontMatter) {
- throw new RuntimeException(sprintf('Documentation entry [%s] is missing a frontmatter.', $path));
- }
+ $markdown = $this->markdown->parse(file_get_contents($path));
$path = new ImmutableString($path);
$category = $path->beforeLast('/')->afterLast('/')->replaceRegex('/\d+-/', '');
$chapter = $path->basename('.md')->replaceRegex('/\d+-/', '');
- $title = get_by_key($markdown->getFrontMatter(), 'title');
- $keywords = get_by_key($markdown->getFrontMatter(), 'keywords');
+ $title = get_by_key($markdown->frontmatter, 'title');
+ $keywords = get_by_key($markdown->frontmatter, 'keywords');
- if (get_by_key($markdown->getFrontMatter(), 'hidden') === true) {
+ if (get_by_key($markdown->frontmatter, 'hidden') === true) {
return [];
}
$main = new Command(
- type: Type::URI,
title: $title,
- uri: uri(DocumentationController::class, version: $version, category: $category, slug: $chapter),
+ type: Type::URI,
hierarchy: [
'Documentation',
to_sentence_case($category),
$title,
],
+ uri: uri(DocumentationController::class, version: $version, category: $category, slug: $chapter),
fields: [
...wrap($keywords),
],
);
- /** @var Heading[] */
- $matchingNodes = new Query()
- ->where(Query::type(Heading::class))
- ->findAll($markdown->getDocument());
+ preg_match_all('//', $markdown->html, $matches);
+
+ $indices = arr($matches[0] ?? [])
+ ->map(static function (string $h2) use ($main) {
+ $title = str($h2)->afterLast('')->beforeLast('trim();
- $indices = arr(iterator_to_array($matchingNodes))
- ->map(static function (Heading $heading) use ($main) {
- /** @var Text */
- $text = $heading->firstChild();
- $slug = to_kebab_case($text->getLiteral());
+ $slug = $title->kebab();
return new Command(
+ title: $title->toString(),
type: Type::URI,
- title: $text->getLiteral(),
- uri: $main->uri . '#' . $slug,
hierarchy: [
...$main->hierarchy,
- $text->getLiteral(),
+ $slug->toString(),
],
+ uri: $main->uri . '#' . $slug,
);
})
->filter();
diff --git a/src/Web/Homepage/HomeController.php b/src/Web/Homepage/HomeController.php
index 800da34f..8f4ba84e 100644
--- a/src/Web/Homepage/HomeController.php
+++ b/src/Web/Homepage/HomeController.php
@@ -4,20 +4,19 @@
namespace App\Web\Homepage;
-use League\CommonMark\MarkdownConverter;
use Tempest\Http\Responses\Redirect;
+use Tempest\Markdown\Markdown;
use Tempest\Router\Get;
use Tempest\Router\StaticPage;
use Tempest\View\View;
use function Tempest\Support\Arr\map_with_keys;
use function Tempest\Support\Str\strip_end;
-use function Tempest\View\view;
final readonly class HomeController
{
public function __construct(
- private MarkdownConverter $markdown,
+ private Markdown $markdown,
) {}
#[StaticPage]
@@ -26,7 +25,7 @@ public function __invoke(): View
{
$codeBlocks = map_with_keys(
glob(__DIR__ . '/codeblocks/*.md'),
- fn (string $path) => yield strip_end(basename($path), '.md') => $this->markdown->convert(file_get_contents($path)),
+ fn (string $path) => yield strip_end(basename($path), '.md') => $this->markdown->parse(file_get_contents($path))->html,
);
return \Tempest\View\view('./home.view.php', codeBlocks: $codeBlocks);
diff --git a/src/Web/Homepage/codeblocks/static-pages.md b/src/Web/Homepage/codeblocks/static-pages.md
index f558cd99..bd3e8e13 100644
--- a/src/Web/Homepage/codeblocks/static-pages.md
+++ b/src/Web/Homepage/codeblocks/static-pages.md
@@ -1,4 +1,4 @@
-```console ">_ ./tempest static:generate"
+```console >_ ./tempest static:generate
/framework/01-getting-started .. /public/framework/01-getting-started/index.html
/framework/02-the-container ...... /public/framework/02-the-container/index.html
/framework/03-controllers .......... /public/framework/03-controllers/index.html
diff --git a/src/Web/Homepage/x-home-section.view.php b/src/Web/Homepage/x-home-section.view.php
index 8862ec00..e55fa97b 100644
--- a/src/Web/Homepage/x-home-section.view.php
+++ b/src/Web/Homepage/x-home-section.view.php
@@ -6,7 +6,7 @@
{{ $heading }}
-
+
{{ $paragraph }}
@@ -22,7 +22,7 @@
-
+
{!! $this->codeBlocks[$snippet] !!}
diff --git a/src/Web/assets/main.entrypoint.css b/src/Web/assets/main.entrypoint.css
index 621378ab..12d9eaf0 100644
--- a/src/Web/assets/main.entrypoint.css
+++ b/src/Web/assets/main.entrypoint.css
@@ -277,20 +277,36 @@ pre {
|--------------------------------------------------------------------------
*/
-.home .code-block {
- @apply flex flex-col gap-y-0 p-0.5 rounded-xl border border-(--ui-border)
- bg-(--ui-bg-elevated)/50 overflow-hidden;
+.home .home-code-block {
+ & + .home-code-block {
+ margin-top: 1em;
+ }
- &.named-code-block {
- .code-block-name {
- @apply mb-1.5 mt-1 px-2 font-mono text-(--ui-text-dimmed) text-sm;
+ & .code-title {
+ margin-bottom: 0;
+ margin-left: 1.1em;
+ font-size: 1em;
+ color: var(--ui-text-muted);
+ background: var(--code-border);
+ padding: 0em 0.5em;
+ display: flex;
+ float: left;
+ border-radius: 0.2em 0.2em 0 0;
+
+ & + pre {
+ margin-top: 0;
}
}
- pre {
- @apply border border-(--ui-border) rounded-[0.6rem] p-4 my-0
- bg-(--ui-bg-elevated)/50 overflow-x-auto;
- tab-size: 2;
+ & pre {
+ background-color: #ffffffdd;
+ border-radius: 3px;
+ display: block;
+ clear:both;
+ padding: 1em 2ch;
+ box-shadow: 0 0 15px 5px var(--ui-border);
+ overflow-x: auto;
+ line-height: 1.8em;
}
}
diff --git a/src/Web/assets/typography.css b/src/Web/assets/typography.css
index 67d793d2..f431b0a0 100644
--- a/src/Web/assets/typography.css
+++ b/src/Web/assets/typography.css
@@ -79,39 +79,19 @@
}
}
- .code-group {
- @apply my-[1.71em] flex flex-col gap-y-0 rounded-lg border
- border-(--ui-border) bg-(--ui-bg-elevated)/20;
-
- .code-block-name {
- @apply hidden;
- }
-
- .code-group-tabs {
- @apply flex gap-x-1 px-2 py-1 pt-1.5 overflow-x-auto grow;
-
- .code-group-tab {
- @apply px-3 whitespace-nowrap py-1.5 font-mono text-sm text-left
- text-(--ui-text-dimmed) rounded-md hover:text-(--ui-text-highlighted)
- transition-colors cursor-pointer;
-
- &.active {
- @apply text-(--ui-text-highlighted) border-(--ui-border)
- bg-(--ui-bg-elevated) border-b-transparent;
- }
- }
- }
-
- .code-group-panel {
- @apply hidden;
-
- &.active {
- @apply block;
- }
-
- .code-block {
- @apply my-0 border-0 rounded-none bg-transparent;
- }
+ .code-title {
+ margin-bottom: 0;
+ margin-left: 1.1em;
+ font-size: 0.8em;
+ color: var(--ui-text-muted);
+ background: var(--code-border);
+ padding: 0em 0.5em;
+ display: flex;
+ float: left;
+ border-radius: 0.2em 0.2em 0 0;
+
+ & + pre {
+ margin-top: 0;
}
}
diff --git a/src/Web/x-header.view.php b/src/Web/x-header.view.php
index 0a8819ec..58450047 100644
--- a/src/Web/x-header.view.php
+++ b/src/Web/x-header.view.php
@@ -22,7 +22,7 @@ class="group transition-[top,border] z-[1] fixed top-4 data-[scrolling]:top-0 fl
-
+
Tempest
diff --git a/src/functions.php b/src/functions.php
index 27e05ff7..c27d629c 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -2,15 +2,6 @@
declare(strict_types=1);
-use App\Markdown\MarkdownPost;
-use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
-use League\CommonMark\MarkdownConverter;
-use Tempest\View\View;
-
-use function Tempest\Container\get;
-use function Tempest\Mapper\map;
-use function Tempest\View\view;
-
function recursive_search(string $folder, string $pattern): Generator
{
$directory = new RecursiveDirectoryIterator($folder);
@@ -20,23 +11,4 @@ function recursive_search(string $folder, string $pattern): Generator
foreach ($files as $file) {
yield $file->getPathName();
}
-}
-
-function markdown(string $file): View
-{
- $markdown = get(MarkdownConverter::class);
-
- $content = $markdown->convert(file_get_contents($file));
-
- $data = [
- 'content' => $content,
- ];
-
- if ($content instanceof RenderedContentWithFrontMatter) {
- $data = [...$data, ...$content->getFrontMatter()];
- }
-
- $post = map($data)->to(MarkdownPost::class);
-
- return \Tempest\View\view(__DIR__ . '/Web/markdown.view.php', post: $post);
-}
+}
\ No newline at end of file
diff --git a/tests/Markdown/Extensions/GitHubLink/GitHubLinkRuleTest.php b/tests/Markdown/Extensions/GitHubLink/GitHubLinkRuleTest.php
new file mode 100644
index 00000000..dce03305
--- /dev/null
+++ b/tests/Markdown/Extensions/GitHubLink/GitHubLinkRuleTest.php
@@ -0,0 +1,53 @@
+parser = new Parser();
+ }
+
+ #[Test]
+ public function test_parse(): void
+ {
+ $token = new ParagraphToken(
+ Version::VERSION_3,
+ 'These attributes implement the {b`Tempest\Router\Route`} interface, allowing custom route attributes to be created',
+ );
+
+ $parsed = $token->parse($this->parser);
+
+ $this->assertStringContainsString(
+ 'https://github.com/tempestphp/tempest-framework/blob/3.x/packages/router/src/Route.php',
+ $parsed,
+ );
+ }
+
+ #[Test]
+ public function test_parse_without_b(): void
+ {
+ $token = new ParagraphToken(
+ Version::VERSION_3,
+ 'These attributes implement the {`Tempest\Router\Route`} interface, allowing custom route attributes to be created',
+ );
+
+ $parsed = $token->parse($this->parser);
+
+ $this->assertStringContainsString(
+ 'https://github.com/tempestphp/tempest-framework/blob/3.x/packages/router/src/Route.php',
+ $parsed,
+ );
+ }
+}
diff --git a/tests/Markdown/Extensions/GitHubLink/GitHubLinkTokenTest.php b/tests/Markdown/Extensions/GitHubLink/GitHubLinkTokenTest.php
new file mode 100644
index 00000000..cbb18d8c
--- /dev/null
+++ b/tests/Markdown/Extensions/GitHubLink/GitHubLinkTokenTest.php
@@ -0,0 +1,51 @@
+parse(new Parser());
+
+ $this->assertSame(
+ 'Route',
+ $html,
+ );
+ }
+
+ #[Test]
+ public function test_with_class_and_leading_slash(): void
+ {
+ $token = new GitHubLinkToken(Version::VERSION_3, '\Tempest\Router\Route');
+
+ $html = $token->parse(new Parser());
+
+ $this->assertSame(
+ 'Route',
+ $html,
+ );
+ }
+
+ #[Test]
+ public function test_with_attribute_and_leading_slash(): void
+ {
+ $token = new GitHubLinkToken(Version::VERSION_3, '#[Tempest\Discovery\SkipDiscovery]');
+
+ $html = $token->parse(new Parser());
+
+ $this->assertSame(
+ '#[SkipDiscovery]',
+ $html,
+ );
+ }
+}