Yellow Rabbit

Грамматика HTML в самом первом приближении

Web-движок на Lisp: грамматика и элемент text

После знакомства с парсером 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, вот схема его грамматики: Схема грамматики для 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))

Ещё один полезный тип — пустое место :smile:


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

"}"
*

Выглядит неплохо: Дерево после разбора