Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
940 lines (785 sloc) 63.4 KB
\chapter{Практика: библиотека для генерации HTML -- интерпретатор}
\label{ch:30}
\thispagestyle{empty}
В~этой и следующей главах вы загляните под капот FOO~-- генератора HTML, который вы
использовали в нескольких предыдущих главах. FOO является примером подхода к
программированию, вполне обычного для Common Lisp, но сравнительно редкого для не
Lisp-языков, а именно~-- языкоориентированного программирования. Вместо того чтобы
определять API, базирующиеся преимущественно на функциях, классах и макросах, FOO
реализует обработчики для DSL\translationnote{Domain Specific Language~--
предметно-ориентированный язык программирования, мини-язык, созданный специально для
некоторых задач.}, которые вы можете встроить в ваши программы на Common Lisp.
FOO предоставляет два языковых обработчика для одного и того же языка
s-выражений. Первый~-- это интерпретатор, который получает программу на <<FOO>> в качестве
входных данных и интерпретирует её, формируя HTML. Второй~-- это компилятор, который
компилирует выражения FOO (возможно, со вставками на Common Lisp) в выражения Common Lisp,
которые генерируют HTML и запускают внедрённый код. Интерпретатор представлен функцией
\lstinline{emit-html}, а компилятор~-- макросом \lstinline{html}, который вы использовали в
предыдущих главах.
В~этой главе мы рассмотрим составные части инфраструктуры, разделяемые интерпретатором и
компилятором, а также реализацию интерпретатора. В~следующей главе я покажу вам, как
работает компилятор.
\section{Проектирование языка специального назначения}
Проектирование встраиваемого языка выполняется в два этапа: первый~-- это проектирование
языка, который позволит вам выразить желаемые вещи, а второй~-- реа\-ли\-за\-ция обработчика
или обработчиков, которые принимают <<программу>> на этом языке и либо выполняют действия,
указанные программой, либо переводят программу в код на Common Lisp, который выполнит
эквивалентные действия.
Итак, первым этапом является проектирование языка для формирования HTML. Ключом к
проектированию хорошего языка специального назначения является нахождение верного баланса
между выразительностью и краткостью. Например, очень выразительный, но не достаточно
краткий <<язык>> для формирования HTML~-- это язык литеральных строк HTML. Разрешёнными
<<формами>> этого языка являются строки, содержащие литералы HTML. Языковые процессоры
для этого <<языка>> могут обрабатывать такие формы путём их вывода без изменений.
\begin{myverb}
(defvar *html-output* *standard-output*)
(defun emit-html (html)
"Интерпретатор для языка HTML."
(write-sequence html *html-output*))
(defmacro html (html)
"Компилятор для языка HTML."
`(write-sequence ,html *html-output*))
\end{myverb}
Этот <<язык>> очень выразительный, поскольку он может сформировать любой HTML, который вы
захотите сгенерировать\pclfootnote{Фактически он, наверное, слишком выразителен, так как он
может также генерировать все виды выходных данных, а не только разрешённые
HTML. Конечно, это может быть полезным свойством, если вам нужно генерировать HTML, который не является
абсолютно корректным, для совместимости с лёгкими веб-браузерами. Кроме того, это
обычная практика для обработчиков языков~-- принимать программы, которые синтаксически
корректны, но, с другой стороны, понятно, что это вызовет неопределённое поведение при
выполнении.}. С другой стороны, этот язык не является настолько кратким, насколько
хотелось бы, потому что он даёт вам нулевую компрессию~-- его выход эквивалентен входу.
Для проектирования языка, дающего вам некоторое полезное сжатие без ощутимих жертв
выразительностью, вам необходимо определить детали вывода, которые являются лишними или не
представляют интереса. Вы можете сделать эти аспекты вывода неявными в семантике языка.
Например, согласно структуре HTML, каждый открывающий тег имеет соответствующую пару в
виде закрывающего тега\footnote{Хорошо, почти каждый тег. Определённые теги, такие как
\lstinline{IMG} и \lstinline{BR}, не имеют закрывающих тегов. Вы встретитесь с ними в
разделе <<Базовое правило вычисления>>.}\hspace{\footnotenegspace}. Когда вы формируете HTML вручную, то вам
необходимо писать эти закрывающие теги, но вы можете улучшить краткость вашего языка,
формирующего HTML, путём неявного включения закрывающего тега.
Другой способ, который поможет вам выиграть в краткости, не сильно влияя на
выразительность~-- это возложить на обработчики языка ответственность за добавление
необходимых разделителей между элементами~-- пустых строк и отступов. Когда вы генерируете
HTML программно, то вы обычно не сильно заботитесь о том, какие элементы должны
обрамляться переводами строк, или о том, должны ли элементы быть выровнены относительно
своих родительских элементов. Вам не придётся беспокоиться о разделителях, если дать
возможность обработчику языка самому вставлять их согласно некоторым правилам. Заметим
здесь, что FOO в действительности поддерживает два режима~-- один, использующий
минимальное количество разделителей, который позволяет генерировать очень эффективный код
и компактный HTML, и другой, генерирующий аккуратный форматированный HTML с различными
элементами, которые выровнены и отделены друг от друга согласно своим ролям.
Самая важная деталь, которую необходимо поместить в языковой обработчик,~-- это
экранирование определённых знаков, которые имеют специальное значение в HTML, таких как
\lstinline{<}, \lstinline{>}, \lstinline!&!. Очевидно, что если вы генерируете HTML, просто печатая
строки в поток, то вы отвечаете за замену всех вхождений этих знаков на соответствующую
экранирующую последовательность \lstinline!&lt;!, \lstinline!&gt!; и \lstinline!&amp;!. Но
если обработчик языка знает, какие строки будут формироваться как данные элемента, тогда
он может позаботиться об автоматическом экранировании этих знаков за вас.
\section{Язык FOO}
Итак, хватит теории. Я дам быстрый обзор языка, реализуемого FOO, и затем вы посмотрите на
реализацию двух его обработчиков~-- интерпретатора, который описан в этой главе, и
компилятора, который описан в следующей.
Подобно самому Lisp, базовый синтаксис языка FOO определён в терминах выражений, созданных
из Lisp объектов. Язык определяет то, как каждое выражение FOO переводится в HTML.
Самые простые выражения FOO~-- это самовычисляющиеся объекты Lisp, такие как строки,
числа и ключевые символы\pclfootnote{По строгому (strict) стандарту языка Common Lisp,
ключевые символы не являются самовычисляющимися, хотя фактически они делают
вычисление в самих себя. См.~раздел 3.1.2.1.3 стандарта языка или HyperSpec для
подробностей.}. Вам понадобится функция \lstinline{self-evaluating-p}, которая проверяет,
является ли данный объект самовычисляющимся:
\begin{myverb}
(defun self-evaluating-p (form)
(and (atom form) (if (symbolp form) (keywordp form) t)))
\end{myverb}
Объекты, которые удовлетворяют этому предикату, будут выведены путём формирования из них
строк с помощью \lstinline{PRINC-TO-STRING} и затем экранирования всех зарезервированных
знаков, таких как \lstinline{<}, \lstinline{>}, или \lstinline!&!. При формировании атрибутов знаки
\lstinline!"!, и \lstinline!'! также экранируются. Таким образом, вы можете применить
макрос \lstinline{html} к самовычисляющемуся объекту для вывода его в
\lstinline{*html-output*} (которая изначально связана с
\lstinline{*STANDARD-OUTPUT*}). Таб.~\ref{table:30-1} показывает, как несколько различных
самовычисляющихся значений будут выведены.
\begin{table}[h]
\centering{}
\begin{tabular}{|c|c|}
\hline
Форма FOO & Сгенерированный HTML \\
\hline
\lstinline!"foo"! &\lstinline!foo! \\
\lstinline!10! & \lstinline!10! \\
\lstinline!:foo! & \lstinline!FOO! \\
\lstinline!"foo & bar"! & \lstinline!foo &amp; bar!\\
\hline
\end{tabular}
\caption{Вывод FOO для самовычисляющихся объектов}
\label{table:30-1}
\end{table}
Конечно, большая часть HTML состоит из элементов в тегах. Каждый такой элемент имеет три
составляющие: тег, множество атрибутов и тело, содержащее текст и/или другие элементы
HTML. Поэтому вам нужен способ представлять эти три составляющие в виде объектов Lisp,
желательно таких, которые понимает процедура чтения Lisp\pclfootnote{Требование использовать
объекты, которые принимает процедура чтения Lisp, не является жёстким. Так как процедура
чтения Lisp сама по себе настраиваема, вы можете также определить новый синтаксис на
уровне процедуры чтения для нового вида объекта. Но такой подход принесёт больше
проблем, чем пользы.}. Если на время забыть об атрибутах, можно заметить, что существует
очевидное соответствие между списками Lisp и элементами HTML: каждый элемент HTML может
быть представлен как список, чей первый элемент (\lstinline{FIRST})~-- это символ, имя
которого~-- это название тега элемента, а остальные (\lstinline{REST})~-- это список
самовычисляющихся объектов или списков, представляющих другие элементы HTML. Тог\-да:
\begin{myverb}
<p>Foo</p> <==> (:p "Foo")
<p><i>Now</i> is the time</p> <==> (:p (:i "Now") " is the time")
\end{myverb}
Теперь остаётся придумать, как повысить краткость записи атрибутов. Так как у многих
элементов нет атрибутов, было бы здорово иметь возможность использовать для них упомянутый
выше синтаксис. FOO предоставят два способа нотации элементов с атрибутами. Первое, что
приходит в голову~-- это просто включать атрибуты в список сразу же за символом, чередуя
ключевые символы, именующие атрибуты, и объекты, представляющие значения атрибутов. Тело
элемента начинается с первого объекта в списке, который находится в позиции имени атрибута
и не является ключевым символом. Таким образом:
\begin{myverb}
HTML> (html (:p "foo"))
<p>foo</p>
NIL
HTML> (html (:p "foo " (:i "bar") " baz"))
<p>foo <i>bar</i> baz</p>
NIL
HTML> (html (:p :style "foo" "Foo"))
<p style='foo'>Foo</p>
NIL
HTML> (html (:p :id "x" :style "foo" "Foo"))
<p id='x' style='foo'>Foo</p>
NIL
\end{myverb}
Для тех, кто предпочитает более очевидное разграничение между телом элемента и его
атрибутами, FOO поддерживает альтернативный синтаксис: если первый элемент списка сам
является списком с ключевым словом в качестве первого элемента, тогда внешний список
представляет элемент HTML с этим ключевым словом в качестве тега, с остатком (\lstinline{REST})
вложенного списка в качестве атрибутов и с остатком (\lstinline{REST}) внешнего списка в
качестве тела. То есть вы можете написать два предыдущих выражения вот так:
\begin{myverb}
HTML> (html ((:p :style "foo") "Foo"))
<p style='foo'>Foo</p>
NIL
HTML> (html ((:p :id "x" :style "foo") "Foo"))
<p id='x' style='foo'>Foo</p>
NIL
\end{myverb}
Следующая функция проверяет, соответствует ли данный объект одному из этих синтаксисов:
\begin{myverb}
(defun cons-form-p (form &optional (test #'keywordp))
(and (consp form)
(or (funcall test (car form))
(and (consp (car form)) (funcall test (caar form))))))
\end{myverb}
Функцию \lstinline{test} следует сделать параметром, потому что позже вам потребуется проверять
те же самые два синтаксиса с немного отличающимся именем предиката.
Чтобы полностью абстрагироваться от различий между двумя вариантами синтаксиса, вы можете
определить функцию \lstinline{parse-cons-form}, которая принимает форму и разбивает её на три
элемента: тег, список свойств атрибутов и список тела, возвращая их как множественные
значения (multiple values). Код, который непосредственно вычисляет формы, будет
использовать эту функцию, и ему не придётся беспокоиться о том, какой синтаксис был
использован.
\begin{myverb}
(defun parse-cons-form (sexp)
(if (consp (first sexp))
(parse-explicit-attributes-sexp sexp)
(parse-implicit-attributes-sexp sexp)))
(defun parse-explicit-attributes-sexp (sexp)
(destructuring-bind ((tag &rest attributes) &body body) sexp
(values tag attributes body)))
(defun parse-implicit-attributes-sexp (sexp)
(loop with tag = (first sexp)
for rest on (rest sexp) by #'cddr
while (and (keywordp (first rest)) (second rest))
when (second rest)
collect (first rest) into attributes and
collect (second rest) into attributes
end
finally (return (values tag attributes rest))))
\end{myverb}
Теперь, когда у вас есть базовая спецификация языка, вы можете подумать о том, как вы
собираетесь реализовать обработчики языка. Как вы получите желаемый HTML из
последовательности выражений FOO? Как я упоминал ранее, вы реализуете два языковых
обработчика для FOO: интерпретатор, который проходит по дереву выражений FOO и формирует
соответствующий HTML непосредственно, и компилятор, который проходит по дереву выражений и
транслирует его в Common Lisp код, который будет формировать такой же HTML. И
интерпретатор, и компилятор будут построены поверх общего фундамента кода, предоставляющего
поддержку для таких вещей, как экранирование зарезервированных знаков и формирование
аккуратного, выровненного вывода, так что с этого мы и начнём.
\section{Экранирование знаков}
Базой, которую вам необходимо заложить, будет код, который знает, как экранировать знаки
специального назначения в HTML. Существует три таких знака, и они не должны появляться в
тексте элемента или в значении атрибута; вот они: \lstinline{<}, \lstinline{>} и
\lstinline!&!. В~тексте значения элемента или атрибута эти знаки должны быть заменены на
знаки ссылок на сущность (character reference entities) \lstinline!&lt;!, \lstinline!&gt;!
и \lstinline!&amp;!. Также в значениях атрибутов знаки кавычек, используемые для
разделения значения, должны быть экранированы: \lstinline!'! в \lstinline!&apos!; и
\lstinline!"! в \lstinline!&quot;!. Вдобавок любой знак может быть представлен в виде
числовой ссылки на символ, состоящей из амперсанда, за которым следует знак <<диез>>
(\lstinline!#!, он же sharp), за которым следуют числовой код в десятичной системе
счисления, за которым следует точка с запятой. Эти числовые экранирования иногда
используются для формирования не ASCII-знаков в HTML.
\vspace{0.8cm}
\begin{lrbox}{\chthreezeroone}
\begin{minipage}{\linewidth}
\begin{myverb}
(defpackage :com.gigamonkeys.html
(:use :common-lisp :com.gigamonkeys.macro-utilities)
(:export :with-html-output
:in-html-style
:define-html-macro
:html
:emit-html
:\&attributes))
\end{myverb}
\end{minipage}
\end{lrbox}
\textintable{Пакет}{Так как FOO~-- это низкоуровневая библиотека, пакет, в котором вы её
разрабатываете, не зависит от внешнего кода, за исключением стандартных имён из пакета
\lstinline{COMMON-LISP} и почти стандартных имён вспомогательных макросов из пакета
\lstinline{COM.GIGAMONKEYS.MACRO-UTILITIES}. С другой стороны, пакет нуждается в
экспорте всех имён, необходимых коду, который использует FOO. Вот \lstinline{DEFPACKAGE}
из исходных текстов, которые вы можете скачать с веб-сайта книги:\\[-3pt]
\noindent{}\usebox{\chthreezeroone}}
Следующая функция принимает один знак и возвращает строку, которая содержит
соответствующую данному знаку символьную сущность:
\begin{myverb}
(defun escape-char (char)
(case char
(#\bslash{}& "&amp;")
(#\bslash{}< "&lt;")
(#\bslash{}> "&gt;")
(#\bslash{}' "&apos;")
(#\bslash{}" "&quot;")
(t (format nil "&#~d;" (char-code char)))))
\end{myverb}
Вы можете использовать эту функцию как основу для функции \lstinline{escape}, которая принимает
строку и последовательность знаков и возвращает копию первого аргумента, в которой все
вхождения знаков из второго аргумента заменены соответствующими символьными сущностями,
возвращёнными функцией \lstinline{escape-char}.
\begin{myverb}
(defun escape (in to-escape)
(flet ((needs-escape-p (char) (find char to-escape)))
(with-output-to-string (out)
(loop for start = 0 then (1+ pos)
for pos = (position-if #'needs-escape-p in :start start)
do (write-sequence in out :start start :end pos)
when pos do (write-sequence (escape-char (char in pos)) out)
while pos))))
\end{myverb}
Вы также можете определить два параметра: \lstinline{*element-escapes*}, который содержит
знаки, которые вам нужно экранировать в данных элемента, и \lstinline{*attribute-escapes*},
который содержит множество знаков, которые необходимо экранировать в значениях атрибутов.
\begin{myverb}
(defparameter *element-escapes* "<>&")
(defparameter *attribute-escapes* "<>&\"'")
\end{myverb}
Вот несколько примеров:
\begin{myverb}
HTML> (escape "foo & bar" *element-escapes*)
"foo &amp; bar"
HTML> (escape "foo & 'bar'" *element-escapes*)
"foo &amp; 'bar'"
HTML> (escape "foo & 'bar'" *attribute-escapes*)
"foo &amp; &apos;bar&apos;"
\end{myverb}
Наконец, вам нужна переменная \lstinline{*escapes*}, которая будет связана со множеством знаков,
которые должны быть экранированы. Изначально она установлена в значение
\lstinline{*element-escapes*}, но, как вы увидите, при формировании атрибутов она будет
установлена в значение \lstinline{*attribute-escapes*}.
\begin{myverb}
(defvar *escapes* *element-escapes*)
\end{myverb}
\section{Вывод отступов}
Для формирования аккуратно выровненного вывода вы можете определить класс
\lstinline{indenting-printer}, который является обёрткой вокруг потока вывода, и функции,
которые используют экземпляр этого класса для вывода строк в поток и имеют возможность
отслеживать начала новых строк. Класс выглядит примерно так:
\begin{myverb}
(defclass indenting-printer ()
((out :accessor out :initarg :out)
(beginning-of-line-p :accessor beginning-of-line-p :initform t)
(indentation :accessor indentation :initform 0)
(indenting-p :accessor indenting-p :initform t)))
\end{myverb}
Главная функция, работающая с \lstinline{indenting-printer},~-- это \lstinline{emit}, которая принимает
принтер и строку и выводит строку в поток вывода принтера, отслеживая переходы на новую
строку, что позволяет ей управлять значением слота \lstinline{beginning-of-line-p}.
\begin{myverb}
(defun emit (ip string)
(loop for start = 0 then (1+ pos)
for pos = (position #\bslash{}Newline string :start start)
do (emit/no-newlines ip string :start start :end pos)
when pos do (emit-newline ip)
while pos))
\end{myverb}
Для непосредственного вывода строки она использует функцию \lstinline{emit/no-newlines},
которая формирует необходимое количество отступов посредством вспомогательной функции
\lstinline{indent-if-necessary} и затем записывает строку в поток. Эта функция может также
быть вызвана из любого другого кода для вывода строки, которая заведомо не содержит
переводов строк.
\begin{myverb}
(defun emit/no-newlines (ip string &key (start 0) end)
(indent-if-necessary ip)
(write-sequence string (out ip) :start start :end end)
(unless (zerop (- (or end (length string)) start))
(setf (beginning-of-line-p ip) nil)))
\end{myverb}
Вспомогательная функция \lstinline{indent-if-necessary} проверяет значения
\lstinline{beginning-of-line-p} и \lstinline{indenting-p}, чтобы определить, нужно ли
выводить отступ, и, если они оба имеют истинное значение, выводит столько пробелов,
сколько указывается значением \lstinline{indentation}. Код, использующий
\lstinline{indenting-printer}, может управлять выравниванием, изменяя значения слотов
\lstinline{indentation} и \lstinline{indenting-p}. Увеличивая или уменьшая значение
\lstinline{indentation}, можно изменять количество ведущих пробелов, в то время как
установка \lstinline{indenting-p} в \lstinline{NIL} может временно выключить выравнивание.
\begin{myverb}
(defun indent-if-necessary (ip)
(when (and (beginning-of-line-p ip) (indenting-p ip))
(loop repeat (indentation ip) do (write-char #\bslash{}Space (out ip)))
(setf (beginning-of-line-p ip) nil)))
\end{myverb}
Последние две функции в API \lstinline{indenting-printer}~-- это \lstinline{emit-newline} и
\lstinline{emit-freshline}, которые используются для вывода знака новой строки и похожи на
\lstinline!~%! и \lstinline!~&! директивы функции \lstinline{FORMAT}. Единственное различие в
том, что \lstinline{emit-newline} всегда выводит перевод строки, в то время как
\lstinline{emit-freshline} делает это только тогда, когда \lstinline{beginning-of-line-p}
установлено в ложное значение. Таким образом, множественные вызовы \lstinline{emit-freshline}
без промежуточных вызовов \lstinline{emit} не отразятся на количестве пустых строк. Это удобно,
когда один кусок кода хочет сгенерировать некоторый вывод, который должен заканчиваться
переводом строки, в то время как другой кусок кода хочет сгенерировать некоторый выход,
который должен начаться с перевода строки, но вы не хотите избыточных пустых линий между
двумя частями вывода.
\begin{myverb}
(defun emit-newline (ip)
(write-char #\bslash{}Newline (out ip))
(setf (beginning-of-line-p ip) t))
(defun emit-freshline (ip)
(unless (beginning-of-line-p ip) (emit-newline ip)))
\end{myverb}
Теперь вы готовы перейти к внутреннему устройству FOO процессора.
\section{Интерфейс HTML-процессора}
Теперь вы готовы к тому, чтобы определить интерфейс, с помощью которого вы будете
использовать процессор языка FOO для формирования HTML. Вы можете определить этот
интерфейс как множество обобщённых функций, потому что вам потребуются две реализации~--
одна, которая непосредственно формирует HTML, и другая, которую макрос \lstinline{html} может
использовать как список инструкций для выполнения, которые затем могут быть оптимизированы
и скомпилированы в код, формирующий такой же вывод более эффективно. Я буду называть это
множество обобщённых функций интерфейсом выходного буфера. Он состоит из следующих восьми
обобщённых функций:
\begin{myverb}
(defgeneric raw-string (processor string &optional newlines-p))
(defgeneric newline (processor))
(defgeneric freshline (processor))
(defgeneric indent (processor))
(defgeneric unindent (processor))
(defgeneric toggle-indenting (processor))
(defgeneric embed-value (processor value))
(defgeneric embed-code (processor code))
\end{myverb}
В~то время как некоторые из этих функций имеют очевидное соответствие функциям
\lstinline{indenting-printer}, очень важно понять, что эти обобщённые функции определяют
абстрактные операции, которые используются обработчиками языка FOO и не всегда будут
реализованы в терминах вызовов функций \lstinline{indenting-printer}.
Возможно, самый лёгкий способ понять семантику этих абстрактных операций~-- это взглянуть на
конкретные реализации специализированных методов в \lstinline{html-pretty-printer}, классе,
используемом для генерации удобочитаемого HTML.
\section{Реализация форматированного вывода}
Вы можете начать реализацию, определив класс с двумя слотами: одним для хранения
экземпляра~-- \lstinline{indenting-printer}~-- и одним для хранения размера табуляции~--
количества пробелов, на которое вы хотите увеличить отступ для каждого вложенного уровня
элементов HTML.
\begin{myverb}
(defclass html-pretty-printer ()
((printer :accessor printer :initarg :printer)
(tab-width :accessor tab-width :initarg :tab-width :initform 2)))
\end{myverb}
Теперь вы можете реализовать методы, специализированные для \lstinline{html-pretty-printer}, в
виде~8 обобщённых функций, которые составляют интерфейс выходного буфера.
Обработчики FOO используют функцию \lstinline{raw-string} для вывода строк, которые не
нуждаются в экранировании знаков, либо потому, что вы действительно хотите вы\-вес\-ти
зарезервированные знаки как есть, либо потому, что все зарезервированные знаки уже были
экранированы. Обычно \lstinline{raw-string} вызывается для строк, которые не содержат переводов
строки, таким образом поведение по умолчанию заключается в использовании
\lstinline{emit/no-newlines} до тех пор, пока клиент не передаст не \lstinline{NIL}-значение в
качестве аргумента \lstinline{newlines-p}.
\begin{myverb}
(defmethod raw-string ((pp html-pretty-printer) string &optional newlines-p)
(if newlines-p
(emit (printer pp) string)
(emit/no-newlines (printer pp) string)))
\end{myverb}
Функции \lstinline{newline}, \lstinline{freshline}, \lstinline{indent},
\lstinline{unindent} и \lstinline{toggle-indenting} реализуют достаточно простые операции
с~\lstinline{indenting-printer}. Единственная загвоздка заключается в том, что принтер
HTML формирует аккуратный вывод, только когда динамическая переменная \lstinline{*pretty*}
имеет истинное значение. Когда она равна \lstinline{NIL}, то формируется компактный HTML,
без лишних пробелов. Поэтому все эти методы, за исключением \lstinline{newline}, проверяют
значение переменной \lstinline{*pretty*}, перед тем как что-то сделать\footnote{С другой
стороны, применяя более чистый объектно-ориентированный подход, мы могли бы определить
два класса, скажем \lstinline{html-pretty-printer} и \lstinline{html-raw-printer}, а
затем определить на основе \lstinline{html-raw-printer} холостую реализацию для методов,
которые должны делать что-то, только если *pretty* истинно. Однако в таком случае после
определения всех холостых методов вы, в конце концов, получите большее количество кода,
и вскоре вам надоест проверять, создали ли вы экземпляр нужного класса в нужное
время. Но, в общем, замена условных выражений полиморфизмом~-- это оптимальная
стратегия.}\hspace{\footnotenegspace}:
\begin{myverb}
(defmethod newline ((pp html-pretty-printer))
(emit-newline (printer pp)))
(defmethod freshline ((pp html-pretty-printer))
(when *pretty* (emit-freshline (printer pp))))
(defmethod indent ((pp html-pretty-printer))
(when *pretty*
(incf (indentation (printer pp)) (tab-width pp))))
(defmethod unindent ((pp html-pretty-printer))
(when *pretty*
(decf (indentation (printer pp)) (tab-width pp))))
(defmethod toggle-indenting ((pp html-pretty-printer))
(when *pretty*
(with-slots (indenting-p) (printer pp)
(setf indenting-p (not indenting-p)))))
\end{myverb}
В~результате функции \lstinline{embed-value} и \lstinline{embed-code} используются только
компилятором FOO: \lstinline{embed-value} используется для генерации кода, который будет
формировать значение выражений Common Lisp, а \lstinline{embed-code} используется для
внедрения фрагментов выполняемого кода, и результат этой функции не используется.
В~интерпретаторе вы не можете полностью вычислять внедрённый Lisp код, поэтому вызов этих
функций всегда будет сигнализировать об ошибке.
\begin{myverb}
(defmethod embed-value ((pp html-pretty-printer) value)
(error "Can't embed values when interpreting. Value: ~s" value))
(defmethod embed-code ((pp html-pretty-printer) code)
(error "Can't embed code when interpreting. Code: ~s" code))
\end{myverb}
% я не смог сверстать это в виде таблицы с заголовком :-((((
\subsection[Использование условий. И невинность соблюсти, и капитал приобрести]{Использование условий. И невинность соблюсти, и капитал приобрести\protect{\translationnote{В~оригинале: <<To have your cake and eat it too>>~--
известная английская пословица, смысл которой в том, что нельзя одновременно делать
две взаимоисключающие вещи. Почти дословный русский аналог~-- Один пирог два раза не
съешь. Видимо, автор хотел подчеркнуть гибкость механизма условий Common
Lisp.}}}
\small
Альтернативным подходом является использование \lstinline{EVAL} для вычисления
выражений Lisp в интерпретаторе. Проблема, связанная с данным подходом, заключается в
том, что \lstinline{EVAL} не имеет доступа к лексическому окружению. Таким образом, не
существует способа выполнить что-то, подобное следующему:
\begin{myverb}
(let ((x 10)) (emit-html '(:p x)))
\end{myverb}
\noindent{}\lstinline!х!~-- это лексическая переменная. Символ \lstinline!х!, который передаётся
\lstinline!emit-html! во время выполнения, не связан с лексической переменной, названной
этим же символом. Компилятор Lisp создаёт ссылки на \lstinline!х! в коде для обращения к
переменной, но после того, как код скомпилирован, больше нет необходимости в связи между
именем \lstinline!х! и этой переменной. Это главная причина, по которой, когда вы думаете,
что \lstinline!EVAL!~-- это решение вашей проблемы, вы, вероятно, ошибаетесь.
Как бы то ни было, если бы \lstinline{х} был динамической переменной, объявленной с по\-мощью
\lstinline{DEFFVAR} или \lstinline{DEFPARAMETER} (и назван \lstinline{*х*} вместо \lstinline{х}), то
\lstinline{EVAL} могла бы получить доступ к её значению. То есть в некоторых ситуациях имеет
смысл позволить интерпретатору FOO использовать \lstinline{EVAL}. Но использовать \lstinline{EVAL}
всегда~-- это плохая идея. Вы можете взять лучшее из каждого подхода, комбинируя идеи
использования \lstinline{EVAL} и системы условий.
Сначала определим некоторые классы ошибок, которые вы можете просигнализировать,
когда \lstinline{embed-value} и \lstinline{embed-code} вызываются в интерпретаторе.
\begin{myverb}
(define-condition embedded-lisp-in-interpreter (error)
((form :initarg :form :reader form)))
(define-condition value-in-interpreter (embedded-lisp-in-interpreter) ()
(:report
(lambda (c s)
(format s "Can't embed values when interpreting. Value: ~s" (form c)))))
(define-condition code-in-interpreter (embedded-lisp-in-interpreter) ()
(:report
(lambda (c s)
(format s "Can't embed code when interpreting. Code: ~s" (form c)))))
\end{myverb}
Потом вы можете реализовать \lstinline{embed-value} и \lstinline{embed-code}, используя
сигнализирование этих ошибок и предоставление перезапуска, который вычислит форму с
помощью \lstinline{EVAL}.
\begin{myverb}
(defmethod embed-value ((pp html-pretty-printer) value)
(restart-case (error \'value-in-interpreter :form value)
(evaluate ()
:report (lambda (s) (format s "EVAL ~s in null lexical environment." value))
(raw-string pp (escape (princ-to-string (eval value)) *escapes*) t))))
(defmethod embed-code ((pp html-pretty-printer) code)
(restart-case (error \'code-in-interpreter :form code)
(evaluate ()
:report (lambda (s) (format s "EVAL ~s in null lexical environment." code))
(eval code))))
\end{myverb}
Теперь вы можете делать что-то, подобное этому:
\begin{myverb}
HTML> (defvar *x* 10)
*X*
HTML> (emit-html \'(:p *x*))
\end{myverb}
\noindent{}И вас выкинет в отладчик с таким сообщением:
\begin{myverb}
Can't embed values when interpreting. Value: *X*
[Condition of type VALUE-IN-INTERPRETER]
Restarts:
0: [EVALUATE] EVAL *X* in null lexical environment.
1: [ABORT] Abort handling SLIME request.
2: [ABORT] Abort entirely from this process.
\end{myverb}
Если вы вызовете перезапуск \lstinline{evaluate}, то \lstinline{embed-value} вызовет
\lstinline{EVAL *x*}, получит значение~\lstinline{10} и сгенерирует следующий HTML:
\begin{myverb}
<p>10</p>
\end{myverb}
Для удобства вы можете предоставить функции перезапуска~-- функции, которые вызывают
\lstinline{evaluate} перезапуск в определённых ситуациях. Функция перезапуска
\lstinline{evaluate}, безусловно, вызывает перезапуск, в то время как
\lstinline{eval-dynamic-variables} и \lstinline{eval-code} вызывают её, только если форма в
условии является динамической переменной или потенциальным кодом.
\begin{myverb}
(defun evaluate (\&{}optional condition)
(declare (ignore condition))
(invoke-restart 'evaluate))
(defun eval-dynamic-variables (\&{}optional condition)
(when (and (symbolp (form condition)) (boundp (form condition)))
(evaluate)))
(defun eval-code (\&{}optional condition)
(when (consp (form condition))
(evaluate)))
\end{myverb}
Теперь вы можете использовать \lstinline{HANDLER-BIND} для установки обработчика для
автоматического вызова \lstinline{evaluate} перезапуска для вас.
\begin{myverb}
HTML> (handler-bind ((value-in-interpreter \#{}'evaluate)) (emit-html '(:p *x*)))
<p>10</p>
T
\end{myverb}
И наконец, вы можете определить макрос, чтобы предоставить более приятный синтаксис для
связывания обработчиков для двух видов ошибок.
\begin{myverb}
(defmacro with-dynamic-evaluation ((\&{}key values code) \&{}body body)
`(handler-bind (
,@(if values `((value-in-interpreter \#{}'evaluate)))
,@(if code `((code-in-interpreter \#'evaluate))))
,@))
\end{myverb}
Этот макрос позволяет вам писать следующим образом:
\begin{myverb}
HTML> (with-dynamic-evaluation (:values t) (emit-html '(:p *x*)))
<p>10</p>
T
\end{myverb}
\normalsize
\section{Базовое правило вычисления}
Теперь, для того чтобы соединить язык FOO с интерфейсом обработчика, все, что вам
нужно,~-- это функция, которая принимает объект и обрабатывает его, вызывая подходящие
функции обработчика для генерации HTML. Например, когда дано простое выражение, наподобие
такого:
\begin{myverb}
(:p "Foo")
\end{myverb}
\noindent{}эта функция может выполнить эту последовательность вызовов обработчика:
\begin{myverb}
(freshline processor)
(raw-string processor "<p" nil)
(raw-string processor ">" nil)
(raw-string processor "Foo" nil)
(raw-string processor "</p>" nil)
(freshline processor)
\end{myverb}
Теперь вы можете определить простую функцию, которая просто проверяет, является ли данное
выражение разрешённым выражением FOO, и, если это так, передать её функции
\lstinline{process-sexp-html} для обработки. В~следующей главе вы добавите некоторые расширения
в эту функцию, чтобы позволить ей обрабатывать макросы и специальные операторы. Но для
текущих целей она выглядит так:
\begin{myverb}
(defun process (processor form)
(if (sexp-html-p form)
(process-sexp-html processor form)
(error "Malformed FOO form: ~s" form)))
\end{myverb}
Функция \lstinline{sexp-html-p} определяет, является ли данный объект разрешённым выражением
FOO, самовычисляющимся выражением или корректно сформатированной ячейкой.
\begin{myverb}
(defun sexp-html-p (form)
(or (self-evaluating-p form) (cons-form-p form)))
\end{myverb}
Самовычисляющиеся выражения обрабатываются просто: преобразуются в строку с помощью
\lstinline{PRINC-TO-STRING}, а затем экранируются знаки, указанные в переменной
\lstinline{*escapes*}, которая, как вы помните, изначально связана со значением
\lstinline{*element-escapes*}. Формы ячеек вы передаёте в \lstinline{process-cons-sexp-html}.
\begin{myverb}
(defun process-sexp-html (processor form)
(if (self-evaluating-p form)
(raw-string processor (escape (princ-to-string form) *escapes*) t)
(process-cons-sexp-html processor form)))
\end{myverb}
Функция \lstinline{process-cons-sexp-html} отвечает за вывод открывающего тега, всех атрибутов,
тела и закрывающего тега. Главная трудность здесь в том, что для генерирации аккуратного
HTML вам нужно выводить дополнительные линии и регулировать отступы согласно типу
выводимого элемента. Вы можете разделить все элементы, определённые в HTML, на три
категории: блок, параграф и встроенные (inline). Элементы блоки~-- такие как тело и \lstinline{ul}~--
выводятся с дополнительными линиями (переводами строк) перед и после открывающих и
закрывающих тегов и с содержимым, выровненным по одному уровню. Элементы параграфы~--
такие как \lstinline{p}, \lstinline{li} и \lstinline{blockquote}~-- выводятся с переводом строки перед
открывающим тегом и после закрывающего тега. Встроенные элементы просто выводятся в
линию. Три следующих параметра являются списками элементов каждого типа:
\begin{myverb}
(defparameter *block-elements*
'(:body :colgroup :dl :fieldset :form :head :html :map :noscript :object
:ol :optgroup :pre :script :select :style :table :tbody :tfoot :thead
:tr :ul))
(defparameter *paragraph-elements*
'(:area :base :blockquote :br :button :caption :col :dd :div :dt :h1
:h2 :h3 :h4 :h5 :h6 :hr :input :li :link :meta :option :p :param
:td :textarea :th :title))
(defparameter *inline-elements*
'(:a :abbr :acronym :address :b :bdo :big :cite :code :del :dfn :em
:i :img :ins :kbd :label :legend :q :samp :small :span :strong :sub
:sup :tt :var))
\end{myverb}
Функции \lstinline{block-element-p} и \lstinline{paragraph-element-p} проверяют, является ли данный
тег членом соответствующего списка\footnote{Вам не нужен предикат для
\lstinline{*inline-elements*}, так как вы проверяете всегда только для блока и параграфа
элементов. Я включил этот параметр здесь для завершённости.}\hspace{\footnotenegspace}.
\begin{myverb}
(defun block-element-p (tag) (find tag *block-elements*))
(defun paragraph-element-p (tag) (find tag *paragraph-elements*))
\end{myverb}
К двум другим категориям со своими собственными предикатами относятся элементы, которые
всегда пусты, такие как \lstinline{br} и \lstinline{hr} и три элемента \lstinline{pre}, \lstinline{style} и
\lstinline{script}, в которых положено сохранение разделителей. Формы обрабатываются особо при
формировании регулярного HTML (другими словами, не XHTML), так как в них не предполагаются
закрывающие теги. И при выводе трёх тегов, в которых пробелы сохраняются, вы можете
временно выключить выравнивание, и тогда \lstinline{pretty printer} не добавит каких-либо
разделителей, которые не являются частью действительного содержимого элементов.
\begin{myverb}
(defparameter *empty-elements*
'(:area :base :br :col :hr :img :input :link :meta :param))
(defparameter *preserve-whitespace-elements* '(:pre :script :style))
(defun empty-element-p (tag) (find tag *empty-elements*))
(defun preserve-whitespace-p (tag) (find tag *preserve-whitespace-elements*))
\end{myverb}
Последнее, что вам понадобится при генерации HTML~-- это параметр, указывающий, генерируете
ли вы XHTML, так как это влияет на то, как вам нужно выводить пустые элементы.
\begin{myverb}
(defparameter *xhtml* nil)
\end{myverb}
Со всей этой информацией вы готовы к обработке форм FOO. Вы используете
\lstinline{parse-cons-form}, чтобы разбить список на три части, символ тега, возможно пустой,
список свойств пар ключ/значение атрибутов и, возможно пустой, список форм тела. Затем вы
формируете открывающий тег, тело и закрывающий тег с помощью вспомогательных функций
\lstinline{emit-open-tag}, \lstinline{emit-element-body} и \lstinline{emit-close-tag}.
\begin{myverb}
(defun process-cons-sexp-html (processor form)
(when (string= *escapes* *attribute-escapes*)
(error "Can't use cons forms in attributes: ~a" form))
(multiple-value-bind (tag attributes body) (parse-cons-form form)
(emit-open-tag processor tag body attributes)
(emit-element-body processor tag body)
(emit-close-tag processor tag body)))
\end{myverb}
В~\lstinline{emit-open-tag} вам нужно вызвать \lstinline{freshline}, когда это необходимо, и затем
вывести атрибуты с помощью \lstinline{emit-attributes}. Вам нужно передать тело элемента в
функцию \lstinline{emit-open-tag}, тогда в случае формирования XHTML она определит, закончить
тег с \lstinline{/>} или \lstinline{>}.
\begin{myverb}
(defun emit-open-tag (processor tag body-p attributes)
(when (or (paragraph-element-p tag) (block-element-p tag))
(freshline processor))
(raw-string processor (format nil "<~(~a~)" tag))
(emit-attributes processor attributes)
(raw-string processor (if (and *xhtml* (not body-p)) "/>" ">")))
\end{myverb}
В~\lstinline{emit-attributes} имена атрибутов не вычисляются, так как они являются
ключевыми символами, но вам следует вызывать функцию \lstinline{process} верхнего уровня
для вычисления значений атрибутов, связывая \lstinline{*escapes*} с
\lstinline{*attribute-escapes*}. Для удобства при спецификации булевых атрибутов, чьи
значения должны быть именем атрибута, если это значение равно \lstinline{Т} (не любое
истинное значение, а именно \lstinline{Т}), вы заменяете значение именем
атрибута\footnote{В~то время как в нотации XHTML требуется, чтобы в логических атрибутах
имя совпадало со значением для указания значения true, в HTML также разрешено просто
включить имя атрибута без значения, например \lstinline{<option selected>}, так же как и
\lstinline{<option selected='selected'>}. Все HTML-4.0 совместимые браузеры должны
понимать обе формы, но некоторые лёгкие браузеры понимают только форму без значения для
определённых атрибутов. Если вам нужно генерировать HTML для таких браузеров, вам
потребуется исправить \lstinline{emit-attributes}, чтобы формировать эти атрибуты
немного по-другому.}\hspace{\footnotenegspace}.
\begin{myverb}
(defun emit-attributes (processor attributes)
(loop for (k v) on attributes by #'cddr do
(raw-string processor (format nil " ~(~a~)='" k))
(let ((*escapes* *attribute-escapes*))
(process processor (if (eql v t) (string-downcase k) v)))
(raw-string processor "'")))
\end{myverb}
Формирование тела элемента похоже на формирование значений атрибута: вы можете циклически
проходить по телу, вызывая \lstinline{process} для вычисления каждого выражения. Основа кода
заключена в выводе переводов строк и регулирования отступов подходящим образом в
соответствии с типом элемента.
\begin{myverb}
(defun emit-element-body (processor tag body)
(when (block-element-p tag)
(freshline processor)
(indent processor))
(when (preserve-whitespace-p tag) (toggle-indenting processor))
(dolist (item body) (process processor item))
(when (preserve-whitespace-p tag) (toggle-indenting processor))
(when (block-element-p tag)
(unindent processor)
(freshline processor)))
\end{myverb}
Наконец, \lstinline{emit-close-tag}, как вы, вероятно ожидаете, выводит закрывающий тег (до тех
пор, пока в нем нет необходимости, например когда тело пустое и вы либо формируете XHTML,
либо элемент является одним из специальных пустых элементов). Независимо от того, выводите
ли вы закрывающий тег, вам нужно вывести завершающий перевод строки для элементов блока и
параграфа.
\begin{myverb}
(defun emit-close-tag (processor tag body-p)
(unless (and (or *xhtml* (empty-element-p tag)) (not body-p))
(raw-string processor (format nil "</~(~a~)>" tag)))
(when (or (paragraph-element-p tag) (block-element-p tag))
(freshline processor)))
\end{myverb}
Функция \lstinline{process}~-- это основа интерпретатора FOO. Чтобы сделать её немного проще в
использовании, вы можете определить функцию \lstinline{emit-html}, которая вызывает
\lstinline{process}, передавая ей \lstinline{html-pretty-printer} и форму для вычисления. Вы можете
определить и использовать вспомогательную функцию \lstinline{get-pretty-printer} для получения
\lstinline{pretty printer}, которая возвращает текущее значение \lstinline{*html-pretty-printer*},
если оно связано; в ином случае она создаёт новый экземпляр \lstinline{html-pretty-printer} с
\lstinline{*html-output*} в качестве выходного потока.
\begin{myverb}
(defun emit-html (sexp) (process (get-pretty-printer) sexp))
(defun get-pretty-printer ()
(or *html-pretty-printer*
(make-instance
'html-pretty-printer
:printer (make-instance 'indenting-printer :out *html-output*))))
\end{myverb}
С этой функцией вы можете выводить HTML в \lstinline{*html-output*}. Вместо того чтобы
предоставлять переменную \lstinline{*html-output*} как часть открытого API FOO, вам следует
определить макрос \lstinline{with-html-output}, который берёт на себя заботу о связывании
потока для вас. Он также позволяет вам определить, хотите ли вы использовать аккуратный
вывод HTML, выставляя по умолчанию значение переменной \lstinline{*pretty*}.
\begin{myverb}
(defmacro with-html-output ((stream &key (pretty *pretty*)) &body body)
`(let* ((*html-output* ,stream)
(*pretty* ,pretty))
,@body))
\end{myverb}
Итак, если вы хотите использовать \lstinline{emit-html} для вывода HTML в файл, вы можете
написать следующее:
\begin{myverb}
(with-open-file (out "foo.html" :direction output)
(with-html-output (out :pretty t)
(emit-html *some-foo-expression*)))
\end{myverb}
\section{Что дальше?}
В~следующей главе вы увидите, как реализовать макрос, который компилирует выражения FOO в
Common Lisp, что позволит вам внедрить код генерации HTML прямо в ваши программы на Lisp. Вы
также расширите язык FOO, чтобы сделать его немного более выразительным, путём добавления
специальных операторов и макросов.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: