Короткий ответ: не нужно тащить 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, поддержка кастомных префиксов).