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,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');
}
}