пятница, 26 февраля 2010 г.

Парсинг переменных в cl-routes

Я очень давно не трогал cl-routes, меня там далеко не всё устраивало с точки зрения реализации (имеет место некоторый бардак), но оно работало и мотивации для переработки у меня не было. Однако, хотя оно и работало, был один момент, который можно считать явным и серьёзным недостатком: не было возможности предъявить какие-либо требования к возможным значениям параметров шаблонов url. Например, в том же Django для задания шаблонов url используются регулярные выражения и это позволяет, например, задать что какой-нибудь параметр id может содержать только цифры.

Не знаю, почему я не сделал этого ещё год назад, но сейчас залез в исходный код, удалил несколько мертворождённых идей и добавил даже, как мне кажется, лучшую возможность: теперь можно при создании маршрута указывать пользовательскую функцию для парсинга переменной шаблона. Например:
CL-USER>(defparameter *map* (make-instance 'routes:mapper))
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
В данном примере используется стандартная функция #'parse-integer, что позволяет убедиться в том, что параметр id содержит только цифры, а кроме того - в случае успешного сопоставления url шаблону в параметре :id будет не строка, а целое число.

Подобным образом можно использовать любую функцию, которая должна принимать строку, а возвращать объект, который будет сопоставлен переменной шаблона. Если строка не соответствует требованиям, то эта функция должна вернуть 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.

пятница, 19 февраля 2010 г.

Старый parenscript в gentoo-lisp-overlay

Внезапно обнаружил, что cl-closure-template не совместима с версией parenscript, представленной в gentoo-lisp-overlay. Пришлось добавить в свой форк более свежую версию parenscript и обновить зависимости для cl-closure-template.

суббота, 13 февраля 2010 г.

AJAX с cl-closure-template. Часть 2.


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

Тут хотел бы обратить внимание, что подобной красоты нельзя добиваться с помощью тем jquery UI, при чём, это принципиальный момент. jquery UI в некоторых случаях модифицирует оригинальную разметку, добавляя к ней несколько своих элементов, в других вообще её никак не изменяет, полагаясь полностью на использование CSS. Но для создания подобных красивых вещей необходимо создавать достаточно сложную разметку фактически ещё в момент генерации страницы. Таким образом нельзя на сервере создать каркас, а потом доработать его с помощью jquery UI до подобного уровня. Как бы факт, что приложения на базе jquery UI не хватает элегантности.

Вот, теперь я хочу иметь возможность создавать подобные панельки простым и эффективным способом, ведь ручное создание подобной разметки для каждой панели будет весьма трудоёмким. С помощью cl-closure-template проблема решается тривиально.

Сначала нужен шаблона для каркаса панели:
{template panel}
<table class="panel">
<tbody>
<tr>
<td class="TL"></td>
<td class="TM">
<span class="TML">{$title |noAutoescape}</span>
{if $close}
<div class="TRx"></div>
{/if}
</td>
<td class="TR"></td>
</tr>

<tr>
<td class="ML"></td>
<td class="MM">{$body |noAutoescape}</td>
<td class="MR"></td>
</tr>

<tr>
<td class="BL"></td>
<td class="BM"></td>
<td class="BR"></td>
</tr>
</tbody>
</table>
{/template}
Данный шаблон принимает три параметра:
  • close - признак того, надо ли создавать кнопку для закрытия панели.
  • title - заголовок панели, в данном параметре можно передать не только текст, но и вообще произвольную разметку
  • body - разметка для основного содержимое панели
CSS для данного шаблона можно посмотреть здесь (.panel). В оригинальном варианте автор использовал разметку на базе элементов div, но в этом случае требуется доработка панели после загрузки с помощью JavaScript-напильника, поэтому я выбрал вариант разметки на базе table.

В предыдущем сообщении я ввёл шаблон editable-text и с помощью него создавал пару простейших элементов управления. Использовался он следующим образом:
{call editable-text data="$name" /}
Теперь, поместить результат работы этого шаблоны в красивую панельку можно следующим образом:
{call panel}
{param title: 'Name' /}
{param close: true /}
{param body}
{call editable-text data="$name" /}
{/param}
{/call}
Из чего видно, что результат вызова шаблона можно использовать как параметр для другого шаблона. Теперь можно не особо напрягаясь плодить подобные панельки в неограниченных колличествах :)

Продолжение следует.

понедельник, 8 февраля 2010 г.

paredit

Попробовал использовать paredit, который все так хвалят, для редактирования лисп-кода: это ужасно - оно насильно поддерживает правильный баланс скобок, что полностью сбивает меня с толку. Я использую незакрытые s-выражения для выражения того, что мысль не закончена. Я ставлю последнюю скобку когда считаю, что выражение готово.

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

P.S. У меня есть схожая проблема с js2-mode, но там хоть можно спокойно удалять автоматически подставленные символы.

суббота, 6 февраля 2010 г.

AJAX с cl-closure-template. Часть 1.

В результате работы над текущим проектом графического редактора у меня зародилась идея, которая может быть применима к широкому кругу AJAX-приложений. Поскольку эта идея демонстрирует преимущества использования cl-closure-template, то решил рассказать об этом здесь. Ну и надеюсь, что будет целая серия постов, поэтому это "Часть 1". И да, в данный момент я не обладаю полным решением, поэтому оно будет изменяться от поста к посту.

Мне нравится как сделан github, поэтому я решил показать, как можно реализовать элементы управления в его стиле. Самый простой элемент (он встречается там достаточно часто) можно найти на странице проекта (если у вас есть проекты на github, то вы им безусловно пользовались), например строка с описанием проекта. По ней можно кликнуть мышкой, в результате чего появится форма, в которой можно редактировать это описание. Несколько обобщив концепцию данного элемента можно получить следующую схему:
  • Элемент модели данных (хранящийся на сервере) описывается с помощью фрагмента разметки
  • Пользователь может активировать процесс редактирования этого элемента с помощью клика мышкой или иным образом
  • В результате изначальная разметка скрывается и вместо неё появляется форма для ввода данных
  • Пользователь может либо сохранить внесённые изменения, либо отказаться от них, в результате чего форма для ввода данных скрывается и снова появляется изначальная разметка, но модифицированная на основе введённых данных
Реализовать подобное поведение можно полностью на уровне JavaScript (как это видимо и сделано на github), но в этом случае JavaScript код должен иметь достаточно точные знания о разметке. Во-первых, это приводит к появлению сильных связей между серверным кодом, генерирующим страницу, и JavaScript-кодом. Во-вторых, написание JavaScript кода даже в достаточно простых случаях может оказаться довольно нудным занятием. Я хочу, что бы JavaScript код, решающий подобную проблему был абсолютно минимальным и обладал бы некоторой степенью общности, что позволило бы использовать его для различных элементов, работающих по подобной схеме.

Используемое мной решение заключается в том, что бы не заниматься "тонким редактированием" разметки, а производить полное её замещение на основе шаблонов, реализованных с помощью cl-closure-template: изменилась модель - создали новую разметку и подставили её вместо старой.

Для начала потребуются следующие шаблоны (которые в основном повторяют соответствующую разметку, используемую в github на момент написания данного поста):
{namespace example.githubway.view}

// Представление текстового элемента
{template editable-text}
<div class="editable-text" json="{$json}">
<p>
{$value}
<em class="edit-text">click to edit</em>
</p>
</div>
{/template}

// Форма для редактирование элемента
{template edit-text}
<form method="post" action="{$saveLink}" json="{$json}">
<input type="text" value="{$value}" name="value"></input>

<div class="form-actions">
<input type="submit" class="minibutton save" value="Save"></input>
<span class="fakelink cancel">cancel</span>
</div>
</form>
{/template}
Эти шаблоны принимают следующие аргументы:
  • value - значение элемента
  • saveLink - URL, который может использоваться для обновления значения элемента с помощью POST-запроса
  • json - а вот это любопытно, это предыдущие аргументы в формате JSON. Зачем это надо? Напомню, что cl-closure-template позволяет создавать шаблоны, доступные как на сервеной, так и на клиентской стороне. Я хочу, что бы JavaScript-код получил полную информацию об элементе модели, которую он сможет в последующем использовать для передачи в эти же шаблоны для генерации новой разметки. Для этого я сохраняю эти данные в атрибуте json корневого элемента генерируемой разметки, что существенно упрощает последующую обработку.
Теперь, JavaScript код, я выделил базовый класс, который может быть использован для различных подобных элементов:
function EObject (node) {
// node - узел DOM-дерева, представляющего элемент данных
if (node) {
this.node = node;
}
}

// Возвращает описание элемента модели в виде
// пригодном для генерации разметки с помощью шаблонов
EObject.prototype.modelData = function () {
var data = $.evalJSON(this.node.attr("json"))
data.json = $.toJSON(data);
return data;
};

// Метод для генерации основной разметки
EObject.prototype.toHTML = function (data) {
throw "Method toHTML not implemented";
};

// Метод для генерации формы редактирования
EObject.prototype.editForm = function (data) {
throw "Method editForm not implemented";
};

// Вспомогательный метод, позволяющий заменить разметку элемента
// таким образом, что бы в объекте осталась ссылка на актуальный
// элемент DOM-дерева
EObject.prototype.replaceHTML = function (html) {
this.node.after(html);
this.node = this.node.next();
this.node.prev().remove();
};

// Заменяет основное представление на форму редактирования
EObject.prototype.startEdit = function () {
this.replaceHTML(this.editForm(this.modelData()));

var obj = this;

$('.cancel:first', this.node).click(function (evt) { obj.endEdit(); });

this.node.ajaxForm({
dataType: 'json',
success: function (data) { obj.endEdit(data)},
error: function () { alert("Не удалось сохранить данные"); obj.endEdit() }
});
};

// Заменяет форму редактирования на основной вариант разметки
EObject.prototype.endEdit = function (data) {
this.replaceHTML(this.toHTML(data || this.modelData()));

// Похоже на хак, но мне кажется нормальным решением.
// Фактически, просто ре-инициализация объекта, которая,
// например, приведёт к активизации нужных обработчиков событий
this.constructor(this.node);
};
Данный код использует библиотеке jquery и плагины jquery.form и jquery.json. Теперь, создать код для редактирования текстового элемента очень просто:
function EditableText (node) {
if (node) {
// вызываем конструктор базового класса
EObject.prototype.constructor.call(this, node);

// Инициируем вызов процедуру редактирования по клику мышкой
var obj = this;
this.node.click(function (evt) { obj.startEdit(); });
}
}

// Инициализируем прототип
EditableText.prototype = new EObject;

// Необходимо, что бы иметь возможность создать новый класс, наследующий от EditableText
EditableText.prototype.constructor = EditableText;

// Генерируем разметку с помощью ранее описанного шаблона editable-text
EditableText.prototype.toHTML = example.githubway.view.editableText;

// Генерируем форму редактирования с помощью ранее описанного шаблона edit-text
EditableText.prototype.editForm = example.githubway.view.editText;
И наконец инициализация при загрузке документа:
$(document).ready(function () {
// Тоже довольно любопытно. Просто создаём новые объекты "в никуда".
// Но благодаря привязке этих объектов к DOM-дереву через обработчики
// событий они останутся "жить" и будут выполнять свою работу
$(".editable-text").each( function (i, node) { new EditableText($(node)); })
});
Теперь серверная часть. Для демонстрации я использую очень простое приложение с одной страницей, на которой показывается имя и email некоего человека. Пользователь приложения может отредактировать их в стиле github. Код данного приложения зависит от:Сначала определим очень простую модель
(defparameter *name* "Ivan Petrov")

(defparameter *email* "Ivan.Petrov@example.com")


(defun with-json (&rest args)
(list* :json (json:encode-json-plist-to-string args)
args))

(defun name-to-json ()
(with-json :value *name*
:save-link (genurl 'save-name)))

(defun email-to-json ()
(with-json :value *email*
:save-link (genurl 'save-email)))
Здесь функции name-to-json и email-to-json генерируют plist, подходящий для использования с шаблонами, описанными в начале.

Описываем маршрут для основной страницы:
(define-route main ("")
(example.githubway.view:page (list :name (name-to-json)
:email (email-to-json))))
Ради экономии места здесь я не буду приводить текст шаблона example.githubway.view:page, укажу лишь, что в нём есть следующие строчки:
{call editable-text data="$name" /}
{call editable-text data="$email" /}
Теперь определяем обработчики для обновления данных об имени и email, ради упрощения какая-либо обработка ошибок опущена:
(define-route save-name ("api/name" :method :post :content-type "application/json")
(setf *name*
(hunchentoot:post-parameter "value"))
(json:encode-json-plist-to-string (name-to-json)))

(define-route save-email ("api/email" :method :post :content-type "application/json")
(setf *email*
(hunchentoot:post-parameter "value"))
(json:encode-json-plist-to-string (email-to-json)))
Вот и всё. Полный код данного приложения я включил в состав closure-template и посмотреть его можно здесь.

Продолжение следует...

среда, 3 февраля 2010 г.

Спасём Утриш!

Продолжается сбор подписей в защиту уникальной природной территории, которую хотят уничтожить ради строительства правительственной дачи. Заказник Утриш может погибнуть. Просьба ко всем неравнодушным людям: зайдите, почитайте, поставьте свою подпись под обращением к президенту и правительству, разошлите ссылку друзьям и знакомым, размещайте в блогах и форумах, помогите спасти Утриш!
http://save-utrish.spb.ru/vote/

группа «Спасём Утриш!» : http://vkontakte.ru/club1183453

P.S. Почему тэг lisp? Что бы больше людей прочитало, извиняюсь :)

вторник, 2 февраля 2010 г.

EAPI 3 в gentoo-lisp-overlay

Сегодня, после обновления gentoo-lisp-overlay вдруг обнаружил, что система хочет откатить мой SBCL-1.0.34 до версии 1.0.31, и это вместо того, что бы обновить его до версии 1.0.35! Как вскоре выяснилось, это связано с тем, что Stelian Ionescu перевёл пакеты для SBCL, ClozureCL и CMUCL на использование EAPI 3. Извините, я конечно понимаю, что переход на новые возможности portage обычно начинается с оверлеев, но я использую gentoo-lisp-overlay на нескольких машинах, в том числе и на рабочих серверах и переходить на использование portage-2.2, до тех пор пока он не попал в стабильную ветку, у меня никакого желания нет.

В общем, сделал revert для этих коммитов с EAPI 3, сам обновил ebuild для SBCL до версии 1.0.35 и залил в свой форк.

понедельник, 1 февраля 2010 г.

И ещё форум о Clojure

Ну и ещё один новый форум, о Clojure - http://lisper.ru/forum/clojure. Пожалуй на этом стоит пока остановиться :)