Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Obsługa Markdown w PHP przy użyciu CommonMark 2.x

Obraz Markdown

+ tworzenie własnego rozszerzenia @[tekst]{atrybuty}

Ten artykuł pokazuje:

  1. Jak działa architektura league/commonmark
  2. Jak skonfigurować parser w wersji 2.x
  3. Jak napisać własne rozszerzenie inline
  4. Jak dodać obsługę .class, #id i dowolnych atrybutów
  5. Na co uważać w kontekście bezpieczeństwa

1. Wprowadzenie

Biblioteka The PHP League CommonMark (league/commonmark) to nowoczesna implementacja specyfikacji Markdown w PHP, zgodna ze standardem CommonMark.

Wersja 2.x wprowadziła:

  • nowy system środowisk (Environment)
  • wyraźny podział na parsery i renderery
  • rozszerzalną architekturę opartą o Extension API
  • wsparcie dla GFM (GitHub Flavored Markdown)

2. Instalacja

composer require league/commonmark

3. Architektura CommonMark 2.x

Pipeline przetwarzania wygląda tak:

Markdown
   ↓
Block Parsers
   ↓
Inline Parsers
   ↓
AST (Abstract Syntax Tree)
   ↓
Renderery
   ↓
HTML

Kluczowe komponenty:

Element Odpowiedzialność
Environment Rejestruje rozszerzenia
Extension Dodaje parsery i renderery
InlineParser Rozpoznaje składnię inline
Node Reprezentuje element AST
Renderer Generuje HTML

4. Konfiguracja środowiska

Minimalna konfiguracja:

use League\CommonMark\Environment\Environment;
use League\CommonMark\MarkdownConverter;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;

$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());

$converter = new MarkdownConverter($environment);

echo $converter->convert('# Hello');

5. Tworzenie własnego rozszerzenia inline

Załóżmy, że chcemy obsłużyć składnię:

@[tekst]{.class #id data-x=1}

która generuje:

<span class="class" id="id" data-x="1">tekst</span>

5.1 Tworzenie Node

namespace App\Markdown;

use League\CommonMark\Node\Inline\AbstractInline;

class SpanNode extends AbstractInline
{
    public function __construct(
        private string $content,
        private array $attributes = []
    ) {}

    public function getContent(): string
    {
        return $this->content;
    }

    public function getAttributes(): array
    {
        return $this->attributes;
    }
}

5.2 Parser Inline

Najważniejsze elementy:

  • regex dopasowujący składnię
  • getPriority() (ważne!)
  • advanceBy() (konieczne w 2.x)
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;

class SpanParser implements InlineParserInterface
{
    public function getMatchDefinition(): InlineParserMatch
    {
        return InlineParserMatch::regex('@\[(.*?)\]\{(.*?)\}');
    }

    public function getPriority(): int
    {
        return 150;
    }

    public function parse(InlineParserContext $context): bool
    {
        $cursor = $context->getCursor();
        $matches = $context->getMatches();

        $text = $matches[1];
        $attrString = $matches[2];

        $attributes = $this->parseAttributes($attrString);

        $node = new SpanNode($text, $attributes);
        $context->getContainer()->appendChild($node);

        $cursor->advanceBy(strlen($matches[0]));

        return true;
    }

    private function parseAttributes(string $input): array
    {
        $attributes = [];
        $classes = [];

        preg_match_all('/
            (\.[\w-]+)
            |(\#[\w-]+)
            |([\w-]+)=(".*?"|\'.*?\'|[^\s]+)
        /x', $input, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {

            if (!empty($match[1])) {
                $classes[] = substr($match[1], 1);
            } elseif (!empty($match[2])) {
                $attributes['id'] = substr($match[2], 1);
            } elseif (!empty($match[3])) {
                $attributes[$match[3]] = trim($match[4], '"\'');
            }
        }

        if ($classes) {
            $attributes['class'] = implode(' ', $classes);
        }

        return $attributes;
    }
}

5.3 Renderer

use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Util\HtmlElement;

class SpanRenderer implements NodeRendererInterface
{
    public function render($node, ChildNodeRendererInterface $childRenderer)
    {
        return new HtmlElement(
            'span',
            $node->getAttributes(),
            $node->getContent()
        );
    }
}

5.4 Rejestracja jako Extension

Zamiast rejestrować wszystko ręcznie, lepiej stworzyć pełne rozszerzenie:

use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Environment\EnvironmentBuilderInterface;

class SpanExtension implements ExtensionInterface
{
    public function register(EnvironmentBuilderInterface $environment): void
    {
        $environment->addInlineParser(new SpanParser());
        $environment->addRenderer(SpanNode::class, new SpanRenderer());
    }
}

Rejestracja:

$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new SpanExtension());

6. Bezpieczeństwo

Jeśli Markdown pochodzi od użytkownika:

  • rozważ whitelistę atrybutów
  • wyłącz html_input lub ustaw na strip
  • rozważ użycie DisallowedRawHtmlExtension

Przykład:

$config = [
    'html_input' => 'strip',
    'allow_unsafe_links' => false,
];

7. Wydajność

Regex inline jest wygodny, ale:

  • może być wolniejszy przy dużych dokumentach
  • może powodować backtracking

Dla systemów CMS warto rozważyć parser oparty o analizę znak po znaku zamiast regex.

8. Podsumowanie

CommonMark 2.x daje:

  • modularną architekturę
  • bezpieczne przetwarzanie Markdown
  • pełną rozszerzalność
  • możliwość tworzenia własnej składni inline i block

Dzięki temu możesz:

  • tworzyć własne komponenty (@[...])
  • implementować DSL wewnątrz Markdown
  • budować systemy dokumentacji
  • rozszerzać GFM
27 lutego 2026 0

Kategorie

programowanie

Dziękujemy!
()

Powiązane wpisy


Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.