Какой препроцессор использовать для обработки сокращений в HTML?

Ранее была библиотека, которая во время выполнения преобразовывала такой фрагмент: <code class="html">&lt;div #my-div .card.info&gt;&lt;/div&gt;</code> в этот: <code class="html">&lt;div id="my-div" class="card info"&gt;&lt;/div&gt;</code>. Хочу перенести этот препроцессинг в пайплайн CI/CD (подойдёт модуль на Node.js) и было бы отлично иметь дополнительные, настраиваемые возможности. Что лучше взять за основу: есть ли готовые решения для такой задачи или придётся изучать парсеры вроде ANTLR (с которыми я не знаком)?
  • 4 мая 2026 г. 14:01
Ответы на вопрос 2
Короткий ответ: не нужно тащить ANTLR — берите HTML-парсер + плагинный инструмент и напишите небольшую трансформацию. Хорошие варианты на Node.js: PostHTML или rehype (unified). Они парсят HTML в AST, дают удобный API для плагинов и легко встраиваются в CI/билд.

Почему не ANTLR / ручный парсер
- ANTLR — тяжеловес для этой задачи. У вас уже валидный HTML с «сокращёнными» атрибутами, достаточно стандартного HTML‑парсера и обхода дерева.  
- Регулярки для HTML — хрупкие; лучше оперировать AST.

Варианты и рекомендации
- PostHTML — простая система плагинов, удобна для трансформаций атрибутов. Хороша, если вам нужно просто пройтись по элементам и поменять attrs.
- rehype (unified) — более современная экосистема, совместима с remark, имеет unist AST, хорошие утилиты (unist-util-visit). Подойдёт, если планируете более сложные цепочки трансформаций.
- Cheerio/parse5 — можно использовать для простых преобразований (cheerio даёт jQuery-подобный API), но лучше всё же плагинный подход (PostHTML/rehype), если хотите CI-интеграцию и расширяемость.
- Emmet / emmet-core — умеет разворачивать сокращения типа div#id.class в HTML. Но Emmet больше про генерацию новых фрагментов в редакторе, не про трансформацию уже существующего HTML в AST‑плагине (можно использовать, если ваша задача — разворачивать аббревиатуры в элементы, а не только атрибуты).

Пример простого плагина (PostHTML)
Ниже минимальный пример плагина, который превращает <div #my .card.info> в <div id="my" class="card info">:

const posthtml = require('posthtml');

function shorthandAttrsPlugin() {
  return tree => {
    tree.walk(node => {
      if (!node.attrs) return node;
      const attrs = node.attrs;
      Object.keys(attrs).forEach(name => {
        if (!name) return;
        if (name[0] === '#') {
          // #id или #id=value (реже)
          attrs.id = name.slice(1);
          delete attrs[name];
        } else if (name[0] === '.') {
          // .cls1.cls2 или .cls1.cls2=value
          const classes = name.slice(1).split('.').filter(Boolean);
          const existing = (attrs.class || '').split(' ').filter(Boolean);
          attrs.class = [...existing, ...classes].join(' ');
          delete attrs[name];
        }
      });
      node.attrs = attrs;
      return node;
    });
  };
}

// использование
posthtml([shorthandAttrsPlugin()])
  .process('<div #my-div .card.info></div>')
  .then(result => console.log(result.html));
// -> <div id="my-div" class="card info"></div>

Пример на rehype
Если вы предпочитаете rehype/unified, идея та же — парсите, ходите по элементам и меняете node.properties:

const unified = require('unified');
const rehypeParse = require('rehype-parse');
const rehypeStringify = require('rehype-stringify');
const visit = require('unist-util-visit');

function shorthandPlugin() {
  return tree => {
    visit(tree, 'element', node => {
      const props = node.properties || {};
      Object.keys(props).forEach(k => {
        if (!k) return;
        if (k[0] === '#') {
          props.id = k.slice(1);
          delete props[k];
        } else if (k[0] === '.') {
          const classes = k.slice(1).split('.').filter(Boolean);
          const existing = (props.className || []).slice(); // rehype/unist uses className array
          props.className = existing.concat(classes);
          delete props[k];
        }
      });
      node.properties = props;
    });
  };
}

unified()
  .use(rehypeParse, {fragment: true})
  .use(shorthandPlugin)
  .use(rehypeStringify)
  .process('<div #my-div .card.info></div>')
  .then(file => console.log(String(file)));

Дополнительные идеи / настройки
- Поддержка существующего id/class: объединять, заменять или отдавать приоритет — настраиваемо в плагине.  
- Поддержать разные префиксы (например, data-*) — легко добавить в опции.  
- Если хотите поддерживать сокращения без пробелов (div#id.class), это немного сложнее: HTML-парсер воспримет это как имя тега, поэтому нужно либо пред‑парсить строку (с проверенным парсером сокращений, например Emmet), либо требовать пробелы/формат, который ваш HTML-парсер корректно разобьёт.  
- Интеграция в CI: написать небольшой Node CLI/скрипт, который обрабатывает файлы (glob), либо встроить в Webpack/Gulp/Rollup с соответствным плагином.

Вывод
- Самый практичный путь: PostHTML или rehype + ваш плагин. Это даёт простую реализацию, расширяемость и хорошую интеграцию в пайплайн.  
- ANTLR не нужен для такой задачи. Emmet можно использовать, если вам нужна полноценная генерация аббревиатур (особенно без пробелов).

Если хотите, могу:
- прислать готовый npm‑модуль/скрипт под ваши требования, или
- помочь реализовать опции (слияние классов, правила при конфликте id, поддержка кастомных префиксов).
posthtml — под такой трансформ он буквально и создан. emmet тут не то, он работает с аббревиатурами ( <code>div#id.class</code> ) без угловых скобок. В posthtml пишешь плагин через tree.walk(), который переносит <code>#id</code> → id, <code>.class</code> → class. ANTLR оверкилл.
Похожие вопросы