RegExp с флагом /v: наборы, пересечения и юникод-свойства

от автора

Привет, Хабр!

Сегодня рассмотрим флаг регулярных выражений v в JavaScript. Флаг поддержан в современных движках и Node 20+, а для старых окружений есть транспиляция через Babel. Начнём с краткой ориентации где это уже работает и почему синтаксис отличается, а потом пойдём в практику.

Что такое v и почему это не просто u++

Флаг v включает режим unicodeSets. Это отдельный вариант интерпретации шаблона: u и v нельзя смешивать одновременно в одном регексе. В v режиме доступны:

  1. свойства строк Юникода через \p{…}, т.е совпадения могут быть не только одиночными кодовыми точками, но и последовательностями;

  2. расширенная запись символьных классов с вложенностью и операциями пересечения и вычитания;

  3. исправленная логика для комплементарных классов с флагом i.

Поддержка по браузерам и Node стабильна: Chrome с 112, Firefox с 116, Safari с 17, Node начиная с 20. Для лего-совместимости в сборке есть плагин @babel/plugin-transform-unicode-sets-regex, он уже входит в preset-env и переписывает v в эквивалент под u, насколько это возможно.

Коротко про синтаксис: классы, пересечения, вычитания

В v режиме можно писать внутри одного символьного класса выражения-множества:

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

  • Вычитание: --

  • Юнион: просто перечисление без оператора

  • Вложенность: разрешена, чтобы группировать операнды

Нельзя на одном уровне смешивать && и -- — группируйте вложенными [...]. И помните: некоторые символы внутри v-классов нельзя ставить как есть из-за конфликта с двойными пунктуаторами, иначе будет SyntaxError.

Комплементарный класс [^…] в v — это комплемент множества, а не отрицание результата, благодаря чему поведение с флагом i становится ожидаемым и согласованным с \P{…}.

Далее смотрим что там с кодом.

Пересечение: фильтруем только греческие буквы, а не знаки

// Пересечение Script_Extensions=Greek с Letter const reGreekLetters = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;  reGreekLetters.test('π');     // true reGreekLetters.test(' ');     // false (это OGHAM SPACE MARK) reGreekLetters.test('ᾀ');     // true (греческая буква с диакритикой)

Почему именно Script_Extensions, а не Script: первый включает символы, которые принадлежат нескольким скриптам, и его чаще ожидают в валидациях. С пересечением выражаем это без lookahead и без огромных перечислений диапазонов.

Вычитание: все десятичные цифры, кроме ASCII

// Совпадает с любой «десятичной цифрой» Юникода, кроме ASCII 0-9 const reNonAsciiDigit = /[\p{Decimal_Number}--[0-9]]/v;  reNonAsciiDigit.test('٤');    // true (арабско-индийская цифра 4) reNonAsciiDigit.test('4');    // false

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

Теперь сделаем функцию нормализации строки с заменой любых десятичных цифр Юникода на ASCII. В JS нет готового API, которое вернёт цифровое значение символа из Юникода, поэтому используем известные диапазоны десятичных цифр.

// Преобразует все десятичные цифры Юникода к ASCII 0-9 export function normalizeDecimalDigits(input) {   // Быстрая проверка: есть ли вообще не-ASCII цифры   if (!/[\p{Decimal_Number}--[0-9]]/v.test(input)) return input;    // Поддержанные диапазоны «нулей» для Decimal_Number   // (добавляйте при необходимости — шаблон легко расширяется)   const zeros = [     0x0660, // Arabic-Indic     0x06F0, // Extended Arabic-Indic     0x07C0, // N'Ko     0x0966, // Devanagari     0x09E6, // Bengali     0x0A66, // Gurmukhi     0x0AE6, // Gujarati     0x0B66, // Oriya     0x0BE6, // Tamil     0x0C66, // Telugu     0x0CE6, // Kannada     0x0D66, // Malayalam     0x0E50, // Thai     0x0ED0, // Lao     0x0F20, // Tibetan     0x1040, // Myanmar     0x17E0, // Khmer     0x1810, // Mongolian     0xFF10  // Fullwidth   ];    const mapDigit = (cp) => {     for (const z of zeros) {       const delta = cp - z;       if (delta >= 0 && delta <= 9) return String.fromCharCode(0x30 + delta);     }     return null; // не цифра из поддержанных диапазонов   };    let out = "";   for (let i = 0; i < input.length; ) {     const cp = input.codePointAt(i);     const repl = mapDigit(cp);     out += repl ?? String.fromCodePoint(cp);     i += cp > 0xFFFF ? 2 : 1;   }   return out; }  // Пример // "٠١٢٣٤5٦789" => "0123456789"

Регексп не делает замену сам, у него задача выделить класс. Диапазоны взяты из стандартных блоков цифр Юникода — список легко проверить в спецификациях Юникода и адаптировать под свои регионы.

Свойства строк: наконец-то совпадения длиннее одной точки кода

С \p{…} в режиме u вы уже могли обращаться к свойствам символов. В режиме v те же \p{…} могут ссылаться на свойства строк. Сейчас это в первую очередь RGI-эмодзи: корректные последовательности с модификаторами, вариационными селекторами, ZWJ и флагами. Шаблон ^\p{RGI_Emoji}$ в v режиме совпадает и с одиночным эмодзи, и с составными последовательностями.

// Ровно один RGI-эмодзи (символ или валидная последовательность) const reEmoji = /^\p{RGI_Emoji}$/v;  reEmoji.test('⚽');            // true reEmoji.test('👨🏾‍⚕️');       // true reEmoji.test('😎');        // true reEmoji.test('A');             // false

Плюс доступен литерал строк внутри класса: \q{…}. Это даёт возможность делать операции множеств и со строками, не только с одиночными символами:

// Исключим строго один конкретный эмодзи-паттерн из множества RGI // \q{...} — литерал строки в классе. Можно перечислять через | const reEmojiExceptEngland = /^[\p{RGI_Emoji_Tag_Sequence}--\q{}]$/v;  reEmojiExceptEngland.test(''); // true — любой другой теговый флаг reEmojiExceptEngland.test(''); // false — именно England

Список поддержанных свойств строк в спецификации включает RGI_Emoji и его подтипы для кейкапов, флагов и ZWJ-последовательностей. Идея в том, что движок разворачивает свойство в набор альтернатив, упорядоченных от длинных к коротким, чтобы префиксы не съедали более длинные варианты.

Кейсы

1) Валидация логина по правилам: латиница, кириллица, цифры, дефис, без подчёркиваний, длина 3–24

// Разрешаем буквы и цифры любых скриптов ИЛИ дефис. // Для строгой ASCII-версии пересекаем с \p{ASCII}. const ALLOWED = /^(?:[\p{Letter}\p{Number}-]{3,24})$/v;  export function isValidLogin(s) {   // Дополнительно запретим ведущий/хвостовой дефис и подряд двойные дефисы   if (!ALLOWED.test(s)) return false;   if (/^-|-$|--/.test(s)) return false;   return true; }

Если нужна строгая ASCII-версия, замените класс на [\p{Letter}&&\p{ASCII}\p{Number}&&\p{ASCII}-] с правильной группировкой. Это наглядней, чем ручные диапазоны.

2)Нормализуем пробелы: только ASCII-пробелы, все остальное — в обычный пробел

const reAsciiWhitespace = /[\p{White_Space}&&\p{ASCII}]+/v; const reAnyWhitespace    = /\p{White_Space}+/v;  export function squeezeSpaces(s) {   // Сначала приводим все виды whitespace к пробелу   const step1 = s.replace(reAnyWhitespace, ' ');   // Затем ужимаем группы ASCII-пробелов и тримим   return step1.replace(reAsciiWhitespace, ' ').trim(); } 

Подход изолирует ASCII-пробелы от остальных и не трогает нестандартные разделители, если это важно для доменной логики.

3) Анти-подмена цифр: ищем наличие не-ASCII десятичных цифр

// Быстрый чек перед парсингом цены/количества export function hasNonAsciiDecimalDigits(s) {   return /[\p{Decimal_Number}--[0-9]]/v.test(s); }

Сценарий встречается в платежных формах и админках: не все пользователи вводят латинские цифры. Выявляем и показываем понятную подсказку, а не неверный формат.

4) Подсчёт эмодзи-токенов в сообщении

// Матчим только RGI-эмодзи, без прочих символов const reEmojiToken = /\p{RGI_Emoji}/gv;  export function countEmojis(s) {   let c = 0;   for (const _ of s.matchAll(reEmojiToken)) c++;   return c; }

Свойства строк в v режиме дают гранично точное совпадение RGI-эмодзи, включая флаги и ZWJ-последовательности. В u это приходилось собирать вручную через альтернативы.

Нюансы

Нельзя смешивать операторы на одном уровне. Пишите так: [\p{L}&&[\p{Greek}--[α-ω]] ], а не [\p{L}&&\p{Greek}--[α-ω]]. Иначе SyntaxError.

Экранируйте «двойные пунктуаторы» в классах. В v режиме некоторые символы внутри классов не могут стоять буквально — в частности, последовательности, похожие на -- и &&. Ошибка диагностируется как invalid character in class. Экранируйте или разбивайте класс.

\P{…} и свойства строк. В v \p{…} может описывать свойство строки, а \P{…} — только комплемент к свойству символов. Для отрицания свойства строки применяйте вычитание или комплементарный класс

Флаг i и комплемент. В v [^\p{X}], \P{X} и [\P{X}] эквивалентны, поведение стабильно и совпадает по смыслу. В u так не было.

HTML pattern и неожиданная синтаксическая ошибка. Если у вас внезапно сломался клиентский паттерн в форме — проверьте, не компилирует ли браузер его в v-режиме.

Рекомендации по стилю написания регексов с v

Для сложных правил всегда выражайте смысл через множества. Если нужна «латиница без подчёркивания», пишите [\p{Letter}&&\p{ASCII}--[_]].

Для работы с эмодзи используйте только \p{RGI_Emoji} и подсемейства.

Валидации форм в HTML проверяйте в реальных браузерах. Blink компилирует pattern как v, что отличается от старой логики u.

При транспиляции следите за размером паттернов после развёртки свойств строк. Babel-плагин сделает всё корректно, но итоговый класс может стать крупным.


Поддержка уже есть в актуальных браузерах и Node, транспиляция доступна из коробки. Если вы давно хотели навести порядок в своих регекспах, v — удобный момент заняться этим.

Если вам близка идея «управлять кодом, а не костылями», то, вероятно, есть и другой пробел — базовые интерфейсы. Удивительно, но даже опытные разработчики нередко тратят часы на формы, карточки и интерактивность, решая задачи «в лоб». Приглашаем на серию бесплатных уроков, где разбирем это без магии фреймворков, только на чистом HTML, CSS и JavaScript:

Актуальный стек технологий для решения задач фронтенда на junior+ уровне можно изучить под руководством экспертов на курсе «JavaScript Developer. Basic».


ссылка на оригинал статьи https://habr.com/ru/articles/941054/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *