Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

30. Практика: Библиотека для генерации HTML. Интерпретатор.

В этой и следующей главе вы загляните под капот FOO – генератора HTML, который вы использовали в нескольких предыдущих главах. FOO является примером подхода к программированию, вполне обычного для Common Lisp, но сравнительно редкого для не-Lisp языков, а именно – языкоориентированного программирования. Вместо того чтобы определять API, базирующиеся преимущественно на функциях, классах и макросах, FOO реализует обработчики для DSL 1), которые вы можете встроить в ваши программы на Common Lisp.

FOO предоставляет два языковых обработчика для одного и того же языка s-выражений. Первый – это интерпретатор, который получает программу на "FOO" в качестве входных данных и интерпретирует ее, формируя HTML. Второй – это компилятор, который компилирует выражения FOO (возможно со вставками на Common Lisp) в выражения Common Lisp, которые генерируют HTML и запускает внедренный код. Интерпретатор представлен функцией emit-html, а компилятор – макросом html, который вы использовали в предыдущих главах.

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

Проектирование языка специального назначения

Проектирование встраиваемого языка выполняется в два этапа: первый – это проектирование языка, который позволит вам выразить желаемые вещи, а второй – реализация обработчика, или обработчиков, которые принимают "программу" на этом языке и либо выполняют действия, указанные программой, либо переводят программу в код на Common Lisp, который выполнит эквивалентные действия.

Итак, первым этапом является проектирование языка для формирования HTML. Ключом к проектированию хорошего языка специального назначения является нахождение верного баланса между выразительностью и краткостью. Например, очень выразительный, но не достаточно краткий "язык" для формирования HTML – это язык литеральных строк HTML. Разрешенными "формами" этого языка являются строки, содержащие литералы HTML. Языковые процессоры для этого "языка" могут обрабатывать такие формы путем их вывода без изменений.

(defvar *html-output* *standard-output*)

(defun emit-html (html)
  "Интерпретатор для языка HTML."
  (write-sequence html *html-output*)
)


