luajitos

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

document.lua (30566B)


      1 -- Document state manager
      2 -- Handles DOM, styles, and interactive state (focus, input values, etc)
      3 
      4 local Document = {}
      5 Document.__index = Document
      6 
      7 function Document:new(dom, style_manager, lua_engine)
      8     return setmetatable({
      9         dom = dom,
     10         style = style_manager,
     11         lua_engine = lua_engine,
     12 
     13         -- Interactive state
     14         focused_element = nil,
     15         input_values = {},  -- element -> current value
     16         cursor_positions = {},  -- element -> cursor position
     17         pressed_element = nil,  -- currently pressed button
     18         checked_elements = {},  -- element -> true/false for checkboxes/radios
     19         selected_options = {},  -- select element -> selected option element
     20     }, self)
     21 end
     22 
     23 -- Helper function to find parent form
     24 local function find_parent_form(element)
     25     local current = element.parent
     26     while current do
     27         if current.tag == "form" then
     28             return current
     29         end
     30         current = current.parent
     31     end
     32     return nil
     33 end
     34 
     35 function Document:click(x, y, keep_pressed)
     36     if not self.dom then return false end
     37 
     38     -- Find the element at this position
     39     local element = self.dom.root:find_element_at(x, y)
     40 
     41     if element then
     42         -- Handle button clicks
     43         if element.tag == "button" then
     44             -- Set pressed state for visual feedback
     45             self.pressed_element = element
     46 
     47             -- Check if button is inside a form
     48             local parent_form = find_parent_form(element)
     49             local button_type = element.attributes.type or "submit"
     50 
     51             -- Execute onclick handler if present
     52             if element.attributes.onclick and self.lua_engine then
     53                 local onclick_code = element.attributes.onclick
     54                 local success, err = pcall(function()
     55                     self.lua_engine:run_script(onclick_code, "onclick_" .. (element.id or "button"))
     56                 end)
     57 
     58                 if not success then
     59                     print("Error in onclick handler: " .. tostring(err))
     60                 end
     61             end
     62 
     63             -- If button is submit type and inside a form, trigger onsubmit
     64             if button_type == "submit" and parent_form and parent_form.attributes.onsubmit and self.lua_engine then
     65                 local onsubmit_code = parent_form.attributes.onsubmit
     66                 local success, err = pcall(function()
     67                     self.lua_engine:run_script(onsubmit_code, "onsubmit_" .. (parent_form.id or "form"))
     68                 end)
     69 
     70                 if not success then
     71                     print("Error in onsubmit handler: " .. tostring(err))
     72                 end
     73             end
     74 
     75             -- Clear pressed state after execution (unless keep_pressed is true)
     76             if not keep_pressed then
     77                 self.pressed_element = nil
     78             end
     79 
     80             return true
     81         end
     82 
     83         -- Handle anchor links
     84         if element.tag == "a" then
     85             local href = element.attributes.href
     86             if href then
     87                 -- Output navigation command
     88                 print("get " .. href)
     89             end
     90             return true
     91         end
     92 
     93         -- Handle input elements
     94         if element.tag == "input" then
     95             local input_type = element.attributes.type or "text"
     96 
     97             if input_type == "checkbox" then
     98                 -- Toggle checkbox
     99                 self.checked_elements[element] = not self.checked_elements[element]
    100 
    101                 -- Execute onclick handler if present
    102                 if element.attributes.onclick and self.lua_engine then
    103                     local onclick_code = element.attributes.onclick
    104                     local success, err = pcall(function()
    105                         self.lua_engine:run_script(onclick_code, "onclick_" .. (element.id or "checkbox"))
    106                     end)
    107                     if not success then
    108                         print("Error in onclick handler: " .. tostring(err))
    109                     end
    110                 end
    111 
    112                 return true
    113             elseif input_type == "radio" then
    114                 -- Get radio group name
    115                 local radio_name = element.attributes.name
    116 
    117                 if radio_name then
    118                     -- Uncheck all radios in the same group
    119                     local function uncheck_group(elem)
    120                         if elem.tag == "input" and
    121                            elem.attributes.type == "radio" and
    122                            elem.attributes.name == radio_name then
    123                             self.checked_elements[elem] = false
    124                         end
    125                         for _, child in ipairs(elem.children or {}) do
    126                             uncheck_group(child)
    127                         end
    128                     end
    129                     uncheck_group(self.dom.root)
    130                 end
    131 
    132                 -- Check this radio button
    133                 self.checked_elements[element] = true
    134 
    135                 -- Execute onclick handler if present
    136                 if element.attributes.onclick and self.lua_engine then
    137                     local onclick_code = element.attributes.onclick
    138                     local success, err = pcall(function()
    139                         self.lua_engine:run_script(onclick_code, "onclick_" .. (element.id or "radio"))
    140                     end)
    141                     if not success then
    142                         print("Error in onclick handler: " .. tostring(err))
    143                     end
    144                 end
    145 
    146                 return true
    147             else
    148                 -- Text input - handle focus
    149                 self.focused_element = element
    150 
    151                 -- Initialize value if not set
    152                 if not self.input_values[element] then
    153                     self.input_values[element] = element.attributes.value or ""
    154                 end
    155 
    156                 -- Set cursor to end of current value
    157                 self.cursor_positions[element] = #(self.input_values[element] or "")
    158 
    159                 return true
    160             end
    161         elseif element.tag == "textarea" then
    162             -- Handle textarea focus
    163             self.focused_element = element
    164 
    165             -- Initialize value if not set
    166             if not self.input_values[element] then
    167                 self.input_values[element] = element.content or ""
    168             end
    169 
    170             -- Set cursor to end of current value
    171             self.cursor_positions[element] = #(self.input_values[element] or "")
    172 
    173             return true
    174         elseif element.tag == "select" then
    175             -- Handle select click - cycle through options
    176             local options = {}
    177             for _, child in ipairs(element.children) do
    178                 if child.tag == "option" then
    179                     table.insert(options, child)
    180                 end
    181             end
    182 
    183             if #options > 0 then
    184                 -- Find current selection
    185                 local current_selection = self.selected_options[element]
    186                 local current_index = 0
    187 
    188                 for i, opt in ipairs(options) do
    189                     if opt == current_selection then
    190                         current_index = i
    191                         break
    192                     end
    193                 end
    194 
    195                 -- Cycle to next option
    196                 current_index = current_index + 1
    197                 if current_index > #options then
    198                     current_index = 1
    199                 end
    200 
    201                 self.selected_options[element] = options[current_index]
    202 
    203                 -- Execute onchange handler if present
    204                 if element.attributes.onchange and self.lua_engine then
    205                     local onchange_code = element.attributes.onchange
    206                     local success, err = pcall(function()
    207                         self.lua_engine:run_script(onchange_code, "onchange_" .. (element.id or "select"))
    208                     end)
    209                     if not success then
    210                         print("Error in onchange handler: " .. tostring(err))
    211                     end
    212                 end
    213 
    214                 return true
    215             end
    216         else
    217             -- Clicked on non-input, blur current focus
    218             self.focused_element = nil
    219         end
    220     end
    221 
    222     return false
    223 end
    224 
    225 function Document:type(text)
    226     if not self.focused_element then
    227         return false
    228     end
    229 
    230     if self.focused_element.tag == "input" or self.focused_element.tag == "textarea" then
    231         -- Get current value
    232         local current = self.input_values[self.focused_element] or ""
    233         local cursor_pos = self.cursor_positions[self.focused_element] or #current
    234 
    235         -- Insert text at cursor position
    236         local before = current:sub(1, cursor_pos)
    237         local after = current:sub(cursor_pos + 1)
    238         local new_value = before .. text .. after
    239 
    240         -- Update value and cursor
    241         self.input_values[self.focused_element] = new_value
    242         self.cursor_positions[self.focused_element] = cursor_pos + #text
    243 
    244         -- Execute oninput handler if present
    245         if self.focused_element.attributes.oninput and self.lua_engine then
    246             local oninput_code = self.focused_element.attributes.oninput
    247             local success, err = pcall(function()
    248                 self.lua_engine:run_script(oninput_code, "oninput_" .. (self.focused_element.id or "input"))
    249             end)
    250 
    251             if not success then
    252                 print("Error in oninput handler: " .. tostring(err))
    253             end
    254         end
    255 
    256         return true
    257     end
    258 
    259     return false
    260 end
    261 
    262 function Document:get_input_value(element)
    263     return self.input_values[element]
    264 end
    265 
    266 function Document:get_cursor_position(element)
    267     return self.cursor_positions[element]
    268 end
    269 
    270 function Document:is_focused(element)
    271     return self.focused_element == element
    272 end
    273 
    274 function Document:is_pressed(element)
    275     return self.pressed_element == element
    276 end
    277 
    278 function Document:is_checked(element)
    279     return self.checked_elements[element] == true
    280 end
    281 
    282 function Document:set_checked(element, checked)
    283     self.checked_elements[element] = checked
    284 end
    285 
    286 function Document:get_selected_option(select_element)
    287     return self.selected_options[select_element]
    288 end
    289 
    290 function Document:set_selected_option(select_element, option_element)
    291     self.selected_options[select_element] = option_element
    292 end
    293 
    294 -- CSS selector matcher (simplified - supports tag, #id, .class)
    295 local function matches_selector(element, selector)
    296     selector = selector:match("^%s*(.-)%s*$") -- trim
    297 
    298     -- Tag selector
    299     if selector:match("^[%w]+$") then
    300         return element.tag == selector
    301     end
    302 
    303     -- ID selector
    304     if selector:match("^#") then
    305         local id = selector:sub(2)
    306         return element.id == id
    307     end
    308 
    309     -- Class selector
    310     if selector:match("^%.") then
    311         local class = selector:sub(2)
    312         return element:has_class(class)
    313     end
    314 
    315     -- Tag with class
    316     local tag, class = selector:match("^([%w]+)%.([%w_-]+)$")
    317     if tag and class then
    318         return element.tag == tag and element:has_class(class)
    319     end
    320 
    321     -- Tag with id
    322     local tag2, id = selector:match("^([%w]+)#([%w_-]+)$")
    323     if tag2 and id then
    324         return element.tag == tag2 and element.id == id
    325     end
    326 
    327     return false
    328 end
    329 
    330 -- Query selector - find first matching element
    331 local function query_selector(root, selector)
    332     if matches_selector(root, selector) then
    333         return root
    334     end
    335 
    336     for _, child in ipairs(root.children) do
    337         local found = query_selector(child, selector)
    338         if found then
    339             return found
    340         end
    341     end
    342 
    343     return nil
    344 end
    345 
    346 -- Query selector all - find all matching elements
    347 local function query_selector_all(root, selector)
    348     local results = {}
    349 
    350     local function collect(elem)
    351         if matches_selector(elem, selector) then
    352             table.insert(results, elem)
    353         end
    354 
    355         for _, child in ipairs(elem.children) do
    356             collect(child)
    357         end
    358     end
    359 
    360     collect(root)
    361     return results
    362 end
    363 
    364 function Document:getElementById(id)
    365     if not self.dom or not self.dom.root then
    366         return nil
    367     end
    368     return query_selector(self.dom.root, "#" .. id)
    369 end
    370 
    371 function Document:querySelector(selector)
    372     if not self.dom or not self.dom.root then
    373         return nil
    374     end
    375     return query_selector(self.dom.root, selector)
    376 end
    377 
    378 function Document:query(selector)
    379     return self:querySelector(selector)
    380 end
    381 
    382 function Document:querySelectorAll(selector)
    383     if not self.dom or not self.dom.root then
    384         return {}
    385     end
    386     return query_selector_all(self.dom.root, selector)
    387 end
    388 
    389 function Document:queryAll(selector)
    390     return self:querySelectorAll(selector)
    391 end
    392 
    393 function Document:save_dom(filename, base_path)
    394     local file = io.open(filename, "w")
    395     if not file then
    396         error("Could not open file for writing: " .. filename)
    397     end
    398 
    399     -- Determine base path for resolving relative references
    400     base_path = base_path or ""
    401 
    402     local function serialize_element(elem, indent)
    403         indent = indent or 0
    404         local spaces = string.rep("  ", indent)
    405         local output = {}
    406 
    407         -- Special case: <link rel="stylesheet" href="..."> → <style>...</style>
    408         if elem.tag == "link" and elem.attributes.rel == "stylesheet" and elem.attributes.href then
    409             local css_path = base_path .. elem.attributes.href
    410             local css_file = io.open(css_path, "r")
    411             if css_file then
    412                 local css_content = css_file:read("*all")
    413                 css_file:close()
    414                 table.insert(output, spaces .. "<style>\n")
    415                 table.insert(output, css_content)
    416                 if not css_content:match("\n$") then
    417                     table.insert(output, "\n")
    418                 end
    419                 table.insert(output, spaces .. "</style>\n")
    420                 return table.concat(output)
    421             else
    422                 print("Warning: Could not load stylesheet: " .. css_path)
    423             end
    424         end
    425 
    426         -- Special case: <script src="..."> → <script type="text/lua">...</script>
    427         if elem.tag == "script" and elem.attributes.src then
    428             local script_path = base_path .. elem.attributes.src
    429             local script_file = io.open(script_path, "r")
    430             if script_file then
    431                 local script_content = script_file:read("*all")
    432                 script_file:close()
    433                 table.insert(output, spaces .. '<script type="text/lua">\n')
    434                 table.insert(output, script_content)
    435                 if not script_content:match("\n$") then
    436                     table.insert(output, "\n")
    437                 end
    438                 table.insert(output, spaces .. "</script>\n")
    439                 return table.concat(output)
    440             else
    441                 print("Warning: Could not load script: " .. script_path)
    442             end
    443         end
    444 
    445         table.insert(output, spaces .. "<" .. elem.tag)
    446 
    447         -- Attributes
    448         if elem.id then
    449             table.insert(output, ' id="' .. elem.id .. '"')
    450         end
    451         if #elem.classes > 0 then
    452             table.insert(output, ' class="' .. table.concat(elem.classes, " ") .. '"')
    453         end
    454         for key, value in pairs(elem.attributes) do
    455             if key ~= "id" and key ~= "class" then
    456                 -- Skip src for script tags (already inlined above)
    457                 if elem.tag == "script" and key == "src" then
    458                     -- Skip, already inlined
    459                 elseif key == "style" and type(value) == "table" then
    460                     -- Serialize style table as CSS
    461                     local style_parts = {}
    462                     for prop, val in pairs(value) do
    463                         table.insert(style_parts, prop .. ": " .. val)
    464                     end
    465                     if #style_parts > 0 then
    466                         table.insert(output, ' style="' .. table.concat(style_parts, "; ") .. '"')
    467                     end
    468                 else
    469                     table.insert(output, ' ' .. key .. '="' .. tostring(value) .. '"')
    470                 end
    471             end
    472         end
    473 
    474         table.insert(output, ">\n")
    475 
    476         -- Content
    477         if elem.content and elem.content ~= "" and elem.tag ~= "text" then
    478             table.insert(output, spaces .. "  " .. elem.content .. "\n")
    479         end
    480 
    481         -- Children
    482         for _, child in ipairs(elem.children) do
    483             if child.tag == "text" then
    484                 table.insert(output, spaces .. "  " .. child.content .. "\n")
    485             else
    486                 table.insert(output, serialize_element(child, indent + 1))
    487             end
    488         end
    489 
    490         table.insert(output, spaces .. "</" .. elem.tag .. ">\n")
    491 
    492         return table.concat(output)
    493     end
    494 
    495     if self.dom.root then
    496         -- Start with opening body tag
    497         file:write("<" .. self.dom.root.tag)
    498 
    499         -- Write body attributes if any
    500         if self.dom.root.id then
    501             file:write(' id="' .. self.dom.root.id .. '"')
    502         end
    503         if #self.dom.root.classes > 0 then
    504             file:write(' class="' .. table.concat(self.dom.root.classes, " ") .. '"')
    505         end
    506         for key, value in pairs(self.dom.root.attributes) do
    507             if key ~= "id" and key ~= "class" then
    508                 if key == "style" and type(value) == "table" then
    509                     local style_parts = {}
    510                     for prop, val in pairs(value) do
    511                         table.insert(style_parts, prop .. ": " .. val)
    512                     end
    513                     if #style_parts > 0 then
    514                         file:write(' style="' .. table.concat(style_parts, "; ") .. '"')
    515                     end
    516                 else
    517                     file:write(' ' .. key .. '="' .. tostring(value) .. '"')
    518                 end
    519             end
    520         end
    521         file:write(">\n")
    522 
    523         -- Inject external styles as <style> tags
    524         if self.external_styles and #self.external_styles > 0 then
    525             for _, css in ipairs(self.external_styles) do
    526                 file:write("  <style>\n")
    527                 file:write(css)
    528                 if not css:match("\n$") then
    529                     file:write("\n")
    530                 end
    531                 file:write("  </style>\n")
    532             end
    533         end
    534 
    535         -- Write children
    536         for _, child in ipairs(self.dom.root.children) do
    537             if child.tag == "text" then
    538                 file:write("  " .. child.content .. "\n")
    539             else
    540                 file:write(serialize_element(child, 1))
    541             end
    542         end
    543 
    544         -- Close body tag
    545         file:write("</" .. self.dom.root.tag .. ">\n")
    546     end
    547 
    548     file:close()
    549 end
    550 
    551 -- Serialize a value to Lua code
    552 local function serialize_value(val, indent, seen)
    553     indent = indent or ""
    554     seen = seen or {}
    555     local t = type(val)
    556 
    557     if t == "nil" then
    558         return "nil"
    559     elseif t == "boolean" or t == "number" then
    560         return tostring(val)
    561     elseif t == "string" then
    562         return string.format("%q", val)
    563     elseif t == "table" then
    564         -- Prevent infinite recursion
    565         if seen[val] then
    566             return "nil -- circular reference"
    567         end
    568         seen[val] = true
    569 
    570         local lines = {"{"}
    571         for k, v in pairs(val) do
    572             -- Skip parent references to avoid circular refs
    573             if k ~= "parent" then
    574                 local key_str
    575                 if type(k) == "string" and k:match("^[%a_][%w_]*$") then
    576                     key_str = k
    577                 else
    578                     key_str = "[" .. serialize_value(k, "", seen) .. "]"
    579                 end
    580                 table.insert(lines, indent .. "  " .. key_str .. " = " .. serialize_value(v, indent .. "  ", seen) .. ",")
    581             end
    582         end
    583         table.insert(lines, indent .. "}")
    584         return table.concat(lines, "\n")
    585     else
    586         return "nil -- unsupported type: " .. t
    587     end
    588 end
    589 
    590 -- Serialize DOM element tree
    591 local function serialize_dom_element(elem, indent)
    592     indent = indent or ""
    593     local lines = {"{"}
    594 
    595     -- Basic properties
    596     table.insert(lines, indent .. "  tag = " .. string.format("%q", elem.tag) .. ",")
    597 
    598     if elem.id then
    599         table.insert(lines, indent .. "  id = " .. string.format("%q", elem.id) .. ",")
    600     end
    601 
    602     if elem.content and elem.content ~= "" then
    603         table.insert(lines, indent .. "  content = " .. string.format("%q", elem.content) .. ",")
    604     end
    605 
    606     -- Classes
    607     if #elem.classes > 0 then
    608         local class_parts = {}
    609         for _, cls in ipairs(elem.classes) do
    610             table.insert(class_parts, string.format("%q", cls))
    611         end
    612         table.insert(lines, indent .. "  classes = {" .. table.concat(class_parts, ", ") .. "},")
    613     end
    614 
    615     -- Attributes
    616     if next(elem.attributes) then
    617         table.insert(lines, indent .. "  attributes = {")
    618         for key, value in pairs(elem.attributes) do
    619             if type(value) == "table" then
    620                 -- For style tables
    621                 table.insert(lines, indent .. "    " .. key .. " = " .. serialize_value(value, indent .. "    ") .. ",")
    622             else
    623                 table.insert(lines, indent .. "    " .. key .. " = " .. string.format("%q", tostring(value)) .. ",")
    624             end
    625         end
    626         table.insert(lines, indent .. "  },")
    627     end
    628 
    629     -- Children
    630     if #elem.children > 0 then
    631         table.insert(lines, indent .. "  children = {")
    632         for _, child in ipairs(elem.children) do
    633             table.insert(lines, indent .. "    " .. serialize_dom_element(child, indent .. "    ") .. ",")
    634         end
    635         table.insert(lines, indent .. "  },")
    636     end
    637 
    638     table.insert(lines, indent .. "}")
    639     return table.concat(lines, "\n")
    640 end
    641 
    642 function Document:saveState(state_name)
    643     local state_dir = "states"
    644     local state_file = state_dir .. "/" .. state_name .. ".lua"
    645 
    646     -- Create states directory if it doesn't exist
    647     os.execute("mkdir -p " .. state_dir)
    648 
    649     -- Build state table
    650     local state = {
    651         -- Save input values (element id -> value)
    652         input_values = {},
    653 
    654         -- Save focused element id
    655         focused_element_id = self.focused_element and self.focused_element.id or nil,
    656 
    657         -- Save cursor positions (element id -> position)
    658         cursor_positions = {},
    659 
    660         -- Save DOM content that was modified by scripts
    661         element_contents = {},  -- element id -> content
    662         element_styles = {},    -- element id -> style table
    663 
    664         -- Save checked state for checkboxes and radios
    665         checked_elements = {},  -- element id -> true/false
    666 
    667         -- Save selected options for select elements
    668         selected_options = {},  -- select element id -> option element id
    669 
    670         -- Save complete DOM tree
    671         dom = nil,  -- Will be set below
    672 
    673         -- Save style rules
    674         style_rules = {},
    675 
    676         -- Save external styles for inlining
    677         external_styles = self.external_styles or {},
    678 
    679         -- Save base path for resolving relative references
    680         base_path = self.base_path or "",
    681     }
    682 
    683     -- Save input values keyed by element ID
    684     for element, value in pairs(self.input_values) do
    685         if element.id then
    686             state.input_values[element.id] = value
    687         end
    688     end
    689 
    690     -- Save cursor positions keyed by element ID
    691     for element, pos in pairs(self.cursor_positions) do
    692         if element.id then
    693             state.cursor_positions[element.id] = pos
    694         end
    695     end
    696 
    697     -- Save checked state keyed by element ID
    698     for element, checked in pairs(self.checked_elements) do
    699         if element.id and checked then
    700             state.checked_elements[element.id] = checked
    701         end
    702     end
    703 
    704     -- Save selected options keyed by select element ID -> option index
    705     for select_elem, option_elem in pairs(self.selected_options) do
    706         if select_elem.id then
    707             -- Find the index of the selected option
    708             local option_index = 0
    709             for i, child in ipairs(select_elem.children) do
    710                 if child.tag == "option" then
    711                     option_index = option_index + 1
    712                     if child == option_elem then
    713                         state.selected_options[select_elem.id] = option_index
    714                         break
    715                     end
    716                 end
    717             end
    718         end
    719     end
    720 
    721     -- Walk DOM and save element content/styles that have IDs
    722     local function save_element_state(elem)
    723         if elem.id then
    724             -- Save content if it's not empty
    725             if elem.content and elem.content ~= "" then
    726                 state.element_contents[elem.id] = elem.content
    727             end
    728 
    729             -- Save text child content
    730             if #elem.children > 0 and elem.children[1].tag == "text" then
    731                 state.element_contents[elem.id] = elem.children[1].content
    732             end
    733 
    734             -- Save inline styles that were set by scripts
    735             if elem.attributes.style and type(elem.attributes.style) == "table" then
    736                 state.element_styles[elem.id] = {}
    737                 for prop, val in pairs(elem.attributes.style) do
    738                     state.element_styles[elem.id][prop] = val
    739                 end
    740             end
    741         end
    742 
    743         for _, child in ipairs(elem.children) do
    744             save_element_state(child)
    745         end
    746     end
    747 
    748     save_element_state(self.dom.root)
    749 
    750     -- Save DOM tree
    751     -- We need to manually write this since serialize_value can't handle the special DOM structure
    752     local dom_str = serialize_dom_element(self.dom.root, "  ")
    753 
    754     -- Save style rules
    755     if self.style and self.style.rules then
    756         for _, rule in ipairs(self.style.rules) do
    757             table.insert(state.style_rules, {
    758                 selector = rule.selector,
    759                 declarations = rule.declarations,
    760                 specificity = rule.specificity,
    761                 important_flags = rule.important_flags,
    762             })
    763         end
    764     end
    765 
    766     -- Write to file
    767     local file = io.open(state_file, "w")
    768     if not file then
    769         error("Could not create state file: " .. state_file)
    770     end
    771 
    772     file:write("-- MoonBrowser State: " .. state_name .. "\n")
    773     file:write("-- Saved: " .. os.date("%Y-%m-%d %H:%M:%S") .. "\n\n")
    774 
    775     -- Write state but replace dom placeholder with actual DOM tree
    776     local state_without_dom = {}
    777     for k, v in pairs(state) do
    778         if k ~= "dom" then
    779             state_without_dom[k] = v
    780         end
    781     end
    782 
    783     local state_str = serialize_value(state_without_dom, "")
    784 
    785     -- Insert DOM tree into the state string
    786     state_str = state_str:gsub("^{", "{\n  dom = " .. dom_str .. ",")
    787 
    788     file:write("return " .. state_str .. "\n")
    789     file:close()
    790 
    791     print("State saved to: " .. state_file)
    792 end
    793 
    794 function Document:resumeState(state_name)
    795     local state_file = "states/" .. state_name .. ".lua"
    796 
    797     -- Load state file
    798     local state_fn, err = loadfile(state_file)
    799     if not state_fn then
    800         error("Could not load state file: " .. state_file .. " - " .. tostring(err))
    801     end
    802 
    803     local state = state_fn()
    804     if not state then
    805         error("State file did not return a table: " .. state_file)
    806     end
    807 
    808     -- Reconstruct DOM if present
    809     if state.dom then
    810         local dom_module = require("dom")
    811         local Element = dom_module.Element
    812 
    813         local function reconstruct_element(elem_data)
    814             local elem = Element:new(elem_data.tag, elem_data.attributes or {})
    815             elem.id = elem_data.id
    816             elem.content = elem_data.content or ""
    817             elem.classes = elem_data.classes or {}
    818 
    819             -- Reconstruct children
    820             if elem_data.children then
    821                 for _, child_data in ipairs(elem_data.children) do
    822                     local child = reconstruct_element(child_data)
    823                     elem:add_child(child)
    824                 end
    825             end
    826 
    827             return elem
    828         end
    829 
    830         self.dom.root = reconstruct_element(state.dom)
    831     end
    832 
    833     -- Restore style rules if present
    834     if state.style_rules and #state.style_rules > 0 then
    835         local style_module = require("style")
    836         local CSSRule = style_module.CSSRule or {}
    837 
    838         -- Clear existing rules and reload
    839         self.style.rules = {}
    840         for _, rule_data in ipairs(state.style_rules) do
    841             local rule = {
    842                 selector = rule_data.selector,
    843                 declarations = rule_data.declarations,
    844                 specificity = rule_data.specificity,
    845                 important_flags = rule_data.important_flags,
    846             }
    847             table.insert(self.style.rules, rule)
    848         end
    849     end
    850 
    851     -- Restore external styles if present
    852     if state.external_styles then
    853         self.external_styles = state.external_styles
    854     end
    855 
    856     -- Find element by ID
    857     local function find_by_id(elem, id)
    858         if elem.id == id then return elem end
    859         for _, child in ipairs(elem.children) do
    860             local found = find_by_id(child, id)
    861             if found then return found end
    862         end
    863         return nil
    864     end
    865 
    866     -- Restore input values
    867     for elem_id, value in pairs(state.input_values or {}) do
    868         local elem = find_by_id(self.dom.root, elem_id)
    869         if elem then
    870             self.input_values[elem] = value
    871         end
    872     end
    873 
    874     -- Restore cursor positions
    875     for elem_id, pos in pairs(state.cursor_positions or {}) do
    876         local elem = find_by_id(self.dom.root, elem_id)
    877         if elem then
    878             self.cursor_positions[elem] = pos
    879         end
    880     end
    881 
    882     -- Restore focused element
    883     if state.focused_element_id then
    884         local elem = find_by_id(self.dom.root, state.focused_element_id)
    885         if elem then
    886             self.focused_element = elem
    887         end
    888     end
    889 
    890     -- Restore element contents
    891     for elem_id, content in pairs(state.element_contents or {}) do
    892         local elem = find_by_id(self.dom.root, elem_id)
    893         if elem then
    894             -- Set text child content if exists
    895             if #elem.children > 0 and elem.children[1].tag == "text" then
    896                 elem.children[1].content = content
    897             else
    898                 elem.content = content
    899             end
    900         end
    901     end
    902 
    903     -- Restore element styles
    904     for elem_id, styles in pairs(state.element_styles or {}) do
    905         local elem = find_by_id(self.dom.root, elem_id)
    906         if elem then
    907             if not elem.attributes.style then
    908                 elem.attributes.style = {}
    909             end
    910             for prop, val in pairs(styles) do
    911                 elem.attributes.style[prop] = val
    912             end
    913         end
    914     end
    915 
    916     print("State resumed from: " .. state_file)
    917 end
    918 
    919 return {
    920     Document = Document
    921 }