- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

При помощи нехитрого скрипта и упомянутой библиотеки можно получить список в формате JSON, содержащий источники для регулярных выражений. Пример такого скрипта и его вывода можно посмотреть там же в комментарии [20] — всего 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 [21] (5 MB в архиве, ~60 MB текста) или .json [22] (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`,
    );
  }
})();

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

Автор: vmb

Источник [23]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/274478

Ссылки в тексте:

[1] RegExp Unicode Property Escapes: https://github.com/tc39/proposal-regexp-unicode-property-escapes

[2] будут включены в ES2018: https://github.com/tc39/proposals/blob/master/finished-proposals.md

[3] без флага начиная с v6.4: https://bugs.chromium.org/p/v8/issues/detail?id=4743&desc=2

[4] в ночных сборках: https://nodejs.org/download/nightly/

[5] в ветке v8-canary: https://nodejs.org/download/v8-canary/

[6] Internationalization Support: https://github.com/nodejs/node/blob/master/doc/api/intl.md

[7] в известной таблице: http://kangax.github.io/compat-table/es2016plus/#test-RegExp_Unicode_Property_Escapes

[8] Unicode property escapes in JavaScript regular expressions: https://mathiasbynens.be/notes/es-unicode-property-escapes

[9] #Unicode property escapes: https://mathiasbynens.be/notes/es-regexp-proposals#unicode-property-escapes

[10] #RegExp Unicode property escapes: http://exploringjs.com/es2018-es2019/ch_regexp-unicode-property-escapes.html

[11] #Unicode Property Escapes: https://ponyfoo.com/articles/regular-expressions-post-es6#unicode-property-escapes

[12] страница RegExp на MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp

[13] Runtime Semantics: UnicodeMatchProperty ( p ): https://tc39.github.io/ecma262/#sec-runtime-semantics-unicodematchproperty-p

[14] Runtime Semantics: UnicodeMatchPropertyValue ( p, v ): https://tc39.github.io/ecma262/#sec-runtime-semantics-unicodematchpropertyvalue-p-v

[15] спецификацией предложения: https://tc39.github.io/proposal-regexp-unicode-property-escapes/

[16] в корне репозитория спецификации ECMAScript: https://github.com/tc39/ecma262

[17] http://htmlpreview.github.io/: http://htmlpreview.github.io/

[18] крохотную библиотечку: https://gist.github.com/vsemozhetbyt/893044264a088b96025b2d51ed17aec3

[19] документа из базы Юникода: http://unicode.org/reports/tr44/#GC_Values_Table

[20] там же в комментарии: https://gist.github.com/vsemozhetbyt/893044264a088b96025b2d51ed17aec3#gistcomment-2369719

[21] .txt: https://vsemozhetbyt.github.io/var/uniprops/re-unicode-properties.code-points.txt.zip

[22] .json: https://vsemozhetbyt.github.io/var/uniprops/re-unicode-properties.code-points.json.zip

[23] Источник: https://habrahabr.ru/post/350448/?utm_source=habrahabr&utm_medium=rss&utm_campaign=350448