Перейти к основному содержимому

Флаг `v` в регулярных выражениях с использованием нотации множеств и свойств строк

· 9 мин. чтения
Марк Дэвис ([@mark_e_davis](https://twitter.com/mark_e_davis)), Маркус Шерер и Матиас Биненс ([@mathias](https://twitter.com/mathias))

JavaScript поддерживает регулярные выражения с ECMAScript 3 (1999). Спустя шестнадцать лет в ES2015 были введены режим Unicode (флаг u), режим липкости (флаг y) и геттер RegExp.prototype.flags. Ещё через три года в ES2018 появились режим dotAll (флаг s), обратные проверки, именованные группы захвата и экранирование свойств символов Unicode. А в ES2020 String.prototype.matchAll упростил работу с регулярными выражениями. Регулярные выражения в JavaScript прошли долгий путь и продолжают совершенствоваться.

Последним достижением является новый режим unicodeSets, включаемый с помощью флага v. Этот новый режим предоставляет поддержку расширенных символьных классов, включая следующие функции:

Эта статья подробно рассматривает каждую из этих функций. Но сначала — вот как использовать новый флаг:

const re = //v;

Флаг v можно комбинировать с уже существующими флагами регулярных выражений, за исключением одного случая. Флаг v включает все хорошие стороны флага u, но с дополнительными функциями и улучшениями — некоторые из которых несовместимы с флагом u. Важно отметить, что v является полностью отдельным режимом от u, а не дополняющим его. Поэтому флаги v и u не могут быть объединены — попытка использовать оба флага в одном регулярном выражении приводит к ошибке. Единственные возможные варианты: либо использовать u, либо использовать v, либо не использовать ни u, ни v. Но поскольку v является наиболее полнофункциональной опцией, выбор очевиден…

Давайте углубимся в новую функциональность!

Свойства строк Unicode

Стандарт Unicode назначает различные свойства и значения свойств каждому символу. Например, чтобы получить набор символов, используемых в греческом письме, выполните поиск в базе данных Unicode символов, значение свойства Script_Extensions которых включает Greek.

Экранирование свойств символов Unicode в ES2018 позволяет получить доступ к этим свойствам символов Unicode непосредственно в регулярных выражениях ECMAScript. Например, шаблон \p{Script_Extensions=Greek} соответствует каждому символу, который используется в греческом письме:

const regexGreekSymbol = /\p{Script_Extensions=Greek}/u;
regexGreekSymbol.test('π');
// → true

Согласно определению, свойства символов Unicode расширяются до множества кодовых точек, и, следовательно, их можно интерпретировать как символьный класс, содержащий кодовые точки, которые они индивидуально соответствуют. Например, \p{ASCII_Hex_Digit} эквивалентен [0-9A-Fa-f]: он всегда соответствует только одному символу Unicode/кодовой точке за раз. В некоторых ситуациях этого недостаточно:

// Unicode определяет свойство символа под названием «Emoji».
const re = /^\p{Emoji}$/u;

// Соответствие эмодзи, состоящему из всего 1 кодовой точки:
re.test('⚽'); // '\u26BD'
// → true ✅

// Соответствие эмодзи, состоящему из нескольких кодовых точек:
re.test('👨🏾‍⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → false ❌

В приведённом выше примере регулярное выражение не соответствует эмодзи 👨🏾‍⚕️, потому что он состоит из нескольких кодовых точек, а Emoji является свойством символа Unicode.

К счастью, стандарт Unicode также определяет несколько свойств строк. Эти свойства расширяются до набора строк, каждая из которых содержит одну или несколько кодовых точек. В регулярных выражениях свойства строк преобразуются в набор альтернатив. Для иллюстрации представьте, что существует свойство Unicode, которое применимо к строкам 'a', 'b', 'c', 'W', 'xy' и 'xyz'. Это свойство переводится в один из следующих шаблонов регулярного выражения (с использованием чередования): xyz|xy|a|b|c|W или xyz|xy|[a-cW]. (Сначала длинные строки, чтобы префикс, такой как 'xy', не скрывал более длинную строку, такую как 'xyz'.) В отличие от существующих Unicode-экрапов свойств, этот шаблон может совпадать с многосимвольными строками. Вот пример использования свойства строк:

const re = /^\p{RGI_Emoji}$/v;

// Совпадение эмодзи, состоящего всего из одной кодовой точки:
re.test('⚽'); // '\u26BD'
// → true ✅

// Совпадение эмодзи, состоящего из нескольких кодовых точек:
re.test('👨🏾‍⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true ✅

Этот фрагмент кода относится к свойству строк RGI_Emoji, которое Unicode определяет как «подмножество всех допустимых эмодзи (символов и последовательностей), рекомендованных для общего обмена». С этим теперь мы можем сопоставлять эмодзи независимо от того, сколько кодовых точек они содержат!

Флаг v включает поддержку следующих Unicode свойств строк изначально:

  • Basic_Emoji
  • Emoji_Keycap_Sequence
  • RGI_Emoji_Modifier_Sequence
  • RGI_Emoji_Flag_Sequence
  • RGI_Emoji_Tag_Sequence
  • RGI_Emoji_ZWJ_Sequence
  • RGI_Emoji

Этот список поддерживаемых свойств может расшириться в будущем, если стандарт Unicode определит дополнительные свойства строк. Хотя все текущие свойства строк связаны с эмодзи, будущие свойства могут служить совершенно другим целям.

примечание

Примечание: Хотя свойства строк в настоящее время ограничены новым флагом v, мы планируем в конечном итоге сделать их доступными и в режиме u.

Нотация множества + синтаксис строковых литералов

При работе с экрапами \p{…} (будь то свойства символов или новые свойства строк) может быть полезно выполнять разность/вычитание или пересечение. С флагом v теперь можно вкладывать классы символов, и эти операции с множествами можно выполнять внутри них, а не с использованием смежных утверждений предвосхищения или ретроспективы или длинных классов символов, выражающих вычисленные диапазоны.

Разность/вычитание с помощью --

Синтаксис A--B можно использовать для сопоставления строк в A, но не в B, иначе говоря разность/вычитание.

Например, что если вы хотите сопоставить все греческие символы, кроме буквы π? Используя нотацию множества, решить это просто:

/[\p{Script_Extensions=Greek}--π]/v.test('π'); // → false

Используя -- для разности/вычитания, движок регулярных выражений выполняет сложную работу за вас, сохраняя ваш код читаемым и поддерживаемым.

Что если, вместо одного символа, мы хотим вычесть набор символов α, β и γ? Нет проблем — мы можем использовать вложенный класс символов и вычесть его содержимое:

/[\p{Script_Extensions=Greek}--[αβγ]]/v.test('α'); // → false
/[\p{Script_Extensions=Greek}--[α-γ]]/v.test('β'); // → false

Другой пример — сопоставление не-ASCII цифр, например, для их преобразования в ASCII цифры позже:

/[\p{Decimal_Number}--[0-9]]/v.test('𑜹'); // → true
/[\p{Decimal_Number}--[0-9]]/v.test('4'); // → false

Нотация множества также может быть использована с новыми свойствами строк:

// Примечание: 🏴 состоит из 7 кодовых точек.

/^\p{RGI_Emoji_Tag_Sequence}$/v.test('🏴'); // → true
/^[\p{RGI_Emoji_Tag_Sequence}--\q{🏴}]$/v.test('🏴'); // → false

Этот пример сопоставляет любую последовательность тегов эмодзи RGI, кроме флага Шотландии. Обратите внимание на использование \q{…}, который является новым синтаксисом для строковых литералов внутри классов символов. Например, \q{a|bc|def} сопоставляет строки a, bc, и def. Без \q{…} нельзя было бы вычесть жестко закодированные многосимвольные строки.

Пересечение с &&

Синтаксис A&&B сопоставляет строки, которые есть и в A, и в B, иначе говоря пересечение. Это позволяет делать, например, сопоставление греческих букв:

const re = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;
// U+03C0 МАЛАЯ ГРЕЧЕСКАЯ БУКВА ПИ
re.test('π'); // → true
// U+1018A ГРЕЧЕСКИЙ НУЛЕВОЙ ЗНАК
re.test('𐆊'); // → false

Сопоставление всех пробелов ASCII:

const re = /[\p{White_Space}&&\p{ASCII}]/v;
re.test('\n'); // → true
re.test('\u2028'); // → false

Или сопоставление всех монгольских чисел:

const re = /[\p{Script_Extensions=Mongolian}&&\p{Number}]/v;
// U+1817 МОНОЛЬСКАЯ ЦИФРА СЕМЬ
re.test('᠗'); // → true
// U+1834 БУКВА МОНОЛЬСКОЙ ЧА
re.test('ᠴ'); // → false

Объединение

Сопоставление строк, которые лежат в A или в B, уже было возможно для одиночных символов строк, используя класс символов, например [\p{Letter}\p{Number}]. С флагом v эта функциональность становится более мощной, поскольку теперь она может быть комбинирована с свойствами строк или строковыми литералами:

const re = /^[\p{Emoji_Keycap_Sequence}\p{ASCII}\q{🇧🇪|abc}xyz0-9]$/v;

re.test('4️⃣'); // → true
re.test('_'); // → true
re.test('🇧🇪'); // → true
re.test('abc'); // → true
re.test('x'); // → true
re.test('4'); // → true

Класс символов в этом шаблоне объединяет:

  • свойство строки (\p{Emoji_Keycap_Sequence})
  • свойство символа (\p{ASCII})
  • синтаксис строкового литерала для многокодовых точек строк 🇧🇪 и abc
  • классический синтаксис класса символов для одиночных символов x, y и z
  • классический синтаксис класса символов для диапазона символов от 0 до 9

Другой пример — это сопоставление всех часто используемых флажков-эмодзи, независимо от того, закодированы ли они как двухбуквенный код ISO (RGI_Emoji_Flag_Sequence) или как последовательность тегов со специальным случаем (RGI_Emoji_Tag_Sequence):

const reFlag = /[\p{RGI_Emoji_Flag_Sequence}\p{RGI_Emoji_Tag_Sequence}]/v;
// Последовательность флага, состоящая из 2 кодовых точек (флаг Бельгии):
reFlag.test('🇧🇪'); // → true
// Последовательность тегов, состоящая из 7 кодовых точек (флаг Англии):
reFlag.test('🏴'); // → true
// Последовательность флага, состоящая из 2 кодовых точек (флаг Швейцарии):
reFlag.test('🇨🇭'); // → true
// Последовательность тегов, состоящая из 7 кодовых точек (флаг Уэльса):
reFlag.test('🏴'); // → true

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

Флаг u из ES2015 страдает от запутанного поведения регистронезависимого сопоставления. Рассмотрим следующие два регулярных выражения:

const re1 = /\p{Lowercase_Letter}/giu;
const re2 = /[^\P{Lowercase_Letter}]/giu;

Первый шаблон соответствует всем строчным буквам. Второй шаблон использует \P вместо \p, чтобы соответствовать всем символам, кроме строчных букв, но затем обернут в отрицательный класс символов ([^…]). Оба регулярных выражения становятся регистронезависимыми благодаря установке флага i (ignoreCase).

Интуитивно можно ожидать, что оба регулярных выражения будут работать одинаково. На практике их поведение сильно отличается:

const re1 = /\p{Lowercase_Letter}/giu;
const re2 = /[^\P{Lowercase_Letter}]/giu;

const string = 'aAbBcC4#';

string.replaceAll(re1, 'X');
// → 'XXXXXX4#'

string.replaceAll(re2, 'X');
// → 'aAbBcC4#''

Новый флаг v имеет менее удивительное поведение. С использованием флага v вместо флага u оба шаблона работают одинаково:

const re1 = /\p{Lowercase_Letter}/giv;
const re2 = /[^\P{Lowercase_Letter}]/giv;

const string = 'aAbBcC4#';

string.replaceAll(re1, 'X');
// → 'XXXXXX4#'

string.replaceAll(re2, 'X');
// → 'XXXXXX4#'

Более общо, флаг v делает [^\p{X}][\P{X}]\P{X} и [^\P{X}][\p{X}]\p{X}, независимо от того, установлен ли флаг i.

Дополнительное чтение

Репозиторий предложения содержит больше деталей и фона вокруг этих функций и их проектных решений.

Как часть нашей работы над этими функциями JavaScript, мы пошли дальше простого предложения изменений спецификации ECMAScript. Мы передали определение «свойств строк» в Unicode UTS#18, чтобы другие языки программирования могли реализовать аналогичный функционал унифицированным образом. Мы также предлагаем изменение стандарта HTML с целью включения этих новых функций в атрибут pattern.

Поддержка флага RegExp v

V8 v11.0 (Chrome 110) предлагает экспериментальную поддержку этой новой функциональности через флаг --harmony-regexp-unicode-sets. V8 v12.0 (Chrome 112) включает новые функции по умолчанию. Babel также поддерживает транспиляцию флага vпопробуйте примеры из этой статьи в Babel REPL! Таблица поддержки ниже содержит ссылки на отслеживание проблем, на которые можно подписаться для получения обновлений.