(defmacro html (html)
  "Компилятор для языка HTML."
  `(write-sequence ,html *html-output*)
)

Этот "язык" очень выразительный, поскольку он может сформировать любой HTML, который вы захотите сгенерировать.2) С другой стороны, этот язык не является настолько кратким, насколько хотелось бы, потому что он дает вам нулевую компрессию – его выход FIXME (совпадает|равняется|еквивалентен) входу.

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

Например, согласно структуре HTML, каждый открывающий тэг имеет соответствующую пару в виде закрывающего тэга.3) Когда вы формируете HTML вручную, то вам необходимо писать эти закрывающие тэги но вы можете улучшить краткость вашего языка формирующего HTML путем неявного включения закрывающего тэга.

Другой способ, который поможет вам выиграть в краткости, не сильно влияя на выразительность, это возложить на обработчики языка ответственность за добавление необходимых разделителей между элементами - пустых строк и отступов. Когда вы генерируете HTML программно, то вы обычно не сильно заботитесь о том, какие элементы должны обрамляться переводами строк или о том, должны ли элементы быть выровнены относительно своих родительских элементов. Вам не придется беспокоиться о разделителях, если дать возможность обработчику языка самому вставлять их согласно некоторым правилам. Заметим здесь, что FOO в действительности поддерживает два режима – один, использующий минимальное количество разделителей, который позволяет генерировать очень эффективный код и компактный HTML, и другой, генерирующий аккуратный форматированный HTML с различными элементами, которые выровнены и отделены друг от друга согласно своих ролей.

Самая важная деталь, которую необходимо поместить в языковой обработчик – это экранирование определенных знаков, которые имеют специальное значение в HTML, таких как <, >, &. Очевидно, что если вы генерируете HTML просто печатая строки в поток, то вы отвечаете за замену всех вхождений этих знаков на соответствующую экранирующую последовательность &lt;, &gt; и &amp;. Но если обработчик языка знает, какие строки будут формироваться как данные элемента, тогда он может позаботиться об автоматическом экранировании этих знаков за вас.

Язык FOO

Итак, хватит теории. Я дам быстрый обзор языка, реализуемого FOO и затем вы посмотрите на реализацию двух его обработчиков – интерпретатора, который описан в этой главе и компилятора, который описан в следующей.

Подобно самому Lisp, базовый синтаксис языка FOO определен в терминах выражений, созданных из Lisp объектов. Язык определяет то, как каждое выражение FOO переводится в HTML.

Самые простые выражения FOO – это FIXME само-вычисляющиеся Lisp объекты, такие как строки, числа и ключевые символы.4) Вам понадобится функция self-evaluating-p, которая проверяет является ли данный объект FIXME само-вычисляющимся для целей FOO.

(defun self-evaluating-p (form)
  (and (atom form) (if (symbolp form) (keywordp form) t))
)

Объекты, которые удовлетворяют этому предикату будут выведены путем формирования из них строк с помощью PRINC-TO-STRING и затем экранирования всех зарезервированных знаков, таких как <, >, или &. При формировании атрибутов знаки ", и ' также экранируются. Таким образом, вы можете применить макрос html к FIXME само-вычисляющемуся объекту для вывода его в *html-output* (которая изначально связанная с *STANDARD-OUTPUT*). Таблица 30-1 показывает как несколько различных само-вычисляющихся значений будут выведены.

Таблица 30-1. Выход FOO для FIXME само-вычисляющихся объектов

FOO Form	Generated HTML
"foo" foo
10 10
:foo FOO
"foo & bar" foo &amp; bar

Конечно, большая часть HTML состоит из элементов в тэгах. Каждый такой элемент имеет три составляющие: тэг, множество атрибутов, и тело, содержащее текст и/или другие HTML элементы. Поэтому вам нужен способ представлять эти три составляющие в виде Lisp объектов, желательно таких, которые понимает считываетель Lisp.5) Если на время забыть об атрибутах, можно заметить, что существует очевидное соответствие между списками Lisp и элементами HTML: каждый HTML элемент может быть представлен как список, чей первый элемент (FIRST) – это символ, имя которого это название тэга элемента, а остальные (REST) – это список FIXME само-вычисляющихся объектов или списков, представляющих другие HTML элементы. Тогда:

<p>Foo</p> <==> (:p "Foo")
<p><i>Now</i> is the time</p> <==> (:p (:i "Now") " is the time")

Теперь остается придумать, как повысить краткость записи атрибутов. Так как у многих элементов нет атрибутов, было бы здорово иметь возможность использовать для них упомянутый выше синтаксис. FOO предоставят два способа нотации элементов с атрибутами. Первое, что приходит в голову, это просто включать атрибуты в список сразу же за символом, чередуя ключевые символы, именующие атрибуты, и объекты, представляющие значения атрибутов. Тело элемента начинается с первого объекта в списке, который находится в позиции имени атрибута и не является ключевым символом. Таким образом:

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

Для тех, кто предпочитает более очевидное разграничение между телом элемента и его атрибутами, FOO поддерживает альтернативный синтаксис: если первый элемент списка сам является списком с ключевым словом в качестве первого элемента, тогда внешний список представляет элемент HTML с этим ключевым словом в качестве тэга, с остатком (REST) вложенного списка в качестве атрибутов и с остатком (REST) внешнего списка в качестве тела. То есть вы можете написать два предыдущих выражения вот так:

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

Следующая функция проверяет, соответствует ли данный объект одному из этих синтаксисов:

(defun cons-form-p (form &optional (test #'keywordp))
  (and (consp form)
       (or (funcall test (car form))
           (and (consp (car form)) (funcall test (caar form)))
)
)
)

Функцию test следует сделать параметром, потому что позже вам потребуется проверять те же самые два синтаксиса с немного отличающимся именем предиката.

Чтобы полностью абстрагироваться от различий между двумя вариантами синтаксиса, вы можете определить функцию parse-cons-form, которая принимает форму и разбивает ее на три элемента: тэг, список свойств атрибутов и список тела, возвращая их как множественные значения (multiple values). Код, который непосредственно вычисляет формы, будет использовать эту функцию, и ему не придется беспокоиться о том, какой синтаксис был использован.

(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))
)
)

Теперь, когда у вас есть базовая спецификация языка, вы можете подумать о том, как вы собираетесь реализовать обработчики языка. Как вы получите желаемый HTML из последовательности выражений FOO? Как я упоминал ранее, вы реализуете два языковых обработчика для FOO: интерпретатор, который проходит по дереву выражений FOO и формирует соответствующий HTML непосредственно, и компилятор, который проходит по дереву выражений и транслирует его в Common Lisp код, который будет формировать такой же HTML. И интерпретатор и компилятор будут построены поверх общего фундамента кода, предоставляющего поддержку для таких вещей, как экранирование зарезервированных знаков и формирование аккуратного, выровненного вывода, так что с этого мы и начнем.

Экранирование знаков

Базой, которую вам необходимо заложить, будет код, который знает, как экранировать знаки специального назначения в HTML. Существует три таких знака, и они не должны появляться в тексте элемента или в значении атрибута; вот они: <, > и &. В тексте значения элемента или атрибута эти знаки должны быть заменены на знаки ссылок на сущность (character reference entities) &lt;, &gt; и &amp;. Также, в значениях атрибутов знаки кавычек, используемые для разделения значения, должны быть экранированы, ' в &apos; и " в &quot;. Вдобавок, любой знак может быть представлен в виде числовой ссылки на символ, состоящей из амперсанда, за которым следует знак "диез" (#, он же sharp), за которым следует числовой код в десятичной системе счисления, за которым следует точка с запятой. Эти числовые экранирования иногда используются для формирования не-ASCII знаков в HTML.

FIXME это таблица в тексте

Пакет

Так как FOO это низкоуровневая библиотека, пакет, в котором вы ее разрабатываете, не зависит от внешнего кода, за исключением стандартных имен из пакета COMMON-LISP и, почти стандартных, имен вспомогательных макросов из пакета COM.GIGAMONKEYS.MACRO-UTILITIES. С другой стороны, пакет нуждается в экспорте всех имен, необходимых коду, который использует FOO. Вот DEFPACKAGE из исходных текстов, которые вы можете скачать с Web-сайта книги:

(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
)
)

FIXME конец таблицы

Следующая функция принимает один знак и возвращает строку, которая содержит соответствующую данному знаку символьную сущность:

(defun escape-char (char)
  (case char
    (#\& "&amp;")
    (#\< "&lt;")
    (#\> "&gt;")
    (#\' "&apos;")
    (#\" "&quot;")
    (t (format nil "&#~d;" (char-code char)))
)
)

Вы можете использовать эту функцию как основу для функции escape, которая принимает строку и последовательность знаков и возвращает копию первого аргумента, в которой все вхождения знаков из второго аргумента, заменены соответствующими символьными сущностями, возвращенными функцией escape-char.

(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
)
)
)
)

Вы также можете определить два параметра: *element-escapes*, который содержит знаки, которые вам нужно экранировать в данных элемента, и *attribute-escapes*, который содержит множество знаков, которые необходимо экранировать в значениях атрибутов.

(defparameter *element-escapes* "<>&")
(defparameter *attribute-escapes* "<>&\"'")

Вот несколько примеров:

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;"

Наконец, вам нужна переменная *escapes*, которая будет связана с множеством знаков, которые должны быть экранированы. Изначально она установлена в значение *element-escapes*, но, как вы увидите, при формировании атрибутов, она будет установлена в значение *attribute-escapes*.

(defvar *escapes* *element-escapes*)

Вывод отступов

Для формирования аккуратно выровненного вывода, вы можете определить класс indenting-printer, который является оберткой вокруг потока вывода, и функции, которые используют экземпляр этого класса для вывода строк в поток и имеют возможность отслеживать начала новых строк. Класс выглядит примерно вот так:

(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)
)
)

Главная функция, работающая с indenting-printer это emit, которая принимает принтер и строку и выводит строку в поток вывода принтера, отслеживая переходы на новую строку, что позволяет ей управлять значением слота beginning-of-line-p.

(defun emit (ip string)
  (loop for start = 0 then (1+ pos)
     for pos = (position #\Newline string :start start)
     do (emit/no-newlines ip string :start start :end pos)
     when pos do (emit-newline ip)
     while pos
)
)

Для непосредственного вывода строки она использует функцию emit/no-newlines, которая формирует необходимое количество отступов посредством вспомогательной функции indent-if-necessary и затем записывает строку в поток. Эта функция может также быть вызвана из любого другого кода для вывода строки, которая заведомо не содержит переводов строк.

(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)
)
)

Вспомогательная функция indent-if-necessary проверяет значения beginning-of-line-p и indenting-p, чтобы определить, нужно ли выводить отступ, и если они оба имеют истинное значение, выводит столько пробелов, сколько указывается значением indentation. Код, использующий indenting-printer, может управлять выравниванием, изменяя значения слотов indentation и indenting-p. Увеличивая или уменьшая значение indentation, можно изменять количество ведущих пробелов, в то время как установка indenting-p в NIL может временно выключить выравнивание.

(defun indent-if-necessary (ip)
  (when (and (beginning-of-line-p ip) (indenting-p ip))
    (loop repeat (indentation ip) do (write-char #\Space (out ip)))
    (setf (beginning-of-line-p ip) nil)
)
)

Последние две функции в API indenting-printer это emit-newline и emit-freshline, которые используются для вывода знака новой строки и похожи на ~% и ~& директивы функции FORMAT. Единственное различие в том, что emit-newline всегда выводит перевод строки, в то время как emit-freshline делает это только тогда, когда beginning-of-line-p установлено в ложное значение. Таким образом, множественные вызовы emit-freshline без промежуточных вызовов emit не отразятся на количестве пустых линии. Это удобно, когда один кусок кода хочет сгенерировать некоторый вывод, который должен заканчиваться переводом строки, в то время как другой кусок кода хочет сгенерировать некоторый выход, который должен начаться с перевода строки, но вы не хотите избыточных пустых линий между двумя частями вывода.

(defun emit-newline (ip)
  (write-char #\Newline (out ip))
  (setf (beginning-of-line-p ip) t)
)


(defun emit-freshline (ip)
  (unless (beginning-of-line-p ip) (emit-newline ip))
)

Теперь вы готовы перейти к внутреннему устройству FOO процессора.

Интерфейс HTML процессора

Теперь вы готовы к тому, чтобы определить интерфейс, с помощью которого вы будете использовать процессор языка FOO для формирования HTML. Вы можете определить этот интерфейс как множество обобщенных функций, потому что вам потребуются две реализации –- одна, которая непосредственно формирует HTML, и другая, которую макрос html может использовать как список инструкций для выполнения, которые затем могут быть оптимизированы и скомпилированы в код, формирующий такой же вывод более эффективно. Я буду называть это множество обобщенных функций интерфейсом выходного буфера. Он состоит из следующих восьми обобщенных функций:

(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))

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

Возможно, самый легкий способ понять семантику этих абстрактных операций, это взглянуть на конкретные реализации специализированных методов в html-pretty-printer, классе, используемом для генерации удобочитаемого HTML.

FIXME backend Внутренняя реализация форматированного вывода

Вы можете начать реализацию, определив класс с двумя слотами, – одним для хранения экземпляра indenting-printer и одним – для хранения размера табуляции – количества пробелов, на которое вы хотите увеличить отступ для каждого вложенного уровня HTML элементов.

(defclass html-pretty-printer ()
  ((printer   :accessor printer   :initarg :printer)
   (tab-width :accessor tab-width :initarg :tab-width :initform 2)
)
)

Теперь вы можете реализовать методы, специализированные для html-pretty-printer, в виде 8 обобщенных функций, которые составляют интерфейс выходного буфера.

Обработчики FOO используют функцию raw-string для вывода строк, которые не нуждаются в экранировании знаков, либо потому, что вы действительно хотите вывести зарезервированные знаки как есть, либо потому, что все зарезервированные знаки уже были экранированы. Обычно raw-string вызывается для строк, которые не содержат переводов строки, таким образом поведение по умолчанию заключается в использовании emit/no-newlines до тех пор, пока клиент не передаст не-NIL значение в качестве аргумента newlines-p.

(defmethod raw-string ((pp html-pretty-printer) string &optional newlines-p)
  (if newlines-p
    (emit (printer pp) string)
    (emit/no-newlines (printer pp) string)
)
)

Функции newline, freshline, indent, unindent и toggle-indenting реализуют достаточно простые манипуляции нижележащего indenting-printer. Единственная загвоздка заключается в том, что принтер HTML формирует аккуратный вывод только когда динамическая переменная *pretty* имеет истинное значение. Когда она равна NIL, то формируется компактный HTML, без лишних пробелов. Поэтому все эти методы, за исключением newline, проверяют значение переменной *pretty* перед тем, как что-то сделать:6)

(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))
)
)
)

В результате, функции embed-value и embed-code используются только компилятором FOO: embed-value используется для генерации кода, который будет формировать значение выражений Common Lisp, а embed-code используется для внедрения фрагментов кода для запуска и ее результат исключается FIXME. В интерпретаторе вы не можете полностью вычислять внедренный Lisp код, поэтому вызов этих функций всегда будет сигнализировать об ошибке.

(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)
)

FIXME окно в тексте

Использование Условий. И невинность соблюсти, и капитал приобрести.

В оригинале – To have your cake and eat it too – известная английская пословица, смысл который в том, что нельзя одновременно делать две взаимоисключающие вещи. Почти дословный русский аналог – Один пирог два раза не съешь. Видимо автор хотел подчеркнуть гибкость механизма условий Common Lisp – прим. перев.

Альтернативным подходом является использование EVAL для вычисления Lisp выражений в интерпретаторе. Проблема, связанная с данным подходом заключается в том, что EVAL не имеет доступа к лексическому окружению. Таким образом, не существует способа выполнить что-то, подобное следующему:

(let ((x 10)) (emit-html '(:p x)))

если х это лексическая переменная. Символ х, который передается emit-html во время выполнения, не связан с лексической переменной, названной этим же символом. Компилятор Lisp создает ссылки на х в коде для обращения к переменной, но после того, как код скомпилирован, больше нет необходимости в связи между именем х и этой переменной. Это главная причина, по которой когда вы думаете, что EVAL – это решение вашей проблемы, вы вероятно ошибаетесь.

Как бы то ни было, если бы х был динамической переменной, объявленной с помощью DEFFVAR или DEFPARAMETER (и назван *х* вместо х), то EVAL могла бы получить доступ к ее значению. То есть, в некоторых ситуациях имеет смысл позволить интерпретатору FOO использовать EVAL. Но использовать EVAL всегда – это плохая идея. Вы можете взять лучшее из каждого подхода, комбинируя идеи использования EVAL и системы условий.

Сначала определим некоторые классы ошибок, которые вы можете просигнализировать, когдаembed-value и embed-code вызываются в интерпретаторе.

(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))
)
)
)

Потом вы можете реализовать embed-value и embed-code, используя сигнализирование этих ошибок и предоставление перезапуска, который вычислит форму с помощью EVAL.

(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)
)
)
)

Теперь вы можете делать что-то подобное этому:

HTML> (defvar *x* 10)
*X*
HTML> (emit-html '(:p *x*))

и вас выкинет в отладчик с таким сообщением:

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.

Если вы вызовите перезапуск evaluate, то embed-value вызовет EVAL *x*, получит значение 10 и сгенерирует следующий HTML:

<p>10</p>

Для удобства, вы можете предоставить функции перезапуска – функции, которые вызывают evaluate перезапуск в определенных ситуациях. Функция evaluate перезапуска безусловно вызывает перезапуск, в то время как eval-dynamic-variables и eval-code вызывают ее только если форма в условии является динамической переменной или потенциальный код.

(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)
)
)

Теперь вы можете использовать HANDLER-BIND для установки обработчика для автоматического вызова evaluate перезапуска для вас.

HTML> (handler-bind ((value-in-interpreter #'evaluate)) (emit-html '(:p *x*)))
<p>10</p>
T

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

(defmacro with-dynamic-evaluation ((&key values code) &body body)
  `(handler-bind (
       ,@(if values `((value-in-interpreter #'evaluate)))
       ,@(if code `((code-in-interpreter #'evaluate)))
)

     ,@body
)
)

Этот макрос позволяет вам писать следующим образом:

HTML> (with-dynamic-evaluation (:values t) (emit-html '(:p *x*)))
<p>10</p>
T

FIXME конец таблицы в тексте

Базовое правило вычисления

Теперь для того, чтобы соединить язык FOO с интерфейсом обработчика, все, что вам нужно, это функция, которая принимает объект и обрабатывает его, вызывая подходящие функции обработчика для генерации HTML. Например, когда дано простое выражение, наподобие такого:

(:p "Foo")

эта функция может выполнить эту последовательность вызовов обработчика:

(freshline processor)
(raw-string processor "<p" nil)
(raw-string processor ">" nil)
(raw-string processor "Foo" nil)
(raw-string processor "</p>" nil)
(freshline processor)

Теперь вы можете определить простую функцию, которая просто проверяет, является данное выражение разрешенным выражением FOO, и если это так, передать ее функции process-sexp-html для обработки. В следующей главе вы добавите некоторые расширения в эту функцию, чтобы позволить ей обрабатывать макросы и специальные операторы. Но для текущих целей она выглядит так:

(defun process (processor form)
  (if (sexp-html-p form)
    (process-sexp-html processor form)
    (error "Malformed FOO form: ~s" form)
)
)

Функция sexp-html-p определяет, является ли данный объект разрешенным выражением FOO, само-вычисляющимся выражением или корректно сформатированной ячейкой.

(defun sexp-html-p (form)
  (or (self-evaluating-p form) (cons-form-p form))
)

Само-вычисляющиеся выражения обрабатываются просто: преобразуются в строку с помощью PRINC-TO-STRING, а затем экранируются знаки, указанные в переменной *escapes*, которая, как вы помните, изначально связана со значением *element-escapes*. Формы ячеек вы передаете в process-cons-sexp-html.

(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)
)
)

Функция process-cons-sexp-html отвечает за вывод открывающего тэга, всех атрибутов, тела и закрывающего тэга. Главная трудность здесь в том, что для генерирования аккуратного HTML, вам нужно выводить дополнительные линии и регулировать отступы согласно типу выводимого элемента. Вы можете разделить все элементы, определенные в HTML, на три категории: блок, параграф, и встроенные. Элементы блоки – такие как тело и ul – выводятся с дополнительными линиями (переводами строк) перед и после открывающих и закрывающих тэгов, и с содержимым, выровненным по одному уровню. Элементы параграфы – такие как p, li и blockquote – выводятся с переводом строки перед открывающим тэгом и после закрывающего тэга. Встроенные элементы просто выводятся в линию. Три следующих параметра являются списками элементов каждого типа:

(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
)
)

Функции block-element-p и paragraph-element-p проверяют, является ли данный тэг членом соответствующего списка.7)

(defun block-element-p (tag) (find tag *block-elements*))

(defun paragraph-element-p (tag) (find tag *paragraph-elements*))

К двум другим категориям со своими собственными предикатами относятся элементы, которые всегда пусты, такие как br и hr и три элемента pre, style и script, в которых положено сохранение разделителей. Формы обрабатываются особо при формировании регулярного HTML (другими словами, не XHTML), так как в них не предполагаются закрывающие тэги. И при выводе трех тэгов, в которых пробелы сохраняются, вы можете временно выключить выравнивание, и тогда pretty printer не добавит каких-либо разделителей, которые не являются частью действительного содержимого элементов.

(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*))

