понедельник, 8 марта 2010 г.
воскресенье, 7 марта 2010 г.
Hunchentoot: упрощение отладки удалённых серверов
В hunchentoot версии 1.1 для управления отладочным режимом используется глобальная переменная *catch-errors-p*: если она установлена в значение, отличное от NIL, то hunchentoot перехватывает все conditions, в обратном случае следует вызов invoke-debugger и разработчик получает возможность разобраться в причинах ошибки с помощью стандартного отладчика. При разработке на машине разработчика никаких проблем не возникает, наоборот, это очень удобно. Однако, при отладке на боевых удалённых серверах (порой это необходимо) эта простая схема начинает представлять угрозу. Во-первых, на серверах с реальной нагрузкой если ошибка возникает на регулярной основе может случиться так, что отладчик будет активизироваться чаще, чем вы способны на это реагировать. Во-вторых, можно забыть сбросить значение *catch-errors-p* при разрыве соединения с удалённым swank-сервером, что приведёт к тому, что потоки начнут подвисать на вызове invoke-debugger и в конце концов система просто откажется работать. Таким образом, для удобной и безопасной отладки удалённых серверов необходимо ограничивать число возможных активных вызовов invoke-debugger, а также автоматически отменять отладочный режим при отключении от удалённого процесса. Сегодня в репозиторий hunchentoot был принят мой патч, решающий описанные проблемы следующим образом:
- Постоянно поддерживается список отлаживаемых в данный момент потоков
- Добавлен параметр *max-debugging-threads*: максимально возможное количество одновременно отлаживаемых потоков, значение по умолчанию - 5.
- Функция debug-mode-on - активизирует отладочный режим.
- Функция debug-mode-off - отменяет отладочный режим, имеет необязательный параметр kill-debugging-threads (по-умолчанию T) , который определяет надо ли уничтожать отлаживаемые в данный момент потоки.
- В случая наличия в системе swank-сервера, в переменную swank::*connection-closed-hook* добавляется вызов debug-mode-off , который обеспечивает отмену отладочного режима и уничтожение отлаживаемых потоков после разрыва соединения.
- Описанная схема применяется только к потокам, которые созданы hunchentoot для обработки запросов.
среда, 3 марта 2010 г.
Символьные вычисления 2
Ранее я уже писал о символьных вычислениях, но в тот раз ограничился только общими впечатлениями и обещал написать отдельную большую статью. К таковой я, к сожалению, ещё не готов, но зато могу рассказать о том, как я использую символьные вычисления для реализации cl-closure-template, тем более, что последний троллинг на ЛОР-е буквально вынуждает меня к этому.
cl-closure-template - это система шаблонов для веб и казалось бы точно никакого отношения к символьным вычислениям иметь не может. И это, вероятно, действительно так если речь идёт о php. Но использование Common Lisp для решения этой задачи даёт богатую почву для использования символьных вычислений. Мало того, они чрезвычайно эффективны для решения данной задачи. Итак, cl-closure-template имеет следующие компоненты:
1. src/expression.lisp - парсер выражений, используемых в языке шаблонов. Это расширенная и переработанная версия файла infix.lisp из AIMA. Пусть дано выражение:
Т.е. здесь очевидно имеют две фазы вычислений, которые можно с полным правом отнести к символьным: преобразование строки в символьную форму и последующее приведение её к префиксной нотации.
2. src/template.lisp - собственно парсер шаблонов. Для описания грамматики языка шаблонов используются макросы define-mode, вот как выглядит описание тэга "for":
3. src/common-lisp-backend.lisp - предоставляет возможность использовать шаблоны в программах на Common Lisp. Шаблоны компилируются в машинный код (на поддерживающих это реализациях), для этого символьное представление шаблона преобразуется в код на Common Lisp, который, естественно, тоже имеет символьную форму. Опять получилось две фазы символьных вычислений: преобразование одной символьной формы в другую и выполнение её с помощью eval.
4. src/javascript-backend.lisp - аналогичен предыдущему компоненту, но только предназначен для генерации кода на JavaScript, для чего шаблон в символьной форме сначала преобразуется в символьный формат parenscript и затем с помощью parenscript компилируется в JavaScript. Опять совершенно явный пример символьных вычислений.
5. t/cl-backend-test.lisp - тесты для CL-бэкэнда, примечательным является то, что используемая библиотека lift сохраняет полную информацию о тестах в символьной форме в свойствах символов. Обычно это не имеет никакого значения, но оказалось очень важным для данного проекта.
6. t/js-backend-test.lisp - обеспечивает возможность тестирования JS-бэкэнда. Тесты для CL и JS-бэкэндов должны быть, вообще говоря, совершенно одинаковыми и я очень не хотел писать их руками, что бы потом ещё и мучатся с сопровождением. На помощь пришло описанное выше свойство lift. Код в данном файле извлекает полную информацию о тестах для CL-бэкэнда, в том числе код самих тестов, преобразует эти тесты в формат parenscript и компилирует с его помощью их в тесты для jsunittest.js. Кроме того, запускается веб-сервер и теперь можно смотреть результаты выполнения тестов в браузере. Я был чрезвычайно впечатлен этим ярким примером использования символьных вычислений.
Вывод: реализация cl-closure-template является примеров использования символьных вычислений, собственно, кроме символьных вычислений там больше и нет ничего.
У меня имеется ещё богатый материал по использованию символьных вычислений в cl-routes и RESTAS, да и вообще, чем больше я пишу на Common Lisp, тем больше использую возможности символьных вычислений, но об этом пожалуй в следующий раз.
cl-closure-template - это система шаблонов для веб и казалось бы точно никакого отношения к символьным вычислениям иметь не может. И это, вероятно, действительно так если речь идёт о php. Но использование Common Lisp для решения этой задачи даёт богатую почву для использования символьных вычислений. Мало того, они чрезвычайно эффективны для решения данной задачи. Итак, cl-closure-template имеет следующие компоненты:
1. src/expression.lisp - парсер выражений, используемых в языке шаблонов. Это расширенная и переработанная версия файла infix.lisp из AIMA. Пусть дано выражение:
$a + $b[1]Первым делом оно считывается в символьную форму:
((:VARIABLE :A) + (:VARIABLE :B) [ 1 ])Теперь полученную инфиксную форму необходимо привести к принятой в лисп префиксной нотации:
(+ (:VARIABLE :A) (ELT (:VARIABLE :B) 1))Сам алгоритм преобразования представляется мне довольно сложным, но он был сложен ещё в AIMA и я поначалу не верил, что смогу его модифицировать, а cl-closure-template должна обрабатывать куда более сложные выражение, чем оригинальная версия (если кто забыл, то в AIMA этот код используется для разбора выражений логики первого порядка).
Т.е. здесь очевидно имеют две фазы вычислений, которые можно с полным правом отнести к символьным: преобразование строки в символьную форму и последующее приведение её к префиксной нотации.
2. src/template.lisp - собственно парсер шаблонов. Для описания грамматики языка шаблонов используются макросы define-mode, вот как выглядит описание тэга "for":
(define-mode for-tag (70 :all)Информация, указанная в данной форме сохраняется в свойствах символа for-tag, который в последующем используется для сборки парсера, а сам символ for-tag будет использоваться в s-выражении, являющимся результатом разбора шаблона. Например, разбор следующего шаблона:
(:allowed :all)
(:entry "{for\\s+[^}]*}(?=.*{/for})")
(:entry-attribute-parser parse-for-attributes)
(:exit "{/for}"))
{template test}
{for $x in range(10)} ! {/for}
{/template} вернёт такую символьную форму: (closure-template.parser:template ("test")
(closure-template.parser:for-tag ((:variable :x)
(:range 10))
" ! ")) Таким образом, в данном компоненте можно отметить, что для описания грамматики шаблонов используется символьная форма, а результатом разбора шаблона является символьное выражение - весьма характерный пример использования символьных вычислений. Кроме того, в коде используется предоставляемая cl-ppcre возможность задания регулярных выражений в символьной форме, которая весьма существенно упрощает реализацию.3. src/common-lisp-backend.lisp - предоставляет возможность использовать шаблоны в программах на Common Lisp. Шаблоны компилируются в машинный код (на поддерживающих это реализациях), для этого символьное представление шаблона преобразуется в код на Common Lisp, который, естественно, тоже имеет символьную форму. Опять получилось две фазы символьных вычислений: преобразование одной символьной формы в другую и выполнение её с помощью eval.
4. src/javascript-backend.lisp - аналогичен предыдущему компоненту, но только предназначен для генерации кода на JavaScript, для чего шаблон в символьной форме сначала преобразуется в символьный формат parenscript и затем с помощью parenscript компилируется в JavaScript. Опять совершенно явный пример символьных вычислений.
5. t/cl-backend-test.lisp - тесты для CL-бэкэнда, примечательным является то, что используемая библиотека lift сохраняет полную информацию о тестах в символьной форме в свойствах символов. Обычно это не имеет никакого значения, но оказалось очень важным для данного проекта.
6. t/js-backend-test.lisp - обеспечивает возможность тестирования JS-бэкэнда. Тесты для CL и JS-бэкэндов должны быть, вообще говоря, совершенно одинаковыми и я очень не хотел писать их руками, что бы потом ещё и мучатся с сопровождением. На помощь пришло описанное выше свойство lift. Код в данном файле извлекает полную информацию о тестах для CL-бэкэнда, в том числе код самих тестов, преобразует эти тесты в формат parenscript и компилирует с его помощью их в тесты для jsunittest.js. Кроме того, запускается веб-сервер и теперь можно смотреть результаты выполнения тестов в браузере. Я был чрезвычайно впечатлен этим ярким примером использования символьных вычислений.
Вывод: реализация cl-closure-template является примеров использования символьных вычислений, собственно, кроме символьных вычислений там больше и нет ничего.
У меня имеется ещё богатый материал по использованию символьных вычислений в cl-routes и RESTAS, да и вообще, чем больше я пишу на Common Lisp, тем больше использую возможности символьных вычислений, но об этом пожалуй в следующий раз.
понедельник, 1 марта 2010 г.
Новые версии моих пакетов
Вобщем, решил таки зарелизить все последние изменения в моих пакетах, итого:
- cl-routes-0.2.1 (новая возможность описанная здесь)
- cl-closure-template-0.1.4 (скомпилированные шаблоны должны стать быстрее)
- restas-0.0.4 (подробности в предыдущем сообщении)
Кроме того, привёл в соответствие с новыми версиями свой форк gentoo-lisp-overlay, а также и форк clbuild.
P.S. Vladimir Sedach сообщил о релизе parenscript-2.1. В gentoo-lisp-overlay этого ещё нет, поэтому сделал сам для своего форка.
Модули в RESTAS
В последние дни провёл большую чистку RESTAS (а также и rulisp), по удалял устаревший код и переработал архитектуру модулей. На последнем моменте хотел бы остановиться подробней.
Изначально, в RESTAS было понятие site и понятие plugin, которые определялись через defsite и define-plugin соответственно. site можно было запустить как web-сайт, а plugin являлся элементом повторного использования, который (например, wiki или форум) можно было использовать повторно на различных сайтах. В результате проведённой переработки я избавился от обоих этих понятий, а вместо них ввёл единственное - module. Предыдущая схема была двухуровневой, текущая - иерархической.
И наконец, полученный модуль можно запустить как web-сайт:
Таким образом, module, разместив в нём несколько маршрутов, можно использовать для запуска web-приложения. Но у модулей есть ещё и другой вариант использования.
В таком виде данный функционал не выглядит очень полезным и вот почему. Для успешного повторного использования любого компонента надо уметь его конфигурировать, настраивать его параметры - без этой возможности повторное использование сведётся к технике copy/paste с последующим редактированием кода, что выглядит удручающее само по себе, а в контексте CL ещё имеет и множество технических ограничений (что связано с тем, что понятие package никак не связано с физическим размещением кода на файловой системе). В ООП традиционным способом решения проблем конфигурации является использование классов, но, хотя Common Lisp и имеет сверхмощную поддержку ООП (CLOS + MOP), я решил всё таки отказаться от подобного подхода: возникающие проблемы дизайна, проектирования, бесконечные интерфейсы и наследование всегда существенным образом повышают уровень сложности системы, что кажется мне совершенно излишним для такой простой области, как разработка web-приложений. Для решения этой проблемы в Common Lisp есть ещё один потрясающий механизм: динамические переменные. Сразу реальный пример кода, используемый на lisper.ru для публикации статических файлов:
В модуле restas.directory-publisher определены (с помощью defparameter или defvar) несколько глобальных динамических переменных, которые можно использоваться для настройки его работы. В макросе restas:define-submodule некоторые из этих переменных связываются c новыми значения, но эти связывания не применяются непосредственно, а сохраняются в виде контекста для использования в будущем. При обработке запроса диспетчер находит маршрут, определяет связанный с ним submodule, настраивает окружение на основе сохранённого контекста и производит дальнейшую обработку запроса в рамках этого окружения (для этого используется вызов progv). Данный механизм напоминает Buffer-Local Variables в Emacs, я описывал вариант его реализации здесь.
При определении нового модуля с помощью restas:define-module в него (в пакет) добавляется переменная *baseurl*, которая должна быть списком строк и определяет базовый url, по которому будет активизирован данный модуль, по-умолчанию она установленная в nil. Данную переменную можно использовать в restas:define-submodule для задания url, по которому будет включён submodule. Вот более сложный пример использования модуля restas-directory-publisher на сайте lisper.ru (посмотреть этот код в работе можно по адресу http://lisper.ru/files/):
В качестве демонстрации, вот код для запуска restas-directory-publisher, который я использовал выше как повторно используемый компонент, в виде standalone-приложения:
P.S. Описанная архитектура доступна в git версии RESTAS и если вы захотите её опробовать, но ранее уже запускали более старые версии RESTAS, то настоятельно рекомендую удалить .fasl файлы, имеющие к нему какое-либо отношение.
Изначально, в RESTAS было понятие site и понятие plugin, которые определялись через defsite и define-plugin соответственно. site можно было запустить как web-сайт, а plugin являлся элементом повторного использования, который (например, wiki или форум) можно было использовать повторно на различных сайтах. В результате проведённой переработки я избавился от обоих этих понятий, а вместо них ввёл единственное - module. Предыдущая схема была двухуровневой, текущая - иерархической.
Modules
С точки зрения Common Lisp module это просто package. С точки зрения RESTAS module это набор маршрутов (routes, подробнее о них можно почитать здесь), определяющих структуру web-приложения. module создаётся с помощью макроса restas:define-module, что приводит к созданию пакета с соответствующим именем, плюс проводится некоторая дополнительная инициализация этого пакета. Пример:(restas:define-module #:hello-worldТеперь можно создать в этом модуле несколько маршрутов:
(:use :cl))
(in-package #:hello-world)Обращаю внимание на принципиальную важность размещения макроса define-route в пакете (после in-package), связанном с определённым модулем.
(define-route main ("hello")
"<h1>Hello world!</h1>")
И наконец, полученный модуль можно запустить как web-сайт:
(restas:start '#:hello-world :port 8080)В функцию restas:start (в предыдущих версиях данная функция называлась start-site) можно также передать ключевой параметр :hostname, что позволяет обслуживать несколько виртуальных хостов в рамках одного процесса.
Таким образом, module, разместив в нём несколько маршрутов, можно использовать для запуска web-приложения. Но у модулей есть ещё и другой вариант использования.
Submodules
Меня всегда волновала проблема повторного использования кода и занявшись web я заинтересовался как я могу повторно использовать код для, скажем, форума или вики, на нескольких сайтах, или даже несколько раз в рамках одного сайта? Отличие подобного web-приложения от простой библиотеки, содержащей функции, макросы, классы и т.п в том, что web-компонент также должен содержать информацию об обслуживаемых им url и эту информацию надо использовать в механизме диспетчеризации запросов, правильно определяя код, ответственный за обработку поступившего запроса. В терминах routes (маршрутов) любой повторно используемый web-компонент является просто списком обрабатываемых им маршрутов, а это как раз и есть то, чем являются modules с точки зрения RESTAS. Для повторного использования модуля в рамках другого модуля используется макрос define-submodule. Пример (зависит от кода выше):(restas:define-module #:testВ данном примере определяется новый модуль test, к нему присоединяется определённый выше модуль hello-world (а получившийся submodule ассоциируется с символом test-hello-world) и модуль test запускается на порту 8080. Хотя в самом модуле test не определён ни один маршрут, но в итоге он способен обрабатывать запросы, поступающие на /hello благодаря включению в себя модуля hello-world.
(:use :cl))
(in-package #:test)
(restas:define-submodule test-hello-world (#:hello-world))
(restas:start '#:test :port 8080)
В таком виде данный функционал не выглядит очень полезным и вот почему. Для успешного повторного использования любого компонента надо уметь его конфигурировать, настраивать его параметры - без этой возможности повторное использование сведётся к технике copy/paste с последующим редактированием кода, что выглядит удручающее само по себе, а в контексте CL ещё имеет и множество технических ограничений (что связано с тем, что понятие package никак не связано с физическим размещением кода на файловой системе). В ООП традиционным способом решения проблем конфигурации является использование классов, но, хотя Common Lisp и имеет сверхмощную поддержку ООП (CLOS + MOP), я решил всё таки отказаться от подобного подхода: возникающие проблемы дизайна, проектирования, бесконечные интерфейсы и наследование всегда существенным образом повышают уровень сложности системы, что кажется мне совершенно излишним для такой простой области, как разработка web-приложений. Для решения этой проблемы в Common Lisp есть ещё один потрясающий механизм: динамические переменные. Сразу реальный пример кода, используемый на lisper.ru для публикации статических файлов:
(restas:define-submodule rulisp-static (#:restas.directory-publisher)В данном примере используется модуль restas-directory-publisher, о котором я уже писал ранее.
(restas.directory-publisher:*directory* (merge-pathnames "static/" *resources-dir*))
(restas.directory-publisher:*autoindex* nil))
В модуле restas.directory-publisher определены (с помощью defparameter или defvar) несколько глобальных динамических переменных, которые можно использоваться для настройки его работы. В макросе restas:define-submodule некоторые из этих переменных связываются c новыми значения, но эти связывания не применяются непосредственно, а сохраняются в виде контекста для использования в будущем. При обработке запроса диспетчер находит маршрут, определяет связанный с ним submodule, настраивает окружение на основе сохранённого контекста и производит дальнейшую обработку запроса в рамках этого окружения (для этого используется вызов progv). Данный механизм напоминает Buffer-Local Variables в Emacs, я описывал вариант его реализации здесь.
При определении нового модуля с помощью restas:define-module в него (в пакет) добавляется переменная *baseurl*, которая должна быть списком строк и определяет базовый url, по которому будет активизирован данный модуль, по-умолчанию она установленная в nil. Данную переменную можно использовать в restas:define-submodule для задания url, по которому будет включён submodule. Вот более сложный пример использования модуля restas-directory-publisher на сайте lisper.ru (посмотреть этот код в работе можно по адресу http://lisper.ru/files/):
(restas:define-submodule rulisp-files (#:restas.directory-publisher)Кстати, вкупе с предыдущим, данный пример демонстрирует двукратное использование одного модуля с различными режимами работы в рамках одного и того же сайта, без каких-либо конфликтов между собой.
(restas.directory-publisher:*baseurl* '("files"))
(restas.directory-publisher:*directory* (merge-pathnames "files/" *vardir*))
(restas.directory-publisher:*autoindex-template*
(lambda (data)
(rulisp-finalize-page :title (getf data :title)
:css '("style.css" "autoindex.css")
:content (restas.directory-publisher.view:autoindex-content data)))))
Внутренняя инициализация
Макрос restas:define-submodule позволяет контролировать настройку модуля "снаружи", но порой надо иметь возможность влиять на создаваемый контекст изнутри модуля. Например, модуль restas-planet, который используется для организации Russian Lisp Planet нуждается в механизме для сохранения объекта-робота (он по заданному расписанию считывает ленты и объединяет их в одну), который должен быть вычислен на основе переменных, которые могут быть помещены в контекст submodule. Для такого случая предусмотрен макрос restas:define-initialization, вот реальный код из planet.lisp:(restas:define-initialization (context)Здесь производится вычисления объекта spider, который ассоциируется с переменной *spider* и помещается в контекст создаваемого submodule. Данный код будет вычислен при вычислении формы restas:define-submodule. Поскольку создание объекта spider приводит к запуску планировщика (в частности, создаётся таймер), то также надо уметь останавливать их при повторном вычислении формы restas:define-submodule, для этого предусмотрен макрос restas:define-finalization:
(restas:with-context context
(when *feeds*
(restas:context-add-variable context
'*spider*
(make-instance 'spider
:feeds *feeds*
:schedule *schedule*
:cache-dir (if *cache-dir*
(ensure-directories-exist (merge-pathnames "spider/"
*cache-dir*))))))))
(restas:define-finalization (context)
(let ((spider (restas:context-symbol-value context '*spider*)))
(when spider
(spider-stop-scheduler spider))))
Дуализм
Описанная схема подразумевает дуализм: модуль как standalone-приложение, и он же как компонент повторного использования, что позволяет разрабатывать модуль без какого-либо учёта возможности повторного использования, а потом минимальной ценой (просто приводя его к "правильному" дизайну) превращать в многократно используемый компонент. Подобный подход, как мне кажется, позволяет в значительной степени избежать проблем, свойственных традиционному ООП-дизайну.В качестве демонстрации, вот код для запуска restas-directory-publisher, который я использовал выше как повторно используемый компонент, в виде standalone-приложения:
(restas:start '#:restas.directory-publisherТеперь открыв в браузере страницу http://localhost:8080/tmp/ можно будет наблюдать содержимое директории #P"/tmp/".
:port 8080
:context (restas:make-context (restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/")
(restas.directory-publisher:*autoindex* t)))
P.S. Описанная архитектура доступна в git версии RESTAS и если вы захотите её опробовать, но ранее уже запускали более старые версии RESTAS, то настоятельно рекомендую удалить .fasl файлы, имеющие к нему какое-либо отношение.
пятница, 26 февраля 2010 г.
Парсинг переменных в cl-routes
Я очень давно не трогал cl-routes, меня там далеко не всё устраивало с точки зрения реализации (имеет место некоторый бардак), но оно работало и мотивации для переработки у меня не было. Однако, хотя оно и работало, был один момент, который можно считать явным и серьёзным недостатком: не было возможности предъявить какие-либо требования к возможным значениям параметров шаблонов url. Например, в том же Django для задания шаблонов url используются регулярные выражения и это позволяет, например, задать что какой-нибудь параметр id может содержать только цифры.
Не знаю, почему я не сделал этого ещё год назад, но сейчас залез в исходный код, удалил несколько мертворождённых идей и добавил даже, как мне кажется, лучшую возможность: теперь можно при создании маршрута указывать пользовательскую функцию для парсинга переменной шаблона. Например:
CL-USER>(defparameter *map* (make-instance 'routes:mapper))В данном примере используется стандартная функция #'parse-integer, что позволяет убедиться в том, что параметр id содержит только цифры, а кроме того - в случае успешного сопоставления url шаблону в параметре :id будет не строка, а целое число.
CL-USER>(routes:connect *map* (routes:make-route "/foo/:id" (list :id #'parse-integer)))
CL-USER>(routes:match *map* "/foo/1")
(#<ROUTES:ROUTE {...}> (:ID . 1))
CL-USER>(routes:match *map* "/foo/hello")
NIL
Подобным образом можно использовать любую функцию, которая должна принимать строку, а возвращать объект, который будет сопоставлен переменной шаблона. Если строка не соответствует требованиям, то эта функция должна вернуть nil или сигнализировать об ошибке (эта возможность добавлена для возможности непосредственного использования #'parse-integer)
Соответственно, тут же немного доработал RESTAS, например теперь возможно задавать маршруты следующим образом:
(define-route myroute ("/foo/:id" :parse-vars (list :id #'parse-integer))
...)Для использования этой новой возможности необходимо использовать git-версии cl-routes и RESTAS. Впрочем, новые релизы я думаю будут довольно скоро.
воскресенье, 21 февраля 2010 г.
cl-closure-template-0.1.3
На днях мне прислали баг-репорт на cl-closure-template, в котором сообщалось о следующих проблемах:
- Не работали выражения типа $a[i][j], т.е. не работал доступ к элементам вложенных массивов
- Вызов шаблона (call) в параметрах (param) другого шаблона не работал с пустым телом, т.е. не работала инструкция типа
{call template1}
{param x}{call template2 data="$a" /}{/param}
{/call} - Блок literal удалял "лишние" пробельные символы, хотя по спецификации не должен был этого делать
- Директивы печати требовали отсутствия пробелов справа от |, что не соответствует спецификации.
- Так же, было указано, что в качестве "массива" в параметрах шаблона можно было передать только список (Common Lisp backend), но имеет смысл разрешить использовать любую sequence.
Я устранил эти недостатки (это оказалось очень просто) и оформил в виде новой версии - cl-closure-template-0.1.3.
Подписаться на:
Сообщения (Atom)
