Перейти к содержимому
Поиск
Разработка

League CommonMark: Markdown в PHP — мастерская и 8 практических сценариев

League CommonMark в PHP: Markdown для статей, справки и комментариев. Восемь живых сценариев, GFM, оглавление, своё расширение, JSON API, CLI и исходники Windows/Linux.

Александр Некрасов
League CommonMark — преобразование Markdown в HTML на PHP

League CommonMark в PHP: Markdown для статей, справки и комментариев. Восемь живых сценариев, GFM, оглавление, своё расширение, JSON API, CLI и исходники Windows/Linux.

League CommonMark: из Markdown в страницу, которую можно использовать

Автор пишет инструкцию, оператор дополняет базу знаний, клиент оставляет комментарий. Им нужны заголовки, списки, ссылки и примеры кода, но вводить HTML вручную неудобно. Markdown даёт простой текстовый формат, а League CommonMark превращает его в HTML на PHP. Это полезный строительный блок для собственного сайта, административной панели, справочника и сервиса поддержки.

Результат вполне конкретный: обычная HTML-разметка, которую можно показать в шаблоне, сохранить в кеше, отправить в систему публикаций или выгрузить отдельным файлом. Библиотека не создаёт CMS, не хранит статьи и не управляет правами пользователей. Эти задачи остаются приложению; конвертер отвечает за переход от текста к структуре документа.

В статье восемь практических сценариев и мини-мастерская с настоящим PHP. Можно менять Markdown, сравнивать расширения, переключать правила обработки HTML, смотреть точный ответ сервера и скачивать результат. В исходниках есть Composer lock, CLI, HTTP endpoint, собственное расширение заметок и команды Windows, Linux и VPS.

Где применять: статьи, справка, задачи и комментарии

В базе знаний Markdown удобен тем, что исходный текст остаётся читаемым без специального редактора. Инструкции можно хранить рядом с кодом, обсуждать изменения в Git и проверять разницу по строкам. Например, команда меняет порядок обработки заявки: редактор исправляет три пункта, а сервер заново строит страницу и оглавление.

В карточках задач такой формат помогает описать критерии готовности, список изменений и технические детали. Таблица подходит для небольшого сравнения вариантов, fenced code — для примера запроса, цитата — для замечания. Однако Markdown-флажок не заменяет поле «выполнено» в базе: это часть текста, пока приложение не связывает его с реальными данными.

В комментариях требования строже: человек может прислать произвольный HTML и необычные ссылки. Поэтому редакционная статья и комментарий должны иметь разные профили возможностей. Для справки доступны оглавление и таблицы, для простого комментария достаточно базовой разметки и явной HTML-политики.

CommonMark, GFM и HTML: что различается

Markdown — семейство близких форматов, поэтому одинаковый текст иногда выглядит по-разному в разных системах. CommonMark задаёт согласованный разбор базового синтаксиса. GitHub-Flavored Markdown, или GFM, добавляет привычные для задач возможности: таблицы, списки с флажками, зачёркивание и дополнительные автоматические ссылки. В PHP-пакете расширения подключаются явно.

Практический вывод: «мы поддерживаем Markdown» недостаточно для контракта редактора. Нужно назвать профиль, показать допустимые конструкции и использовать одинаковую конфигурацию при просмотре и публикации. Иначе человек увидит таблицу в редакторе, а после сохранения получит строки с вертикальными чертами.

HTML остаётся выходным форматом. CommonMark не добавляет дизайн сайта и не подключает подсветку синтаксиса. Вы оформляете заголовки, код и таблицы своими стилями. Возможности GFM собраны в официальном описании расширения. В мастерской результат можно сравнить без установки дополнительных инструментов.

Мини-мастерская: восемь задач на настоящем PHP

Выберите задачу, прочитайте подсказку «Попробуйте» и измените исходный текст. Кнопка «Преобразовать в PHP» отправляет ограниченный JSON-запрос к конвертеру. HTML возвращает установленный League CommonMark 2.10.7; другого Markdown-парсера в браузере нет. Время рядом с результатом измеряется внутри PHP-конвертации и не включает сетевой путь целиком.

Сравнивайте точный HTML ответа и изолированный предпросмотр. HTML показывает работу библиотеки; предпросмотр дополнительно отключает внешние переходы и сетевые изображения. Это позволяет исследовать пользовательский текст внутри публикации. Для работы с большим редактором откройте мастерскую отдельной вкладкой.

Изменение текста или профиля очищает старый результат, чтобы он не выглядел актуальным. Есть сброс, следующий пример, отмена ожидания, настоящая ошибка размера, выбор документа пакета и выгрузка HTML/JSON. Наш код ничего не сохраняет: используйте учебные данные, не вставляйте пароли, токены или переписку клиентов. Текст передаётся серверу только после нажатия кнопки.

Архитектура проекта: один конвертер для веба и CLI

Главное правило проекта — общая логика преобразования. Класс Converter создаёт окружение, подключает доверенные расширения, проверяет размер текста и возвращает HTML с метаданными. HTTP endpoint и консольная команда вызывают этот класс, поэтому правила не расходятся между ручным просмотром и пакетным экспортом.

В public находятся страница мастерской и HTTP-вход. Выше web root лежат src, vendor, Composer-файлы, примеры и тесты. Посетителю не нужно видеть техническую структуру проекта. Скачиваемый архив собирается отдельно, как осознанно опубликованный набор исходников. При размещении внутри WordPress дополнительно закрывается прямой веб-доступ к каталогам runtime.

Профили имеют фиксированные имена: core, gfm, docs, comments, links и notes. Посетитель выбирает имя, но не передаёт PHP-класс, конфигурационный файл или код renderer. Входом служит текст, а не путь на сервере. Такая граница упрощает поддержку и делает поведение воспроизводимым.

project/
  composer.json
  composer.lock
  src/Converter.php
  src/NoteExtension.php
  public/convert.php
  public/index.html
  fixtures/01.md ... 08.md
  bin/batch.php
  tests/run.php

Установка через Composer и требования к PHP

Готовый проект использует PHP 8.2 или новее и расширение mbstring. Версии зависимостей закреплены в composer.lock; при подготовке установлен League CommonMark 2.10.7. Для нового приложения пакет добавляют командой require, а скачанный проект воспроизводят через install. Первая команда меняет набор зависимостей, вторая восстанавливает согласованный lock.

Автозагрузчик подключается один раз на входе приложения. Дальше классы доступны по пространствам имён, без ручного перечисления файлов пакета. Если Composer сообщает о недостающем mbstring, исправьте окружение PHP, а не используйте игнорирование требований. Иначе установка пройдёт, но выполнение на реальном тексте может сломаться.

Подробности установки, обновлений, автозагрузки и различий install/update разобраны в нашем руководстве по Composer. Требования пакета доступны в официальной инструкции установки. Перед развёртыванием проверьте требования именно на том runtime, который будет выполнять веб-запросы.

# Новый проект:
composer require league/commonmark:^2.10.7
# Скачанный проект:
composer install --no-interaction --prefer-dist
composer check-platform-reqs
composer test

Первое преобразование: Markdown → HTML

Короткий пример создаёт CommonMarkConverter и вызывает convert. Возвращаемый результат приводится к строке, когда нужен именно HTML. Даже в первом примере полезно явно указать правила raw HTML и опасных ссылок: тогда код не зависит от того, насколько доверенным оказался источник текста сегодня.

Для одного документа такого объекта достаточно. В большом приложении настройку лучше вынести в сервис, чтобы обработчики не собирали её каждый по-своему. Изменения конфигурации считаются изменениями поведения контента: добавление GFM может превратить обычный текст в таблицу, а новый renderer — изменить ожидаемый HTML.

Перед выводом определите контекст. HTML-фрагмент предназначен для содержимого страницы; его нельзя бездумно вставлять в JavaScript-строку, HTML-атрибут или URL. Выход конвертера остаётся отдельным типом данных. Если шаблон по умолчанию экранирует строки, вывод готовой разметки должен быть осознанным решением в конкретном месте.

require __DIR__ . '/vendor/autoload.php';
use League\CommonMark\CommonMarkConverter;

$converter = new CommonMarkConverter([
    'html_input' => 'escape',
    'allow_unsafe_links' => false,
    'max_nesting_level' => 20,
    'max_delimiters_per_line' => 200,
]);
$html = (string) $converter->convert("# Инструкция\n\n**Начните здесь.**");

Пример 1. Карточка статьи или инструкции

