diff options
-rw-r--r-- | src/bindings/quickjs.nim | 88 | ||||
-rw-r--r-- | src/config/config.nim | 1 | ||||
-rw-r--r-- | src/css/match.nim | 10 | ||||
-rw-r--r-- | src/display/client.nim | 184 | ||||
-rw-r--r-- | src/html/dom.nim | 75 | ||||
-rw-r--r-- | src/html/htmlparser.nim | 22 | ||||
-rw-r--r-- | src/html/htmltokenizer.nim | 2 | ||||
-rw-r--r-- | src/io/lineedit.nim | 4 | ||||
-rw-r--r-- | src/io/loader.nim | 1 | ||||
-rw-r--r-- | src/io/request.nim | 50 | ||||
-rw-r--r-- | src/js/javascript.nim | 294 | ||||
-rw-r--r-- | src/main.nim | 8 | ||||
-rw-r--r-- | src/types/url.nim | 49 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 81 |
14 files changed, 638 insertions, 231 deletions
diff --git a/src/bindings/quickjs.nim b/src/bindings/quickjs.nim index bdd156e2..4a1ad639 100644 --- a/src/bindings/quickjs.nim +++ b/src/bindings/quickjs.nim @@ -23,11 +23,43 @@ when hlib != "": {.passC: "-I" & hlib.} {.passL: "-lquickjs -lm -lpthread".} +const ## all tags with a reference count are negative + JS_TAG_FIRST* = -10 ## first negative tag + JS_TAG_BIG_INT* = -10 + JS_TAG_BIG_FLOAT* = -9 + JS_TAG_SYMBOL* = -8 + JS_TAG_STRING* = -7 + JS_TAG_SHAPE* = -6 ## used internally during GC + JS_TAG_ASYNC_FUNCTION* = -5 ## used internally during GC + JS_TAG_VAR_REF* = -4 ## used internally during GC + JS_TAG_MODULE* = -3 ## used internally + JS_TAG_FUNCTION_BYTECODE* = -2 ## used internally + JS_TAG_OBJECT* = -1 + JS_TAG_INT* = 0 + JS_TAG_BOOL* = 1 + JS_TAG_NULL* = 2 + JS_TAG_UNDEFINED* = 3 + JS_TAG_UNINITIALIZED* = 4 + JS_TAG_CATCH_OFFSET* = 5 + JS_TAG_EXCEPTION* = 6 + JS_TAG_FLOAT64* = 7 ## any larger tag is FLOAT64 if JS_NAN_BOXING + when sizeof(int) < sizeof(int64): {.passC: "-DJS_NAN_BOXING".} type JSValue* {.importc, header: qjsheader.} = uint64 - # uh this won't compile you're on your own + + template JS_VALUE_GET_TAG*(v: untyped): int32 = + cast[int32](v shr 32) + + template JS_VALUE_GET_PTR*(v: untyped): pointer = + cast[pointer](v) + + template JS_MKVAL*(t, val: untyped): JSValue = + JSValue((uint64(t) shl 32) or uint32(val)) + + template JS_MKPTR*(t, p: untyped): JSValue = + JSValue((cast[uint64](t) shl 32) or cast[uint](p)) else: type JSValueUnion* {.importc, header: qjsheader, union.} = object @@ -38,6 +70,18 @@ else: u*: JSValueUnion tag*: int64 + template JS_VALUE_GET_TAG*(v: untyped): int32 = + cast[int32](v.tag) + + template JS_VALUE_GET_PTR*(v: untyped): pointer = + cast[pointer](v.u) + + template JS_MKVAL*(t, val: untyped): JSValue = + JSValue(u: JSValueUnion(`int32`: val), tag: t) + + template JS_MKPTR*(t, p: untyped): JSValue = + JSValue(u: JSValueUnion(`ptr`: p), tag: t) + type JSRuntime* = ptr object JSContext* = ptr object @@ -136,39 +180,6 @@ converter toBool*(js: JS_BOOl): bool {.inline.} = converter toJSBool*(b: bool): JS_BOOL {.inline.} = cast[JS_BOOL](cint(b)) -const ## all tags with a reference count are negative - JS_TAG_FIRST* = -10 ## first negative tag - JS_TAG_BIG_INT* = -10 - JS_TAG_BIG_FLOAT* = -9 - JS_TAG_SYMBOL* = -8 - JS_TAG_STRING* = -7 - JS_TAG_SHAPE* = -6 ## used internally during GC - JS_TAG_ASYNC_FUNCTION* = -5 ## used internally during GC - JS_TAG_VAR_REF* = -4 ## used internally during GC - JS_TAG_MODULE* = -3 ## used internally - JS_TAG_FUNCTION_BYTECODE* = -2 ## used internally - JS_TAG_OBJECT* = -1 - JS_TAG_INT* = 0 - JS_TAG_BOOL* = 1 - JS_TAG_NULL* = 2 - JS_TAG_UNDEFINED* = 3 - JS_TAG_UNINITIALIZED* = 4 - JS_TAG_CATCH_OFFSET* = 5 - JS_TAG_EXCEPTION* = 6 - JS_TAG_FLOAT64* = 7 ## any larger tag is FLOAT64 if JS_NAN_BOXING - -template JS_MKVAL*(t, val: untyped): JSValue = - JSValue(u: JSValueUnion(`int32`: val), tag: t) - -template JS_MKPTR*(t, p: untyped): JSValue = - JSValue(u: JSValueUnion(`ptr`: p), tag: t) - -template JS_VALUE_GET_TAG*(v: untyped): int = - cast[int32](v.tag) - -template JS_VALUE_GET_PTR*(v: untyped): pointer = - cast[pointer](v.u) - const JS_NULL* = JS_MKVAL(JS_TAG_NULL, 0) JS_UNDEFINED* = JS_MKVAL(JS_TAG_UNDEFINED, 0) @@ -211,6 +222,7 @@ const JS_PROP_GETSET* = (1 shl 4) JS_PROP_VARREF* = (2 shl 4) # used internally JS_PROP_AUTOINIT* = (3 shl 4) # used internally + JS_PROP_THROW* = (1 shl 14) const JS_GPN_STRING_MASK* = (1 shl 0) @@ -219,7 +231,6 @@ const JS_GPN_ENUM_ONLY* = (1 shl 3) JS_GPN_SET_ENUM* = (1 shl 4) - template JS_CFUNC_DEF*(n: string, len: uint8, func1: JSCFunction): JSCFunctionListEntry = JSCFunctionListEntry(name: cstring(n), prop_flags: JS_PROP_WRITABLE or JS_PROP_CONFIGURABLE, @@ -237,6 +248,7 @@ template JS_CGETSET_DEF*(n: string, fgetter, fsetter: untyped): JSCFunctionListE getset: JSCFunctionListEntryGetSet(get: JSCFunctionType(getter: fgetter), set: JSCFunctionType(setter: fsetter)))) + {.push header: qjsheader, importc, cdecl.} proc JS_NewRuntime*(): JSRuntime @@ -252,9 +264,11 @@ proc JS_FreeContext*(ctx: JSContext) proc JS_GetGlobalObject*(ctx: JSContext): JSValue proc JS_IsInstanceOf*(ctx: JSContext, val: JSValue, obj: JSValue): cint +proc JS_NewArray*(ctx: JSContext): JSValue proc JS_NewObject*(ctx: JSContext): JSValue proc JS_NewObjectClass*(ctx: JSContext, class_id: JSClassID): JSValue proc JS_NewObjectProto*(ctx: JSContext, proto: JSValue): JSValue +proc JS_NewObjectProtoClass*(ctx: JSContext, proto: JSValue, class_id: JSClassID): JSValue proc JS_SetOpaque*(obj: JSValue, opaque: pointer) proc JS_GetOpaque*(obj: JSValue, class_id: JSClassID): pointer proc JS_GetOpaque2*(ctx: JSContext, obj: JSValue, class_id: JSClassID): pointer @@ -288,6 +302,9 @@ proc JS_NewCFunction*(ctx: JSContext, cfunc: JSCFunction, name: cstring, length: proc JS_NewString*(ctx: JSContext, str: cstring): JSValue +proc JS_SetProperty*(ctx: JSContext, this_obj: JSValue, prop: JSAtom, val: JSValue): cint +proc JS_SetPropertyUint32*(ctx: JSContext, this_obj: JSValue, idx: uint32, val: JSValue): cint +proc JS_SetPropertyInt64*(ctx: JSContext, this_obj: JSValue, idx: int64, val: JSValue): cint proc JS_SetPropertyStr*(ctx: JSContext, this_obj: JSValue, prop: cstring, val: JSValue): cint proc JS_SetPropertyFunctionList*(ctx: JSContext, obj: JSValue, tab: ptr JSCFunctionListEntry, len: cint) proc JS_GetProperty*(ctx: JSContext, this_obj: JSValue, prop: JSAtom): JSValue @@ -300,6 +317,7 @@ proc JS_Call*(ctx: JSContext, func_obj, this_obj: JSValue, argc: cint, argv: ptr proc JS_DefineProperty*(ctx: JSContext, this_obj: JSValue, prop: JSAtom, val: JSValue, getter: JSValue, setter: JSValue, flags: cint): cint proc JS_DefinePropertyValue*(ctx: JSContext, this_obj: JSValue, prop: JSAtom, val: JSValue, flags: cint): cint proc JS_DefinePropertyValueUint32*(ctx: JSContext, this_obj: JSValue, idx: uint32, val: JSValue, flags: cint): cint +proc JS_DefinePropertyValueInt64*(ctx: JSContext, this_obj: JSValue, idx: int64, val: JSValue, flags: cint): cint proc JS_DefinePropertyValueStr*(ctx: JSContext, this_obj: JSValue, prop: cstring, val: JSValue, flags: cint): cint proc JS_DefinePropertyValueGetSet*(ctx: JSContext, this_obj: JSValue, prop: JSAtom, getter: JSValue, setter: JSValue, flags: cint): cint diff --git a/src/config/config.nim b/src/config/config.nim index ddbbaa95..fe24db51 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -44,6 +44,7 @@ type nmap*: Table[string, string] lemap*: ActionMap stylesheet*: string + startup*: string ambiguous_double*: bool markcolor*: CellColor diff --git a/src/css/match.nim b/src/css/match.nim index a50f8396..7d502df1 100644 --- a/src/css/match.nim +++ b/src/css/match.nim @@ -203,15 +203,17 @@ func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: ComplexSelector, return false return true -proc querySelectorAll*(document: Document, q: string): seq[Element] = +proc querySelectorAll(node: Node, q: string): seq[Element] = let selectors = parseSelectors(newStringStream(q)) - for element in document.elements: + for element in node.elements: if element.selectorsMatch(selectors): result.add(element) +doqsa = (proc(node: Node, q: string): seq[Element] = querySelectorAll(node, q)) -proc querySelector*(document: Document, q: string): Element = +proc querySelector(node: Node, q: string): Element = let selectors = parseSelectors(newStringStream(q)) - for element in document.elements: + for element in node.elements: if element.selectorsMatch(selectors): return element return nil +doqs = (proc(node: Node, q: string): Element = querySelector(node, q)) diff --git a/src/display/client.nim b/src/display/client.nim index 6e3c8c45..d06ec87c 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -1,11 +1,15 @@ import options import os import streams +import strutils import terminal +import times import unicode import css/sheet import config/config +import html/dom +import html/htmlparser import io/buffer import io/cell import io/lineedit @@ -149,12 +153,143 @@ proc readPipe(client: Client, ctype: string) = else: client.buffer.drawBuffer() +type Cookie = ref object of RootObj + name {.jsget.}: string + value {.jsget.}: string + expires {.jsget.}: int64 # unix time + maxAge {.jsget.}: int64 + secure {.jsget.}: bool + httponly {.jsget.}: bool + samesite {.jsget.}: bool + domain {.jsget.}: string + path {.jsget.}: string + +proc parseCookieDate(val: string): Option[DateTime] = + # cookie-date + const Delimiters = {'\t', ' '..'/', ';'..'@', '['..'`', '{'..'~'} + const NonDigit = Ascii + NonAscii - Digits + var foundTime = false + var foundDayOfMonth = false + var foundMonth = false + var foundYear = false + # date-token-list + var time: array[3, int] + var dayOfMonth: int + var month: int + var year: int + for dateToken in val.split(Delimiters): + if dateToken == "": continue # *delimiter + if not foundTime: + block timeBlock: # test for time + let hmsTime = dateToken.until(NonDigit - {':'}) + var i = 0 + for timeField in hmsTime.split(':'): + if i > 2: break timeBlock # too many time fields + # 1*2DIGIT + if timeField.len != 1 and timeField.len != 2: break timeBlock + var timeFields: array[3, int] + for c in timeField: + if c notin Digits: break timeBlock + timeFields[i] *= 10 + timeFields[i] += c.decValue + time = timeFields + inc i + if i != 3: break timeBlock + foundTime = true + continue + if not foundDayOfMonth: + block dayOfMonthBlock: # test for day-of-month + let digits = dateToken.until(NonDigit) + if digits.len != 1 and digits.len != 2: break dayOfMonthBlock + var n = 0 + for c in digits: + if c notin Digits: break dayOfMonthBlock + n *= 10 + n += c.decValue + dayOfMonth = n + foundDayOfMonth = true + continue + if not foundMonth: + block monthBlock: # test for month + if dateToken.len < 3: break monthBlock + case dateToken.substr(0, 2).toLower() + of "jan": month = 1 + of "feb": month = 2 + of "mar": month = 3 + of "apr": month = 4 + of "may": month = 5 + of "jun": month = 6 + of "jul": month = 7 + of "aug": month = 8 + of "sep": month = 9 + of "oct": month = 10 + of "nov": month = 11 + of "dec": month = 12 + else: break monthBlock + foundMonth = true + continue + if not foundYear: + block yearBlock: # test for year + let digits = dateToken.until(NonDigit) + if digits.len != 2 and digits.len != 4: break yearBlock + var n = 0 + for c in digits: + if c notin Digits: break yearBlock + n *= 10 + n += c.decValue + year = n + foundYear = true + continue + if not (foundDayOfMonth and foundMonth and foundYear and foundTime): return none(DateTime) + if dayOfMonth notin 0..31: return none(DateTime) + if year < 1601: return none(DateTime) + if time[0] > 23: return none(DateTime) + if time[1] > 59: return none(DateTime) + if time[2] > 59: return none(DateTime) + var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2])) + return some(dateTime) + +proc parseCookie(client: Client, str: string): Cookie {.jsfunc.} = + let cookie = new(Cookie) + var first = true + for part in str.split(';'): + if first: + cookie.name = part.until('=') + cookie.value = part.after('=') + first = false + continue + let part = percentDecode(part).strip(leading = true, trailing = false, AsciiWhitespace) + var n = 0 + for i in 0..part.high: + if part[i] == '=': + n = i + break + if n == 0: + continue + let key = part.substr(0, n - 1) + let val = part.substr(n + 1) + case key.toLower() + of "expires": + let date = parseCookieDate(val) + if date.issome: + cookie.expires = date.get.toTime().toUnix() + of "max-age": cookie.maxAge = parseInt64(val) + of "secure": cookie.secure = true + of "httponly": cookie.httponly = true + of "samesite": cookie.samesite = true + of "path": cookie.path = val + of "domain": cookie.domain = val + return cookie + +proc doRequest(client: Client, req: Request): Response {.jsfunc.} = + client.loader.doRequest(req) + # Load request in a new buffer. var g_client: Client proc gotoUrl(client: Client, request: Request, prevurl = none(URL), force = false, ctype = "") = if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or prevurl.get.equals(request.url) or request.httpmethod != HTTP_GET: - let page = client.loader.doRequest(request) + let page = client.doRequest(request) client.needsauth = page.status == 401 # Unauthorized client.redirecturl = page.redirect if page.body != nil: @@ -262,10 +397,8 @@ proc evalJS(client: Client, src, filename: string): JSObject = unblockStdin() return client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL) -proc command(client: Client, src: string) = - restoreStdin() - let previ = client.console.err.getPosition() - let ret = client.evalJS(src, "<command>") +proc command0(client: Client, src: string, filename = "<command>") = + let ret = client.evalJS(src, filename) if ret.isException(): let ex = client.jsctx.getException() let str = ex.toString() @@ -283,6 +416,11 @@ proc command(client: Client, src: string) = if str.issome: client.console.err.write(str.get & '\n') free(ret) + +proc command(client: Client, src: string) = + restoreStdin() + let previ = client.console.err.getPosition() + client.command0(src) g_client = client client.console.err.setPosition(previ) if client.console.lastbuf == nil or client.console.lastbuf != client.buffer: @@ -418,8 +556,8 @@ proc isearchBack(client: Client) {.jsfunc.} = client.buffer.cpos = cpos proc quit(client: Client) {.jsfunc.} = - eraseScreen() - print(HVP(0, 0)) + #eraseScreen() + #print(HVP(0, 0)) quit(0) proc feedNext(client: Client) {.jsfunc.} = @@ -527,7 +665,25 @@ proc inputLoop(client: Client) = except ActionError as e: client.buffer.setStatusMessage(e.msg) +#TODO this is dumb +proc readFile(client: Client, path: string): string {.jsfunc.} = + try: + return readFile(path) + except IOError: + discard + +#TODO ditto +proc writeFile(client: Client, path: string, content: string) {.jsfunc.} = + writeFile(path, content) + proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool) = + if gconfig.startup != "": + let s = readFile(gconfig.startup) + #client.command(s) + client.console.err = newFileStream(stderr) + client.command0(s, gconfig.startup) + client.console.err = newStringStream() + quit() client.userstyle = gconfig.stylesheet.parseStylesheet() if not stdin.isatty: client.readPipe(ctype) @@ -569,6 +725,9 @@ proc log(console: Console, ss: varargs[string]) {.jsfunc.} = console.err.write(' ') console.err.write('\n') +proc sleep(client: Client, millis: int) {.jsfunc.} = + sleep millis + proc newClient*(): Client = new(result) result.loader = newFileLoader() @@ -579,13 +738,18 @@ proc newClient*(): Client = result.jsrt = rt result.jsctx = ctx var global = ctx.getGlobalObject() - discard ctx.registerType(Client, asglobal = true, addto = some(global)) + ctx.registerType(Cookie) + ctx.registerType(Client, asglobal = true) global.setOpaque(result) - global.setProperty("client", global) + ctx.setProperty(global.val, "client", global.val) let consoleClassId = ctx.registerType(Console) let jsConsole = ctx.newJSObject(consoleClassId) jsConsole.setOpaque(result.console) - global.setProperty("console", jsConsole) + ctx.setProperty(global.val, "console", jsConsole.val) + free(global) ctx.addUrlModule() + ctx.addDOMModule() + ctx.addHTMLModule() + ctx.addRequestModule() diff --git a/src/html/dom.nim b/src/html/dom.nim index 4bf73302..9c0f0a59 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -5,6 +5,7 @@ import tables import css/sheet import html/tags +import js/javascript import types/url import utils/twtstr @@ -34,7 +35,7 @@ type Node* = ref object of EventTarget nodeType*: NodeType - childNodes*: seq[Node] + childNodes* {.jsget.}: seq[Node] nextSibling*: Node previousSibling*: Node parentNode*: Node @@ -83,7 +84,7 @@ type id*: string classList*: seq[string] - attributes*: Table[string, string] + attributes* {.jsget, jsset.}: Table[string, string] hover*: bool invalid*: bool @@ -97,7 +98,7 @@ type inputType*: InputType autofocus*: bool required*: bool - value*: string + value* {.jsget.}: string size*: int checked*: bool xcoord*: int @@ -129,7 +130,7 @@ type start*: Option[int] HTMLLIElement* = ref object of HTMLElement - value*: Option[int] + value* {.jsget.}: Option[int] HTMLStyleElement* = ref object of HTMLElement sheet*: CSSStylesheet @@ -199,6 +200,11 @@ iterator children_rev*(node: Node): Element {.inline.} = if child.nodeType == ELEMENT_NODE: yield Element(child) +#TODO TODO TODO this should return a live view instead +proc children*(node: Node): seq[Element] {.jsget.} = + for child in node.children: + result.add(child) + # Returns the node's ancestors iterator ancestors*(node: Node): Element {.inline.} = var element = node.parentElement @@ -415,7 +421,7 @@ func attrb*(element: Element, s: string): bool = return true return false -func textContent*(node: Node): string = +func textContent*(node: Node): string {.jsget.} = case node.nodeType of DOCUMENT_NODE, DOCUMENT_TYPE_NODE: return "" #TODO null @@ -533,7 +539,7 @@ func formmethod*(element: Element): FormMethod = return FORM_METHOD_GET -func target*(element: Element): string = +func target*(element: Element): string {.jsfunc.} = if element.attrb("target"): return element.attr("target") for base in element.document.elements(TAG_BASE): @@ -547,13 +553,13 @@ func findAncestor*(node: Node, tagTypes: set[TagType]): Element = return element return nil -func newText*(document: Document, data: string = ""): Text = +func newText*(document: Document, data: string = ""): Text {.jsctor.} = new(result) result.nodeType = TEXT_NODE result.document = document result.data = data -func newComment*(document: Document, data: string = ""): Comment = +func newComment*(document: Document = nil, data: string = ""): Comment {.jsctor.} = new(result) result.nodeType = COMMENT_NODE result.document = document @@ -617,12 +623,12 @@ func newHTMLElement*(document: Document, localName: string, namespace = Namespac if tagType == TAG_UNKNOWN: result.localName = localName -func newDocument*(): Document = +func newDocument*(): Document {.jsctor.} = new(result) result.nodeType = DOCUMENT_NODE result.document = result -func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType = +func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType {.jsctor.} = new(result) result.document = document result.name = name @@ -637,13 +643,20 @@ func newAttr*(parent: Element, key, value: string): Attr = result.name = key result.value = value -func getElementById*(node: Node, id: string): Element = +func getElementById*(node: Node, id: string): Element {.jsfunc.} = if id.len == 0: return nil for child in node.elements: if child.id == id: return child +func getElementById2*(node: Node, id: string): pointer = + if id.len == 0: + return nil + for child in node.elements: + if child.id == id: + return cast[pointer](child) + func getElementsByTag*(document: Document, tag: TagType): seq[Element] = for element in document.elements(tag): result.add(element) @@ -709,7 +722,7 @@ func text*(option: HTMLOptionElement): string = if child.parentElement.tagType != TAG_SCRIPT: #TODO svg result &= child.data.stripAndCollapse() -func value*(option: HTMLOptionElement): string = +func value*(option: HTMLOptionElement): string {.jsget.} = if option.attrb("value"): return option.attr("value") return option.childTextContent.stripAndCollapse() @@ -960,3 +973,41 @@ proc appendAttribute*(element: Element, k, v: string) = select.size = 4 else: discard element.attributes[k] = v + +var doqsa*: proc (node: Node, q: string): seq[Element] +var doqs*: proc (node: Node, q: string): Element + +proc querySelectorAll*(node: Node, q: string): seq[Element] {.jsfunc.} = + return doqsa(node, q) + +proc querySelector*(node: Node, q: string): Element {.jsfunc.} = + return doqs(node, q) + +proc addDOMModule*(ctx: JSContext) = + let eventTargetCID = ctx.registerType(EventTarget) + let nodeCID = ctx.registerType(Node, parent = eventTargetCID) + ctx.registerType(Document, parent = nodeCID) + let characterDataCID = ctx.registerType(CharacterData, parent = nodeCID) + ctx.registerType(Comment, parent = characterDataCID) + ctx.registerType(Text, parent = characterDataCID) + ctx.registerType(DocumentType, parent = nodeCID) + let elementCID = ctx.registerType(Element, parent = nodeCID) + let htmlElementCID = ctx.registerType(HTMLElement, parent = elementCID) + ctx.registerType(HTMLInputElement, parent = htmlElementCID) + ctx.registerType(HTMLAnchorElement, parent = htmlElementCID) + ctx.registerType(HTMLSelectElement, parent = htmlElementCID) + ctx.registerType(HTMLSpanElement, parent = htmlElementCID) + ctx.registerType(HTMLOptGroupElement, parent = htmlElementCID) + ctx.registerType(HTMLOptionElement, parent = htmlElementCID) + ctx.registerType(HTMLHeadingElement, parent = htmlElementCID) + ctx.registerType(HTMLBRElement, parent = htmlElementCID) + ctx.registerType(HTMLMenuElement, parent = htmlElementCID) + ctx.registerType(HTMLUListElement, parent = htmlElementCID) + ctx.registerType(HTMLOListElement, parent = htmlElementCID) + ctx.registerType(HTMLLIElement, parent = htmlElementCID) + ctx.registerType(HTMLStyleElement, parent = htmlElementCID) + ctx.registerType(HTMLLinkElement, parent = htmlElementCID) + ctx.registerType(HTMLFormElement, parent = htmlElementCID) + ctx.registerType(HTMLTemplateElement, parent = htmlElementCID) + ctx.registerType(HTMLUnknownElement, parent = htmlElementCID) + ctx.registerType(HTMLScriptElement, parent = htmlElementCID) diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim index a8ea5458..f28cd300 100644 --- a/src/html/htmlparser.nim +++ b/src/html/htmlparser.nim @@ -6,13 +6,16 @@ import strformat import tables import unicode -import utils/twtstr +import css/sheet import html/dom import html/tags import html/htmltokenizer -import css/sheet +import js/javascript +import utils/twtstr type + DOMParser = ref object # JS interface + HTML5Parser = object case fragment: bool of true: ctx: Element @@ -2085,3 +2088,18 @@ proc parseHTML5*(inputStream: Stream): Document = parser.document = newDocument() parser.tokenizer = inputStream.newTokenizer() return parser.constructTree() + +proc newDOMParser*(): DOMParser {.jsctor.} = + new(result) + +proc parseFromString*(parser: DOMParser, str: string, t: string): Document {.jserr, jsfunc.} = + case t + of "text/html": + return parseHTML5(newStringStream(str)) + of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml": + JS_THROW JS_InternalError, "XML parsing is not supported yet" + else: + JS_THROW JS_TypeError, "Invalid mime type" + +proc addHTMLModule*(ctx: JSContext) = + ctx.registerType(DOMParser) diff --git a/src/html/htmltokenizer.nim b/src/html/htmltokenizer.nim index a1c894c8..c8f96144 100644 --- a/src/html/htmltokenizer.nim +++ b/src/html/htmltokenizer.nim @@ -227,7 +227,7 @@ iterator tokenize*(tokenizer: var Tokenizer): Token = template emit_tmp() = var i = 0 while i < tokenizer.tmp.len: - if tokenizer.tmp[i].isAscii(): + if tokenizer.tmp[i] in Ascii: emit tokenizer.tmp[i] inc i else: diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim index def2feb2..3f9034bf 100644 --- a/src/io/lineedit.nim +++ b/src/io/lineedit.nim @@ -167,8 +167,8 @@ proc readLine(state: var LineState): bool = else: state.fullRedraw() of ACTION_LINED_DELETE: - if state.cursor > 0 and state.cursor < state.news.len: - let w = state.news[state.cursor - 1].lwidth() + if state.cursor >= 0 and state.cursor < state.news.len: + let w = state.news[state.cursor].lwidth() state.news.delete(state.cursor..state.cursor) if state.cursor == state.news.len and state.shift == 0: state.kill(w) diff --git a/src/io/loader.nim b/src/io/loader.nim index 7c68258e..1530bf5b 100644 --- a/src/io/loader.nim +++ b/src/io/loader.nim @@ -100,6 +100,7 @@ proc runFileLoader(loader: FileLoader, loadcb: proc()) = quit(0) proc doRequest*(loader: FileLoader, request: Request): Response = + new(result) let stream = connectSocketStream(loader.process) stream.swrite(request) stream.flush() diff --git a/src/io/request.nim b/src/io/request.nim index 7553bdb5..6e6f3f40 100644 --- a/src/io/request.nim +++ b/src/io/request.nim @@ -2,12 +2,20 @@ import options import streams import tables -import utils/twtstr import types/url +import js/javascript +import utils/twtstr type HttpMethod* = enum - HTTP_CONNECT, HTTP_DELETE, HTTP_GET, HTTP_HEAD, HTTP_OPTIONS, HTTP_PATCH, - HTTP_POST, HTTP_PUT, HTTP_TRACE + HTTP_CONNECT = "CONNECT" + HTTP_DELETE = "DELETE" + HTTP_GET = "GET" + HTTP_HEAD = "HEAD" + HTTP_OPTIONS = "OPTIONS" + HTTP_PATCH = "PATCH" + HTTP_POST = "POST" + HTTP_PUT = "PUT" + HTTP_TRACE = "TRACE" type Request* = ref RequestObj @@ -18,13 +26,13 @@ type body*: Option[string] multipart*: Option[MimeData] - Response* = object + Response* = ref object body*: Stream - res*: int - contenttype*: string - status*: int - headers*: HeaderList - redirect*: Option[Url] + res* {.jsget.}: int + contenttype* {.jsget.}: string + status* {.jsget.}: int + headers* {.jsget.}: HeaderList + redirect* {.jsget.}: Option[Url] ReadableStream* = ref object of Stream isource*: Stream @@ -32,7 +40,7 @@ type isend: bool HeaderList* = ref object - table*: Table[string, seq[string]] + table* {.jsget.}: Table[string, seq[string]] # Originally from the stdlib MimePart* = object @@ -122,9 +130,9 @@ func newHeaderList*(table: Table[string, string]): HeaderList = func newRequest*(url: Url, httpmethod = HTTP_GET, - headers: openarray[(string, string)] = [], + headers: seq[(string, string)] = @[], body = none(string), - multipart = none(MimeData)): Request = + multipart = none(MimeData)): Request {.jsctor.} = new(result) result.httpmethod = httpmethod result.url = url @@ -135,6 +143,16 @@ func newRequest*(url: Url, result.body = body result.multipart = multipart +func newRequest*(url: Url, + httpmethod: HttpMethod, + headers: openarray[(string, string)], + body = none(string), + multipart = none(MimeData)): Request = + var s: seq[(string, string)] + for it in headers: + s.add(it) + return newRequest(url, httpmethod, s, body, multipart) + proc `[]=`*(multipart: var MimeData, k, v: string) = multipart.content.add(MimePart(name: k, content: v)) @@ -154,3 +172,11 @@ func getOrDefault*(headers: HeaderList, k: string, default = ""): string = headers.table[k][0] else: default + +proc readAll*(response: Response): string {.jsfunc.} = + return response.body.readAll() + +proc addRequestModule*(ctx: JSContext) = + ctx.registerType(Request) + ctx.registerType(Response) + ctx.registerType(HeaderList) diff --git a/src/js/javascript.nim b/src/js/javascript.nim index 11253875..9ba4e87f 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -30,9 +30,12 @@ type JSContextOpaque* = ref object creg: Table[string, JSClassID] + typemap: Table[pointer, JSClassID] + ctors: Table[JSClassID, JSValue] #TODO TODO TODO free these gclaz: string sym_iterator: JSAtom sym_asyncIterator: JSAtom + sym_toStringTag: JSAtom done: JSAtom next: JSAtom value: JSAtom @@ -78,6 +81,11 @@ proc newJSContext*(rt: JSRuntime): JSContext = opaque.sym_asyncIterator = JS_ValueToAtom(ctx, ait) JS_FreeValue(ctx, ait) block: + let ait = JS_GetPropertyStr(ctx, sym, "toStringTag") + assert JS_IsSymbol(ait) + opaque.sym_toStringTag = JS_ValueToAtom(ctx, ait) + JS_FreeValue(ctx, ait) + block: let s = "done" opaque.done = JS_NewAtomLen(ctx, cstring(s), csize_t(s.len)) block: @@ -131,9 +139,8 @@ func newJSObject*(ctx: JSContext, class: string): JSObject = result.ctx = ctx result.val = JS_NewObjectClass(ctx, ctx.getClass(class)) -func newJSCFunction*(ctx: JSContext, name: string, fun: JSCFunction, argc: int = 0, proto = JS_CFUNC_generic, magic = 0): JSObject = - result.ctx = ctx - result.val = JS_NewCFunction2(ctx, fun, cstring(name), cint(argc), proto, cint(magic)) +func newJSCFunction*(ctx: JSContext, name: string, fun: JSCFunction, argc: int = 0, proto = JS_CFUNC_generic, magic = 0): JSValue = + return JS_NewCFunction2(ctx, fun, cstring(name), cint(argc), proto, cint(magic)) func getGlobalObject*(ctx: JSContext): JSObject = result.ctx = ctx @@ -152,6 +159,7 @@ proc free*(ctx: var JSContext) = if opaque != nil: JS_FreeAtom(ctx, opaque.sym_iterator) JS_FreeAtom(ctx, opaque.sym_asyncIterator) + JS_FreeAtom(ctx, opaque.sym_toStringTag) JS_FreeAtom(ctx, opaque.done) JS_FreeAtom(ctx, opaque.next) GC_unref(opaque) @@ -183,7 +191,6 @@ func isGlobal*(ctx: JSContext, class: string): bool = return ctx.getOpaque().gclaz == class # A hack to retrieve a given val's class id. -# Used when we can't query ctx's hash table. func getClassID*(val: JSValue): JSClassID = const index = sizeof(cint) + # gc_ref_count sizeof(uint8) + # gc_mark @@ -199,7 +206,7 @@ func getOpaque*(ctx: JSContext, val: JSValue, class: string): pointer = let opaque = JS_GetOpaque(global.val, 1) # JS_CLASS_OBJECT free(global) return opaque - return JS_GetOpaque(val, ctx.getClass(class)) + return JS_GetOpaque(val, val.getClassID()) func getOpaque*(obj: JSObject, class: string): pointer = getOpaque(obj.ctx, obj.val, class) @@ -230,46 +237,59 @@ func isGlobal*(obj: JSObject): bool = let global = obj.ctx.getGlobalObject() result = JS_VALUE_GET_PTR(global.val) == JS_VALUE_GET_PTR(obj.val) -func isInstanceOf*(obj: JSObject, class: string): bool = - return obj.getOpaque(class) != nil +func isInstanceOf*(ctx: JSContext, obj: JSValue, class: string): bool = + let clazz = ctx.getClass(class) + if clazz in ctx.getOpaque().ctors: + let ctor = ctx.getOpaque().ctors[clazz] + if JS_IsInstanceOf(ctx, obj, ctor) == 1: + return true + return false #TODO handle exception? + else: + #TODO TODO TODO LegacyNoInterfaceObject has no constructor... + return false -proc setProperty*(obj: JSObject, name: string, prop: JSObject) = - if JS_SetPropertyStr(obj.ctx, obj.val, cstring(name), prop.val) <= 0: +proc setProperty*(ctx: JSContext, val: JSValue, name: string, prop: JSValue) = + if JS_SetPropertyStr(ctx, val, cstring(name), prop) <= 0: raise newException(Defect, "Failed to set property string: " & name) -proc setProperty*(obj: JSObject, name: string, fun: JSCFunction, argc: int = 0) = - obj.setProperty(name, obj.ctx.newJSCFunction(name, fun, argc)) +proc setProperty*(ctx: JSContext, val: JSValue, name: string, fun: JSCFunction, argc: int = 0) = + ctx.setProperty(val, name, ctx.newJSCFunction(name, fun, argc)) -func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, cctor: JSCFunction, funcs: JSFunctionList, parent: JSClassID = 0, asglobal = false, addto = none(JSObject)): JSClassID = +func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, cctor: JSCFunction, funcs: JSFunctionList, nimt: pointer, parent: JSClassID, asglobal: bool, nointerface: bool): JSClassID {.discardable.} = let rt = JS_GetRuntime(ctx) discard JS_NewClassID(addr result) var ctxOpaque = ctx.getOpaque() var rtOpaque = rt.getOpaque() - ctxOpaque.creg[$cdef.class_name] = result if JS_NewClass(rt, result, cdef) != 0: raise newException(Defect, "Failed to allocate JS class: " & $cdef.class_name) - var proto: JSObject + ctxOpaque.typemap[nimt] = result + ctxOpaque.creg[$cdef.class_name] = result + var proto: JSValue if parent != 0: - let parentProto = ctx.getClassProto(parent) - proto = ctx.newJSObject(parentProto) - free(parentProto) + let parentProto = JS_GetClassProto(ctx, parent) + proto = JS_NewObjectProtoClass(ctx, parentProto, parent) + JS_FreeValue(ctx, parentProto) else: - proto = ctx.newJSObject() + proto = JS_NewObject(ctx) if funcs.len > 0: rtOpaque.flist.add(funcs.toSeq()) - JS_SetPropertyFunctionList(ctx, proto.val, addr rtOpaque.flist[^1][0], cint(funcs.len)) - JS_SetClassProto(ctx, result, proto.val) + JS_SetPropertyFunctionList(ctx, proto, addr rtOpaque.flist[^1][0], cint(funcs.len)) + assert JS_SetProperty(ctx, proto, ctxOpaque.sym_toStringTag, JS_NewString(ctx, cdef.class_name)) == 1 + JS_SetClassProto(ctx, result, proto) if asglobal: let global = ctx.getGlobalObject() assert ctxOpaque.gclaz == "" ctxOpaque.gclaz = $cdef.class_name - if JS_SetPrototype(ctx, global.val, proto.val) != 1: + if JS_SetPrototype(ctx, global.val, proto) != 1: raise newException(Defect, "Failed to set global prototype: " & $cdef.class_name) free(global) - if addto.issome: + if not nointerface: + let global = JS_GetGlobalObject(ctx) let jctor = ctx.newJSCFunction($cdef.class_name, cctor, 0, JS_CFUNC_constructor) - JS_SetConstructor(ctx, jctor.val, proto.val) - addto.get.setProperty($cdef.class_name, jctor) + JS_SetConstructor(ctx, jctor, proto) + ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor) + ctx.setProperty(global, $cdef.class_name, jctor) + JS_FreeValue(ctx, global) type FuncParam = tuple[name: string, t: NimNode, val: Option[NimNode], generic: Option[NimNode]] @@ -291,12 +311,12 @@ func fromJSInt[T: SomeInteger](ctx: JSContext, val: JSValue): Option[T] = var ret: int32 if JS_ToInt32(ctx, addr ret, val) < 0: return none(T) - return some(ret) + return some(int(ret)) else: var ret: int64 if JS_ToInt64(ctx, addr ret, val) < 0: return none(T) - return some(ret) + return some(int(ret)) elif T is uint: when sizeof(int) <= sizeof(int32): var ret: uint32 @@ -367,6 +387,14 @@ macro fromJSTupleBody(a: tuple) = ) if i == len - 1: result.add(quote do: + let next = JS_Call(ctx, next_method, it, 0, nil) + if JS_IsException(next): + return none(T) + defer: JS_FreeValue(ctx, next) + let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().done) + `done` = fromJS[bool](ctx, doneVal) + if `done`.isnone: # exception + return none(T) var i = `i` # we're simulating a sequence, so we must query all remaining parameters too: while not `done`.get: @@ -402,8 +430,8 @@ proc fromJSTuple[T: tuple](ctx: JSContext, val: JSValue): Option[T] = return none(T) defer: JS_FreeValue(ctx, next_method) var x: T - result = some(x) - fromJSTupleBody(result.get) + fromJSTupleBody(x) + return some(x) proc fromJSSeq[T](ctx: JSContext, val: JSValue): Option[seq[T]] = let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_iterator) @@ -444,7 +472,6 @@ proc fromJSSeq[T](ctx: JSContext, val: JSValue): Option[seq[T]] = proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] = var ptab: ptr JSPropertyEnum - #TODO free ptab var plen: uint32 let flags = cint(JS_GPN_STRING_MASK) if JS_GetOwnPropertyNames(ctx, addr ptab, addr plen, val, flags) < -1: @@ -471,12 +498,14 @@ proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] = return none(Table[A, B]) result.get[kn.get] = vn.get -#TODO handle exceptions in all cases proc fromJS[T](ctx: JSContext, val: JSValue): Option[T] = when T is string: return toString(ctx, val) - elif typeof(result.get) is Option: # unwrap - return fromJS[typeof(result.get)](ctx, val) + elif typeof(result.unsafeGet) is Option: # unwrap + let res = fromJS[typeof(result.get.get)](ctx, val) + if res.isnone: + return none(T) + return some(res) elif T is seq: return fromJSSeq[typeof(result.get[0])](ctx, val) elif T is tuple: @@ -491,13 +520,30 @@ proc fromJS[T](ctx: JSContext, val: JSValue): Option[T] = elif typeof(result.get) is Table: return fromJSTable[typeof(result.get.keys), typeof(result.get.values)](ctx, val) elif T is SomeInteger: - return fromJSInt[T](obj.ctx, obj.val) + return fromJSInt[T](ctx, val) elif T is SomeFloat: let f64: float64 if JS_ToFloat64(ctx, addr f64, val) < 0: return none(T) return some(cast[T](f64)) + elif T is enum: + #TODO implement enum handling... + if JS_IsException(val): + return none(T) + let s = toString(ctx, val) + if s.isnone: + return none(T) + try: + return some(parseEnum[T](s.get)) + except ValueError: + JS_ThrowTypeError(ctx, "`%s' is not a valid value for enumeration %s", cstring(s.get), $T) + return none(T) + elif T is object: + #TODO TODO TODO dictionary case + return none(T) else: + if JS_IsException(val): + return none(T) let op = cast[T](getOpaque(ctx, val, $T)) if op == nil: JS_ThrowTypeError(ctx, "Value is not an instance of %s", $T) @@ -510,7 +556,7 @@ func toJSString(ctx: JSContext, str: string): JSValue = func toJSInt(ctx: JSContext, n: SomeInteger): JSValue = when n is int: when sizeof(int) <= sizeof(int32): - return JS_NewInt32(ctx, n) + return JS_NewInt32(ctx, int32(n)) else: return JS_NewInt64(ctx, n) elif n is uint: @@ -529,34 +575,62 @@ func toJSInt(ctx: JSContext, n: SomeInteger): JSValue = func toJSNumber(ctx: JSContext, n: SomeNumber): JSValue = when n is SomeInteger: - return toJSInt(n) + return toJSInt(ctx, n) else: - return JS_NewFloat64(n) + return JS_NewFloat64(ctx, n) func toJSBool(ctx: JSContext, b: bool): JSValue = return JS_NewBool(ctx, b) -func toJSObject[T](ctx: JSContext, obj: T, class: string): JSValue = - let claz = ctx.findClass(class) - assert claz.issome +proc getTypePtr[T](x: T): pointer = + when T is RootRef: + # I'm so sorry. + # (This dereferences the object's first member, m_type. Probably.) + return cast[ptr pointer](x)[] + else: + return getTypeInfo(x) + +func toJSObject[T](ctx: JSContext, obj: T): JSValue = let op = JS_GetRuntime(ctx).getOpaque() let p = cast[pointer](obj) if p in op.plist: # a JSValue already points to this object. return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, op.plist[p])) - let jsObj = ctx.newJSObject(claz.get) + let clazz = ctx.getOpaque().typemap[getTypePtr(obj)] + let jsObj = ctx.newJSObject(clazz) jsObj.setOpaque(obj) return jsObj.val -proc toJS*[T](ctx: JSContext, obj: T, class = ""): JSValue = +proc toJS*[T](ctx: JSContext, obj: T): JSValue = when T is string: - ctx.toJSString(obj) + return ctx.toJSString(obj) elif T is SomeNumber: - ctx.toJSNumber(obj) + return ctx.toJSNumber(obj) elif T is bool: - ctx.toJSBool(obj) + return ctx.toJSBool(obj) + elif T is Table: + result = JS_NewObject(ctx) + if not JS_IsException(result): + for k, v in obj: + setProperty(ctx, result, k, toJS(ctx, v)) + elif T is Option: + if obj.issome: + return toJS(ctx, obj.get) + return JS_NULL + elif T is seq: + let a = JS_NewArray(ctx) + if not JS_IsException(a): + for i in 0..obj.high: + let j = toJS(ctx, obj[i]) + if JS_IsException(j): + return j + if JS_DefinePropertyValueInt64(ctx, a, int64(i), j, JS_PROP_C_W_E or JS_PROP_THROW) < 0: + return JS_EXCEPTION + return a else: - ctx.toJSObject(obj, class) + if obj == nil: + return JS_NULL + return ctx.toJSObject(obj) type JS_Error = object of CatchableError @@ -642,7 +716,8 @@ proc getParams(fun: NimNode): seq[FuncParam] = quote do: typeof(`x`) let val = if it[2].kind != nnkEmpty: - some(it[2]) + let x = it[2] + some(newPar(x)) else: none(NimNode) var g = none(NimNode) @@ -738,16 +813,17 @@ proc addUnionParam(gen: var JSFuncGenerator, tt: NimNode, s: NimNode, fallback: elif g == string.getType(): hasString = true # 4. If V is null or undefined, then: - if tableg.issome: - let a = tableg.get[1] - let b = tableg.get[2] - gen.addUnionParamBranch(quote do: ( - let val = getJSValue(ctx, argv, `j`) - JS_IsNull(val) or JS_IsUndefined(val) - ), - quote do: - let `s` = Table[`a`, `b`](), #TODO is this correct? - fallback) + #TODO this is wrong. map dictionary to object instead + #if tableg.issome: + # let a = tableg.get[1] + # let b = tableg.get[2] + # gen.addUnionParamBranch(quote do: ( + # let val = getJSValue(ctx, argv, `j`) + # JS_IsNull(val) or JS_IsUndefined(val) + # ), + # quote do: + # let `s` = Table[`a`, `b`](), + # fallback) # 10. If Type(V) is Object, then: if tableg.issome or seqg.issome: # Sequence: @@ -860,8 +936,7 @@ proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode = ) if gen.hasthis: result.add(quote do: - var this = JSObject(ctx: ctx, val: this) - if not (this.isUndefined() or ctx.isGlobal(`tt`)) and not this.isInstanceOf(`tt`): + if not (JS_IsUndefined(this) or ctx.isGlobal(`tt`)) and not isInstanceOf(ctx, this, `tt`): # undefined -> global. return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `tt`) ) @@ -870,16 +945,13 @@ proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode = var tryWrap = newNimNode(nnkTryStmt) tryWrap.add(gen.jsCallAndRet) for error in js_errors[gen.funcName]: - let ename = ident("JS_" & error) + let ename = ident(error) var exceptBranch = newNimNode(nnkExceptBranch) let eid = ident("e") exceptBranch.add(newNimNode(nnkInfix).add(ident("as"), ename, eid)) - let throwName = ident("JS_Throw" & error) - exceptBranch.add(newCall(throwName, - ident("ctx"), - newLit("%s"), - newCall(ident("cstring"), - newDotExpr(eid, ident("msg"))))) + let throwName = ident("JS_Throw" & error.substr("JS_".len)) + exceptBranch.add(quote do: + return `throwName`(ctx, "%s", cstring(`eid`.msg))) tryWrap.add(exceptBranch) gen.jsCallAndRet = tryWrap result.add(gen.jsCallAndRet) @@ -890,14 +962,15 @@ proc newJSProc(gen: var JSFuncGenerator, params: openArray[NimNode], isva = true result = newProc(ident(gen.newName), params, jsBody, pragmas = jsPragmas) gen.res = result +# WARNING: for now, this only works correctly when the .jserr pragma was +# declared on the parent function. # Note: this causes the entire nim function body to be inlined inside the JS # interface function. #TODO: implement actual inlining (so we can e.g. get rid of JS_Error, use format strings, etc.) -macro JS_THROW*(a: untyped, b: string) = - let es = ident("JS_" & a.strVal) +macro JS_THROW*(a: typed, b: string) = result = quote do: block when_js: - raise newException(`es`, `b`) + raise newException(`a`, `b`) proc setupGenerator(fun: NimNode, hasthis = true, hasfuncall = true): JSFuncGenerator = result.funcName = $fun[0] @@ -952,7 +1025,7 @@ macro jserr*(fun: untyped) = gen.rewriteExceptions() var pragma = gen.original.findChild(it.kind == nnkPragma) for i in 0..<pragma.len: - if pragma[i] == ident("jsctor"): + if pragma[i].eqIdent(ident("jsctor")) or pragma[i].eqIdent(ident("jsfunc")) or pragma[i].eqIdent(ident("jsget")) or pragma[i].eqIdent(ident("jsset")): pragma.del(i) gen.copied.addPragma(quote do: inline) @@ -974,9 +1047,8 @@ macro jsctor*(fun: typed) = gen.addOptionalParams() gen.finishFunCallList() let jfcl = gen.jsFunCallList - let tt = gen.thisType gen.jsCallAndRet = quote do: - return ctx.toJS((`jfcl`), `tt`) + return ctx.toJS(`jfcl`) discard gen.newJSProc(getJSParams()) gen.registerFunction() result = newStmtList(fun) @@ -992,9 +1064,8 @@ macro jsget*(fun: typed) = gen.addFixParam("this") gen.finishFunCallList() let jfcl = gen.jsFunCallList - let tt = gen.thisType gen.jsCallAndRet = quote do: - return ctx.toJS(`jfcl`, `tt`) + return ctx.toJS(`jfcl`) let jsProc = gen.newJSProc(getJSGetterParams(), false) gen.registerFunction() result = newStmtList(fun, jsProc) @@ -1033,10 +1104,9 @@ macro jsfunc*(fun: typed) = gen.addOptionalParams() gen.finishFunCallList() let jfcl = gen.jsFunCallList - let tt = gen.thisType gen.jsCallAndRet = if gen.returnType.issome: quote do: - return ctx.toJS(`jfcl`, `tt`) + return ctx.toJS(`jfcl`) else: quote do: `jfcl` @@ -1126,7 +1196,7 @@ template fromJS_or_return*(t, ctx, val: untyped): untyped = x.get ) -macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = false, addto = none(JSObject)): JSClassID = +macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = false, nointerface = false): JSClassID = let s = t.strVal var sctr = ident("js_illegal_ctor") var sfin = ident("js_" & s & "ClassFin") @@ -1144,44 +1214,64 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = fals let fn = $node result.add(quote do: proc `id`(ctx: JSContext, this: JSValue): JSValue = - if not (JS_IsUndefined(this) or ctx.isGlobal(`s`)) and not JSObject(ctx: ctx, val: this).isInstanceOf(`s`): + if not (JS_IsUndefined(this) or ctx.isGlobal(`s`)) and not ctx.isInstanceOf(this, `s`): # undefined -> global. return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `s`) let arg_0 = fromJS_or_return(`t`, ctx, this) - return toJS(ctx, arg_0.`node`, $typeof(arg_0.`node`)) + return toJS(ctx, arg_0.`node`) ) - RegisteredFunctions[s].add((node, id)) + if s notin RegisteredFunctions: + RegisteredFunctions[s] = @[(node, id)] + else: + RegisteredFunctions[s].add((node, id)) for node in pragmas["jsset"]: let id = ident("js_set_" & s & "_" & $node) let fn = $node result.add(quote do: proc `id`(ctx: JSContext, this: JSValue, val: JSValue): JSValue = - var this = JSObject(ctx: ctx, val: this) - if not (this.isUndefined() or ctx.isGlobal(`s`)) and not this.isInstanceOf(`s`): + if not (JS_IsUndefined(this) or ctx.isGlobal(`s`)) and not ctx.isInstanceOf(this, `s`): # undefined -> global. return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `s`) - let arg_0 = fromJS[`t`](this) - let arg_1 = JSObject(ctx: ctx, val: val) - arg_0.`node` = fromJS[typeof(arg_0.`node`)](arg_1) - return JS_DupValue(ctx, arg_1.val) + let arg_0 = (block: + let t = fromJS[`t`](ctx, this) + if t.isnone: + return JS_EXCEPTION + t.get + ) + let arg_1 = val + arg_0.`node` = (block: + let t = fromJS[typeof(arg_0.`node`)](ctx, arg_1) + if t.isnone: + return JS_EXCEPTION + t.get + ) + return JS_DupValue(ctx, arg_1) ) - RegisteredFunctions[s].add((node, id)) - let tabList = newNimNode(nnkBracket) - for fun in RegisteredFunctions[s]: - let f0 = fun[0] - let f1 = fun[1] - if f1.strVal.startsWith("js_new"): - ctorImpl = js_funcs[$f0].res - if ctorFun != nil: - error("Class " & $s & " has 2+ constructors.") - ctorFun = f1 - elif f1.strVal.startsWith("js_get"): - getters[$f0] = f1 - elif f1.strVal.startsWith("js_set"): - setters[$f0] = f1 + if s notin RegisteredFunctions: + RegisteredFunctions[s] = @[(node, id)] else: - tabList.add(quote do: - JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`))) + RegisteredFunctions[s].add((node, id)) + let tabList = newNimNode(nnkBracket) + if s in RegisteredFunctions: + for fun in RegisteredFunctions[s]: + #TODO this is a mess + var f0 = fun[0] + let f1 = fun[1] + let f2 = fun[0] + if f0.strVal.endsWith("_exceptions"): + f0 = newLit(f0.strVal.substr(0, f0.strVal.high - "_exceptions".len)) + if f1.strVal.startsWith("js_new"): + ctorImpl = js_funcs[$f2].res + if ctorFun != nil: + error("Class " & $s & " has 2+ constructors.") + ctorFun = f1 + elif f1.strVal.startsWith("js_get"): + getters[$f0] = f1 + elif f1.strVal.startsWith("js_set"): + setters[$f0] = f1 + else: + tabList.add(quote do: + JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`))) for k, v in getters: if k in setters: let s = setters[k] @@ -1233,7 +1323,7 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = fals var x: `t` new(x, nim_finalize_for_js) const classDef = JSClassDef(class_name: `s`, finalizer: `sfin`) - `ctx`.newJSClass(JSClassDefConst(unsafeAddr classDef), `sctr`, `tabList`, `parent`, `asglobal`, `addto`) + `ctx`.newJSClass(JSClassDefConst(unsafeAddr classDef), `sctr`, `tabList`, getTypePtr(x), `parent`, `asglobal`, `nointerface`) ) proc getMemoryUsage*(rt: JSRuntime): string = diff --git a/src/main.nim b/src/main.nim index 51824798..584c92e6 100644 --- a/src/main.nim +++ b/src/main.nim @@ -68,6 +68,12 @@ while i < params.len: help(1) of "-h", "--help": help(0) + of "-r", "--run": + inc i + if i < params.len: + gconfig.startup = params[i] + else: + help(1) of "--": escape_all = true elif param.len > 0: @@ -77,7 +83,7 @@ while i < params.len: pages.add(param) inc i -if pages.len == 0: +if pages.len == 0 and gconfig.startup == "": if stdin.isatty: help(1) diff --git a/src/types/url.nim b/src/types/url.nim index 4e258b22..ef5c32ae 100644 --- a/src/types/url.nim +++ b/src/types/url.nim @@ -911,20 +911,57 @@ proc newURLSearchParams*[T: seq[(string, string)]|Table[string, string]|string]( proc `$`*(params: URLSearchParams): string {.jsfunc.} = return serializeApplicationXWWWFormUrlEncoded(params.list) +proc update(params: URLSearchParams) = + if params.url.isnone: + return + let serializedQuery = $params + if serializedQuery == "": + params.url.get.query = none(string) + else: + params.url.get.query = some(serializedQuery) + +proc append*(params: URLSearchParams, name: string, value: string) {.jsfunc.} = + params.list.add((name, value)) + params.update() + +proc delete*(params: URLSearchParams, name: string) {.jsfunc.} = + for i in countdown(params.list.high, 0): + if params.list[i][0] == name: + params.list.delete(i) + +proc get*(params: URLSearchParams, name: string): Option[string] {.jsfunc.} = + for it in params.list: + if it[0] == name: + return some(it[1]) + +proc getAll*(params: URLSearchParams, name: string): seq[string] {.jsfunc.} = + for it in params.list: + if it[0] == name: + result.add(it[1]) + +proc set*(params: URLSearchParams, name: string, value: string) {.jsfunc.} = + var first = true + for i in 0..params.list.high: + if params.list[i][0] == name: + if first: + first = false + params.list[i][1] = value + #TODO add Option wrapper proc newURL*(s: string, base: Option[string] = none(string)): URL {.jserr, jsctor.} = if base.issome: let baseUrl = parseUrl(base.get) if baseUrl.isnone: - JS_THROW TypeError, base.get & " is not a valid URL" + JS_THROW JS_TypeError, base.get & " is not a valid URL" let url = parseUrl(s, baseUrl) if url.isnone: - JS_THROW TypeError, s & " is not a valid URL" + JS_THROW JS_TypeError, s & " is not a valid URL" return url.get let url = parseUrl(s) if url.isnone: - JS_THROW TypeError, s & " is not a valid URL" + JS_THROW JS_TypeError, s & " is not a valid URL" url.get.searchParams = newURLSearchParams() + url.get.searchParams.url = url url.get.searchParams.initURLSearchParams(url.get.query.get("")) return url.get @@ -1030,7 +1067,5 @@ proc hash*(url: URL, s: string) {.jsset.} = discard basicParseUrl(s, url = url, stateOverride = some(FRAGMENT_STATE)) proc addUrlModule*(ctx: JSContext) = - let global = ctx.getGlobalObject() - discard ctx.registerType(URL, addto = some(global)) - discard ctx.registerType(URLSearchParams, addto = some(global)) - free(global) + ctx.registerType(URL) + ctx.registerType(URLSearchParams) diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 861edd48..41ffd09f 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -102,18 +102,16 @@ func findChar*(str: string, c: Rune, start: int = 0): int = i = n return -1 -func getLowerChars*(): string = - result = "" +const lowerChars = (func(): array[char, char] = for i in 0..255: - if chr(i) in 'A'..'Z': - result &= chr(i + 32) + if char(i) in 'A'..'Z': + result[char(i)] = char(i + 32) else: - result &= chr(i) - -const lowerChars = getLowerChars() + result[char(i)] = char(i) +)() func tolower*(c: char): char = - return lowerChars[int(c)] + return lowerChars[c] func toAsciiLower*(str: string): string = result = newString(str.len) @@ -137,45 +135,31 @@ func startsWithNoCase*(str, prefix: string): bool = if str[i].tolower() != prefix[i].tolower(): return false inc i -func genHexCharMap(): seq[int] = +const hexCharMap = (func(): array[char, int] = for i in 0..255: case chr(i) - of '0'..'9': result &= i - ord('0') - of 'a'..'f': result &= i - ord('a') + 10 - of 'A'..'F': result &= i - ord('A') + 10 - else: result &= -1 + of '0'..'9': result[char(i)] = i - ord('0') + of 'a'..'f': result[char(i)] = i - ord('a') + 10 + of 'A'..'F': result[char(i)] = i - ord('A') + 10 + else: result[char(i)] = -1 +)() -func genDecCharMap(): seq[int] = +const decCharMap = (func(): array[char, int] = for i in 0..255: - case chr(i) - of '0'..'9': result &= i - ord('0') - else: result &= -1 - -const hexCharMap = genHexCharMap() -const decCharMap = genDecCharMap() + case char(i) + of '0'..'9': result[char(i)] = i - ord('0') + else: result[char(i)] = -1 +)() func hexValue*(c: char): int = - return hexCharMap[int(c)] + return hexCharMap[c] func decValue*(c: char): int = - return decCharMap[int(c)] - -func isAscii*(c: char): bool = - return int(c) < 128 + return decCharMap[c] func isAscii*(r: Rune): bool = return int(r) < 128 -func hexValue*(r: Rune): int = - if int(r) < 256: - return hexValue(char(r)) - return -1 - -func decValue*(r: Rune): int = - if int(r) < 256: - return decValue(char(r)) - return -1 - const HexChars = "0123456789ABCDEF" func toHex*(c: char): string = result = newString(2) @@ -231,14 +215,25 @@ func skipBlanks*(buf: string, at: int): int = while result < buf.len and buf[result].isWhitespace(): inc result -func until*(s: string, c: char): string = +func until*(s: string, c: set[char]): string = var i = 0 while i < s.len: - if s[i] == c: + if s[i] in c: break result.add(s[i]) inc i +func until*(s: string, c: char): string = s.until({c}) + +func after*(s: string, c: set[char]): string = + var i = 0 + while i < s.len: + if s[i] in c: + return s.substr(i + 1) + inc i + +func after*(s: string, c: char): string = s.after({c}) + func number_additive*(i: int, range: HSlice[int, int], symbols: openarray[(int, string)]): string = if i notin range: return $i @@ -465,7 +460,7 @@ else: proc percentEncode*(append: var string, c: char, set: set[char], spaceAsPlus = false) {.inline.} = if spaceAsPlus and c == ' ': - append &= c + append &= '+' elif c notin set: append &= c else: @@ -474,13 +469,13 @@ proc percentEncode*(append: var string, c: char, set: set[char], spaceAsPlus = f proc percentEncode*(append: var string, s: string, set: set[char], spaceAsPlus = false) {.inline.} = for c in s: - append.percentEncode(c, set) + append.percentEncode(c, set, spaceAsPlus) -func percentEncode*(c: char, set: set[char]): string {.inline.} = - result.percentEncode(c, set) +func percentEncode*(c: char, set: set[char], spaceAsPlus = false): string {.inline.} = + result.percentEncode(c, set, spaceAsPlus) -func percentEncode*(s: string, set: set[char]): string = - result.percentEncode(s, set) +func percentEncode*(s: string, set: set[char], spaceAsPlus = false): string = + result.percentEncode(s, set, spaceAsPlus) func percentDecode*(input: string): string = var i = 0 |