Мелодия: новый способ регулярного выражения

Сегодня Йоав Лави анонсировал Мелодию, язык, который компилируется в ECMAScript RegEx. Сейчас я много пишу о RegEx, поэтому этот проект сразу же заинтересовал меня.

Поскольку проект был выпущен всего пару дней назад, в нем отсутствуют несколько важных функций. Например, нельзя…

  • установить флаги (i для нечувствительности к регистру, u для поддержки юникода, g для глобального поиска и т. д.)
  • инвертировать диапазоны (например, /[^A]/)
    как оказалось, на самом деле это возможно с функциональностью Melody raw
  • создавать произвольные мультидиапазоны (например, [/a-c1-3]/)
  • передавать переменные (JavaScript, а не RegEx)

Все это говорит о том, что синтаксис довольно приятный. Вот простой пример из документации для поиска хэштега в строке:

"#";
some of <word>;

Вот регулярное выражение, которое выводится:

/#(?:\w)+/

Синтаксис интересный, но я бы сказал, что если бы вы не знали RegEx, то версия Melody показалась бы вам бесконечно более читабельной.

Интересный синтаксис

Давайте поговорим о некоторых способах, которыми Melody делает RegEx более понятным для человека и менее похожим на использование крови для рисования рун в грязи с целью исполнения тайного заклинания.

Символы

Символы — это способ Melody упростить множество общих задач RegEx. Например, если вы хотите захватить любой обычный латинский символ в любом случае, вы можете написать [a-zA-Z]. Однако с Melody вы можете использовать символ <alphabetic>! На момент написания этой статьи существует около 16 символов, но вот некоторые из моих любимых на данный момент:

  • <char>
    Альтернатива подстановочному знаку (.), который соответствует всему. <char> берет на себя всю работу по догадкам, чтобы выяснить, является ли \\\. подстановочным знаком или буквальным символом точки. 🙃
  • <word>
    Эскейп-коды RegEx чрезвычайно полезны, но не всегда понятно, что они делают. Символ <word> соответствует любому символу слова. Это то же самое, что и escape-код \w в RegEx.
  • <alphanumeric>
    Соответствует любому латинскому символу (A-Z) в любом регистре (a-z), а также цифрам (0-9). Это то же самое, что использовать [a-zA-Z0-9] в RegEx.

Специальные символы
На момент написания этой статьи существует два специальных символа: <start> и <end>. Эти символы соответствуют символам ^ и $ соответственно. Они используются для указания того, что поиск должен начинаться в начале или в конце строки, или если поиск должен быть всеобъемлющим (при использовании обоих символов).

квантификаторы

Квантификаторы позволяют нам, ммм… ну, они позволяют нам количественно оценивать наши выражения. Например, вы можете использовать что-то вроде этого для проверки UUID с помощью RegEx:

/^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/

Здесь {8}, {4} и {12} — все квантификаторы. Они указывают, что вам нужны ровно 8, 4 и 12 результатов предыдущего поиска соответственно. В случае с Melody это можно было бы решить с помощью квантификатора ... of ...:

<start>;
8 of <word>;
"-";
4 of <word>;
"-";
4 of <word>;
"-";
4 of <word>;
"-";
12 of <word>;
<end>;

Если вам нужно количество символов в определенном диапазоне, вы можете использовать {min,max}. Например, \d{1,2} означает, что вы хотите ввести от 1 до 2 цифр. Мелодия предоставляет квантификатор … to … of …:

1 to 2 of <digit>;

Мелодия также предоставляет альтернативы для квантификаторов * (ноль или более), + (один или несколько) и ? (ноль или один):

// \d*
any of <digit>;
// \d+
some of <digit>;
// \d?
option of <digit>;

Диапазоны символов

При поиске чего-либо в пределах известного набора символов вам необходимо использовать диапазон символов (например, шестнадцатеричный код будет [0–9a-f]). Объявление диапазонов обрабатывается выражением … to ….

// [a-f]
a to f;
// [1–5]
1 to 5;

Группы

Одна из самых важных функций RegEx — это группы! Группы с захватом и без захвата позволяют создавать чрезвычайно сложные поиски. Мелодия включает эти группы capture, match и either.

Чтобы зафиксировать основную, второстепенную и патч-версии строки semver:

capture major {
  some of <digit>;
}
".";
capture minor {
  some of <digit>;
}
".";
capture patch {
  some of <digit>;
}

