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 1

Kategorie

programowanie

Dziękujemy!
()

Powiązane wpisy

Obraz ilustrujacy Automatyczny motyw Bootstrap 53 na bazie pory dnia
4 stycznia 2025 6 min 71

Automatyczny motyw Bootstrap 5.3 na bazie pory dnia

php
Czytaj więcej
Obraz ilustrujacy Wyraenia regularne i ich obsuga w PHP oraz JavaScript
1 lutego 2025 5 min 19

Wyrażenia regularne i ich obsługa w PHP oraz JavaScript

php
Czytaj więcej
Grafika przedstawia Klasa Promise w PHP i jej zastosowanie
5 lutego 2025 7 min 39

Klasa Promise w PHP i jej zastosowanie

php
Czytaj więcej
Wymiana doświadczeń

Masz podobne doświadczenia?

Chętnie poznam Twoją perspektywę i porozmawiam o tym temacie szerzej.

Napisz do mnie

Każda perspektywa może wnieść coś wartościowego do dyskusji.

Twoja prywatność i pliki cookies

  1. Ta strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania – m.in. do poprawnego wyświetlania treści, zapamiętania podstawowych ustawień przeglądarki oraz zapewnienia stabilności serwisu.
  2. Nie stosuję plików cookies w celach marketingowych, reklamowych ani analitycznych.
  3. Strona ma charakter wyłącznie informacyjny i nie zawiera formularzy kontaktowych, rejestracyjnych ani zakupowych, przez które dane mogłyby być przesyłane na serwer.
  4. Nie zbieram danych osobowych podczas zwykłego korzystania z witryny.
  5. Serwis nie korzysta z certyfikatu SSL, jednak ze względu na informacyjny charakter strony nie jest wymagane przesyłanie poufnych danych. Zalecam jednak, aby nigdy nie wpisywać haseł ani danych osobowych na stronach bez szyfrowanego połączenia.
  6. Korzystając z tej strony, wyrażasz zgodę na używanie wyłącznie niezbędnych plików cookies.

Więcej informacji znajdziesz w mojej polityce prywatności.