Доброго времени суток, дорогие читатели.
Пришла идея поделиться опытом написания конечного автомата. Скажу сразу же, я из мира фронтенд разработки, поэтому в моей реализации могут быть косяки, которые не допустят, например, бородатые сишники) Поэтому, критика, если она по делу да и к тому же от людей уже имеющих подобный опыт, категорически приветствуется. Думаю, не одному мне она будет интересна.
Здесь и далее весь код будет приводиться на html/js, но код на js может быть легко портирован на любой другой язык, поскольку он не будет содержать специфичных для js конструкций.
Тут будет приведен упрощенный пример конечного автомата. Например, в нем не будет учитываться, что значение атрибута может заключаться в одинарных кавычках или быть вовсе без них. Или что в разметке могут быть комментарии. Основная цель статьи, поделиться своим опытом и идеями.
Постановка задачи
Есть некая разметка
<div class="mb-4"> <span>Text</span> </div>
Которую нужно распарсить за один while/for не прибегая к специальным возможностям работы со строками, типа сплита и прочего. То есть в идеале код должен будет выглядеть как-то так:
function getNextStatus(ch, status) { // вот тут и будет написан конечный автомат, который будет принимать на вход символ и текущий статус и возвращать следующий статус в зависимости от символа return status; } function parseHtml(text) { let status = 0; for(let i = 0; i < text.length; i++) { status = getNextStatus(text[i], status); // тут будет обрабатываться результат работы конечного автомата } }
Мы должны написать код, который будет нам сообщать о том, что в переданном тексте есть тег div, у которого есть атрибут class со значением mb-4. Также есть тег span с текстом Text.
Анализ
Рассмотрим эту строку
<div class="mb-4">
Зададимся несколькими вопросами:
Как эту строку преобразовать в набор событий? Как эти события будут называться и сколько их будет?
Мы знаем что теги состоят из угловых скобок <, > и самого названия внутри. Название должно начинаться с буквы английского алфавита и может содержать в себе буквы английского алфавита, цифры, знаки - и _. Также в тегах могут присутствовать атрибуты, которые состоят из имени и значения разделенные знаком = (или иногда просто из имени, опустим это). Само значение должно быть в кавычках двойных или одинарных. (да, может быть и без кавычек, но тут мы пишем упрощенную версию автомата)
Попробуем перевести эти знания в последовательность событий
Тег открывается
Начало имени тега
Продолжение имени тега
Конец имени тега
Начало имени атрибута
Продолжение имени атрибута
Конец имени атрибута
Начало значения атрибута
Продолжение значения атрибута
Конец значения атрибута
Тег закрывается
Причем для тега <b> набор будет уже другим
Тег открывается
Начало имени тега
Конец имени тега | Тег закрывается
Обратите внимание на последнюю строку, там 2 события наступают одновременно, так как на символе > можно точно сказать, что тег закрыт и у него есть имя.
Попробуем написать начальную версию конечного автомата, который будет распознавать начало и конец тега.
Учимся считывать угловые скобки
Для начала введем константы, которые будут обозначать состояния автомата, назовем их, например, TAG_START, TAG_END.
const TAG_START = 1; const TAG_END = TAG_START * 2;
Странная запись на второй строке, не правда ли? Можно было бы написать просто 2, зачем умножение на 2?
А дело в том, что сама переменная статуса будет в себе содержать одновременно несколько значений. Как это возможно? Все дело в битовых масках. Именно их мы и будем использовать на всем протяжении написания нашего автомата.
Итак, переписав нашу функцию getNextStatus мы получим что-то типа такого
function getNextStatus(ch, status) { if(ch === '<') { // Всегда будем возвращать TAG_START, если встретим символ '<' return TAG_START; } else if(ch === '>') { if(status & TAG_START) { // Если тег был открыт status ^= TAG_START; // Снимаем флаг начала тега status |= TAG_END; // Добавляем флаг закрытия тега return status; } } return 0; // Если что-то пошло не так, возвращаем 0 }
Полная версия
const TAG_START = 1; const TAG_END = TAG_START * 2; function getNextStatus(ch, status) { if(ch === '<') { return TAG_START; } else if(ch === '>') { if(status & TAG_START) { status ^= TAG_START; status |= TAG_END; return status; } } return 0; } function parseHtml(text) { let status = 0; for(let i = 0; i < text.length; i++) { status = getNextStatus(text[i], status); if(status & TAG_START) { console.log('Начало тега'); } else if(status & TAG_END) { console.log('Конец тега'); } } }
Откройте в браузере консоль и вставьте этот код.
Если выполнить parseHtml('<>'), то мы увидим
Начало тега Конец тега
Если же выполним parseHtml('><'), то увидим только Начало тега, поскольку начало тега действительно присутствует.
Если же выполним parseHtml('>>>'), то не увидим ничего.
Достаем имя тега
После того, как мы научились определять начало и конец тега самое то научиться определять его имя.
Как уже было написано ранее имя тега должно начинаться с буквы английского алфавита и т.д. и т.п.
Введем новые константы для статуса
const NAME_START = TAG_END * 2; // начало имени тега const NAME_CONT = NAME_START * 2; // продолжение имени тега, если оно больше 1 символа const NAME_END = NAME_CONT * 2; // конец имени тега
И допишем функцию конечного автомата
function getNextStatus(ch, status) { if(ch === '<') { ... } else if(ch === '>') { ... } else if(status & (NAME_START|NAME_CONT)) { // Дополняем условие если есть имя тега // Удаляем флаги про начало и продолжение имени тега status ^= status & NAME_START; status ^= status & NAME_CONT; status |= TAG_END|NAME_END; // Сетаем флаги конца тега и конца имени тега return status; } } else if(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { // Допустимые символы начала и продолжения имени тега if(status & NAME_CONT) { // Если это продолжение имени тега, то ничего не трогаем, оставляем статус текущим return status; } else if(status & TAG_START) { // Если это первый символ имени тега status ^= TAG_START; // Удаляем флаг начала тега status |= NAME_START; // Сетаем флаг начала имени тега return status; } else if(status & NAME_START) { // Если это не первый символ имени тега status ^= NAME_START; // Удаляем флаг начала имени тега status |= NAME_CONT; // Сетаем флаг продолжения имени тега return status; } } else if(ch >= '0' && ch <= '9' || ch === '-' || ch === '_') { // Допустимые символы в продолжении имени тега if(status & NAME_CONT) { // Если стоит флаг продолжения, то оставляем все как есть return status; } else if(status & NAME_START) { // Если это второй символ имени тега status ^= NAME_START; // Удаляем флаг начала имени тега status |= NAME_CONT; // Сетаем флаг продолжения имени тега return status; } } return 0; }
Полная версия
const TAG_START = 1; const TAG_END = TAG_START * 2; const NAME_START = TAG_END * 2; const NAME_CONT = NAME_START * 2; const NAME_END = NAME_CONT * 2; function getNextStatus(ch, status) { if(ch === '<') { return TAG_START; } else if(ch === '>') { if(status & TAG_START) { status ^= TAG_START; status |= TAG_END; return status; } else if(status & (NAME_START|NAME_CONT)) { status |= TAG_END|NAME_END; status ^= status & NAME_START; status ^= status & NAME_CONT; return status; } } else if(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { if(status & NAME_CONT) { return status; } else if(status & TAG_START) { status ^= TAG_START; status |= NAME_START; return status; } else if(status & NAME_START) { status ^= NAME_START; status |= NAME_CONT; return status; } } else if(ch >= '0' && ch <= '9' || ch === '-' || ch === '_') { if(status & NAME_CONT) { return status; } else if(status & NAME_START) { status ^= NAME_START; status |= NAME_CONT; return status; } } return 0; } function parseHtml(text) { let status = 0; for(let i = 0; i < text.length; i++) { status = getNextStatus(text[i], status); if(status & TAG_START) { console.log('Начало тега'); } else if(status & TAG_END) { if(status & NAME_END) { console.log('Конец имени тега'); } console.log('Конец тега'); } else if(status & NAME_START) { console.log(`Начало имени тега "${text[i]}"`); } else if(status & NAME_CONT) { console.log(`Продолжение имени тега "${text[i]}"`); } else if(status & NAME_END) { console.log('Конец имени тега'); } } }
Запустим наш код в консоли браузера и выполним следующие команды
Ввод
parseHtml('<b>')
Результат
Начало тега Начало имени тега "b" Конец имени тега Конец тега
Ввод
parseHtml('<div>')
Результат
Начало тега Начало имени тега "d" Продолжение имени тега "i" Продолжение имени тега "v" Конец имени тега Конец тега
Ввод
parseHtml('<1div>')
Результат
Начало тега
Как мы видим наш конечный автомат корректно обработал ситуацию с невалидным именем тега.
Получаем имена атрибутов
Реализация этого пункта во многом будет напоминать предыдущий. Добавим три новых константы статуса
const ATTR_NAME_START = NAME_END * 2; // Начало имени атрибута const ATTR_NAME_CONT = ATTR_NAME_START * 2; // Продолжение имени атрибута const ATTR_NAME_END = ATTR_NAME_CONT * 2; // Конец имени атрибута const WAITING_ATTR_NAME = ATTR_NAME_END * 2; // Ожидание имени атрибута
И, конечно же, допишем функцию конечного автомата
function getNextStatus(ch, status) { if(ch === '<') { ... } else if(ch === '>') { ... } else if(status & WAITING_ATTR_NAME) { // Пробел могу поставить перед закрытием тега, // учитываем это и удаляем флаги дублирующихся событий status ^= WAITING_ATTR_NAME; status ^= status & NAME_END; status ^= status & ATTR_NAME_END; status |= TAG_END; return status; } else if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { // А тут если нет пробела перед закрытием тега, но уже написано имя атрибута status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= ATTR_NAME_END|TAG_END; return status; } } else if(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { ... } else if(status & ATTR_NAME_START) { // тут все то же самое по аналогии с именем тега status ^= ATTR_NAME_START; status |= ATTR_NAME_CONT; return status; } else if(status & WAITING_ATTR_NAME) { // тут у нас могут быть флаги конца имени атрибута или тега, // поэтому снимаем их и устанавливаем флаг начала имени атрибута status ^= status & WAITING_ATTR_NAME; status ^= status & ATTR_NAME_END; status ^= status & NAME_END; status |= ATTR_NAME_START; return status; } } else if(ch >= '0' && ch <= '9' || ch === '-' || ch === '_') { ... } else if(status & ATTR_NAME_START) { // Те же самые операции, что и с именем тега status ^= ATTR_NAME_START; status |= ATTR_NAME_CONT; return status; } } else if(ch === ' ') { // Начинаем смотреть на пробел, как на разделитель if(status & (NAME_START|NAME_CONT)) { // Если пробел стоит после имени тега // то сбрасываем флаги имена тега status ^= status & NAME_START; status ^= status & NAME_CONT; // и сетаем флаг конца имени тега и ожидаем, что нам прилетит имя атрибута status |= WAITING_ATTR_NAME|NAME_END; return status; } else if(status & (ATTR_NAME_END|NAME_END|WAITING_ATTR_NAME)) { // на случай если пробелы введены более 1 раза, снимаем событийные флаги // чтобы не дублировать произошедшие события status ^= status & ATTR_NAME_END; status ^= status & NAME_END; return status; } else if(status & (ATTR_NAME_CONT|ATTR_NAME_START)) { // если у нас уже есть имя тега, снимаем флаги про его имя status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; // сетаем флаг конца имени атрибута и начинаем заново ожидать новое имя атрибута status |= WAITING_ATTR_NAME|ATTR_NAME_END; return status; } } return 0; }
Полная версия
const TAG_START = 1; const TAG_END = TAG_START * 2; const NAME_START = TAG_END * 2; const NAME_CONT = NAME_START * 2; const NAME_END = NAME_CONT * 2; const ATTR_NAME_START = NAME_END * 2; const ATTR_NAME_CONT = ATTR_NAME_START * 2; const ATTR_NAME_END = ATTR_NAME_CONT * 2; const WAITING_ATTR_NAME = ATTR_NAME_END * 2; function getNextStatus(ch, status) { if(ch === '<') { return TAG_START; } else if(ch === '>') { if(status & TAG_START) { status ^= TAG_START; status |= TAG_END; return status; } else if(status & (NAME_START|NAME_CONT)) { status |= TAG_END|NAME_END; status ^= status & NAME_START; status ^= status & NAME_CONT; return status; } else if(status & WAITING_ATTR_NAME) { status ^= WAITING_ATTR_NAME; status ^= status & NAME_END; status ^= status & ATTR_NAME_END; status |= TAG_END; return status; } else if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= ATTR_NAME_END|TAG_END; return status; } } else if(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { if(status & (NAME_CONT|ATTR_NAME_CONT)) { return status; } else if(status & TAG_START) { status ^= TAG_START; status |= NAME_START; return status; } else if(status & NAME_START) { status ^= NAME_START; status |= NAME_CONT; return status; } else if(status & ATTR_NAME_START) { status ^= ATTR_NAME_START; status |= ATTR_NAME_CONT; return status; } else if(status & WAITING_ATTR_NAME) { status ^= status & WAITING_ATTR_NAME; status ^= status & ATTR_NAME_END; status ^= status & NAME_END; status |= ATTR_NAME_START; return status; } } else if(ch >= '0' && ch <= '9' || ch === '-' || ch === '_') { if(status & (NAME_CONT|ATTR_NAME_CONT)) { return status; } else if(status & NAME_START) { status ^= NAME_START; status |= NAME_CONT; return status; } else if(status & ATTR_NAME_START) { status ^= ATTR_NAME_START; status |= ATTR_NAME_CONT; return status; } } else if(ch === ' ') { if(status & (NAME_START|NAME_CONT)) { status ^= status & NAME_START; status ^= status & NAME_CONT; status |= WAITING_ATTR_NAME|NAME_END; return status; } else if(status & (ATTR_NAME_END|NAME_END|WAITING_ATTR_NAME)) { status ^= status & ATTR_NAME_END; status ^= status & NAME_END; return status; } else if(status & (ATTR_NAME_CONT|ATTR_NAME_START)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= WAITING_ATTR_NAME|ATTR_NAME_END; return status; } } return 0; } function parseHtml(text) { let status = 0; for(let i = 0; i < text.length; i++) { status = getNextStatus(text[i], status); if(status & TAG_START) { console.log('Начало тега'); } else if(status & TAG_END) { if(status & NAME_END) { console.log('Конец имени тега'); } else if(status & ATTR_NAME_END) { console.log('Конец имени атрибута'); } console.log('Конец тега'); } else if(status & NAME_START) { console.log(`Начало имени тега "${text[i]}"`); } else if(status & NAME_CONT) { console.log(`Продолжение имени тега "${text[i]}"`); } else if(status & NAME_END) { console.log('Конец имени тега'); } else if(status & ATTR_NAME_START) { console.log(`Начало имени атрибута "${text[i]}"`); } else if(status & ATTR_NAME_CONT) { console.log(`Продолжение имени атрибута "${text[i]}"`); } else if(status & ATTR_NAME_END) { console.log('Конец имени атрибута'); } } }
Протестируем наш код
Ввод
parseHtml('<div test>')
Результат
Начало тега Начало имени тега "d" Продолжение имени тега "i" Продолжение имени тега "v" Конец имени тега Начало имени атрибута "t" Продолжение имени атрибута "e" Продолжение имени атрибута "s" Продолжение имени атрибута "t" Конец имени атрибута Конец тега
Попробуем запутать наш автомат пробелами
parseHtml('<div b b2 b4 >')
Результат
Начало тега Начало имени тега "d" Продолжение имени тега "i" Продолжение имени тега "v" Конец имени тега Начало имени атрибута "b" Конец имени атрибута Начало имени атрибута "b" Продолжение имени атрибута "2" Конец имени атрибута Начало имени атрибута "b" Продолжение имени атрибута "4" Конец имени атрибута Конец тега
Как мы видим, не получилось, значит автомат отработал корректно.
Получаем значения атрибутов
Пожалуй, это будет самый заковыристый пункт. Вспомним, что в нашей реализации значение атрибута должно быть ограничено двойными кавычками. Немного усложним задачу добавив условие, что в значении может быть экранированная обратным слешом двойная кавычка.
Дополним список статусов
const ATTR_VAL_START = WAITING_ATTR_NAME * 2; // Начало значения атрибута const ATTR_VAL_CONT = ATTR_VAL_START * 2; // Продолжение значения атрибута const ATTR_VAL_END = ATTR_VAL_CONT * 2; // Конец значения атрибута const ATTR_VAL_EMPTY = ATTR_VAL_END * 2; // Пустое значение атрибута const WAITING_ATTR_VAL = ATTR_VAL_EMPTY * 2; // Ожидание начала значения атрибута после двойной кавычки const WAITING_ATTR_VAL_QUOTE = WAITING_ATTR_VAL * 2; // Ожидание двойной кавычки после знака = const WAITING_ATTR_VAL_ESC = WAITING_ATTR_VAL_QUOTE * 2; // Ожидание двойного обратно слеша (для экранирования двойной кавычки)
Дописываем автомат
function getNextStatus(ch, status) { if(ch === '<') { // Поскольку в значении атрибута может быть любой символ, дописываем это условие if(status & WAITING_ATTR_VAL) { // Если находися в режиме ожидания начала атритуба // снимаем флаг ожидания status ^= WAITING_ATTR_VAL; // вешаем флаг начала status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { // если у нас начало значения атрибута // сбрасываем флаг начала status ^= ATTR_VAL_START; // сбрасываем флаг обратного слеша status ^= status & WAITING_ATTR_VAL_ESC; // вешаем флаг продолжения значения атрибута status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { // тут просто сбрасываем флаг обратного слеша status ^= status & WAITING_ATTR_VAL_ESC; return status; } else { ... } } else if(ch === '>') { ... } else if(status & WAITING_ATTR_VAL) { // тоже самое и тут status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & (ATTR_VAL_END|ATTR_VAL_EMPTY)) { // здесь если у есть событие конца значения атрибута // снимаем флаг этого события status ^= status & ATTR_VAL_END; status ^= status & ATTR_VAL_EMPTY; // и сетаем флаг конца тега status |= TAG_END; return status; } } else if(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { ... } else if(status & WAITING_ATTR_VAL) { // тоже самое что и в начале тега status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } else if(ch >= '0' && ch <= '9' || ch === '-' || ch === '_') { ... } else if(status & WAITING_ATTR_VAL) { // копипаста status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } else if(ch === ' ') { ... } else if(status & WAITING_ATTR_VAL) { // копипаста status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & ATTR_VAL_END) { status ^= ATTR_VAL_END; status |= WAITING_ATTR_NAME; return status; } else if(status & ATTR_VAL_EMPTY) { status ^= ATTR_VAL_EMPTY; status |= WAITING_ATTR_NAME; return status; } } else if(ch === '=') { // здесь начинаем смотреть на символ = if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { // если у нас уже есть имя атрибута // сбрасываем флаги про имена атрибута status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; // сетаем флаг, что имя атрибута закончено и ждем кавычку status |= ATTR_NAME_END|WAITING_ATTR_VAL_QUOTE; return status; } else if(status & WAITING_ATTR_VAL) { // тут знакомая копипаста status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } else if(ch === '"') { // смотрим на двойную кавычку if(status & WAITING_ATTR_VAL_QUOTE) { // если мы ее ожидали // то снимаем флаг ожидания и конца имени атрибута status ^= status & WAITING_ATTR_VAL_QUOTE; status ^= status & ATTR_NAME_END; // и вешаем флаг, что ожиданием значения атрибута status |= WAITING_ATTR_VAL; return status; } else if(status & WAITING_ATTR_VAL) { // если у нас пустая строка // то удаляем флаг ожидания значения атрибута status ^= WAITING_ATTR_VAL // сетаем флаг пустой строки status |= ATTR_VAL_EMPTY; return status; } else if(status & WAITING_ATTR_VAL_ESC) { // если было экранирование, то сбрасываем этот флаг status ^= WAITING_ATTR_VAL_ESC; return status; } else if(status & (ATTR_VAL_START|ATTR_VAL_CONT)) { // если у нас уже что-то есть в атрибуте // удаляем флаги про атрибуты status ^= status & ATTR_VAL_START; status ^= status & ATTR_VAL_CONT; // сетаем флаг конца значения атрибута status |= ATTR_VAL_END; return status; } } else if(ch === '\\') { // если прилетел обратный слеш if(status & WAITING_ATTR_VAL) { // а мы находимся после открывающейся кавычки // удаляем этот флаг status ^= WAITING_ATTR_VAL; // сетаем флаг о том, что значение атрибута начато и ждем обратного слеша status |= ATTR_VAL_START|WAITING_ATTR_VAL_ESC; return status; } else if(status & ATTR_VAL_START) { // если уже значение атрибута начато // сбрасываем этот флаг status ^= ATTR_VAL_START; // инвертим значение ожидания обратного слеша status = status & WAITING_ATTR_VAL_ESC ? status ^ WAITING_ATTR_VAL_ESC : status | WAITING_ATTR_VAL_ESC; // сетаем флаг продолжения атрибута status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { // тут просто инверт ожидания обратного слеша status = status & WAITING_ATTR_VAL_ESC ? status ^ WAITING_ATTR_VAL_ESC : status | WAITING_ATTR_VAL_ESC; return status; } } else { // тут любой символ // копипаста if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } return 0; }
Полная версия
const TAG_START = 1; const TAG_END = TAG_START * 2; const NAME_START = TAG_END * 2; const NAME_CONT = NAME_START * 2; const NAME_END = NAME_CONT * 2; const ATTR_NAME_START = NAME_END * 2; const ATTR_NAME_CONT = ATTR_NAME_START * 2; const ATTR_NAME_END = ATTR_NAME_CONT * 2; const WAITING_ATTR_NAME = ATTR_NAME_END * 2; const ATTR_VAL_START = WAITING_ATTR_NAME * 2; const ATTR_VAL_CONT = ATTR_VAL_START * 2; const ATTR_VAL_END = ATTR_VAL_CONT * 2; const ATTR_VAL_EMPTY = ATTR_VAL_END * 2; const WAITING_ATTR_VAL = ATTR_VAL_EMPTY * 2; const WAITING_ATTR_VAL_QUOTE = WAITING_ATTR_VAL * 2; const WAITING_ATTR_VAL_ESC = WAITING_ATTR_VAL_QUOTE * 2; function getNextStatus(ch, status) { if(ch === '<') { if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else { return TAG_START; } } else if(ch === '>') { if(status & TAG_START) { status ^= TAG_START; status |= TAG_END; return status; } else if(status & (NAME_START|NAME_CONT)) { status |= TAG_END|NAME_END; status ^= status & NAME_START; status ^= status & NAME_CONT; return status; } else if(status & WAITING_ATTR_NAME) { status ^= WAITING_ATTR_NAME; status ^= status & NAME_END; status ^= status & ATTR_NAME_END; status |= TAG_END; return status; } else if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= ATTR_NAME_END|TAG_END; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & (ATTR_VAL_END|ATTR_VAL_EMPTY)) { status ^= status & ATTR_VAL_END; status ^= status & ATTR_VAL_EMPTY; status |= TAG_END; return status; } } else if(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { if(status & (NAME_CONT|ATTR_NAME_CONT)) { return status; } else if(status & TAG_START) { status ^= TAG_START; status |= NAME_START; return status; } else if(status & NAME_START) { status ^= NAME_START; status |= NAME_CONT; return status; } else if(status & ATTR_NAME_START) { status ^= ATTR_NAME_START; status |= ATTR_NAME_CONT; return status; } else if(status & WAITING_ATTR_NAME) { status ^= status & WAITING_ATTR_NAME; status ^= status & ATTR_NAME_END; status ^= status & NAME_END; status |= ATTR_NAME_START; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } else if(ch >= '0' && ch <= '9' || ch === '-' || ch === '_') { if(status & (NAME_CONT|ATTR_NAME_CONT)) { return status; } else if(status & NAME_START) { status ^= NAME_START; status |= NAME_CONT; return status; } else if(status & ATTR_NAME_START) { status ^= ATTR_NAME_START; status |= ATTR_NAME_CONT; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } else if(ch === ' ') { if(status & (NAME_START|NAME_CONT)) { status ^= status & NAME_START; status ^= status & NAME_CONT; status |= WAITING_ATTR_NAME|NAME_END; return status; } else if(status & (ATTR_NAME_END|NAME_END|WAITING_ATTR_NAME)) { status ^= status & ATTR_NAME_END; status ^= status & NAME_END; return status; } else if(status & (ATTR_NAME_CONT|ATTR_NAME_START)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= WAITING_ATTR_NAME|ATTR_NAME_END; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & ATTR_VAL_END) { status ^= ATTR_VAL_END; status |= WAITING_ATTR_NAME; return status; } else if(status & ATTR_VAL_EMPTY) { status ^= ATTR_VAL_EMPTY; status |= WAITING_ATTR_NAME; return status; } } else if(ch === '=') { if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= ATTR_NAME_END|WAITING_ATTR_VAL_QUOTE; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } else if(ch === '"') { if(status & WAITING_ATTR_VAL_QUOTE) { status ^= status & WAITING_ATTR_VAL_QUOTE; status ^= status & ATTR_NAME_END; status |= WAITING_ATTR_VAL; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL status |= ATTR_VAL_EMPTY; return status; } else if(status & WAITING_ATTR_VAL_ESC) { status ^= WAITING_ATTR_VAL_ESC; return status; } else if(status & (ATTR_VAL_START|ATTR_VAL_CONT)) { status ^= status & ATTR_VAL_START; status ^= status & ATTR_VAL_CONT; status |= ATTR_VAL_END; return status; } } else if(ch === '\\') { if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START|WAITING_ATTR_VAL_ESC; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status = status & WAITING_ATTR_VAL_ESC ? status ^ WAITING_ATTR_VAL_ESC : status | WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status = status & WAITING_ATTR_VAL_ESC ? status ^ WAITING_ATTR_VAL_ESC : status | WAITING_ATTR_VAL_ESC; return status; } } else { if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } return 0; } function parseHtml(text) { let status = 0; for(let i = 0; i < text.length; i++) { status = getNextStatus(text[i], status); if(status & TAG_START) { console.log('Начало тега'); } else if(status & TAG_END) { if(status & NAME_END) { console.log('Конец имени тега'); } else if(status & ATTR_NAME_END) { console.log('Конец имени атрибута'); } console.log('Конец тега'); } else if(status & NAME_START) { console.log(`Начало имени тега "${text[i]}"`); } else if(status & NAME_CONT) { console.log(`Продолжение имени тега "${text[i]}"`); } else if(status & NAME_END) { console.log('Конец имени тега'); } else if(status & ATTR_NAME_START) { console.log(`Начало имени атрибута "${text[i]}"`); } else if(status & ATTR_NAME_CONT) { console.log(`Продолжение имени атрибута "${text[i]}"`); } else if(status & ATTR_NAME_END) { console.log('Конец имени атрибута'); } else if(status & ATTR_VAL_START) { console.log(`Начало значения атрибута "${text[i]}"`); } else if(status & ATTR_VAL_CONT) { console.log(`Продолжение значения атрибута "${text[i]}"`); } else if(status & ATTR_VAL_END) { console.log('Конец значения атрибута'); } } }
Ну и давайте протестируем то что мы написали
Ввод
parseHtml('<div attr="name">')
Результат
Начало тега Начало имени тега "d" Продолжение имени тега "i" Продолжение имени тега "v" Конец имени тега Начало имени атрибута "a" Продолжение имени атрибута "t" Продолжение имени атрибута "t" Продолжение имени атрибута "r" Конец имени атрибута Начало значения атрибута "n" Продолжение значения атрибута "a" Продолжение значения атрибута "m" Продолжение значения атрибута "e" Конец значения атрибута Конец тега
Ну и попытаемся запутать автомат
parseHtml('<div attr="<div attr=\\"test\\">">')
Результат
Начало тега Начало имени тега "d" Продолжение имени тега "i" Продолжение имени тега "v" Конец имени тега Начало имени атрибута "a" Продолжение имени атрибута "t" Продолжение имени атрибута "t" Продолжение имени атрибута "r" Конец имени атрибута Начало значения атрибута "<" Продолжение значения атрибута "d" Продолжение значения атрибута "i" Продолжение значения атрибута "v" Продолжение значения атрибута " " Продолжение значения атрибута "a" Продолжение значения атрибута "t" Продолжение значения атрибута "t" Продолжение значения атрибута "r" Продолжение значения атрибута "=" Продолжение значения атрибута "\" Продолжение значения атрибута """ Продолжение значения атрибута "t" Продолжение значения атрибута "e" Продолжение значения атрибута "s" Продолжение значения атрибута "t" Продолжение значения атрибута "\" Продолжение значения атрибута """ Продолжение значения атрибута ">" Конец значения атрибута Конец тега
Ничего не получилось, автомат отработал верно
Определяем, закрывающийся ли это тег
Надеюсь, этот пункт будет немного полегче предыдущих
Дополняем статусы
const SINGLE_CLOSE_TAG = WAITING_ATTR_VAL_ESC * 2; // Одиночный закрывающийся тег const NORMAL_CLOSE_TAG = SINGLE_CLOSE_TAG * 2; // Обычный закрывающийся тег
И дописываем автомат
function getNextStatus(ch, status) { if(ch === '<') { ... } else if(ch === '>') { ... } else if(status & SINGLE_CLOSE_TAG && !(status & TAG_END)) { // если это одиночный закрывающийся тег, то сбрасываем флаги про атрибуты и устанавливаем флаг, что тег закрыт status ^= status & ATTR_NAME_END; status ^= status & ATTR_VAL_END; status ^= status & ATTR_VAL_EMPTY; status ^= status & NAME_END; status |= TAG_END; return status; } } else if(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { ... } else if(status & NORMAL_CLOSE_TAG && !(status & TAG_END)) { // проверяем флаг установки закрытия тега и что мы еще внутри тега status |= NAME_START; return status; } } else if(ch >= '0' && ch <= '9' || ch === '-' || ch === '_') { ... } else if(ch === ' ') { if(status & (NAME_START|NAME_CONT) && !(status & NORMAL_CLOSE_TAG)) { // дописываем условие проверкой на флаг, что тег на закрывающий ... } else if(status & (ATTR_NAME_END|NAME_END|WAITING_ATTR_NAME) && !(status & SINGLE_CLOSE_TAG) && !(status & TAG_END)) { // тоже самое плюс проверка что мы еще внутри тега ... ... } else if(status & ATTR_VAL_END && !(status & SINGLE_CLOSE_TAG)) { // тоже самое ... } else if(status & ATTR_VAL_EMPTY && !(status & SINGLE_CLOSE_TAG)) { // тоже самое ... } } else if(ch === '=') { ... } else if(ch === '"') { ... } else if(ch === '\\') { ... } else if(ch === '/') { // начинаем смотреть на слеш // учитываем, если читаем значение атрибута if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & TAG_START) { // если слеш идет сразу после открытия тега, выставляем флаг закрывающего тега status ^= TAG_START; status |= NORMAL_CLOSE_TAG; return status; } else if(status & (NAME_START|NAME_CONT)) { // если уже есть имя атрибута, выставляем флаг одиночного закрывающегося тега status ^= status & NAME_START; status ^= status & NAME_CONT; status |= NAME_END|SINGLE_CLOSE_TAG; return status; } else if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { // тоже самое, если слеш идет сразу же после имени атрибута status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= ATTR_NAME_END|SINGLE_CLOSE_TAG; return status; } else if(status & (ATTR_NAME_END|ATTR_VAL_END|ATTR_VAL_EMPTY)) { // или его значения status ^= status & ATTR_NAME_END; status ^= status & ATTR_VAL_END; status ^= status & ATTR_VAL_EMPTY; status |= SINGLE_CLOSE_TAG; return status; } else if(status & WAITING_ATTR_NAME) { // или после пробела, который идет за атрибутом status ^= status & WAITING_ATTR_NAME; status ^= status & NAME_END; status |= SINGLE_CLOSE_TAG; return status; } } else { ... } return 0; }
Полная версия
const TAG_START = 1; const TAG_END = TAG_START * 2; const NAME_START = TAG_END * 2; const NAME_CONT = NAME_START * 2; const NAME_END = NAME_CONT * 2; const ATTR_NAME_START = NAME_END * 2; const ATTR_NAME_CONT = ATTR_NAME_START * 2; const ATTR_NAME_END = ATTR_NAME_CONT * 2; const WAITING_ATTR_NAME = ATTR_NAME_END * 2; const ATTR_VAL_START = WAITING_ATTR_NAME * 2; const ATTR_VAL_CONT = ATTR_VAL_START * 2; const ATTR_VAL_END = ATTR_VAL_CONT * 2; const ATTR_VAL_EMPTY = ATTR_VAL_END * 2; const WAITING_ATTR_VAL = ATTR_VAL_EMPTY * 2; const WAITING_ATTR_VAL_QUOTE = WAITING_ATTR_VAL * 2; const WAITING_ATTR_VAL_ESC = WAITING_ATTR_VAL_QUOTE * 2; const SINGLE_CLOSE_TAG = WAITING_ATTR_VAL_ESC * 2; const NORMAL_CLOSE_TAG = SINGLE_CLOSE_TAG * 2; function getNextStatus(ch, status) { if(ch === '<') { if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else { return TAG_START; } } else if(ch === '>') { if(status & TAG_START) { status ^= TAG_START; status |= TAG_END; return status; } else if(status & (NAME_START|NAME_CONT)) { status |= TAG_END|NAME_END; status ^= status & NAME_START; status ^= status & NAME_CONT; return status; } else if(status & WAITING_ATTR_NAME) { status ^= WAITING_ATTR_NAME; status ^= status & NAME_END; status ^= status & ATTR_NAME_END; status |= TAG_END; return status; } else if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= ATTR_NAME_END|TAG_END; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & (ATTR_VAL_END|ATTR_VAL_EMPTY)) { status ^= status & ATTR_VAL_END; status ^= status & ATTR_VAL_EMPTY; status |= TAG_END; return status; } else if(status & SINGLE_CLOSE_TAG && !(status & TAG_END)) { status ^= status & ATTR_NAME_END; status ^= status & ATTR_VAL_END; status ^= status & ATTR_VAL_EMPTY; status ^= status & NAME_END; status |= TAG_END; return status; } } else if(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { if(status & (NAME_CONT|ATTR_NAME_CONT)) { return status; } else if(status & TAG_START) { status ^= TAG_START; status |= NAME_START; return status; } else if(status & NAME_START) { status ^= NAME_START; status |= NAME_CONT; return status; } else if(status & ATTR_NAME_START) { status ^= ATTR_NAME_START; status |= ATTR_NAME_CONT; return status; } else if(status & WAITING_ATTR_NAME) { status ^= status & WAITING_ATTR_NAME; status ^= status & ATTR_NAME_END; status ^= status & NAME_END; status |= ATTR_NAME_START; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & NORMAL_CLOSE_TAG && !(status & TAG_END)) { status |= NAME_START; return status; } } else if(ch >= '0' && ch <= '9' || ch === '-' || ch === '_') { if(status & (NAME_CONT|ATTR_NAME_CONT)) { return status; } else if(status & NAME_START) { status ^= NAME_START; status |= NAME_CONT; return status; } else if(status & ATTR_NAME_START) { status ^= ATTR_NAME_START; status |= ATTR_NAME_CONT; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } else if(ch === ' ') { if(status & (NAME_START|NAME_CONT) && !(status & NORMAL_CLOSE_TAG)) { status ^= status & NAME_START; status ^= status & NAME_CONT; status |= WAITING_ATTR_NAME|NAME_END; return status; } else if(status & (ATTR_NAME_END|NAME_END|WAITING_ATTR_NAME) && !(status & SINGLE_CLOSE_TAG) && !(status & TAG_END)) { status ^= status & ATTR_NAME_END; status ^= status & NAME_END; return status; } else if(status & (ATTR_NAME_CONT|ATTR_NAME_START)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= WAITING_ATTR_NAME|ATTR_NAME_END; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & ATTR_VAL_END && !(status & SINGLE_CLOSE_TAG)) { status ^= ATTR_VAL_END; status |= WAITING_ATTR_NAME; return status; } else if(status & ATTR_VAL_EMPTY && !(status & SINGLE_CLOSE_TAG)) { status ^= ATTR_VAL_EMPTY; status |= WAITING_ATTR_NAME; return status; } } else if(ch === '=') { if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= ATTR_NAME_END|WAITING_ATTR_VAL_QUOTE; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } else if(ch === '"') { if(status & WAITING_ATTR_VAL_QUOTE) { status ^= status & WAITING_ATTR_VAL_QUOTE; status ^= status & ATTR_NAME_END; status |= WAITING_ATTR_VAL; return status; } else if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL status |= ATTR_VAL_EMPTY; return status; } else if(status & WAITING_ATTR_VAL_ESC) { status ^= WAITING_ATTR_VAL_ESC; return status; } else if(status & (ATTR_VAL_START|ATTR_VAL_CONT)) { status ^= status & ATTR_VAL_START; status ^= status & ATTR_VAL_CONT; status |= ATTR_VAL_END; return status; } } else if(ch === '\\') { if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START|WAITING_ATTR_VAL_ESC; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status = status & WAITING_ATTR_VAL_ESC ? status ^ WAITING_ATTR_VAL_ESC : status | WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status = status & WAITING_ATTR_VAL_ESC ? status ^ WAITING_ATTR_VAL_ESC : status | WAITING_ATTR_VAL_ESC; return status; } } else if(ch === '/') { if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } else if(status & TAG_START) { status ^= TAG_START; status |= NORMAL_CLOSE_TAG; return status; } else if(status & (NAME_START|NAME_CONT)) { status ^= status & NAME_START; status ^= status & NAME_CONT; status |= NAME_END|SINGLE_CLOSE_TAG; return status; } else if(status & (ATTR_NAME_START|ATTR_NAME_CONT)) { status ^= status & ATTR_NAME_START; status ^= status & ATTR_NAME_CONT; status |= ATTR_NAME_END|SINGLE_CLOSE_TAG; return status; } else if(status & (ATTR_NAME_END|ATTR_VAL_END|ATTR_VAL_EMPTY)) { status ^= status & ATTR_NAME_END; status ^= status & ATTR_VAL_END; status ^= status & ATTR_VAL_EMPTY; status |= SINGLE_CLOSE_TAG; return status; } else if(status & WAITING_ATTR_NAME) { status ^= status & WAITING_ATTR_NAME; status ^= status & NAME_END; status |= SINGLE_CLOSE_TAG; return status; } } else { if(status & WAITING_ATTR_VAL) { status ^= WAITING_ATTR_VAL; status |= ATTR_VAL_START; return status; } else if(status & ATTR_VAL_START) { status ^= ATTR_VAL_START; status ^= status & WAITING_ATTR_VAL_ESC; status |= ATTR_VAL_CONT; return status; } else if(status & ATTR_VAL_CONT) { status ^= status & WAITING_ATTR_VAL_ESC; return status; } } return 0; } function parseHtml(text) { let status = 0; for(let i = 0; i < text.length; i++) { status = getNextStatus(text[i], status); if(status & TAG_START) { console.log('Начало тега'); } else if(status & TAG_END) { if(status & NORMAL_CLOSE_TAG) { console.log('Закрывающийся тег'); } else if(status & SINGLE_CLOSE_TAG) { console.log('Одиночный закрывающийся тег'); } if(status & NAME_END) { console.log('Конец имени тега'); } else if(status & ATTR_NAME_END) { console.log('Конец имени атрибута'); } console.log('Конец тега'); } else if(status & NAME_START) { console.log(`Начало имени тега "${text[i]}"`); } else if(status & NAME_CONT) { console.log(`Продолжение имени тега "${text[i]}"`); } else if(status & NAME_END) { console.log('Конец имени тега'); } else if(status & ATTR_NAME_START) { console.log(`Начало имени атрибута "${text[i]}"`); } else if(status & ATTR_NAME_CONT) { console.log(`Продолжение имени атрибута "${text[i]}"`); } else if(status & ATTR_NAME_END) { console.log('Конец имени атрибута'); } else if(status & ATTR_VAL_START) { console.log(`Начало значения атрибута "${text[i]}"`); } else if(status & ATTR_VAL_CONT) { console.log(`Продолжение значения атрибута "${text[i]}"`); } else if(status & ATTR_VAL_END) { console.log('Конец значения атрибута'); } } }
Посмотрим на результат выполнения
Ввод
parseHtml('<div />');
Результат
Начало тега Начало имени тега "d" Продолжение имени тега "i" Продолжение имени тега "v" Конец имени тега Одиночный закрывающийся тег Конец тега
parseHtml('</div>');
Начало тега Начало имени тега "d" Продолжение имени тега "i" Продолжение имени тега "v" Закрывающийся тег Конец имени тега Конец тега
А как же текст?
Наш автомат умеет многое: определять теги, их имена и атрибуты, понимать, закрывающийся это тег или нет. Осталось получить, что не относится к тегу, а именно текст.
Можно смотреть по прежнему на статус и если он равен нулю, понимать, что мы сейчас читаем текст. Но что будет, если откроется тег, и он окажется битым? Предлагаю переписать функцию обработки статуса конечного автомата.
Обновленная функция parseHtml
function parseHtml(text) { let status = 0; let textBufferList = []; let attributes = []; let attrName = ''; let attrVal = ''; let tagName = ''; let textBufferItem = ''; for(let i = 0; i < text.length; i++) { const ch = text[i]; status = getNextStatus(ch, status); if(status & TAG_START) { if(textBufferItem) textBufferList.push(textBufferItem); if(textBufferList.length) console.log(`Текст: ${textBufferList.join('')}`); tagName = ''; textBufferItem = ch; attrName = ''; attributes = []; textBufferList = []; } else if(status & TAG_END) { textBufferItem = ''; textBufferList = []; if(status & NORMAL_CLOSE_TAG) { console.log(`Закрывающийся тег: ${tagName}`); } else if(status & SINGLE_CLOSE_TAG) { console.log(`Одиночный закрывающийся тег: ${tagName}`); } else { console.log(`Открывающийся тег: ${tagName}`); } if(attrName) { if(attrVal) { attributes.push({name: attrName, val: attrVal}); } else { attributes.push({name: attrName}); } } if(attributes.length) { console.log('Атрибуты'); attributes.forEach(attr => { if(attr.val) { console.log(` Имя: ${attr.name}, значение: ${attr.val}`); } else { console.log(` Имя: ${attr.name}`); } }); } } else if(status & NAME_START) { tagName = ch; textBufferItem += ch; } else if(status & NAME_CONT) { tagName += ch; textBufferItem += ch; } else if(status & ATTR_NAME_START) { if(attrName) { if(attrVal) { attributes.push({name: attrName, val: attrVal}); } else { attributes.push({name: attrName}); } } attrName = ch; attrVal = ''; textBufferItem += ch; } else if(status & ATTR_NAME_CONT) { attrName += ch; textBufferItem += ch; } else if(status & ATTR_VAL_START) { attrVal = ch; textBufferItem += ch; } else if(status & ATTR_VAL_CONT) { attrVal += ch; textBufferItem += ch; } else { textBufferItem += ch; } } if(textBufferItem) textBufferList.push(textBufferItem); if(textBufferList.length) console.log(`Текст: ${textBufferList.join('')}`); }
Попробуем скормить нашему автомату изначальный пример
Ввод
parseHtml('<div class="mb-4"><span>Text</span></div>');
Результат
Открывающийся тег: div Атрибуты Имя: class, значение: mb-4 Открывающийся тег: span Текст: Text Закрывающийся тег: span Закрывающийся тег: div
Попробуем подсунуть битый тег
parseHtml('<123div class="mb-4" />123<span title="Привет" data-test>мир</span>');
Результат
Текст: <123div class="mb-4" />123 Открывающийся тег: span Атрибуты Имя: title, значение: Привет Имя: data-test Текст: мир Закрывающийся тег: span
Автомат отработал корректно и это все за один прогон цикла.
Заключение
То, что мы написали отдаленно напоминает SAX-парсер. Как видите, писать конечные автоматы в некоторых случаях, как, например, в этом не так уж тяжело, как это может показаться. Главное запастись терпением, быть внимательным и все получится.
Конечно в текущей реализации не хватает поддержки парсинга комментариев, одинарных кавычек в атрибутах и прочих мелочей. Но главной целью статьи было поделиться собственным опытом.
Спасибо всем за внимание.
Комментарии (2)

winorun
05.05.2026 06:59создаёте функцию которая принимает на вход обрабатываемый символ, а возвращает следующию функцию. Запихиваете в цикл. Парсер готов.
Actek92
Для парсинга HTML лучше всего подойдёт подход с токенизацией: разбиваем поток на
<,>, текст и закрывающие теги, а потом применяем конечный автомат для распознавания вложенности. Если используете C — можно сделать это через побитовые флаги (например,state & 0x1для открытого тега). Для более сложных случаев рекомендую использовать parser combinator libraries — PEG.js для JS,nomдля Rust илиpyparsingдля Python. Не забывайте про обработку атрибутов и HTML-сущностей (&,<), иначе парсер будет ломаться на простых случаях. Для системной защиты стоит посмотреть на open-source WAF "SeptumCore" на GitHub — он лёгкий, без магии и ставится за 5 минут.