Один зі способів пошуку неэкранированных символів за допомогою нових засобів JavaScript

1. З чого все почалося
Нещодавно у мене виникла необхідність написати чергову утиліту, обробну текстовий файл у форматі, схожому на спрощений BBCode, а саме у форматі исходников для словників ABBYY Lingvo — DSL (Dictionary Specification Language). (Не плутати з іншим DSL (Domain-specific language) — цікавий випадок, коли гипоним є омонімом до гиперониму).

Досить сказати, що в мові використовуються теги в квадратних дужках і що квадратні дужки можна екранувати зворотною косою рискою, якщо потрібно використовувати їх як частина звичайного тексту.

Одним із завдань утиліти було як раз знаходження цих тегів з виключенням екранованих поєднань.

Оскільки в регулярних виразах JavaScript з недавнього часу можна користуватися lookbehind assertions (в особистих цілях), я подумав, чи не можна реалізувати пошук за допомогою цього засобу, — тим більше, що в даній різновиди lookbehind можна використовувати вираження змінної довжини.

2. Попередні зауваження
Щоб оцінити подальший експеримент, необхідно знайомство з деякими новими можливостями JavaScript.

1. Template literals — довгоочікувані рядки з інтерполяцією змінних.

2. String.raw(). Можливості цієї функції можна порівняти з одинарними лапками в Perl і префіксом
r
в Python: всі вони допомагають створювати рядки з буквальної інтерпретації спецсимвола екранування.

3. Lookbehind assertions (в тому числі див. способи активізації їх у Google Chrome і Node.js).

3. Реалізація
Код скрипта з пробної (наївною) реалізацією пошуку і перевірки:

/******************************************************************************/
'use strict';
/******************************************************************************/
const r = String.raw;

const startOfString = '^';
const notEscapeSymbol = r`[^\x5c]`;
const escapedEscapeSymbols = r`(?:${startOfString}|${notEscapeSymbol})(?:\x5c{2})+`;
const tag = r`\x5b[^\x5d]+\x5d`;

const tagRE = new RegExp(
`(?<=${startOfString}|${notEscapeSymbol}|${escapedEscapeSymbols})${tag}`, 'g'
);

console.log(r`[tag]text[/tag]`.match(tagRE));
console.log(r`\\[tag]text\\\\[/tag]`.match(tagRE));

console.log(r`\[tag]text\\\[/tag]`.match(tagRE));
/******************************************************************************/

Спершу ми створюємо синонім для
String.raw
, щоб можна було використовувати коротку форму, подібно префікса
r
в Python.

Потім ми створюємо складові частини майбутнього регулярного виразу.

Я виходив з припущення, що правильному тегу може передувати один з трьох варіантів: початок рядка, будь-який символ окрім риски і екранована зворотна коса риса (тобто поєднання двох обернених косих рис). При цьому потрібно стежити за тим, щоб екрануючий косу риску символ сам не піддався екрануванню: іншими словами, передувати тегу може лише парну кількість обернених косих рис, перед якими, у свою чергу, може бути або початок рядка, або будь-який відмінний від них символ.

Таким чином, нам потрібно чотири ключових елемента складного регулярного виразу: сам тег і три його допустимих попередника — початок рядка, будь-який символ за винятком риски і екранована коса риска або її повторення будь-яку кількість разів. Третій попередник тега може бути представлений як поєднання одного з перших двох попередників і пари обернених косих рис у будь-якій кількості.

Щоб не рябіло в очах, я все буквальні символи обернених косих рис і квадратних дужок замінив на шістнадцяткові літерали (
[ — \x5b, \ — \x5c, ] — \x5d
).

Еквівалентом скомпільованого з частин регулярного виразу буде таке поєднання (його можна використовувати замість всієї першої частини, присвоївши його змінній
tagRE
безпосередньо):

/(?<=^|[^\x5c]|(?:^|[^\x5c])(?:\x5c{2})+)\x5b[^\x5d]+\x5d/g


В кінці скрипта отриманий вираз тестується на мінімальному наборі правильних і екранованих тегів. Перший рядок містить тег після початку рядка і після символу, відмінного від риски. Другий рядок містить теги після екранованої риски, яку (або яких) передує або початок рядка, або символ, відмінний від них самих. Третій рядок містить екрановані теги.

В консоль виводиться наступний результат:

[ '[i]', '[/i]' ]
 
[ '[i]', '[/i]' ]
 
null


При оцінці рішення слід мати на увазі два застереження:

1. Це реалізація для домашнього використання, а не для випуску в масову продукцію (поки lookbehind assertions не вийдуть з-під прапора в Node.js і Google Chrome і не будуть реалізовані в інших браузерах).

2. Даний вираз не покликане перевіряти правильність вмісту самих тегів, тільки відрізнити їх від екранованих поєднань.

Буду вдячний за вказівки не непомічені ризики і за поради щодо оптимізації.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.