Если вам нужно сопоставить поиск без его захвата, вы можете использовать match. Если вам нужно объединить несколько операторов match, вы можете использовать either. Здесь мы будем использовать оба, чтобы справиться с отсутствием нескольких диапазонов для соответствия 2-значному шестнадцатеричному значению:

2 of match {
  either {
    0 to 9;
    a to f;
  }
}

Гораздо больше!

Melody поддерживает множество других функций, поэтому обязательно ознакомьтесь с документацией!

Проведение мелодии через темпы

Базовые примеры классные и все такое, но я хотел преобразовать некоторые из моих реальных регулярных выражений в Melody, чтобы посмотреть, сохраняется ли аргумент читабельности.

Простой тест

Недавно, работая над своей игрой (отладка), я написал регулярное выражение для получения имени, идентификатора поставщика и идентификатора продукта с геймпада. Вот как выглядела исходная версия, которую я написал:

/^(.*?) \((?:standard gamepad )?vendor: (\w+) product: (\w+)\)$/ui

Единственная проблема, с которой я столкнулся при преобразовании этого в Melody, заключается в том, что Melody не поддерживает флаги, поэтому мои флаги u (юникод) и i (нечувствительность к регистру) не будут переведены. На данный момент я могу обработать это в строке, прежде чем передавать ее в RegEx Melody, но это значительный недостаток, о котором следует помнить.

Без лишних слов, вот мой исходный RegEx, преобразованный в синтаксис Melody:

<start>;

capture {
  lazy any of <char>;
}

<space>;
"(";

option of match {
  "standard gamepad ";
}

"vendor: ";

capture {
  some of <word>;
}

<space>;
"product: ";

capture {
  some of <word>;
}

")";

<end>;

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

Когда я впервые написал эту статью, у компилятора Melody возникла проблема, связанная с добавлением ненужных незахватываемых групп. Yoav уже решил проблему и опубликовал исправление для ненужных незахватываемых групп! 🥰

Давайте усложним

В прошлом году я столкнулся с абсурдной проблемой проверки пароля. Вы можете увидеть мое решение в действии на RegExr.com, но вот реальное регулярное выражение, которое я придумал:

/(?:.*(?:(?:[A-Z].*(?:[0–9].*[a-z]|[a-z].*[0–9]))|(?:[a-z].*(?:[A-Z].*[0–9]|[0–9].*[A-Z]))|(?:[0–9].*(?:[A-Z].*[a-z]|[a-z].*[A-Z]))).*)/

Каждый раз возвращаюсь и пытаюсь прочитать… 🤢

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

match {
  any of <char>;

  either {
    match {
      A to Z;
      any of <char>;

      either {
        match {
          0 to 9;
          any of <char>;
          a to z;
        }

        match {
          a to z;
          any of <char>;
          0 to 9;
        }
      }
    }

    match {
      a to z;
      any of <char>;

      either {
        match {
          A to Z;
          any of <char>;
          0 to 9;
        }

        match {
          0 to 9;
          any of <char>;
          A to Z;
        }
      }
    }

    match {
      0 to 9;
      any of <char>;

      either {
        match {
          A to Z;
          any of <char>;
          a to z;
        }

        match {
          a to z;
          any of <char>;
          A to Z;
        }
      }
    }
  }

  any of <char>;
}

Это ... много, чтобы жевать. Однако его несомненно легче читать, чем исходное регулярное выражение! ❤️

Последние мысли

Похоже, Melody станет отличным дополнением к экосистеме JavaScript! У него есть пути развития, но я лично рад наблюдать за тем, как он взрослеет.

На случай, если Yoav читает это, позвольте мне сказать вам, что я люблю увидеть: я могу написать свое регулярное выражение, создав файл .melody, затем я могу import myRegex from './my-regex.melody' и использовать myRegex непосредственно вместо обычное регулярное выражение! Существует плагин Babel, который позволяет писать мелодию в строках шаблона, но было бы здорово иметь возможность писать ее в совершенно отдельных файлах и импортировать через собственный загрузчик Webpack или плагин Rollup. HMU, если вы хотите объединиться в этом проекте. 🥳

Обновления от Йоава

Йоав сообщил мне (через Reddit), что они исправили проблему, которая создавала ненужные группы без захвата! Они также упомянули пару вещей, которые, по моему мнению, стоит повторить здесь:

  • Отрицательные классы символов уже имеют недокументированную первоначальную реализацию с обновлениями в ближайшее время.
  • Помимо флагов, Melody поддерживает все функции RegEx с помощью метода raw.