luajitos

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

HTMLWindow.lua (20713B)


      1 -- HTMLWindow.lua - HTML rendering window for apps
      2 -- Allows any app to create an HTML-based window using their own context
      3 
      4 local HTMLWindow = {}
      5 
      6 -- Load parser and renderer from moonbrowser
      7 local parserModule = nil
      8 local rendererModule = nil
      9 
     10 local function loadModules()
     11     if parserModule and rendererModule then
     12         return true
     13     end
     14 
     15     -- Load parser
     16     local parserPath = "/apps/com.luajitos.moonbrowser/src/parser.luac"
     17     local parserCode = nil
     18 
     19     if CRamdiskExists and CRamdiskExists(parserPath) then
     20         local handle = CRamdiskOpen(parserPath, "r")
     21         if handle then
     22             parserCode = CRamdiskRead(handle)
     23             CRamdiskClose(handle)
     24         end
     25     end
     26 
     27     if not parserCode then
     28         -- Try .lua version
     29         parserPath = "/apps/com.luajitos.moonbrowser/src/parser.lua"
     30         if CRamdiskExists and CRamdiskExists(parserPath) then
     31             local handle = CRamdiskOpen(parserPath, "r")
     32             if handle then
     33                 parserCode = CRamdiskRead(handle)
     34                 CRamdiskClose(handle)
     35             end
     36         end
     37     end
     38 
     39     if not parserCode then
     40         return false, "Could not load HTML parser"
     41     end
     42 
     43     -- Load renderer
     44     local renderPath = "/apps/com.luajitos.moonbrowser/src/render.luac"
     45     local renderCode = nil
     46 
     47     if CRamdiskExists and CRamdiskExists(renderPath) then
     48         local handle = CRamdiskOpen(renderPath, "r")
     49         if handle then
     50             renderCode = CRamdiskRead(handle)
     51             CRamdiskClose(handle)
     52         end
     53     end
     54 
     55     if not renderCode then
     56         -- Try .lua version
     57         renderPath = "/apps/com.luajitos.moonbrowser/src/render.lua"
     58         if CRamdiskExists and CRamdiskExists(renderPath) then
     59             local handle = CRamdiskOpen(renderPath, "r")
     60             if handle then
     61                 renderCode = CRamdiskRead(handle)
     62                 CRamdiskClose(handle)
     63             end
     64         end
     65     end
     66 
     67     if not renderCode then
     68         return false, "Could not load HTML renderer"
     69     end
     70 
     71     -- Load DOM module (required by parser)
     72     local domPath = "/apps/com.luajitos.moonbrowser/src/dom.luac"
     73     local domCode = nil
     74 
     75     if CRamdiskExists and CRamdiskExists(domPath) then
     76         local handle = CRamdiskOpen(domPath, "r")
     77         if handle then
     78             domCode = CRamdiskRead(handle)
     79             CRamdiskClose(handle)
     80         end
     81     end
     82 
     83     if not domCode then
     84         domPath = "/apps/com.luajitos.moonbrowser/src/dom.lua"
     85         if CRamdiskExists and CRamdiskExists(domPath) then
     86             local handle = CRamdiskOpen(domPath, "r")
     87             if handle then
     88                 domCode = CRamdiskRead(handle)
     89                 CRamdiskClose(handle)
     90             end
     91         end
     92     end
     93 
     94     if not domCode then
     95         return false, "Could not load DOM module"
     96     end
     97 
     98     -- Compile and execute DOM module
     99     local domEnv = setmetatable({}, { __index = _G, __metatable = false })
    100     local domFunc, domErr = loadstring(domCode, "dom")
    101     if not domFunc then
    102         return false, "Failed to compile DOM: " .. tostring(domErr)
    103     end
    104     setfenv(domFunc, domEnv)
    105     local domOk, domModule = pcall(domFunc)
    106     if not domOk then
    107         return false, "Failed to execute DOM: " .. tostring(domModule)
    108     end
    109 
    110     -- Compile and execute parser with DOM available
    111     local parserEnv = setmetatable({
    112         require = function(name)
    113             if name == "dom" then
    114                 return domModule
    115             end
    116             return nil
    117         end
    118     }, { __index = _G, __metatable = false })
    119 
    120     local parserFunc, parserErr = loadstring(parserCode, "parser")
    121     if not parserFunc then
    122         return false, "Failed to compile parser: " .. tostring(parserErr)
    123     end
    124     setfenv(parserFunc, parserEnv)
    125     local parserOk, parserResult = pcall(parserFunc)
    126     if not parserOk then
    127         return false, "Failed to execute parser: " .. tostring(parserResult)
    128     end
    129     parserModule = parserResult
    130 
    131     -- Compile and execute renderer
    132     local renderEnv = setmetatable({}, { __index = _G, __metatable = false })
    133     local renderFunc, renderErr = loadstring(renderCode, "render")
    134     if not renderFunc then
    135         return false, "Failed to compile renderer: " .. tostring(renderErr)
    136     end
    137     setfenv(renderFunc, renderEnv)
    138     local renderOk, renderResult = pcall(renderFunc)
    139     if not renderOk then
    140         return false, "Failed to execute renderer: " .. tostring(renderResult)
    141     end
    142     rendererModule = renderResult
    143 
    144     return true
    145 end
    146 
    147 --- Create a new HTML window
    148 -- @param options Table with:
    149 --   - app: Application instance (required)
    150 --   - html: HTML string to render (either html or file required)
    151 --   - file: Path to HTML file (either html or file required)
    152 --   - fs: SafeFS instance for file access (required if using file)
    153 --   - width: Window width (default 800)
    154 --   - height: Window height (default 600)
    155 --   - title: Window title (default "HTML Window")
    156 --   - Dialog: Dialog module for alert/confirm/prompt
    157 --   - loadstring: loadstring function for onclick handlers
    158 --   - showStatusBar: Show status bar (default false)
    159 -- @return window object or nil, error
    160 function HTMLWindow.new(options)
    161     if not options then
    162         return nil, "Options required"
    163     end
    164 
    165     local app = options.app
    166     if not app then
    167         return nil, "app instance required"
    168     end
    169 
    170     -- Load modules if needed
    171     local loadOk, loadErr = loadModules()
    172     if not loadOk then
    173         return nil, loadErr
    174     end
    175 
    176     local htmlContent = options.html
    177     local filePath = options.file
    178 
    179     -- Read file if provided
    180     if not htmlContent and filePath then
    181         local fs = options.fs
    182         if fs then
    183             if fs:exists(filePath) then
    184                 htmlContent = fs:read(filePath)
    185             else
    186                 return nil, "File not found: " .. filePath
    187             end
    188         else
    189             -- Try direct ramdisk access
    190             if CRamdiskExists and CRamdiskExists(filePath) then
    191                 local handle = CRamdiskOpen(filePath, "r")
    192                 if handle then
    193                     htmlContent = CRamdiskRead(handle)
    194                     CRamdiskClose(handle)
    195                 end
    196             end
    197         end
    198     end
    199 
    200     if not htmlContent then
    201         return nil, "No HTML content provided"
    202     end
    203 
    204     -- Store base path for relative URLs
    205     local basePath = filePath and filePath:match("(.*/)[^/]*$") or "/"
    206 
    207     -- Helper to resolve relative paths
    208     local function resolvePath(href)
    209         if not href then return nil end
    210         if href:sub(1, 1) == "/" then
    211             return href  -- Absolute path
    212         elseif href:sub(1, 2) == "./" then
    213             return basePath .. href:sub(3)
    214         elseif href:sub(1, 3) == "../" then
    215             local parent = basePath:match("(.*/).-/$") or "/"
    216             return parent .. href:sub(4)
    217         else
    218             return basePath .. href
    219         end
    220     end
    221 
    222     -- Helper to read a file
    223     local function readFile(path)
    224         local fs = options.fs
    225         if fs then
    226             if fs:exists(path) then
    227                 return fs:read(path)
    228             end
    229         end
    230         -- Try direct ramdisk access
    231         if CRamdiskExists and CRamdiskExists(path) then
    232             local handle = CRamdiskOpen(path, "r")
    233             if handle then
    234                 local content = CRamdiskRead(handle)
    235                 CRamdiskClose(handle)
    236                 return content
    237             end
    238         end
    239         return nil
    240     end
    241 
    242     -- Parse HTML
    243     local Parser = parserModule.Parser
    244     local htmlParser = Parser.new()
    245     local dom = htmlParser:parse(htmlContent)
    246 
    247     if not dom then
    248         return nil, "Failed to parse HTML"
    249     end
    250 
    251     -- Collected styles from <link> and <style> tags
    252     local collectedStyles = {}
    253 
    254     -- Collected scripts to execute after page load
    255     local collectedScripts = {}
    256 
    257     -- Process <head> for stylesheets and scripts
    258     local headStart = htmlContent:find("<head")
    259     local headEnd = htmlContent:find("</head>")
    260     if headStart and headEnd then
    261         local headContent = htmlContent:sub(headStart, headEnd)
    262 
    263         -- Find <link rel="stylesheet"> tags
    264         for href in headContent:gmatch('<link[^>]+rel%s*=%s*["\']?stylesheet["\']?[^>]+href%s*=%s*["\']([^"\']+)["\']') do
    265             local stylePath = resolvePath(href)
    266             if stylePath then
    267                 local styleContent = readFile(stylePath)
    268                 if styleContent then
    269                     table.insert(collectedStyles, styleContent)
    270                 end
    271             end
    272         end
    273 
    274         -- Also check href before rel
    275         for href in headContent:gmatch('<link[^>]+href%s*=%s*["\']([^"\']+)["\'][^>]+rel%s*=%s*["\']?stylesheet["\']?') do
    276             local stylePath = resolvePath(href)
    277             if stylePath then
    278                 local styleContent = readFile(stylePath)
    279                 if styleContent then
    280                     table.insert(collectedStyles, styleContent)
    281                 end
    282             end
    283         end
    284 
    285         -- Find <script src="..."> tags in head
    286         for src in headContent:gmatch('<script[^>]+src%s*=%s*["\']([^"\']+)["\']') do
    287             local scriptPath = resolvePath(src)
    288             if scriptPath then
    289                 local scriptContent = readFile(scriptPath)
    290                 if scriptContent then
    291                     table.insert(collectedScripts, { type = "file", path = scriptPath, content = scriptContent })
    292                 end
    293             end
    294         end
    295     end
    296 
    297     -- Process <style> tags anywhere in the document
    298     for styleContent in htmlContent:gmatch("<style[^>]*>(.-)</style>") do
    299         table.insert(collectedStyles, styleContent)
    300     end
    301 
    302     -- Process <script> tags in body (both inline and src)
    303     local bodyStart = htmlContent:find("<body") or 1
    304     local bodyContent = htmlContent:sub(bodyStart)
    305 
    306     -- Find <script src="..."> tags
    307     for src in bodyContent:gmatch('<script[^>]+src%s*=%s*["\']([^"\']+)["\'][^>]*>') do
    308         local scriptPath = resolvePath(src)
    309         if scriptPath then
    310             local scriptContent = readFile(scriptPath)
    311             if scriptContent then
    312                 table.insert(collectedScripts, { type = "file", path = scriptPath, content = scriptContent })
    313             end
    314         end
    315     end
    316 
    317     -- Find inline <script> tags (without src attribute)
    318     for scriptContent in bodyContent:gmatch("<script>(.-)") do
    319         local endTag = scriptContent:find("</script>")
    320         if endTag then
    321             scriptContent = scriptContent:sub(1, endTag - 1)
    322         end
    323         if scriptContent and scriptContent:match("%S") then
    324             table.insert(collectedScripts, { type = "inline", content = scriptContent })
    325         end
    326     end
    327 
    328     -- Create window
    329     local width = options.width or 800
    330     local height = options.height or 600
    331     local title = options.title or "HTML Window"
    332     local showStatusBar = options.showStatusBar or false
    333 
    334     local window = app:newWindow(title, width, height, true)
    335     if not window then
    336         return nil, "Failed to create window"
    337     end
    338 
    339     -- Create renderer
    340     local renderer = rendererModule.new(nil, width, height)
    341     renderer:setDOM(dom)
    342 
    343     -- Get Dialog and loadstring from options (app's context)
    344     local Dialog = options.Dialog
    345     local appLoadstring = options.loadstring
    346 
    347     -- Create page sandbox for this window
    348     local function queryFunc(selector)
    349         return renderer:query(selector)
    350     end
    351 
    352     local function queryAllFunc(selector)
    353         return renderer:queryAll(selector)
    354     end
    355 
    356     local pageSandbox = {
    357         -- DOM query functions
    358         query = queryFunc,
    359         queryAll = queryAllFunc,
    360         document = {
    361             querySelector = queryFunc,
    362             querySelectorAll = queryAllFunc,
    363             getElementById = function(id)
    364                 return renderer:query("#" .. id)
    365             end,
    366         },
    367 
    368         -- Dialog functions using app's context
    369         alert = function(msg)
    370             if Dialog and Dialog.alert then
    371                 Dialog.alert(tostring(msg), { app = app })
    372             else
    373                 print("ALERT: " .. tostring(msg))
    374             end
    375         end,
    376         confirm = function(msg)
    377             if Dialog and Dialog.confirm then
    378                 return Dialog.confirm(tostring(msg), { app = app })
    379             else
    380                 print("CONFIRM: " .. tostring(msg))
    381                 return true
    382             end
    383         end,
    384         prompt = function(msg, default)
    385             if Dialog and Dialog.prompt then
    386                 return Dialog.prompt(tostring(msg), { app = app, default = default })
    387             else
    388                 print("PROMPT: " .. tostring(msg))
    389                 return default or ""
    390             end
    391         end,
    392 
    393         -- Console
    394         console = {
    395             log = function(...)
    396                 local args = {...}
    397                 local parts = {}
    398                 for i = 1, #args do
    399                     parts[i] = tostring(args[i])
    400                 end
    401                 print("[console] " .. table.concat(parts, " "))
    402             end,
    403             error = function(...)
    404                 local args = {...}
    405                 local parts = {}
    406                 for i = 1, #args do
    407                     parts[i] = tostring(args[i])
    408                 end
    409                 print("[console.error] " .. table.concat(parts, " "))
    410             end,
    411             warn = function(...)
    412                 local args = {...}
    413                 local parts = {}
    414                 for i = 1, #args do
    415                     parts[i] = tostring(args[i])
    416                 end
    417                 print("[console.warn] " .. table.concat(parts, " "))
    418             end,
    419         },
    420 
    421         -- Basic Lua functions
    422         print = print,
    423         tostring = tostring,
    424         tonumber = tonumber,
    425         type = type,
    426         pairs = pairs,
    427         ipairs = ipairs,
    428         string = string,
    429         table = table,
    430         math = math,
    431     }
    432 
    433     -- Function to execute code in page sandbox
    434     local function executeInSandbox(code, chunkName)
    435         if not code or code == "" then return end
    436 
    437         if not appLoadstring then
    438             print("HTMLWindow: Cannot execute script - loadstring not available")
    439             return
    440         end
    441 
    442         local func, err = appLoadstring(code, chunkName or "script", pageSandbox)
    443         if not func then
    444             print("HTMLWindow: Script error: " .. tostring(err))
    445             return
    446         end
    447 
    448         local ok, result = pcall(func)
    449         if not ok then
    450             print("HTMLWindow: Script runtime error: " .. tostring(result))
    451         end
    452         return result
    453     end
    454 
    455     -- Forward declaration for navigation
    456     local htmlWindow
    457     local currentFilePath = filePath
    458 
    459     -- Function to navigate to a new page
    460     local function navigateTo(href)
    461         if not href then return false end
    462 
    463         local targetPath = resolvePath(href)
    464         if not targetPath then
    465             print("HTMLWindow: Invalid href: " .. tostring(href))
    466             return false
    467         end
    468 
    469         -- Read new file
    470         local newContent = readFile(targetPath)
    471         if not newContent then
    472             print("HTMLWindow: File not found: " .. targetPath)
    473             return false
    474         end
    475 
    476         -- Update base path
    477         basePath = targetPath:match("(.*/)[^/]*$") or "/"
    478         currentFilePath = targetPath
    479 
    480         -- Parse new HTML
    481         local newDom = htmlParser:parse(newContent)
    482         if not newDom then
    483             print("HTMLWindow: Failed to parse: " .. targetPath)
    484             return false
    485         end
    486 
    487         -- Process scripts from new page
    488         collectedScripts = {}
    489 
    490         -- Find inline <script> tags in new content
    491         for scriptContent in newContent:gmatch("<script>(.-)") do
    492             local endTag = scriptContent:find("</script>")
    493             if endTag then
    494                 scriptContent = scriptContent:sub(1, endTag - 1)
    495             end
    496             if scriptContent and scriptContent:match("%S") then
    497                 table.insert(collectedScripts, { type = "inline", content = scriptContent })
    498             end
    499         end
    500 
    501         -- Find <script src="..."> tags
    502         for src in newContent:gmatch('<script[^>]+src%s*=%s*["\']([^"\']+)["\'][^>]*>') do
    503             local scriptPath = resolvePath(src)
    504             if scriptPath then
    505                 local scriptFileContent = readFile(scriptPath)
    506                 if scriptFileContent then
    507                     table.insert(collectedScripts, { type = "file", path = scriptPath, content = scriptFileContent })
    508                 end
    509             end
    510         end
    511 
    512         -- Update DOM and renderer
    513         dom = newDom
    514         renderer:setDOM(dom)
    515         renderer:scroll(-renderer.scrollY)  -- Reset scroll
    516         renderer.inputValues = {}  -- Reset form values
    517         renderer.focusedInput = nil
    518 
    519         -- Update window title
    520         window.title = title .. " - " .. targetPath
    521 
    522         -- Execute scripts
    523         for _, script in ipairs(collectedScripts) do
    524             executeInSandbox(script.content, script.path or "inline")
    525         end
    526 
    527         window:markDirty()
    528         return true
    529     end
    530 
    531     -- Add navigation to page sandbox
    532     pageSandbox.location = {
    533         href = currentFilePath,
    534         navigate = navigateTo,
    535         reload = function()
    536             if currentFilePath then
    537                 navigateTo(currentFilePath:match("[^/]+$"))
    538             end
    539         end,
    540     }
    541 
    542     pageSandbox.window = {
    543         location = pageSandbox.location,
    544         navigate = navigateTo,
    545     }
    546 
    547     -- Draw callback
    548     window.onDraw = function(gfx)
    549         renderer:render(dom, gfx)
    550 
    551         if showStatusBar and filePath then
    552             gfx:fillRect(0, height - 20, width, 20, 0x333333)
    553             gfx:drawText(5, height - 16, filePath, 0xFFFFFF)
    554         end
    555     end
    556 
    557     -- Input callback
    558     window.onInput = function(key, scancode)
    559         if renderer:hasFocus() then
    560             if renderer:handleKey(key, scancode) then
    561                 window:markDirty()
    562                 return
    563             end
    564         end
    565 
    566         -- Arrow key scrolling
    567         if scancode == 72 then  -- Up arrow
    568             renderer:scroll(-40)
    569             window:markDirty()
    570         elseif scancode == 80 then  -- Down arrow
    571             renderer:scroll(40)
    572             window:markDirty()
    573         end
    574     end
    575 
    576     -- Click callback
    577     window.onClick = function(x, y, button)
    578         local result = renderer:handleClick(x, y)
    579         if result then
    580             if result.type == "button" then
    581                 if result.onclick then
    582                     executeInSandbox(result.onclick)
    583                 end
    584             elseif result.type == "submit" then
    585                 -- Form submitted - could trigger onSubmit callback
    586                 if options.onSubmit then
    587                     options.onSubmit(result.formData)
    588                 end
    589             elseif result.type == "link" then
    590                 -- Navigate to link href
    591                 if result.href then
    592                     -- Check for javascript: links
    593                     if result.href:match("^javascript:") then
    594                         local code = result.href:sub(12)  -- Remove "javascript:"
    595                         executeInSandbox(code)
    596                     else
    597                         navigateTo(result.href)
    598                     end
    599                 end
    600             end
    601             window:markDirty()
    602         end
    603     end
    604 
    605     -- Execute initial scripts after page is set up
    606     for _, script in ipairs(collectedScripts) do
    607         executeInSandbox(script.content, script.path or "inline")
    608     end
    609 
    610     -- Return extended window object
    611     htmlWindow = {
    612         window = window,
    613         renderer = renderer,
    614         dom = dom,
    615         pageSandbox = pageSandbox,
    616 
    617         -- Query functions
    618         query = queryFunc,
    619         queryAll = queryAllFunc,
    620 
    621         -- Execute script in page context
    622         executeScript = executeInSandbox,
    623 
    624         -- Navigate to a new page
    625         navigate = navigateTo,
    626 
    627         -- Close window
    628         close = function(self)
    629             window:close()
    630         end,
    631 
    632         -- Mark dirty
    633         markDirty = function(self)
    634             window:markDirty()
    635         end,
    636 
    637         -- Reload HTML
    638         reload = function(self, newHtml)
    639             if newHtml then
    640                 local newDom = htmlParser:parse(newHtml)
    641                 if newDom then
    642                     dom = newDom
    643                     renderer:setDOM(dom)
    644                     window:markDirty()
    645                     return true
    646                 end
    647             end
    648             return false
    649         end,
    650 
    651         -- Get current file path
    652         getCurrentPath = function(self)
    653             return currentFilePath
    654         end,
    655     }
    656 
    657     return htmlWindow
    658 end
    659 
    660 return HTMLWindow