from std/strutils import split, toUpperAscii, find, AllChars import std/macros import std/options import std/os import std/posix import std/tables import chagashi/charset import chagashi/decoder import chagashi/decodercore import chame/tags import config/config import css/box import css/cascade import css/layout import css/lunit import css/render import css/sheet import css/stylednode import html/catom import html/chadombuilder import html/dom import html/enums import html/env import html/event import html/formdata as formdata_impl import io/bufreader import io/bufwriter import io/console import io/dynstream import io/poll import io/promise import io/timeout import local/select import monoucha/fromjs import monoucha/javascript import monoucha/jsregex import monoucha/libregexp import monoucha/quickjs import server/headers import server/loaderiface import server/request import types/blob import types/cell import types/color import types/formdata import types/opt import types/url import types/winattrs import utils/strwidth import utils/twtstr type BufferCommand* = enum bcLoad, bcForceRender, bcWindowChange, bcReadSuccess, bcReadCanceled, bcClick, bcFindNextLink, bcFindPrevLink, bcFindNthLink, bcFindRevNthLink, bcFindNextMatch, bcFindPrevMatch, bcGetLines, bcUpdateHover, bcGotoAnchor, bcCancel, bcGetTitle, bcSelect, bcClone, bcFindPrevParagraph, bcFindNextParagraph, bcMarkURL, bcToggleImages, bcCheckRefresh BufferState = enum bsLoadingPage, bsLoadingResources, bsLoaded HoverType* = enum htTitle, htLink, htImage, htCachedImage BufferMatch* = object success*: bool x*: int y*: int str*: string Buffer = ref object attrs: WindowAttributes bgcolor: CellColor bytesRead: int cacheId: int charset: Charset charsetStack: seq[Charset] config: BufferConfig ctx: TextDecoderContext document: Document estream: DynFileStream # error stream factory: CAtomFactory fd: int # file descriptor of buffer source firstBufferRead: bool hoverText: array[HoverType, string] htmlParser: HTML5ParserWrapper images: seq[PosBitmap] ishtml: bool istream: PosixStream lines: FlexibleGrid loader: FileLoader needsBOMSniff: bool outputId: int pollData: PollData prevHover: Element prevStyled: StyledNode pstream: SocketStream # control stream quirkstyle: CSSStylesheet reportedBytesRead: int rfd: int # file descriptor of command pipe rootBox: BlockBox savetask: bool ssock: ServerSocket state: BufferState tasks: array[BufferCommand, int] #TODO this should have arguments uastyle: CSSStylesheet url: URL # URL before readFromFd userstyle: CSSStylesheet window: Window BufferIfaceItem = object id: int p: EmptyPromise get: GetValueProc BufferInterface* = ref object map: seq[BufferIfaceItem] packetid: int len: int auxLen: int stream*: BufStream BufferConfig* = object userstyle*: string refererFrom*: bool styling*: bool scripting*: bool images*: bool isdump*: bool autofocus*: bool charsetOverride*: Charset metaRefresh*: MetaRefresh charsets*: seq[Charset] protocol*: Table[string, ProtocolConfig] imageTypes*: Table[string, string] GetValueProc = proc(iface: BufferInterface; promise: EmptyPromise) {.nimcall.} # Forward declarations proc submitForm(buffer: Buffer; form: HTMLFormElement; submitter: Element): Request proc getFromStream[T](iface: BufferInterface; promise: EmptyPromise) = if iface.len != 0: let promise = Promise[T](promise) var r = iface.stream.initReader(iface.len, iface.auxLen) r.sread(promise.res) iface.len = 0 proc addPromise[T](iface: BufferInterface; id: int): Promise[T] = let promise = Promise[T]() iface.map.add(BufferIfaceItem(id: id, p: promise, get: getFromStream[T])) return promise proc addEmptyPromise(iface: BufferInterface; id: int): EmptyPromise = let promise = EmptyPromise() iface.map.add(BufferIfaceItem(id: id, p: promise, get: nil)) return promise func findPromise(iface: BufferInterface; id: int): int = for i in 0 ..< iface.map.len: if iface.map[i].id == id: return i return -1 proc resolve(iface: BufferInterface; id: int) = let i = iface.findPromise(id) if i != -1: let it = iface.map[i] if it.get != nil: it.get(iface, it.p) it.p.resolve() iface.map.del(i) proc newBufferInterface*(stream: SocketStream; registerFun: proc(fd: int)): BufferInterface = return BufferInterface( packetid: 1, # ids below 1 are invalid stream: newBufStream(stream, registerFun) ) # After cloning a buffer, we need a new interface to the new buffer process. # Here we create a new interface for that clone. proc cloneInterface*(stream: SocketStream; registerFun: proc(fd: int)): BufferInterface = let iface = newBufferInterface(stream, registerFun) #TODO buffered data should probably be copied here # We have just fork'ed the buffer process inside an interface function, # from which the new buffer is going to return as well. So we must also # consume the return value of the clone function, which is the pid 0. var pid: int var r = stream.initPacketReader() r.sread(iface.packetid) r.sread(pid) return iface proc resolve*(iface: BufferInterface; packetid, len, auxLen: int) = iface.len = len iface.auxLen = auxLen iface.resolve(packetid) # Protection against accidentally not exhausting data available to read, # by setting len to 0 in getFromStream. # (If this assertion is failing, then it means you then()'ed a promise which # should read something from the stream with an empty function.) assert iface.len == 0 proc hasPromises*(iface: BufferInterface): bool = return iface.map.len > 0 # get enum identifier of proxy function func getFunId(fun: NimNode): string = let name = fun[0] # sym return "bc" & name.strVal[0].toUpperAscii() & name.strVal.substr(1) proc buildInterfaceProc(fun: NimNode; funid: string): tuple[fun, name: NimNode] = let name = fun[0] # sym let params = fun[3] # formalparams let retval = params[0] # sym var body = newStmtList() assert params.len >= 2 # return type, this value let nup = ident(funid) # add this to enums let this2 = newIdentDefs(ident("iface"), ident("BufferInterface")) let thisval = this2[0] var params2: seq[NimNode] = @[] var retval2: NimNode var addfun: NimNode if retval.kind == nnkEmpty: addfun = quote do: `thisval`.addEmptyPromise(`thisval`.packetid) retval2 = ident("EmptyPromise") else: addfun = quote do: addPromise[`retval`](`thisval`, `thisval`.packetid) retval2 = newNimNode(nnkBracketExpr).add(ident("Promise"), retval) params2.add(retval2) params2.add(this2) # flatten args for i in 2 ..< params.len: let param = params[i] for i in 0 ..< param.len - 2: let id2 = newIdentDefs(ident(param[i].strVal), param[^2]) params2.add(id2) body.add(quote do: var writer {.inject.} = `thisval`.stream.initWriter() writer.swrite(BufferCommand.`nup`) writer.swrite(`thisval`.packetid) ) for i in 2 ..< params2.len: let s = params2[i][0] # sym e.g. url body.add(quote do: writer.swrite(`s`) ) body.add(quote do: writer.flush() writer.deinit() let promise = `addfun` inc `thisval`.packetid return promise ) var pragmas: NimNode if retval.kind == nnkEmpty: pragmas = newNimNode(nnkPragma).add(ident("discardable")) else: pragmas = newEmptyNode() return (newProc(name, params2, body, pragmas = pragmas), nup) type ProxyFunction = ref object iname: NimNode # internal name ename: NimNode # enum name params: seq[NimNode] istask: bool ProxyMap = Table[string, ProxyFunction] # Name -> ProxyFunction var ProxyFunctions {.compileTime.}: ProxyMap proc getProxyFunction(funid: string): ProxyFunction = if funid notin ProxyFunctions: ProxyFunctions[funid] = ProxyFunction() return ProxyFunctions[funid] macro proxy0(fun: untyped) = fun[0] = ident(fun[0].strVal & "_internal") return fun macro proxy1(fun: typed) = let funid = getFunId(fun) let iproc = buildInterfaceProc(fun, funid) let pfun = getProxyFunction(funid) pfun.iname = ident(fun[0].strVal & "_internal") pfun.ename = iproc[1] pfun.params.add(fun[3][0]) var params2: seq[NimNode] params2.add(fun[3][0]) for i in 1 ..< fun[3].len: let param = fun[3][i] pfun.params.add(param) for i in 0 ..< param.len - 2: let id2 = newIdentDefs(ident(param[i].strVal), param[^2]) params2.add(id2) ProxyFunctions[funid] = pfun return iproc[0] macro proxy(fun: typed) = quote do: proxy0(`fun`) proxy1(`fun`) macro task(fun: typed) = let funid = getFunId(fun) let pfun = getProxyFunction(funid) pfun.istask = true fun func getTitleAttr(buffer: Buffer; element: Element): string = if element != nil: for element in element.branchElems: if element.attrb(satTitle): return element.attr(satTitle) return "" const ClickableElements = { TAG_A, TAG_INPUT, TAG_OPTION, TAG_BUTTON, TAG_TEXTAREA, TAG_LABEL } proc isClickable(element: Element): bool = if element of HTMLAnchorElement: return HTMLAnchorElement(element).reinitURL().isSome if element.isButton() and FormAssociatedElement(element).form == nil: return false return element.tagType in ClickableElements proc getClickable(element: Element): Element = for element in element.branchElems: if element.isClickable(): return element return nil proc getClickable(styledNode: StyledNode): Element = var styledNode = styledNode while styledNode != nil: if styledNode.node of Element: return Element(styledNode.node).getClickable() styledNode = styledNode.parent return nil func canSubmitOnClick(fae: FormAssociatedElement): bool = if fae.form == nil: return false if fae.form.canSubmitImplicitly(): return true if fae of HTMLButtonElement and HTMLButtonElement(fae).ctype == btSubmit: return true if fae of HTMLInputElement and HTMLInputElement(fae).inputType in {itSubmit, itButton}: return true return false proc getClickHover(buffer: Buffer; element: Element): string = let clickable = element.getClickable() if clickable != nil: if clickable of HTMLAnchorElement: let url = HTMLAnchorElement(clickable).reinitURL() if url.isSome: return $url.get elif clickable of FormAssociatedElement: #TODO this is inefficient and also quite stupid let fae = FormAssociatedElement(clickable) if fae.canSubmitOnClick(): let req = buffer.submitForm(fae.form, fae) if req != nil: return $req.url return "<" & $clickable.tagType & ">" elif clickable of HTMLOptionElement: return "