lua_engine.lua (12137B)
1 -- Lua Script Engine with Sandbox 2 -- Provides safe environment for running user scripts with DOM access 3 4 local LuaEngine = {} 5 LuaEngine.__index = LuaEngine 6 7 -- Create safe sandbox environment 8 local function create_sandbox() 9 local sandbox = { 10 -- Safe standard libraries 11 string = string, 12 math = math, 13 table = table, 14 15 -- Safe os functions only 16 os = { 17 time = os.time, 18 date = os.date, 19 clock = os.clock, 20 difftime = os.difftime, 21 }, 22 23 -- Basic globals 24 print = print, 25 tonumber = tonumber, 26 tostring = tostring, 27 type = type, 28 pairs = pairs, 29 ipairs = ipairs, 30 next = next, 31 select = select, 32 unpack = unpack or table.unpack, 33 assert = assert, 34 error = error, 35 pcall = pcall, 36 xpcall = xpcall, 37 38 -- LuaJIT bit library if available 39 bit = bit, 40 41 -- Metatable for _G to prevent escaping sandbox 42 _VERSION = _VERSION, 43 } 44 45 return sandbox 46 end 47 48 -- CSS selector matcher (simplified - supports tag, #id, .class) 49 local function matches_selector(element, selector) 50 selector = selector:match("^%s*(.-)%s*$") -- trim 51 52 -- Tag selector 53 if selector:match("^[%w]+$") then 54 return element.tag == selector 55 end 56 57 -- ID selector 58 if selector:match("^#") then 59 local id = selector:sub(2) 60 return element.id == id 61 end 62 63 -- Class selector 64 if selector:match("^%.") then 65 local class = selector:sub(2) 66 return element:has_class(class) 67 end 68 69 -- Tag with class 70 local tag, class = selector:match("^([%w]+)%.([%w_-]+)$") 71 if tag and class then 72 return element.tag == tag and element:has_class(class) 73 end 74 75 -- Tag with id 76 local tag2, id = selector:match("^([%w]+)#([%w_-]+)$") 77 if tag2 and id then 78 return element.tag == tag2 and element.id == id 79 end 80 81 return false 82 end 83 84 -- Query selector - find first matching element 85 local function query_selector(root, selector) 86 if matches_selector(root, selector) then 87 return root 88 end 89 90 for _, child in ipairs(root.children) do 91 local found = query_selector(child, selector) 92 if found then 93 return found 94 end 95 end 96 97 return nil 98 end 99 100 -- Query selector all - find all matching elements 101 local function query_selector_all(root, selector) 102 local results = {} 103 104 local function collect(elem) 105 if matches_selector(elem, selector) then 106 table.insert(results, elem) 107 end 108 109 for _, child in ipairs(elem.children) do 110 collect(child) 111 end 112 end 113 114 collect(root) 115 return results 116 end 117 118 -- Create element wrapper with script-accessible API 119 local function create_element_wrapper(element, document) 120 local wrapper = { 121 -- Raw element access (internal) 122 _element = element, 123 _document = document, 124 125 -- Properties 126 tag = element.tag, 127 content = element.content, 128 } 129 130 -- Make style accessible as a table with get/set 131 wrapper.style = setmetatable({}, { 132 __index = function(t, key) 133 if not element.attributes.style then 134 element.attributes.style = {} 135 end 136 return element.attributes.style[key] 137 end, 138 __newindex = function(t, key, value) 139 if not element.attributes.style then 140 element.attributes.style = {} 141 end 142 element.attributes.style[key] = value 143 end 144 }) 145 146 -- ID get/set 147 wrapper.getId = function() return element.id end 148 wrapper.setId = function(id) element.id = id end 149 150 -- Class management 151 wrapper.classList = { 152 add = function(class) 153 if not element:has_class(class) then 154 table.insert(element.classes, class) 155 end 156 end, 157 remove = function(class) 158 for i, cls in ipairs(element.classes) do 159 if cls == class then 160 table.remove(element.classes, i) 161 break 162 end 163 end 164 end, 165 contains = function(class) 166 return element:has_class(class) 167 end, 168 toggle = function(class) 169 if element:has_class(class) then 170 wrapper.classList.remove(class) 171 else 172 wrapper.classList.add(class) 173 end 174 end, 175 } 176 177 -- Get/set value (for inputs and text elements) 178 wrapper.getValue = function() 179 -- For inputs, try to get from document state first (for focused inputs) 180 if element.tag == "input" then 181 if wrapper._document and wrapper._document.input_values[element] then 182 return wrapper._document.input_values[element] 183 end 184 return element.attributes.value 185 end 186 187 -- For span/div/p etc, get from content or first text child 188 if element.content and element.content ~= "" then 189 return element.content 190 end 191 192 -- Check for text child node 193 if #element.children > 0 and element.children[1].tag == "text" then 194 return element.children[1].content 195 end 196 197 return element.attributes.value 198 end 199 wrapper.setValue = function(value) 200 element.attributes.value = value 201 202 -- For inputs, update document state 203 if element.tag == "input" and wrapper._document then 204 wrapper._document.input_values[element] = value 205 return 206 end 207 208 -- For span/div/p etc, update content 209 if element.tag == "span" or element.tag == "div" or element.tag == "p" or 210 element.tag == "label" or element.tag == "b" or element.tag == "i" then 211 -- If there's a text child, update it 212 if #element.children > 0 and element.children[1].tag == "text" then 213 element.children[1].content = tostring(value) 214 else 215 -- Otherwise set element content and clear children 216 element.content = tostring(value) 217 element.children = {} 218 end 219 end 220 end 221 222 -- Property-style value access 223 setmetatable(wrapper, { 224 __index = function(t, key) 225 if key == "value" then 226 return wrapper.getValue() 227 end 228 return rawget(t, key) 229 end, 230 __newindex = function(t, key, value) 231 if key == "value" then 232 wrapper.setValue(value) 233 else 234 rawset(t, key, value) 235 end 236 end 237 }) 238 239 -- Navigation properties and methods 240 wrapper.getParent = function() 241 return element.parent and create_element_wrapper(element.parent, document) or nil 242 end 243 244 wrapper.nextElement = function() 245 if not element.parent then return nil end 246 local siblings = element.parent.children 247 for i, child in ipairs(siblings) do 248 if child == element and i < #siblings then 249 return create_element_wrapper(siblings[i + 1], document) 250 end 251 end 252 return nil 253 end 254 wrapper.next = wrapper.nextElement 255 256 wrapper.previousElement = function() 257 if not element.parent then return nil end 258 local siblings = element.parent.children 259 for i, child in ipairs(siblings) do 260 if child == element and i > 1 then 261 return create_element_wrapper(siblings[i - 1], document) 262 end 263 end 264 return nil 265 end 266 wrapper.previous = wrapper.previousElement 267 268 -- Modification 269 wrapper.remove = function() 270 if not element.parent then return false end 271 local siblings = element.parent.children 272 for i, child in ipairs(siblings) do 273 if child == element then 274 table.remove(siblings, i) 275 element.parent = nil 276 return true 277 end 278 end 279 return false 280 end 281 wrapper.delete = wrapper.remove 282 283 wrapper.appendChild = function(child_wrapper) 284 local child = child_wrapper._element 285 element:add_child(child) 286 end 287 wrapper.addChild = wrapper.appendChild 288 289 wrapper.insertBefore = function(new_child_wrapper, ref_child_wrapper) 290 local new_child = new_child_wrapper._element 291 local ref_child = ref_child_wrapper._element 292 293 for i, child in ipairs(element.children) do 294 if child == ref_child then 295 table.insert(element.children, i, new_child) 296 new_child.parent = element 297 return true 298 end 299 end 300 return false 301 end 302 303 -- Set content 304 wrapper.setContent = function(text) 305 element.content = text 306 end 307 308 return wrapper 309 end 310 311 function LuaEngine:new(document) 312 local obj = setmetatable({ 313 document = document, 314 sandbox = create_sandbox(), 315 scripts = {}, 316 }, self) 317 318 -- Add document API to sandbox 319 -- Use obj.document at runtime so it works even if document is set later 320 local querySelector_impl = function(selector) 321 if not obj.document then return nil end 322 local elem = query_selector(obj.document.dom.root, selector) 323 return elem and create_element_wrapper(elem, obj.document) or nil 324 end 325 326 local querySelectorAll_impl = function(selector) 327 if not obj.document then return {} end 328 local elems = query_selector_all(obj.document.dom.root, selector) 329 local wrapped = {} 330 for _, elem in ipairs(elems) do 331 table.insert(wrapped, create_element_wrapper(elem, obj.document)) 332 end 333 return wrapped 334 end 335 336 local getElementById_impl = function(self_or_id, id_if_method) 337 if not obj.document then return nil end 338 -- Handle both document:getElementById(id) and document.getElementById(id) syntax 339 local id = id_if_method or self_or_id 340 if type(id) ~= "string" and type(id) ~= "number" then 341 error("getElementById expects a string or number ID, got " .. type(id)) 342 end 343 local elem = query_selector(obj.document.dom.root, "#" .. tostring(id)) 344 return elem and create_element_wrapper(elem, obj.document) or nil 345 end 346 347 obj.sandbox.document = { 348 getElementById = getElementById_impl, 349 querySelector = querySelector_impl, 350 querySelectorAll = querySelectorAll_impl, 351 352 createElement = function(tag) 353 local dom_module = require("dom") 354 local Element = dom_module.Element 355 local elem = Element:new(tag, {}) 356 return create_element_wrapper(elem, obj.document) 357 end, 358 } 359 360 -- Shortcuts - point to the same function implementations 361 obj.sandbox.query = querySelector_impl 362 obj.sandbox.queryAll = querySelectorAll_impl 363 obj.sandbox.newEl = obj.sandbox.document.createElement 364 365 -- Do NOT provide _G access to prevent sandbox escape 366 -- obj.sandbox._G = nil (not needed, just don't set it) 367 368 return obj 369 end 370 371 function LuaEngine:run_script(script_code, script_name) 372 script_name = script_name or "script" 373 374 -- Load script in sandbox 375 local func, err = load(script_code, script_name, "t", self.sandbox) 376 377 if not func then 378 error("Script parse error in " .. script_name .. ": " .. err) 379 end 380 381 -- Run script with error handling 382 local success, result = pcall(func) 383 384 if not success then 385 error("Script runtime error in " .. script_name .. ": " .. result) 386 end 387 388 return result 389 end 390 391 function LuaEngine:add_script(script_code, script_name) 392 table.insert(self.scripts, { 393 code = script_code, 394 name = script_name or ("script_" .. #self.scripts + 1) 395 }) 396 end 397 398 function LuaEngine:run_all_scripts() 399 for _, script in ipairs(self.scripts) do 400 local success, err = pcall(function() 401 self:run_script(script.code, script.name) 402 end) 403 404 if not success then 405 print("Error in " .. script.name .. ": " .. err) 406 end 407 end 408 end 409 410 return { 411 LuaEngine = LuaEngine 412 }