Первый сценарий показывает частый путь: автор создаёт заголовок, последовательность действий и цитату. Текст можно хранить в базе или Markdown-файле, а HTML формировать при сохранении либо чтении. Для посетителя важен результат: инструкция легко просматривается, пункты различимы, замечания не смешиваются с обычным абзацем.

Добавьте пункт списка и второй заголовок. Затем откройте HTML: появятся ol, li и h2. Это способ понять синтаксис без запоминания всех правил: ввод, преобразование и структура находятся рядом. Уберите пустую строку между блоками и сравните результат. Поиграйте с нумерованным и ненумерованным списком, чтобы выбрать подходящий вид инструкции.

В настоящей карточке название записи обычно хранится отдельно. Команда договаривается, разрешён ли h1 внутри содержимого и с какого уровня начинаются разделы. Это вопрос структуры сайта, а не автоматическое свойство Markdown. Понятный редакционный шаблон предотвращает случайную иерархию заголовков.

# Как принять заявку

1. Уточните контакт.
2. Запишите задачу.
3. Назначьте исполнителя.

> Сохраните краткий результат звонка.

Пример 2. Changelog, таблица и список готовности

GFM полезен для выпуска продукта, отчёта команды и небольшой задачи. Таблица сравнивает старое и новое поведение, зачёркивание отмечает отменённый вариант, флажки показывают готовность пунктов. В мастерской преобразуйте один документ сначала профилем gfm, затем core: различие расширений становится видимым.

Флажки в HTML являются представлением исходного текста. Они не отправляют изменение на сервер и в нашем примере не редактируются через preview. Для рабочего чек-листа CRM храните пункты структурированно и явно реализуйте сохранение. Markdown удобно использовать для пояснений рядом с таким интерфейсом.

Таблица хороша для нескольких колонок. Большой отчёт лучше показывать отдельным компонентом с фильтрами, сортировкой и доступной навигацией. На телефоне даже небольшой Markdown-таблице нужен контейнер с горизонтальной прокруткой, чтобы она не растягивала страницу. Такое оформление предусмотрено в нашем предпросмотре и автономном HTML.

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

