refactor: Cleanup git state - commit all staged changes

Major refactoring cleanup:
- Add new controller architecture (class-controller-*.php)
- Add new settings-v2 UI (views/settings-v2/)
- Add new CSS architecture (agentic-sidebar.css, tokens)
- Add esbuild build pipeline (scripts/build.js, package.json)
- Add composer dependencies (vendor/)
- Add frontend src directory (assets/js/src/index.jsx)
- Add documentation files
- Remove old/obsolete files (class-settings.php, old CSS)

This commits all pending changes from previous refactoring efforts.
This commit is contained in:
Dwindi Ramadhana
2026-06-17 05:27:58 +07:00
parent d3f142222c
commit 690991c526
7963 changed files with 941566 additions and 67372 deletions

View File

@@ -0,0 +1,23 @@
======================================
Contribute to the phpDocumentor Guides
======================================
Go to the mono-repository
=========================
This project is developed in the mono-repository `phpDocumentor Guides <https://github.com/phpDocumentor/guides>`__.
The repository you are currently in gets auto-created by splitting the mono-repository. You **must not** contribute
to this repository directly but always to the mono-repository linked above.
Create Issues
=============
* If you find something missing or something is wrong in this library, you are welcome to write an issue
describing the problem: `Issues on GitHub <https://github.com/phpDocumentor/guides/issues>`__.
* If you can, please try to fix the problem yourself.
Make changes (create pull requests)
===================================
See the `Contribution chapter <https://docs.phpdoc.org/components/guides/guides/contributions/index.html>`__ in the
`Documentation` <https://docs.phpdoc.org/components/guides/guides/index.html>`__.

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2010 Mike van Riel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,36 @@
.. image:: https://poser.pugx.org/phpdocumentor/guides-markdown/require/php
:alt: PHP Version Require
:target: https://packagist.org/packages/phpdocumentor/guides-markdown
.. image:: https://poser.pugx.org/phpdocumentor/guides-markdown/v/stable
:alt: Latest Stable Version
:target: https://packagist.org/packages/phpdocumentor/guides-markdown
.. image:: https://poser.pugx.org/phpdocumentor/guides-markdown/v/unstable
:alt: Latest Unstable Version
:target: https://packagist.org/packages/phpdocumentor/guides-markdown
.. image:: https://poser.pugx.org/phpdocumentor/guides-markdown/d/total
:alt: Total Downloads
:target: https://packagist.org/packages/phpdocumentor/guides-markdown
.. image:: https://poser.pugx.org/phpdocumentor/guides-markdown/d/monthly
:alt: Monthly Downloads
:target: https://packagist.org/packages/phpdocumentor/guides-markdown
====================
phpDocumentor Guides
====================
This repository is part of `phpDocumentor's Guides library <https://github.com/phpDocumentor/guides>`__, a framework
designed to take hand-written documentation in code repositories and create an AST (abstract syntax tree) from it.
This AST is then fed to a renderer, which produces the desired output, such as HTML.
The package `phpdocumentor/guides-markdown <https://packagist.org/packages/phpdocumentor/guides-markdown>`__ adds
Markdown support to the phpDocumentor's Guides library.
:Mono-Repository: https://github.com/phpDocumentor/guides
:Documentation: https://docs.phpdoc.org/components/guides/guides/index.html
:Packagist: https://packagist.org/packages/phpdocumentor/guides-markdown
:Contribution: https://github.com/phpDocumentor/guides/tree/main/CONTRIBUTING.rst

View File

