-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathparser.lisp
140 lines (111 loc) · 4.13 KB
/
parser.lisp
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
(in-package :ten/parser)
;;; Utilities
(defparameter +whitespace+
(list #\Space #\Tab #\Newline #\Linefeed #\Backspace
#\Page #\Return #\Rubout))
(defun whitespacep (char)
(member char +whitespace+))
(defun trim-whitespace (str)
(string-trim +whitespace+ str))
;;; Element classes
(defclass <tag> () ())
(defclass <output-tag> (<tag>)
((code :reader code :initarg :code)))
(defclass <fcall-tag> (<output-tag>)
())
(defclass <var-tag> (<output-tag>)
())
(defclass <control-tag> (<tag>)
((code :reader code :initarg :code)
(body :reader body :initarg :body :initform nil)))
(defclass <end-tag> (<tag>)
())
(defclass <else-tag> (<tag>) ())
(defmethod print-object ((tag <output-tag>) stream)
(format stream "<~a ~a>"
(class-name (class-of tag))
(code tag)))
(defmethod print-object ((tag <control-tag>) stream)
(format stream "<~a ~a ~a>"
(class-name (class-of tag))
(code tag)
(body tag)))
;;; Parsing rules
(defparameter +start-output-delimiter+ "{{")
(defparameter +end-output-delimiter+ "}}")
(defparameter +start-control-delimiter+ "{%")
(defparameter +end-control-delimiter+ "%}")
(defparameter +start-comment-delimiter "{#")
(defparameter +stop-comment-delimiter "#}")
(defrule comment (and "{#" (+ (not "#}")) "#}")
(:lambda (list)
(declare (ignore list))
""))
(defrule control-string (+ (not "%}"))
(:text t))
(defrule control-tag (and "{%";;+start-control-delimiter+
control-string
"%}";;+end-control-delimiter+
)
(:destructure (open code close)
(declare (ignore open close))
(let ((text (trim-whitespace code)))
(cond
((equal text "end")
(make-instance '<end-tag>))
((equal text "else")
(make-instance '<else-tag>))
(t (make-instance '<control-tag> :code text))))))
(defrule output-string (+ (not "}}"))
(:lambda (list) (text list)))
(defrule output-tag (and "{{";;+start-output-delimiter+
output-string
"}}";;+end-output-delimiter+
)
(:destructure (open code close)
(declare (ignore open close))
(let ((text (trim-whitespace code)))
(if (find #\space text)
(make-instance '<fcall-tag> :code text)
(make-instance '<var-tag> :code text)))))
(defrule raw-text (+ (not (or "{{" ;;+start-output-delimiter+)
"{%" ;;+start-control-delimiter+)
"{#") ;;+start-comment-delimiter+)
))
(:lambda (list) (text list)))
(defrule expr (+ (or comment control-tag output-tag raw-text)))
(defun tokenize-template (string)
(parse 'expr string))
(defun def-control-without-body (symbol)
(setf (getf (symbol-plist symbol)
:ten-control-without-body)
t))
;;; Token parsing
;;; Take a list of either strings or <tag>s and turn it into a tree
(defun parse-tokens (tokens)
(let ((tokens (copy-list tokens)))
(labels ((next-token ()
(prog1 (first tokens)
(setf tokens (rest tokens))))
(rec-parse (&optional toplevel)
(let ((out (make-array 1 :adjustable 1 :fill-pointer 0))
(tok (next-token)))
(loop while (and tok (not (typep tok '<end-tag>))) do
(vector-push-extend
(cond
((typep tok '<control-tag>)
;; Start a block
(make-instance (class-of tok)
:code (code tok)
:body (rec-parse)))
(t tok))
out)
(setf tok (next-token))
(when (and (not tok) (not toplevel)) ;; Next tok is nil
(error "Missing 'end' tag" )))
out)))
(rec-parse t))))
(defun parse-template (string-or-pathname)
(if (pathnamep string-or-pathname)
(parse-template (alexandria:read-file-into-string string-or-pathname))
(parse-tokens (tokenize-template string-or-pathname))))