Хабрахабр

RegExp Unicode Property Escapes в JavaScript: штрихи к портрету

RegExp Unicode Property Escapes перешли на 4-ю ступень и будут включены в ES2018.

В V8 они доступны без флага начиная с v6.4, так что готовы к использованию во всех текущих каналах Google Chrome от стабильного до Canary.

В Node.js они будут доступны без флага уже в v10 (выходит в апреле). В других версиях требуется флаг --harmony_regexp_property (Node.js v6–v9) или --harmony (Node.js v8-v9). Сейчас без флага их можно испробовать или в ночных сборках, или в ветке v8-canary.

При этом нужно иметь в виду, что сборки Node.js, скомпилированные без поддержки ICU, будут лишены возможности использовать этот класс регулярных выражений (подробнее см. Internationalization Support).

Подробнее о поддержке в других движках и средах см. в известной таблице (после перехода проскрольте чуть выше).

Я не буду повторять описания этой долгожданной возможности, лишь сошлюсь на несколько статей от известных специалистов:

Мне же захотелось рассказать о паре не совсем очевидных мелочей.

Когда я начинал знакомство с этой новой возможностью, то пожалел о двух недостающих удобствах: способе программно получить список всех допустимых вариантов в этом самом обширном теперь классе регулярных выражений и способе получить список подходящих свойств для конкретного символа Юникода.

Если кто-то почувствует такую же нужду, пусть эти заметки сэкономят ему время 🙂

Список всех доступных свойств для регулярного выражения

На данный момент, авторитетным и исчерпывающим источником, перечисляющим все возможные свойства, служит сама текущая спецификация ECMAScript, в частности таблицы (осторожно, по ссылкам тяжеловесная страница) в разделах Runtime Semantics: UnicodeMatchProperty ( p ) и Runtime Semantics: UnicodeMatchPropertyValue ( p, v ).

Если кому-то неудобно загружать всю спецификацию, можно ограничиться спецификацией предложения с теми же таблицами. И совсем облегчённый вариант: эти таблицы существуют в виде четырёх отдельных файлов в корне репозитория спецификации ECMAScript. Собственно, только они и существуют в виде отдельных файлов, импортируемых в спецификацию, — уже одно это, наверное, может свидетельствовать об их беспрецедентном объёме. Таблицы можно с относительным удобством просмотреть при помощи родного подсервиса http://htmlpreview.github.io/.

Я же извлёк эти данные и набросал крохотную библиотечку, содержащую структурированный список всех возможных имён и значений и экспортирующую этот объект в виде уплощённого массива всех возможных членов из данного класса регулярных выражений.

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

При помощи нехитрого скрипта и упомянутой библиотеки можно получить список в формате JSON, содержащий источники для регулярных выражений. Пример такого скрипта и его вывода можно посмотреть там же в комментарии — всего 372 варианта в текущей версии спецификации.

Получение свойств символов

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

Должен оговориться, что ради простоты я не добавлял в скрипты обработку ошибок, так что об этом стоит позаботиться отдельно.

1. Характеристика отдельного символа.

Небольшая утилита получает в качестве параметра командной строки единичный символ или его шестнадцатеричный номер в базе Юникода (code point) и выдаёт список свойств, которые в будущем можно использовать при поиске данного символа или общего ему класса символов.

re-unicode-properties.character-info.js

'use strict'; const reUnicodeProperties = require('./re-unicode-properties.js'); const RADIX = 16;
const PAD_MAX = 4; const [, , arg] = process.argv;
let character;
let codePoint; if ([...arg].length === 1) { character = arg; codePoint = `U+${character.codePointAt(0).toString(RADIX).padStart(PAD_MAX, '0')}`;
} else { character = String.fromCodePoint(Number.parseInt(arg, RADIX)); codePoint = `U+${arg.padStart(PAD_MAX, '0')}`;
} const characterProperties = reUnicodeProperties .filter(re => re.test(character)) .map(re => re.source) .join('\n') .replace(/\\p\{|\}/g, ''); console.log( `${JSON.stringify(character)} (${codePoint})\n${characterProperties}`,
);

Пример вывода:

$ node re-unicode-properties.character-info.js ё "ё" (U+0451)
gc=Letter
gc=Cased_Letter
gc=Lowercase_Letter
sc=Cyrillic
scx=Cyrillic
Alphabetic
Any
Assigned
Cased
Changes_When_Casemapped
Changes_When_Titlecased
Changes_When_Uppercased
Grapheme_Base
ID_Continue
ID_Start
Lowercase
XID_Continue
XID_Start

2. Получение списка всех символов Юникода с доступными для них свойствами.

Этот вариант скрипта работает на моей машине 2–3 минуты и отъедает около гигабайта памяти, так что будьте осторожны. Для однократного запуска, дающего нам полную базу, это терпимо, при необходимости же можно настроить постепенный вывод в файл вместо построения всей базы в памяти и вывода в один присест.

Скрипт можно запускать без параметров, тогда он выводит базу в упрощённом текстовом формате, по одному символу со свойствами на строку. Если же добавить параметр json, на выходе мы получим читабельную базу в JSON (кстати, использовать в виде ключей шестнадцатеричные цифры не выходит: сортировка результата перестаёт быть детерминированной порядком создания ключей; поэтому к числовому ключу прибавляется префикс U+ — так и сортировка сохраняется, и искать символ в сети будет удобнее, если понадобится полный набор свойств и подробное описание, а не только подходящий для регулярного выражения список).

re-unicode-properties.code-points.js