Последнее, что вам понадобится при генерации HTML, это параметр, указывающий, генерируете ли вы XHTML, так как это влияет на то, как вам нужно выводить пустые элементы.

(defparameter *xhtml* nil)

Со всей этой информацией, вы готовы к обработке ячеек FOO формы. Вы используете parse-cons-form, чтобы разбить список на три части, символ тэга, возможно пустой список свойств пар ключ/значение атрибутов, и, возможно пустой, список форм тела. Затем вы формируете открывающий тэг, тело и закрывающий тэг с помощью вспомогательных функций emit-open-tag, emit-element-body и emit-close-tag.

(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)
)
)

В emit-open-tag вам нужно вызвать freshline, когда это необходимо, и затем вывести атрибуты с помощью emit-attributes. Вам нужно передать тело элемента в функцию emit-open-tag, тогда в случае формирования XHTML, она определит, закончить тэг с /> или >.

(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)) "/>" ">"))
)

В emit-attributes имена атрибутов не вычисляются, так как они являются ключевыми символами, но вам следует вызывать функцию process верхнего уровня для вычисления значений атрибутов, связывая *escapes* с *attribute-escapes*. Для удобства при спецификации булевских атрибутов, чьи значения должны быть именем атрибута, если это значение равно Т (не любое истинное значение, а именно Т), то тогда вы заменяете значение именем атрибута.8)

(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 "'")
)
)

Формирование тела элемента похоже на формирование значений атрибута: вы можете циклически проходить по телу, вызывая process для вычисления каждого выражения. Основа кода заключена в выводе переводов строк и регулирования отступов подходящим образом в соответствии с типом элемента.

(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)
)
)

Наконец emit-close-tag, как вы вероятно ожидаете, выводит закрывающий тэг (до тех пор, пока в нем нет необходимости, например когда тело пустое и вы либо формируете XHTML, либо элемент является одним из специальных пустых элементов). Независимо от того, выводите ли вы закрывающий тэг, вам нужно вывести завершающий перевод строки для элементов блока и параграфа.

(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)
)
)

Функция process – это основа интерпретатора FOO. Чтобы сделать ее немного проще в использовании, вы можете определить функцию emit-html, которая вызывает process, передавая ей html-pretty-printer и форму для вычисления. Вы можете определить и использовать вспомогательную функцию get-pretty-printer для получения pretty printer, которая возвращает текущее значение *html-pretty-printer*, если оно связано; в ином случае, она создает новый экземпляр html-pretty-printer с *html-output* в качестве выходного потока.

(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*)
)
)
)

С этой функцией вы можете выводить HTML в *html-output*. Вместо того, чтобы предоставлять переменную *html-output* как часть открытого API FOO, вам следует определить макрос with-html-output, который берет на себя заботу о связывании потока для вас. Он также позволяет вам определить, хотите ли вы использовать аккуратный HTML вывод, выставляя по умолчанию значение переменной *pretty*.

(defmacro with-html-output ((stream &key (pretty *pretty*)) &body body)
  `(let* ((*html-output* ,stream)
          (*pretty* ,pretty)
)

    ,@body
)
)

Итак, если вы хотите использовать emit-html для вывода HTML в файл, вы можете написать следующее:

(with-open-file (out "foo.html" :direction output)
  (with-html-output (out :pretty t)
    (emit-html *some-foo-expression*)
)
)

Что Дальше?

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

1)Domain specific language - предметно-ориентированный язык программирования, мини-язык созданный специально для некоторых задач - прим. переводчика
2)Фактически, он, наверное, слишком выразителен, так как он может также генерировать все виды выходных данных, а не только разрешенные HTML. Конечно, это может быть фичей если вам нужно генерировать HTML который не является абсолютно корректным, для совместимости с легкими Web-браузерами. Кроме того, это обычная практика для обработчиков языков принимать программы, которые синтаксически корректны, но с другой стороны понятно, что это вызовет неопределенное поведение при выполнении.
3)Хорошо, почти каждый тэг. Определенные тэги, такие как IMG и BR не имеют закрывающих тегов. Вы встретитесь с ними в разделе "Базовое правило вычисления".
4)По строгому (strict) стандарту языка Common Lisp, ключевые символы не FIXME само-вычисляющиеся, хотя, фактически, они делают вычисление в самих себя. Смотри раздел 3.1.2.1.3 стандарта языка или HyperSpec для подробностей.
5)Требование использовать объекты, которые умеет интерпретировать считываетель Lisp не является жёстким. Так как считыватель Lisp сам по себе настраиваемый, вы можете также определить новый синтаксис на уровне считывателя для нового вида объекта. Но в таком подход принесет больше проблем, чем пользы.
6)С другой стороны, применяя более чистый объектно-ориентированный подход, мы могли бы определить два класса, скажем 'html-pretty-printer' и 'html-raw-printer', а затем определить на основе 'html-raw-printer' холостую реализацию для методов, которые должны делать что-то, только если *pretty* истинно. Однако, в таком случае, после определения всех холостых методов, вы, в конце концов получите большее количество кода, и вскоре вам надоест проверять, создали ли вы экземпляр нужного класса в нужное время. Но, в общем, замена условных выражений полиморфизмом это оптимальная стратегия.
7)Вам не нужен предикат для *inline-elements*, так как вы проверяете всегда только для блока и параграфа элементов. Я включил этот параметр здесь для завершенности.
8)В то время как в нотации XHTML требуется, чтобы в логических атрибутах имя совпадало со значением для указания значения true, в HTML также разрешено просто включить имя атрибута без значения, например, <option selected> также как и <option selected='selected'>. Все HTML-4.0 совместимые браузеры должны понимать обе формы, но некоторые легкие браузеры понимают только форму без значения для определенных атрибутов. Если вам нужно генерировать HTML для таких браузеров, вам потребуется исправить emit-attributes, чтобы формировать эти атрибуты немного по-другому.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru