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 }