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).
Подробнее о поддержке в других движках и средах см. в известной таблице (после перехода проскрольте чуть выше).
Я не буду повторять описания этой долгожданной возможности, лишь сошлюсь на несколько статей от известных специалистов:
- Mathias Bynens. Unicode property escapes in JavaScript regular expressions (от самого автора предложения).
- Mathias Bynens. ECMAScript regular expressions are getting better! #Unicode property escapes
- Dr. Axel Rauschmayer. Exploring ES2018 and ES2019. #RegExp Unicode property escapes
- Nicolás Bevacqua. Regular Expressions in a post-ES6 world. #Unicode Property Escapes
- К сожалению, страница RegExp на MDN пока лишена всякого упоминания о нововведении, но это, наверняка, скоро будет исправлено.
Мне же захотелось рассказать о паре не совсем очевидных мелочей.
Когда я начинал знакомство с этой новой возможностью, то пожалел о двух недостающих удобствах: способе программно получить список всех допустимых вариантов в этом самом обширном теперь классе регулярных выражений и способе получить список подходящих свойств для конкретного символа Юникода.
Если кто-то почувствует такую же нужду, пусть эти заметки сэкономят ему время :)
Список всех доступных свойств для регулярного выражения
На данный момент, авторитетным и исчерпывающим источником, перечисляющим все возможные свойства, служит сама текущая спецификация ECMAScript, в частности таблицы (осторожно, по ссылкам тяжеловесная страница) в разделах Runtime Semantics: UnicodeMatchProperty (p) и Runtime Semantics: UnicodeMatchPropertyValue (p, v).
Если кому-то неудобно загружать всю спецификацию, можно ограничиться спецификацией предложения с теми же таблицами. И совсем облегчённый вариант: эти таблицы существуют в виде четырёх отдельных файлов в корне репозитория спецификации ECMAScript. Собственно, только они и существуют в виде отдельных файлов, импортируемых в спецификацию, — уже одно это, наверное, может свидетельствовать об их беспрецедентном объёме. Таблицы можно с относительным удобством просмотреть при помощи родного подсервиса.
Я же извлёк эти данные и набросал крохотную библиотечку, содержащую структурированный список всех возможных имён и значений и экспортирующую этот объект в виде уплощённого массива всех возможных членов из данного класса регулярных выражений.
Все подразделы представлены в алфавитном порядке за исключением общих свойств (тут удобнее и привычнее порядок документа из базы Юникода). Список не содержит синонимов, а сокращения используются только для общих свойств, что существенно экономит место в последующих операциях с библиотекой.
При помощи нехитрого скрипта и упомянутой библиотеки можно получить список в формате JSON, содержащий источники для регулярных выражений. Пример такого скрипта и его вывода можно посмотреть там же в комментарии — всего 372 варианта в текущей версии спецификации.
Получение свойств символов
Описанная библиотека позволяет нам использовать этот класс регулярок с не совсем обычной целью: не искать символы на основании свойств, а получать свойства на основании имеющегося символа. С ходу можно придумать несколько применений.
Должен оговориться, что ради простоты я не добавлял в скрипты обработку ошибок, так что об этом стоит позаботиться отдельно.
1. Характеристика отдельного символа.
Небольшая утилита получает в качестве параметра командной строки единичный символ или его шестнадцатеричный номер в базе Юникода (code point) и выдаёт список свойств, которые в будущем можно использовать при поиске данного символа или общего ему класса символов.
'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+
— так и сортировка сохраняется, и искать символ в сети будет удобнее, если понадобится полный набор свойств и подробное описание, а не только подходящий для регулярного выражения список).
'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 мегабайт памяти.
'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`,
);
}
})();
На этом, пожалуй, всё. Спасибо за уделённое время.