Процессор для Markdown

Markdown-файлы в современных проектах часто выступают не только как документация, но и как контейнер для исполняемых фрагментов JavaScript. Внутри таких файлов встречаются блоки кода, примеры API, конфигурации и тестовые сниппеты, которые должны соответствовать тем же стандартам качества, что и основной код базы.

Проблема возникает из-за того, что линтер, ориентированный на анализ JavaScript, работает с файлами .js, .ts, но Markdown представляет собой смешанный формат. Внутри одного файла могут одновременно существовать текстовая разметка и множество независимых JavaScript-фрагментов.

Именно для решения этой задачи в архитектуре ESLint предусмотрен механизм processor, позволяющий преобразовывать входной файл в набор виртуальных файлов, пригодных для анализа.


Архитектура Processor в ESLint

Processor в ESLint — это слой предварительной обработки, который отвечает за:

  • извлечение фрагментов кода из нестандартных форматов;
  • преобразование исходного файла в набор виртуальных JavaScript-модулей;
  • обратное сопоставление сообщений об ошибках с оригинальным источником.

Процессор реализует два ключевых метода:

  • preprocess(text, filename) — разбор входного файла и выделение блоков кода;
  • postprocess(messages, filename) — агрегация результатов линтинга и сопоставление с исходным файлом.

Таким образом, Markdown-файл не анализируется напрямую как JavaScript. Вместо этого создаётся набор временных модулей, которые проходят стандартный цикл линтинга.


Принцип работы Markdown Processor

Markdown содержит структурированные блоки вида:

# Документация

Пример функции:

```js
function sum(a, b) {
  return a + b
}

Processor извлекает только содержимое fenced code blocks, игнорируя текстовую часть.

На этапе `preprocess` происходит:

- разбиение Markdown на токены;
- поиск блоков ```js, ```javascript, ```ts;
- создание массива виртуальных файлов.

Пример результата:

```js
[
  {
    text: "function sum(a, b) { return a + b }",
    lineOffset: 4
  }
]

Каждый блок превращается в отдельный файл, который затем передаётся в стандартный пайплайн ESLint.


eslint-plugin-markdown и обработка fenced code blocks

Наиболее распространённая реализация Markdown Processor — плагин eslint-plugin-markdown.

Он автоматически подключает processor для файлов .md, расширяя возможности анализа.

Ключевая особенность заключается в том, что каждый кодовый блок рассматривается как изолированный JavaScript-файл.

Поддерживаются языковые идентификаторы:

  • js
  • javascript
  • mjs
  • иногда ts (в зависимости от конфигурации)

Блоки без указания языка игнорируются, так как невозможно гарантировать их синтаксическую корректность.


Виртуальные файлы и сопоставление ошибок

После этапа preprocess ESLint работает с набором виртуальных документов.

Каждый виртуальный файл получает:

  • собственное имя (например, file.md[1]);
  • смещённые координаты строк;
  • независимый AST.

Когда линтер обнаруживает ошибку, сообщение содержит координаты относительно виртуального файла. Затем postprocess выполняет обратное преобразование:

  • вычисляет смещение относительно исходного Markdown;
  • привязывает сообщение к конкретному code fence;
  • возвращает агрегированный список ошибок.

Это позволяет отображать ошибки прямо в Markdown-файле, сохраняя контекст документации.


Конфигурация обработки Markdown

Подключение Markdown Processor обычно происходит через конфигурацию ESLint:

module.exports = {
  overrides: [
    {
      files: ["*.md"],
      processor: "markdown/markdown"
    }
  ]
}

После этого ESLint начинает обрабатывать Markdown как контейнер для JavaScript-блоков.

Дополнительно можно включать правила, ориентированные на документацию:

module.exports = {
  plugins: ["markdown"],
  overrides: [
    {
      files: ["*.md"],
      processor: "markdown/markdown",
      rules: {
        "no-undef": "error",
        "no-unused-vars": "warn"
      }
    }
  ]
}

Парсинг и выбор JavaScript-движка

Каждый извлечённый блок проходит через стандартный parser ESLint. Чаще всего используется встроенный парсер Espree.

Возможные альтернативы:

  • Babel parser для поддержки современных возможностей ECMAScript;
  • TypeScript parser для анализа TS-блоков внутри Markdown.

Важно, что Markdown Processor не заменяет parser, а только подготавливает данные для него.


Ограничения обработки Markdown

Несмотря на гибкость, Markdown Processor имеет ряд ограничений:

  • отсутствует анализ взаимосвязей между code blocks;
  • каждый блок рассматривается изолированно;
  • невозможна проверка контекста между примерами;
  • сложные вложенные структуры Markdown могут приводить к неточному разбору;
  • не все языковые идентификаторы поддерживаются одинаково.

Также существует ограничение на точность отображения ошибок при глубокой вложенности документации, особенно если code fences расположены внутри нестандартных Markdown-расширений.


Внутренняя модель преобразования

Processor строит промежуточное представление документа:

  1. Исходный Markdown разбивается на последовательность сегментов.
  2. Каждый сегмент классифицируется как текст или код.
  3. Кодовые сегменты нормализуются.
  4. Формируется список виртуальных источников.

Упрощённо:

Markdown → Segments → Code Blocks → Virtual JS Files → ESLint Core → Results → Mapping

Эта модель позволяет сохранить единый пайплайн анализа для всех типов входных данных.


Пользовательские процессоры

ESLint допускает создание собственных processors, что расширяет применение Markdown-подобной логики.

Базовая структура:

module.exports = {
  preprocess(text, filename) {
    return extractBlocks(text)
  },

  postprocess(messages, filename) {
    return mergeMessages(messages)
  },

  supportsAutofix: false
}

Такой подход позволяет:

  • обрабатывать нестандартные форматы документации;
  • интегрировать собственные DSL внутри Markdown;
  • расширять поддержку кастомных языков.

Связь с экосистемой JavaScript-инструментов

Markdown Processor часто используется в связке с инструментами документирования:

  • генераторы документации;
  • системы статической проверки примеров;
  • CI-пайплайны, валидирующие код в README.

В среде Node.js подобные процессы особенно важны, поскольку позволяют поддерживать консистентность между документацией и реальным кодом исполнения.


Поведение правил внутри Markdown

Не все правила ESLint одинаково полезны для Markdown-контекста. Некоторые категории правил особенно актуальны:

  • проверка переменных (no-undef, no-unused-vars);
  • контроль синтаксиса ES;
  • ограничения на использование устаревших конструкций;
  • правила форматирования кода внутри примеров.

При этом правила, завязанные на структуру проекта (например, импорт модулей), могут работать некорректно из-за отсутствия полноценного контекста файла.


Механика source map внутри Markdown Processor

Каждый code block получает собственную систему координат. Processor хранит метаданные:

  • начальная строка блока в Markdown;
  • смещение символов;
  • язык блока;
  • индекс виртуального файла.

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


Производительность обработки

При работе с большими Markdown-документами возникают особенности производительности:

  • рост числа виртуальных файлов увеличивает нагрузку на parser;
  • повторный парсинг одинаковых блоков может замедлять CI;
  • оптимизация достигается кэшированием результатов preprocess.

В крупных документационных системах часто применяются стратегии:

  • фильтрация только JS-блоков;
  • исключение больших текстовых разделов;
  • ограничение глубины анализа.

Роль Markdown Processor в архитектуре линтинга

Processor выступает связующим звеном между нетипичными форматами данных и ядром ESLint. Он обеспечивает:

  • унификацию анализа;
  • расширяемость;
  • переносимость правил между форматами;
  • сохранение стандартного pipeline проверки.

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