luajitos

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

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 }