-
Notifications
You must be signed in to change notification settings - Fork 8
/
chapter-30.tex
939 lines (785 loc) · 63.4 KB
/
chapter-30.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
\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!<!, \lstinline!>!; и \lstinline!&!. Но
если обработчик языка знает, какие строки будут формироваться как данные элемента, тогда
он может позаботиться об автоматическом экранировании этих знаков за вас.
\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 & 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!<!, \lstinline!>!
и \lstinline!&!. Также в значениях атрибутов знаки кавычек, используемые для
разделения значения, должны быть экранированы: \lstinline!'! в \lstinline!&apos!; и
\lstinline!"! в \lstinline!"!. Вдобавок любой знак может быть представлен в виде
числовой ссылки на символ, состоящей из амперсанда, за которым следует знак <<диез>>
(\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{}& "&")
(#\bslash{}< "<")
(#\bslash{}> ">")
(#\bslash{}' "'")
(#\bslash{}" """)
(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 & bar"
HTML> (escape "foo & 'bar'" *element-escapes*)
"foo & 'bar'"
HTML> (escape "foo & 'bar'" *attribute-escapes*)
"foo & 'bar'"
\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: