from std/strutils import split, toUpperAscii import std/macros import std/nativesockets import std/net import std/options import std/os import std/posix import std/selectors import std/streams import std/tables import std/unicode import bindings/quickjs import config/config import css/cascade import css/cssparser import css/mediaquery import css/sheet import css/stylednode import css/values 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/bufstream import io/bufwriter import io/posixstream import io/promise import io/serialize import io/serversocket import io/socketstream import js/fromjs import js/javascript import js/regex import js/timeout import js/tojs import layout/renderdocument import loader/headers import loader/loader import types/cell import types/color import types/cookie import types/formdata import types/opt import types/url import types/winattrs import utils/strwidth import utils/twtstr from chagashi/decoder import newTextDecoder import chagashi/charset import chagashi/decodercore import chagashi/validatorcore import chame/tags type BufferCommand* = enum bcPassFd, bcLoad, bcForceRender, bcWindowChange, bcFindAnchor, bcReadSuccess, bcReadCanceled, bcClick, bcFindNextLink, bcFindPrevLink, bcFindNthLink, bcFindRevNthLink, bcFindNextMatch, bcFindPrevMatch, bcGetLines, bcUpdateHover, bcGotoAnchor, bcCancel, bcGetTitle, bcSelect, bcClone, bcFindPrevParagraph, bcFindNextParagraph BufferState = enum bsLoadingPage, bsLoadingResources, bsLoaded HoverType* = enum htTitle = "TITLE" htLink = "URL" htImage = "IMAGE" BufferMatch* = object success*: bool x*: int y*: int str*: string Buffer* = ref object rfd: int # file descriptor of command pipe fd: int # file descriptor of buffer source url: URL # URL before readFromFd pstream: SocketStream # control stream savetask: bool ishtml: bool firstBufferRead: bool lines: FlexibleGrid request: Request # source request attrs: WindowAttributes window: Window document: Document prevStyled: StyledNode selector: Selector[int] istream: PosixStream bytesRead: int reportedBytesRead: int state: BufferState prevnode: StyledNode loader: FileLoader config: BufferConfig tasks: array[BufferCommand, int] #TODO this should have arguments hoverText: array[HoverType, string] estream: Stream # error stream ssock: ServerSocket factory: CAtomFactory uastyle: CSSStylesheet quirkstyle: CSSStylesheet userstyle: CSSStylesheet htmlParser: HTML5ParserWrapper bgcolor: CellColor needsBOMSniff: bool decoder: TextDecoder validator: ref TextValidatorUTF8 validateBuf: seq[char] charsetStack: seq[Charset] charset: Charset cacheId: int outputId: int InterfaceOpaque = ref object stream: Stream len: int BufferInterface* = ref object map: PromiseMap packetid: int opaque: InterfaceOpaque stream*: BufStream BufferConfig* = object userstyle*: string referer_from*: bool scripting*: bool images*: bool isdump*: bool charsets*: seq[Charset] charsetOverride*: Charset proc getFromOpaque[T](opaque: pointer, res: var T) = let opaque = cast[InterfaceOpaque](opaque) if opaque.len != 0: opaque.stream.sread(res) proc newBufferInterface*(stream: SocketStream, registerFun: proc(fd: int)): BufferInterface = let opaque = InterfaceOpaque(stream: stream) result = BufferInterface( map: newPromiseMap(cast[pointer](opaque)), packetid: 1, # ids below 1 are invalid opaque: opaque, 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 len: int var pid: int stream.sread(len) stream.sread(iface.packetid) stream.sread(pid) return iface proc resolve*(iface: BufferInterface, packetid, len: int) = iface.opaque.len = len iface.map.resolve(packetid) proc hasPromises*(iface: BufferInterface): bool = return not iface.map.empty() # 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] body.add(quote do: var writer {.inject.} = `thisval`.stream.initWriter() writer.swrite(BufferCommand.`nup`) writer.swrite(`thisval`.packetid) ) var params2: seq[NimNode] var retval2: NimNode var addfun: NimNode if retval.kind == nnkEmpty: addfun = quote do: `thisval`.map.addEmptyPromise(`thisval`.packetid) retval2 = ident("EmptyPromise") else: addfun = quote do: addPromise[`retval`](`thisval`.map, `thisval`.packetid, getFromOpaque[`retval`]) 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) for i in 2 ..< params2.len: let s = params2[i][0] # sym e.g. url body.add(quote do: when typeof(`s`) is FileHandle: writer.flush() SocketStream(`thisval`.stream.source).sendFileHandle(`s`) else: 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 proc passFd*(buffer: Buffer; fd: FileHandle; cacheId: int) {.proxy.} = buffer.fd = fd buffer.istream = newPosixStream(fd) buffer.istream.setBlocking(false) buffer.selector.registerHandle(fd, {Read}, 0) buffer.cacheId = cacheId func getTitleAttr(node: StyledNode): string = if node == nil: return "" if node.t == STYLED_ELEMENT and node.node != nil: let element = Element(node.node) if element.attrb(atTitle): return element.attr(atTitle) if node.node != nil: var node = node.node for element in node.ancestors: if element.attrb(atTitle): return element.attr(atTitle) #TODO pseudo-elements return "" const ClickableElements = { TAG_A, TAG_INPUT, TAG_OPTION, TAG_BUTTON, TAG_TEXTAREA, TAG_LABEL } func isClickable(styledNode: StyledNode): bool = if styledNode.t != STYLED_ELEMENT or styledNode.node == nil: return false if styledNode.computed{"visibility"} != VISIBILITY_VISIBLE: return false let element = Element(styledNode.node) if element of HTMLAnchorElement: return HTMLAnchorElement(element).href != "" return element.tagType in ClickableElements func getClickable(styledNode: StyledNode): Element = var styledNode = styledNode while styledNode != nil: if styledNode.isClickable(): return Element(styledNode.node) styledNode = styledNode.parent proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] 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 == BUTTON_SUBMIT: return true if fae of HTMLInputElement and HTMLInputElement(fae).inputType in {INPUT_SUBMIT, INPUT_BUTTON}: return true return false proc getClickHover(styledNode: StyledNode): string = let clickable = styledNode.getClickable() if clickable != nil: if clickable of HTMLAnchorElement: return HTMLAnchorElement(clickable).href elif clickable of FormAssociatedElement: #TODO this is inefficient and also quite stupid let fae = FormAssociatedElement(clickable) if fae.canSubmitOnClick(): let req = fae.form.submitForm(fae) if req.isSome: return $req.get.url return "<" & $clickable.tagType & ">" elif clickable of HTMLOptionElement: return "