luajitos

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

browser.lua (15591B)


      1 -- browser.lua - Moonbrowser main entry point
      2 -- Opens HTML files and renders them to a window
      3 
      4 local parser = require("parser", true)  -- Use bytecode if available
      5 local Renderer = require("render", true)  -- Use bytecode if available
      6 
      7 local Parser = parser.Parser
      8 
      9 -- Default file to open
     10 local htmlPath = "/home/Documents/test.html"
     11 
     12 -- Check for command line arguments
     13 if args then
     14     local argPath = args.o or args.open or args[1]
     15     if argPath and argPath ~= "" then
     16         htmlPath = argPath
     17     end
     18 end
     19 
     20 print("Moonbrowser: Opening " .. htmlPath)
     21 
     22 -- Expand ~ to /home
     23 if htmlPath:sub(1, 1) == "~" then
     24     htmlPath = "/home" .. htmlPath:sub(2)
     25 end
     26 
     27 -- Check if SafeFS is available
     28 if not fs then
     29     print("ERROR: SafeFS not available (missing 'filesystem' permission)")
     30     return
     31 end
     32 
     33 -- Check if file exists - if not, show a "file not found" page
     34 local htmlContent
     35 if not fs:exists(htmlPath) then
     36     print("Moonbrowser: File not found, showing error page: " .. htmlPath)
     37     htmlContent = [[
     38 <!DOCTYPE html>
     39 <html>
     40 <head>
     41     <title>File Not Found</title>
     42 </head>
     43 <body style="background-color: #2d2d2d; color: #ffffff; font-family: sans-serif; padding: 20px;">
     44     <h1 style="color: #ff6b6b;">File Not Found</h1>
     45     <p>The requested file could not be found:</p>
     46     <p style="color: #aaaaaa; font-family: monospace; background-color: #1a1a1a; padding: 10px;">]] .. htmlPath .. [[</p>
     47     <p style="margin-top: 20px;">Please check the path and try again.</p>
     48     <p style="color: #888888; font-size: 12px; margin-top: 30px;">Tip: Use the file dialog in Explorer to open HTML files, or pass a path as an argument.</p>
     49 </body>
     50 </html>
     51 ]]
     52 else
     53     -- Read the HTML file
     54     local err
     55     htmlContent, err = fs:read(htmlPath)
     56     if not htmlContent then
     57         print("Moonbrowser: Failed to read file, showing error page: " .. tostring(err))
     58         htmlContent = [[
     59 <!DOCTYPE html>
     60 <html>
     61 <head>
     62     <title>Read Error</title>
     63 </head>
     64 <body style="background-color: #2d2d2d; color: #ffffff; font-family: sans-serif; padding: 20px;">
     65     <h1 style="color: #ff6b6b;">Failed to Read File</h1>
     66     <p>Could not read the file:</p>
     67     <p style="color: #aaaaaa; font-family: monospace; background-color: #1a1a1a; padding: 10px;">]] .. htmlPath .. [[</p>
     68     <p style="color: #ff9999;">Error: ]] .. tostring(err) .. [[</p>
     69 </body>
     70 </html>
     71 ]]
     72     end
     73 end
     74 
     75 print("Moonbrowser: Read " .. #htmlContent .. " bytes")
     76 
     77 -- Parse HTML
     78 local htmlParser = Parser.new()
     79 local dom = htmlParser:parse(htmlContent)
     80 
     81 if not dom then
     82     print("ERROR: Failed to parse HTML")
     83     return
     84 end
     85 
     86 print("Moonbrowser: HTML parsed successfully")
     87 
     88 -- Parse meta tags for width and height
     89 local windowWidth = 800
     90 local windowHeight = 600
     91 
     92 -- Look for <meta name="width" content="..."> and <meta name="height" content="...">
     93 for name, content in htmlContent:gmatch('<meta[^>]+name%s*=%s*["\']([^"\']+)["\'][^>]+content%s*=%s*["\']([^"\']+)["\']') do
     94     if name == "width" then
     95         local w = tonumber(content)
     96         if w and w > 0 and w <= 2048 then
     97             windowWidth = w
     98         end
     99     elseif name == "height" then
    100         local h = tonumber(content)
    101         if h and h > 0 and h <= 2048 then
    102             windowHeight = h
    103         end
    104     end
    105 end
    106 
    107 -- Also check reverse order: content before name
    108 for content, name in htmlContent:gmatch('<meta[^>]+content%s*=%s*["\']([^"\']+)["\'][^>]+name%s*=%s*["\']([^"\']+)["\']') do
    109     if name == "width" then
    110         local w = tonumber(content)
    111         if w and w > 0 and w <= 2048 then
    112             windowWidth = w
    113         end
    114     elseif name == "height" then
    115         local h = tonumber(content)
    116         if h and h > 0 and h <= 2048 then
    117             windowHeight = h
    118         end
    119     end
    120 end
    121 
    122 -- Toolbar settings
    123 local toolbarHeight = 30
    124 local statusBarHeight = 20
    125 
    126 print("Moonbrowser: Window size " .. windowWidth .. "x" .. windowHeight)
    127 
    128 -- Adjust window height to include toolbar
    129 local totalWindowHeight = windowHeight + toolbarHeight
    130 
    131 -- Create window
    132 local window = app:newWindow("Moonbrowser - " .. htmlPath, windowWidth, totalWindowHeight, true)
    133 
    134 if not window then
    135     print("ERROR: Failed to create window")
    136     return
    137 end
    138 
    139 -- Create renderer with content area (excluding toolbar and status bar)
    140 local contentHeight = windowHeight - statusBarHeight
    141 local renderer = Renderer.new(nil, windowWidth, contentHeight)
    142 renderer.offsetY = toolbarHeight  -- Set offset for toolbar
    143 
    144 -- Store DOM in renderer for query() support
    145 renderer:setDOM(dom)
    146 
    147 -- Create page sandbox for executing onclick handlers and inline scripts
    148 -- Query function used by both query() and document.querySelector()
    149 local function queryFunc(selector)
    150     return renderer:query(selector)
    151 end
    152 
    153 local function queryAllFunc(selector)
    154     return renderer:queryAll(selector)
    155 end
    156 
    157 local pageSandbox = {
    158     -- DOM query functions
    159     query = queryFunc,
    160     queryAll = queryAllFunc,
    161     document = {
    162         querySelector = queryFunc,
    163         querySelectorAll = queryAllFunc,
    164         getElementById = function(id)
    165             return renderer:query("#" .. id)
    166         end,
    167     },
    168 
    169     -- Dialog functions (browser-like) - wrapped to use Moonbrowser's app context
    170     alert = function(msg)
    171         if Dialog and Dialog.alert then
    172             Dialog.alert(tostring(msg), { app = app })
    173         else
    174             print("ALERT: " .. tostring(msg))
    175         end
    176     end,
    177     confirm = function(msg)
    178         if Dialog and Dialog.confirm then
    179             return Dialog.confirm(tostring(msg), { app = app })
    180         else
    181             print("CONFIRM: " .. tostring(msg))
    182             return true
    183         end
    184     end,
    185     prompt = function(msg, default)
    186         if Dialog and Dialog.prompt then
    187             return Dialog.prompt(tostring(msg), { app = app, default = default })
    188         else
    189             print("PROMPT: " .. tostring(msg))
    190             return default or ""
    191         end
    192     end,
    193 
    194     -- Console
    195     console = {
    196         log = function(...)
    197             local args = {...}
    198             local parts = {}
    199             for i = 1, #args do
    200                 parts[i] = tostring(args[i])
    201             end
    202             print("[console] " .. table.concat(parts, " "))
    203         end,
    204         error = function(...)
    205             local args = {...}
    206             local parts = {}
    207             for i = 1, #args do
    208                 parts[i] = tostring(args[i])
    209             end
    210             print("[console.error] " .. table.concat(parts, " "))
    211         end,
    212         warn = function(...)
    213             local args = {...}
    214             local parts = {}
    215             for i = 1, #args do
    216                 parts[i] = tostring(args[i])
    217             end
    218             print("[console.warn] " .. table.concat(parts, " "))
    219         end,
    220     },
    221 
    222     -- Basic Lua functions
    223     print = print,
    224     tostring = tostring,
    225     tonumber = tonumber,
    226     type = type,
    227     pairs = pairs,
    228     ipairs = ipairs,
    229     string = string,
    230     table = table,
    231     math = math,
    232 
    233     -- Window control
    234     window = {
    235         setSize = function(w, h)
    236             if type(w) == "number" and type(h) == "number" and w > 0 and h > 0 then
    237                 windowWidth = w
    238                 windowHeight = h
    239                 window:setSize(w, h)
    240                 renderer.width = w
    241                 renderer.height = h
    242                 window:markDirty()
    243             end
    244         end,
    245         getWidth = function()
    246             return windowWidth
    247         end,
    248         getHeight = function()
    249             return windowHeight
    250         end,
    251     },
    252 }
    253 
    254 -- Function to execute code in page sandbox
    255 local function executeInSandbox(code)
    256     if not code or code == "" then return end
    257 
    258     -- Use loadstring with custom environment
    259     -- The third parameter to loadstring sets the environment
    260     local func, err = loadstring(code, "onclick", pageSandbox)
    261     if not func then
    262         print("Moonbrowser: Script error: " .. tostring(err))
    263         return
    264     end
    265 
    266     local ok, result = pcall(func)
    267     if not ok then
    268         print("Moonbrowser: Script runtime error: " .. tostring(result))
    269     end
    270     return result
    271 end
    272 
    273 -- Store executeInSandbox for use in click handler
    274 renderer.executeScript = executeInSandbox
    275 
    276 -- Toolbar button definitions
    277 local toolbarButtons = {
    278     { x = 5, y = 5, width = 50, height = 20, label = "Open", action = "open" },
    279     { x = 60, y = 5, width = 50, height = 20, label = "Back", action = "back" },
    280     { x = 115, y = 5, width = 60, height = 20, label = "Refresh", action = "refresh" },
    281 }
    282 
    283 -- History for back button
    284 local history = {}
    285 
    286 -- Draw callback
    287 window.onDraw = function(gfx)
    288     -- Draw toolbar background
    289     gfx:fillRect(0, 0, windowWidth, toolbarHeight, 0x404040)
    290 
    291     -- Draw toolbar buttons
    292     for _, btn in ipairs(toolbarButtons) do
    293         -- Button background
    294         gfx:fillRect(btn.x, btn.y, btn.width, btn.height, 0x606060)
    295         -- Button border
    296         gfx:drawRect(btn.x, btn.y, btn.width, btn.height, 0x808080)
    297         -- Button label
    298         local textX = btn.x + (btn.width - #btn.label * 6) / 2
    299         local textY = btn.y + 6
    300         gfx:drawText(textX, textY, btn.label, 0xFFFFFF)
    301     end
    302 
    303     -- Draw separator line below toolbar
    304     gfx:fillRect(0, toolbarHeight - 1, windowWidth, 1, 0x303030)
    305 
    306     -- Render DOM to content area (renderer handles its own offset)
    307     renderer:render(dom, gfx)
    308 
    309     -- Draw status bar at bottom
    310     local statusY = totalWindowHeight - statusBarHeight
    311     gfx:fillRect(0, statusY, windowWidth, statusBarHeight, 0x333333)
    312     gfx:drawText(5, statusY + 4, htmlPath, 0xFFFFFF)
    313     gfx:drawText(windowWidth - 150, statusY + 4, "Arrows: Scroll  Q: Quit", 0xAAAAAA)
    314 end
    315 
    316 -- Input callback
    317 window.onInput = function(key, scancode)
    318     -- First, let renderer handle input if it has focus
    319     if renderer:hasFocus() then
    320         if renderer:handleKey(key, scancode) then
    321             window:markDirty()
    322             return
    323         end
    324     end
    325 
    326     if key == "q" or key == "Q" then
    327         window:close()
    328     elseif scancode == 72 then  -- Up arrow
    329         renderer:scroll(-40)
    330         window:markDirty()
    331     elseif scancode == 80 then  -- Down arrow
    332         renderer:scroll(40)
    333         window:markDirty()
    334     end
    335 end
    336 
    337 -- Helper to resolve relative paths
    338 local function resolvePath(href, currentPath)
    339     if not href then return nil end
    340 
    341     -- Handle javascript: URLs
    342     if href:sub(1, 11) == "javascript:" then
    343         return nil, href:sub(12)  -- Return nil path, but return the JS code
    344     end
    345 
    346     -- Absolute path
    347     if href:sub(1, 1) == "/" then
    348         return href
    349     end
    350 
    351     -- Get directory of current file
    352     local currentDir = currentPath:match("(.*/)")
    353     if not currentDir then
    354         currentDir = "/"
    355     end
    356 
    357     -- Handle ../ (parent directory)
    358     while href:sub(1, 3) == "../" do
    359         href = href:sub(4)
    360         -- Go up one directory
    361         currentDir = currentDir:match("(.*/)[^/]+/$") or "/"
    362     end
    363 
    364     -- Handle ./ (current directory)
    365     if href:sub(1, 2) == "./" then
    366         href = href:sub(3)
    367     end
    368 
    369     return currentDir .. href
    370 end
    371 
    372 -- Function to navigate to a new page
    373 local function navigateTo(newPath)
    374     print("Moonbrowser: Navigating to " .. newPath)
    375 
    376     -- Check if file exists
    377     if not fs:exists(newPath) then
    378         print("ERROR: File not found: " .. newPath)
    379         return false
    380     end
    381 
    382     -- Read the new HTML file
    383     local newContent, err = fs:read(newPath)
    384     if not newContent then
    385         print("ERROR: Failed to read file: " .. tostring(err))
    386         return false
    387     end
    388 
    389     -- Update current path
    390     htmlPath = newPath
    391 
    392     -- Parse new HTML
    393     local newDom = htmlParser:parse(newContent)
    394     if not newDom then
    395         print("ERROR: Failed to parse HTML")
    396         return false
    397     end
    398 
    399     -- Update DOM and renderer
    400     dom = newDom
    401     renderer:setDOM(dom)
    402     renderer:resetScroll()
    403 
    404     print("Moonbrowser: Navigation complete")
    405     return true
    406 end
    407 
    408 -- Function to open file dialog
    409 local function openFileDialog()
    410     if not Dialog or not Dialog.fileOpen then
    411         print("Moonbrowser: Dialog.fileOpen not available")
    412         return
    413     end
    414 
    415     local dialog = Dialog.fileOpen("/home", {
    416         app = app,
    417         fs = fs,
    418         title = "Open HTML File"
    419     })
    420 
    421     dialog:openDialog(function(selectedPath)
    422         if selectedPath then
    423             -- Check if it's an HTML file
    424             if selectedPath:match("%.html?$") then
    425                 -- Save current path to history
    426                 table.insert(history, htmlPath)
    427                 -- Navigate to selected file
    428                 if navigateTo(selectedPath) then
    429                     window.title = "Moonbrowser - " .. selectedPath
    430                     window:markDirty()
    431                 end
    432             else
    433                 print("Moonbrowser: Not an HTML file: " .. selectedPath)
    434                 if Dialog.alert then
    435                     Dialog.alert("Please select an HTML file (.html or .htm)", { app = app })
    436                 end
    437             end
    438         end
    439     end)
    440 end
    441 
    442 -- Function to go back in history
    443 local function goBack()
    444     if #history > 0 then
    445         local prevPath = table.remove(history)
    446         if navigateTo(prevPath) then
    447             window.title = "Moonbrowser - " .. prevPath
    448             window:markDirty()
    449         end
    450     end
    451 end
    452 
    453 -- Function to refresh current page
    454 local function refreshPage()
    455     if navigateTo(htmlPath) then
    456         window:markDirty()
    457     end
    458 end
    459 
    460 -- Click callback
    461 window.onClick = function(x, y, button)
    462     -- Check if click is in toolbar area
    463     if y < toolbarHeight then
    464         -- Check toolbar buttons
    465         for _, btn in ipairs(toolbarButtons) do
    466             if x >= btn.x and x < btn.x + btn.width and
    467                y >= btn.y and y < btn.y + btn.height then
    468                 print("Moonbrowser: Toolbar button clicked: " .. btn.action)
    469                 if btn.action == "open" then
    470                     openFileDialog()
    471                 elseif btn.action == "back" then
    472                     goBack()
    473                 elseif btn.action == "refresh" then
    474                     refreshPage()
    475                 end
    476                 return
    477             end
    478         end
    479         return
    480     end
    481 
    482     -- Adjust click position for content area
    483     local contentY = y - toolbarHeight
    484 
    485     local result = renderer:handleClick(x, contentY)
    486     if result then
    487         if result.type == "button" then
    488             print("Button clicked: " .. (result.text or "(no text)"))
    489             if result.onclick then
    490                 -- Execute onclick handler in page sandbox
    491                 executeInSandbox(result.onclick)
    492             end
    493         elseif result.type == "submit" then
    494             print("Form submitted with data:")
    495             for name, value in pairs(result.formData) do
    496                 print("  " .. name .. " = " .. tostring(value))
    497             end
    498         elseif result.type == "link" then
    499             print("Link clicked: " .. (result.href or "(no href)"))
    500             if result.href then
    501                 local newPath, jsCode = resolvePath(result.href, htmlPath)
    502                 if jsCode then
    503                     -- javascript: URL
    504                     executeInSandbox(jsCode)
    505                 elseif newPath then
    506                     -- Save current path to history before navigating
    507                     table.insert(history, htmlPath)
    508                     -- Navigate to new page
    509                     navigateTo(newPath)
    510                 end
    511             end
    512         elseif result.type == "focus" then
    513             -- Input focused, no action needed
    514         elseif result.type == "toggle" then
    515             -- Checkbox toggled, no action needed
    516         end
    517         window:markDirty()
    518     end
    519 end
    520 
    521 print("Moonbrowser: Window ready")