@@ -0,0 +1,34 @@
{
"name": "phpdocumentor/guides-markdown",
"description": "Adds Markdown support to the phpDocumentor's Guides library.",
"type": "library",
"license": "MIT",
"homepage": "https://www.phpdoc.org",
"config": {
"sort-packages": true
},
"autoload": {
"psr-4": {
"phpDocumentor\\Guides\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"phpDocumentor\\Guides\\": [
"tests/unit/"
]
}
},
"minimum-stability": "stable",
"require": {
"php": "^8.1",
"league/commonmark": "^2.4",
"phpdocumentor/guides": "^1.0 || ^2.0",
"symfony/yaml": "^6.4 || ^7.0 || ^8.0"
},
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
use phpDocumentor\Guides\Markdown\MarkupLanguageParser;
use phpDocumentor\Guides\Markdown\Parsers\BlockQuoteParser;
use phpDocumentor\Guides\Markdown\Parsers\CodeBlockParser;
use phpDocumentor\Guides\Markdown\Parsers\FrontMatter\TitleParser;
use phpDocumentor\Guides\Markdown\Parsers\FrontMatterParser;
use phpDocumentor\Guides\Markdown\Parsers\HeaderParser;
use phpDocumentor\Guides\Markdown\Parsers\HtmlParser;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\EmphasisParser;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\InlineCodeParser;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\InlineImageParser;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\LinkParser;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\NewLineParser;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\PlainTextParser;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\StrongParser;
use phpDocumentor\Guides\Markdown\Parsers\ListBlockParser;
use phpDocumentor\Guides\Markdown\Parsers\ListItemParser;
use phpDocumentor\Guides\Markdown\Parsers\ParagraphParser;
use phpDocumentor\Guides\Markdown\Parsers\SeparatorParser;
use phpDocumentor\Guides\Markdown\Parsers\Table\TableParser;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\String\Slugger\AsciiSlugger;
use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator;
return static function (ContainerConfigurator $container): void {
$container->services()
->defaults()
->autowire()
->autoconfigure()
->set(AsciiSlugger::class)
->set(HeaderParser::class)
->arg('$inlineParsers', tagged_iterator('phpdoc.guides.markdown.parser.inlineParser'))
->tag('phpdoc.guides.markdown.parser.blockParser')
->set(BlockQuoteParser::class)
->arg('$subParsers', tagged_iterator('phpdoc.guides.markdown.parser.subParser'))
->tag('phpdoc.guides.markdown.parser.blockParser')
->tag('phpdoc.guides.markdown.parser.subParser')
->set(HtmlParser::class)
->tag('phpdoc.guides.markdown.parser.blockParser')
->tag('phpdoc.guides.markdown.parser.subParser')
->set(ListBlockParser::class)
->tag('phpdoc.guides.markdown.parser.blockParser')
->tag('phpdoc.guides.markdown.parser.subParser')
->set(ListItemParser::class)
->arg('$subParsers', tagged_iterator('phpdoc.guides.markdown.parser.subParser'))
->tag('phpdoc.guides.markdown.parser.blockParser')
->set(ParagraphParser::class)
->arg('$inlineParsers', tagged_iterator('phpdoc.guides.markdown.parser.inlineParser'))
->tag('phpdoc.guides.markdown.parser.blockParser')
->tag('phpdoc.guides.markdown.parser.subParser')
->set(SeparatorParser::class)
->tag('phpdoc.guides.markdown.parser.blockParser')
->tag('phpdoc.guides.markdown.parser.subParser')
->set(CodeBlockParser::class)
->tag('phpdoc.guides.markdown.parser.blockParser')
->tag('phpdoc.guides.markdown.parser.subParser')
->set(TableParser::class)
->arg('$subParsers', tagged_iterator('phpdoc.guides.markdown.parser.inlineParser'))
->tag('phpdoc.guides.markdown.parser.blockParser')
->set(EmphasisParser::class)
->arg('$inlineParsers', tagged_iterator('phpdoc.guides.markdown.parser.inlineParser'))
->tag('phpdoc.guides.markdown.parser.inlineParser')
->set(LinkParser::class)
->arg('$inlineParsers', tagged_iterator('phpdoc.guides.markdown.parser.inlineParser'))
->tag('phpdoc.guides.markdown.parser.inlineParser')
->set(PlainTextParser::class)
->tag('phpdoc.guides.markdown.parser.inlineParser')
->set(StrongParser::class)
->arg('$inlineParsers', tagged_iterator('phpdoc.guides.markdown.parser.inlineParser'))
->tag('phpdoc.guides.markdown.parser.inlineParser')
->set(InlineCodeParser::class)
->tag('phpdoc.guides.markdown.parser.inlineParser')
->set(InlineImageParser::class)
->arg('$inlineParsers', tagged_iterator('phpdoc.guides.markdown.parser.inlineParser'))
->tag('phpdoc.guides.markdown.parser.inlineParser')
->set(NewLineParser::class)
->tag('phpdoc.guides.markdown.parser.inlineParser')
->set(FrontMatterParser::class)
->arg('$fieldParsers', tagged_iterator('phpdoc.guides.markdown.front_matter', 'fieldName'))
->tag('phpdoc.guides.markdown.parser.blockParser')
->set(TitleParser::class)
->tag('phpdoc.guides.markdown.front_matter')
->set(MarkupLanguageParser::class)
->arg('$parsers', tagged_iterator('phpdoc.guides.markdown.parser.blockParser'))
->tag('phpdoc.guides.parser.markupLanguageParser');
};

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use function dirname;
final class MarkdownExtension extends Extension implements PrependExtensionInterface, CompilerPassInterface
{
/** @param mixed[] $configs */
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new PhpFileLoader(
$container,
new FileLocator(dirname(__DIR__, 3) . '/resources/config'),
);
$loader->load('guides-markdown.php');
}
public function prepend(ContainerBuilder $container): void
{
}
public function process(ContainerBuilder $container): void
{
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown;
use League\CommonMark\Environment\Environment as CommonMarkEnvironment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\FrontMatter\Data\SymfonyYamlFrontMatterParser;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Node\Block\Document;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Parser\MarkdownParser;
use phpDocumentor\Guides\MarkupLanguageParser as MarkupLanguageParserInterface;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\ParserContext;
use phpDocumentor\Guides\Settings\ProjectSettings;
use phpDocumentor\Guides\Settings\SettingsManager;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function ltrim;
use function md5;
use function sprintf;
use function strtolower;
final class MarkupLanguageParser implements MarkupLanguageParserInterface
{
private readonly MarkdownParser $markdownParser;
private ParserContext|null $parserContext = null;
private DocumentNode|null $document = null;
private SettingsManager $settingsManager;
/** @param iterable<ParserInterface<Node>> $parsers */
public function __construct(
private readonly LoggerInterface $logger,
private readonly iterable $parsers,
SettingsManager|null $settingsManager,
) {
$cmEnvironment = new CommonMarkEnvironment(['html_input' => 'strip']);
$cmEnvironment->addExtension(new CommonMarkCoreExtension());
$cmEnvironment->addExtension(new TableExtension());
$cmEnvironment->addExtension(new AutolinkExtension());
$cmEnvironment->addExtension(new FrontMatterExtension(new SymfonyYamlFrontMatterParser()));
$this->markdownParser = new MarkdownParser($cmEnvironment);
// if for backward compatibility reasons no settings manager was passed, use the defaults
$this->settingsManager = $settingsManager ?? new SettingsManager(new ProjectSettings());
}
public function supports(string $inputFormat): bool
{
return strtolower($inputFormat) === 'md';
}
public function parse(ParserContext $parserContext, string $contents): DocumentNode
{
$this->parserContext = $parserContext;
$ast = $this->markdownParser->parse($contents);
return $this->parseDocument($ast->walker(), md5($contents));
}
private function parseDocument(NodeWalker $walker, string $hash): DocumentNode
{
$document = new DocumentNode($hash, ltrim($this->getParserContext()->getCurrentFileName(), '/'));
$document->setOrphan(!$this->settingsManager->getProjectSettings()->isAutomaticMenu());
$this->document = $document;
while ($event = $walker->next()) {
$commonMarkNode = $event->getNode();
if ($event->isEntering()) {
// Use entering events for context switching
foreach ($this->parsers as $parser) {
if ($parser->supports($event)) {
$document->addChildNode($parser->parse($this, $walker, $commonMarkNode));
break;
}
}
continue;
}
if ($commonMarkNode instanceof Document) {
return $document;
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'Document'));
}
return $document;
}
public function getParserContext(): ParserContext
{
if ($this->parserContext === null) {
throw new RuntimeException(
'A parser\'s Environment should not be consulted before parsing has started',
);
}
return $this->parserContext;
}
public function getDocument(): DocumentNode
{
if ($this->document === null) {
throw new RuntimeException('Cannot get document as parser is not started');
}
return $this->document;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown;
use phpDocumentor\Guides\Nodes\TextNode;
final class NullNode extends TextNode
{
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown;
use RuntimeException;
class ParserException extends RuntimeException
{
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser as GuidesParser;
use phpDocumentor\Guides\Nodes\Node;
/** @template-covariant TValue as Node */
interface ParserInterface
{
/** @return TValue */
public function parse(GuidesParser $parser, NodeWalker $walker, CommonMarkNode $current): Node;
public function supports(NodeWalkerEvent $event): bool;
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use phpDocumentor\Guides\Markdown\ParserInterface;
use phpDocumentor\Guides\Nodes\Node;
/**
* @template TValue as Node
* @implements ParserInterface<TValue>
*/
abstract class AbstractBlockParser implements ParserInterface
{
}

View File

@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\AdmonitionNode;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\ParagraphNode;
use phpDocumentor\Guides\Nodes\QuoteNode;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function array_shift;
use function array_values;
use function count;
use function is_string;
use function sprintf;
use function trim;
/** @extends AbstractBlockParser<Node> */
final class BlockQuoteParser extends AbstractBlockParser
{
/** @param iterable<AbstractBlockParser<Node>> $subParsers */
public function __construct(
private readonly iterable $subParsers,
private readonly LoggerInterface $logger,
) {
}
/**
* @param array<Node> $content
*
* @phpstan-assert-if-false non-empty-list $content
*/
private static function contentIsTextOnlyParagraph(array $content): bool
{
if (count($content) === 0) {
return true;
}
if ($content[0] instanceof ParagraphNode === false) {
return true;
}
$paragraphContent = $content[0]->getValue()[0]->getValue();
if (is_string($paragraphContent)) {
return true;
}
return $paragraphContent[0] instanceof PlainTextInlineNode === false;
}
/** @param array<Node> $content */
private static function contentIsNotParagraph(array $content): bool
{
if (count($content) === 0) {
return true;
}
return $content[0] instanceof ParagraphNode === false;
}
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): Node
{
$content = [];
while ($event = $walker->next()) {
$commonMarkNode = $event->getNode();
if ($event->isEntering()) {
foreach ($this->subParsers as $subParser) {
if ($subParser->supports($event)) {
$content[] = $subParser->parse($parser, $walker, $commonMarkNode);
break;
}
}
continue;
}
// leaving the heading node
if ($commonMarkNode instanceof BlockQuote === false) {
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'BlockQuote'));
throw new RuntimeException('Unexpected end of NodeWalker');
}
if (self::contentIsNotParagraph($content)) {
return new QuoteNode($content);
}
if (self::contentIsTextOnlyParagraph($content)) {
return new QuoteNode($content);
}
$admonitionNode = $this->toAdmonition(array_values($content));
return $admonitionNode ?? new QuoteNode($content);
}
throw new RuntimeException('Unexpected end of NodeWalker');
}
/** @param non-empty-list<Node> $content */
private function toAdmonition(array $content): AdmonitionNode|null
{
if ($content[0] instanceof ParagraphNode === false) {
return null;
}
$paragraphContent = $content[0]->getValue()[0]->getValue();
if ($paragraphContent[0] instanceof PlainTextInlineNode === false) {
return null;
}
$text = trim($paragraphContent[0]->getValue());
$newParagraphContent = $paragraphContent;
array_shift($newParagraphContent);
$content[0] = new ParagraphNode([new InlineCompoundNode($newParagraphContent)]);
switch ($text) {
case '[!NOTE]':
return new AdmonitionNode(
'note',
new InlineCompoundNode([new PlainTextInlineNode('Note')]),
'Note',
$content,
);
case '[!TIP]':
return new AdmonitionNode(
'tip',
new InlineCompoundNode([new PlainTextInlineNode('Tip')]),
'Tip',
$content,
);
case '[!IMPORTANT]':
return new AdmonitionNode(
'important',
new InlineCompoundNode([new PlainTextInlineNode('Important')]),
'Important',
$content,
);
case '[!WARNING]':
return new AdmonitionNode(
'warning',
new InlineCompoundNode([new PlainTextInlineNode('Warning')]),
'Warning',
$content,
);
case '[!CAUTION]':
return new AdmonitionNode(
'caution',
new InlineCompoundNode([new PlainTextInlineNode('Caution')]),
'Caution',
$content,
);
}
return null;
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof BlockQuote;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\CodeNode;
use function assert;
use function count;
use function explode;
/** @extends AbstractBlockParser<CodeNode> */
final class CodeBlockParser extends AbstractBlockParser
{
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): CodeNode
{
assert($current instanceof IndentedCode || $current instanceof FencedCode);
$walker->next();
$codeNode = new CodeNode(explode("\n", $current->getLiteral()));
if ($current instanceof FencedCode) {
$infoWords = $current->getInfoWords();
if (count($infoWords) !== 0 && $infoWords[0] !== '') {
$codeNode->setLanguage($infoWords[0]);
}
}
return $codeNode;
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->getNode() instanceof IndentedCode || $event->getNode() instanceof FencedCode;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\FrontMatter;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\Nodes\Metadata\AuthorNode;
final class AuthorParser implements Parser
{
/** {@inheritDoc} */
public function process(DocumentNode $document, mixed $value, array $frontMatter): void
{
$value = '' . $value;
$document->addHeaderNode(new AuthorNode($value, [new PlainTextInlineNode($value)]));
}
public function field(): string
{
return 'title';
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\FrontMatter;
use phpDocumentor\Guides\Nodes\DocumentNode;
interface Parser
{
public function field(): string;
/** @param array<string, mixed> $frontMatter */
public function process(DocumentNode $document, mixed $value, array $frontMatter): void;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\FrontMatter;
use phpDocumentor\Guides\Nodes\DocumentNode;
final class TitleParser implements Parser
{
/** {@inheritDoc} */
public function process(DocumentNode $document, mixed $value, array $frontMatter): void
{
$document->setMetaTitle('' . $value);
}
public function field(): string
{
return 'title';
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Node\Block\Document;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\Markdown\NullNode;
use phpDocumentor\Guides\Markdown\ParserInterface;
use phpDocumentor\Guides\Markdown\Parsers\FrontMatter\Parser as FieldParser;
use phpDocumentor\Guides\MarkupLanguageParser as GuidesParser;
use phpDocumentor\Guides\Nodes\Node;
use function array_key_exists;
use function is_array;
/** @implements ParserInterface<NullNode> */
final class FrontMatterParser implements ParserInterface
{
/** @var array<string, FieldParser> */
private array $fieldParsers;
/** @param iterable<string, FieldParser> $fieldParsers */
public function __construct(iterable $fieldParsers)
{
foreach ($fieldParsers as $parser) {
$this->fieldParsers[$parser->field()] = $parser;
}
}
public function parse(GuidesParser $parser, NodeWalker $walker, CommonMarkNode $current): Node
{
$frontMatter = $current->data->get('front_matter', []);
if (is_array($frontMatter) === false) {
return new NullNode('');
}
foreach ($frontMatter as $field => $value) {
if (!array_key_exists($field, $this->fieldParsers)) {
continue;
}
$this->fieldParsers[$field]->process($parser->getDocument(), $value, $frontMatter);
}
return new NullNode('');
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->getNode() instanceof Document;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\AbstractInlineParser;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\Inline\InlineNode;
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TitleNode;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\String\Slugger\AsciiSlugger;
use function sprintf;
/** @extends AbstractBlockParser<TitleNode> */
final class HeaderParser extends AbstractBlockParser
{
/** @param iterable<AbstractInlineParser<InlineNode>> $inlineParsers */
public function __construct(
private readonly iterable $inlineParsers,
private readonly LoggerInterface $logger,
private readonly AsciiSlugger $idGenerator,
) {
}
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): Node
{
$content = [];
while ($event = $walker->next()) {
$commonMarkNode = $event->getNode();
if ($event->isEntering()) {
foreach ($this->inlineParsers as $subParser) {
if ($subParser->supports($event)) {
$content[] = $subParser->parse($parser, $walker, $commonMarkNode);
break;
}
}
continue;
}
// leaving the heading node
if ($commonMarkNode instanceof Heading) {
return new TitleNode(
new InlineCompoundNode($content),
$commonMarkNode->getLevel(),
$this->idGenerator->slug($content[0]->toString() ?? '')->lower()->toString(),
);
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'Header'));
}
throw new RuntimeException('Unexpected end of NodeWalker');
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof Heading;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\RawNode;
use function assert;
/** @extends AbstractBlockParser<RawNode> */
final class HtmlParser extends AbstractBlockParser
{
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): RawNode
{
assert($current instanceof HtmlBlock);
$walker->next();
return new RawNode($current->getLiteral());
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof HtmlBlock;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use phpDocumentor\Guides\Markdown\ParserInterface;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface;
/**
* @template TValue as InlineNodeInterface
* @implements ParserInterface<TValue>
*/
abstract class AbstractInlineParser implements ParserInterface
{
abstract public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): InlineNodeInterface;
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\Inline\InlineNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function count;
use function sprintf;
/**
* @template TValue as InlineNodeInterface
* @extends AbstractInlineParser<TValue>
*/
abstract class AbstractInlineTextDecoratorParser extends AbstractInlineParser
{
/** @param iterable<AbstractInlineParser<InlineNode>> $inlineParsers */
public function __construct(
private readonly iterable $inlineParsers,
private readonly LoggerInterface $logger,
) {
}
/** @return TValue */
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): InlineNodeInterface
{
$content = [];
if ($current->firstChild() === null) {
// Handle inline nodes without content
return $this->createInlineNode($current, null);
}
while ($event = $walker->next()) {
$commonMarkNode = $event->getNode();
if ($event->isEntering()) {
foreach ($this->inlineParsers as $subParser) {
if (!$subParser->supports($event)) {
continue;
}
$content[] = $subParser->parse($parser, $walker, $commonMarkNode);
}
continue;
}
if ($this->supportsCommonMarkNode($commonMarkNode)) {
if (count($content) === 1 && $content[0] instanceof PlainTextInlineNode) {
return $this->createInlineNode($commonMarkNode, $content[0]->getValue(), $content);
}
return $this->createInlineNode($commonMarkNode, null, $content);
}
$this->logger->warning(sprintf('%s context does not allow a %s node', $this->getType(), $commonMarkNode::class));
}
throw new RuntimeException(sprintf('Unexpected end of NodeWalker, %s context was not closed', $this->getType()));
}
abstract protected function getType(): string;
/** @return TValue */
abstract protected function createInlineNode(CommonMarkNode $commonMarkNode, string|null $content): InlineNodeInterface;
abstract protected function supportsCommonMarkNode(CommonMarkNode $commonMarkNode): bool;
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $this->supportsCommonMarkNode($event->getNode());
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
use League\CommonMark\Node\Node as CommonMarkNode;
use phpDocumentor\Guides\Nodes\Inline\EmphasisInlineNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use Psr\Log\LoggerInterface;
/** @extends AbstractInlineTextDecoratorParser<EmphasisInlineNode> */
final class EmphasisParser extends AbstractInlineTextDecoratorParser
{
/** @param iterable<AbstractInlineParser<InlineNode>> $inlineParsers */
public function __construct(
iterable $inlineParsers,
LoggerInterface $logger,
) {
parent::__construct($inlineParsers, $logger);
}
protected function getType(): string
{
return 'Emphasis';
}
/** @param InlineNodeInterface[] $children */
protected function createInlineNode(CommonMarkNode $commonMarkNode, string|null $content, array $children = []): InlineNodeInterface
{
return new EmphasisInlineNode($content ? [new PlainTextInlineNode($content)] : $children);
}
protected function supportsCommonMarkNode(CommonMarkNode $commonMarkNode): bool
{
return $commonMarkNode instanceof Emphasis;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface;
use phpDocumentor\Guides\Nodes\Inline\LiteralInlineNode;
use function assert;
/** @extends AbstractInlineParser<LiteralInlineNode> */
final class InlineCodeParser extends AbstractInlineParser
{
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): InlineNodeInterface
{
assert($current instanceof Code);
return new LiteralInlineNode($current->getLiteral());
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->getNode() instanceof Code;
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
use League\CommonMark\Node\Node as CommonMarkNode;
use phpDocumentor\Guides\Nodes\Inline\ImageInlineNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface;
use Psr\Log\LoggerInterface;
use function assert;
use function sprintf;
/** @extends AbstractInlineTextDecoratorParser<ImageInlineNode> */
final class InlineImageParser extends AbstractInlineTextDecoratorParser
{
/** @param iterable<AbstractInlineParser<InlineNode>> $inlineParsers */
public function __construct(
iterable $inlineParsers,
private readonly LoggerInterface $logger,
) {
parent::__construct($inlineParsers, $logger);
}
protected function getType(): string
{
return 'Image';
}
protected function createInlineNode(CommonMarkNode $commonMarkNode, string|null $content): InlineNodeInterface
{
assert($commonMarkNode instanceof Image);
if ($content === null) {
$this->logger->warning(
sprintf(
'Image %s does not have an alternative text. Add an alternative text like this: ![Image description](%s)',
$commonMarkNode->getUrl(),
$commonMarkNode->getUrl(),
),
);
}
return new ImageInlineNode($commonMarkNode->getUrl(), $content ?? '', $commonMarkNode->getTitle() ?? '');
}
protected function supportsCommonMarkNode(CommonMarkNode $commonMarkNode): bool
{
return $commonMarkNode instanceof Image;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Node\Node as CommonMarkNode;
use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use Psr\Log\LoggerInterface;
use function assert;
use function filter_var;
use function str_ends_with;
use function substr;
use const FILTER_VALIDATE_URL;
/** @extends AbstractInlineTextDecoratorParser<HyperLinkNode> */
final class LinkParser extends AbstractInlineTextDecoratorParser
{
/** @param iterable<AbstractInlineParser<InlineNode>> $inlineParsers */
public function __construct(
iterable $inlineParsers,
LoggerInterface $logger,
) {
parent::__construct($inlineParsers, $logger);
}
protected function getType(): string
{
return 'Link';
}
/** @param InlineNodeInterface[] $children */
protected function createInlineNode(CommonMarkNode $commonMarkNode, string|null $content, array $children = []): InlineNodeInterface
{
assert($commonMarkNode instanceof Link);
$url = $commonMarkNode->getUrl();
if (str_ends_with($url, '.md') && filter_var($url, FILTER_VALIDATE_URL) === false) {
$url = substr($url, 0, -3);
}
return new HyperLinkNode($content ? [new PlainTextInlineNode($content)] : $children, $url);
}
protected function supportsCommonMarkNode(CommonMarkNode $commonMarkNode): bool
{
return $commonMarkNode instanceof Link;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Node\Inline\Newline;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
/** @extends AbstractInlineParser<PlainTextInlineNode> */
class NewLineParser extends AbstractInlineParser
{
public function __construct()
{
}
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): InlineNodeInterface
{
return new PlainTextInlineNode(' ');
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof Newline;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use Psr\Log\LoggerInterface;
use function sprintf;
/** @extends AbstractInlineParser<PlainTextInlineNode> */
final class PlainTextParser extends AbstractInlineParser
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): PlainTextInlineNode
{
if (!$current instanceof Text) {
$this->logger->warning(sprintf('Expected plaintext, encountered a %s node', $current::class));
return new PlainTextInlineNode('');
}
return new PlainTextInlineNode($current->getLiteral());
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof Text;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\InlineParsers;
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
use League\CommonMark\Node\Node as CommonMarkNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\Nodes\Inline\StrongInlineNode;
use Psr\Log\LoggerInterface;
/** @extends AbstractInlineTextDecoratorParser<StrongInlineNode> */
final class StrongParser extends AbstractInlineTextDecoratorParser
{
/** @param iterable<AbstractInlineParser<InlineNode>> $inlineParsers */
public function __construct(
iterable $inlineParsers,
LoggerInterface $logger,
) {
parent::__construct($inlineParsers, $logger);
}
protected function getType(): string
{
return 'StrongDecorator';
}
/** @param InlineNodeInterface[] $children */
protected function createInlineNode(CommonMarkNode $commonMarkNode, string|null $content, array $children = []): InlineNodeInterface
{
return new StrongInlineNode($content ? [new PlainTextInlineNode($content)] : $children);
}
protected function supportsCommonMarkNode(CommonMarkNode $commonMarkNode): bool
{
return $commonMarkNode instanceof Strong;
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\ListNode;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function sprintf;
/** @extends AbstractBlockParser<ListNode> */
final class ListBlockParser extends AbstractBlockParser
{
public function __construct(
private readonly ListItemParser $listItemParser,
private readonly LoggerInterface $logger,
) {
}
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): ListNode
{
$content = [];
while ($event = $walker->next()) {
$commonMarkNode = $event->getNode();
if ($event->isEntering()) {
if ($this->listItemParser->supports($event)) {
$content[] = $this->listItemParser->parse($parser, $walker, $commonMarkNode);
}
continue;
}
if ($commonMarkNode instanceof ListBlock) {
$start = $commonMarkNode->getListData()->start;
return new ListNode(
$content,
$content[0]->isOrdered(),
$start !== null && $start !== 1 ? (string) $start : null,
);
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'List'));
}
throw new RuntimeException('Unexpected end of NodeWalker in list block');
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof ListBlock;
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\ListItemNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\ParagraphNode;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function count;
use function current;
use function sprintf;
/** @extends AbstractBlockParser<ListItemNode> */
final class ListItemParser extends AbstractBlockParser
{
/** @param iterable<AbstractBlockParser<Node>> $subParsers */
public function __construct(
private readonly iterable $subParsers,
private readonly LoggerInterface $logger,
) {
}
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): ListItemNode
{
$content = [];
while ($event = $walker->next()) {
$commonMarkNode = $event->getNode();
if ($event->isEntering()) {
foreach ($this->subParsers as $subParser) {
if ($subParser->supports($event)) {
$content[] = $subParser->parse($parser, $walker, $commonMarkNode);
break;
}
}
continue;
}
if ($commonMarkNode instanceof ListItem) {
$prefix = $commonMarkNode->getListData()->bulletChar ?? $commonMarkNode->getListData()->delimiter ?? '';
$ordered = $commonMarkNode->getListData()->type === ListBlock::TYPE_ORDERED;
if (count($content) === 1 && current($content) instanceof ParagraphNode) {
$content = current($content)->getChildren();
}
return new ListItemNode($prefix, $ordered, $content);
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'List'));
}
throw new RuntimeException('Unexpected end of NodeWalker in list item');
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof ListItem;
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Node\Block\Paragraph as CommonMarkParagraph;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\Markdown\Parsers\InlineParsers\AbstractInlineParser;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\CompoundNode;
use phpDocumentor\Guides\Nodes\Inline\InlineNode;
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
use phpDocumentor\Guides\Nodes\ParagraphNode;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function sprintf;
/** @extends AbstractBlockParser<ParagraphNode> */
final class ParagraphParser extends AbstractBlockParser
{
/** @param iterable<AbstractInlineParser<InlineNode>> $inlineParsers */
public function __construct(
private readonly iterable $inlineParsers,
private readonly LoggerInterface $logger,
) {
}
/** @return ParagraphNode */
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): CompoundNode
{
$content = [];
while ($event = $walker->next()) {
$commonMarkNode = $event->getNode();
if ($event->isEntering()) {
foreach ($this->inlineParsers as $subParser) {
if (!$subParser->supports($event)) {
continue;
}
$content[] = $subParser->parse($parser, $walker, $commonMarkNode);
}
continue;
}
if ($commonMarkNode instanceof CommonMarkParagraph) {
return new ParagraphNode([new InlineCompoundNode($content)]);
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'Paragraph'));
}
throw new RuntimeException('Unexpected end of NodeWalker');
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof CommonMarkParagraph;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers;
use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\MarkupLanguageParser;
use phpDocumentor\Guides\Nodes\SeparatorNode;
/** @extends AbstractBlockParser<SeparatorNode> */
final class SeparatorParser extends AbstractBlockParser
{
public function parse(MarkupLanguageParser $parser, NodeWalker $walker, CommonMarkNode $current): SeparatorNode
{
$walker->next();
return new SeparatorNode(1);
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->getNode() instanceof ThematicBreak;
}
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Markdown\Parsers\Table;
use League\CommonMark\Extension\Table\Table as CommonMarkTable;
use League\CommonMark\Extension\Table\TableCell;
use League\CommonMark\Extension\Table\TableRow as CommonMarkTableRow;
use League\CommonMark\Extension\Table\TableSection;
use League\CommonMark\Node\Node as CommonMarkNode;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Node\NodeWalkerEvent;
use phpDocumentor\Guides\Markdown\ParserException;
use phpDocumentor\Guides\Markdown\Parsers\AbstractBlockParser;
use phpDocumentor\Guides\MarkupLanguageParser as GuidesParser;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\Table\TableColumn;
use phpDocumentor\Guides\Nodes\Table\TableRow;
use phpDocumentor\Guides\Nodes\TableNode;
use Psr\Log\LoggerInterface;
use function sprintf;
/** @extends AbstractBlockParser<TableNode> */
final class TableParser extends AbstractBlockParser
{
/** @param iterable<AbstractBlockParser<Node>> $subParsers */
public function __construct(
private readonly iterable $subParsers,
private readonly LoggerInterface $logger,
) {
}
public function parse(GuidesParser $parser, NodeWalker $walker, CommonMarkNode $current): TableNode
{
$headerRows = [];
$bodyRows = [];
while ($event = $walker->next()) {
$commonMarkNode = $event->getNode();
if ($event->isEntering()) {
if ($commonMarkNode instanceof TableSection) {
if ($commonMarkNode->isHead()) {
$headerRows = $this->parseTableSection($parser, $walker);
continue;
}
$bodyRows = $this->parseTableSection($parser, $walker);
}
continue;
}
if ($commonMarkNode instanceof CommonMarkTable) {
return new TableNode($bodyRows, $headerRows);
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'Header'));
}
throw new ParserException('Unexpected end of NodeWalker');
}
public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof CommonMarkTable;
}
/** @return TableRow[] */
private function parseTableSection(GuidesParser $parser, NodeWalker $walker): array
{
$rows = [];
while ($event = $walker->next()) {
if ($event->isEntering()) {
$rows[] = $this->parseRow($parser, $walker);
continue;
}
if ($event->getNode() instanceof TableSection) {
return $rows;
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $event->getNode()::class, 'Table section'));
}
throw new ParserException('Unexpected end of NodeWalker');
}
private function parseRow(GuidesParser $parser, NodeWalker $walker): TableRow
{
$cells = [];
while ($event = $walker->next()) {
if ($event->isEntering()) {
$cells[] = $this->parseCell($parser, $walker);
continue;
}
if ($event->getNode() instanceof CommonMarkTableRow) {
return new TableRow($cells);
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $event->getNode()::class, 'Table row'));
}
throw new ParserException('Unexpected end of NodeWalker');
}
private function parseCell(GuidesParser $parser, NodeWalker $walker): TableColumn
{
$nodes = [];
while ($event = $walker->next()) {
if ($event->isEntering()) {
foreach ($this->subParsers as $subParser) {
if ($subParser->supports($event)) {
$nodes[] = $subParser->parse($parser, $walker, $event->getNode());
break;
}
}
continue;
}
if ($event->getNode() instanceof TableCell) {
return new TableColumn('', 1, $nodes, 1);
}
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $event->getNode()::class, 'Table Cell'));
}
throw new ParserException('Unexpected end of NodeWalker');
}
}