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 }