← Blog of oldlisper

Последние изменения в cl-closure-template

· 05.01.2013 00:00
· original author: archimag

Последние изменения в cl-closure-template

Тут незадолго до нового года вышел новый релиз Google Closure Templates и я решил посмотреть, что же изменилось в библиотеке пока я занимался разными другими делами. Оказалось, что разрыв в возможностях с cl-closure-template уже довольно внушительный, так что я решил его немного сократить. Правда, некоторые решения оригинальной реализации мне не понравились и я решил от них отклониться. Итак, в последнее время в cl-closure-template были добавлены:

  • Поддержка литералов для списков: [<expr1>, <expr2>, ...]. Например: [1, 'two', [3, false]]. [] - пустой список.

  • Поддержка литералов для словарей: {<keyExpr1>: <valueExpr1>, <keyExpr2>: <valueExpr2>, ...}. Например: {'aaa': 42, 'bbb': 'hello'}. {:} - пустой словарь. В оригинальной версии вместо фигурных скобок используются квадратные и мотивируется это тем, что фигурные уже используются для команд. И в начале я тоже сделал на квадратных. Но потом решил что это не нормально, что гораздо естественнее и приятнее использовать всё таки фигурные скобки. Скорей всего такое решение в оригинальной реализации обусловлено особенностями парсера, но у меня таких особенностей нет, поэтому в итоге переделал на фигурные скобки.

  • Добавлена функция keys. keys(map) - возвращает ключи словаря map в виде списка.

  • Добавлена функция augmentMap. augmentMap(baseMap, additionalMap) - создаёт новый словарь (ну, реализация несколько сложнее, не просто словарь), содержащий записи как из additionalMap, так и из baseMap. При поиске ключа в таком словаре он сначала ищется в additionalMap, а если не найден, то поиск продолжается в baseMap. Для Common Lisp получилась хорошая реализация, без лишних движений, а вот в случае JavaScript приходиться заниматься копированием additionalMap.

  • Добавлена функция strContains. strContains(str, subStr) - проверят является ли subStr частью str.

  • Добавлена функция isNonnull. isNonnull(value) - возвращает true если value определенна и не равна null.

  • Реализована возможность вызывать (call) шаблоны из других namespace. Например:

    {namespace mytemplate.test1}
    
    {template helloWorld}
        Hello world!
    {/template}

    {namespace mytemplate.test2}
    
    {template callHelloWorld}
        {call mytemplate.test1.helloWorld /}
    {/template}

    В случае Common Lisp вызвать таким образом можно только зарегистрированный шаблон, а в JavaScript всё что угодно.

  • Добавлена команда let. Мне не понравилось как она сделана в Google Closure Templates - не ясно как определяется область видимости создаваемых переменных. При чём, не ясно как в спецификации, там и при просмотре кода шаблоны так же могут быть трудности. Поэтому я решил сделать вариант с более чёткой структурой: {let $<identifier1>="<expression1>" $<identifierN>="<expressionN>"}...{/let}

    Пример:

    {let $x="1" $y="$foo.bar + 10"}
      // Здесь код, использующий $x и $y
    {/let}

    Честно говоря, ранее у меня уже была команда with, которая делала то же самое. Но let, конечно, куда лучше подходит по названию, плюс я немного поправил синтаксис. Правда, при этом была потеряна одна возможность (rendering to string) оригинальной реализации, но я считаю её совершенно несущественной.

В Google Closure Templates сейчас есть такая штука, как "delegates", с которой связанно несколько команд: delpackage, deltemplate и delcall. Я считаю данный функционал исключительно мутным и реализовывать что-либо подобное не хочу. А вместо это предлагаю, как мне представляется, куда более простой и понятный механизм - прототипы пространств имён.

Рассмотрим Common Lisp backend. При компиляции namespace создаётся package с таким же именем, а в нём функции для каждого шаблона из namespace. Но эти функции на самом деле никакой реализации не содержат, а просто обращаются к объекту *ttable*, находящемся в том же пакете. *ttable* это объект класса closure-template:ttable, который содержит в себе словарь с реальными обработчикам. Так вот, при создании этого объекта ему можно указать prototype - другой объект класса ttable. Вот небольшой пример, скажем у нас есть такой незамысловатый namespace:

{namespace myapp.view.base}

{template helloWorld}
    Hello world!
{/template}

{template mainPage}
    <h1>{call helloWorld /}</h1>
{/template}

Если теперь выполнить:

(closure-template:ensure-ttable-package
  '#:myapp.view.mytheme
  :prototype (closure-template:package-ttable '#:myapp.view.base)
)

И скомпилировать файл с шаблонами:

{namespace myapp.view.mytheme}

{template helloWorld}
    Привет мир!
{/template}

То станет возможным выполнить следующий код:

CL-USER> (myapp.view.base:main-page)
"<h1>Hello world!</h1>"
CL-USER> (myapp.view.mytheme:main-page)
"<h1>Привет мир!</h1>"

Надеюсь демонстрация получилась достаточно наглядной. Для JavaScript никакой специальной поддержки нет, достаточно просто реализовать такое поведение написав несколько строк на

myapp.view.mytheme = (function () {
    function C () {}
    C.prototype = myapp.view.base;
    return new C;
})();

Вообще, из-за накопления различий и появлением не вполне очевидных возможностей задумался про написание документации.