render.lua (30488B)
1 -- Simple HTML Renderer for Moonbrowser 2 -- Uses Application.lua draw operations 3 4 local Renderer = {} 5 Renderer.__index = Renderer 6 7 -- Default styles for HTML elements 8 local defaultStyles = { 9 h1 = { fontSize = 32, fontWeight = "bold", marginTop = 21, marginBottom = 21, display = "block" }, 10 h2 = { fontSize = 24, fontWeight = "bold", marginTop = 19, marginBottom = 19, display = "block" }, 11 h3 = { fontSize = 18, fontWeight = "bold", marginTop = 18, marginBottom = 18, display = "block" }, 12 h4 = { fontSize = 16, fontWeight = "bold", marginTop = 21, marginBottom = 21, display = "block" }, 13 h5 = { fontSize = 13, fontWeight = "bold", marginTop = 22, marginBottom = 22, display = "block" }, 14 h6 = { fontSize = 10, fontWeight = "bold", marginTop = 24, marginBottom = 24, display = "block" }, 15 p = { fontSize = 16, marginTop = 16, marginBottom = 16, display = "block" }, 16 div = { fontSize = 16, display = "block" }, 17 span = { fontSize = 16, display = "inline" }, 18 body = { fontSize = 16, display = "block" }, 19 html = { fontSize = 16, display = "block" }, 20 a = { fontSize = 16, color = 0x0000FF, display = "inline" }, 21 strong = { fontSize = 16, fontWeight = "bold", display = "inline" }, 22 b = { fontSize = 16, fontWeight = "bold", display = "inline" }, 23 em = { fontSize = 16, fontStyle = "italic", display = "inline" }, 24 i = { fontSize = 16, fontStyle = "italic", display = "inline" }, 25 ul = { fontSize = 16, marginTop = 16, marginBottom = 16, paddingLeft = 40, display = "block" }, 26 ol = { fontSize = 16, marginTop = 16, marginBottom = 16, paddingLeft = 40, display = "block" }, 27 li = { fontSize = 16, marginTop = 0, marginBottom = 0, display = "list-item" }, 28 br = { display = "block" }, 29 hr = { marginTop = 8, marginBottom = 8, display = "block" }, 30 pre = { fontSize = 13, fontFamily = "monospace", display = "block", marginTop = 16, marginBottom = 16 }, 31 code = { fontSize = 13, fontFamily = "monospace", display = "inline" }, 32 blockquote = { fontSize = 16, marginTop = 16, marginBottom = 16, paddingLeft = 40, display = "block" }, 33 button = { fontSize = 14, display = "inline", marginLeft = 4, marginRight = 4 }, 34 input = { fontSize = 14, display = "inline", marginLeft = 4, marginRight = 4 }, 35 form = { fontSize = 16, display = "block", marginTop = 8, marginBottom = 8 }, 36 label = { fontSize = 14, display = "inline", marginRight = 8 }, 37 textarea = { fontSize = 14, display = "block", marginTop = 4, marginBottom = 4 }, 38 select = { fontSize = 14, display = "inline", marginLeft = 4, marginRight = 4 }, 39 } 40 41 -- Get style property for an element 42 local function getStyle(elem, prop) 43 local tag = elem.tag and elem.tag:lower() or "div" 44 local styles = defaultStyles[tag] or defaultStyles.div 45 return styles[prop] 46 end 47 48 -- Get font size for element (with inheritance from parent) 49 local function getFontSize(elem) 50 local tag = elem.tag and elem.tag:lower() or "div" 51 local styles = defaultStyles[tag] 52 if styles and styles.fontSize then 53 return styles.fontSize 54 end 55 -- Default font size 56 return 16 57 end 58 59 -- Get font scale from fontSize (base font is 8 pixels) 60 local function getFontScale(fontSize) 61 local scale = math.floor(fontSize / 8) 62 if scale < 1 then scale = 1 end 63 if scale > 8 then scale = 8 end 64 return scale 65 end 66 67 -- Get line height based on font size 68 local function getLineHeight(fontSize) 69 -- Line height is scale * 8 * 1.25 for reasonable spacing 70 local scale = getFontScale(fontSize) 71 return math.floor(scale * 8 * 1.25) 72 end 73 74 -- Get character width based on font size 75 local function getCharWidth(fontSize) 76 -- Character width is scale * 8 77 local scale = getFontScale(fontSize) 78 return scale * 8 79 end 80 81 function Renderer.new(app, width, height) 82 local self = setmetatable({}, Renderer) 83 self.app = app 84 self.width = width or 800 85 self.height = height or 600 86 self.scrollY = 0 87 self.offsetY = 0 -- Vertical offset for toolbar etc. 88 self.contentHeight = 0 89 self.interactiveElements = {} -- Store buttons/inputs for click handling 90 self.inputValues = {} -- Store input field values 91 self.focusedInput = nil -- Currently focused input 92 self.inlineX = 0 -- Track X position for inline elements 93 self.inlineY = 0 -- Track Y position for current inline row 94 self.inlineHeight = 0 -- Track max height of current inline row 95 return self 96 end 97 98 -- Get input value 99 function Renderer:getInputValue(name) 100 return self.inputValues[name] or "" 101 end 102 103 -- Set input value 104 function Renderer:setInputValue(name, value) 105 self.inputValues[name] = value 106 end 107 108 function Renderer:render(dom, gfx) 109 if not dom then return end 110 111 -- Clear interactive elements for this frame 112 self.interactiveElements = {} 113 114 -- Clear background to white (with offset) 115 gfx:fillRect(0, self.offsetY, self.width, self.height, 0xFFFFFF) 116 117 -- Start rendering from root (with offset) 118 local y = 10 - self.scrollY + self.offsetY 119 y = self:renderElement(dom, gfx, 10, y, self.width - 20, 16) 120 121 -- Store content height for scrolling 122 self.contentHeight = y + self.scrollY 123 124 -- Debug: show how many interactive elements we found 125 if #self.interactiveElements > 0 and not self._debugPrinted then 126 print("Moonbrowser: Found " .. #self.interactiveElements .. " interactive elements") 127 for i, elem in ipairs(self.interactiveElements) do 128 print(" [" .. i .. "] " .. elem.type .. " at (" .. elem.x .. "," .. elem.y .. ") size " .. elem.width .. "x" .. elem.height) 129 end 130 self._debugPrinted = true 131 end 132 end 133 134 function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize) 135 if not elem then return y end 136 137 local tag = elem.tag and elem.tag:lower() or "" 138 139 -- Skip certain tags 140 if tag == "head" or tag == "script" or tag == "style" or tag == "title" then 141 return y 142 end 143 144 -- Get font size for this element 145 local fontSize = getFontSize(elem) 146 -- If element doesn't specify size, inherit from parent 147 if not (defaultStyles[tag] and defaultStyles[tag].fontSize) then 148 fontSize = inheritedFontSize or 16 149 end 150 151 local fontScale = getFontScale(fontSize) 152 local lineHeight = getLineHeight(fontSize) 153 local charWidth = getCharWidth(fontSize) 154 155 -- Get display type 156 local display = getStyle(elem, "display") or "block" 157 158 -- Reset inline flow when hitting a block element 159 if display == "block" and self.inlineX > 0 then 160 y = y + self.inlineHeight + 4 161 self.inlineX = 0 162 self.inlineHeight = 0 163 end 164 165 -- Apply top margin for block elements 166 local marginTop = getStyle(elem, "marginTop") or 0 167 if display == "block" and marginTop > 0 then 168 y = y + marginTop 169 end 170 171 -- Handle padding 172 local paddingLeft = getStyle(elem, "paddingLeft") or 0 173 local currentX = x + paddingLeft 174 local currentMaxWidth = maxWidth - paddingLeft 175 176 -- Render text content 177 if elem.text and #elem.text > 0 then 178 local text = elem.text:gsub("%s+", " ") -- Normalize whitespace 179 if #text > 0 then 180 -- Get text color 181 local color = getStyle(elem, "color") or 0x000000 182 183 -- Word wrap 184 local words = {} 185 for word in text:gmatch("%S+") do 186 table.insert(words, word) 187 end 188 189 local lineX = currentX 190 local lineWords = {} 191 192 for _, word in ipairs(words) do 193 local wordWidth = #word * charWidth 194 local spaceWidth = charWidth 195 196 -- Check if word fits on current line 197 local lineWidth = 0 198 for _, w in ipairs(lineWords) do 199 lineWidth = lineWidth + #w * charWidth + spaceWidth 200 end 201 202 if lineWidth + wordWidth > currentMaxWidth and #lineWords > 0 then 203 -- Draw current line 204 local lineText = table.concat(lineWords, " ") 205 if y >= -lineHeight and y < self.height then 206 gfx:drawText(lineX, y, lineText, color, fontScale) 207 end 208 y = y + lineHeight 209 lineWords = {word} 210 else 211 table.insert(lineWords, word) 212 end 213 end 214 215 -- Draw remaining words 216 if #lineWords > 0 then 217 local lineText = table.concat(lineWords, " ") 218 if y >= -lineHeight and y < self.height then 219 gfx:drawText(currentX, y, lineText, color, fontScale) 220 end 221 y = y + lineHeight 222 end 223 end 224 end 225 226 -- Handle special elements 227 if tag == "br" then 228 y = y + lineHeight 229 elseif tag == "hr" then 230 if y >= 0 and y < self.height then 231 gfx:fillRect(currentX, y + 4, currentMaxWidth, 1, 0x888888) 232 end 233 y = y + 10 234 elseif tag == "li" then 235 -- Draw bullet point (use same scale as text) 236 if y >= 0 and y < self.height then 237 gfx:drawText(x - charWidth * 2, y, "*", 0x000000, fontScale) 238 end 239 elseif tag == "a" then 240 -- Get link text from children or text 241 local linkText = elem.text or "" 242 if elem.children then 243 for _, child in ipairs(elem.children) do 244 if child.text then 245 linkText = linkText .. child.text 246 end 247 end 248 end 249 linkText = linkText:gsub("^%s+", ""):gsub("%s+$", "") 250 if linkText == "" then linkText = "Link" end 251 252 local href = elem.attributes and elem.attributes.href 253 254 local linkWidth = #linkText * charWidth 255 local linkHeight = lineHeight 256 257 if y >= -linkHeight and y < self.height then 258 -- Draw link text in blue with underline 259 gfx:drawText(currentX, y, linkText, 0x0000FF, fontScale) 260 -- Draw underline 261 gfx:fillRect(currentX, y + linkHeight - 2, linkWidth, 1, 0x0000FF) 262 263 -- Store for click handling (subtract offsetY since y includes it but clicks are relative to content) 264 table.insert(self.interactiveElements, { 265 type = "link", 266 x = currentX, 267 y = y + self.scrollY - self.offsetY, 268 width = linkWidth, 269 height = linkHeight, 270 text = linkText, 271 href = href, 272 elem = elem 273 }) 274 end 275 276 y = y + linkHeight 277 return y -- Don't render children for links (already got text) 278 279 elseif tag == "button" then 280 -- Get button text from children or text 281 local buttonText = elem.text or "" 282 if elem.children then 283 for _, child in ipairs(elem.children) do 284 if child.text then 285 buttonText = buttonText .. child.text 286 end 287 end 288 end 289 buttonText = buttonText:gsub("^%s+", ""):gsub("%s+$", "") 290 if buttonText == "" then buttonText = "Button" end 291 292 local btnWidth = #buttonText * 8 + 16 293 local btnHeight = 20 294 local btnMargin = 4 295 296 -- Use inline flow - check if we need to wrap to new line 297 if self.inlineX > 0 and self.inlineX + btnWidth + btnMargin > maxWidth then 298 -- Wrap to new line 299 y = y + self.inlineHeight + 4 300 self.inlineX = 0 301 self.inlineHeight = 0 302 end 303 304 local drawX = x + self.inlineX 305 local drawY = y 306 307 if drawY >= -btnHeight and drawY < self.height then 308 -- Button background (gradient effect) 309 gfx:fillRect(drawX, drawY, btnWidth, btnHeight, 0xE0E0E0) 310 -- Top/left highlight 311 gfx:fillRect(drawX, drawY, btnWidth, 1, 0xFFFFFF) 312 gfx:fillRect(drawX, drawY, 1, btnHeight, 0xFFFFFF) 313 -- Bottom/right shadow 314 gfx:fillRect(drawX, drawY + btnHeight - 1, btnWidth, 1, 0x808080) 315 gfx:fillRect(drawX + btnWidth - 1, drawY, 1, btnHeight, 0x808080) 316 -- Button text (centered) 317 gfx:drawText(drawX + 8, drawY + 6, buttonText, 0x000000) 318 319 -- Store for click handling (subtract offsetY since drawY includes it) 320 table.insert(self.interactiveElements, { 321 type = "button", 322 x = drawX, 323 y = drawY + self.scrollY - self.offsetY, 324 width = btnWidth, 325 height = btnHeight, 326 text = buttonText, 327 onclick = elem.attributes and elem.attributes.onclick, 328 elem = elem 329 }) 330 end 331 332 -- Advance inline position 333 self.inlineX = self.inlineX + btnWidth + btnMargin 334 if btnHeight > self.inlineHeight then 335 self.inlineHeight = btnHeight 336 end 337 338 return y -- Don't render children for button (already got text) 339 340 elseif tag == "input" then 341 local inputType = (elem.attributes and elem.attributes.type) or "text" 342 local inputName = (elem.attributes and elem.attributes.name) or ("input_" .. #self.interactiveElements) 343 local placeholder = (elem.attributes and elem.attributes.placeholder) or "" 344 local inputValue = (elem.attributes and elem.attributes.value) or self.inputValues[inputName] or "" 345 346 -- Initialize input value if not set 347 if not self.inputValues[inputName] then 348 self.inputValues[inputName] = inputValue 349 end 350 351 if inputType == "text" or inputType == "password" or inputType == "" then 352 local inputWidth = 150 353 local inputHeight = 20 354 355 if y >= -inputHeight and y < self.height then 356 local isFocused = (self.focusedInput == inputName) 357 358 -- Input background 359 gfx:fillRect(currentX, y, inputWidth, inputHeight, 0xFFFFFF) 360 -- Border (blue if focused) 361 local borderColor = isFocused and 0x0066CC or 0x888888 362 gfx:fillRect(currentX, y, inputWidth, 1, borderColor) 363 gfx:fillRect(currentX, y, 1, inputHeight, borderColor) 364 gfx:fillRect(currentX + inputWidth - 1, y, 1, inputHeight, borderColor) 365 gfx:fillRect(currentX, y + inputHeight - 1, inputWidth, 1, borderColor) 366 367 -- Display value or placeholder 368 local displayText = self.inputValues[inputName] or "" 369 if inputType == "password" then 370 displayText = string.rep("*", #displayText) 371 end 372 local textColor = 0x000000 373 if displayText == "" and placeholder ~= "" then 374 displayText = placeholder 375 textColor = 0x888888 376 end 377 -- Truncate if too long 378 local maxChars = math.floor((inputWidth - 8) / 8) 379 if #displayText > maxChars then 380 displayText = displayText:sub(-maxChars) 381 end 382 gfx:drawText(currentX + 4, y + 6, displayText, textColor) 383 384 -- Cursor if focused 385 if isFocused then 386 local cursorX = currentX + 4 + #(self.inputValues[inputName] or "") * 8 387 if cursorX < currentX + inputWidth - 4 then 388 gfx:fillRect(cursorX, y + 4, 1, 12, 0x000000) 389 end 390 end 391 392 -- Store for click handling (subtract offsetY) 393 table.insert(self.interactiveElements, { 394 type = "input", 395 inputType = inputType, 396 x = currentX, 397 y = y + self.scrollY - self.offsetY, 398 width = inputWidth, 399 height = inputHeight, 400 name = inputName, 401 elem = elem 402 }) 403 end 404 405 y = y + inputHeight + 4 406 407 elseif inputType == "submit" then 408 -- Submit button 409 local buttonText = (elem.attributes and elem.attributes.value) or "Submit" 410 local btnWidth = #buttonText * 8 + 16 411 local btnHeight = 20 412 413 if y >= -btnHeight and y < self.height then 414 -- Button background 415 gfx:fillRect(currentX, y, btnWidth, btnHeight, 0x4080FF) 416 -- Highlight 417 gfx:fillRect(currentX, y, btnWidth, 1, 0x60A0FF) 418 gfx:fillRect(currentX, y, 1, btnHeight, 0x60A0FF) 419 -- Shadow 420 gfx:fillRect(currentX, y + btnHeight - 1, btnWidth, 1, 0x2060CC) 421 gfx:fillRect(currentX + btnWidth - 1, y, 1, btnHeight, 0x2060CC) 422 -- Text 423 gfx:drawText(currentX + 8, y + 6, buttonText, 0xFFFFFF) 424 425 table.insert(self.interactiveElements, { 426 type = "submit", 427 x = currentX, 428 y = y + self.scrollY - self.offsetY, 429 width = btnWidth, 430 height = btnHeight, 431 text = buttonText, 432 elem = elem 433 }) 434 end 435 436 y = y + btnHeight + 4 437 438 elseif inputType == "checkbox" then 439 local checkSize = 14 440 441 -- Initialize checkbox if not already set 442 if self.inputValues[inputName] == nil then 443 -- Check for 'checked' attribute (boolean or string) 444 if elem.attributes and (elem.attributes.checked == true or elem.attributes.checked == "checked") then 445 self.inputValues[inputName] = "checked" 446 else 447 self.inputValues[inputName] = "" 448 end 449 end 450 451 if y >= -checkSize and y < self.height then 452 local isChecked = self.inputValues[inputName] == "checked" 453 454 -- Checkbox background 455 gfx:fillRect(currentX, y + 2, checkSize, checkSize, 0xFFFFFF) 456 -- Border 457 gfx:fillRect(currentX, y + 2, checkSize, 1, 0x888888) 458 gfx:fillRect(currentX, y + 2, 1, checkSize, 0x888888) 459 gfx:fillRect(currentX + checkSize - 1, y + 2, 1, checkSize, 0x888888) 460 gfx:fillRect(currentX, y + 2 + checkSize - 1, checkSize, 1, 0x888888) 461 462 -- Checkmark if checked 463 if isChecked then 464 gfx:drawText(currentX + 3, y + 2, "X", 0x000000) 465 end 466 467 table.insert(self.interactiveElements, { 468 type = "checkbox", 469 x = currentX, 470 y = y + 2 + self.scrollY - self.offsetY, 471 width = checkSize, 472 height = checkSize, 473 name = inputName, 474 elem = elem 475 }) 476 end 477 478 -- Don't add full line height, checkbox is inline 479 currentX = currentX + checkSize + 8 480 end 481 482 return y 483 484 elseif tag == "textarea" then 485 local inputName = (elem.attributes and elem.attributes.name) or ("textarea_" .. #self.interactiveElements) 486 local rows = tonumber(elem.attributes and elem.attributes.rows) or 4 487 local cols = tonumber(elem.attributes and elem.attributes.cols) or 40 488 489 local textareaWidth = cols * 8 + 8 490 local textareaHeight = rows * 12 + 8 491 492 -- Initialize value 493 if not self.inputValues[inputName] then 494 self.inputValues[inputName] = elem.text or "" 495 end 496 497 if y >= -textareaHeight and y < self.height then 498 local isFocused = (self.focusedInput == inputName) 499 500 -- Background 501 gfx:fillRect(currentX, y, textareaWidth, textareaHeight, 0xFFFFFF) 502 -- Border 503 local borderColor = isFocused and 0x0066CC or 0x888888 504 gfx:fillRect(currentX, y, textareaWidth, 1, borderColor) 505 gfx:fillRect(currentX, y, 1, textareaHeight, borderColor) 506 gfx:fillRect(currentX + textareaWidth - 1, y, 1, textareaHeight, borderColor) 507 gfx:fillRect(currentX, y + textareaHeight - 1, textareaWidth, 1, borderColor) 508 509 -- Text content 510 local textContent = self.inputValues[inputName] or "" 511 local textY = y + 4 512 for line in (textContent .. "\n"):gmatch("([^\n]*)\n") do 513 if textY < y + textareaHeight - 4 then 514 gfx:drawText(currentX + 4, textY, line:sub(1, cols), 0x000000) 515 textY = textY + 12 516 end 517 end 518 519 table.insert(self.interactiveElements, { 520 type = "textarea", 521 x = currentX, 522 y = y + self.scrollY - self.offsetY, 523 width = textareaWidth, 524 height = textareaHeight, 525 name = inputName, 526 elem = elem 527 }) 528 end 529 530 y = y + textareaHeight + 4 531 return y 532 end 533 534 -- Render children 535 if elem.children then 536 for _, child in ipairs(elem.children) do 537 y = self:renderElement(child, gfx, currentX, y, currentMaxWidth, fontSize) 538 end 539 end 540 541 -- Apply bottom margin for block elements 542 local marginBottom = getStyle(elem, "marginBottom") or 0 543 if display == "block" and marginBottom > 0 then 544 y = y + marginBottom 545 end 546 547 return y 548 end 549 550 function Renderer:scroll(delta) 551 self.scrollY = self.scrollY + delta 552 if self.scrollY < 0 then 553 self.scrollY = 0 554 end 555 local maxScroll = math.max(0, self.contentHeight - self.height + 50) 556 if self.scrollY > maxScroll then 557 self.scrollY = maxScroll 558 end 559 end 560 561 function Renderer:resetScroll() 562 self.scrollY = 0 563 self.focusedInput = nil 564 self.interactiveElements = {} 565 self.inputValues = {} 566 end 567 568 -- Handle click at position (x, y relative to content area) 569 function Renderer:handleClick(x, y) 570 -- Adjust y for scroll position 571 local absY = y + self.scrollY 572 573 for _, elem in ipairs(self.interactiveElements) do 574 if x >= elem.x and x < elem.x + elem.width and 575 absY >= elem.y and absY < elem.y + elem.height then 576 577 if elem.type == "input" or elem.type == "textarea" then 578 -- Focus this input 579 self.focusedInput = elem.name 580 return { type = "focus", name = elem.name } 581 582 elseif elem.type == "checkbox" then 583 -- Toggle checkbox 584 if self.inputValues[elem.name] == "checked" then 585 self.inputValues[elem.name] = "" 586 else 587 self.inputValues[elem.name] = "checked" 588 end 589 return { type = "toggle", name = elem.name, value = self.inputValues[elem.name] } 590 591 elseif elem.type == "button" then 592 return { type = "button", text = elem.text, onclick = elem.onclick, elem = elem.elem } 593 594 elseif elem.type == "submit" then 595 -- Collect form data 596 local formData = {} 597 for name, value in pairs(self.inputValues) do 598 formData[name] = value 599 end 600 return { type = "submit", text = elem.text, formData = formData } 601 602 elseif elem.type == "link" then 603 return { type = "link", text = elem.text, href = elem.href, elem = elem.elem } 604 end 605 end 606 end 607 608 -- Click outside any element - unfocus 609 if self.focusedInput then 610 self.focusedInput = nil 611 return { type = "unfocus" } 612 end 613 614 return nil 615 end 616 617 -- Handle keyboard input 618 function Renderer:handleKey(key, scancode) 619 if not self.focusedInput then 620 return false 621 end 622 623 local name = self.focusedInput 624 625 if key == "\b" then 626 -- Backspace 627 local val = self.inputValues[name] or "" 628 if #val > 0 then 629 self.inputValues[name] = val:sub(1, -2) 630 end 631 return true 632 elseif key == "\n" then 633 -- Enter - unfocus (or submit form) 634 self.focusedInput = nil 635 return true 636 elseif key == "\t" then 637 -- Tab - unfocus 638 self.focusedInput = nil 639 return true 640 elseif scancode == 1 then 641 -- Escape - unfocus 642 self.focusedInput = nil 643 return true 644 elseif key and #key == 1 and key:byte() >= 32 then 645 -- Printable character 646 self.inputValues[name] = (self.inputValues[name] or "") .. key 647 return true 648 end 649 650 return false 651 end 652 653 -- Check if an input is focused 654 function Renderer:hasFocus() 655 return self.focusedInput ~= nil 656 end 657 658 -- Create a proxy object for a DOM element that allows .value access 659 local function createElementProxy(renderer, elem) 660 if not elem then return nil end 661 662 local proxy = {} 663 local mt = { 664 __index = function(t, key) 665 if key == "value" then 666 -- For input/textarea, get from inputValues 667 local name = elem.attributes and elem.attributes.name 668 if name and renderer.inputValues[name] ~= nil then 669 return renderer.inputValues[name] 670 end 671 -- Fallback to attribute value or text content 672 if elem.attributes and elem.attributes.value then 673 return elem.attributes.value 674 end 675 return elem.text or "" 676 677 elseif key == "checked" then 678 local name = elem.attributes and elem.attributes.name 679 if name then 680 return renderer.inputValues[name] == "checked" 681 end 682 return elem.attributes and (elem.attributes.checked == true or elem.attributes.checked == "checked") 683 684 elseif key == "id" then 685 return elem.id 686 687 elseif key == "className" or key == "class" then 688 return elem.className 689 690 elseif key == "tagName" or key == "tag" then 691 return elem.tag 692 693 elseif key == "name" then 694 return elem.attributes and elem.attributes.name 695 696 elseif key == "text" or key == "textContent" or key == "innerText" then 697 return elem.text or "" 698 699 elseif key == "innerHTML" then 700 -- Simple innerHTML - just return text for now 701 return elem.text or "" 702 703 elseif key == "children" then 704 -- Return proxied children 705 local proxiedChildren = {} 706 for i, child in ipairs(elem.children or {}) do 707 proxiedChildren[i] = createElementProxy(renderer, child) 708 end 709 return proxiedChildren 710 711 elseif key == "parentNode" or key == "parent" then 712 return createElementProxy(renderer, elem.parent) 713 714 elseif key == "getAttribute" then 715 return function(_, attrName) 716 return elem.attributes and elem.attributes[attrName] 717 end 718 719 elseif key == "setAttribute" then 720 return function(_, attrName, attrValue) 721 if elem.attributes then 722 elem.attributes[attrName] = attrValue 723 end 724 end 725 726 elseif key == "querySelector" then 727 return function(_, selector) 728 local found = elem:querySelector(selector) 729 return createElementProxy(renderer, found) 730 end 731 732 elseif key == "querySelectorAll" then 733 return function(_, selector) 734 local found = elem:querySelectorAll(selector) 735 local proxied = {} 736 for i, e in ipairs(found) do 737 proxied[i] = createElementProxy(renderer, e) 738 end 739 return proxied 740 end 741 742 elseif key == "_element" then 743 -- Access to raw element (for debugging) 744 return elem 745 746 else 747 -- Try attributes 748 if elem.attributes and elem.attributes[key] ~= nil then 749 return elem.attributes[key] 750 end 751 end 752 753 return nil 754 end, 755 756 __newindex = function(t, key, value) 757 if key == "value" then 758 -- For input/textarea, set in inputValues 759 local name = elem.attributes and elem.attributes.name 760 if name then 761 renderer.inputValues[name] = tostring(value) 762 else 763 -- Set attribute if no name 764 if elem.attributes then 765 elem.attributes.value = tostring(value) 766 end 767 end 768 769 elseif key == "checked" then 770 local name = elem.attributes and elem.attributes.name 771 if name then 772 renderer.inputValues[name] = value and "checked" or "" 773 end 774 775 elseif key == "text" or key == "textContent" or key == "innerText" then 776 elem.text = tostring(value) 777 778 elseif key == "innerHTML" then 779 -- Simple innerHTML - just set text 780 elem.text = tostring(value) 781 782 elseif key == "className" or key == "class" then 783 elem.className = tostring(value) 784 785 else 786 -- Set as attribute 787 if elem.attributes then 788 elem.attributes[key] = value 789 end 790 end 791 end 792 } 793 794 setmetatable(proxy, mt) 795 return proxy 796 end 797 798 -- Query the DOM like document.querySelector 799 -- Supports: #id, .class, tagname, tag#id, tag.class, [attr], [attr=value], [name=value] 800 function Renderer:query(selector) 801 if not self.dom then return nil end 802 local elem = self.dom:querySelector(selector) 803 return createElementProxy(self, elem) 804 end 805 806 -- Query all matching elements 807 function Renderer:queryAll(selector) 808 if not self.dom then return {} end 809 local elems = self.dom:querySelectorAll(selector) 810 local proxied = {} 811 for i, elem in ipairs(elems) do 812 proxied[i] = createElementProxy(self, elem) 813 end 814 return proxied 815 end 816 817 -- Store DOM reference for queries 818 function Renderer:setDOM(dom) 819 self.dom = dom 820 end 821 822 return Renderer