diff options
author | bptato <nincsnevem662@gmail.com> | 2023-09-14 01:41:47 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-09-14 02:01:21 +0200 |
commit | c1b8338045716b25d664c0b8dd91eac0cb76480e (patch) | |
tree | a9c0a6763f180c2b6dd380aa880253ffc7685d85 /src/buffer/buffer.nim | |
parent | db0798acccbedcef4b16737f6be0cf7388cc0528 (diff) | |
download | chawan-c1b8338045716b25d664c0b8dd91eac0cb76480e.tar.gz |
move around more modules
* ips -> io/ * loader related stuff -> loader/ * tempfile -> extern/ * buffer, forkserver -> server/ * lineedit, window -> display/ * cell -> types/ * opt -> types/
Diffstat (limited to 'src/buffer/buffer.nim')
-rw-r--r-- | src/buffer/buffer.nim | 1481 |
1 files changed, 0 insertions, 1481 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim deleted file mode 100644 index 4616704e..00000000 --- a/src/buffer/buffer.nim +++ /dev/null @@ -1,1481 +0,0 @@ -import macros -import nativesockets -import net -import options -import os -import posix -import selectors -import streams -import tables -import unicode - -import bindings/quickjs -import buffer/cell -import config/config -import css/cascade -import css/cssparser -import css/mediaquery -import css/sheet -import css/stylednode -import css/values -import html/chadombuilder -import html/dom -import html/env -import html/event -import img/png -import io/connecterror -import io/loader -import io/posixstream -import io/promise -import io/teestream -import io/window -import ips/serialize -import ips/serversocket -import ips/socketstream -import js/error -import js/fromjs -import js/javascript -import js/regex -import js/timeout -import layout/box -import render/renderdocument -import render/rendertext -import types/buffersource -import types/color -import types/cookie -import types/formdata -import types/referer -import types/url -import utils/opt -import utils/twtstr -import xhr/formdata as formdata_impl - -import chakasu/charset -import chakasu/decoderstream - -import chame/tags - -type - LoadInfo* = enum - CONNECT, DOWNLOAD, RENDER, DONE - - BufferCommand* = enum - LOAD, RENDER, WINDOW_CHANGE, FIND_ANCHOR, READ_SUCCESS, READ_CANCELED, - CLICK, FIND_NEXT_LINK, FIND_PREV_LINK, FIND_NEXT_MATCH, FIND_PREV_MATCH, - GET_SOURCE, GET_LINES, UPDATE_HOVER, PASS_FD, CONNECT, CONNECT2, - GOTO_ANCHOR, CANCEL, GET_TITLE, SELECT, REDIRECT_TO_FD, READ_FROM_FD, - SET_CONTENT_TYPE - - # LOADING_PAGE: istream open - # LOADING_RESOURCES: istream closed, resources open - # LOADED: istream closed, resources closed - BufferState* = enum - LOADING_PAGE, LOADING_RESOURCES, LOADED - - HoverType* = enum - HOVER_TITLE = "TITLE" - HOVER_LINK = "URL" - - 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 - alive: bool - readbufsize: int - contenttype: string #TODO already stored in source - lines: FlexibleGrid - rendered: bool - source: BufferSource - width: int - height: int - attrs: WindowAttributes - window: Window - document: Document - viewport: Viewport - prevstyled: StyledNode - selector: Selector[int] - istream: Stream - sstream: Stream - available: int - pstream: Stream # pipe stream - srenderer: StreamRenderer - connected: bool - state: BufferState - prevnode: StyledNode - loader: FileLoader - config: BufferConfig - userstyle: CSSStylesheet - tasks: array[BufferCommand, int] #TODO this should have arguments - savetask: bool - hovertext: array[HoverType, string] - estream: Stream # error stream - - InterfaceOpaque = ref object - stream: Stream - len: int - - BufferInterface* = ref object - map: PromiseMap - packetid: int - opaque: InterfaceOpaque - stream*: Stream - -proc getFromOpaque[T](opaque: pointer, res: var T) = - let opaque = cast[InterfaceOpaque](opaque) - if opaque.len != 0: - opaque.stream.sread(res) - -proc newBufferInterface*(stream: Stream): BufferInterface = - let opaque = InterfaceOpaque(stream: stream) - result = BufferInterface( - map: newPromiseMap(cast[pointer](opaque)), - packetid: 1, # ids below 1 are invalid - opaque: opaque, - stream: stream - ) - -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 - result = name.strVal.toScreamingSnakeCase() - if result[^1] == '=': - result = "SET_" & result[0..^2] - -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: - `thisval`.stream.swrite(BufferCommand.`nup`) - `thisval`.stream.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) - 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: - SocketStream(`thisval`.stream).sendFileHandle(`s`) - else: - `thisval`.stream.swrite(`s`)) - body.add(quote do: - `thisval`.stream.flush()) - body.add(quote do: - 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 url(buffer: Buffer): URL = - return buffer.source.location - -func charsets(buffer: Buffer): seq[Charset] = - if buffer.source.charset != CHARSET_UNKNOWN: - return @[buffer.source.charset] - return buffer.config.charsets - -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("title"): - return element.attr("title") - if node.node != nil: - var node = node.node - for element in node.ancestors: - if element.attrb("title"): - return element.attr("title") - #TODO pseudo-elements - -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.tagType == TAG_A: - 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 - -func 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.tagType == TAG_BUTTON: - if HTMLButtonElement(fae).ctype == BUTTON_SUBMIT: - return true - if fae.tagType == TAG_INPUT: - if HTMLInputElement(fae).inputType in {INPUT_SUBMIT, INPUT_BUTTON}: - return true - return false - -func getClickHover(styledNode: StyledNode): string = - let clickable = styledNode.getClickable() - if clickable != nil: - case clickable.tagType - of TAG_A: - return HTMLAnchorElement(clickable).href - of TAG_INPUT: - #TODO this is inefficient and also quite stupid - if clickable.tagType in FormAssociatedElements: - let fae = FormAssociatedElement(clickable) - if fae.canSubmitOnClick(): - let req = fae.form.submitForm(fae) - if req.isSome: - return $req.get.url - return "<input>" - of TAG_OPTION: - return "<option>" - of TAG_BUTTON: - return "<button>" - of TAG_TEXTAREA: - return "<textarea>" - else: discard - -func getCursorStyledNode(buffer: Buffer, cursorx, cursory: int): StyledNode = - let i = buffer.lines[cursory].findFormatN(cursorx) - 1 - if i >= 0: - return buffer.lines[cursory].formats[i].node - -func getCursorElement(buffer: Buffer, cursorx, cursory: int): Element = - let styledNode = buffer.getCursorStyledNode(cursorx, cursory) - if styledNode == nil or styledNode.node == nil: - return nil - if styledNode.t == STYLED_ELEMENT: - return Element(styledNode.node) - return styledNode.node.parentElement - -func getCursorClickable(buffer: Buffer, cursorx, cursory: int): Element = - let styledNode = buffer.getCursorStyledNode(cursorx, cursory) - if styledNode != nil: - return styledNode.getClickable() - -func cursorBytes(buffer: Buffer, y: int, cc: int): int = - let line = buffer.lines[y].str - var w = 0 - var i = 0 - while i < line.len and w < cc: - var r: Rune - fastRuneAt(line, i, r) - w += r.twidth(w) - return i - -proc navigate(buffer: Buffer, url: URL) = - #TODO how? - eprint "navigate to", url - -proc findPrevLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} = - if cursory >= buffer.lines.len: return (-1, -1) - let line = buffer.lines[cursory] - var i = line.findFormatN(cursorx) - 1 - var link: Element = nil - if i >= 0: - link = line.formats[i].node.getClickable() - dec i - - var ly = 0 #last y - var lx = 0 #last x - template link_beginning() = - #go to beginning of link - ly = y #last y - lx = format.pos #last x - - #on the current line - let line = buffer.lines[y] - while i >= 0: - let format = line.formats[i] - let nl = format.node.getClickable() - if nl == fl: - lx = format.pos - dec i - - #on previous lines - for iy in countdown(ly - 1, 0): - let line = buffer.lines[iy] - i = line.formats.len - 1 - while i >= 0: - let format = line.formats[i] - let nl = format.node.getClickable() - if nl == fl: - ly = iy - lx = format.pos - dec i - - while i >= 0: - let format = line.formats[i] - let fl = format.node.getClickable() - if fl != nil and fl != link: - let y = cursory - link_beginning - return (lx, ly) - dec i - - for y in countdown(cursory - 1, 0): - let line = buffer.lines[y] - i = line.formats.len - 1 - while i >= 0: - let format = line.formats[i] - let fl = format.node.getClickable() - if fl != nil and fl != link: - link_beginning - return (lx, ly) - dec i - return (-1, -1) - -proc findNextLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} = - if cursory >= buffer.lines.len: return (-1, -1) - let line = buffer.lines[cursory] - var i = line.findFormatN(cursorx) - 1 - var link: Element = nil - if i >= 0: - link = line.formats[i].node.getClickable() - inc i - - while i < line.formats.len: - let format = line.formats[i] - let fl = format.node.getClickable() - if fl != nil and fl != link: - return (format.pos, cursory) - inc i - - for y in (cursory + 1)..(buffer.lines.len - 1): - let line = buffer.lines[y] - i = 0 - while i < line.formats.len: - let format = line.formats[i] - let fl = format.node.getClickable() - if fl != nil and fl != link: - return (format.pos, y) - inc i - return (-1, -1) - -proc findPrevMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} = - if cursory >= buffer.lines.len: return - var y = cursory - let b = buffer.cursorBytes(y, cursorx) - let res = regex.exec(buffer.lines[y].str, 0, b) - if res.success and res.captures.len > 0: - let cap = res.captures[^1] - let x = buffer.lines[y].str.width(0, cap.s) - let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) - return BufferMatch(success: true, x: x, y: y, str: str) - dec y - while true: - if y < 0: - if wrap: - y = buffer.lines.high - else: - break - let res = regex.exec(buffer.lines[y].str) - if res.success and res.captures.len > 0: - let cap = res.captures[^1] - let x = buffer.lines[y].str.width(0, cap.s) - let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) - return BufferMatch(success: true, x: x, y: y, str: str) - if y == cursory: - break - dec y - -proc findNextMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} = - if cursory >= buffer.lines.len: return - var y = cursory - let b = buffer.cursorBytes(y, cursorx + 1) - let res = regex.exec(buffer.lines[y].str, b, buffer.lines[y].str.len) - if res.success and res.captures.len > 0: - let cap = res.captures[0] - let x = buffer.lines[y].str.width(0, cap.s) - let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) - return BufferMatch(success: true, x: x, y: y, str: str) - inc y - while true: - if y > buffer.lines.high: - if wrap: - y = 0 - else: - break - let res = regex.exec(buffer.lines[y].str) - if res.success and res.captures.len > 0: - let cap = res.captures[0] - let x = buffer.lines[y].str.width(0, cap.s) - let str = buffer.lines[y].str.substr(cap.s, cap.e - 1) - return BufferMatch(success: true, x: x, y: y, str: str) - if y == cursory: - break - inc y - -proc gotoAnchor*(buffer: Buffer): tuple[x, y: int] {.proxy.} = - if buffer.document == nil: return (-1, -1) - let anchor = buffer.document.getElementById(buffer.url.anchor) - if anchor == nil: return - for y in 0 ..< buffer.lines.len: - let line = buffer.lines[y] - for i in 0 ..< line.formats.len: - let format = line.formats[i] - if format.node != nil and anchor in format.node.node: - return (format.pos, y) - return (-1, -1) - -proc do_reshape(buffer: Buffer) = - case buffer.contenttype - of "text/html": - if buffer.viewport == nil: - buffer.viewport = Viewport(window: buffer.attrs) - let ret = renderDocument(buffer.document, buffer.userstyle, - buffer.viewport, buffer.prevstyled) - buffer.lines = ret.grid - buffer.prevstyled = ret.styledRoot - else: - buffer.lines.renderStream(buffer.srenderer, buffer.available) - buffer.available = 0 - -proc windowChange*(buffer: Buffer, attrs: WindowAttributes) {.proxy.} = - buffer.attrs = attrs - buffer.viewport = Viewport(window: buffer.attrs) - buffer.width = buffer.attrs.width - buffer.height = buffer.attrs.height - 1 - -type UpdateHoverResult* = object - link*: Option[string] - title*: Option[string] - repaint*: bool - -proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.proxy.} = - if buffer.lines.len == 0: return - var thisnode: StyledNode - let i = buffer.lines[cursory].findFormatN(cursorx) - 1 - if i >= 0: - thisnode = buffer.lines[cursory].formats[i].node - let prevnode = buffer.prevnode - - if thisnode != prevnode and (thisnode == nil or prevnode == nil or thisnode.node != prevnode.node): - for styledNode in thisnode.branch: - if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: - let elem = Element(styledNode.node) - if not elem.hover: - elem.hover = true - result.repaint = true - - let title = thisnode.getTitleAttr() - if buffer.hovertext[HOVER_TITLE] != title: - result.title = some(title) - buffer.hovertext[HOVER_TITLE] = title - let click = thisnode.getClickHover() - if buffer.hovertext[HOVER_LINK] != click: - result.link = some(click) - buffer.hovertext[HOVER_LINK] = click - - for styledNode in prevnode.branch: - if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: - let elem = Element(styledNode.node) - if elem.hover: - elem.hover = false - result.repaint = true - if result.repaint: - buffer.do_reshape() - - buffer.prevnode = thisnode - -proc loadResource(buffer: Buffer, elem: HTMLLinkElement): EmptyPromise = - let document = buffer.document - let href = elem.attr("href") - if href == "": return - let url = parseURL(href, document.url.some) - if url.isSome: - let url = url.get - let media = elem.media - if media != "": - let cvals = parseListOfComponentValues(newStringStream(media)) - let media = parseMediaQueryList(cvals) - if not media.applies(document.window): return - return buffer.loader.fetch(newRequest(url)) - .then(proc(res: JSResult[Response]): Promise[JSResult[string]] = - if res.isOk: - let res = res.get - #TODO we should use ReadableStreams for this (which would allow us to - # parse CSS asynchronously) - if res.contenttype == "text/css": - return res.text() - res.unregisterFun() - ).then(proc(s: JSResult[string]) = - if s.isOk: - #TODO this is extremely inefficient, and text() should return - # utf8 anyways - let ss = newStringStream(s.get) - #TODO non-utf-8 css - let source = newDecoderStream(ss, cs = CHARSET_UTF_8).readAll() - let ss2 = newStringStream(source) - elem.sheet = parseStylesheet(ss2)) - -proc loadResource(buffer: Buffer, elem: HTMLImageElement): EmptyPromise = - let document = buffer.document - let src = elem.attr("src") - if src == "": return - let url = parseURL(src, document.url.some) - if url.isSome: - let url = url.get - return buffer.loader.fetch(newRequest(url)) - .then(proc(res: JSResult[Response]): Promise[JSResult[string]] = - if res.isErr: - return - let res = res.get - if res.contenttype == "image/png": - #TODO using text() for PNG is wrong - return res.text() - ).then(proc(pngData: JSResult[string]) = - if pngData.isErr: - return - let pngData = pngData.get - elem.bitmap = fromPNG(toOpenArrayByte(pngData, 0, pngData.high))) - -proc loadResources(buffer: Buffer): EmptyPromise = - let document = buffer.document - var promises: seq[EmptyPromise] - if document.html != nil: - var searchElems = {TAG_LINK} - if buffer.config.images: - searchElems.incl(TAG_IMG) - for elem in document.html.elements(searchElems): - var p: EmptyPromise = nil - case elem.tagType - of TAG_LINK: - let elem = HTMLLinkElement(elem) - if elem.rel == "stylesheet": - p = buffer.loadResource(elem) - of TAG_IMG: - let elem = HTMLImageElement(elem) - p = buffer.loadResource(elem) - else: discard - if p != nil: - promises.add(p) - return all(promises) - -type ConnectResult* = object - invalid*: bool - code*: int - needsAuth*: bool - redirect*: Request - contentType*: string - cookies*: seq[Cookie] - referrerpolicy*: Option[ReferrerPolicy] - charset*: Charset - -proc connect*(buffer: Buffer): ConnectResult {.proxy.} = - if buffer.connected: - return ConnectResult(invalid: true) - let source = buffer.source - # Warning: source content type overrides received content types, but source - # charset is just a fallback. - let setct = source.contenttype.isNone - if not setct: - buffer.contenttype = source.contenttype.get - var charset = source.charset - var needsAuth = false - var redirect: Request - var cookies: seq[Cookie] - var referrerpolicy: Option[ReferrerPolicy] - case source.t - of CLONE: - #TODO clone should probably just fork() the buffer instead. - let s = connectSocketStream(source.clonepid, blocking = false) - buffer.istream = s - buffer.fd = int(s.source.getFd()) - if buffer.istream == nil: - return ConnectResult(code: ERROR_SOURCE_NOT_FOUND) - if setct: - buffer.contenttype = "text/plain" - of LOAD_PIPE: - discard fcntl(source.fd, F_SETFL, fcntl(source.fd, F_GETFL, 0) or O_NONBLOCK) - buffer.istream = newPosixStream(source.fd) - buffer.fd = source.fd - if setct: - buffer.contenttype = "text/plain" - of LOAD_REQUEST: - let request = source.request - let response = buffer.loader.doRequest(request, blocking = true, canredir = true) - if response.body == nil: - return ConnectResult(code: response.res) - if response.charset != CHARSET_UNKNOWN: - charset = charset - if setct: - buffer.contenttype = response.contenttype - buffer.istream = response.body - let fd = SocketStream(response.body).source.getFd() - buffer.fd = int(fd) - needsAuth = response.status == 401 # Unauthorized - redirect = response.redirect - if "Set-Cookie" in response.headers.table: - for s in response.headers.table["Set-Cookie"]: - let cookie = newCookie(s, response.url) - if cookie.isOk: - cookies.add(cookie.get) - if "Referrer-Policy" in response.headers.table: - referrerpolicy = getReferrerPolicy(response.headers.table["Referrer-Policy"][0]) - buffer.connected = true - return ConnectResult( - charset: charset, - needsAuth: needsAuth, - redirect: redirect, - cookies: cookies, - contentType: if setct: buffer.contenttype else: "" - ) - -# After connect, pager will call one of the following: -# * connect2, telling loader to load at last (we block loader until then) -# * redirectToFd, telling loader to load into the passed fd -proc connect2*(buffer: Buffer) {.proxy.} = - if buffer.source.t == LOAD_REQUEST: - # Notify loader that we can proceed with loading the input stream. - let ss = SocketStream(buffer.istream) - ss.swrite(false) - ss.setBlocking(false) - buffer.istream = newTeeStream(buffer.istream, buffer.sstream, - closedest = false) - buffer.selector.registerHandle(buffer.fd, {Read}, 0) - -proc redirectToFd*(buffer: Buffer, fd: FileHandle, wait: bool) {.proxy.} = - #TODO also clone & fd - if buffer.source.t == LOAD_REQUEST: - let ss = SocketStream(buffer.istream) - ss.swrite(true) - ss.sendFileHandle(fd) - if wait: - #TODO this is kind of dumb - # Basically, after redirect the network process keeps the socket open, - # and writes a boolean after transfer has been finished. This way, - # we can block this promise so it only returns after e.g. the whole - # file has been saved. - var dummy: bool - ss.sread(dummy) - discard close(fd) - ss.close() - -proc readFromFd*(buffer: Buffer, fd: FileHandle, ishtml: bool) {.proxy.} = - let contentType = if ishtml: - "text/html" - else: - "text/plain" - buffer.source = BufferSource( - t: LOAD_PIPE, - fd: fd, - location: buffer.source.location, - contenttype: some(contentType), - charset: buffer.source.charset - ) - buffer.contenttype = contentType - discard fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) or O_NONBLOCK) - let ps = newPosixStream(fd) - buffer.istream = newTeeStream(ps, buffer.sstream, - closedest = false) - buffer.fd = fd - buffer.selector.registerHandle(buffer.fd, {Read}, 0) - -proc setContentType*(buffer: Buffer, contentType: string) {.proxy.} = - buffer.source.contenttype = some(contentType) - -const BufferSize = 4096 - -proc finishLoad(buffer: Buffer): EmptyPromise = - if buffer.state != LOADING_PAGE: - let p = EmptyPromise() - p.resolve() - return p - var p: EmptyPromise - case buffer.contenttype - of "text/html": - buffer.sstream.setPosition(0) - buffer.available = 0 - if buffer.window == nil: - buffer.window = newWindow(buffer.config.scripting, buffer.selector, - buffer.attrs) - let doc = parseHTML(buffer.sstream, charsets = buffer.charsets, - window = buffer.window, url = buffer.url) - buffer.document = doc - buffer.state = LOADING_RESOURCES - p = buffer.loadResources() - else: - p = EmptyPromise() - p.resolve() - buffer.selector.unregister(buffer.fd) - buffer.loader.unregistered.add(buffer.fd) - buffer.fd = -1 - buffer.istream.close() - return p - -type LoadResult* = tuple[ - atend: bool, - lines: int, - bytes: int -] - -proc load*(buffer: Buffer): LoadResult {.proxy, task.} = - if buffer.state == LOADED: - return (true, buffer.lines.len, -1) - else: - buffer.savetask = true - -proc resolveTask[T](buffer: Buffer, cmd: BufferCommand, res: T) = - let packetid = buffer.tasks[cmd] - if packetid == 0: - return # no task to resolve (TODO this is kind of inefficient) - let len = slen(buffer.tasks[cmd]) + slen(res) - buffer.pstream.swrite(len) - buffer.pstream.swrite(packetid) - buffer.tasks[cmd] = 0 - buffer.pstream.swrite(res) - buffer.pstream.flush() - -proc onload(buffer: Buffer) = - var res: LoadResult = (false, buffer.lines.len, -1) - case buffer.state - of LOADING_RESOURCES: - assert false - of LOADED: - buffer.resolveTask(LOAD, res) - return - of LOADING_PAGE: - discard - let op = buffer.sstream.getPosition() - var s = newString(buffer.readbufsize) - try: - buffer.sstream.setPosition(op + buffer.available) - let n = buffer.istream.readData(addr s[0], buffer.readbufsize) - if n != 0: # n can be 0 if we get EOF. (in which case we shouldn't reshape unnecessarily.) - s.setLen(n) - buffer.sstream.setPosition(op) - if buffer.readbufsize < BufferSize: - buffer.readbufsize = min(BufferSize, buffer.readbufsize * 2) - buffer.available += s.len - case buffer.contenttype - of "text/html": - res.bytes = buffer.available - else: - buffer.do_reshape() - if buffer.istream.atEnd(): - res.atend = true - buffer.finishLoad().then(proc() = - buffer.state = LOADED - buffer.resolveTask(LOAD, res)) - return - buffer.resolveTask(LOAD, res) - except ErrorAgain, ErrorWouldBlock: - if buffer.readbufsize > 1: - buffer.readbufsize = buffer.readbufsize div 2 - -proc getTitle*(buffer: Buffer): string {.proxy.} = - if buffer.document != nil: - return buffer.document.title - -proc render*(buffer: Buffer): int {.proxy.} = - buffer.do_reshape() - return buffer.lines.len - -proc cancel*(buffer: Buffer): int {.proxy.} = - #TODO TODO TODO cancel resource loading too - if buffer.state != LOADING_PAGE: return - buffer.istream.close() - buffer.state = LOADED - case buffer.contenttype - of "text/html": - buffer.sstream.setPosition(0) - buffer.available = 0 - if buffer.window == nil: - buffer.window = newWindow(buffer.config.scripting, buffer.selector, - buffer.attrs) - buffer.document = parseHTML(buffer.sstream, - charsets = buffer.charsets, window = buffer.window, - url = buffer.url, canReinterpret = false) - buffer.do_reshape() - return buffer.lines.len - -#https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm -proc serializeMultipartFormData(entries: seq[FormDataEntry]): FormData = - let formData = newFormData0() - for entry in entries: - let name = makeCRLF(entry.name) - if entry.isstr: - let value = makeCRLF(entry.svalue) - formData.append(name, value) - else: - formData.append(name, entry.value, opt(entry.filename)) - return formData - -proc serializePlainTextFormData(kvs: seq[(string, string)]): string = - for it in kvs: - let (name, value) = it - result &= name - result &= '=' - result &= value - result &= "\r\n" - -func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = - if form.constructingEntryList: - return - let entrylist = form.constructEntryList(submitter).get(@[]) - - let action = if submitter.action() == "": - $form.document.url - else: - submitter.action() - - let url = submitter.document.parseURL(action) - if url.isnone: - return none(Request) - - var parsedaction = url.get - let scheme = parsedaction.scheme - let enctype = submitter.enctype() - let formmethod = submitter.formmethod() - if formmethod == FORM_METHOD_DIALOG: - #TODO - return none(Request) - let httpmethod = if formmethod == FORM_METHOD_GET: - HTTP_GET - else: - assert formmethod == FORM_METHOD_POST - HTTP_POST - - #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"): - # submitter.attr("formtarget") - #else: - # submitter.target() - #let noopener = true #TODO - - template mutateActionUrl() = - let kvlist = entrylist.toNameValuePairs() - let query = serializeApplicationXWWWFormUrlEncoded(kvlist) - parsedaction.query = query.some - return newRequest(parsedaction, httpmethod).some - - template submitAsEntityBody() = - var mimetype: string - var body: Opt[string] - var multipart: Opt[FormData] - case enctype - of FORM_ENCODING_TYPE_URLENCODED: - let kvlist = entrylist.toNameValuePairs() - body.ok(serializeApplicationXWWWFormUrlEncoded(kvlist)) - mimeType = $enctype - of FORM_ENCODING_TYPE_MULTIPART: - multipart.ok(serializeMultipartFormData(entrylist)) - mimetype = $enctype - of FORM_ENCODING_TYPE_TEXT_PLAIN: - let kvlist = entrylist.toNameValuePairs() - body.ok(serializePlainTextFormData(kvlist)) - mimetype = $enctype - let req = newRequest(parsedaction, httpmethod, @{"Content-Type": mimetype}, - body, multipart) - return some(req) #TODO multipart - - template getActionUrl() = - return newRequest(parsedaction).some - - case scheme - of "http", "https": - if formmethod == FORM_METHOD_GET: - mutateActionUrl - else: - assert formmethod == FORM_METHOD_POST - submitAsEntityBody - of "ftp": - getActionUrl - of "data": - if formmethod == FORM_METHOD_GET: - mutateActionUrl - else: - assert formmethod == FORM_METHOD_POST - getActionUrl - -proc setFocus(buffer: Buffer, e: Element): bool = - if buffer.document.focus != e: - buffer.document.focus = e - buffer.do_reshape() - return true - -proc restoreFocus(buffer: Buffer): bool = - if buffer.document.focus != nil: - buffer.document.focus = nil - buffer.do_reshape() - return true - -type ReadSuccessResult* = object - open*: Option[Request] - repaint*: bool - -func implicitSubmit(input: HTMLInputElement): Option[Request] = - if input.form != nil and input.form.canSubmitImplicitly(): - return submitForm(input.form, input.form) - -proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} = - if buffer.document.focus != nil: - case buffer.document.focus.tagType - of TAG_INPUT: - let input = HTMLInputElement(buffer.document.focus) - case input.inputType - of INPUT_SEARCH, INPUT_TEXT, INPUT_PASSWORD: - input.value = s - input.invalid = true - buffer.do_reshape() - result.repaint = true - result.open = implicitSubmit(input) - of INPUT_FILE: - let cdir = parseURL("file://" & getCurrentDir() & DirSep) - let path = parseURL(s, cdir) - if path.issome: - input.file = path - input.invalid = true - buffer.do_reshape() - result.repaint = true - result.open = implicitSubmit(input) - else: discard - of TAG_TEXTAREA: - let textarea = HTMLTextAreaElement(buffer.document.focus) - textarea.value = s - textarea.invalid = true - buffer.do_reshape() - result.repaint = true - else: discard - let r = buffer.restoreFocus() - if not result.repaint: - result.repaint = r - -type ReadLineResult* = object - prompt*: string - value*: string - hide*: bool - area*: bool - -type - SelectResult* = object - multiple*: bool - options*: seq[string] - selected*: seq[int] - - ClickResult* = object - open*: Option[Request] - readline*: Option[ReadLineResult] - repaint*: bool - select*: Option[SelectResult] - -proc click(buffer: Buffer, clickable: Element): ClickResult - -proc click(buffer: Buffer, label: HTMLLabelElement): ClickResult = - let control = label.control - if control != nil: - return buffer.click(control) - -proc click(buffer: Buffer, select: HTMLSelectElement): ClickResult = - let repaint = buffer.setFocus(select) - var options: seq[string] - var selected: seq[int] - var i = 0 - for option in select.options: - options.add(option.textContent.stripAndCollapse()) - if option.selected: - selected.add(i) - inc i - let select = SelectResult( - multiple: select.attrb("multiple"), - options: options, - selected: selected - ) - return ClickResult( - repaint: repaint, - select: some(select) - ) - -func baseURL(buffer: Buffer): URL = - return buffer.document.baseURL - -proc evalJSURL(buffer: Buffer, url: URL): Opt[string] = - let encodedScriptSource = ($url)["javascript:".len..^1] - let scriptSource = percentDecode(encodedScriptSource) - let ctx = buffer.window.jsctx - let ret = ctx.eval(scriptSource, $buffer.baseURL, JS_EVAL_TYPE_GLOBAL) - if JS_IsException(ret): - ctx.writeException(buffer.estream) - return err() # error - if JS_IsUndefined(ret): - return err() # no need to navigate - let s = ?fromJS[string](ctx, ret) - JS_FreeValue(ctx, ret) - # Navigate to result. - return ok(s) - -proc click(buffer: Buffer, anchor: HTMLAnchorElement): ClickResult = - var repaint = buffer.restoreFocus() - let url = parseURL(anchor.href, some(buffer.baseURL)) - if url.isSome: - let url = url.get - if url.scheme == "javascript": - if buffer.config.scripting: - let s = buffer.evalJSURL(url) - buffer.do_reshape() - repaint = true - if s.isSome: - let url = newURL("data:text/html," & s.get).get - let req = newRequest(url, HTTP_GET) - return ClickResult( - repaint: repaint, - open: some(req) - ) - return ClickResult( - repaint: repaint - ) - return ClickResult( - repaint: repaint, - open: some(newRequest(url, HTTP_GET)) - ) - return ClickResult( - repaint: repaint - ) - -proc click(buffer: Buffer, option: HTMLOptionElement): ClickResult = - let select = option.select - if select != nil: - return buffer.click(select) - -proc click(buffer: Buffer, button: HTMLButtonElement): ClickResult = - if button.form != nil: - case button.ctype - of BUTTON_SUBMIT: result.open = submitForm(button.form, button) - of BUTTON_RESET: - button.form.reset() - buffer.do_reshape() - return ClickResult(repaint: true) - of BUTTON_BUTTON: discard - result.repaint = buffer.setFocus(button) - -proc click(buffer: Buffer, textarea: HTMLTextAreaElement): ClickResult = - let repaint = buffer.setFocus(textarea) - let readline = ReadLineResult( - value: textarea.value, - area: true, - ) - return ClickResult( - readline: some(readline), - repaint: repaint - ) - -proc click(buffer: Buffer, input: HTMLInputElement): ClickResult = - result.repaint = buffer.restoreFocus() - case input.inputType - of INPUT_SEARCH: - result.repaint = buffer.setFocus(input) - result.readline = some(ReadLineResult( - prompt: "SEARCH: ", - value: input.value - )) - of INPUT_TEXT, INPUT_PASSWORD: - result.repaint = buffer.setFocus(input) - result.readline = some(ReadLineResult( - prompt: "TEXT: ", - value: input.value, - hide: input.inputType == INPUT_PASSWORD - )) - of INPUT_FILE: - result.repaint = buffer.setFocus(input) - var path = if input.file.issome: - input.file.get.path.serialize_unicode() - else: - "" - result.readline = some(ReadLineResult( - prompt: "Filename: ", - value: path - )) - of INPUT_CHECKBOX: - input.checked = not input.checked - input.invalid = true - result.repaint = true - buffer.do_reshape() - of INPUT_RADIO: - for radio in input.radiogroup: - radio.checked = false - radio.invalid = true - input.checked = true - input.invalid = true - result.repaint = true - buffer.do_reshape() - of INPUT_RESET: - if input.form != nil: - input.form.reset() - result.repaint = true - buffer.do_reshape() - of INPUT_SUBMIT, INPUT_BUTTON: - if input.form != nil: - result.open = submitForm(input.form, input) - else: - result.repaint = buffer.restoreFocus() - -proc click(buffer: Buffer, clickable: Element): ClickResult = - case clickable.tagType - of TAG_LABEL: - return buffer.click(HTMLLabelElement(clickable)) - of TAG_SELECT: - return buffer.click(HTMLSelectElement(clickable)) - of TAG_A: - return buffer.click(HTMLAnchorElement(clickable)) - of TAG_OPTION: - return buffer.click(HTMLOptionElement(clickable)) - of TAG_BUTTON: - return buffer.click(HTMLButtonElement(clickable)) - of TAG_TEXTAREA: - return buffer.click(HTMLTextAreaElement(clickable)) - of TAG_INPUT: - return buffer.click(HTMLInputElement(clickable)) - else: - result.repaint = buffer.restoreFocus() - -proc dispatchEvent(buffer: Buffer, ctype: string, elem: Element): tuple[ - called: bool, - canceled: bool - ] = - var called = false - var canceled = false - for a in elem.branch: - var stop = false - for el in a.eventListeners: - if el.ctype == "click": - let event = newEvent(buffer.window.jsctx, ctype, elem, a) - let e = el.callback(event) - called = true - if e.isErr: - buffer.window.jsctx.writeException(buffer.estream) - if FLAG_STOP_IMMEDIATE_PROPAGATION in event.flags: - stop = true - break - if FLAG_STOP_PROPAGATION in event.flags: - stop = true - if FLAG_CANCELED in event.flags: - canceled = true - if stop: - break - return (called, canceled) - -proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = - if buffer.lines.len <= cursory: return - var called = false - var canceled = false - if buffer.config.scripting: - let elem = buffer.getCursorElement(cursorx, cursory) - (called, canceled) = buffer.dispatchEvent("click", elem) - if called: - buffer.do_reshape() - if not canceled: - let clickable = buffer.getCursorClickable(cursorx, cursory) - if clickable != nil: - var res = buffer.click(clickable) - res.repaint = called - return res - return ClickResult(repaint: called) - -proc select*(buffer: Buffer, selected: seq[int]): ClickResult {.proxy.} = - if buffer.document.focus != nil and - buffer.document.focus.tagType == TAG_SELECT: - let select = HTMLSelectElement(buffer.document.focus) - var i = 0 - var j = 0 - var repaint = false - for option in select.options: - var wasSelected = option.selected - if i < selected.len and selected[i] == j: - option.selected = true - inc i - else: - option.selected = false - if not repaint: - repaint = wasSelected != option.selected - inc j - return ClickResult(repaint: buffer.restoreFocus()) - -proc readCanceled*(buffer: Buffer): bool {.proxy.} = - return buffer.restoreFocus() - -proc findAnchor*(buffer: Buffer, anchor: string): bool {.proxy.} = - return buffer.document != nil and buffer.document.getElementById(anchor) != nil - -type GetLinesResult* = tuple[ - numLines: int, - lines: seq[SimpleFlexibleLine] -] - -proc getLines*(buffer: Buffer, w: Slice[int]): GetLinesResult {.proxy.} = - var w = w - if w.b < 0 or w.b > buffer.lines.high: - w.b = buffer.lines.high - #TODO this is horribly inefficient - for y in w: - var line = SimpleFlexibleLine(str: buffer.lines[y].str) - for f in buffer.lines[y].formats: - line.formats.add(SimpleFormatCell(format: f.format, pos: f.pos)) - result.lines.add(line) - result.numLines = buffer.lines.len - -proc passFd*(buffer: Buffer, fd: FileHandle) {.proxy.} = - buffer.source.fd = fd - -#TODO this is mostly broken -proc getSource*(buffer: Buffer) {.proxy.} = - let ssock = initServerSocket() - let stream = ssock.acceptSocketStream() - let op = buffer.sstream.getPosition() - buffer.sstream.setPosition(0) - stream.write(buffer.sstream.readAll()) - buffer.sstream.setPosition(op) - stream.close() - ssock.close() - -macro bufferDispatcher(funs: static ProxyMap, buffer: Buffer, - cmd: BufferCommand, packetid: int) = - let switch = newNimNode(nnkCaseStmt) - switch.add(ident("cmd")) - for k, v in funs: - let ofbranch = newNimNode(nnkOfBranch) - ofbranch.add(v.ename) - let stmts = newStmtList() - let call = newCall(v.iname, buffer) - for i in 2 ..< v.params.len: - let param = v.params[i] - for i in 0 ..< param.len - 2: - let id = ident(param[i].strVal) - let typ = param[^2] - stmts.add(quote do: - when `typ` is FileHandle: - let `id` = SocketStream(`buffer`.pstream).recvFileHandle() - else: - var `id`: `typ` - `buffer`.pstream.sread(`id`)) - call.add(id) - var rval: NimNode - if v.params[0].kind == nnkEmpty: - stmts.add(call) - else: - rval = ident("retval") - stmts.add(quote do: - let `rval` = `call`) - var resolve = newStmtList() - if rval == nil: - resolve.add(quote do: - let len = slen(`packetid`) - buffer.pstream.swrite(len) - buffer.pstream.swrite(`packetid`) - buffer.pstream.flush()) - else: - resolve.add(quote do: - let len = slen(`packetid`) + slen(`rval`) - buffer.pstream.swrite(len) - buffer.pstream.swrite(`packetid`) - buffer.pstream.swrite(`rval`) - buffer.pstream.flush()) - if v.istask: - let en = v.ename - stmts.add(quote do: - if buffer.savetask: - buffer.savetask = false - buffer.tasks[BufferCommand.`en`] = `packetid` - else: - `resolve`) - else: - stmts.add(resolve) - ofbranch.add(stmts) - switch.add(ofbranch) - return switch - -proc readCommand(buffer: Buffer) = - var cmd: BufferCommand - buffer.pstream.sread(cmd) - var packetid: int - buffer.pstream.sread(packetid) - bufferDispatcher(ProxyFunctions, buffer, cmd, packetid) - -proc handleRead(buffer: Buffer, fd: int) = - if fd == buffer.rfd: - try: - buffer.readCommand() - except EOFError: - #eprint "EOF error", $buffer.url & "\nMESSAGE:", - # getCurrentExceptionMsg() & "\n", - # getStackTrace(getCurrentException()) - buffer.alive = false - elif fd == buffer.fd: - buffer.onload() - elif fd in buffer.loader.connecting: - buffer.loader.onConnected(fd) - if buffer.config.scripting: - buffer.window.runJSJobs() - elif fd in buffer.loader.ongoing: - buffer.loader.onRead(fd) - if buffer.config.scripting: - buffer.window.runJSJobs() - elif fd in buffer.loader.unregistered: - discard # ignore - else: assert false - -proc handleError(buffer: Buffer, fd: int, err: OSErrorCode) = - if fd == buffer.rfd: - # Connection reset by peer, probably. Close the buffer. - buffer.alive = false - elif fd == buffer.fd: - buffer.onload() - elif fd in buffer.loader.connecting: - # probably shouldn't happen. TODO - assert false, $fd & ": " & $err - elif fd in buffer.loader.ongoing: - buffer.loader.onError(fd) - if buffer.config.scripting: - buffer.window.runJSJobs() - elif fd in buffer.loader.unregistered: - discard # ignore - else: - assert false, $fd & ": " & $err - -proc runBuffer(buffer: Buffer, rfd: int) = - buffer.rfd = rfd - while buffer.alive: - let events = buffer.selector.select(-1) - for event in events: - if Read in event.events: - buffer.handleRead(event.fd) - if Error in event.events: - buffer.handleError(event.fd, event.errorCode) - if not buffer.alive: - break - if selectors.Event.Timer in event.events: - assert buffer.window != nil - assert buffer.window.timeouts.runTimeoutFd(event.fd) - buffer.window.runJSJobs() - buffer.loader.unregistered.setLen(0) - -proc launchBuffer*(config: BufferConfig, source: BufferSource, - attrs: WindowAttributes, loader: FileLoader, ssock: ServerSocket) = - let buffer = Buffer( - alive: true, - userstyle: parseStylesheet(config.userstyle), - attrs: attrs, - config: config, - loader: loader, - source: source, - sstream: newStringStream(), - viewport: Viewport(window: attrs), - width: attrs.width, - height: attrs.height - 1 - ) - buffer.readbufsize = BufferSize - buffer.selector = newSelector[int]() - loader.registerFun = proc(fd: int) = buffer.selector.registerHandle(fd, {Read}, 0) - loader.unregisterFun = proc(fd: int) = buffer.selector.unregister(fd) - buffer.srenderer = newStreamRenderer(buffer.sstream, buffer.charsets) - if buffer.config.scripting: - buffer.window = newWindow(buffer.config.scripting, buffer.selector, - buffer.attrs, proc(url: URL) = buffer.navigate(url), some(buffer.loader)) - let socks = ssock.acceptSocketStream() - buffer.estream = newFileStream(stderr) - buffer.pstream = socks - let rfd = int(socks.source.getFd()) - buffer.selector.registerHandle(rfd, {Read}, 0) - buffer.runBuffer(rfd) - buffer.pstream.close() - buffer.loader.quit() - quit(0) |