local unicode=require("unicode") local W={} local function ui_size_(n) if n>=1048576 then return string.format("%.1f MB",n/1048576)end if n>=1024 then return string.format("%.1f KB",n/1024)end return n.." B" end W.size=ui_size_ function W.parse(url) local scheme,rest=url:match("^(%a[%w+.-]*):(.*)$") if not scheme then return{path=url}end local u={scheme=scheme:lower()} local frag rest,frag=rest:match("^([^#]*)#?(.*)$") u.fragment=frag~=""and frag or nil local q rest,q=rest:match("^([^?]*)%??(.*)$") u.query=q~=""and q or nil local auth,path=rest:match("^//([^/]*)(.*)$") u.host=auth u.path=(path or rest) if u.host and u.path==""then u.path="/"end return u end local function dotSegments(path) local out,parts={},{} for seg in(path.."/"):gmatch("([^/]*)/")do parts[#parts+1]=seg end for i,seg in ipairs(parts)do if seg==".."then if#out>1 then out[#out]=nil end elseif seg~="."then out[#out+1]=seg end if(seg=="."or seg=="..")and i==#parts then out[#out+1]=""end end local r=table.concat(out,"/") if path:sub(1,1)=="/"and r:sub(1,1)~="/"then r="/"..r end return r end function W.resolve(base,href) href=(href or""):gsub("^%s+",""):gsub("%s+$","") if href==""then return base end if href:match("^%a[%w+.-]*:")then return href end local b=W.parse(base) if not b.scheme or not b.host then return href end local origin=b.scheme.."://"..b.host if href:sub(1,2)=="//"then return b.scheme..":"..href end if href:sub(1,1)=="#"then return(base:gsub("#.*$",""))..href end if href:sub(1,1)=="?"then return origin..b.path..href end if href:sub(1,1)=="/"then return origin..dotSegments(href:match("^[^?#]*"))..(href:match("[?#].*$")or"")end local dir=b.path:match("^(.*/)")or"/" local p,tail=href:match("^([^?#]*)(.*)$") return origin..dotSegments(dir..p)..tail end function W.normalize(input,searchUrl) input=input:gsub("^%s+",""):gsub("%s+$","") if input==""then return nil end if input:match("^%a[%w+.-]*:")then return input end if not input:find("%s")and(input:find("%.")or input:match("^localhost")or input:match("^%d+%.%d+%.%d+%.%d+"))then return"http://"..input end return(searchUrl or"https://html.duckduckgo.com/html/?q=")..W.urlencode(input) end function W.urlencode(s) return(s:gsub("[^%w%-_%.~]",function(c)return string.format("%%%02X",c:byte())end)) end local CP1251={[0xA8]="Ё",[0xB8]="ё",[0xAB]="«",[0xBB]="»",[0x96]="–",[0x97]="—",[0x85]="…", [0xB9]="№",[0x93]="“",[0x94]="”",[0xA0]=" "} function W.fromCP1251(s) return(s:gsub("[\128-\255]",function(c) local b=c:byte() if b>=0xC0 then return utf8.char(0x410+b-0xC0)end return CP1251[b]or"?" end)) end function W.sanitize(s) if utf8.len(s)then return s end local out,i,n={},1,#s while i<=n do local ok,cp=pcall(utf8.codepoint,s,i) if ok then local ch=utf8.char(cp) out[#out+1]=ch i=i+#ch else out[#out+1]="?" i=i+1 end end return table.concat(out) end local ENT={amp="&",lt="<",gt=">",quot='"',apos="'",nbsp=" ",copy="©",reg="®",trade="™", mdash="—",ndash="–",hellip="…",laquo="«",raquo="»",bull="•",middot="·",deg="°", euro="€",times="×",larr="←",rarr="→",uarr="↑",darr="↓",ldquo="“",rdquo="”", lsquo="‘",rsquo="’",shy=""} function W.decodeEntities(s) return(s:gsub("&(#?[%w]+);?",function(e) local n=e:match("^#[xX](%x+)$") if n then n=tonumber(n,16)else n=e:match("^#(%d+)$");n=n and tonumber(n)end if n then if n<32 or n>0x10FFFF then return" "end return utf8.char(n) end return ENT[e:lower()]or("&"..e..";") end)) end local REDIRECT={[301]=true,[302]=true,[303]=true,[307]=true,[308]=true} W.USER_AGENT="OCBrowser/3.0 (OpenComputers)" W.MAX_BYTES=512*1024 W.TIMEOUT=15 local function header(h,name) if not h then return nil end local v=h[name] if v==nil then for k,x in pairs(h)do if type(k)=="string"and k:lower()==name:lower()then v=x break end end end if type(v)=="table"then v=v[1]end return v end function W.fetch(inet,url,opts) opts=opts or{} local job={url=url,state="connect",chunks={},received=0,redirects=0,started=os.clock()} local computer=require("computer") local function open(u) job.url=u job.state="connect" job.deadline=computer.uptime()+(opts.timeout or W.TIMEOUT) local post=job.redirects==0 and opts.post or nil local hdr={["User-Agent"]=W.USER_AGENT,["Accept"]="text/html,text/plain;q=0.9,*/*;q=0.5", ["Accept-Encoding"]="identity",["Accept-Language"]="ru,en;q=0.8"} if post then hdr["Content-Type"]="application/x-www-form-urlencoded"end local ok,req=pcall(inet.request,u,post,hdr) if not ok or not req then job.state,job.err="error","request failed: "..tostring(req)return end job.req=req end local function fail(msg) job.state,job.err="error",msg if job.req then pcall(job.req.close)end end function job:step() if self.state=="done"or self.state=="error"then return self.state end local ok,err=pcall(function() if self.state=="connect"then local done,e=self.req.finishConnect() if e then return fail(tostring(e))end if done then self.state="headers"elseif computer.uptime()>self.deadline then fail("connection timed out")end return end if self.state=="headers"then local code,msg,headers=self.req.response() if not code then if computer.uptime()>self.deadline then fail("no response")end return end self.code,self.msg,self.headers=code,msg,headers if REDIRECT[code]then local loc=header(headers,"location") if loc then pcall(self.req.close) self.redirects=self.redirects+1 if self.redirects>10 then return fail("too many redirects")end return open(W.resolve(self.url,loc)) end end self.ct=header(headers,"content-type")or"" self.total=tonumber(header(headers,"content-length")or"") self.state="body" return end if self.state=="body"then for _=1,4 do local chunk,e=self.req.read(8192) if not chunk then if e then return fail(tostring(e))end pcall(self.req.close) self.body=table.concat(self.chunks) self.chunks=nil self.state="done" return end if#chunk>0 then self.chunks[#self.chunks+1]=chunk self.received=self.received+#chunk if self.received>(opts.maxBytes or W.MAX_BYTES)then pcall(self.req.close) self.body=table.concat(self.chunks) self.chunks,self.truncated,self.state=nil,true,"done" return end else return end end end end) if not ok then fail(tostring(err))end self.progress=self.total and self.total>0 and math.min(1,self.received/self.total)or nil return self.state end function job:cancel()if self.req then pcall(self.req.close)end self.state="error";self.err="cancelled"end open(url) return job end function W.decodeBody(body,ct) local cs=(ct or""):lower():match("charset=([%w%-_]+)") if not cs then cs=body:sub(1,2048):lower():match("]-charset=[\"']?([%w%-_]+)")end if cs and(cs:find("1251")or cs=="cp1251")then body=W.fromCP1251(body)end return W.sanitize(body) end local BLOCK={p=true,div=true,section=true,article=true,header=true,footer=true,main=true, nav=true,aside=true,form=true,table=true,tr=true,dl=true,dt=true,dd=true,figure=true, figcaption=true,center=true,address=true,details=true,summary=true} local HEAD={h1=1,h2=2,h3=3,h4=4,h5=5,h6=6} local SKIP={script=true,style=true,noscript=true,svg=true,template=true,iframe=true,select=true} function W.layout(html,width,C,isHtml) local lines,links,anchors={},{},{} local pendingBullet,pendingSep local afterLink=false local cur,curLen,indent=nil,0,0 local title local function newLine() cur={segs={},indent=indent} lines[#lines+1]=cur curLen=indent end local function blank() if#lines>0 and(#lines[#lines].segs>0)then newLine()end if cur and#cur.segs>0 then newLine()end end local function breakLine()newLine()end local function addSeg(text,style) if not cur then newLine()end local last=cur.segs[#cur.segs] if last and last.fg==style.fg and last.href==style.href and last.bg==style.bg then last.text=last.text..text else cur.segs[#cur.segs+1]={text=text,fg=style.fg,href=style.href,bg=style.bg} end if style.href then local l=links[#links] if l and l.line==#lines and l.href==style.href and l.x2==curLen then l.x2=curLen+unicode.len(text);l.text=l.text..text else links[#links+1]={line=#lines,x1=curLen,x2=curLen+unicode.len(text),href=style.href,text=text}end end curLen=curLen+unicode.len(text) end local addText0 local function addText(text,style,pre) if(pendingBullet or pendingSep)and text:find("%S")then if pendingBullet then newLine();curLen=indent addSeg(pendingBullet,{fg=C.accent}) pendingBullet=nil elseif pendingSep then if cur and#cur.segs>0 then addSeg(" · ",{fg=C.dim})end pendingSep=nil end text=text:gsub("^%s+","") end return addText0(text,style,pre) end function addText0(text,style,pre) if pre then local first=true for part in(text.."\n"):gmatch("([^\n]*)\n")do if not first then breakLine()end first=false while unicode.len(part)>0 do local room=width-curLen if room<=0 then breakLine();room=width-curLen end addSeg(unicode.sub(part,1,room),style) part=unicode.sub(part,room+1) end end return end for space,word in text:gmatch("(%s*)(%S*)")do if space~=""and cur and#cur.segs>0 and curLenwidth and cur and#cur.segs>0 then breakLine()end while unicode.len(word)>width-curLen do local room=math.max(1,width-curLen) addSeg(unicode.sub(word,1,room),style) word=unicode.sub(word,room+1) breakLine() end if word~=""then addSeg(word,style)end end end end if not isHtml then for l in(html:gsub("\r\n?","\n").."\n"):gmatch("([^\n]*)\n")do newLine() addText(l:gsub("\t"," "),{fg=C.text},true) end return{lines=lines,links=links,anchors=anchors} end local style={fg=C.text} local styles={} local function push(s) styles[#styles+1]=style local n={} for k,v in pairs(style)do n[k]=v end for k,v in pairs(s)do n[k]=v end style=n end local function pop()if#styles>0 then style=table.remove(styles)end end local skip,pre=nil,0 local lists={} local hideTag,hideDepth=nil,0 local navDepth=0 local VOID={br=true,hr=true,img=true,input=true,meta=true,link=true,area=true,base=true, col=true,embed=true,source=true,track=true,wbr=true} local function isHidden(tag) local low=tag:lower() if low:find("%shidden[%s/>=]")or low:find("%shidden$")or low:find('aria%-hidden%s*=%s*["\']?true')then return true end local style=low:match('style%s*=%s*"([^"]*)"')or low:match("style%s*=%s*'([^']*)'")or"" if style:find("display%s*:%s*none")or style:find("visibility%s*:%s*hidden")then return true end local cls=" "..(low:match('class%s*=%s*"([^"]*)"')or low:match("class%s*=%s*'([^']*)'")or"").." " for _,c in ipairs({"hidden","d%-none","sr%-only","visually%-hidden","invisible","screen%-reader%-text", "mobile%-menu","mobile%-nav","offcanvas","modal","dropdown%-menu","tooltip","popover","cookie"})do if cls:find("[%s]"..c.."[%s]")then return true end end return false end local pos,len=1,#html while pos<=len do if skip then local e=html:lower():find("=pos and not hideTag then local t=W.decodeEntities(html:sub(pos,textEnd)) if t:find("%S")then afterLink=false end if pre>0 then addText(t,style,true)elseif t:find("%S")or(cur and#cur.segs>0)then addText(t,style,false)end end if not lt then break end if html:sub(lt,lt+3)=="",lt+4,true) pos=e and e+3 or len+1 else local gt=html:find(">",lt,true) if not gt then break end local tag=html:sub(lt+1,gt-1) pos=gt+1 local close=tag:sub(1,1)=="/" local name=(tag:match("^/?%s*([%w]+)")or""):lower() if hideTag then if name==hideTag and not VOID[name]then hideDepth=hideDepth+(close and-1 or 1)end if hideDepth<=0 then hideTag=nil end goto continue end if not close and not VOID[name]and not tag:find("/%s*$")and isHidden(tag)then hideTag,hideDepth=name,1 goto continue end if name=="nav"or name=="header"then navDepth=math.max(0,navDepth+(close and-1 or 1))end local function attr(a) return tag:match(a..'%s*=%s*"([^"]*)"')or tag:match(a.."%s*=%s*'([^']*)'")or tag:match(a.."%s*=%s*([^%s>]+)") end if name=="title"and not close then local e=html:lower():find("0 and cur.segs[#cur.segs].text:sub(-1)~=" "then addSeg(" · ",{fg=C.dim}) end local href=attr("href") local id=attr("name")or attr("id") if id then anchors[id]=#lines+1 end push(href and{fg=C.link,href=href}or{}) end elseif name=="ul"or name=="ol"then if close then local l=table.remove(lists) if l and l.inline then newLine()else indent=math.max(0,indent-2);blank()end else local cls=(tag:lower():match('class%s*=%s*"([^"]*)"')or"") local inline=navDepth>0 or cls:find("nav")or cls:find("menu")or cls:find("breadcrumb")or cls:find("inline") or(lists[#lists]and lists[#lists].inline) if inline then if cur and#cur.segs>0 then newLine()end else blank();indent=indent+2 end lists[#lists+1]={ordered=name=="ol",n=0,inline=inline and true or false} end elseif name=="li"and not close then local l=lists[#lists] if l then l.n=l.n+1 end if l and l.inline then pendingSep=l.n>1 afterLink=false else pendingBullet=l and l.ordered and(l.n..". ")or"• " end elseif name=="blockquote"then blank() if close then indent=math.max(0,indent-2);pop()else indent=indent+2;push({fg=C.em})end elseif name=="td"or name=="th"then if not close and cur and#cur.segs>0 then addSeg(" │ ",{fg=C.dim})end if name=="th"then if close then pop()else push({fg=C.strong})end end elseif name=="img"then local alt=attr("alt") if alt and alt~=""then addText("["..W.decodeEntities(alt).."]",{fg=C.dim})end elseif name=="input"and not close then local t=(attr("type")or"text"):lower() if t=="submit"or t=="button"then addText("["..W.decodeEntities(attr("value")or"OK").."]",{fg=C.accent}) elseif t~="hidden"then addText("[____]",{fg=C.dim})end elseif BLOCK[name]then blank() end local id=attr("id") if id and not close then anchors[id]=anchors[id]or(#lines+(cur and#cur.segs>0 and 1 or 0))end ::continue:: end end while#lines>0 and#lines[#lines].segs==0 do lines[#lines]=nil end return{lines=lines,links=links,title=title,anchors=anchors} end local CONF="/home/.config/ocbrowser" local function loadList(name) local f=io.open(CONF.."/"..name) if not f then return{}end local ok,t=pcall(require("serialization").unserialize,f:read("*a")) f:close() return ok and type(t)=="table"and t or{} end local function saveList(name,t) require("filesystem").makeDirectory(CONF) local f=io.open(CONF.."/"..name,"w") if f then f:write(require("serialization").serialize(t));f:close()end end local function esc(s)return(s:gsub("&","&"):gsub("<","<"):gsub(">",">"):gsub('"',"""))end function W.session(o) local S={url="about:home",back={},fwd={},width=o.width or 80,colors=o.colors, bookmarks=loadList("bookmarks"),globalHistory=loadList("history"),status="",source=false} local inet=o.inet local function aboutPage(url) local name=url:match("^about:(%w*)")or"home" local h={"",name=="home"and"OCBrowser"or("about:"..name),""} local function add(...)for _,x in ipairs({...})do h[#h+1]=x end end if name=="bookmarks"then add("

Bookmarks

") if#S.bookmarks==0 then add("

No bookmarks yet. Press Ctrl+D on a page.

")end add("") elseif name=="history"then add("

History

") else add("

OCBrowser 3.0

Type an address or words to search (DuckDuckGo).

") if#S.bookmarks>0 then add("

Bookmarks

") end add([[

Try

Keys

Ctrl+L address · Enter open · Tab next link · Alt+Left/Right back/forward · F5 reload · Ctrl+F find · Ctrl+D bookmark · Ctrl+U view source · Ctrl+S save page · Space/PgDn scroll · Esc stop

]]) end add("") return table.concat(h) end local cfgMode=loadList("mode") S.textOnly=cfgMode.textOnly or false function S.setTextOnly(v)S.textOnly=v and true or false;saveList("mode",{textOnly=S.textOnly})end S.imgCache,S.imgNat,S.imgData={},{},{} local budget=0.04 local sliceStart=0 local okc,computerLib=pcall(require,"computer") local function freeMem() if okc and computerLib and computerLib.freeMemory then return computerLib.freeMemory()end return math.huge end local lowCount=0 local function slice() if os.clock()-sliceStart>budget then if freeMem()<100*1024 then lowCount=lowCount+1 if lowCount>25 then error("not enough memory for the styled view",0)end else lowCount=0 end coroutine.yield() end end S.cssCache={} local function fetchInside(url,maxBytes) if not inet then return nil end local job=W.fetch(inet,url,{maxBytes=maxBytes}) while true do local st=job:step() if st=="done"then return job.body,job.ct end if st=="error"then return nil end coroutine.yield() end end local function styledPipeline(html,url) local D,CSS,LAY=require("ocdom"),require("occss"),require("oclayout") S.status="Parsing…" if collectgarbage then collectgarbage()end local free=freeMem() local maxNodes=free==math.huge and 8000 or math.max(500,math.min(8000,(free-300*1024)//900)) LAY.maxRows=math.max(150,math.min(3000,maxNodes//2)) local dom=D.parse(html,W.decodeEntities,maxNodes,slice) local vw=require("oclayout").viewport(S.cols) local rules={} local base=function(u)return W.resolve(url,u)end local sheets,total=0,0 D.walk(dom,function(n) if n.tag=="title"and n.children[1]then S.title=n.children[1].text:gsub("%s+"," ")end end) local list,present={},nil D.walk(dom,function(n) if n.css then list[#list+1]={css=n.css} elseif n.tag=="link"and(n.attrs.rel or""):lower():find("stylesheet")and n.attrs.href then local media=(n.attrs.media or"all"):lower() if not media:find("print")or media:find("screen")then list[#list+1]={href=base(n.attrs.href)}end end end) for _,it in ipairs(list)do local css,sheetUrl=it.css,url if it.href and sheets<6 and total<400*1024 then S.status="Loading style "..(it.href:match("[^/?]+[^/]*$")or it.href).."…" if o.onChange then o.onChange("status")end css=S.cssCache[it.href] if not css then css=fetchInside(it.href,300*1024) if css and#css<120*1024 then S.cssCache[it.href]=css end end sheets=sheets+1 sheetUrl=it.href if css then total=total+#css end end if css then present=present or CSS.present(dom) CSS.parse(W.sanitize(css),vw,rules,function(u)return W.resolve(sheetUrl,u)end,present) css=nil slice() end end S.status=string.format("Styling (%d rules)…",#rules) if o.onChange then o.onChange("status")end CSS.cascade(dom,rules,vw,slice) rules=nil LAY.natural=function(src)return S.imgNat[src]end local function doLayout() S.status="Layout…" local page=LAY.layout(dom,S.cols,base,slice) local used={} for _,op in ipairs(page.images)do used[op[6].."|"..op[4].."x"..op[5]]=true end for k in pairs(S.imgCache)do if not used[k]then S.imgCache[k]=nil end end S.styled=page S.styledDom=dom S.scroll=math.min(S.scroll or 0,math.max(0,page.height-1)) if o.onChange then o.onChange("page")end return page end local page=doLayout() local I=require("ocimg") local rawBudget=160*1024 local function decodeFor(pg,src,data) for _,op in ipairs(pg.images)do if op[6]==src then local key=src.."|"..op[4].."x"..op[5] if S.imgCache[key]==nil then local img=I.decode(data,op[4],op[5]*2,slice) S.imgCache[key]=img or false if o.onChange then o.onChange("page")end end end end end local seen,n,guessed={},0,false for _,op in ipairs(page.images)do local src=op[6] if src and src~=""and not seen[src]and not src:find("^data:")and n<30 then seen[src]=true n=n+1 S.status=string.format("Picture %d of %d…",n,#page.images) if o.onChange then o.onChange("status")end local data=fetchInside(src,400*1024) if data then local w,h=I.size(data) if w then if not S.imgNat[src]then guessed=true end S.imgNat[src]={w,h} decodeFor(page,src,data) if#data<=rawBudget then S.imgData[src]=data;rawBudget=rawBudget-#data end end end data=nil end end if guessed then page=doLayout() for src in pairs(seen)do local missing=false for _,op in ipairs(page.images)do if op[6]==src and S.imgCache[src.."|"..op[4].."x"..op[5]]==nil then missing=true end end if missing then local data=S.imgData[src]or fetchInside(src,400*1024) if data then decodeFor(page,src,data)end end end end S.status=string.format("%s · %d pictures%s",S.httpInfo or"",n,dom.truncated and" · long page cut (memory)"or"") if o.onChange then o.onChange("page")end end function S.picture(src,w,h) local c=S.imgCache[src.."|"..w.."x"..h] return c or nil end function S.layout() local body,isHtml=S.body or"",S.isHtml if S.source then isHtml=false end S.page=W.layout(body,S.width,S.colors,isHtml) S.title=(not S.source and S.page.title)or S.url S.proc=nil if isHtml and not S.textOnly and not S.source then local body2,url=body,S.url S.proc=coroutine.create(function()styledPipeline(body2,url)end) end end local function show(body,ct,url) S.frame=nil S.styled=nil S.cssCache,S.imgData,S.imgCache={},{},{} S.body=body S.ct=ct or"text/html" S.isHtml=S.ct:find("html")or body:sub(1,512):lower():find("200 do table.remove(S.globalHistory,1)end saveList("history",S.globalHistory) end if o.onChange then o.onChange("page")end end function S.stop() if S.job then S.job:cancel();S.job=nil;S.status="Stopped"end if S.proc then S.proc=nil;S.status="Stopped"end end function S.navigate(input,push) local url=W.normalize(input,o.searchUrl) if not url then return end if push~=false and S.url and S.url~=url then table.insert(S.back,S.url) S.fwd={} end S.stop() S.source=false if url:match("^about:")then S.status=url return show(aboutPage(url),"text/html",url) end if not url:match("^https?:")then S.status="Unsupported address: "..url;if o.onChange then o.onChange("status")end return end if S.renderMode then S.frame=S.frame return S.rOpen(url,push) end if S.body and url:gsub("#.*$","")==(S.url or""):gsub("#.*$","")and url:find("#")then S.url=url local a=S.page.anchors[url:match("#(.+)$")] if a then S.scroll=a-1 end if o.onChange then o.onChange("page")end return end if not inet then S.status="No internet card";return show(aboutPage("about:home"),"text/html","about:home")end S.job=W.fetch(inet,url) S.loadingUrl=url S.status="Connecting to "..(W.parse(url).host or url).."…" if o.onChange then o.onChange("status")end end function S.goBack() if S.renderMode and S.sid and S.frame then return S.rNav("back")end local u=table.remove(S.back)if u then table.insert(S.fwd,S.url)S.navigate(u,false)end end function S.goForward() if S.renderMode and S.sid and S.frame then return S.rNav("forward")end local u=table.remove(S.fwd)if u then table.insert(S.back,S.url)S.navigate(u,false)end end function S.reload() if S.renderMode and S.sid and S.frame then return S.rNav("reload")end S.navigate(S.url,false) end function S.step() if S.proc and not S.job then sliceStart=os.clock() local ok,err=coroutine.resume(S.proc) if not ok then S.status="Styled view failed: "..tostring(err).." (text view shown)";S.proc,S.styled=nil,nil end if S.proc and coroutine.status(S.proc)=="dead"then S.proc=nil end return true end local job=S.job if not job then return false end local st=job:step() if st=="done"and job.render then S.job=nil local f,err=W.parseFrame(job.body or"") if not f then S.status="Render server: "..tostring(err) if err and err:find("unknown session")then S.sid=nil end else S.sid,S.frame=f.sid,f if f.url~=S.url and S.url and not S.url:match("^about:")and job.url:find("/open?",1,true)==nil then table.insert(S.back,S.url) end S.url,S.title=f.url,(f.title~=""and f.title or f.url) S.status=string.format("Rendered · %s · rows %d-%d of %d",W.size(#job.body),f.scroll+1,f.scroll+f.rows,f.total) S.page=nil if not S.url:match("^about:")then table.insert(S.globalHistory,{url=S.url,title=S.title}) while#S.globalHistory>200 do table.remove(S.globalHistory,1)end saveList("history",S.globalHistory) end end if o.onChange then o.onChange("page")end return true elseif st=="error"and job.render then S.job=nil S.status="Render server unreachable: "..tostring(job.err).." (menu → Render server)" if o.onChange then o.onChange("status")end return true elseif st=="done"then S.job=nil local ct=job.ct or"" local text=ct==""or ct:find("text")or ct:find("html")or ct:find("json")or ct:find("xml")or ct:find("javascript") if not text and o.download then o.download(job.url,job.body,ct) S.status="Downloaded "..job.url return true end local body=W.decodeBody(job.body or"",ct) S.status=string.format("HTTP %s · %s%s",tostring(job.code),ui_size_(#(job.body or"")),job.truncated and" (truncated)"or"") S.httpInfo=S.status show(body,ct,job.url) return true elseif st=="error"then S.job=nil S.status="Error: "..tostring(job.err) show("Error

Can't open the page

"..esc(S.loadingUrl or"").. "

"..esc(tostring(job.err)).."

Home

","text/html",S.loadingUrl or"about:home") return true end S.status=string.format("Loading %s… %s",W.parse(job.url).host or"",job.received>0 and ui_size_(job.received)or"") return true end local cfgServer=loadList("server") S.server=cfgServer.url S.renderMode=cfgServer.on and S.server~=nil function S.setServer(url,on) S.server=url~=""and url or nil S.renderMode=(on and S.server~=nil)and true or false saveList("server",{url=S.server,on=S.renderMode}) end S.cols,S.rows=80,24 local function rq(cmd,params) local q={} for k,v in pairs(params or{})do q[#q+1]=k.."="..W.urlencode(tostring(v))end if S.sid then q[#q+1]="sid="..S.sid end return S.server:gsub("/+$","").."/"..cmd.."?"..table.concat(q,"&") end local function renderJob(cmd,params,label) if not inet then S.status="No internet card";return end if S.job then S.job:cancel()end S.job=W.fetch(inet,rq(cmd,params),{maxBytes=2*1024*1024,timeout=40}) S.job.render=true S.status=label or"Rendering…" if o.onChange then o.onChange("status")end end function S.rOpen(url,push) if push~=false and S.url and S.url~=url then table.insert(S.back,S.url);S.fwd={}end S.loadingUrl=url renderJob("open",{url=url,cols=S.cols,rows=S.rows},"Rendering "..(W.parse(url).host or url).."…") end function S.rClick(x,y)renderJob("click",{x=x,y=y},"Click…")end function S.rType(text)renderJob("type",{text=text},"Typing…")end function S.rKey(key)renderJob("key",{key=key},key.."…")end function S.rScroll(row) if not S.frame then return end row=math.max(0,math.min(row,math.max(0,S.frame.total-S.rows))) renderJob("frame",{row=row,cols=S.cols,rows=S.rows},"Scrolling…") end function S.rNav(cmd)renderJob(cmd,{},cmd.."…")end function S.submit(form,submitter) local D=require("ocdom") local fields={} D.walk(form,function(n) local a=n.attrs or{} if not n.tag or not a.name or a.disabled then return end if n.tag=="input"then local t=(a.type or"text"):lower() if t=="checkbox"or t=="radio"then if a.checked then fields[#fields+1]={a.name,a.value or"on"}end elseif t=="submit"or t=="button"or t=="image"or t=="reset"or t=="file"then if n==submitter then fields[#fields+1]={a.name,a.value or""}end else fields[#fields+1]={a.name,a.value or""}end elseif n.tag=="textarea"then fields[#fields+1]={a.name,a.value or D.text(n)} elseif n.tag=="select"then local val D.walk(n,function(o2)if o2.tag=="option"and(not val or o2.attrs.selected)then val=o2.attrs.value or D.text(o2)end end) fields[#fields+1]={a.name,val or""} elseif n.tag=="button"and n==submitter then fields[#fields+1]={a.name,a.value or""}end end) local q={} for _,f in ipairs(fields)do q[#q+1]=W.urlencode(f[1]).."="..W.urlencode(f[2])end local query=table.concat(q,"&") local action=W.resolve(S.url,form.attrs.action or S.url) if(form.attrs.method or"get"):lower()=="post"then if not inet then return end table.insert(S.back,S.url);S.fwd={} S.stop() S.job=W.fetch(inet,action,{post=query}) S.loadingUrl=action S.status="Sending form…" if o.onChange then o.onChange("status")end else S.navigate(action:gsub("%?.*$","").."?"..query) end end function S.isBookmarked()for i,b in ipairs(S.bookmarks)do if b.url==S.url then return i end end end function S.toggleBookmark() local i=S.isBookmarked() if i then table.remove(S.bookmarks,i)else table.insert(S.bookmarks,1,{url=S.url,title=S.title})end saveList("bookmarks",S.bookmarks) return not i end function S.toggleSource()S.source=not S.source;S.layout();S.scroll=0 end function S.find(text,from) if not S.page or text==""then return end local q=unicode.lower(text) local n=#S.page.lines for k=1,n do local i=(from+k-1)%n+1 if unicode.lower(W.lineText(S.page.lines[i])):find(q,1,true)then return i end end end function S.resize(width) if width~=S.width then S.width=width S.cols=width+1 if S.body then S.layout()end end end return S end local R_LV,G_LV,B_LV={},{},{} for i=0,5 do R_LV[i]=math.floor(i*255/5+0.5)end for i=0,7 do G_LV[i]=math.floor(i*255/7+0.5)end for i=0,4 do B_LV[i]=math.floor(i*255/4+0.5)end function W.cubeRGB(i) i=i-16 if i<0 then i=0 end return R_LV[i//40]<<16|G_LV[(i//5)%8]<<8|B_LV[i%5] end function W.parseFrame(data) local nl=data:find("\n",1,true) if data:sub(1,3)=="ERR"then return nil,data:sub(5)end if not nl or data:sub(1,4)~="SID "then return nil,"not a render server answer"end local sid=data:sub(5,nl-1) local p=nl+1 if data:sub(p,p+3)~="OCF2"then return nil,"bad frame"end p=p+4 local f={sid=sid} f.cols,f.rows,f.total,f.scroll,f.nlinks,f.ninputs,p=string.unpack(">I2I2I2I2I2I2",data,p) local function str() local s s,p=string.unpack(">s2",data,p) return s end f.url,f.title=str(),str() local cells={} local byte,sub=string.byte,string.sub for i=1,f.cols*f.rows do local t,b,fg,n=byte(data,p,p+3) p=p+4 if n and n>0 then cells[i]={utf8.codepoint(sub(data,p,p+n-1)),fg,t} p=p+n else cells[i]={false,t,b} end end f.cells=cells f.links,f.inputs={},{} for i=1,f.nlinks do local x,y,w,h x,y,w,h,p=string.unpack(">I2I2I2I2",data,p) f.links[i]={x=x,y=y,w=w,h=h,href=str(),text=str()} end for i=1,f.ninputs do local x,y,w,h x,y,w,h,p=string.unpack(">I2I2I2I2",data,p) f.inputs[i]={x=x,y=y,w=w,h=h,type=str(),value=str(),placeholder=str()} end return f end function W.lineText(line) local t={} for _,s in ipairs(line.segs)do t[#t+1]=s.text end return string.rep(" ",line.indent or 0)..table.concat(t) end return W