Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Enable basic Markdown files #640

Merged
merged 12 commits into from
Oct 31, 2023
60 changes: 60 additions & 0 deletions packages/guides-markdown/resources/config/guides-markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,74 @@
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\HeaderParser;
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\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 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(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(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(MarkupLanguageParser::class)
->arg('$parsers', tagged_iterator('phpdoc.guides.markdown.parser.blockParser'))
->tag('phpdoc.guides.parser.markupLanguageParser');
};
119 changes: 17 additions & 102 deletions packages/guides-markdown/src/Markdown/MarkupLanguageParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,18 @@

use League\CommonMark\Environment\Environment as CommonMarkEnvironment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Node\Block\Document;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Node\NodeWalker;
use League\CommonMark\Parser\MarkdownParser;
use phpDocumentor\Guides\Markdown\Parsers\ListBlock;
use phpDocumentor\Guides\Markdown\Parsers\Paragraph;
use phpDocumentor\Guides\Markdown\Parsers\ThematicBreak;
use phpDocumentor\Guides\MarkupLanguageParser as MarkupLanguageParserInterface;
use phpDocumentor\Guides\Nodes\AnchorNode;
use phpDocumentor\Guides\Nodes\CodeNode;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
use phpDocumentor\Guides\Nodes\ListNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\ParagraphNode;
use phpDocumentor\Guides\Nodes\RawNode;
use phpDocumentor\Guides\Nodes\SpanNode;
use phpDocumentor\Guides\Nodes\TitleNode;
use phpDocumentor\Guides\ParserContext;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\String\Slugger\AsciiSlugger;

use function md5;
use function sprintf;
use function strtolower;

final class MarkupLanguageParser implements MarkupLanguageParserInterface
Expand All @@ -42,23 +26,16 @@ final class MarkupLanguageParser implements MarkupLanguageParserInterface

private ParserContext|null $parserContext = null;

/** @var ParserInterface<Node>[] */
private readonly array $parsers;

private DocumentNode|null $document = null;
private readonly AsciiSlugger $idGenerator;

public function __construct()
{
/** @param iterable<ParserInterface<Node>> $parsers */
public function __construct(
private readonly LoggerInterface $logger,
private readonly iterable $parsers,
) {
$cmEnvironment = new CommonMarkEnvironment(['html_input' => 'strip']);
$cmEnvironment->addExtension(new CommonMarkCoreExtension());
$this->markdownParser = new MarkdownParser($cmEnvironment);
$this->idGenerator = new AsciiSlugger();
$this->parsers = [
new Paragraph(),
new ListBlock(),
new ThematicBreak(),
];
}

public function supports(string $inputFormat): bool
Expand All @@ -81,92 +58,30 @@ private function parseDocument(NodeWalker $walker, string $hash): DocumentNode
$this->document = $document;

while ($event = $walker->next()) {
$node = $event->getNode();

foreach ($this->parsers as $parser) {
if (!$parser->supports($event)) {
continue;
}
$commonMarkNode = $event->getNode();

$document->addChildNode($parser->parse($this, $walker));
}

// ignore all Entering events; these are only used to switch to another context and context switching
// is defined above
if ($event->isEntering()) {
continue;
}

if ($node instanceof Document) {
return $document;
}

if ($node instanceof Heading) {
$content = $node->firstChild();
if ($content instanceof Text === false) {
continue;
// Use entering events for context switching
foreach ($this->parsers as $parser) {
if ($parser->supports($event)) {
$document->addChildNode($parser->parse($this, $walker, $commonMarkNode));
break;
}
}

$title = new TitleNode(
InlineCompoundNode::getPlainTextInlineNode($content->getLiteral()),
$node->getLevel(),
$this->idGenerator->slug($content->getLiteral())->lower()->toString(),
);
$document->addChildNode($title);
continue;
}

if ($node instanceof Text) {
$spanNode = new SpanNode($node->getLiteral(), []);
$document->addChildNode($spanNode);
continue;
}

if ($node instanceof Code) {
$spanNode = new CodeNode([$node->getLiteral()]);
$document->addChildNode($spanNode);
continue;
}

if ($node instanceof Link) {
$spanNode = new AnchorNode($node->getUrl());
$document->addChildNode($spanNode);
continue;
}

if ($node instanceof FencedCode) {
$spanNode = new CodeNode([$node->getLiteral()]);
$document->addChildNode($spanNode);
continue;
}

if ($node instanceof HtmlBlock) {
$spanNode = new RawNode($node->getLiteral());
$document->addChildNode($spanNode);
continue;
if ($commonMarkNode instanceof Document) {
return $document;
}

echo 'DOCUMENT CONTEXT: I am '
. 'leaving'
. ' a '
. $node::class
. ' node'
. "\n";
$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'Document'));
}

return $document;
}

public function parseParagraph(NodeWalker $walker): ParagraphNode
{
return (new Paragraph())->parse($this, $walker);
}

public function parseListBlock(NodeWalker $walker): ListNode
{
return (new ListBlock())->parse($this, $walker);
}

public function getParserContext(): ParserContext
{
if ($this->parserContext === null) {
Expand Down
3 changes: 2 additions & 1 deletion packages/guides-markdown/src/Markdown/ParserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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;
Expand All @@ -13,7 +14,7 @@
interface ParserInterface
{
/** @return TValue */
public function parse(GuidesParser $parser, NodeWalker $walker): Node;
public function parse(GuidesParser $parser, NodeWalker $walker, CommonMarkNode $current): Node;

public function supports(NodeWalkerEvent $event): bool;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
* @template TValue as Node
* @implements ParserInterface<TValue>
*/
abstract class AbstractBlock implements ParserInterface
abstract class AbstractBlockParser implements ParserInterface
{
}
62 changes: 62 additions & 0 deletions packages/guides-markdown/src/Markdown/Parsers/BlockQuoteParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

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\Node;
use phpDocumentor\Guides\Nodes\QuoteNode;
use Psr\Log\LoggerInterface;
use RuntimeException;

use function sprintf;

/** @extends AbstractBlockParser<QuoteNode> */
final class BlockQuoteParser 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): QuoteNode
{
$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) {
return new QuoteNode($content);
}

$this->logger->warning(sprintf('"%s" node is not yet supported in context %s. ', $commonMarkNode::class, 'BlockQuote'));
}

throw new RuntimeException('Unexpected end of NodeWalker');
}

public function supports(NodeWalkerEvent $event): bool
{
return $event->isEntering() && $event->getNode() instanceof BlockQuote;
}
}
37 changes: 37 additions & 0 deletions packages/guides-markdown/src/Markdown/Parsers/CodeBlockParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

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 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 && $current->getInfo() !== null) {
$codeNode = $codeNode->withOptions(['caption' => $current->getInfo()]);
}

return $codeNode;
}

public function supports(NodeWalkerEvent $event): bool
{
return $event->getNode() instanceof IndentedCode || $event->getNode() instanceof FencedCode;
}
}
Loading