Advertisement
nonogamer9

CC-WEB: A Web Browser For ComputerCraft

Sep 25th, 2024 (edited)
91
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.28 KB | Software | 0 0
  1. local tArgs, initialURL = {...}, "http://example.com"
  2. local history, currentPage, scroll, horizontalScroll, cache, bookmarks = {}, 0, 0, 0, {}, {}
  3. local width, height = term.getSize()
  4. local running = true
  5.  
  6. local function urlEncode(url)
  7.   return url:gsub("([^%w%-%.%_%~])", function(c) return string.format("%%%02X", string.byte(c)) end)
  8. end
  9.  
  10. local function resolveURL(base, relative)
  11.   if relative:match("^https?://") then return relative end
  12.   if relative:sub(1,1) == "/" then return base:match("^(https?://[^/]+)") .. relative end
  13.   return base:match("^(.*/)") .. relative
  14. end
  15.  
  16. local function trim(s)
  17.   return s:match("^%s*(.-)%s*$")
  18. end
  19.  
  20. local function parseCSS(css)
  21.   local styles = {}
  22.   for selector, rules in css:gmatch("([^{]+){([^}]+)}") do
  23.     selector = trim(selector)
  24.     styles[selector] = {}
  25.     for property, value in rules:gmatch("([^:]+):([^;]+);?") do
  26.       styles[selector][trim(property)] = trim(value)
  27.     end
  28.   end
  29.   return styles
  30. end
  31.  
  32. local colorMap = {
  33.   black = colors.black, red = colors.red, green = colors.green,
  34.   yellow = colors.yellow, blue = colors.blue, purple = colors.purple,
  35.   cyan = colors.cyan, white = colors.white, gray = colors.gray,
  36.   lightGray = colors.lightGray, lime = colors.lime, orange = colors.orange,
  37. }
  38.  
  39. local function getCCColor(cssColor)
  40.   if colorMap[cssColor] then return colorMap[cssColor]
  41.   elseif cssColor:match("^#%x%x%x$") then
  42.     local r, g, b = cssColor:match("#(%x)(%x)(%x)")
  43.     r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)
  44.     return 2^math.floor(r/8) + 2^(math.floor(g/8)+4) + 2^(math.floor(b/8)+8)
  45.   end
  46.   return colors.white
  47. end
  48.  
  49. local function parseHTML(html)
  50.   local content = {}
  51.   local stack = {}
  52.   local inStyle = false
  53.   local cssContent = ""
  54.  
  55.   local function addText(text)
  56.     text = text:gsub(" ", " ")
  57.            :gsub("<", "<")
  58.            :gsub(">", ">")
  59.            :gsub("&", "&")
  60.            :gsub('"', '"')
  61.            :gsub("&#(%d+);", function(n) return string.char(tonumber(n)) end)
  62.     text = text:gsub("^%s+", ""):gsub("%s+$", ""):gsub("%s+", " ")
  63.     if text ~= "" then
  64.       table.insert(content, {type = "text", text = text})
  65.     end
  66.   end
  67.  
  68.   for tag, text in html:gmatch("(<[^>]+>)([^<]*)") do
  69.     local tagName = tag:match("</?(%w+)")
  70.     if tagName then
  71.       if inStyle then
  72.         if tag:match("^%s*/%s*" .. tagName .. "%s*>") then
  73.           inStyle = false
  74.           table.insert(content, {type = "tag_end", name = tagName})
  75.           table.remove(stack)
  76.         end
  77.       else
  78.         local attrs = {}
  79.         for k, v in tag:gmatch('(%w+)="([^"]*)"') do
  80.           attrs[k] = v
  81.         end
  82.         if tag:match("^<style") then
  83.           inStyle = true
  84.         elseif tag:match("^</") then
  85.           table.insert(content, {type = "tag_end", name = tagName})
  86.           table.remove(stack)
  87.         else
  88.           table.insert(content, {type = "tag_start", name = tagName, attrs = attrs})
  89.           if not tag:match("/>$") then
  90.             table.insert(stack, tagName)
  91.           end
  92.         end
  93.       end
  94.     end
  95.     if inStyle then
  96.       cssContent = cssContent .. text
  97.     else
  98.       addText(text)
  99.     end
  100.   end
  101.  
  102.   return content, parseCSS(cssContent)
  103. end
  104.  
  105. local function renderContent(content, styles, width)
  106.   local output = {}
  107.   local line = ""
  108.   local links = {}
  109.   local linkIndex = 1
  110.   local listStack = {}
  111.   local inPre = false
  112.   local currentStyle = {color = colors.white, background = colors.black}
  113.   local styleStack = {}
  114.  
  115.   local function applyStyle(tag)
  116.     local style = styles[tag] or {}
  117.     if style.color then currentStyle.color = getCCColor(style.color) end
  118.     if style.background then currentStyle.background = getCCColor(style.background) end
  119.     table.insert(styleStack, {color = currentStyle.color, background = currentStyle.background})
  120.   end
  121.  
  122.   local function removeStyle()
  123.     table.remove(styleStack)
  124.     if #styleStack > 0 then
  125.       currentStyle = styleStack[#styleStack]
  126.     else
  127.       currentStyle = {color = colors.white, background = colors.black}
  128.     end
  129.   end
  130.  
  131.   local function renderLine()
  132.     if #line > 0 then
  133.       table.insert(output, {text = string.rep(" ", #listStack) .. line, color = currentStyle.color, background = currentStyle.background})
  134.       line = ""
  135.     end
  136.   end
  137.  
  138.   for _, item in ipairs(content) do
  139.     if item.type == "text" then
  140.       if inPre then
  141.         line = line .. item.text
  142.         if item.text:find("\n") then renderLine() end
  143.       else
  144.         for word in item.text:gmatch("%S+") do
  145.           if #line + #word + 1 > width - 2 * #listStack then renderLine() end
  146.           line = #line == 0 and word or line .. " " .. word
  147.         end
  148.       end
  149.     elseif item.type == "tag_start" then
  150.       applyStyle(item.name)
  151.       if item.name == "br" then
  152.         renderLine()
  153.       elseif item.name:match("^h%d$") then
  154.         renderLine()
  155.         local level = tonumber(item.name:sub(2))
  156.         line = string.rep("#", level) .. " "
  157.       elseif item.name == "p" then
  158.         renderLine()
  159.         table.insert(output, {text = "", color = currentStyle.color, background = currentStyle.background})
  160.       elseif item.name == "ul" or item.name == "ol" then
  161.         renderLine()
  162.         table.insert(listStack, item.name)
  163.       elseif item.name == "li" then
  164.         renderLine()
  165.         line = (listStack[#listStack] == "ul" and "• " or #listStack .. ". ")
  166.       elseif item.name == "a" and item.attrs and item.attrs.href then
  167.         table.insert(links, {url = item.attrs.href, index = linkIndex})
  168.         line = line .. "[" .. linkIndex .. "]"
  169.         currentStyle.color = colors.blue
  170.       elseif item.name == "pre" then
  171.         inPre = true
  172.         renderLine()
  173.       end
  174.     elseif item.type == "tag_end" then
  175.       if item.name == "a" then
  176.         linkIndex = linkIndex + 1
  177.         removeStyle()
  178.       elseif item.name == "p" or item.name:match("^h%d$") then
  179.         renderLine()
  180.         table.insert(output, {text = "", color = currentStyle.color, background = currentStyle.background})
  181.       elseif item.name == "ul" or item.name == "ol" then
  182.         renderLine()
  183.         table.remove(listStack)
  184.       elseif item.name == "pre" then
  185.         inPre = false
  186.         renderLine()
  187.       end
  188.       removeStyle()
  189.     end
  190.   end
  191.   renderLine()
  192.   return output, links
  193. end
  194.  
  195. local function fetchPage(url)
  196.   if cache[url] then return cache[url] end
  197.   local response = http.get(url)
  198.   if not response then return nil end
  199.   local content = response.readAll()
  200.   response.close()
  201.   cache[url] = content
  202.   return content
  203. end
  204.  
  205. local buttons = {}
  206.  
  207. local function createButton(name, x, y, width, height, text, onClick)
  208.   buttons[name] = {x=x, y=y, width=width, height=height, text=text, onClick=onClick}
  209. end
  210.  
  211. local function drawButton(name)
  212.   local button = buttons[name]
  213.   paintutils.drawFilledBox(button.x, button.y, button.x + button.width - 1, button.y + button.height - 1, colors.lightGray)
  214.   term.setCursorPos(button.x + math.floor((button.width - #button.text) / 2), button.y + math.floor(button.height / 2))
  215.   term.setTextColor(colors.black)
  216.   term.write(button.text)
  217. end
  218.  
  219. local function checkButtonClick(x, y)
  220.   for name, button in pairs(buttons) do
  221.     if x >= button.x and x < button.x + button.width and y >= button.y and y < button.y + button.height then
  222.       paintutils.drawFilledBox(button.x, button.y, button.x + button.width - 1, button.y + button.height - 1, colors.gray)
  223.       term.setCursorPos(button.x + math.floor((button.width - #button.text) / 2), button.y + math.floor(button.height / 2))
  224.       term.setTextColor(colors.white)
  225.       term.write(button.text)
  226.       sleep(0.1)
  227.       button.onClick()
  228.       return true
  229.     end
  230.   end
  231.   return false
  232. end
  233.  
  234. local function loadBookmarks()
  235.   if fs.exists("cc_web_bookmarks") then
  236.     local file = fs.open("cc_web_bookmarks", "r")
  237.     for line in file.readLine do
  238.       local name, url = line:match("([^,]+),(.+)")
  239.       if name and url then bookmarks[name] = url end
  240.     end
  241.     file.close()
  242.   end
  243. end
  244.  
  245. local function saveBookmarks()
  246.   local file = fs.open("cc_web_bookmarks", "w")
  247.   for name, url in pairs(bookmarks) do
  248.     file.writeLine(name .. "," .. url)
  249.   end
  250.   file.close()
  251. end
  252.  
  253. local function showLoadingScreen()
  254.   term.clear()
  255.   term.setCursorPos(1, 1)
  256.  
  257.   local logo = [[
  258.    (     (      (  (          (  
  259.    )\    )\     )\))(   '(  ( )\
  260. (((_) (((_)___((_)()\ ) )\ )((_)
  261. )\___ )\__|___|(())\_)(|(_|(_)_
  262. ((/ __((/ __|  \ \((_)/ / __| _ )
  263. | (__ | (__    \ \/\/ /| _|| _ \
  264.  \___| \___|    \_/\_/ |___|___/
  265.  ]]
  266.  
  267.  local logoLines = {}
  268.  for line in logo:gmatch("[^\r\n]+") do
  269.    table.insert(logoLines, line)
  270.  end
  271.  
  272.  local startY = math.floor((height - #logoLines) / 2) - 2
  273.  for i, line in ipairs(logoLines) do
  274.    term.setCursorPos(math.floor((width - #line) / 2), startY + i)
  275.    term.write(line)
  276.  end
  277.  
  278.  local barWidth = 40
  279.  local barStartX = math.floor((width - barWidth) / 2)
  280.  local barY = startY + #logoLines + 2
  281.  
  282.  term.setCursorPos(barStartX, barY)
  283.  term.write("[" .. string.rep(" ", barWidth - 2) .. "]")
  284.  
  285.  local text = "A Web Browser For CC Made by nonogamer9"
  286.  term.setCursorPos(math.floor((width - #text) / 2), barY + 2)
  287.  term.write(text)
  288.  
  289.  for i = 1, barWidth - 2 do
  290.    term.setCursorPos(barStartX + i, barY)
  291.    term.write("=")
  292.    sleep(5 / (barWidth - 2))
  293.  end
  294. end
  295.  
  296. local function displayPage(url)
  297.  term.clear()
  298.  term.setCursorPos(1,1)
  299.  term.write("CC-WEB: Loading " .. url .. "...")
  300.  
  301.  if not url:match("^https?://") then url = "http://" .. url end
  302.  local html = fetchPage(url)
  303.  if not html and url:match("^http://") then
  304.    url = url:gsub("^http://", "https://")
  305.    html = fetchPage(url)
  306.  end
  307.  if not html then
  308.    term.clear()
  309.    term.setCursorPos(1,1)
  310.    term.write("CC-WEB: Error loading page")
  311.    os.sleep(2)
  312.    return
  313.  end
  314.  
  315.  local content, styles = parseHTML(html)
  316.  local rendered, links = renderContent(content, styles, width - 1)
  317.  
  318.  local function drawScreen()
  319.    term.setBackgroundColor(colors.black)
  320.    term.setTextColor(colors.white)
  321.    term.clear()
  322.    term.setCursorPos(1, 1)
  323.    term.setTextColor(url:match("^https://") and colors.green or colors.red)
  324.    term.write(url:match("^https://") and "[Secure] " or "[Not Secure] ")
  325.    term.setTextColor(colors.white)
  326.    for i = 2, height - 3 do
  327.      local lineIndex = i + scroll - 1
  328.      if lineIndex <= #rendered then
  329.        local line = rendered[lineIndex]
  330.        term.setCursorPos(1, i)
  331.        term.setTextColor(line.color)
  332.        term.setBackgroundColor(line.background)
  333.        local displayText = line.text:sub(horizontalScroll + 1, horizontalScroll + width)
  334.        term.write(displayText)
  335.      end
  336.    end
  337.    term.setBackgroundColor(colors.black)
  338.    term.setTextColor(colors.white)
  339.    term.setCursorPos(1, height - 2)
  340.    term.write(string.rep("-", width))
  341.    term.setCursorPos(1, height - 1)
  342.    term.write("URL: " .. url:sub(1, width - 9))
  343.    for _, button in pairs(buttons) do
  344.      drawButton(_)
  345.    end
  346.  end
  347.  
  348.  createButton("back", width - 27, height, 3, 1, "<", function()
  349.    if currentPage > 1 then
  350.      currentPage = currentPage - 1
  351.      displayPage(history[currentPage])
  352.    end
  353.  end)
  354.  
  355.  createButton("forward", width - 23, height, 3, 1, ">", function()
  356.    if currentPage < #history then
  357.      currentPage = currentPage + 1
  358.      displayPage(history[currentPage])
  359.    end
  360.  end)
  361.  
  362.  createButton("reload", width - 19, height, 3, 1, "R", function()
  363.    cache[url] = nil
  364.    displayPage(url)
  365.  end)
  366.  
  367.  createButton("bookmark", width - 15, height, 3, 1, "B", function()
  368.    term.setCursorPos(1, height)
  369.    term.clearLine()
  370.    term.write("Enter bookmark name: ")
  371.    local name = read()
  372.    if name and #name > 0 then
  373.      bookmarks[name] = url
  374.      saveBookmarks()
  375.    end
  376.  end)
  377.  
  378.  createButton("toggle", width - 11, height, 3, 1, "T", function()
  379.    local newUrl = url:gsub("^https?://", "")
  380.    newUrl = (url:match("^https://") and "http://" or "https://") .. newUrl
  381.    displayPage(newUrl)
  382.  end)
  383.  
  384.  createButton("bookmarks", width - 7, height, 3, 1, "L", function()
  385.    term.clear()
  386.    term.setCursorPos(1, 1)
  387.    term.write("Bookmarks:")
  388.    local i = 1
  389.    for name, bUrl in pairs(bookmarks) do
  390.      term.setCursorPos(1, i + 1)
  391.      term.write(i .. ". " .. name .. " - " .. bUrl)
  392.      i = i + 1
  393.    end
  394.    term.setCursorPos(1, height)
  395.    term.write("Enter number to load or 'q' to return: ")
  396.    local input = read()
  397.    if input ~= "q" then
  398.      local num = tonumber(input)
  399.      if num and num <= i - 1 then
  400.        local j = 1
  401.        for _, bUrl in pairs(bookmarks) do
  402.          if j == num then
  403.            displayPage(bUrl)
  404.            return
  405.          end
  406.          j = j + 1
  407.        end
  408.      end
  409.    end
  410.    displayPage(url)
  411.  end)
  412.  
  413.  createButton("quit", width - 3, height, 3, 1, "Q", function()
  414.    running = false
  415.  end)
  416.  
  417.  local function handleInput()
  418.    while true do
  419.      drawScreen()
  420.      local event, param, x, y = os.pullEvent()
  421.      if event == "key" then
  422.        if param == keys.up and scroll > 0 then
  423.          scroll = scroll - 1
  424.        elseif param == keys.down and scroll < #rendered - height + 4 then
  425.          scroll = scroll + 1
  426.        elseif param == keys.left and horizontalScroll > 0 then
  427.          horizontalScroll = horizontalScroll - 1
  428.        elseif param == keys.right and horizontalScroll < width then
  429.          horizontalScroll = horizontalScroll + 1
  430.        elseif param == keys.q then
  431.          return "quit"
  432.        end
  433.      elseif event == "mouse_click" and param == 1 then
  434.        if checkButtonClick(x, y) then
  435.          if not running then
  436.            return "quit"
  437.          end
  438.        elseif y == height - 1 and x > 5 and x <= width - 1 then
  439.          term.setCursorPos(6, height - 1)
  440.          term.clearLine()
  441.          term.write(url)
  442.          term.setCursorPos(6, height - 1)
  443.          local input = read(nil, nil, function(text)
  444.            if text == "" then
  445.              return {url}
  446.            else
  447.              return {}
  448.            end
  449.          end)
  450.          if input then
  451.            if not input:match("^https?://") then
  452.              input = "http://" .. input
  453.            end
  454.            table.insert(history, input)
  455.            currentPage = #history
  456.            return displayPage(input)
  457.          end
  458.        elseif y <= height - 3 then
  459.          local clickedLine = rendered[y + scroll - 1]
  460.          if clickedLine then
  461.            local clickedText = clickedLine.text
  462.            local linkNumber = clickedText:match("%[(%d+)%]")
  463.            if linkNumber then
  464.              local clickedLink = links[tonumber(linkNumber)]
  465.              if clickedLink then
  466.                local newURL = resolveURL(url, clickedLink.url)
  467.                table.insert(history, newURL)
  468.                currentPage = #history
  469.                return displayPage(newURL)
  470.              end
  471.            end
  472.          end
  473.        end
  474.      end
  475.    end
  476.  end
  477.  
  478.  return handleInput()
  479. end
  480.  
  481. loadBookmarks()
  482. showLoadingScreen()
  483. table.insert(history, initialURL)
  484. currentPage = 1
  485.  
  486. while running do
  487.  local result = displayPage(history[currentPage])
  488.  if result == "quit" then
  489.    break
  490.  end
  491. end
  492.  
  493. term.clear()
  494. term.setCursorPos(1,1)
  495. print("Thank you for using CC-WEB Browser!")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement