-*- mode: org; fill-column: 75 -*-

@category cl @sort cl001

Введение

Это статья преследует две цели - показать элементы <a href="http://nature.web.ru/db/msg.html?mid=1180230&uri=2i.html">literate programming</a> в Common Lisp и иллюстрировать применение возможностей языка к типичным задачам веб-разработки.

Что касается первого — я расскажу об опыте использования уникальных фич языка для поддержки literate programming; в отношении второго — покажу, как удобно на лиспе можно реализовываются основные применяемые в веб-программированиях вещи — шаблонизация, роутинг, кеширование и другие.

Макросы чтения для literate programming

Внимательно следите за руками — текст, который вы читаете — это одновременно:

@code ★ Файл исходного кода на языке Common Lisp, который можно непосредственно выполнить ★ Файл для препроцессора, который в зависимости от режима: • Выдирает исходный код, код шаблонов и прочее и раскладывает их по файлам с целью получить дистрибутив для развертывания • Создает html-эссе, разбитое по главам, с оглавлением и подсветкой кусков кода, которое приятнее (и понятнее) читать, чем просто исходники. @/code

Реализовано это включением нескольких процедур в файл .sbclrc, который запускается при старте лиспа. В этом файле определяются "макросы чтения" (reader macros). Макросы чтения выполняются еще на этапе обработки программы парсером при обнаружении специальных символов (dispatch characters) и дают возможность возможностью получить прямой доступ к Reader"у, то есть влиять на то, как он формирует абстрактное синтаксическое дерево из "сырого" программного кода. Это эквивалентно изменению процедур разбора токенов в компиляторах традиционных языков.

Я использую это вместе с <a href="http://habrahabr.ru/blogs/linux/80091/">Compose</a> чтобы вводить практически неиспользуемые в программах уникодные символы. Таким образом можно без геморроя иметь в эссе вставки, которые не являеются корректным лисп-кодом, например куски html-кода (шаблоны) или даже ascii-диаграммы.

При этом в режиме исполнения такого эссе эти шаблоны можно компилировать на лету.

Для препроцессора те же символы — являются командами, которые в режиме создания дистрибутива распихивают куски кода по нужным файлам, а в режиме отображения html-эссе управляют форматированием и подсветкой синтаксиса.

Чтобы не быть голословным, но и не перегружать вас большим количеством кода, приведу вот такой пример — этот код, размещенный в .sbclrc заставляет лисп пропускать все, что находится между символами "⟳" и "⟲", включая сами эти символы:

@code (set-macro-character #\⟳ #'(lambda (is char) (declare (ignore char)) (do ((c (read-char is) (read-char is nil 'the-end))) ((or (not (characterp c)) (char= c #\⟲))) ;; Здесь можно вставить обработчик ;; … ) (read is))) @/code

Проверить работу этого кода можно, например, так:

@code (list ⟳ 'a 'b 'c 'd ⟲ 'e 'f) => (E F) @/code

Таким образом мы можем расширять синтаксис языка для целей поддержки literate programming причем так, что код остается компилируемым. Таким образом документация не отделяется от кода, что является полезным профитом.

==<img src"/img/different-lisp.gif" style="margin-left: -30px" alt="Lisp is different!" />

Шаблоны и их препроцессинг

Теперь в деталям. Некоторые куски кода в этой статье снабжены директивами препроцессора, начинающимися с символа "@", которые указывают что препроцессору с ними делать. Ниже — шаблон "root", который препроцессор положит в файл "template2.htm", если возникнет желание собрать дистрибутив:

@code {namespace tpl} {template root} <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">{\n} <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">{\n} <head>{\n} <title>{$headtitle}</title>{\n} <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />{\n} <link rel="stylesheet" type="text/css" media="screen" href="/style.css" />{\n} <link rel="Shortcut Icon" type="image/x-icon" href="/favicon.ico" />{\n} </head>{\n} <body id="top">{\n} {$content | noAutoescape}{\n} </body>{\n} </html>{\n} {/template}

@store template2.htm @/code

C помощью библиотеки CL-CLOSURE-TEMPLATE он на лету компилируется в функцию root, находящуюся в пакете tpl. Таким образом, применение данных к шаблону — это вызов скомпилированной по этому шаблону функции:

@code (tpl:root (list :headtitle "Мой заголовок"</code>

:content "Hello world")) @/code

Вместо "Hello world" можно подставить вызов функции, в которую компилируется другой шаблон — например шаблон "base", обеспечивающий минимальную сетку для сайта:

@code {template base} <div id="center"> <div class="col1 left"> <a id="logo" href="index.html"> <img src="http://www.gravatar.com/avatar/d8a986606b9d5e4769ba062779e95d9f?s=45" style="border: 1px solid #7F7F7F"/> </a> <ul id="nav"> {foreach $elt in $navpoints} {call navelt data="$elt" /} {/foreach} </ul> </div> {$content |noAutoescape} <div class="clear">.</div> </div> <div id="footer"> <p> <a href="/about">About</a> | <a href="/contacts">Contacts</a> </p> </div> {/template}

@append template2.htm @/code

Как видите, препроцессор добавит содержимое этого блока все в тот же файл. В дальнейшем я не буду на этом останавливаться так подробно.

Усовершенствованный диспетчер

Теперь, воспользовавшись примером из <a href="http://habrahabr.ru/blogs/webdev/111365/">вводной статьи</a> и нашим свежесозданным шаблоном, мы могли бы написать request-dispatcher для сайта из одной страницы так:

@code (defun request-dispatcher (request) (tpl:root (list :headtitle "My home page"

:content (tpl:base (list :navpoints ..тут-меню..

:content ..тут-контент..))))) @/code

Маршруты RESTAS

Библиотека RESTAS освобождает нас от увлекательного написания диспетчеров. Теперь диспетчер руками будет создан на базе задаваемых нами маршрутов (route), которые мы определяем вот так:

@code (restas:define-module #:rigidus (:use :cl))

(in-package #:rigidus)

(defparameter base-dir "path/to/site/directory")

(restas:define-route main ("") (tpl:main (list :headtitle "My main page"

:content "Hello! <a href=\"/articles\">Articles</a>")))

(restas:define-route css ("css:cssfile") (hunchentoot:handle-static-file (format nil "~a/css/~a" base-dir cssfile))) @/code

— Что это за бред? — спросит искушенный веб-разработчик. — Это я же должен задавать для каждого сраного css-файла свой маршрут?

— Вовсе нет! — отвечу я. Для достижения максимального уровня гибкости можно задавать лямбду :requirement, которая решит, подходит ли маршрут или нет. Вот обновленный код, который отдает файл, если находит его на диске в каталоге сайта:

@code (restas:define-route static ("/:staticfile"

:requirement (lambda () (let ((request-file (pathname (format nil "~a/~a" base-dir (hunchentoot:request-uri hunchentoot:*request*)))) (files (directory (format nil "~a/*.*" base-dir)))) (not (null (find request-file files :test #'equal)))))) (hunchentoot:handle-static-file (format nil "~a/~a" base-dir staticfile))) @/code

Здесь мы просто определили маршруты для главной страницы и для отдачи css-файлов - как видите можно использовать :wildcards

=

О статьях на сайте

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

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

Теперь мне хотелось бы, чтобы тексты, которые я редактирую в org-mode на сайте выглядели так же хорошо: чтобы у них было оглавление, переходы по секциям, подсветка и форматирование кода, и даже метаинформация — чтобы все это можно было организовать в единый набор статей, разбитых по категориям. Кажется что это слишком много, чтобы запрограммировать за пару вечеров за компьютером? Отнюдь!

Заголовки

Как выглядит типичная статья, пока я ее пишу?

<source lang="text"> Небольшое описание того чем мы будем заниматься, буквально пара строчек

  • Заголовок первого уровня (свернутая секция)…

** Заголовок второго уровня А здесь текста развернут и я занимаюсь его написанием

  • И вновь заголовок первого…

</source>

Этого вполне достаточно чтобы с лету написать свой парсер. Чтобы он не был сложным, лучше рассматривать его шаг за шагом.

Разбираем заголовки

Отображение исходников

Разделители секций

Директивы препроцессора

Метаданные и их извлечение

Вставки кода

Директивы препроцессора

Применение макросов

Литература

Для тех, кто нашел в себе силы в вдохновение углубиться в тему, оставлю здесь несколько полезных ссылок.

<ol> <li>Подробную документацию на RESTAS можно посмотреть на на <a href="http://restas.lisper.ru/">http://restas.lisper.ru/</a></li> <li>Веб-сервер Hunchentoot: <a href="http://weitz.de/hunchentoot/">http://weitz.de/hunchentoot/</a></li> <li>[RUS] <a href="http://pcl.catap.ru/doku.php?id=pcl:loopforblackbelts">LOOP для мастеров с черным поясом</a></li> </ol>