ElementNode.lua

main 351 lines · 9.1 KB Raw
Alex Alex Commit Client Upload Part 2 19/10/2026 00:06
1-- vim: ft=lua ts=2
2local Set = {}
3Set.mt = { __index = Set }
4function Set:new(values)
5 local instance = {}
6 local isSet
7 if getmetatable(values) == Set.mt then isSet = true end
8 if type(values) == "table" then
9 if not isSet and #values > 0 then
10 for _, v in ipairs(values) do
11 instance[v] = true
12 end
13 else
14 for k in pairs(values) do
15 instance[k] = true
16 end
17 end
18 elseif values ~= nil then
19 instance = { [values] = true }
20 end
21 return setmetatable(instance, Set.mt)
22end
23
24function Set:add(e)
25 if e ~= nil then self[e] = true end
26 return self
27end
28
29function Set:remove(e)
30 if e ~= nil then self[e] = nil end
31 return self
32end
33
34function Set:tolist()
35 local res = {}
36 for k in pairs(self) do
37 table.insert(res, k)
38 end
39 return res
40end
41
42Set.mt.__add = function(a, b)
43 local res, a, b = Set:new(), Set:new(a), Set:new(b)
44 for k in pairs(a) do res[k] = true end
45 for k in pairs(b) do res[k] = true end
46 return res
47end
48
49-- Subtraction
50Set.mt.__sub = function(a, b)
51 local res, a, b = Set:new(), Set:new(a), Set:new(b)
52 for k in pairs(a) do res[k] = true end
53 for k in pairs(b) do res[k] = nil end
54 return res
55end
56
57-- Intersection
58Set.mt.__mul = function(a, b)
59 local res, a, b = Set:new(), Set:new(a), Set:new(b)
60 for k in pairs(a) do
61 res[k] = b[k]
62 end
63 return res
64end
65
66-- String representation
67Set.mt.__tostring = function(set)
68 local list = { "{" }
69 for k in pairs(set) do
70 list[#list + 1] = tostring(k)
71 list[#list + 1] = ", " -- <= Theoretically, it should't be a problem because of string buffering.
72 -- Especially, as it allows to avoid string concatenation at all, and also allows to
73 -- avoid things like [1]
74 -- it looks like good idea to add separators this way.
75 -- But it needs to be tested on real-world examples with giant inputs.
76 -- If any (if it'd show that string buffering fails, and it is still leads huge RAM usage),
77 -- it can be downgraded to [1] or even string.format
78 --
79 -- [1] = something like table.concat{"{",table.concat(list,","),"}"},
80 --
81 end
82 list[#list] = "}"
83
84 return table.concat(list)
85end
86
87
88
89local ElementNode = {}
90ElementNode.mt = { __index = ElementNode }
91function ElementNode:new(index, nameortext, node, descend, openstart, openend)
92 local instance = {
93 index = index,
94 name = nameortext and nameortext:lower() or nameortext,
95 level = 0,
96 parent = nil,
97 root = nil,
98 nodes = {},
99 _openstart = openstart,
100 _openend = openend,
101 _closestart = openstart,
102 _closeend = openend,
103 attributes = {},
104 id = nil,
105 classes = {},
106 deepernodes = Set:new(),
107 deeperelements = {},
108 deeperattributes = {},
109 deeperids = {},
110 deeperclasses = {}
111 }
112 if not node then
113 instance.name = "root"
114 instance.root = instance
115 instance._text = nameortext
116 local length = string.len(nameortext)
117 instance._openstart, instance._openend = 1, length
118 instance._closestart, instance._closeend = 1, length
119 elseif descend then
120 instance.root = node.root
121 instance.parent = node
122 instance.level = node.level + 1
123 table.insert(node.nodes, instance)
124 else
125 instance.root = node.root
126 instance.parent = node.parent or
127 node --XXX: adds some safety but needs more testing for heisenbugs in corner cases
128 instance.level = node.level
129 table.insert((node.parent and node.parent.nodes or node.nodes), instance) --XXX: see above about heisenbugs
130 end
131
132 return setmetatable(instance, ElementNode.mt)
133end
134
135function ElementNode:gettext()
136 return string.sub(self.root._text, self._openstart, self._closeend)
137end
138
139function ElementNode:settext(c)
140 self.root._text = c
141end
142
143function ElementNode:textonly()
144 return (self:gettext():gsub("<[^>]*>", ""))
145end
146
147function ElementNode:getcontent()
148 return string.sub(self.root._text, self._openend + 1, self._closestart - 1)
149end
150
151function ElementNode:addattribute(k, v)
152 self.attributes[k] = v
153 if string.lower(k) == "id" then
154 self.id = v
155 -- class attribute contains "space-separated tokens", each of which we'd like quick access to
156 elseif string.lower(k) == "class" then
157 for class in string.gmatch(v, "%S+") do
158 table.insert(self.classes, class:lower())
159 end
160 end
161end
162
163local function insert(table, name, node)
164 table[name] = table[name] or Set:new()
165 table[name]:add(node)
166end
167
168function ElementNode:close(closestart, closeend)
169 if closestart and closeend then
170 self._closestart, self._closeend = closestart, closeend
171 end
172 -- inform hihger level nodes about this element's existence in their branches
173 local node = self
174 while true do
175 node = node.parent
176 if not node then break end
177 node.deepernodes:add(self)
178 insert(node.deeperelements, self.name, self)
179 for k in pairs(self.attributes) do
180 insert(node.deeperattributes, k, self)
181 end
182 if self.id then
183 insert(node.deeperids, self.id, self)
184 end
185 for _, v in ipairs(self.classes) do
186 insert(node.deeperclasses, v, self)
187 end
188 end
189end
190
191local function escape(s)
192 -- escape all ^, $, (, ), %, ., [, ], *, +, - , and ? with a % prefix
193 return string.gsub(s, "([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%" .. "%1")
194end
195
196local function find(self, s)
197 if not s or type(s) ~= "string" or s == "" then return Set:new() end
198
199 s = s:lower()
200
201 local splited = s:split(',')
202 if #splited > 1 then
203 s = splited[1]
204 end
205
206 local sets = {
207 [""] = self.deeperelements,
208 ["["] = self.deeperattributes,
209 ["#"] = self.deeperids,
210 ["."] = self.deeperclasses
211 }
212 local function match(t, w)
213 local m, e, v
214 if t == "[" then
215 w, m, e, v = string.match(w,
216 "([^=|%*~%$!%^]+)" .. -- w = 1 or more characters up to a possible "=", "|", "*", "~", "$", "!", or "^"
217 "([|%*~%$!%^]?)" .. -- m = an optional "|", "*", "~", "$", "!", or "^", preceding the optional "="
218 "(=?)" .. -- e = the optional "="
219 "(.*)" -- v = anything following the "=", or else ""
220 )
221 end
222 local matched = Set:new(sets[t][w])
223 -- attribute value selectors
224 if e == "=" then
225 if #v < 2 then v = "'" .. v .. "'" end -- values should be quoted
226 v = string.sub(v, 2, #v - 1) -- strip quotes
227 if m == "!" then matched = Set:new(self.deepernodes) end -- include those without that attribute
228 for node in pairs(matched) do
229 local a = node.attributes[w]
230 -- equals
231 if m == "" and a ~= v then
232 matched:remove(node)
233 -- not equals
234 elseif m == "!" and a == v then
235 matched:remove(node)
236 -- prefix
237 elseif m == "|" and string.match(a, "^[^-]*") ~= v then
238 matched:remove(node)
239 -- contains
240 elseif m == "*" and string.match(a, escape(v)) ~= v then
241 matched:remove(node)
242 -- word
243 elseif m == "~" then
244 matched:remove(node)
245 for word in string.gmatch(a, "%S+") do
246 if word == v then
247 matched:add(node)
248 break
249 end
250 end
251 -- starts with
252 elseif m == "^" and string.match(a, "^" .. escape(v)) ~= v then
253 matched:remove(node)
254 -- ends with
255 elseif m == "$" and string.match(a, escape(v) .. "$") ~= v then
256 matched:remove(node)
257 end
258 end -- for node
259 end -- if v
260 return matched
261 end
262
263 local subjects, resultset, childrenonly = Set:new({ self })
264 for part in string.gmatch(s, "%S+") do
265 repeat
266 if part == ">" then
267 childrenonly = true --[[goto nextpart]]
268 break
269 end
270 resultset = Set:new()
271 for subject in pairs(subjects) do
272 local star = subject.deepernodes
273 if childrenonly then star = Set:new(subject.nodes) end
274 resultset = resultset + star
275 end
276 childrenonly = false
277 if part == "*" then --[[goto nextpart]] break end
278 local excludes, filter = Set:new()
279 local start, pos = 0, 0
280 while true do
281 local switch, stype, name, eq, quote
282 start, pos, switch, stype, name, eq, quote = string.find(part,
283 "(%(?%)?)" .. -- switch = a possible ( or ) switching the filter on or off
284 "([:%[#.]?)" .. -- stype = a possible :, [, #, or .
285 "([%w-_\\]+)" .. -- name = 1 or more alfanumeric chars (+ hyphen, reverse slash and uderscore)
286 "([|%*~%$!%^]?=?)" .. -- eq = a possible |=, *=, ~=, $=, !=, ^=, or =
287 "(['\"]?)", -- quote = a ' or " delimiting a possible attribute value
288 pos + 1
289 )
290 if not name then break end
291 repeat
292 if ":" == stype then
293 filter = name
294 --[[goto nextname]]
295 break
296 end
297 if ")" == switch then
298 filter = nil
299 end
300 if "[" == stype and "" ~= quote then
301 local value
302 start, pos, value = string.find(part, "(%b" .. quote .. quote .. ")]", pos)
303 name = name .. eq .. value
304 end
305 local matched = match(stype, name)
306 if filter == "not" then
307 excludes = excludes + matched
308 else
309 resultset = resultset * matched
310 end
311 --::nextname::
312 break
313 until true
314 end
315 resultset = resultset - excludes
316 subjects = Set:new(resultset)
317 --::nextpart::
318 break
319 until true
320 end
321 resultset = resultset:tolist()
322
323 for i, q in pairs(splited) do
324 if i > 1 then
325 table.insertall(resultset, find(self, q:gsub("[\n\r]", "")))
326 end
327 end
328
329 table.sort(resultset, function(a, b) return a.index < b.index end)
330 return resultset
331end
332
333function ElementNode:find(s) return find(self, s) end
334
335function ElementNode:findWidgets(s)
336 local els = find(self, s)
337
338 local widgets = {}
339 for _, el in pairs(els) do
340 if el.widget then
341 table.insert(widgets, el.widget)
342 end
343 end
344
345 return widgets
346end
347
348ElementNode.mt.__call = select
349
350return ElementNode
351
Top