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

Внутренняя модель ESLint основана на предположении, что анализируемый файл можно разобрать в абстрактное синтаксическое дерево JavaScript. Однако реальные проекты часто содержат файлы, в которых JavaScript встроен в другие форматы: HTML, Markdown, Vue SFC, шаблонные системы. В таких случаях исходный текст не является чистым JavaScript, и стандартный парсер ESLint не способен работать напрямую.

Механизм процессоров (processors) решает задачу преобразования нетипичных форматов в набор виртуальных JavaScript-артефактов, пригодных для анализа. Процессор выступает промежуточным слоем между исходным файлом и линтером, извлекая фрагменты кода, подготавливая их к разбору и затем сопоставляя диагностические сообщения обратно с оригинальным источником.

Модель обработки файлов с процессором

Общий конвейер работы ESLint с процессором можно представить как последовательность этапов:

  1. Загрузка исходного файла (например, .html)
  2. Передача содержимого в процессор
  3. Извлечение JavaScript-фрагментов
  4. Генерация виртуальных файлов для анализа
  5. Запуск линтинга на каждом фрагменте
  6. Постобработка результатов
  7. Маппинг сообщений обратно в оригинальный HTML

Ключевое свойство процессора заключается в том, что он не изменяет правила ESLint, а изменяет представление входных данных.

Формат API процессора

Процессор в ESLint реализуется как объект с двумя основными стадиями:

  • preprocess — разбиение исходного файла на фрагменты
  • postprocess — объединение результатов анализа

Дополнительно используется метаинформация о поддерживаемых расширениях.

Структура процессора

module.exports = {
  preprocess(text, filename) {
    return [];
  },

  postprocess(messages, filename) {
    return [];
  },

  supportsAutofix: true
};

Функция preprocess возвращает массив строк, каждая из которых рассматривается ESLint как отдельный виртуальный файл. Функция postprocess получает массив массивов сообщений и должна вернуть единый список диагностик.

Разбор HTML и извлечение JavaScript

HTML-файлы могут содержать JavaScript в нескольких местах:

  • внутри <script> тегов
  • в inline-атрибутах (onclick, onchange)
  • в динамических шаблонных вставках

Наиболее стабильный и распространённый сценарий — извлечение содержимого <script>.

Пример HTML:

<html>
  <body>
    <script>
      function test() {
        console.log("hello");
      }
    </script>
  </body>
</html>

Процессор должен:

  • найти блоки <script>
  • извлечь содержимое
  • сохранить смещения строк
  • создать виртуальные JS-файлы

Простейшая реализация HTML-процессора

const SCRIPT_REGEX = /<script[^>]*>([\s\S]*?)<\/script>/gi;

function extractScripts(text) {
  const scripts = [];
  let match;

  while ((match = SCRIPT_REGEX.exec(text)) !== null) {
    scripts.push(match[1]);
  }

  return scripts;
}

module.exports = {
  preprocess(text) {
    return extractScripts(text);
  },

  postprocess(messages) {
    return messages.flat();
  }
};

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

Проблема позиционирования ошибок

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

Для решения требуется:

  • хранить карту смещений (offset map)
  • учитывать позицию <script> блока
  • корректировать line/column

Пример структуры маппинга

{
  index: 0,
  startLine: 10,
  startColumn: 2
}

При обработке сообщения:

finalLine = startLine + message.line - 1
finalColumn = (message.line === 1)
  ? startColumn + message.column
  : message.column

Улучшенная версия процессора с маппингом

const SCRIPT_REGEX = /<script[^>]*>([\s\S]*?)<\/script>/gi;

function extractScriptsWithMap(text) {
  const result = [];
  const maps = [];

  let match;

  while ((match = SCRIPT_REGEX.exec(text)) !== null) {
    const before = text.slice(0, match.index);
    const startLine = before.split("\n").length;
    const startColumn =
      before.length - before.lastIndexOf("\n");

    result.push(match[1]);

    maps.push({
      startLine,
      startColumn
    });
  }

  return { result, maps };
}

module.exports = {
  preprocess(text) {
    const { result, maps } = extractScriptsWithMap(text);

    this._maps = maps;
    return result;
  },

  postprocess(messages) {
    return messages.flat().map((msg, i) => {
      const map = this._maps[i] || { startLine: 1, startColumn: 1 };

      return {
        ...msg,
        line: msg.line + map.startLine - 1,
        column:
          msg.line === 1
            ? msg.column + map.startColumn
            : msg.column
      };
    });
  }
};

Конфигурация ESLint с процессором

Процессоры подключаются через секцию overrides или через плагины.

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

В более старых версиях использовалась система eslint-plugin-html, которая автоматически извлекала <script> блоки и передавала их в ESLint.

eslint-plugin-html и его роль

Плагин eslint-plugin-html исторически служил для линтинга JavaScript внутри HTML без необходимости ручной настройки процессора. Он автоматически:

  • извлекал <script> блоки
  • создавал виртуальные файлы
  • объединял сообщения

Однако его подход был ограничен:

  • слабая поддержка inline-скриптов
  • сложное сопоставление ошибок
  • отсутствие гибкости для кастомных шаблонов

В современных архитектурах предпочтение отдаётся явным процессорам или фреймворковым плагинам.

Поддержка нескольких скриптовых блоков

HTML может содержать несколько <script> блоков:

<script>const a = 1;</script>
<script>const b = 2;</script>

Процессор должен:

  • создавать отдельный виртуальный файл для каждого блока
  • сохранять порядок
  • корректно объединять сообщения

Каждый блок становится независимой единицей анализа.

Inline-обработчики событий

Сложный случай — JavaScript в атрибутах:

<button oncl ick="alert('ok')">OK</button>

Для таких случаев требуется:

  • парсинг HTML AST (например, через parse5)
  • извлечение значений атрибутов
  • виртуализация как выражений JavaScript

Подобный подход значительно увеличивает сложность процессора, поскольку код не является полноценным скриптом.

Ограничения модели процессоров

Механизм процессоров имеет ряд системных ограничений:

  • отсутствие полноценного AST HTML в ESLint ядре
  • невозможность точной реконструкции контекста исполнения
  • сложности с source maps при динамическом коде
  • несовместимость с некоторыми правилами, зависящими от модуля ECMAScript

Процессор работает на уровне текста, а не семантики документа.

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

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

  • увеличение числа парсинговых проходов
  • дополнительную память на хранение промежуточных строк
  • рост стоимости postprocess-этапа

Оптимизация достигается через:

  • кэширование результатов извлечения
  • минимизацию числа виртуальных файлов
  • отказ от сложных регулярных выражений в пользу парсеров

Современные альтернативные подходы

В экосистеме JavaScript линтинг HTML-контента часто реализуется через специализированные плагины:

  • Vue SFC плагины используют собственные парсеры SFC
  • Angular использует интеграцию через шаблонные компиляторы
  • React избегает HTML-строк, перенося логику в JSX

В таких системах процессор ESLint заменяется полноценным синтаксическим анализом на уровне фреймворка.

Архитектурное значение процессора

Процессор выступает адаптером между различными языковыми слоями. Его роль сводится к следующему:

  • трансформация нестандартного формата в JS-представление
  • сохранение семантической связи с оригиналом
  • обеспечение совместимости с ядром ESLint без его модификации

Эта модель позволяет расширять ESLint за пределы JavaScript-файлов без изменения его внутреннего парсера.