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). Например, это касается популярной сборки под Android от сообщества Termux.


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


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



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


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


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


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


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


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


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


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


    При помощи нехитрого скрипта и упомянутой библиотеки можно получить список в формате 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`,
        );
      }
    })();

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

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

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 2

      +1
      Спасибо Вам за полезную статью!
        0
        Спасибо за доброе слово)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое