luajitos

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

style.lua (6706B)


      1 -- CSS Style System
      2 -- Handles CSS parsing, cascading, specificity, and computed styles
      3 
      4 local Style = {}
      5 Style.__index = Style
      6 
      7 -- CSS Rule representation
      8 local CSSRule = {}
      9 CSSRule.__index = CSSRule
     10 
     11 function CSSRule:new(selector, declarations, specificity, important_flags)
     12     return setmetatable({
     13         selector = selector,
     14         declarations = declarations or {},
     15         specificity = specificity or {0, 0, 0},
     16         important_flags = important_flags or {}
     17     }, self)
     18 end
     19 
     20 -- Parse CSS selector and calculate specificity [ids, classes, tags]
     21 local function calculate_specificity(selector)
     22     local ids = 0
     23     local classes = 0
     24     local tags = 0
     25 
     26     -- Count IDs
     27     for _ in selector:gmatch("#[%w_-]+") do
     28         ids = ids + 1
     29     end
     30 
     31     -- Count classes and pseudo-classes
     32     for _ in selector:gmatch("%.[%w_-]+") do
     33         classes = classes + 1
     34     end
     35 
     36     -- Count tags
     37     for word in selector:gmatch("[%w]+") do
     38         -- Skip if it's right after # or .
     39         local escaped_word = word:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
     40         local before = selector:match("([#.])" .. escaped_word)
     41         if not before then
     42             tags = tags + 1
     43         end
     44     end
     45 
     46     return {ids, classes, tags}
     47 end
     48 
     49 -- Compare two specificity arrays
     50 local function compare_specificity(a, b)
     51     for i = 1, 3 do
     52         if a[i] > b[i] then
     53             return 1
     54         elseif a[i] < b[i] then
     55             return -1
     56         end
     57     end
     58     return 0
     59 end
     60 
     61 -- Check if a selector matches an element
     62 local function selector_matches(selector, element)
     63     -- Remove whitespace
     64     selector = selector:match("^%s*(.-)%s*$")
     65 
     66     -- Handle descendant selectors (simplified - just check the last part)
     67     local parts = {}
     68     for part in selector:gmatch("[^%s]+") do
     69         table.insert(parts, part)
     70     end
     71     local simple_selector = parts[#parts] or selector
     72 
     73     -- Parse simple selector
     74     local tag, id, classes = simple_selector:match("^([%w*]*)#?([%w_-]*)%.?(.*)$")
     75 
     76     -- Match tag
     77     if tag and tag ~= "" and tag ~= "*" then
     78         if tag ~= element.tag then
     79             return false
     80         end
     81     end
     82 
     83     -- Match ID
     84     if id and id ~= "" then
     85         if element.id ~= id then
     86             return false
     87         end
     88     end
     89 
     90     -- Match classes
     91     if classes and classes ~= "" then
     92         for class in classes:gmatch("[%w_-]+") do
     93             if not element:has_class(class) then
     94                 return false
     95             end
     96         end
     97     end
     98 
     99     return true
    100 end
    101 
    102 -- Parse CSS text into rules
    103 local function parse_css(css_text)
    104     local rules = {}
    105 
    106     -- Remove comments
    107     css_text = css_text:gsub("/%*.-%*/", "")
    108 
    109     -- Match rule blocks
    110     for selector, declarations_text in css_text:gmatch("([^{}]+)%s*{([^}]*)}") do
    111         -- Parse declarations
    112         local declarations = {}
    113         local important_flags = {}
    114 
    115         for declaration in declarations_text:gmatch("[^;]+") do
    116             local prop, value = declaration:match("^%s*([%w-]+)%s*:%s*(.-)%s*$")
    117             if prop and value then
    118                 -- Check for !important
    119                 local important = false
    120                 if value:match("!important") then
    121                     important = true
    122                     value = value:gsub("%s*!important%s*", "")
    123                 end
    124 
    125                 declarations[prop] = value
    126                 important_flags[prop] = important
    127             end
    128         end
    129 
    130         -- Calculate specificity
    131         local specificity = calculate_specificity(selector)
    132 
    133         -- Create rule
    134         table.insert(rules, CSSRule:new(selector, declarations, specificity, important_flags))
    135     end
    136 
    137     return rules
    138 end
    139 
    140 -- Load default CSS from file
    141 local function load_default_css()
    142     local file = io.open("default.css", "r")
    143     if not file then
    144         -- Fallback minimal CSS if file doesn't exist
    145         return [[
    146 * {
    147     display: block;
    148     color: #000000;
    149     background-color: transparent;
    150 }
    151         ]]
    152     end
    153 
    154     local content = file:read("*all")
    155     file:close()
    156     return content
    157 end
    158 
    159 -- Style manager
    160 function Style:new(dom)
    161     local obj = setmetatable({
    162         dom = dom,
    163         rules = {}
    164     }, self)
    165 
    166     -- Load default styles from default.css
    167     local default_css = load_default_css()
    168     obj:load(default_css)
    169 
    170     return obj
    171 end
    172 
    173 function Style:load(css_text, css_path)
    174     local new_rules = parse_css(css_text)
    175     for _, rule in ipairs(new_rules) do
    176         table.insert(self.rules, rule)
    177     end
    178 end
    179 
    180 function Style:inline(css_text)
    181     self:load(css_text)
    182 end
    183 
    184 function Style:get(element)
    185     local computed = {}
    186     local important_props = {}
    187 
    188     -- Apply rules in order, respecting specificity and !important
    189     for _, rule in ipairs(self.rules) do
    190         if selector_matches(rule.selector, element) then
    191             for prop, value in pairs(rule.declarations) do
    192                 local is_important = rule.important_flags[prop]
    193                 local existing_important = important_props[prop]
    194 
    195                 -- Only override if:
    196                 -- 1. No existing value, OR
    197                 -- 2. New value is !important and old is not, OR
    198                 -- 3. Both are !important or neither is, and new has higher/equal specificity
    199                 if not computed[prop] or
    200                    (is_important and not existing_important) or
    201                    ((is_important == existing_important) and
    202                     compare_specificity(rule.specificity,
    203                                       self:get_rule_specificity(prop, computed[prop])) >= 0) then
    204                     computed[prop] = value
    205                     important_props[prop] = is_important
    206                 end
    207             end
    208         end
    209     end
    210 
    211     -- Handle inline styles (highest specificity)
    212     if element.attributes.style then
    213         local style_attr = element.attributes.style
    214 
    215         if type(style_attr) == "string" then
    216             -- Parse inline style string like "color: cyan; font-size: 14px"
    217             for declaration in style_attr:gmatch("[^;]+") do
    218                 local prop, value = declaration:match("^%s*([%w-]+)%s*:%s*(.-)%s*$")
    219                 if prop and value then
    220                     computed[prop] = value
    221                 end
    222             end
    223         elseif type(style_attr) == "table" then
    224             -- Style set programmatically via script (already a table)
    225             for prop, value in pairs(style_attr) do
    226                 computed[prop] = value
    227             end
    228         end
    229     end
    230 
    231     return computed
    232 end
    233 
    234 function Style:get_rule_specificity(prop, value)
    235     -- Find the specificity of the rule that set this property
    236     -- For simplicity, return a low specificity
    237     return {0, 0, 0}
    238 end
    239 
    240 return {
    241     Style = Style
    242 }