luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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 }