$environment = new Environment([
    'html_input' => 'escape', 'allow_unsafe_links' => false,
    'max_nesting_level' => 20, 'max_delimiters_per_line' => 200,
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new GithubFlavoredMarkdownExtension());
$html = (string) (new MarkdownConverter($environment))->convert($markdown);

Пример 3. Документация с кодом без исполнения

Инструкция разработчика содержит запрос API, PHP-функцию, команду консоли или HTML-компонент. Fenced code отделяет этот текст от обычной разметки. Язык после тройных обратных кавычек попадает в class language-*, но сам по себе не запускает подсветчик и не исполняет программу.

В третьем примере внутри PHP-строки находится HTML. Он должен остаться читаемым образцом. Смотрите результат: угловые скобки экранированы, браузер показывает исходный код вместо создания кнопок или выполнения PHP. Для интерактивной песочницы нужна отдельная архитектура; Markdown-конвертер такой средой не является.

Если добавите подсветку, проверьте её на длинных строках и мобильном экране. Кодовый блок должен прокручиваться внутри собственной области. Принудительный перенос каждой команды иногда делает копирование хуже, поэтому здесь сохранены исходные строки. Отдельно проверьте, что копируется именно код, без декоративных номеров строк и значков оболочки.

Пример 4. Оглавление, якоря и одинаковые заголовки

Для длинной инструкции хочется перейти сразу к нужному шагу. Профиль docs подключает HeadingPermalinkExtension и TableOfContentsExtension: первая создаёт адресуемые заголовки, вторая собирает список ссылок. Маркер [TOC] ставится отдельной строкой, оглавление включает второй и третий уровни.

Добавьте одинаковые заголовки «Проверка» и изучите id: ссылки должны вести в разные места. Префикс doc отделяет якоря документа от идентификаторов оболочки. Переименование раздела после публикации может поменять адрес; для долговечных внешних ссылок нужна договорённость о стабильных идентификаторах.

В конфигурации max_placeholder_entries равен 100. Он ограничивает суммарное число пунктов по всем копиям оглавления, а не количество маркеров [TOC]. Это важно при многочисленных повторениях. Когда бюджет исчерпан, лишний placeholder не превращается в ещё одну полную копию списка. Настройки двух расширений описаны в документации оглавления.

'heading_permalink' => [
    'id_prefix' => 'doc', 'fragment_prefix' => 'doc',
    'apply_id_to_heading' => true,
],
'table_of_contents' => [
    'position' => 'placeholder', 'placeholder' => '[TOC]',
    'min_heading_level' => 2, 'max_heading_level' => 3,
    'max_placeholder_entries' => 100,
],

Пример 5. Комментарии: escape, strip и опасные ссылки

В комментарии человек управляет входным текстом. Пятый сценарий содержит выделение, присланный HTML, script и ссылку с опасным протоколом. Переключение escape/strip показывает два решения: отобразить raw HTML как текст либо удалить его. Разрешения произвольного HTML в публичном интерфейсе нет.

allow_unsafe_links=false применяется при обоих вариантах. Он исключает предусмотренные библиотекой опасные URI, но не делает любую HTTPS-ссылку надёжным ресурсом. Репутация адресата, разрешённые домены и правила переходов — отдельные решения продукта. Проверяйте конфигурацию расширений, с которой будете выводить комментарии.

Настройки HTML, URI и сложности разбора перечислены в официальном разделе безопасности. Если приложение принимает полноценный HTML другого происхождения, нужен отдельный процесс очистки; для клиентской стороны есть мастерская DOMPurify. Сам факт прохождения строки через Markdown не делает её универсально подходящей для любого контекста вывода.

// Aen\MarkdownLab\Converter из проекта:
$escaped = $converter->convert($markdown, 'comments', 'escape');
$stripped = $converter->convert($markdown, 'comments', 'strip');
// В обоих профилях allow_unsafe_links = false.

Пример 6. Ссылки и изображения базы знаний

Изображение часто объясняет процесс лучше абзаца: схема, скриншот панели, последовательность действий. Markdown создаёт img с адресом и альтернативным описанием. PHP-конвертер нашего проекта ничего по этому адресу не скачивает. Уже браузер решает, какие ресурсы загрузить при отображении, поэтому политика изображений является частью приложения.

В preview разрешена встроенная иллюстрация с точным исходным путём assets/diagram.svg. Остальные картинки заменяются подписью, внешние переходы отключаются. Точный HTML ответа виден отдельно. Можно исследовать ссылки, не отправляя запросы на произвольные внешние сайты из предпросмотра.

Профиль links использует ExternalLinkExtension: внешним адресам добавляются выбранные rel и target. Внутренние хосты задаёт разработчик; здесь это ae-nekrasov.ru. Значения rel имеют разные назначения, их не следует воспринимать как универсальную гарантию надёжности ссылки. Варианты настройки представлены в документации внешних ссылок.

'external_link' => [
    'internal_hosts' => ['ae-nekrasov.ru'],
    'open_in_new_window' => true,
    'noopener' => 'external',
    'noreferrer' => 'external',
    'nofollow' => 'external',
],

Пример 7. Своё расширение для заметок

Редакции бывает нужен повторяемый элемент: предупреждение, подсказка или пометка перед импортом. Разрешать ради него произвольный HTML неудобно. В проекте добавлен синтаксис {{note:текст}}, превращающийся в span с фиксированным классом и ролью note. Стиль централизованно задаётся приложением.

Парсер принимает одну строку до 160 символов, без вложенных фигурных скобок. Текст не разбирается как второй Markdown и явно экранируется renderer. Напишите внутри тег: он останется текстом. Ограничения делают формат понятным и позволяют проверить его предметными тестами.

Расширение содержит собственный узел, parser и renderer, затем регистрирует их в окружении. Посетитель не может подменить классы: это доверенный код проекта. Контракты описаны в официальных разделах inline parsing и rendering. Полный рабочий файл, включая регистрацию и экранирование, лежит в архиве.

return new HtmlElement(
    'span',
    ['class' => 'lab-note', 'role' => 'note'],
    htmlspecialchars($node->text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
);

Пример 8. Несколько документов одним запросом

Пакетный режим нужен при переносе справки, просмотре подборки инструкций или подготовке нескольких страниц из задания редактора. В восьмом сценарии документы разделяются отдельной строкой —DOC—. JavaScript превращает их в массив и отправляет JSON, PHP возвращает отдельный результат для каждого документа.

Можно переключать документ, смотреть его HTML и измерения, выгружать весь JSON. Публичный endpoint принимает от одного до пяти документов с общим лимитом 8192 байта. Это демонстрационный предел, показывающий алгоритм без неограниченной пакетной нагрузки на хостинг.

Для больших импортов не увеличивайте предел наугад. Удобнее очередь заданий или CLI с контролем количества файлов, журнала ошибок и повторного запуска. В рабочем процессе нужны правила атомарности: можно ли сохранить успешно обработанные документы, если один из соседних некорректен. Наш endpoint ничего не сохраняет, поэтому вопрос частичной записи здесь не возникает.

$result = $converter->request([
    'documents' => ["# Приём\n\nКонтакт.", "# Выполнение\n\nВремя."],
    'profile' => 'docs',
    'htmlPolicy' => 'escape',
]);

HTTP JSON endpoint: контракт и ошибки

Обработчик convert.php принимает POST с Content-Type application/json. Он проверяет тип входа, поля, профиль и общий размер. Параметров с путями на диске, URL для скачивания или PHP-кодом контракт не содержит. Успех возвращает массив results даже для одного документа: клиентский интерфейс проще поддерживать единообразным.

Ошибки разделены по смыслу. Некорректный JSON получает 400, неправильный метод — 405, превышение размера — 413, Content-Type — 415, неверная структура или профиль — 422. Проблема runtime получает 500 с коротким сообщением. Технический стек и содержимое внутренних файлов посетителю не показываются.

Кнопка ошибки отправляет 8193 байта и получает настоящий ответ сервера. Можно увидеть, как интерфейс очищает старый результат и позволяет повторить запрос. Для приложения согласуйте контракт с формой, чтобы человек понимал, что исправить. Текст ошибки не следует показывать через innerHTML: обычного textContent достаточно.

{
  "markdown": "# Проверка\n\n**Готово**",
  "profile": "core",
  "htmlPolicy": "escape"
}

Лимиты по байтам, вложенности и размеру HTML

Русская буква в UTF-8 обычно занимает больше одного байта, поэтому счётчик символов не заменяет ограничение тела. Редактор показывает байты, сервер независимо повторяет проверку. Markdown ограничен 8 КиБ, JSON — 64 КиБ: оболочка и экранирование делают тело больше самого текста.

Дополнительно ограничены вложенность блоков, число emphasis-разделителей на строке, суммарные пункты оглавлений и HTML на выходе. Это разные ограничения: короткий текст с повторениями тоже способен породить заметно больший результат. Объём ответа проверяется после конвертации, лимиты процесса и веб-сервера ограничивают ресурсный контекст выполнения.

Значения выбраны для маленькой публичной мастерской, а не объявлены универсальными. Подберите их по реальным документам и нагрузке. Для длинных руководств разумен отдельный авторизованный канал, где известны пользователь, объём и способ обработки ошибки. Сохранение черновика и конвертацию тоже можно разделить, чтобы ошибка предпросмотра не уничтожала работу автора.

if (strlen($markdown) > 8192) {
    throw new LengthException('Лимит Markdown — 8192 байта UTF-8.');
}
if (!mb_check_encoding($markdown, 'UTF-8')) {
    throw new InvalidArgumentException('Текст должен быть UTF-8.');
}

Изолированный preview и точный HTML

HTML ответа вставляется в кодовое окно через textContent. Для просмотра создаётся копия: сначала она помещается в неактивный template, затем удаляются внешние переходы и заменяются изображения. Только после этого документ передаётся iframe. Внешние картинки не нужны даже для самого разбора preview.

У iframe пустой sandbox, внутри документа задана CSP с запретом ресурсов по умолчанию. Разрешены встроенные данные изображения и стили оформления. Скрипты не запускаются, формы не отправляются, preview не получает доступа к родительской странице. Якоря оглавления остаются доступными, потому что помогают исследовать документ.

Эта оболочка не является универсальным HTML-редактором. Здесь разметка получена от ограниченного конвертера и доверенных renderer. Если источник другой, модель доверия меняется. Разделяйте форматирование, допустимую разметку и изоляцию интерфейса: три задачи требуют собственных решений и не заменяют друг друга.

Экспорт: что скачивается в HTML и JSON

«Скачать HTML предпросмотра» сохраняет автономный документ предпросмотра: оформление, CSP, встроенную разрешённую иллюстрацию и отключённые внешние переходы. Его удобно открыть отдельно и показать коллегам. Это ограниченная просмотрная версия, поэтому она может отличаться от фрагмента для рабочего сайта.

«Скачать JSON» сохраняет точный ответ PHP с HTML каждого документа и метаданными. «Копировать HTML» берёт тот же фрагмент без изменений preview. Различие подписано в интерфейсе, чтобы человек не перепутал изоляцию демонстрации с поведением самой библиотеки.

Для публикации используйте контролируемый шаблон и выбранный профиль. Проверьте заголовки, мобильные таблицы, ссылки и изображения в конечном окружении. Один HTML выглядит по-разному с разными стилями, а корректный Markdown автоматически не гарантирует удобную вёрстку. Для переноса в CMS нужны также название, описание, категория и URL, которых конвертер не придумывает.

Windows: запуск мастерской и проверка API

На Windows убедитесь, что php и composer относятся к ожидаемой установке. php –ini показывает загруженный конфигурационный файл; именно в нём включается mbstring. После изменения проверьте php -m и требования Composer. Несколько установок PHP часто объясняют ситуацию «расширение уже включено, а ошибка осталась».

Встроенный сервер обслуживает public и endpoint из одного origin, поэтому дополнительный CORS не нужен. Привязка к 127.0.0.1 оставляет стенд локальным. После работы остановите своё окно через Ctrl+C; такой процесс не предназначен для постоянной публикации на VPS.

В README есть запрос Invoke-RestMethod с явным UTF-8. Это устраняет путаницу между псевдонимом curl и curl.exe, строкой PowerShell и фактическими байтами тела. Сначала получите успешный ответ на короткий русский текст, затем переходите к своим документам. Открытие index.html двойным щелчком не запустит PHP, поэтому нужен именно локальный сервер.

php --version
php --ini
php -m | Select-String mbstring
composer install --no-interaction --prefer-dist
composer check-platform-reqs
composer test
composer build-public
php -S 127.0.0.1:8501 -t public
# Открыть http://127.0.0.1:8501/

Linux: тот же проект без изменения PHP-кода

На Linux нужны поддерживаемый PHP, mbstring и Composer. Имена пакетов ниже подходят для типичного Debian/Ubuntu, но выпуск дистрибутива определяет доступные версии. Проверьте их перед установкой, особенно на действующем сервере с другими сайтами. Не меняйте системный PHP вслепую ради демонстрации.

После install запускаются те же тесты и локальный сервер. Для знакомства достаточно 127.0.0.1; при работе через SSH используйте туннель либо настраивайте отдельный виртуальный хост с PHP-FPM. Открывать встроенный сервер в интернет для постоянной эксплуатации не нужно.

CLI и FPM могут использовать разные ini и расширения. Успешный консольный тест не доказывает, что веб-процесс видит mbstring и autoload. После настройки FPM проверьте реальный HTTP-запрос и ошибку, а не только главную HTML-страницу. Такое разделение быстро показывает, проблема в коде конвертера или в конфигурации веб-окружения.

sudo apt update
sudo apt install php-cli php-mbstring php-xml php-curl php-zip unzip composer
php -v
php -m | grep mbstring
composer install --no-interaction --prefer-dist
composer check-platform-reqs
composer test
php -S 127.0.0.1:8501 -t public

Linux/VPS: Nginx, PHP-FPM и web root

Разместите проект в /srv/markdown-lab, корнем хоста сделайте /srv/markdown-lab/public. Тогда vendor, src, тесты и lock не доступны по обычным URL. Пользователь развёртывания устанавливает зависимости; FPM достаточно чтения кода. Наш stateless endpoint не требует права записи в проект.

В Nginx разрешите выполнение ровно convert.php, прочие PHP-пути отклоняйте. Размер тела ограничивается ещё до PHP. README содержит полный фрагмент с rate limit и проверкой конфигурации. Сокет php8.3-fpm.sock приведён как пример: проверьте фактический путь в /run/php и выбранную версию.

HTTPS подключается для реального домена через принятый ACME-процесс или Caddy. Фрагмент маршрутизации сам сертификат не выдаёт. Создайте отдельный хост, выполните nginx -t, затем reload. После размещения повторите успешный POST, ошибку размера и загрузку исходников. Конфигурация работающих соседних сайтов должна оставаться независимой от нового стенда.

root /srv/markdown-lab/public;
client_max_body_size 64k;
location = /convert.php {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root/convert.php;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_read_timeout 5s;
}
location ~ \.php$ { return 404; }
location / { try_files $uri $uri/ =404; }

Нагрузка: предел запроса не заменяет предел параллелизма

Маленькие запросы тоже становятся проблемой, когда приходят одновременно в большом количестве. Для публичного endpoint нужны ограничения прокси и FPM-пула. README показывает Nginx limit_req; значения соотнесите с ресурсами VPS и обычным поведением редактора. Слишком жёсткий предел мешает сотрудникам за общим адресом.

Если перед сервером CDN или reverse proxy, адрес клиента требует доверенной конфигурации. Нельзя брать произвольный X-Forwarded-For за настоящий IP. Иначе ограничение применится ко всем посетителям как к одному человеку или будет обходиться подставленным заголовком.

При редактировании полезно преобразовывать по кнопке либо с задержкой и отменой устаревшего ожидания. Мастерская использует кнопку. Отмена прерывает ожидание клиента, но начавшаяся короткая операция PHP может завершиться. Интерфейс сообщает именно это и не обещает остановить серверный процесс. Поздний ответ не должен подменять результат уже изменённого документа.

CLI: преобразование каталога Markdown

Консольный вариант подходит для переноса справки и сборки страниц. Команда перебирает Markdown-файлы каталога, проверяет размер и UTF-8, вызывает общий Converter и сохраняет HTML отдельно. Обход подкаталогов не выполняется, символические ссылки на входные файлы отклоняются.

CLI не перезаписывает существующие результаты. Это помогает заметить повторный запуск и избежать потери уже отредактированных файлов. Для новой сборки выберите новый выходной каталог. Ошибка файла отражается в stderr, обработка остальных продолжается, итоговый exit code становится ненулевым.

Восьмой учебный файл содержит разделители пакетного редактора; CLI воспринимает его как единый документ. При настоящем переносе каждый материал кладите в отдельный файл. После преобразования остаются редакционные задачи: название страницы, URL, описание и проверка ссылок. Если каталог пуст, обработка не создаёт файлов; в своей сборке отдельно проверяйте ожидаемое количество результатов, чтобы пустая выгрузка не считалась готовым релизом.

php bin/batch.php fixtures var/export-01 docs
php bin/batch.php ./my-markdown ./export-02 gfm

Исходник, кеш и обновление библиотеки

Для редактируемой базы знаний полезно хранить Markdown. HTML вычисляется при чтении, сохранении или в фоне. Если сохраняете готовый HTML, храните версию профиля: изменение расширений и правил может потребовать пересборки старых материалов. Исходник позволяет сделать это без обратного преобразования разметки.

Ключ кеша должен учитывать текст, значимые настройки, версии renderer и расширений. Иначе после изменения политики документ продолжит приходить из старого кеша. Удобен собственный content_profile_version и хеш Markdown конкретной редакции. Такая схема также помогает объяснить, почему два одинаковых текста были преобразованы по-разному.

Обновляйте lock в проверяемой ветке. Прогоните фикстуры, сравните важные фрагменты HTML, затем проверьте страницу с настоящими стилями. Новая версия не гарантирует прежний внешний вид всех текстов. Набор характерных документов делает обновление контролируемым, а повторный composer audit помогает следить за состоянием зависимостей.

Встраивание в Twig, CMS и админку

Сервис конвертации вызывается перед рендером шаблона. Контроллер получает статью, выбирает доверенный профиль, формирует HTML и передаёт его отдельным полем. Название, описание, дата и права доступа остаются структурированными данными. Markdown отвечает за содержимое, а не за все свойства записи.

В Twig обычные строки экранируются по умолчанию. Вывод готового HTML требует осознанного решения в конкретном месте. Нельзя ставить raw на весь пользовательский ввод лишь потому, что часть данных прошла Markdown. Определите границу: какой фрагмент произведён вашим ограниченным Converter и куда он вставляется.

С шаблонами, компонентами и письмами поможет статья о Twig. Если нужен полноценный сайт документации с навигацией и сборкой, посмотрите материал о Docus. Это другой уровень решения, который может оказаться удобнее самостоятельной сборки оболочки вокруг конвертера.

Мобильный интерфейс и доступность контента

Markdown бывает структурно корректным, но неудобным на телефоне. Команда, широкая таблица и непрерывный URL способны растянуть контейнер. В мастерской код и таблицы прокручиваются внутри своих областей, картинки ограничены шириной, кнопки переносятся на следующую строку. Это сохраняет доступность основных действий на узком экране.

У редактора, профиля и результата есть подписи; состояние операции объявляется live region. Кнопки доступны с клавиатуры, текущий вид результата имеет aria-pressed. Предпросмотр получил отдельное название, чтобы человек понимал, в какую вложенную область попал. Сброс не выполняет скрытую отправку текста.

Доступность материала начинается с автора: последовательные заголовки, содержательный alt, понятные ссылки и небольшие таблицы. Автоматический HTML не исправляет бессмысленное «нажмите здесь» или скриншот без пояснения. Проверяйте страницу как человек, которому нужно выполнить задачу. Эмуляция viewport помогает проверке вёрстки, но не заменяет все особенности физического телефона.

Тесты: что проверять перед публикацией

Набор проверяет восемь фикстур, UTF-8, GFM, экранирование кода, уникальные якоря и ссылки оглавления. Отдельно проверяются custom renderer, запрет небезопасной HTML-политики, размер, неизвестные поля и пакет. Это проверки наблюдаемого поведения, а не буквальное повторение каждой строки реализации.

Для HTTP дополнительно нужны статусы и тип ответа. В архиве команда composer http-smoke проверяет 22 HTTP-контракта на собственном временном PHP-сервере и закрывает его после теста. Дополнительные Python или Node не нужны. Главная страница не доказывает работу PHP: HTML может отдаваться статическим сервером, тогда convert.php не выполнится. Кнопка ошибки помогает заметить проблему: должен прийти JSON с 413, а не скачивание PHP-файла или страница хостинга.

Команда composer test запускает PHP-проверки без отдельного фреймворка. Повторите их после clean install, затем проверьте браузер на узкой ширине и самостоятельную загрузку архива. После обновления расширений полезно сравнить HTML не только по строке целиком, но и по важным свойствам: ссылка ведёт к существующему id, код экранирован, таблица не потеряла ячейки.

composer install --no-interaction --prefer-dist
composer check-platform-reqs
composer test
composer audit
php -S 127.0.0.1:8501 -t public

Типичные ошибки и способы найти причину

Таблица осталась символами — проверьте GFM и пустые строки. Оглавление не появилось — проверьте оба расширения, маркер отдельной строкой, уровни и бюджет пунктов. Подсветки кода нет — language-* является меткой, а не подключённым подсветчиком.

CLI работает, сайт падает — сравните FPM и CLI, версии, mbstring, ini и чтение vendor. API возвращает HTML вместо JSON — проверьте маршрут, PHP и страницу ошибки прокси. 413 может выдать endpoint или Nginx ещё до PHP; интерфейс должен справляться с обоими ответами.

Картинка не видна только здесь — внешние изображения в preview отключены. HTML-экспорт отличается от JSON — первый сохраняет ограниченную просмотрную версию, второй содержит точный ответ. Поздний результат исчез — при изменении текста старый запрос считается устаревшим. Это предотвращает показ HTML от предыдущей редакции. Если ожидание отменено, повторный запуск остаётся доступным без перезагрузки страницы.

Исходники и официальные материалы

Проект позволяет повторить весь путь: установить зависимости по lock, открыть редактор, вызвать API и преобразовать каталог. В архиве авторские примеры, расширение заметок, тесты, лицензии и README с Windows/Linux/VPS. Его можно использовать как основу справки или предпросмотра в своей административной панели.

Начните с реального сценария: выберите профиль, источник текста, способ хранения и контекст вывода. Затем добавьте ограничения, понятные сообщения и характерные документы для проверки. Когда эта цепочка работает, расширения и дизайн развиваются без потери предсказуемости.

Основные источники: League CommonMark 2.x, конфигурация, расширения, официальный репозиторий. Следующий шаг внутри сайта: Composer, Twig и DOMPurify.