Статьи

Квази-литералы: встроенные DSL в ECMAScript.next

 Квази-литералы [1] являются синтаксической конструкцией, которая облегчает реализацию встроенных доменных языков (DSL) в JavaScript. В настоящее время их планируется включить в следующую версию ECMAScript [2]. Этот пост объясняет, как работают квази-литералы.

Вступление

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

    quasiHandler`Hello ${firstName} ${lastName}`

Это просто компактный способ записи (примерно) следующего вызова функции:

    quasiHandler("Hello ", firstName, " ", lastName)

Таким образом, имя перед контентом в обратных кавычках является именем вызываемой функции,
квази-обработчиком . Обработчик получает два разных типа данных:

  • Буквальные разделы, такие как «Привет».
  • Подстановки, такие как firstName (ограничены знаком доллара и фигурными скобками). Подстановка может быть любым выражением. Если подстановка является просто идентификатором, вы можете опустить фигурные скобки:
        quasiHandler`Hello $firstName $lastName`
    

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

Примеры

Квази достаточно универсальны, потому что квази-литерал становится вызовом функции, а текст, который получает эта функция, структурирован. Поэтому вам нужно всего лишь написать новую функцию для реализации нового предметно-ориентированного языка. Следующие примеры взяты из [1] (с которым вы можете обратиться за подробностями):

  • Необработанные строки: строковые литералы с несколькими строками текста без интерпретации экранированных символов.
        var str = raw`This is a text
        with multiple lines.
        Escapes are not interpreted,
        \n is not a newline.`;
    
  • Параметризованные литералы регулярного выражения: существует два способа создания экземпляров регулярного выражения.

    • Статически, через литерал регулярного выражения.
    • Динамически, через конструктор RegExp.

    Если вы используете последний способ, это потому, что вам нужно подождать до времени выполнения, чтобы все необходимые ингредиенты были доступны: вы обычно объединяете фрагменты регулярного выражения и текст, который должен быть дословно сопоставлен. Последний должен быть правильно экранирован (точки, квадратные скобки и т. Д.). Определив обработчик регулярного выражения re, мы можем помочь с этой задачей:

        re`\d+(${localeSpecificDecimalPoint}\d+)?`
    
  • Запрашивать языки. Пример:
        $`a.${className}[href=~'//${domain}/']`
    

    Это запрос DOM, который ищет все теги <a>, у которых класс CSS — className, а целью является URL с данным доменом. Квази-обработчик $ обеспечивает правильное экранирование аргументов, что делает этот подход более безопасным, чем ручная конкатенация строк.

  • Локализация текста (L10N): в L10N есть два компонента. Сначала язык и второй язык (как форматировать числа, время и т. Д.). Учитывая следующее сообщение.
        alert(msg`Welcome to ${siteName}, you are visitor number ${visitorNumber}:d!`);
    

    Обработчик сообщения будет работать следующим образом.

    • Он создает следующий скелет для поиска перевода в таблице.
          Welcome to {0}, you are visitor number {1}
      

      Перевод может быть:

          Besucher Nr. {1}, willkommen bei {0}!
      
    • Затем метаданные замещения, такие как: d, извлекаются из буквальных частей и используются для форматирования данных, которые должны быть заполнены. В примере: d указывает, что для замены {1 должен использоваться специфичный для локали десятичный разделитель }. Таким образом, возможный английский результат:
          Welcome to ACME Corp., you are visitor number 1,300!
      

      На немецком языке у нас есть такие результаты, как:

          Besucher Nr. 1.300, willkommen bei ACME Corp.!
      
  • Генерация защищенного контента. С помощью квази можно различить доверенный контент, полученный из программы, и ненадежный контент, полученный от пользователя. Например:
        safehtml`<a href="${url}">${text}</a>`
    

    Буквальные разделы берутся из программы, а URL-адреса и текст — от пользователя. Квази-обработчик safehtml может гарантировать, что никакой вредоносный код не будет введен через замены. Для HTML полезна возможность вложения квази:

        rows = [['Unicorns', 'Sunbeams', 'Puppies'], ['<3', '<3', '<3']],
        safehtml`<table>${
            rows.map(function(row) {
                return safehtml`<tr>${
                    row.map(function(cell) {
                        return safehtml`<td>${cell}</td>`
                    })
                }</tr>`
            })
        }</table>`
    

    Объяснение: Строки таблицы создаются выражением — вызовом метода row.map (). Результатом этого вызова является массив строк, которые создаются путем рекурсивного вызова квази. safehtml объединяет эти строки и вставляет их в заданный кадр. Ячейки для каждого ряда производятся одинаково.

  • Шаблоны: Шаблоны очень похожи на квази, так как они представляют собой текст с отверстиями в них. Но обычно для заполнения дыр используются объекты (например, данные JSON). Например, ниже приведен шаблон:
        <h1>${{title}}</h1>
        ${{content}}
    

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

        var myTmpl = tmpl`
        <h1>${{title}}</h1>
        ${{content}}
        `;
    

    Это работает, потому что {title} и {content} являются фактическими выражениями ECMAScript.next: {foo, bar} является синтаксическим сахаром для {foo: foo, bar: bar}. Таким образом, обработчик получит значение, например {title: undefined} для первой замены. С шаблонами обработчик не интересуется значением заголовка, просто его именем, и этот прием позволяет ему получить к нему доступ. Недостатком использования квази таким образом является то, что такие переменные, как заголовок и контент, должны существовать (но они не должны иметь значения). Следовательно, вышесказанное должно быть записано как

        var title, content;
        var myTmpl = tmpl`
        ...
    

Реализация обработчика

Следующее является квази-буквальным:

    handlerName`lit1\n${subst1} lit2 ${subst2}`

Это внутренне преобразуется в вызов функции (адаптировано из [1]):

    // hoisted declaration.
    const callSiteId1234 = {
        raw: [ 'lit1\\n', ' lit2 ', '' ], // newline as written
        cooked: [ 'lit1\n', ' lit2 ', '' ], // newline interpreted
    };

    // in-situ
    handlerName(callSiteId1234, subst1, subst2)

Параметры обработчика делятся на две категории:

  1. CallSiteID, где вы получаете буквенные части как с экранированными значениями, такими как \ n интерпретированные («приготовленные») и не интерпретированные («сырые»). Количество буквенных частей всегда равно одному плюс количество подстановок. Если подстановка является последней в литерале, то создается пустая литеральная часть (как в примере выше).
  2. Подстановки, значения которых становятся конечными параметрами.

Идея состоит в том, что один и тот же литерал может выполняться несколько раз (например, в цикле); с помощью callSiteID обработчик может кэшировать данные из предыдущих вызовов. (1) потенциально кешируемые данные, (2) изменяется с каждым вызовом.

Назначение замен. Расширенная версия квазиса (которая, вероятно, не будет частью ECMAScript.next) позволяет присваивать подстановки. Например:

    if (re_match`before (${=x}\d+) after`(myString)) {
        // Do something with x
    }

re_match создает функцию, которая немедленно вызывается в myString. Эта функция возвращает true, если myString является совпадающим, и назначает первую совпадающую группу переменной x
одновременно . Сравните приведенный выше код с эквивалентным квази-менее JavaScript-кодом ниже. Обратите внимание, что вам нужна дополнительная переменная для хранения соответствия.

    var match = /before (\d+) after/.exec(myString);
    if (match) {
        x = match[1];
        // Do something with x
    }

Чтобы сделать замену присваиваемой, происходит следующий перевод: каждая доступная для записи подстановка $ {= x} передается в обработчик как следующая функция (другие записываемые замены, такие как $ {= obj.prop}, работают одинаково).

    function () { return arguments.length ? (x = arguments[0]) : x }

Объяснение: Если вы вызываете эту функцию без аргументов, вы получаете значение подстановки. Если вы предоставляете аргумент, он присваивается замене.

Каждая подстановка только для чтения $ {x} передается обработчику как функция.

    function() { return x }

Вывод

Как видите, существует множество приложений для квази-литералов. Вы можете спросить, почему ECMAScript.next не представляет полноценную систему макросов. Это потому, что довольно сложно создать систему макросов для языка, синтаксис которого так же сложен, как и в JavaScript. Таким образом, эта задача займет больше времени и, возможно, исследования. Однако есть надежда: если повезет, мы увидим макросы в ECMAScript 8 [3].

Подтверждение. Спасибо Брендану Айчу, Марку С. Миллеру, Майку Сэмюэлу и Аллену Уирфс-Броку за ответы на мои вопросы, связанные с квазисом, в списке рассылки es-обсуждения.

Рекомендации

  1. Квази-литералы ECMAScript [предложение для ECMAScript.next]
  2. ECMAScript.next: обновление «TXJS» от Eich
  3. Первый взгляд на то, что может быть в ECMAScript 7 и 8

С http://www.2ality.com/2011/09/quasi-literals.html