dom.lua (6605B)
1 -- dom.lua - Simple DOM Element structure 2 3 local Element = {} 4 Element.__index = Element 5 6 function Element.new(tag, attrs) 7 attrs = attrs or {} 8 local self = setmetatable({}, Element) 9 10 self.tag = tag or "div" 11 self.id = attrs.id 12 self.className = attrs.class or "" 13 self.attributes = attrs 14 self.children = {} 15 self.parent = nil 16 self.text = "" 17 18 -- Layout (computed during render) 19 self.x = 0 20 self.y = 0 21 self.width = 0 22 self.height = 0 23 24 return self 25 end 26 27 function Element:addChild(child) 28 child.parent = self 29 table.insert(self.children, child) 30 return child 31 end 32 33 function Element:setText(text) 34 self.text = text or "" 35 end 36 37 function Element:hasClass(name) 38 return self.className:find(name, 1, true) ~= nil 39 end 40 41 function Element:getAttribute(name) 42 return self.attributes[name] 43 end 44 45 function Element:findById(id) 46 if self.id == id then 47 return self 48 end 49 for _, child in ipairs(self.children) do 50 local found = child:findById(id) 51 if found then return found end 52 end 53 return nil 54 end 55 56 function Element:findByTag(tag) 57 local results = {} 58 if self.tag == tag then 59 table.insert(results, self) 60 end 61 for _, child in ipairs(self.children) do 62 local childResults = child:findByTag(tag) 63 for _, elem in ipairs(childResults) do 64 table.insert(results, elem) 65 end 66 end 67 return results 68 end 69 70 function Element:findByClass(className) 71 local results = {} 72 if self:hasClass(className) then 73 table.insert(results, self) 74 end 75 for _, child in ipairs(self.children) do 76 local childResults = child:findByClass(className) 77 for _, elem in ipairs(childResults) do 78 table.insert(results, elem) 79 end 80 end 81 return results 82 end 83 84 -- Find first element matching a CSS selector 85 -- Supports: #id, .class, tagname, tag#id, tag.class, [attr], [attr=value] 86 function Element:querySelector(selector) 87 if not selector or selector == "" then 88 return nil 89 end 90 91 -- Parse selector 92 local tag, id, class, attrName, attrValue 93 94 -- Check for #id 95 local idMatch = selector:match("^#([%w%-_]+)$") 96 if idMatch then 97 return self:findById(idMatch) 98 end 99 100 -- Check for .class 101 local classMatch = selector:match("^%.([%w%-_]+)$") 102 if classMatch then 103 local results = self:findByClass(classMatch) 104 return results[1] 105 end 106 107 -- Check for [attr] or [attr=value] 108 local attrOnly = selector:match("^%[([%w%-_]+)%]$") 109 if attrOnly then 110 attrName = attrOnly 111 else 112 local attrWithVal, attrVal = selector:match("^%[([%w%-_]+)=[\"']?([^\"'%]]+)[\"']?%]$") 113 if attrWithVal then 114 attrName = attrWithVal 115 attrValue = attrVal 116 end 117 end 118 119 -- Check for tag#id 120 local tagId, idPart = selector:match("^([%w%-_]+)#([%w%-_]+)$") 121 if tagId then 122 tag = tagId:lower() 123 id = idPart 124 end 125 126 -- Check for tag.class 127 local tagClass, classPart = selector:match("^([%w%-_]+)%.([%w%-_]+)$") 128 if tagClass then 129 tag = tagClass:lower() 130 class = classPart 131 end 132 133 -- Check for plain tag 134 if not tag and not id and not class and not attrName then 135 tag = selector:lower() 136 end 137 138 -- Search recursively 139 return self:_querySelectorSearch(tag, id, class, attrName, attrValue) 140 end 141 142 function Element:_querySelectorSearch(tag, id, class, attrName, attrValue) 143 -- Check if this element matches 144 local matches = true 145 146 if tag and self.tag ~= tag then 147 matches = false 148 end 149 if id and self.id ~= id then 150 matches = false 151 end 152 if class and not self:hasClass(class) then 153 matches = false 154 end 155 if attrName then 156 local attr = self.attributes[attrName] 157 if attr == nil then 158 matches = false 159 elseif attrValue and tostring(attr) ~= attrValue then 160 matches = false 161 end 162 end 163 164 if matches and (tag or id or class or attrName) then 165 return self 166 end 167 168 -- Search children 169 for _, child in ipairs(self.children) do 170 local found = child:_querySelectorSearch(tag, id, class, attrName, attrValue) 171 if found then 172 return found 173 end 174 end 175 176 return nil 177 end 178 179 -- Find all elements matching a CSS selector 180 function Element:querySelectorAll(selector) 181 local results = {} 182 183 if not selector or selector == "" then 184 return results 185 end 186 187 -- Parse selector (same as querySelector) 188 local tag, id, class, attrName, attrValue 189 190 local idMatch = selector:match("^#([%w%-_]+)$") 191 if idMatch then 192 local elem = self:findById(idMatch) 193 if elem then table.insert(results, elem) end 194 return results 195 end 196 197 local classMatch = selector:match("^%.([%w%-_]+)$") 198 if classMatch then 199 return self:findByClass(classMatch) 200 end 201 202 local attrOnly = selector:match("^%[([%w%-_]+)%]$") 203 if attrOnly then 204 attrName = attrOnly 205 else 206 local attrWithVal, attrVal = selector:match("^%[([%w%-_]+)=[\"']?([^\"'%]]+)[\"']?%]$") 207 if attrWithVal then 208 attrName = attrWithVal 209 attrValue = attrVal 210 end 211 end 212 213 local tagId, idPart = selector:match("^([%w%-_]+)#([%w%-_]+)$") 214 if tagId then 215 tag = tagId:lower() 216 id = idPart 217 end 218 219 local tagClass, classPart = selector:match("^([%w%-_]+)%.([%w%-_]+)$") 220 if tagClass then 221 tag = tagClass:lower() 222 class = classPart 223 end 224 225 if not tag and not id and not class and not attrName then 226 tag = selector:lower() 227 end 228 229 self:_querySelectorAllSearch(tag, id, class, attrName, attrValue, results) 230 return results 231 end 232 233 function Element:_querySelectorAllSearch(tag, id, class, attrName, attrValue, results) 234 local matches = true 235 236 if tag and self.tag ~= tag then 237 matches = false 238 end 239 if id and self.id ~= id then 240 matches = false 241 end 242 if class and not self:hasClass(class) then 243 matches = false 244 end 245 if attrName then 246 local attr = self.attributes[attrName] 247 if attr == nil then 248 matches = false 249 elseif attrValue and tostring(attr) ~= attrValue then 250 matches = false 251 end 252 end 253 254 if matches and (tag or id or class or attrName) then 255 table.insert(results, self) 256 end 257 258 for _, child in ipairs(self.children) do 259 child:_querySelectorAllSearch(tag, id, class, attrName, attrValue, results) 260 end 261 end 262 263 return { 264 Element = Element 265 }