После знакомства с парсером META переходим к рассмотрению грамматики HTML. А также посмотрим как разобрать простейший элемент.
Ядро META это самостоятельная часть движка, так что можно выделить её в отдельный файл и навести общий порядок в проекте. Файл toy-engine.asd
, описывающий состав и последовательность компиляции модулей движка:
;;;; toy-engine.asd
(asdf:defsystem #:toy-engine
:description "Tiny web-engine"
:author "Yellow Rabbit <yrabbit@example.com>"
:license "Public domain"
:serial t
:components ((:file "package")
(:file "dom")
(:file "meta-parser-core")
(:file "html-grammar")
(:file "toy-engine")))
Файлы html-grammar.lisp
и toy-engine.lisp
пока пустые и состоят из одной строчки каждый:
(in-package #:toy-engine)
Движок будет понимать простейшее подмножество HTML, вот схема его грамматики:
Эти простые функции, которые нужны для проверки символов в операции META @
, должны быть доступны во время компиляции:
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun gen-text-p (ch)
"Text is always between > and <"
(char/= ch #\<))
(defun always-true (ch)
"Any character is right one"
(declare (ignore ch))
t)
(defun azAZ09-p (ch)
"Alphanumerical"
(or (and (char>= ch #\a)
(char<= ch #\z))
(and (char>= ch #\A)
(char<= ch #\Z))
(digit-char-p ch))))
Функция вызываются не явно, а через проверку принадлежности к типу данных:
(deftype gen-text ()
"All except a <"
'(satisfies gen-text-p))
(deftype any-text ()
"Any character"
'(satisfies always-true))
(deftype tag-text ()
"Tag name is alphanumeric"
'(satisfies azAZ09-p))
Ещё один полезный тип — пустое место
(deftype whitespace ()
'(member #\Space #\Tab #\LineFeed #\Return #\FormFeed #\Page))
Парсер будет создавать дерево DOM прямо по ходу разбора. За каждый тип узла отвечает своя функция, для начала будем распознавать один простой элемент: текст. И его функция:
;; Create nodes
;; Text
(defun make-text-node (text)
(make-instance 'text-node
:text text))
Парсер принимает на входе строку для разбора, возможно номер первого и последнего символов для разбора и возвращает корневой узел получившегося дерева. Пока не знаю как будут присоединяться дочерние узлы, так что скелет парсера имеет вид:
;;; HTML parser
(defun parse-html (str &optional (index 0) (end (length str)))
(declare (type fixnum index end))
(labels
(
;; node parsers
(parse-text ()
"Text until <"
(let (ch
(text (with-output-to-string (s) (matchit $[@(gen-text ch) !(write-char ch s)]))))
(if (zerop (length text))
nil
(make-text-node text)))))
(parse-text)))
Проверяем на дереве из одного текстового узла:
* (ql:quickload 'toy-engine)
To load "toy-engine":
Load 1 ASDF system:
toy-engine
; Loading "toy-engine"
(TOY-ENGINE)
* (in-package :toy-engine)
#<PACKAGE "TOY-ENGINE">
* (defparameter *str* " ''' This is a text< kj")
*STR*
* (parse-html *str*)
#<TEXT-NODE {10050359A3}>
* (pp->dot "text-node.dot" (lambda () (pp-dom *)))
"}"
*
Выглядит неплохо: