-
Notifications
You must be signed in to change notification settings - Fork 7
/
api.lisp
271 lines (233 loc) · 10.9 KB
/
api.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
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
;;; -*- show-trailing-whitespace: t; indent-tabs-mode: nil -*-
;;; Copyright (c) 2007 Ivan Shvedunov. All rights reserved.
;;; Copyright (c) 2007 David Lichteblau. All rights reserved.
;;; Redistribution and use in source and binary forms, with or without
;;; modification, are permitted provided that the following conditions
;;; are met:
;;;
;;; * Redistributions of source code must retain the above copyright
;;; notice, this list of conditions and the following disclaimer.
;;;
;;; * Redistributions in binary form must reproduce the above
;;; copyright notice, this list of conditions and the following
;;; disclaimer in the documentation and/or other materials
;;; provided with the distribution.
;;;
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(in-package :xpath)
;; public evaluation API
(defun first-node (node-set)
"@arg[node-set]{a @class{node-set}}
@return{a @class{node-set} or nil}
Returns the first node in the @code{node-set} or nil if it's empty."
(pipe-head (pipe-of node-set)))
(defun all-nodes (node-set)
"@arg[node-set]{a @class{node-set}}
@return{a list of nodes}
Returns all nodes of the @code{node-set} as a list."
(force (pipe-of node-set)))
(defun map-node-set (func node-set)
"@arg[func]{a function}
@arg[node-set]{a @class{node-set}}
@return{nil}
@short{Calls @code{func} for each node in @code{node-set}}
The operation is performed lazily, i.e. if it's terminated via
a non-local exit it doesn't necessarily cause the XPath engine to find
out all nodes in the @class{node-set} internally."
(enumerate (pipe-of node-set) :key func :result nil))
(defun map-node-set->list (func node-set)
"@arg[func]{a function}
@arg[node-set]{a @class{node-set}}
@return{a list}
@short{Calls @code{func} for each node in @code{node-set} and conses up
a list of its return values}
The operation is performed lazily, i.e. if it's terminated via
a non-local exit it doesn't necessarily cause the XPath engine to find
out all nodes in the @class{node-set} internally."
(loop for pipe = (pipe-of node-set) then (pipe-tail pipe)
while (not (pipe-empty-p pipe))
collect (funcall func (pipe-head pipe))))
(defmacro map-node-sets->list (func &rest node-sets)
"@arg[func]{a function}
@arg[node-sets]{@class{node-set}}
@return{a list}
@short{Calls @code{func} with N arguments for each N-tuple of nodes
on the N lists of @code{node-sets} and conses up a list of its
return values}
The operation is performed lazily, i.e. if it's terminated via
a non-local exit it doesn't necessarily cause the XPath engine to find
out all the nodes in @code{node-sets} internally."
(let ((pipe-syms (loop for node-set in node-sets
collect (gensym "NODE"))))
`(loop ,@(loop for pipe-sym in pipe-syms
for node-set in node-sets
append `(for ,pipe-sym = (pipe-of ,node-set) then (pipe-tail ,pipe-sym)))
while (not (or ,@(loop for pipe-sym in pipe-syms
collect `(pipe-empty-p ,pipe-sym))))
collect (funcall ,func ,@(loop for pipe-sym in pipe-syms
collect `(pipe-head ,pipe-sym))))))
(defmacro do-node-set ((var node-set &optional result) &body body)
"@arg[var]{symbol, a variable name}
@arg[node-set]{a @class{node-set}}
@arg[result]{a form}
@return{the result of evaluating @code{result}}
@short{Executes @code{body} with @code{var} bound to successive nodes
in @code{node-set}}
The operation is performed lazily, i.e. if it's terminated via
a non-local exit it doesn't necessarily cause the XPath engine to find
out all nodes in the @class{node-set} internally.
Returns nil if @code{result} form isn't specified."
(check-type var symbol)
`(block nil
(map-node-set #'(lambda (,var) ,@body) ,node-set)
,result))
(defstruct (node-set-iterator
(:constructor
%make-node-set-iterator (pipe)))
pipe)
(defun make-node-set-iterator (node-set)
"@arg[node-set]{a @class{node-set}}
@return{a node-set iterator}
@short{Creates a node set iterator for @code{node-set}}
Node set iterators can be used to iterate over node-sets.
This can be done without causing the XPath engine to find out
all their nodes and using non-local exits."
(%make-node-set-iterator (pipe-of node-set)))
(defun node-set-iterator-end-p (iterator)
"@arg[iterator]{a node-set iterator returned by @fun{make-node-set-iterator}}
@return{a generalized boolean}
Returns true if @code{iterator} points to the end of its node set"
(pipe-empty-p (node-set-iterator-pipe iterator)))
(defun node-set-iterator-next (iterator)
"@arg[iterator]{a node-set iterator returned by @fun{make-node-set-iterator}}
@return{the value of @code{iterator}}
Advances @code{iterator} if it's not at the end of its node set,
does nothing otherwise."
(unless (node-set-iterator-end-p iterator)
(setf (node-set-iterator-pipe iterator)
(pipe-tail (node-set-iterator-pipe iterator))))
iterator)
(defun node-set-iterator-current (iterator)
"@arg[iterator]{a node-set iterator returned by @fun{make-node-set-iterator}}
@return{a node or nil}
Returns current node of @code{iterator} or nil if it's at the end
of its node set."
(if (node-set-iterator-end-p iterator)
nil
(pipe-head (node-set-iterator-pipe iterator))))
(defun list->node-set (list)
"@arg[list]{a list of nodes}
@return{a @class{node-set}}
Makes a @class{node-set} from the @code{list} of nodes."
(make-node-set list))
(defmacro xpath (form)
"@arg[form]{a sexpr-based XPath form}
@return{a list consisting of symbol XPATH and the @code{form}}
This macro is used to specify sexpr-based XPath expression for @fun{evaluate}"
`(list 'xpath ',form))
(deftype xpath-expr ()
'(or string function
(cons (eql xpath) (cons t null))))
(defun compile-xpath (xpath &optional (environment (make-dynamic-environment *dynamic-namespaces*)))
"@arg[xpath]{an XPath expression}
@return{a compiled XPath expression}
@short{Compiles an XPath expression}
The @code{xpath} expression is compiled using current environment if it isn't
compiled yet. @code{xpath} can be a string, a sexpr-based XPath epression or
a compiled expression. In the latter case @code{xpath} argument value itself
is returned."
(unless (typep xpath 'xpath-expr)
(xpath-error "invalid xpath designator: ~A" xpath))
(if (functionp xpath)
xpath
(maybe-wrap-profiling
xpath
(compile-xpath/sexpr (if (stringp xpath)
(parse-xpath xpath)
(second xpath))
environment))))
(defun evaluate-compiled (compiled-xpath context &optional unordered-p)
"@arg[compiled-xpath]{a compiled XPath expression}
@arg[context]{an XPath context}
@arg[unordered-p]{specify true to get unordered node-set}
@return{the result of evaluating @code{compiled-xpath} within the @code{context}}
@short{Evaluates a compiled XPath expression returned by @fun{compile-xpath}}
The @code{context} can be obtained using @fun{make-context}. As an alternative,
a node can be specifed.
If @code{unordered-p} is false (default) and value being returned is a @class{node-set},
it will be sorted using @fun{sort-node-set} so its nodes will be in document
order. If @code{unordered-p} is true, the order of the nodes is unspecified.
Unordered mode can be significantly faster in some cases (and never slower)."
;; FIXME: Should this perhaps compute position and size based on
;; the node's siblings instead?
(let ((value
(with-float-traps-masked ()
(funcall compiled-xpath
(if (typep context 'context) context (make-context context))))))
(if (and (not unordered-p)
(node-set-p value))
(sort-node-set value)
value)))
(defun same-expr-p (prev-expr xpath cur-bindings)
(and (equal xpath (first prev-expr))
(loop for (key . value) in (rest prev-expr)
when (not (equal value (cdr (assoc key cur-bindings :test #'equal))))
do (return nil)
finally (return t))))
(defun namespaces-match-p (old-bindings cur-bindings)
(loop for (key . value) in old-bindings
when (not (equal value (cdr (assoc key cur-bindings :test #'equal))))
do (return nil)
finally (return t)))
(defun evaluate (xpath context &optional unordered-p)
"@arg[xpath]{an XPath expression}
@arg[context]{an XPath context}
@arg[unordered-p]{specify true to get unordered node-set}
@return{the result of evaluating @code{xpath} within the @code{context}}
@short{Evaluates an XPath expression}
@code{xpath} can be a string, a sexpr-based XPath epression or
a compiled expression. The @code{context} can be obtained using @fun{make-context}.
As an alternative, a node can be specifed.
If @code{unordered-p} is false (default) and value being returned is a @class{node-set},
it will be sorted using @fun{sort-node-set} so its nodes will be in document
order. If @code{unordered-p} is true, the order of the nodes is unspecified.
Unordered mode can be significantly faster in some cases (and never slower)."
(evaluate-compiled
(if (functionp xpath)
xpath
(compile-xpath xpath (make-dynamic-environment *dynamic-namespaces*)))
context
unordered-p))
(define-compiler-macro evaluate (xpath context &optional unordered-p)
(once-only (xpath)
`(evaluate-compiled
(if (functionp ,xpath)
,xpath
(with-cache ((,xpath :test equal)
(*dynamic-namespaces* :test namespaces-match-p)
(*profiling-enabled-p* :test eql))
(compile-xpath ,xpath
(make-dynamic-environment
*dynamic-namespaces*))))
,context
,unordered-p)))
;; errors
(define-condition xpath-error (simple-error)
()
(:documentation "The class of all xpath errors."))
;; FIXME: function & condition of the same name cause problems for atdoc
(defun xpath-error (fmt &rest args)
"@arg[fmt]{format control string}
@arg[args]{format arguments}
Signals the @class{xpath-error} condition with specified message."
(error 'xpath-error :format-control fmt :format-arguments args))