'use strict'; const { writeFileSync } = require('fs');
const reUnicodeProperties = require('./re-unicode-properties.js'); const [, , format] = process.argv; const LAST_CODE_POINT = 0x10FFFF;
const RADIX = 16;
const PAD_MAX = LAST_CODE_POINT.toString(RADIX).length; const data = {}; let codePoint = 0; while (codePoint <= LAST_CODE_POINT) { const character = String.fromCodePoint(codePoint); data[`U+${codePoint.toString(RADIX).padStart(PAD_MAX, '0')}`] = [ character, ...reUnicodeProperties .filter(re => re.test(character)) .map(re => re.source.replace(/\\p\{|\}/g, '')), ]; codePoint++;
} if (format === 'json') { writeFileSync( 're-unicode-properties.code-points.json', `\uFEFF${JSON.stringify(data, null, 2)}\n`, );
} else { writeFileSync( 're-unicode-properties.code-points.txt', `\uFEFF${ Object.entries(data) .map(([k, v]) => `${k.replace('U+', '')} ${JSON.stringify(v.shift())} ${v.join(' ')}`) .join('\n') }\n`, );
}

Примеры фрагментов в обоих форматах:

000020 " " gc=Separator gc=Space_Separator sc=Common scx=Common ASCII Any Assigned Grapheme_Base Pattern_White_Space White_Space
000021 "!" gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Grapheme_Base Pattern_Syntax Sentence_Terminal Terminal_Punctuation
000022 "\"" gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Grapheme_Base Pattern_Syntax Quotation_Mark
000023 "#" gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Emoji Emoji_Component Grapheme_Base Pattern_Syntax
000024 "$" gc=Symbol gc=Currency_Symbol sc=Common scx=Common ASCII Any Assigned Grapheme_Base Pattern_Syntax
000025 "%" gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Grapheme_Base Pattern_Syntax
000026 "&" gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Grapheme_Base Pattern_Syntax
000027 "'" gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Case_Ignorable Grapheme_Base Pattern_Syntax Quotation_Mark
000028 "(" gc=Punctuation gc=Open_Punctuation sc=Common scx=Common ASCII Any Assigned Bidi_Mirrored Grapheme_Base Pattern_Syntax
000029 ")" gc=Punctuation gc=Close_Punctuation sc=Common scx=Common ASCII Any Assigned Bidi_Mirrored Grapheme_Base Pattern_Syntax
00002a "*" gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Emoji Emoji_Component Grapheme_Base Pattern_Syntax
00002b "+" gc=Symbol gc=Math_Symbol sc=Common scx=Common ASCII Any Assigned Grapheme_Base Math Pattern_Syntax
00002c "," gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Grapheme_Base Pattern_Syntax Terminal_Punctuation
00002d "-" gc=Punctuation gc=Dash_Punctuation sc=Common scx=Common ASCII Any Assigned Dash Grapheme_Base Pattern_Syntax
00002e "." gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Case_Ignorable Grapheme_Base Pattern_Syntax Sentence_Terminal Terminal_Punctuation
00002f "/" gc=Punctuation gc=Other_Punctuation sc=Common scx=Common ASCII Any Assigned Grapheme_Base Pattern_Syntax
[ "U+000020": [ " ", "gc=Separator", "gc=Space_Separator", "sc=Common", "scx=Common", "ASCII", "Any", "Assigned", "Grapheme_Base", "Pattern_White_Space", "White_Space" ], "U+000021": [ "!", "gc=Punctuation", "gc=Other_Punctuation", "sc=Common", "scx=Common", "ASCII", "Any", "Assigned", "Grapheme_Base", "Pattern_Syntax", "Sentence_Terminal", "Terminal_Punctuation" ]
]

Полные базы в архивах можно при желании скачать: .txt (5 MB в архиве, ~60 MB текста) или .json (5.5 MB в архиве, ~112 MB текста). При просмотре не забудьте использовать хорошие шрифты.

3. Список используемых в файле символов с их свойствами.

Это вариант предыдущего скрипта, предоставляющего не полную базу символов, а лишь тот набор, который встречается в заданном файле. Первым параметром скрипта задаётся путь к файлу, вторым необязательным — формат (текстовый используется по умолчанию, также можно задать json). Вывод аналогичный предыдущему, только меньший по объёму. Поскольку файл читается в режиме потока, можно обрабатывать тексты любого разумного размера. У меня гигабайтный файл обрабатывался пять минут, на протяжении всей работы занимал около 60 мегабайт памяти.

re-unicode-properties.file-info.js

'use strict'; const { createReadStream, writeFileSync } = require('fs');
const { basename } = require('path');
const reUnicodeProperties = require('./re-unicode-properties.js'); const [, , filePath, format] = process.argv; const LAST_CODE_POINT = 0x10FFFF;
const RADIX = 16;
const PAD_MAX = LAST_CODE_POINT.toString(RADIX).length; const data = {}; (async function main() { const fileStream = createReadStream(filePath); fileStream.setEncoding('utf8'); const characters = new Set(); for await (const chunk of fileStream) { [...chunk].forEach((character) => { characters.add(character); }); } [...characters].sort().forEach((character) => { data[`U+${character.codePointAt(0).toString(RADIX).padStart(PAD_MAX, '0')}`] = [ character, ...reUnicodeProperties .filter(re => re.test(character)) .map(re => re.source.replace(/\\p\{|\}/g, '')), ]; }); if (format === 'json') { writeFileSync( `re-unicode-properties.file-info.${basename(filePath)}.json`, `\uFEFF${JSON.stringify(data, null, 2)}\n`, ); } else { writeFileSync( `re-unicode-properties.file-info.${basename(filePath)}.txt`, `\uFEFF${ Object.entries(data) .map(([k, v]) => `${k.replace('U+', '')} ${JSON.stringify(v.shift())} ${v.join(' ')}`) .join('\n') }\n`, ); }
})();

На этом, пожалуй, всё. Спасибо за уделённое время.

Показать больше

Похожие публикации

Кнопка «Наверх»