diff options
31 files changed, 2743 insertions, 400 deletions
diff --git a/lib/endians2.nim b/lib/endians2.nim new file mode 100644 index 00000000..c9a39144 --- /dev/null +++ b/lib/endians2.nim @@ -0,0 +1,186 @@ +# Copyright (c) 2018-2019 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +# Endian conversion operations for unsigned integers, suitable for serializing +# and deserializing data. The operations are only defined for unsigned +# integers - if you wish to encode signed integers, convert / cast them to +# unsigned first! +# +# Although it would be possible to enforce correctness with endians in the type +# (`BigEndian[uin64]`) this seems like overkill. That said, some +# static analysis tools allow you to annotate fields with endianness - perhaps +# an idea for the future, akin to `TaintedString`? +# +# Keeping the above in mind, it's generally safer to use `array[N, byte]` to +# hold values of specific endianness and read them out with `fromBytes` when the +# integer interpretation of the bytes is needed. + +{.push raises: [].} + +type + SomeEndianInt* = uint8|uint16|uint32|uint64 + ## types that we support endian conversions for - uint8 is there for + ## for syntactic / generic convenience. Other candidates: + ## * int/uint - uncertain size, thus less suitable for binary interop + ## * intX - over and underflow protection in nim might easily cause issues - + ## need to consider before adding here + +const + useBuiltins = not defined(noIntrinsicsEndians) + +when (defined(gcc) or defined(llvm_gcc) or defined(clang)) and useBuiltins: + func swapBytesBuiltin(x: uint8): uint8 = x + func swapBytesBuiltin(x: uint16): uint16 {. + importc: "__builtin_bswap16", nodecl.} + + func swapBytesBuiltin(x: uint32): uint32 {. + importc: "__builtin_bswap32", nodecl.} + + func swapBytesBuiltin(x: uint64): uint64 {. + importc: "__builtin_bswap64", nodecl.} + +elif defined(icc) and useBuiltins: + func swapBytesBuiltin(x: uint8): uint8 = x + func swapBytesBuiltin(a: uint16): uint16 {.importc: "_bswap16", nodecl.} + func swapBytesBuiltin(a: uint32): uint32 {.importc: "_bswap", nodec.} + func swapBytesBuiltin(a: uint64): uint64 {.importc: "_bswap64", nodecl.} + +elif defined(vcc) and useBuiltins: + func swapBytesBuiltin(x: uint8): uint8 = x + func swapBytesBuiltin(a: uint16): uint16 {. + importc: "_byteswap_ushort", cdecl, header: "<intrin.h>".} + + func swapBytesBuiltin(a: uint32): uint32 {. + importc: "_byteswap_ulong", cdecl, header: "<intrin.h>".} + + func swapBytesBuiltin(a: uint64): uint64 {. + importc: "_byteswap_uint64", cdecl, header: "<intrin.h>".} + +func swapBytesNim(x: uint8): uint8 = x +func swapBytesNim(x: uint16): uint16 = (x shl 8) or (x shr 8) + +func swapBytesNim(x: uint32): uint32 = + let v = (x shl 16) or (x shr 16) + + ((v shl 8) and 0xff00ff00'u32) or ((v shr 8) and 0x00ff00ff'u32) + +func swapBytesNim(x: uint64): uint64 = + var v = (x shl 32) or (x shr 32) + v = + ((v and 0x0000ffff0000ffff'u64) shl 16) or + ((v and 0xffff0000ffff0000'u64) shr 16) + + ((v and 0x00ff00ff00ff00ff'u64) shl 8) or + ((v and 0xff00ff00ff00ff00'u64) shr 8) + +func swapBytes*[T: SomeEndianInt](x: T): T {.inline.} = + ## Reverse the bytes within an integer, such that the most significant byte + ## changes place with the least significant one, etc + ## + ## Example: + ## doAssert swapBytes(0x01234567'u32) == 0x67452301 + when nimvm: + swapBytesNim(x) + else: + when declared(swapBytesBuiltin): + swapBytesBuiltin(x) + else: + swapBytesNim(x) + +func toBytes*(x: SomeEndianInt, endian: Endianness = system.cpuEndian): + array[sizeof(x), byte] {.noinit, inline.} = + ## Convert integer to its corresponding byte sequence using the chosen + ## endianness. By default, native endianness is used which is not portable! + let v = + if endian == system.cpuEndian: x + else: swapBytes(x) + + when nimvm: # No copyMem in vm + for i in 0..<sizeof(result): + result[i] = byte((v shr (i * 8)) and 0xff) + else: + copyMem(addr result, unsafeAddr v, sizeof(result)) + +func toBytesLE*(x: SomeEndianInt): + array[sizeof(x), byte] {.inline.} = + ## Convert a native endian integer to a little endian byte sequence + toBytes(x, littleEndian) + +func toBytesBE*(x: SomeEndianInt): + array[sizeof(x), byte] {.inline.} = + ## Convert a native endian integer to a native endian byte sequence + toBytes(x, bigEndian) + +func fromBytes*( + T: typedesc[SomeEndianInt], + x: openArray[byte], + endian: Endianness = system.cpuEndian): T {.inline.} = + ## Read bytes and convert to an integer according to the given endianness. + ## + ## Note: The default value of `system.cpuEndian` is not portable across + ## machines. + ## + ## Panics when `x.len < sizeof(T)` - for shorter buffers, copy the data to + ## an `array` first using `arrayops.initCopyFrom`, taking care to zero-fill + ## at the right end - usually the beginning for big endian and the end for + ## little endian, but this depends on the serialization of the bytes. + + # This check gets optimized away when the compiler can prove that the length + # is large enough - passing in an `array` or using a construct like + # ` toOpenArray(pos, pos + sizeof(T) - 1)` are two ways that this happens + doAssert x.len >= sizeof(T), "Not enough bytes for endian conversion" + + when nimvm: # No copyMem in vm + for i in 0..<sizeof(result): + result = result or (T(x[i]) shl (i * 8)) + else: + # `copyMem` helps compilers optimize the copy into a single instruction, when + # alignment etc permits + copyMem(addr result, unsafeAddr x[0], sizeof(result)) + + if endian != system.cpuEndian: + # The swap is turned into a CPU-specific instruction and/or combined with + # the copy above, again when conditions permit it - for example, on X86 + # fromBytesBE gets compiled into a single `MOVBE` instruction + result = swapBytes(result) + +func fromBytesBE*( + T: typedesc[SomeEndianInt], + x: openArray[byte]): T {.inline.} = + ## Read big endian bytes and convert to an integer. At runtime, v must contain + ## at least sizeof(T) bytes. By default, native endianness is used which is + ## not portable! + fromBytes(T, x, bigEndian) + +func toBE*[T: SomeEndianInt](x: T): T {.inline.} = + ## Convert a native endian value to big endian. Consider toBytesBE instead + ## which may prevent some confusion. + if cpuEndian == bigEndian: x + else: x.swapBytes + +func fromBE*[T: SomeEndianInt](x: T): T {.inline.} = + ## Read a big endian value and return the corresponding native endian + # there's no difference between this and toBE, except when reading the code + toBE(x) + +func fromBytesLE*( + T: typedesc[SomeEndianInt], + x: openArray[byte]): T {.inline.} = + ## Read little endian bytes and convert to an integer. At runtime, v must + ## contain at least sizeof(T) bytes. By default, native endianness is used + ## which is not portable! + fromBytes(T, x, littleEndian) + +func toLE*[T: SomeEndianInt](x: T): T {.inline.} = + ## Convert a native endian value to little endian. Consider toBytesLE instead + ## which may prevent some confusion. + if cpuEndian == littleEndian: x + else: x.swapBytes + +func fromLE*[T: SomeEndianInt](x: T): T {.inline.} = + ## Read a little endian value and return the corresponding native endian + # there's no difference between this and toLE, except when reading the code + toLE(x) diff --git a/res/unifont_jp-15.0.05.png b/res/unifont_jp-15.0.05.png new file mode 100644 index 00000000..d0e2a92d --- /dev/null +++ b/res/unifont_jp-15.0.05.png Binary files differdiff --git a/src/bindings/curl.nim b/src/bindings/curl.nim index ca31e4f2..169314fd 100644 --- a/src/bindings/curl.nim +++ b/src/bindings/curl.nim @@ -303,7 +303,7 @@ proc curl_mime_init*(handle: CURL): curl_mime proc curl_mime_free*(mime: curl_mime) proc curl_mime_addpart*(mime: curl_mime): curl_mimepart proc curl_mime_name*(part: curl_mimepart, name: cstring) -proc curl_mime_data*(part: curl_mimepart, data: cstring, datasize: csize_t) +proc curl_mime_data*(part: curl_mimepart, data: pointer, datasize: csize_t) proc curl_mime_filename*(part: curl_mimepart, name: cstring) proc curl_mime_filedata*(part: curl_mimepart, filename: cstring) diff --git a/src/bindings/zlib.nim b/src/bindings/zlib.nim new file mode 100644 index 00000000..00acb1ca --- /dev/null +++ b/src/bindings/zlib.nim @@ -0,0 +1,79 @@ +const zlib = (func(): string = + let res = staticExec("pkg-config --libs --silence-errors zlib") + if res != "": + return res +)() +when zlib == "": + error("zlib not found") + +{.passL: zlib.} + +const + Z_NO_FLUSH* = cint(0) + Z_PARTIAL_FLUSH* = cint(1) + Z_SYNC_FLUSH* = cint(2) + Z_FULL_FLUSH* = cint(3) + Z_FINISH* = cint(4) + Z_BLOCK* = cint(5) + Z_TREES* = cint(6) + +const + Z_OK* = cint(0) + Z_STREAM_END* = cint(1) + Z_NEED_DICT* = cint(2) + Z_ERRNO* = cint(-1) + Z_STREAM_ERROR* = cint(-2) + Z_DATA_ERROR* = cint(-3) + Z_MEM_ERROR* = cint(-4) + Z_BUF_ERROR* = cint(-5) + Z_VERSION_ERROR* = cint(-6) + +const + Z_BINARY* = cint(0) + Z_TEXT* = cint(1) + Z_ASCII* = Z_TEXT + Z_UNKNOWN* = cint(2) + +type + alloc_func* {.importc, header: "zlib.h".} = proc (opaque: pointer, + items: cuint, size: cuint): pointer {.cdecl.} + + free_func* {.importc, header: "zlib.h".} = proc (opaque: pointer, + address: pointer) {.cdecl.} + + internal_state* {.importc, header: "zlib.h".} = object + + z_stream* {.importc, header: "zlib.h".} = object + next_in*: ptr uint8 # next input byte + avail_in*: cuint # number of bytes available in next_in + total_in*: culong # total number of input bytes read so far + + next_out*: ptr uint8 # next output byte will go here + avail_out*: cuint # remaining free space at next_out + total_out*: culong # total number of bytes output so far + + msg*: cstring # last error message, NULL if no error + state*: ptr internal_state # not visible by applications + + zalloc*: alloc_func # used to allocate the internal state + zfree*: free_func # used to free the internal state + opaque*: pointer # private data object passed to zalloc and zfree + + data_type*: cint # best guess about the data type: binary or text + # for deflate, or the decoding state for inflate + adler*: culong # Adler-32 or CRC-32 value of the uncompressed data + reserved*: culong # reserved for future use + + z_streamp* = ptr z_stream + +{.push header: "zlib.h", importc, cdecl.} +proc inflateInit*(strm: z_streamp): cint +proc inflate*(strm: z_streamp, flush: cint): cint +proc inflateEnd*(strm: z_streamp): cint +proc compress*(dest: ptr uint8, destLen: ptr culong, source: ptr uint8, + sourceLen: culong): cint +proc compressBound*(sourceLen: culong): culong +proc uncompress*(dest: ptr uint8, destLen: ptr culong, source: ptr uint8, + sourceLen: culong): cint +proc crc32*(crc: culong, buf: ptr uint8, len: cuint): culong +{.pop.} diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim index 7fda2887..d668a01d 100644 --- a/src/buffer/buffer.nim +++ b/src/buffer/buffer.nim @@ -40,9 +40,11 @@ import render/rendertext import types/buffersource import types/color import types/cookie +import types/formdata import types/referer import types/url import utils/twtstr +import xhr/formdata as formdata_impl type LoadInfo* = enum @@ -751,82 +753,19 @@ proc cancel*(buffer: Buffer): int {.proxy.} = buffer.do_reshape() return buffer.lines.len -# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set -proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): seq[tuple[name, value: string]] = - if form.constructingentrylist: - return - form.constructingentrylist = true - - var entrylist: seq[tuple[name, value: string]] - for field in form.controls: - if field.findAncestor({TAG_DATALIST}) != nil or - field.attrb("disabled") or - field.isButton() and Element(field) != submitter: - continue - - if field.tagType == TAG_INPUT: - let field = HTMLInputElement(field) - if field.inputType == INPUT_IMAGE: - let name = if field.attr("name") != "": - field.attr("name") & '.' - else: - "" - entrylist.add((name & 'x', $field.xcoord)) - entrylist.add((name & 'y', $field.ycoord)) - continue - - #TODO custom elements - - let name = field.attr("name") - - if name == "": - continue - - if field.tagType == TAG_SELECT: - let field = HTMLSelectElement(field) - for option in field.options: - if option.selected or option.disabled: - entrylist.add((name, option.value)) - elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}: - let value = if field.attr("value") != "": - field.attr("value") - else: - "on" - entrylist.add((name, value)) - elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE: - #TODO file - discard - elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"): - let charset = if encoding != "": - encoding - else: - "UTF-8" - entrylist.add((name, charset)) - else: - case field.tagType - of TAG_INPUT: - entrylist.add((name, HTMLInputElement(field).value)) - of TAG_BUTTON: - entrylist.add((name, HTMLButtonElement(field).value)) - of TAG_TEXTAREA: - entrylist.add((name, HTMLTextAreaElement(field).value)) - else: assert false, "Tag type " & $field.tagType & " not accounted for in constructEntryList" - if field.tagType == TAG_TEXTAREA or - field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}: - if field.attr("dirname") != "": - let dirname = field.attr("dirname") - let dir = "ltr" #TODO bidi - entrylist.add((dirname, dir)) - - form.constructingentrylist = false - return entrylist - #https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm -proc serializeMultipartFormData(kvs: seq[(string, string)]): MimeData = - for it in kvs: - let name = makeCRLF(it[0]) - let value = makeCRLF(it[1]) - result[name] = value +proc serializeMultipartFormData(entries: seq[FormDataEntry]): FormData = + {.cast(noSideEffect).}: + # This is correct, because newFormData with no params has no side effects. + let formData = newFormData() + 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, entry.filename) + return formData proc serializePlainTextFormData(kvs: seq[(string, string)]): string = for it in kvs: @@ -837,7 +776,9 @@ proc serializePlainTextFormData(kvs: seq[(string, string)]): string = result &= "\r\n" func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = - let entrylist = form.constructEntryList(submitter) + if form.constructingEntryList: + return + let entrylist = form.constructEntryList(submitter).get(@[]) let action = if submitter.action() == "": $form.document.url @@ -868,25 +809,30 @@ func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] = #let noopener = true #TODO template mutateActionUrl() = - let query = serializeApplicationXWWWFormUrlEncoded(entrylist) + let kvlist = entrylist.toNameValuePairs() + let query = serializeApplicationXWWWFormUrlEncoded(kvlist) parsedaction.query = query.some return newRequest(parsedaction, httpmethod).some template submitAsEntityBody() = var mimetype: string var body = none(string) - var multipart = none(MimeData) + var multipart = none(FormData) case enctype of FORM_ENCODING_TYPE_URLENCODED: - body = serializeApplicationXWWWFormUrlEncoded(entrylist).some + let kvlist = entrylist.toNameValuePairs() + body = some(serializeApplicationXWWWFormUrlEncoded(kvlist)) mimeType = $enctype of FORM_ENCODING_TYPE_MULTIPART: - multipart = serializeMultipartFormData(entrylist).some + multipart = some(serializeMultipartFormData(entrylist)) mimetype = $enctype of FORM_ENCODING_TYPE_TEXT_PLAIN: - body = serializePlainTextFormData(entrylist).some + let kvlist = entrylist.toNameValuePairs() + body = some(serializePlainTextFormData(kvlist)) mimetype = $enctype - return newRequest(parsedaction, httpmethod, @{"Content-Type": mimetype}, body).some #TODO multipart + let req = newRequest(parsedaction, httpmethod, + @{"Content-Type": mimetype}, body) + return some(req) #TODO multipart template getActionUrl() = return newRequest(parsedaction).some diff --git a/src/config/toml.nim b/src/config/toml.nim index c34e46a7..05a202ab 100644 --- a/src/config/toml.nim +++ b/src/config/toml.nim @@ -1,5 +1,6 @@ import streams import tables +import options import times import strutils import strformat @@ -367,6 +368,7 @@ proc consumeNumber(state: var TomlParser, c: char): TomlValue = if state.has(1): if state.peek(0) == 'E' or state.peek(0) == 'e': + isfloat = true var j = 2 if state.peek(1) == '-' or state.peek(1) == '+': inc j @@ -383,7 +385,7 @@ proc consumeNumber(state: var TomlParser, c: char): TomlValue = return TomlValue(vt: VALUE_FLOAT, f: val) let val = parseInt64(repr) - return TomlValue(vt: VALUE_INTEGER, i: val) + return TomlValue(vt: VALUE_INTEGER, i: val.get) proc consumeValue(state: var TomlParser): TomlValue diff --git a/src/css/cascade.nim b/src/css/cascade.nim index 0cea87b7..7aac213f 100644 --- a/src/css/cascade.nim +++ b/src/css/cascade.nim @@ -127,17 +127,17 @@ func calcPresentationalHints(element: Element): CSSComputedValues = if c.isSome: set_cv "color", c.get template map_colspan = - let colspan = element.attrigz("colspan") + let colspan = element.attrulgz("colspan") if colspan.isSome: let i = colspan.get if i <= 1000: - set_cv "-cha-colspan", i + set_cv "-cha-colspan", int(i) template map_rowspan = - let rowspan = element.attrigez("rowspan") + let rowspan = element.attrul("rowspan") if rowspan.isSome: let i = rowspan.get if i <= 65534: - set_cv "-cha-rowspan", i + set_cv "-cha-rowspan", int(i) case element.tagType of TAG_DIV: @@ -170,8 +170,8 @@ func calcPresentationalHints(element: Element): CSSComputedValues = map_text of TAG_TEXTAREA: let textarea = HTMLTextAreaElement(element) - let cols = textarea.attri("cols").get(20) - let rows = textarea.attri("rows").get(1) + let cols = textarea.attrul("cols").get(20) + let rows = textarea.attrul("rows").get(1) set_cv "width", CSSLength(unit: UNIT_CH, num: float64(cols)) set_cv "height", CSSLength(unit: UNIT_EM, num: float64(rows)) of TAG_FONT: diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim index 9c8328de..57cfc87e 100644 --- a/src/css/cssparser.nim +++ b/src/css/cssparser.nim @@ -867,10 +867,10 @@ proc parseAnB*(state: var CSSParseState): Option[CSSAnB] = return none(CSSAnB) template parse_sub_int(sub: string, skip: int): int = let s = sub.substr(skip) - for c in s: - if c notin AsciiDigit: - return none(CSSAnB) - parseInt32(s) + let x = parseInt32(s) + if x.isNone: + return none(CSSAnB) + x.get template fail_non_integer(tok: CSSToken, res: Option[CSSAnB]) = if tok.tokenType != CSS_NUMBER_TOKEN: state.reconsume() diff --git a/src/css/values.nim b/src/css/values.nim index c786e47e..dd3f2df4 100644 --- a/src/css/values.nim +++ b/src/css/values.nim @@ -493,7 +493,7 @@ func skipWhitespace(vals: seq[CSSComponentValue], i: var int) = break inc i -func cssColor(val: CSSComponentValue): RGBAColor = +func cssColor*(val: CSSComponentValue): RGBAColor = if val of CSSToken: let tok = CSSToken(val) case tok.tokenType @@ -525,23 +525,15 @@ func cssColor(val: CSSComponentValue): RGBAColor = commaMode = true elif commaMode: raise newException(CSSValueError, "Invalid color") - if slash: + elif slash: if f.value[i] != CSS_DELIM_TOKEN or CSSToken(f.value[i]).rvalue != Rune('/'): raise newException(CSSValueError, "Invalid color") inc i f.value.skipWhitespace(i) - check_err + if not slash: + check_err case f.name - of "rgb": - f.value.skipWhitespace(i) - check_err - let r = CSSToken(f.value[i]).nvalue - next_value true - let g = CSSToken(f.value[i]).nvalue - next_value - let b = CSSToken(f.value[i]).nvalue - return rgba(int(r), int(g), int(b), 255) - of "rgba": + of "rgb", "rgba": f.value.skipWhitespace(i) check_err let r = CSSToken(f.value[i]).nvalue @@ -550,8 +542,11 @@ func cssColor(val: CSSComponentValue): RGBAColor = next_value let b = CSSToken(f.value[i]).nvalue next_value false, true - let a = CSSToken(f.value[i]).nvalue - return rgba(int(r), int(g), int(b), int(a)) + let a = if i < f.value.len: + CSSToken(f.value[i]).nvalue + else: + 1 + return rgba(int(r), int(g), int(b), int(a * 255)) else: discard raise newException(CSSValueError, "Invalid color") diff --git a/src/data/charset.nim b/src/data/charset.nim index 560e8643..f8f833d5 100644 --- a/src/data/charset.nim +++ b/src/data/charset.nim @@ -316,11 +316,6 @@ const CharsetMap = { "x-user-defined": CHARSET_X_USER_DEFINED }.toTable() -func normalizeLocale(s: string): string = - for i in 0 ..< s.len: - if cast[uint8](s[i]) > 0x20 and s[i] != '_' and s[i] != '-': - result &= s[i].toLowerAscii() - const NormalizedCharsetMap = (func(): Table[string, Charset] = for k, v in CharsetMap: result[k.normalizeLocale()] = v)() diff --git a/src/display/client.nim b/src/display/client.nim index d705eab1..95f036f8 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -31,12 +31,15 @@ import ips/forkserver import ips/serialize import ips/serversocket import ips/socketstream +import js/intl import js/javascript import js/module import js/timeout +import types/blob import types/cookie import types/dispatcher import types/url +import xhr/formdata as formdata_impl type Client* = ref ClientObj @@ -535,6 +538,9 @@ proc newClient*(config: Config, dispatcher: Dispatcher): Client = ctx.addURLModule() ctx.addDOMModule() ctx.addHTMLModule() + ctx.addIntlModule() + ctx.addBlobModule() + ctx.addFormDataModule() ctx.addRequestModule() ctx.addLineEditModule() ctx.addConfigModule() diff --git a/src/html/dom.nim b/src/html/dom.nim index 600639ee..28a77aa7 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -1,22 +1,31 @@ import deques import macros +import math import options import sets import streams import strutils import tables +import css/cssparser import css/sheet +import css/values import data/charset import encoding/decoderstream import html/tags +import img/bitmap +import img/path import io/loader import io/request import js/javascript import js/timeout +import types/blob +import types/color +import types/matrix import types/mime import types/referer import types/url +import types/vector import utils/twtstr type @@ -158,7 +167,7 @@ type charset*: Charset window*: Window url*: URL #TODO expose as URL (capitalized) - location {.jsget.}: URL #TODO should be location + location* {.jsget.}: URL #TODO should be location mode*: QuirksMode currentScript: HTMLScriptElement isxml*: bool @@ -305,19 +314,343 @@ type HTMLLabelElement* = ref object of HTMLElement + HTMLCanvasElement* = ref object of HTMLElement + ctx2d: CanvasRenderingContext2D + bitmap: Bitmap + + DrawingState = object + # CanvasTransform + transformMatrix: Matrix + # CanvasFillStrokeStyles + fillStyle: RGBAColor + strokeStyle: RGBAColor + # CanvasPathDrawingStyles + lineWidth: float64 + # CanvasTextDrawingStyles + textAlign: CSSTextAlign + # CanvasPath + path: Path + + RenderingContext = ref object of RootObj + + CanvasRenderingContext2D = ref object of RenderingContext + canvas {.jsget.}: HTMLCanvasElement + bitmap: Bitmap + state: DrawingState + stateStack: seq[DrawingState] + + TextMetrics = ref object + # x-direction + width {.jsget.}: float64 + actualBoundingBoxLeft {.jsget.}: float64 + actualBoundingBoxRight {.jsget.}: float64 + # y-direction + fontBoundingBoxAscent {.jsget.}: float64 + fontBoundingBoxDescent {.jsget.}: float64 + actualBoundingBoxAscent {.jsget.}: float64 + actualBoundingBoxDescent {.jsget.}: float64 + emHeightAscent {.jsget.}: float64 + emHeightDescent {.jsget.}: float64 + hangingBaseline {.jsget.}: float64 + alphabeticBaseline {.jsget.}: float64 + ideographicBaseline {.jsget.}: float64 + +proc parseColor(element: Element, s: string): RGBAColor + +proc resetTransform(state: var DrawingState) = + state.transformMatrix = newIdentityMatrix(3) + +proc resetState(state: var DrawingState) = + state.resetTransform() + state.fillStyle = rgba(0, 0, 0, 255) + state.strokeStyle = rgba(0, 0, 0, 255) + state.path = newPath() + +proc create2DContext*(target: HTMLCanvasElement, options: Option[JSObject]): + CanvasRenderingContext2D = + let ctx = CanvasRenderingContext2D( + bitmap: target.bitmap, + canvas: target + ) + ctx.state.resetState() + return ctx + +# CanvasState +proc save(ctx: CanvasRenderingContext2D) {.jsfunc.} = + ctx.stateStack.add(ctx.state) + +proc restore(ctx: CanvasRenderingContext2D) {.jsfunc.} = + if ctx.stateStack.len > 0: + ctx.state = ctx.stateStack.pop() + +proc reset(ctx: CanvasRenderingContext2D) {.jsfunc.} = + ctx.bitmap.clear() + #TODO empty list of subpaths + ctx.stateStack.setLen(0) + ctx.state.resetState() + +# CanvasTransform +#TODO scale +proc rotate(ctx: CanvasRenderingContext2D, angle: float64) {.jsfunc.} = + if classify(angle) in {fcInf, fcNegInf, fcNan}: + return + ctx.state.transformMatrix *= newMatrix( + me = @[ + cos(angle), -sin(angle), 0, + sin(angle), cos(angle), 0, + 0, 0, 1 + ], + w = 3, + h = 3 + ) + +proc translate(ctx: CanvasRenderingContext2D, x, y: float64) {.jsfunc.} = + for v in [x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + ctx.state.transformMatrix *= newMatrix( + me = @[ + 1f64, 0, x, + 0, 1, y, + 0, 0, 1 + ], + w = 3, + h = 3 + ) + +proc transform(ctx: CanvasRenderingContext2D, a, b, c, d, e, f: float64) + {.jsfunc.} = + for v in [a, b, c, d, e, f]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + ctx.state.transformMatrix *= newMatrix( + me = @[ + a, c, e, + b, d, f, + 0, 0, 1 + ], + w = 3, + h = 3 + ) + +#TODO getTransform, setTransform with DOMMatrix (i.e. we're missing DOMMatrix) +proc setTransform(ctx: CanvasRenderingContext2D, a, b, c, d, e, f: float64) + {.jsfunc.} = + for v in [a, b, c, d, e, f]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + ctx.state.resetTransform() + ctx.transform(a, b, c, d, e, f) + +proc resetTransform(ctx: CanvasRenderingContext2D) {.jsfunc.} = + ctx.state.resetTransform() + +func transform(ctx: CanvasRenderingContext2D, v: Vector2D): Vector2D = + let mul = ctx.state.transformMatrix * newMatrix(@[v.x, v.y, 1], 1, 3) + return Vector2D(x: mul.me[0], y: mul.me[1]) + +# CanvasFillStrokeStyles +proc fillStyle(ctx: CanvasRenderingContext2D): string {.jsfget.} = + return ctx.state.fillStyle.serialize() + +proc fillStyle(ctx: CanvasRenderingContext2D, s: string) {.jsfset.} = + #TODO gradient, pattern + ctx.state.fillStyle = ctx.canvas.parseColor(s) + +proc strokeStyle(ctx: CanvasRenderingContext2D): string {.jsfget.} = + return ctx.state.strokeStyle.serialize() + +proc strokeStyle(ctx: CanvasRenderingContext2D, s: string) {.jsfset.} = + #TODO gradient, pattern + ctx.state.strokeStyle = ctx.canvas.parseColor(s) + +# CanvasRect +proc clearRect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} = + for v in [x, y, w, h]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + #TODO clipping regions (right now we just clip to default) + let bw = float64(ctx.bitmap.width) + let bh = float64(ctx.bitmap.height) + let x0 = uint64(min(max(x, 0), bw)) + let x1 = uint64(min(max(x + w, 0), bw)) + let y0 = uint64(min(max(y, 0), bh)) + let y1 = uint64(min(max(y + h, 0), bh)) + ctx.bitmap.clearRect(x0, x1, y0, y1) + +proc fillRect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} = + for v in [x, y, w, h]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + #TODO do we have to clip here? + if w == 0 or h == 0: + return + let bw = float64(ctx.bitmap.width) + let bh = float64(ctx.bitmap.height) + let x0 = uint64(min(max(x, 0), bw)) + let x1 = uint64(min(max(x + w, 0), bw)) + let y0 = uint64(min(max(y, 0), bh)) + let y1 = uint64(min(max(y + h, 0), bh)) + ctx.bitmap.fillRect(x0, x1, y0, y1, ctx.state.fillStyle) + +proc strokeRect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} = + for v in [x, y, w, h]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + #TODO do we have to clip here? + if w == 0 or h == 0: + return + let bw = float64(ctx.bitmap.width) + let bh = float64(ctx.bitmap.height) + let x0 = uint64(min(max(x, 0), bw)) + let x1 = uint64(min(max(x + w, 0), bw)) + let y0 = uint64(min(max(y, 0), bh)) + let y1 = uint64(min(max(y + h, 0), bh)) + ctx.bitmap.strokeRect(x0, x1, y0, y1, ctx.state.strokeStyle) + +# CanvasDrawPath +proc beginPath(ctx: CanvasRenderingContext2D) {.jsfunc.} = + ctx.state.path.beginPath() + +proc fill(ctx: CanvasRenderingContext2D, + fillRule = CanvasFillRule.NON_ZERO) {.jsfunc.} = #TODO path + ctx.state.path.tempClosePath() + ctx.bitmap.fillPath(ctx.state.path, ctx.state.fillStyle, fillRule) + ctx.state.path.tempOpenPath() + +proc stroke(ctx: CanvasRenderingContext2D) {.jsfunc.} = #TODO path + ctx.bitmap.strokePath(ctx.state.path, ctx.state.strokeStyle) + +proc clip(ctx: CanvasRenderingContext2D, + fillRule = CanvasFillRule.NON_ZERO) {.jsfunc.} = #TODO path + #TODO implement + discard + +#TODO clip, ... + +# CanvasUserInterface + +# CanvasText +#TODO maxwidth +proc fillText(ctx: CanvasRenderingContext2D, text: string, x, y: float64) {.jsfunc.} = + for v in [x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + let vec = ctx.transform(Vector2D(x: x, y: y)) + ctx.bitmap.fillText(text, vec.x, vec.y, ctx.state.fillStyle, ctx.state.textAlign) + +#TODO maxwidth +proc strokeText(ctx: CanvasRenderingContext2D, text: string, x, y: float64) {.jsfunc.} = + for v in [x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + let vec = ctx.transform(Vector2D(x: x, y: y)) + ctx.bitmap.strokeText(text, vec.x, vec.y, ctx.state.strokeStyle, ctx.state.textAlign) + +proc measureText(ctx: CanvasRenderingContext2D, text: string): TextMetrics + {.jsfunc.} = + let tw = text.width() + return TextMetrics( + width: 8 * float64(tw), + actualBoundingBoxLeft: 0, + actualBoundingBoxRight: 8 * float64(tw), + #TODO and the rest... + ) + +# CanvasDrawImage + +# CanvasImageData + +# CanvasPathDrawingStyles +proc lineWidth(ctx: CanvasRenderingContext2D): float64 {.jsfget.} = + return ctx.state.lineWidth + +proc lineWidth(ctx: CanvasRenderingContext2D, f: float64) {.jsfset.} = + if classify(f) in {fcZero, fcNegZero, fcInf, fcNegInf, fcNan}: + return + ctx.state.lineWidth = f + +proc setLineDash(ctx: CanvasRenderingContext2D, segments: seq[float64]) + {.jsfunc.} = + discard #TODO implement + +proc getLineDash(ctx: CanvasRenderingContext2D): seq[float64] {.jsfunc.} = + discard #TODO implement + +# CanvasTextDrawingStyles +proc textAlign(ctx: CanvasRenderingContext2D): string {.jsfget.} = + case ctx.state.textAlign + of TEXT_ALIGN_START: return "start" + of TEXT_ALIGN_END: return "end" + of TEXT_ALIGN_LEFT: return "left" + of TEXT_ALIGN_RIGHT: return "right" + of TEXT_ALIGN_CENTER: return "center" + else: doAssert false + +proc textAlign(ctx: CanvasRenderingContext2D, s: string) {.jsfset.} = + ctx.state.textAlign = case s + of "start": TEXT_ALIGN_START + of "end": TEXT_ALIGN_END + of "left": TEXT_ALIGN_LEFT + of "right": TEXT_ALIGN_RIGHT + of "center": TEXT_ALIGN_CENTER + else: ctx.state.textAlign + +# CanvasPath +proc closePath(ctx: CanvasRenderingContext2D) {.jsfunc.} = + ctx.state.path.closePath() + +proc moveTo(ctx: CanvasRenderingContext2D, x, y: float64) {.jsfunc.} = + ctx.state.path.moveTo(x, y) + +proc lineTo(ctx: CanvasRenderingContext2D, x, y: float64) {.jsfunc.} = + ctx.state.path.lineTo(x, y) + +proc quadraticCurveTo(ctx: CanvasRenderingContext2D, cpx, cpy, x, + y: float64) {.jsfunc.} = + ctx.state.path.quadraticCurveTo(cpx, cpy, x, y) + +proc arcTo(ctx: CanvasRenderingContext2D, x1, y1, x2, y2, radius: float64) + {.jsfunc.} = + if not ctx.state.path.arcTo(x1, y1, x2, y2, radius): + #TODO should be DOMException + JS_ERR JS_TypeError, "IndexSizeError" + +proc arc(ctx: CanvasRenderingContext2D, x, y, radius, startAngle, + endAngle: float64, counterclockwise = false) {.jsfunc.} = + if not ctx.state.path.arc(x, y, radius, startAngle, endAngle, + counterclockwise): + #TODO should be DOMException + JS_ERR JS_TypeError, "IndexSizeError" + +proc ellipse(ctx: CanvasRenderingContext2D, x, y, radiusX, radiusY, rotation, + startAngle, endAngle: float64, counterclockwise = false) {.jsfunc.} = + if not ctx.state.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle, + endAngle, counterclockwise): + #TODO should be DOMException + JS_ERR JS_TypeError, "IndexSizeError" + +proc rect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} = + ctx.state.path.rect(x, y, w, h) + +proc roundRect(ctx: CanvasRenderingContext2D, x, y, w, h, radii: float64) {.jsfunc.} = + ctx.state.path.roundRect(x, y, w, h, radii) + # Reflected attributes. type ReflectType = enum - REFLECT_STR, REFLECT_BOOL, REFLECT_INT, REFLECT_INT_GREATER_ZERO, - REFLECT_INT_GREATER_EQUAL_ZERO - - ReflectEntry = tuple[ - attrname: string, - funcname: string, - t: ReflectType, - tags: set[TagType], - i: int - ] + REFLECT_STR, REFLECT_BOOL, REFLECT_LONG, REFLECT_ULONG_GZ, REFLECT_ULONG + + ReflectEntry = object + attrname: string + funcname: string + tags: set[TagType] + case t: ReflectType + of REFLECT_LONG: + i: int32 + of REFLECT_ULONG, REFLECT_ULONG_GZ: + u: uint32 + else: discard template toset(ts: openarray[TagType]): set[TagType] = var tags: system.set[TagType] @@ -325,23 +658,53 @@ template toset(ts: openarray[TagType]): set[TagType] = tags.incl(tag) tags -template makes(name: string, ts: set[TagType]): ReflectEntry = - (name, name, REFLECT_STR, ts, 0) +func makes(name: string, ts: set[TagType]): ReflectEntry = + ReflectEntry( + attrname: name, + funcname: name, + t: REFLECT_STR, + tags: ts + ) -template makes(attrname: string, funcname: string, ts: set[TagType]): ReflectEntry = - (attrname, funcname, REFLECT_STR, ts, 0) +func makes(attrname: string, funcname: string, ts: set[TagType]): ReflectEntry = + ReflectEntry( + attrname: attrname, + funcname: funcname, + t: REFLECT_STR, + tags: ts + ) -template makes(name: string, ts: varargs[TagType]): ReflectEntry = +func makes(name: string, ts: varargs[TagType]): ReflectEntry = makes(name, toset(ts)) -template makes(attrname: string, funcname: string, ts: varargs[TagType]): ReflectEntry = +func makes(attrname, funcname: string, ts: varargs[TagType]): ReflectEntry = makes(attrname, funcname, toset(ts)) template makeb(name: string, ts: varargs[TagType]): ReflectEntry = - (name, name, REFLECT_BOOL, toset(ts), 0) + ReflectEntry( + attrname: name, + funcname: name, + t: REFLECT_BOOL, + tags: toset(ts) + ) -template makeigz(name: string, ts: varargs[TagType], default = 0): ReflectEntry = - (name, name, REFLECT_INT_GREATER_ZERO, toset(ts), default) +template makeul(name: string, ts: varargs[TagType], default = 0u32): ReflectEntry = + ReflectEntry( + attrname: name, + funcname: name, + t: REFLECT_ULONG, + tags: toset(ts), + u: default + ) + +template makeulgz(name: string, ts: varargs[TagType], default = 0u32): ReflectEntry = + ReflectEntry( + attrname: name, + funcname: name, + t: REFLECT_ULONG_GZ, + tags: toset(ts), + u: default + ) const ReflectTable0 = [ # non-global attributes @@ -350,15 +713,17 @@ const ReflectTable0 = [ makeb("required", TAG_INPUT, TAG_SELECT, TAG_TEXTAREA), makes("rel", "relList", TAG_A, TAG_LINK, TAG_LABEL), makes("for", "htmlFor", TAG_LABEL), - makeigz("cols", TAG_TEXTAREA, 20), - makeigz("rows", TAG_TEXTAREA, 1), + makeul("cols", TAG_TEXTAREA, 20u32), + makeul("rows", TAG_TEXTAREA, 1u32), # <SELECT>: #> For historical reasons, the default value of the size IDL attribute does #> not return the actual size used, which, in the absence of the size content #> attribute, is either 1 or 4 depending on the presence of the multiple #> attribute. - makeigz("size", TAG_SELECT, 0), - makeigz("size", TAG_INPUT, 20), + makeulgz("size", TAG_SELECT, 0u32), + makeulgz("size", TAG_INPUT, 20u32), + makeul("width", TAG_CANVAS, 300u32), + makeul("height", TAG_CANVAS, 150u32), # "super-global" attributes makes("slot", AllTagTypes), makes("class", "className", AllTagTypes) @@ -1033,30 +1398,18 @@ func documentElement(document: Document): Element {.jsfget.} = func attr*(element: Element, s: string): string {.inline.} = return element.attrs.getOrDefault(s, "") -func attri*(element: Element, s: string): Option[int] = - let a = element.attr(s) - try: - return some(parseInt(a)) - except ValueError: - return none(int) +func attrl*(element: Element, s: string): Option[int32] = + return parseInt32(element.attr(s)) -func attrigz*(element: Element, s: string): Option[int] = - let a = element.attr(s) - try: - let i = parseInt(a) - if i > 0: - return some(i) - except ValueError: - discard +func attrulgz*(element: Element, s: string): Option[uint32] = + let x = parseUInt32(element.attr(s)) + if x.isSome and x.get > 0: + return x -func attrigez*(element: Element, s: string): Option[int] = - let a = element.attr(s) - try: - let i = parseInt(a) - if i >= 0: - return some(i) - except ValueError: - discard +func attrul*(element: Element, s: string): Option[uint32] = + let x = parseUInt32(element.attr(s)) + if x.isSome and x.get >= 0: + return x func attrb*(element: Element, s: string): bool = if s in element.attrs: @@ -1108,9 +1461,9 @@ func inputString*(input: HTMLInputElement): string = if input.checked: "*" else: " " of INPUT_SEARCH, INPUT_TEXT: - input.value.padToWidth(input.attri("size").get(20)) + input.value.padToWidth(int(input.attrulgz("size").get(20))) of INPUT_PASSWORD: - '*'.repeat(input.value.len).padToWidth(input.attri("size").get(20)) + '*'.repeat(input.value.len).padToWidth(int(input.attrulgz("size").get(20))) of INPUT_RESET: if input.value != "": input.value else: "RESET" @@ -1119,16 +1472,16 @@ func inputString*(input: HTMLInputElement): string = else: "SUBMIT" of INPUT_FILE: if input.file.isnone: - "".padToWidth(input.attri("size").get(20)) + "".padToWidth(int(input.attrulgz("size").get(20))) else: - input.file.get.path.serialize_unicode().padToWidth(input.attri("size").get(20)) + input.file.get.path.serialize_unicode().padToWidth(int(input.attrulgz("size").get(20))) else: input.value func textAreaString*(textarea: HTMLTextAreaElement): string = let split = textarea.value.split('\n') - let rows = textarea.attri("rows").get(1) + let rows = int(textarea.attrul("rows").get(1)) for i in 0 ..< rows: - let cols = textarea.attri("cols").get(20) + let cols = int(textarea.attrul("cols").get(20)) if cols > 2: if i < split.len: result &= '[' & split[i].padToWidth(cols - 2) & "]\n" @@ -1208,6 +1561,14 @@ func formmethod*(element: Element): FormMethod = return FORM_METHOD_GET +proc parseColor(element: Element, s: string): RGBAColor = + try: + return cssColor(parseComponentValue(newStringStream(s))) + except CSSValueError: + #TODO TODO TODO return element style + # For now we just use white. + return rgb(255, 255, 255) + #TODO ?? func target0*(element: Element): string = if element.attrb("target"): @@ -1321,7 +1682,9 @@ func newComment(window: Window, data: string = ""): Comment {.jsgctor.} = return window.document.newComment(data) #TODO custom elements -func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace.HTML, prefix = none[string](), attrs = Table[string, string]()): HTMLElement = +func newHTMLElement*(document: Document, tagType: TagType, + namespace = Namespace.HTML, prefix = none[string](), + attrs = Table[string, string]()): HTMLElement = case tagType of TAG_INPUT: result = new(HTMLInputElement) @@ -1369,6 +1732,8 @@ func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace result = new(HTMLTextAreaElement) of TAG_LABEL: result = new(HTMLLabelElement) + of TAG_CANVAS: + result = new(HTMLCanvasElement) else: result = new(HTMLElement) result.nodeType = ELEMENT_NODE @@ -1382,10 +1747,19 @@ func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace {.cast(noSideEffect).}: for k, v in attrs: result.attr(k, v) - if tagType == TAG_SCRIPT: + case tagType + of TAG_SCRIPT: HTMLScriptElement(result).internalNonce = result.attr("nonce") + of TAG_CANVAS: + HTMLCanvasElement(result).bitmap = newBitmap( + width = result.attrul("width").get(300), + height = result.attrul("height").get(150) + ) + else: discard -func newHTMLElement*(document: Document, localName: string, namespace = Namespace.HTML, prefix = none[string](), tagType = tagType(localName), attrs = Table[string, string]()): Element = +func newHTMLElement*(document: Document, localName: string, + namespace = Namespace.HTML, prefix = none[string](), + tagType = tagType(localName), attrs = Table[string, string]()): Element = result = document.newHTMLElement(tagType, namespace, prefix, attrs) if tagType == TAG_UNKNOWN: result.localName = localName @@ -1547,16 +1921,15 @@ proc attr*(element: Element, name, value: string) = element.attributes.attrlist.add(element.newAttr(name, value)) element.attr0(name, value) -proc attri(element: Element, name: string, value: int) = +proc attrl(element: Element, name: string, value: int32) = element.attr(name, $value) -proc attrigz(element: Element, name: string, value: int) = - if value > 0: - element.attri(name, value) +proc attrul(element: Element, name: string, value: uint32) = + element.attr(name, $value) -proc attrigez(element: Element, name: string, value: int) = - if value >= 0: - element.attri(name, value) +proc attrulgz(element: Element, name: string, value: uint32) = + if value > 0: + element.attrul(name, value) proc setAttribute(element: Element, qualifiedName, value: string) {.jserr, jsfunc.} = if not qualifiedName.matchNameProduction(): @@ -1717,7 +2090,7 @@ proc resetElement*(element: Element) = of TAG_SELECT: let select = HTMLSelectElement(element) if not select.attrb("multiple"): - if select.attrigez("size").get(1) == 1: + if select.attrul("size").get(1) == 1: var i = 0 var firstOption: HTMLOptionElement for option in select.options: @@ -2277,12 +2650,12 @@ proc jsReflectGet(ctx: JSContext, this: JSValue, magic: cint): JSValue {.cdecl.} return x of REFLECT_BOOl: return toJS(ctx, element.attrb(entry.attrname)) - of REFLECT_INT: - return toJS(ctx, element.attri(entry.attrname).get(entry.i)) - of REFLECT_INT_GREATER_ZERO: - return toJS(ctx, element.attrigz(entry.attrname).get(entry.i)) - of REFLECT_INT_GREATER_EQUAL_ZERO: - return toJS(ctx, element.attrigez(entry.attrname).get(entry.i)) + of REFLECT_LONG: + return toJS(ctx, element.attrl(entry.attrname).get(entry.i)) + of REFLECT_ULONG: + return toJS(ctx, element.attrul(entry.attrname).get(entry.u)) + of REFLECT_ULONG_GZ: + return toJS(ctx, element.attrulgz(entry.attrname).get(entry.u)) proc jsReflectSet(ctx: JSContext, this, val: JSValue, magic: cint): JSValue {.cdecl.} = if unlikely(not ctx.isInstanceOf(this, "Element")): @@ -2305,18 +2678,18 @@ proc jsReflectSet(ctx: JSContext, this, val: JSValue, magic: cint): JSValue {.cd element.attr(entry.attrname, "") else: element.delAttr(entry.attrname) - of REFLECT_INT: - let x = fromJS[int](ctx, val) + of REFLECT_LONG: + let x = fromJS[int32](ctx, val) if x.isSome: - element.attri(entry.attrname, x.get) - of REFLECT_INT_GREATER_ZERO: - let x = fromJS[int](ctx, val) + element.attrl(entry.attrname, x.get) + of REFLECT_ULONG: + let x = fromJS[uint32](ctx, val) if x.isSome: - element.attrigz(entry.attrname, x.get) - of REFLECT_INT_GREATER_EQUAL_ZERO: - let x = fromJS[int](ctx, val) + element.attrul(entry.attrname, x.get) + of REFLECT_ULONG_GZ: + let x = fromJS[uint32](ctx, val) if x.isSome: - element.attrigez(entry.attrname, x.get) + element.attrulgz(entry.attrname, x.get) return JS_DupValue(ctx, val) proc addconsoleModule*(ctx: JSContext) = @@ -2337,12 +2710,35 @@ func getReflectFunctions(tags: set[TagType]): seq[TabGetSet] = func getElementReflectFunctions(): seq[TabGetSet] = var i: uint16 = ReflectAllStartIndex - while i < ReflectTable.len: + while i < uint16(ReflectTable.len): let entry = ReflectTable[i] assert entry.tags == AllTagTypes result.add(TabGetSet(name: ReflectTable[i].funcname, get: jsReflectGet, set: jsReflectSet, magic: i)) inc i +proc getContext*(this: HTMLCanvasElement, contextId: string, + options = none(JSObject)): RenderingContext {.jsfunc.} = + if contextId == "2d": + if this.ctx2d != nil: + return this.ctx2d + return create2DContext(this, options) + return nil + +#TODO quality should be `any' +proc toBlob(this: HTMLCanvasElement, callback: JSObject, + s = "image/png", quality: float64 = 1): JSValue {.jsfunc.} = + let ctx = callback.ctx + var outlen: int + let buf = this.bitmap.toPNG(outlen) + let blob = newBlob(buf, outlen, "image/png", dealloc) + var jsBlob = toJS(ctx, blob) + let res = JS_Call(ctx, callback.val, JS_UNDEFINED, 1, addr jsBlob) + # Hack. TODO: implement JSValue to callback + if res == JS_EXCEPTION: + return JS_EXCEPTION + JS_FreeValue(ctx, res) + return JS_UNDEFINED + proc registerElements(ctx: JSContext, nodeCID: JSClassID) = let elementCID = ctx.registerType(Element, parent = nodeCID) const extra_getset = getElementReflectFunctions() @@ -2377,6 +2773,7 @@ proc registerElements(ctx: JSContext, nodeCID: JSClassID) = register(HTMLButtonElement, TAG_BUTTON) register(HTMLTextAreaElement, TAG_TEXTAREA) register(HTMLLabelElement, TAG_LABEL) + register(HTMLCanvasElement, TAG_CANVAS) proc addDOMModule*(ctx: JSContext) = let eventTargetCID = ctx.registerType(EventTarget) @@ -2395,4 +2792,6 @@ proc addDOMModule*(ctx: JSContext) = ctx.registerType(DocumentType, parent = nodeCID) ctx.registerType(Attr, parent = nodeCID) ctx.registerType(NamedNodeMap) + ctx.registerType(CanvasRenderingContext2D) + ctx.registerType(TextMetrics) ctx.registerElements(nodeCID) diff --git a/src/html/env.nim b/src/html/env.nim index 544f5083..8b558b8a 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -6,9 +6,12 @@ import html/htmlparser import io/loader import io/promise import io/request +import js/intl import js/javascript import js/timeout +import types/blob import types/url +import xhr/formdata as formdata_impl # NavigatorID proc appCodeName(navigator: Navigator): string {.jsfget.} = "Mozilla" @@ -73,6 +76,16 @@ proc clearTimeout(window: Window, id: int32) {.jsfunc.} = proc clearInterval(window: Window, id: int32) {.jsfunc.} = window.timeouts.clearInterval(id) +proc screenX(window: Window): int64 {.jsfget.} = 0 +proc screenY(window: Window): int64 {.jsfget.} = 0 +proc screenLeft(window: Window): int64 {.jsfget.} = 0 +proc screenTop(window: Window): int64 {.jsfget.} = 0 +#TODO outerWidth, outerHeight +proc devicePixelRatio(window: Window): float64 {.jsfget.} = 1 + +func location(window: Window): URL {.jsfget.} = + window.document.location + proc addScripting*(window: Window, selector: Selector[int]) = let rt = newJSRuntime() let ctx = rt.newJSContext() @@ -102,6 +115,10 @@ proc addScripting*(window: Window, selector: Selector[int]) = ctx.addDOMModule() ctx.addURLModule() ctx.addHTMLModule() + ctx.addIntlModule() + ctx.addBlobModule() + ctx.addFormDataModule() + ctx.addRequestModule() proc runJSJobs*(window: Window) = window.jsrt.runJSJobs(window.console.err) diff --git a/src/img/bitmap.nim b/src/img/bitmap.nim new file mode 100644 index 00000000..03048f4d --- /dev/null +++ b/src/img/bitmap.nim @@ -0,0 +1,620 @@ +import algorithm +import math +import unicode + +import bindings/zlib +import css/values +import img/path +import types/color +import types/line +import types/vector + +import lib/endians2 + +type + CanvasFillRule* = enum + NON_ZERO = "nonzero" + EVEN_ODD = "evenodd" + + Bitmap* = ref object of RootObj + px: seq[RGBAColor] + width*: uint64 + height*: uint64 + + ImageBitmap* = ref object of Bitmap + +proc newBitmap*(width, height: uint64): Bitmap = + return ImageBitmap( + px: newSeq[RGBAColor](width * height), + width: width, + height: height + ) + +proc setpx(bmp: Bitmap, x, y: uint64, color: RGBAColor) {.inline.} = + bmp.px[bmp.width * y + x] = color + +proc getpx*(bmp: Bitmap, x, y: uint64): RGBAColor {.inline.} = + return bmp.px[bmp.width * y + x] + +proc setpxb(bmp: Bitmap, x, y: uint64, color: RGBAColor) {.inline.} = + if color.a == 255: + bmp.setpx(x, y, color) + else: + bmp.setpx(x, y, bmp.getpx(x, y).blend(color)) + +# https://en.wikipedia.org/wiki/Bresenham's_line_algorithm#All_cases +proc plotLineLow(bmp: Bitmap, x0, y0, x1, y1: int64, color: RGBAColor) = + var dx = x1 - x0 + var dy = y1 - y0 + var yi = 1 + if dy < 0: + yi = -1 + dy = -dy + var D = 2 * dy - dx; + var y = y0; + for x in x0 ..< x1: + if x < 0 or y < 0 or uint64(x) >= bmp.width or uint64(y) >= bmp.height: + break + bmp.setpxb(uint64(x), uint64(y), color) + if D > 0: + y = y + yi; + D = D - 2 * dx; + D = D + 2 * dy; + +proc plotLineHigh(bmp: Bitmap, x0, y0, x1, y1: int64, color: RGBAColor) = + var dx = x1 - x0 + var dy = y1 - y0 + var xi = 1 + if dx < 0: + xi = -1 + dx = -dx + var D = 2 * dx - dy + var x = x0 + for y in y0 ..< y1: + if x < 0 or y < 0 or uint64(x) >= bmp.width or uint64(y) >= bmp.height: + break + bmp.setpxb(uint64(x), uint64(y), color) + if D > 0: + x = x + xi + D = D - 2 * dy + D = D + 2 * dx + +#TODO should be uint64... +proc plotLine(bmp: Bitmap, x0, y0, x1, y1: int64, color: RGBAColor) = + if abs(y1 - y0) < abs(x1 - x0): + if x0 > x1: + bmp.plotLineLow(x1, y1, x0, y0, color) + else: + bmp.plotLineLow(x0, y0, x1, y1, color) + else: + if y0 > y1: + bmp.plotLineHigh(x1, y1, x0, y0, color) + else: + bmp.plotLineHigh(x0, y0, x1, y1, color) + +proc plotLine(bmp: Bitmap, a, b: Vector2D, color: RGBAColor) = + bmp.plotLine(int64(a.x), int64(a.y), int64(b.x), int64(b.y), color) + +proc plotLine(bmp: Bitmap, line: Line, color: RGBAColor) = + bmp.plotLine(line.p0, line.p1, color) + +proc strokePath*(bmp: Bitmap, path: Path, color: RGBAColor) = + for line in path.lines: + bmp.plotLine(line, color) + +func isInside(windingNumber: int, fillRule: CanvasFillRule): bool = + return case fillRule + of NON_ZERO: windingNumber != 0 + of EVEN_ODD: windingNumber mod 2 == 0 + +# Mainly adapted from SerenityOS. +proc fillPath*(bmp: Bitmap, path: Path, color: RGBAColor, + fillRule: CanvasFillRule) = + let lines = path.getLineSegments() + var i = 0 + var ylines: seq[LineSegment] + for y in int64(lines.miny) .. int64(lines.maxy): + for k in countdown(ylines.high, 0): + if ylines[k].maxy < float64(y): + ylines.del(k) # we'll sort anyways, so del is fine + for j in i ..< lines.len: + if lines[j].miny > float64(y): + break + if lines[j].maxy > float64(y): + ylines.add(lines[j]) + inc i + ylines.sort(cmpLineSegmentX) + var w = if fillRule == NON_ZERO: 1 else: 0 + for k in 0 ..< ylines.high: + let a = ylines[k] + let b = ylines[k + 1] + let sx = int64(a.minyx) + let ex = int64(b.minyx) + if isInside(w, fillRule) and y > 0: + for x in sx .. ex: + if x > 0: + bmp.setpxb(uint64(x), uint64(y), color) + if int64(a.p0.y) != y and int64(a.p1.y) != y and int64(b.p0.y) != y and + int64(b.p1.y) != y and sx != ex or a.islope * b.islope < 0: + case fillRule + of EVEN_ODD: inc w + of NON_ZERO: + if a.p0.y < a.p1.y: + inc w + else: + dec w + ylines[k].minyx += ylines[k].islope + if ylines.len > 0: + ylines[^1].minyx += ylines[^1].islope + +proc fillRect*(bmp: Bitmap, x0, x1, y0, y1: uint64, color: RGBAColor) = + for y in y0 ..< y1: + for x in x0 ..< x1: + bmp.setpxb(x, y, color) + +proc strokeRect*(bmp: Bitmap, x0, x1, y0, y1: uint64, color: RGBAColor) = + for x in x0 ..< x1: + bmp.setpxb(x, y0, color) + bmp.setpxb(x, y1, color) + for y in y0 ..< y1: + bmp.setpxb(x0, y, color) + bmp.setpxb(x1, y, color) + +proc clearRect*(bmp: Bitmap, x0, x1, y0, y1: uint64) = + for y in y0 ..< y1: + for x in x0 ..< x1: + bmp.setpx(x, y, rgba(0, 0, 0, 0)) + +proc clear*(bmp: Bitmap) = + bmp.clearRect(0, bmp.width, 0, bmp.height) + +#TODO clean up templates, also move png encoder to a different file +type PNGWriter = object + buf: pointer + i: int + outlen: int + +func pngInt(i: uint32): auto = + doAssert i < uint32(2 ^ 31) + return i.toBytesBE() + +func oq(writer: PNGWriter): ptr UncheckedArray[uint8] = + cast[ptr UncheckedArray[uint8]](writer.buf) + +proc writeStr[T](writer: var PNGWriter, s: T) = + if writer.outlen < writer.i + s.len: + writer.outlen = writer.i + s.len + writer.buf = realloc(writer.buf, writer.outlen) + copyMem(addr writer.oq[writer.i], unsafeAddr s[0], s.len) + writer.i += s.len + +proc writeInt(writer: var PNGWriter, i: uint32) = + writer.writeStr(i.toBytesBE()) + +proc writePngInt(writer: var PNGWriter, i: uint32) = + doAssert i < uint32(2 ^ 31) + writer.writeInt(i) + +proc writeChunk[T](writer: var PNGWriter, t: string, data: T) = + var crc = uint32(crc32(0, cast[ptr uint8](unsafeAddr t[0]), cuint(t.len))) + if data.len > 0: + crc = uint32(crc32(crc, cast[ptr uint8](unsafeAddr data[0]), + cuint(data.len))) + writer.writePngInt(uint32(data.len)) + writer.writeStr(t) + if data.len > 0: + writer.writeStr(data) + writer.writeInt(uint32(crc)) + +type PNGColorType {.size: sizeof(uint8).} = enum + GRAYSCALE = 0 + TRUECOLOR = 2 + INDEXED_COLOR = 3 + GRAYSCALE_WITH_ALPHA = 4 + TRUECOLOR_WITH_ALPHA = 6 + +func u8toc(x: openArray[uint8]): string = + #TODO ew + var s = newString(x.len) + copyMem(addr s[0], unsafeAddr x[0], x.len) + return s + +const PNGSignature = "\x89PNG\r\n\x1A\n" +proc writeIHDR(writer: var PNGWriter, width, height: uint32, + bitDepth: uint8, colorType: PNGColorType, + compressionMethod, filterMethod, interlaceMethod: uint8) = + writer.writeStr(PNGSignature) + let ihdr = u8toc(pngInt(width)) & + u8toc(pngInt(height)) & + char(bitDepth) & + char(uint8(colorType)) & + char(compressionMethod) & + char(filterMethod) & + char(interlaceMethod) + writer.writeChunk("IHDR", ihdr) + +proc writeIDAT(writer: var PNGWriter, bmp: Bitmap) = + #TODO smaller idat chunks + # +1 height for filter + var idat = newSeq[uint8]((bmp.width + 1) * bmp.height * 4) + var j = 0 # idat pointer + for k in 0 ..< bmp.px.len: + if k mod int(bmp.width) == 0: + # begin row + # For now, filter is always 0. TODO implement other filters + inc j + let p = bmp.px[k] + idat[j] = uint8(p.r) + idat[j + 1] = uint8(p.g) + idat[j + 2] = uint8(p.b) + idat[j + 3] = uint8(p.a) + j += 4 + var hlen = compressBound(culong(idat.len)) + var oidat = newSeq[uint8](int(hlen)) + let res = compress(addr oidat[0], addr hlen, addr idat[0], culong(idat.len)) + doAssert res == Z_OK #TODO error handling... + oidat.setLen(int(hlen)) + writer.writeChunk("IDAT", oidat) + +proc toPNG*(bmp: Bitmap, outlen: var int): pointer = + var writer = PNGWriter( + buf: alloc(PNGSignature.len), + outlen: PNGSignature.len + ) + writer.writeIHDR(uint32(bmp.width), uint32(bmp.height), 8, + TRUECOLOR_WITH_ALPHA, 0, 0, 0) + writer.writeIDAT(bmp) + writer.writeChunk("IEND", "") + outlen = writer.outlen + return writer.buf + +type PNGReader = object + bmp: Bitmap + iq: ptr UncheckedArray[uint8] + limit: int + i: int + bitDepth: uint8 + colorType: PNGColorType + background: RGBColor + isend: bool + idatBuf: seq[uint8] + uprow: seq[uint8] + idatAt: int + hasstrm: bool + strm: z_stream + strmend: bool + atline: int + +func width(reader: PNGReader): int {.inline.} = int(reader.bmp.width) + +func height(reader: PNGReader): int {.inline.} = int(reader.bmp.height) + +func spp(reader: PNGReader): int = + case reader.colorType + of TRUECOLOR: return 3 + of GRAYSCALE: return 1 + of INDEXED_COLOR: return 1 + of GRAYSCALE_WITH_ALPHA: return 2 + of TRUECOLOR_WITH_ALPHA: return 4 + +func scanlen(reader: PNGReader): int {.inline.} = + let w = reader.width + 1 + return (w * reader.spp * int(reader.bitDepth) + 7) div 8 + +proc handleError(reader: var PNGReader, msg: string) = + reader.bmp = nil + if reader.hasstrm: + discard inflateEnd(addr reader.strm) + +template err(reader: var PNGReader, msg: string) = + reader.handleError(msg) + return + +template readStr(reader: var PNGReader, L: int): string = + if reader.i + L > reader.limit: + reader.err "too short" + var s = newString(L) + copyMem(addr s[0], addr reader.iq[reader.i], L) + reader.i += L + s + +template readU8(reader: var PNGReader): uint8 = + if reader.i > reader.limit: + reader.err "too short" + let x = reader.iq[reader.i] + inc reader.i + x + +template readU32(reader: var PNGReader): uint32 = + if reader.i + 4 > reader.limit: + reader.err "too short" + let x = fromBytesBE(uint32, toOpenArray(reader.iq, reader.i, reader.i + 3)) + reader.i += 4 + x + +template readPNGInt(reader: var PNGReader): uint32 = + let x = reader.readU32() + if x >= uint32(2 ^ 31): + reader.err "int too large" + x + +template readColorType(reader: var PNGReader): PNGColorType = + case reader.readU8() + of 0u8: GRAYSCALE + of 2u8: TRUECOLOR + of 3u8: INDEXED_COLOR + of 4u8: GRAYSCALE_WITH_ALPHA + of 6u8: TRUECOLOR_WITH_ALPHA + else: reader.err "unknown color type" + +func bitDepthValid(colorType: PNGColorType, bitDepth: uint8): bool = + case colorType + of GRAYSCALE: + return int(bitDepth) in [1, 2, 4, 8, 16] + of INDEXED_COLOR: + return int(bitDepth) in [1, 2, 4, 8] + of TRUECOLOR, GRAYSCALE_WITH_ALPHA, TRUECOLOR_WITH_ALPHA: + return int(bitDepth) in [8, 16] + +proc readIHDR(reader: var PNGReader) = + if reader.readStr(PNGSignature.len) != PNGSignature: + reader.err "wrong signature" + if reader.readPNGInt() != 13: + reader.err "invalid header length" + if reader.readStr(4) != "IHDR": + reader.err "invalid header chunk" + let width = reader.readPNGInt() + let height = reader.readPNGInt() + reader.bitDepth = reader.readU8() #TODO check? + reader.colorType = reader.readColorType() + if not bitDepthValid(reader.colorType, reader.bitDepth): + reader.err "invalid bit depth" + let compressionMethod = reader.readU8() + if compressionMethod != 0: + reader.err "unknown compression method" + let filterMethod = reader.readU8() + if filterMethod != 0: + reader.err "unknown filter method" + let interlaceMethod = reader.readU8() + if interlaceMethod != 0: + reader.err "unknown interlace method" + let crc = crc32(0, addr reader.iq[reader.i - 17], 17) + if uint32(crc) != reader.readU32(): reader.err "wrong crc" + reader.bmp = newBitmap(width, height) + +proc readbKGD(reader: var PNGReader) = + case reader.colorType + of GRAYSCALE, GRAYSCALE_WITH_ALPHA: + discard reader.readU8() #TODO bit depth > 8 + reader.background = gray(reader.readU8()) + of TRUECOLOR, TRUECOLOR_WITH_ALPHA: + discard reader.readU8() #TODO bit depth > 8 + let r = reader.readU8() + discard reader.readU8() + let g = reader.readU8() + discard reader.readU8() + let b = reader.readU8() + reader.background = rgb(r, g, b) + of INDEXED_COLOR: + discard #TODO + +proc unfilter(reader: var PNGReader, irow: openArray[uint8], bpp: int) = + # none, sub, up -> replace uprow directly + # average, paeth -> copy to temp array, then replace uprow + let fil = irow[0] + let w = reader.width + case fil + of 0u8: # none + copyMem(addr reader.uprow[0], unsafeAddr irow[1], w) + of 1u8: # sub + for i in 1 ..< irow.len: + let j = i - 1 # skip filter byte + reader.uprow[j] = irow[i] + if j - bpp >= 0: + reader.uprow[j] += irow[j - bpp] + of 2u8: # up + for i in 1 ..< irow.len: + let j = i - 1 # skip filter byte + reader.uprow[j] += irow[i] + of 3u8: # average + reader.err "average not implemented yet" + of 4u8: # paeth + reader.err "paeth not implemented yet" + else: + eprint fil + reader.err "got invalid filter" + +proc writepxs(reader: var PNGReader, crow: var openArray[RGBAColor]) = + case reader.colorType + of GRAYSCALE: + var i = 0 + var j = 0 + for x in 0 ..< crow.len: + let u = reader.uprow[i] + let n = case reader.bitDepth + of 1: ((u shr (7 - j)) and 1) * 255 + of 2: ((u shr (6 - j)) and 3) * 85 + of 4: ((u shr (6 - j)) and 15) * 17 + of 8: u + of 16: u div 2 + else: 0 + j += int(reader.bitDepth) + i += j div 8 + j = j mod 8 + let nn = int(n) + crow[x] = rgba(nn, nn, nn, 255) + else: discard + +proc readIDAT(reader: var PNGReader) = + if reader.idatAt == reader.idatBuf.len: + reader.err "idat buffer already filled" + if reader.strmend: + reader.err "stream already ended" + reader.strm.avail_in = cuint(reader.limit - reader.i) + reader.strm.next_in = addr reader.iq[reader.i] + let olen = reader.idatBuf.len - reader.idatAt + reader.strm.avail_out = cuint(olen) + reader.strm.next_out = addr reader.idatBuf[reader.idatAt] + let res = inflate(addr reader.strm, Z_NO_FLUSH) + doAssert res != Z_STREAM_ERROR + case res + of Z_NEED_DICT, Z_DATA_ERROR, Z_MEM_ERROR, Z_BUF_ERROR: + # Z_BUF_ERROR is fatal here, as outlen is at least as large as idat. + reader.err "error decompressing idat stream" + of Z_STREAM_END: + reader.strmend = true + of Z_OK: + if reader.strm.avail_out == 0: + reader.err "not enough space for output; is width or height wrong?" + else: doAssert false + reader.idatAt = int(reader.strm.total_out) + reader.i = reader.limit + let maxline = reader.idatAt div int(reader.scanlen) + let bmp = reader.bmp + let bps = if reader.bitDepth <= 8: 1 else: 2 # else 16 bit + let bpp = bps * reader.spp + let sl = int(reader.scanlen) + for y in reader.atline ..< maxline: + let yi = y * sl + assert yi + sl - 1 < reader.idatAt + reader.unfilter(toOpenArray(reader.idatBuf, yi, yi + sl - 1), bpp) + if unlikely(reader.bmp == nil): return + let yj = y * reader.width + reader.writepxs(toOpenArray(bmp.px, yj, yj + reader.width - 1)) + +proc readIEND(reader: var PNGReader) = + if reader.i < reader.limit: + reader.err "IEND too long" + reader.isend = true + +proc readUnknown(reader: var PNGReader, s: string) = + if (int(s[0]) and 0x20) == 0: + reader.err "unrecognized critical chunk " & s + #else: eprint "warning: unknown chunk " & s #debug + reader.i = reader.limit + +proc zlibAlloc(opaque: pointer, items: cuint, size: cuint): pointer {.cdecl.} = + return alloc(items * size) + +proc zlibFree(opaque: pointer, address: pointer) {.cdecl.} = + dealloc(address) + +proc initZStream(reader: var PNGReader) = + let bps = max(int(reader.bitDepth) div 8, 1) + reader.idatBuf = newSeq[uint8](reader.scanlen * reader.height * bps) + reader.uprow = newSeq[uint8](reader.width * bps) + reader.strm = z_stream( + zalloc: zlibAlloc, + zfree: zlibFree + ) + let ret = inflateInit(addr reader.strm) + if ret != Z_OK: + reader.err "failed to init inflate: " & $ret + reader.hasstrm = true + +proc fromPNG*(iq: openArray[uint8]): Bitmap = + if iq.len == 0: return + var reader = PNGReader( + iq: cast[ptr UncheckedArray[uint8]](unsafeAddr iq[0]), + limit: iq.len + ) + reader.readIHDR() + if reader.bmp == nil: return + if reader.width == 0 or reader.height == 0: + reader.err "invalid zero sized png" + if reader.colorType != GRAYSCALE: + reader.err "only grayscale is implemented" + reader.initZStream() + while reader.i < iq.len and not reader.isend: + let len = int(reader.readPNGInt()) + if reader.i + len > iq.len: + reader.err "chunk too long" + let j = reader.i + let t = reader.readStr(4) + reader.limit = reader.i + len + case t + of "IHDR": reader.err "IHDR expected to be first chunk" + of "IDAT": reader.readIDAT() + of "IEND": reader.readIEND() + of "bKGD": reader.readbKGD() + else: reader.readUnknown(t) + if reader.bmp == nil: return + let crc = crc32(0, unsafeAddr iq[j], cuint(len + 4)) + reader.limit = iq.len + let y = reader.readU32() + if uint32(crc) != y: + reader.err "wrong crc" + if not reader.isend: + reader.err "IEND not found" + return reader.bmp + +const unifont = readFile"res/unifont_jp-15.0.05.png" +var unifontBitmap: Bitmap +var glyphCache: seq[tuple[u: uint32, bmp: Bitmap]] +var glyphCacheI = 0 +proc getCharBmp(u: uint32): Bitmap = + # We only have the BMP. + let u = if u <= 0xFFFF: u else: 0xFFFD + if unifontBitmap == nil: + unifontBitmap = fromPNG(toOpenArrayByte(unifont, 0, unifont.high)) + for (cu, bmp) in glyphCache: + if cu == u: + return bmp + # Unifont glyphs start at x: 32, y: 64, and are of 8x16/16x16 size + let gx = uint64(32 + 16 * (u mod 0xFF)) + let gy = uint64(64 + 16 * (u div 0xFF)) + var fullwidth = false + const white = rgba(255, 255, 255, 255) + block loop: + # hack to recognize full width characters + for y in 0 ..< 16u64: + for x in 8 ..< 16u64: + if unifontBitmap.getpx(gx + x, gy + y) != white: + fullwidth = true + break loop + let bmp = newBitmap(if fullwidth: 16 else: 8, 16) + for y in 0 ..< bmp.height: + for x in 0 ..< bmp.width: + let c = unifontBitmap.getpx(gx + x, gy + y) + if c != white: + bmp.setpx(x, y, c) + if glyphCache.len < 256: + glyphCache.add((u, bmp)) + else: + glyphCache[glyphCacheI] = (u, bmp) + inc glyphCacheI + if glyphCacheI >= glyphCache.len: + glyphCacheI = 0 + return bmp + +proc drawBitmap(a, b: Bitmap, p: Vector2D) = + for y in 0 ..< b.height: + for x in 0 ..< b.width: + let ax = uint64(p.x) + x + let ay = uint64(p.y) + y + if ax >= 0 and ay >= y and ax < a.width and ay < a.height: + a.setpxb(ax, ay, b.getpx(x, y)) + +proc fillText*(bmp: Bitmap, text: string, x, y: float64, color: RGBAColor, + textAlign: CSSTextAlign) = + var w = 0f64 + var glyphs: seq[Bitmap] + for r in text.runes: + let glyph = getCharBmp(uint32(r)) + glyphs.add(glyph) + w += float64(glyph.width) + var x = x + #TODO rtl + case textAlign + of TEXT_ALIGN_LEFT, TEXT_ALIGN_START: discard + of TEXT_ALIGN_RIGHT, TEXT_ALIGN_END: x -= w + of TEXT_ALIGN_CENTER: x -= w / 2 + else: doAssert false + for glyph in glyphs: + bmp.drawBitmap(glyph, Vector2D(x: x, y: y - 8)) + x += float64(glyph.width) + +proc strokeText*(bmp: Bitmap, text: string, x, y: float64, color: RGBAColor, + textAlign: CSSTextAlign) = + #TODO + bmp.fillText(text, x, y, color, textAlign) diff --git a/src/img/path.nim b/src/img/path.nim new file mode 100644 index 00000000..4c112f28 --- /dev/null +++ b/src/img/path.nim @@ -0,0 +1,430 @@ +import algorithm +import deques +import math + +import types/line +import types/vector + +type + Path* = ref object + subpaths: seq[Subpath] + needsNewSubpath: bool + tempClosed: bool + + PathLines* = object + lines*: seq[LineSegment] + miny*: float64 + maxy*: float64 + + PathSegmentType = enum + SEGMENT_STRAIGHT, SEGMENT_QUADRATIC, SEGMENT_BEZIER, SEGMENT_ARC, + SEGMENT_ELLIPSE + + PathSegment = object + case t: PathSegmentType + of SEGMENT_QUADRATIC: + cp: Vector2D + of SEGMENT_BEZIER: + cp0: Vector2D + cp1: Vector2D + of SEGMENT_ARC: + oa: Vector2D + r: float64 + ia: bool + of SEGMENT_ELLIPSE: + oe: Vector2D + rx: float64 + ry: float64 + else: discard + + Subpath* = object + points: seq[Vector2D] + segments: seq[PathSegment] + closed: bool + +proc newPath*(): Path = + return Path( + needsNewSubpath: true + ) + +proc addSubpathAt(path: Path, p: Vector2D) = + path.subpaths.add(Subpath(points: @[p])) + +proc addSegment(path: Path, segment: PathSegment, p: Vector2D) = + path.subpaths[^1].segments.add(segment) + path.subpaths[^1].points.add(p) + +proc addStraightSegment(path: Path, p: Vector2D) = + let segment = PathSegment(t: SEGMENT_STRAIGHT) + path.addSegment(segment, p) + +proc addQuadraticSegment(path: Path, cp, p: Vector2D) = + let segment = PathSegment( + t: SEGMENT_QUADRATIC, + cp: cp + ) + path.addSegment(segment, p) + +proc addBezierSegment(path: Path, cp0, cp1, p: Vector2D) = + let segment = PathSegment( + t: SEGMENT_BEZIER, + cp0: cp0, + cp1: cp1 + ) + path.addSegment(segment, p) + +# Goes from start tangent point to end tangent point +proc addArcSegment(path: Path, o, etan: Vector2D, r: float64, ia: bool) = + let segment = PathSegment( + t: SEGMENT_ARC, + oa: o, + r: r, + ia: ia + ) + path.addSegment(segment, etan) + +proc addEllipseSegment(path: Path, o, etan: Vector2D, rx, ry: float64) = + #TODO simplify to bezier? + let segment = PathSegment( + t: SEGMENT_ELLIPSE, + oe: o, + rx: rx, + ry: ry + ) + path.addSegment(segment, etan) + +# https://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf +func flatEnough(a, b, c: Vector2D): bool = + let ux = 3 * c.x - 2 * a.x - b.x + let uy = 3 * c.y - 2 * a.y - b.y + let vx = 3 * c.x - 2 * b.x - b.x + let vy = 3 * c.y - 2 * b.y - b.y + return max(ux * ux, vx * vx) + max(uy * uy, vy * vy) <= 0.02 + +func flatEnough(a, b, c0, c1: Vector2D): bool = + let ux = 3 * c0.x - 2 * a.x - b.x + let uy = 3 * c0.y - 2 * a.y - b.y + let vx = 3 * c1.x - a.x - 2 * b.x + let vy = 3 * c1.y - a.y - 2 * b.y + return max(ux * ux, vx * vx) + max(uy * uy, vy * vy) <= 0.02 + +iterator items*(pl: PathLines): LineSegment {.inline.} = + for line in pl.lines: + yield line + +func `[]`*(pl: PathLines, i: int): LineSegment = pl.lines[i] +func `[]`*(pl: PathLines, i: BackwardsIndex): LineSegment = pl.lines[i] +func `[]`*(pl: PathLines, s: Slice[int]): seq[LineSegment] = pl.lines[s] +func len*(pl: PathLines): int = pl.lines.len + +iterator quadraticLines(a, b, c: Vector2D): Line {.inline.} = + var points: Deque[tuple[a, b, c: Vector2D]] + let tup = (a, b, c) + points.addFirst(tup) + while points.len > 2: + let (a, b, c) = points.popFirst() + if flatEnough(a, b, c): + yield Line(p0: a, p1: b) + else: + let mid1 = (c + a) / 2 + let mid2 = (c + b) / 2 + let s = (mid1 + mid2) / 2 + points.addFirst((a, s, mid1)) + points.addFirst((s, b, mid2)) + +iterator bezierLines(p0, p1, c0, c1: Vector2D): Line {.inline.} = + var points: Deque[tuple[p0, p1, c0, c1: Vector2D]] + let tup = (p0, p1, c0, c1) + points.addLast(tup) + while points.len > 0: + let (p0, p1, c0, c1) = points.popFirst() + if flatEnough(p0, p1, c0, c1): + yield Line(p0: p0, p1: p1) + else: + let mida1 = (p0 + c0) / 2 + let mida2 = (c0 + c1) / 2 + let mida3 = (c1 + p1) / 2 + let midb1 = (mida1 + mida2) / 2 + let midb2 = (mida2 + mida3) / 2 + let midc = (midb1 + midb2) / 2 + points.addLast((p0, midc, mida1, midb1)) + points.addLast((midc, p1, midb2, mida3)) + +# https://stackoverflow.com/a/44829356 +func arcControlPoints(p1, p4, o: Vector2D): tuple[c0, c1: Vector2D] = + let a = p1 - o + let b = p4 - o + let q1 = a.x * a.x + a.y * a.y + let q2 = q1 + a.x * b.x + a.y * b.y + let k2 = (4 / 3) * (sqrt(2 * q1 * q2) - q2) / a.cross(b) + let c0 = o + a + Vector2D(x: -k2 * a.y, y: k2 * a.x) + let c1 = o + b + Vector2D(x: k2 * b.y, y: -k2 * b.x) + return (c0, c1) + +iterator arcLines(p0, p1, o: Vector2D, r: float64, i: bool): Line {.inline.} = + var p0 = p0 + let pp0 = p0 - o + let pp1 = p1 - o + var theta = pp0.innerAngle(pp1) + if not i: + theta = PI * 2 - theta + while theta > 0: + let step = if theta > PI / 2: PI / 2 else: theta + var p1 = p0 - o + p1 = p1.rotate(step) + p1 += o + let (c0, c1) = arcControlPoints(p0, p1, o) + for line in bezierLines(p0, p1, c0, c1): + yield line + p0 = p1 + theta -= step + +# From SerenityOS +iterator ellipseLines(p0, p1, o: Vector2D, rx, ry, theta_1, rotx, + theta_delta: float64): Line {.inline.} = + if rx > 0 and ry > 0: + var s = p0 + var e = p1 + var theta_1 = theta_1 + var theta_delta = theta_delta + if theta_delta < 0: + swap(s, e) + theta_1 += theta_delta + theta_delta = abs(theta_delta) + # The segments are at most 1 long + let step = arctan2(1f64, max(rx, ry)) + var current_point = s - o + var next_point = Vector2D() + var theta = theta_1 + while theta <= theta_1 + theta_delta: + next_point.x = rx * cos(theta) + next_point.y = ry * sin(theta) + next_point = next_point.rotate(rotx) + yield Line(p0: current_point + o, p1: next_point + o) + current_point = next_point + theta += step + yield Line(p0: current_point + o, p1: e) + +iterator lines(subpath: Subpath, i: int): Line {.inline.} = + let p0 = subpath.points[i] + let p1 = subpath.points[i + 1] + case subpath.segments[i].t + of SEGMENT_STRAIGHT: + yield Line(p0: p0, p1: p1) + of SEGMENT_QUADRATIC: + let c = subpath.segments[i].cp + for line in quadraticLines(p0, p1, c): + yield line + of SEGMENT_BEZIER: + let c0 = subpath.segments[i].cp0 + let c1 = subpath.segments[i].cp1 + for line in bezierLines(p0, p1, c0, c1): + yield line + of SEGMENT_ARC: + let o = subpath.segments[i].oa + let r = subpath.segments[i].r + let i = subpath.segments[i].ia + for line in arcLines(p0, p1, o, r, i): + yield line + of SEGMENT_ELLIPSE: + discard #TODO + +iterator lines*(path: Path): Line {.inline.} = + for subpath in path.subpaths: + assert subpath.points.len == subpath.segments.len + 1 + for i in 0 ..< subpath.segments.len: + for line in subpath.lines(i): + if line.p0 == line.p1: + continue + yield line + +proc getLineSegments*(path: Path): PathLines = + if path.subpaths.len == 0: + return + var miny = Inf + var maxy = -Inf + var segments: seq[LineSegment] + for line in path.lines: + let ls = LineSegment(line) + miny = min(miny, ls.miny) + maxy = max(maxy, ls.maxy) + segments.add(ls) + segments.sort(cmpLineSegmentY) + return PathLines( + miny: miny, + maxy: maxy, + lines: segments + ) + +proc moveTo(path: Path, v: Vector2D) = + path.addSubpathAt(v) + path.needsNewSubpath = false #TODO TODO TODO ???? why here + +proc beginPath*(path: Path) = + path.subpaths.setLen(0) + +proc moveTo*(path: Path, x, y: float64) = + for v in [x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.moveTo(Vector2D(x: x, y: y)) + +proc ensureSubpath(path: Path, x, y: float64) = + if path.needsNewSubpath: + path.moveTo(x, y) + path.needsNewSubpath = false + +proc closePath*(path: Path) = + let lsp = path.subpaths[^1] + if path.subpaths.len > 0 and (lsp.points.len > 0 or lsp.closed): + path.subpaths[^1].closed = true + path.addSubpathAt(path.subpaths[^1].points[0]) + +#TODO this is a hack, and breaks as soon as any draw command is issued +# between tempClosePath and tempOpenPath +proc tempClosePath*(path: Path) = + if path.subpaths.len > 0 and not path.subpaths[^1].closed: + path.subpaths[^1].closed = true + let lsp = path.subpaths[^1] + path.addSubpathAt(lsp.points[^1]) + path.addStraightSegment(lsp.points[0]) + path.tempClosed = true + +proc tempOpenPath*(path: Path) = + if path.tempClosed: + path.subpaths.setLen(path.subpaths.len - 1) + path.subpaths[^1].closed = false + path.tempClosed = false + +proc lineTo*(path: Path, x, y: float64) = + for v in [x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + if path.subpaths.len == 0: + path.ensureSubpath(x, y) + else: + path.addStraightSegment(Vector2D(x: x, y: y)) + +proc quadraticCurveTo*(path: Path, cpx, cpy, x, y: float64) = + for v in [cpx, cpy, x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.ensureSubpath(cpx, cpy) + let cp = Vector2D(x: cpx, y: cpy) + let p = Vector2D(x: x, y: y) + path.addQuadraticSegment(cp, p) + +proc bezierCurveTo*(path: Path, cp0x, cp0y, cp1x, cp1y, x, y: float64) = + for v in [cp0x, cp0y, cp1x, cp1y, x, y]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.ensureSubpath(cp0x, cp0y) + let cp0 = Vector2D(x: cp0x, y: cp0y) + let cp1 = Vector2D(x: cp1x, y: cp1y) + let p = Vector2D(x: x, y: y) + path.addBezierSegment(cp0, cp1, p) + +proc arcTo*(path: Path, x1, y1, x2, y2, radius: float64): bool = + for v in [x1, y1, x2, y2, radius]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + if radius < 0: + return false + path.ensureSubpath(x1, y1) + #TODO this should be transformed by the inverse of the transformation matrix + let v0 = path.subpaths[^1].points[^1] + let v1 = Vector2D(x: x1, y: y1) + let v2 = Vector2D(x: x2, y: y2) + if v0.x == x1 and v0.y == y1 or x1 == x2 and y1 == y2 or radius == 0: + path.addStraightSegment(v1) + elif collinear(v0, v1, v2): + path.addStraightSegment(v1) + else: + let pv0 = v0 - v1 + let pv2 = v2 - v1 + let tv0 = v1 + pv0 * radius * 2 / pv0.norm() + let tv2 = v1 + pv2 * radius * 2 / pv2.norm() + let q = -(pv0.x * tv0.x + pv0.y * tv0.y) + let p = -(pv2.x * tv2.x + pv2.y * tv2.y) + let cr = pv0.cross(pv2) + let origin = Vector2D( + x: (pv0.y * p - pv2.y * q) / cr, + y: (pv2.x * q - pv0.x * p) / cr + ) + path.addStraightSegment(tv0) + path.addArcSegment(origin, tv2, radius, true) #TODO always inner? + return true + +func resolveEllipsePoint(o: Vector2D, angle, radiusX, radiusY, + rotation: float64): Vector2D = + # Stolen from SerenityOS + let tanrel = tan(angle) + let tan2 = tanrel * tanrel + let ab = radiusX * radiusY + let a2 = radiusX * radiusX + let b2 = radiusY * radiusY + let sq = sqrt(b2 + a2 * tan2) + let sn = if cos(angle) >= 0: 1f64 else: -1f64 + let relx = ab / sq * sn + let rely = ab * tanrel / sq * sn + return Vector2D(x: relx, y: rely).rotate(rotation) + o + +proc arc*(path: Path, x, y, radius, startAngle, endAngle: float64, + counterclockwise: bool): bool = + for v in [x, y, radius, startAngle, endAngle]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + if radius < 0: + return false + let o = Vector2D(x: x, y: y) + var s = resolveEllipsePoint(o, startAngle, radius, radius, 0) + var e = resolveEllipsePoint(o, endAngle, radius, radius, 0) + if counterclockwise: + let tmp = s + e = s + s = tmp + if path.subpaths.len > 0: + path.addStraightSegment(s) + else: + path.moveTo(s) + path.addArcSegment(o, e, radius, abs(startAngle - endAngle) < PI) + return true + +proc ellipse*(path: Path, x, y, radiusX, radiusY, rotation, startAngle, + endAngle: float64, counterclockwise: bool): bool = + for v in [x, y, radiusX, radiusY, rotation, startAngle, endAngle]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + if radiusX < 0 or radiusY < 0: + return false + let o = Vector2D(x: x, y: y) + var s = resolveEllipsePoint(o, startAngle, radiusX, radiusY, rotation) + var e = resolveEllipsePoint(o, endAngle, radiusX, radiusY, rotation) + if counterclockwise: + let tmp = s + e = s + s = tmp + if path.subpaths.len > 0: + path.addStraightSegment(s) + else: + path.moveTo(s) + path.addEllipseSegment(o, e, radiusX, radiusY) + return true + +proc rect*(path: Path, x, y, w, h: float64) = + for v in [x, y, w, h]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + path.addSubpathAt(Vector2D(x: x, y: y)) + path.addStraightSegment(Vector2D(x: x + w, y: y)) + path.addStraightSegment(Vector2D(x: x + w, y: y + h)) + path.addStraightSegment(Vector2D(x: x, y: y + h)) + path.addStraightSegment(Vector2D(x: x, y: y)) + path.addSubpathAt(Vector2D(x: x, y: y)) + +proc roundRect*(path: Path, x, y, w, h, radii: float64) = + for v in [x, y, w, h]: + if classify(v) in {fcInf, fcNegInf, fcNan}: + return + #TODO implement + path.rect(x, y, w, h) # :P diff --git a/src/io/http.nim b/src/io/http.nim index 984cae7c..f9023f5f 100644 --- a/src/io/http.nim +++ b/src/io/http.nim @@ -5,6 +5,8 @@ import strutils import bindings/curl import io/request import ips/serialize +import types/blob +import types/formdata import types/url import utils/twtstr @@ -89,7 +91,7 @@ proc applyPostBody(curl: CURL, request: Request, handleData: HandleData) = handleData.ostream.swrite(-1) handleData.ostream.flush() return - for entry in request.multipart.get.content: + for entry in request.multipart.get: let part = curl_mime_addpart(handleData.mime) if part == nil: # fail (TODO: raise?) @@ -97,16 +99,16 @@ proc applyPostBody(curl: CURL, request: Request, handleData: HandleData) = handleData.ostream.flush() return curl_mime_name(part, cstring(entry.name)) - if entry.isFile: - if entry.isStream: - curl_mime_filedata(part, cstring(entry.filename)) + if entry.isstr: + curl_mime_data(part, cstring(entry.svalue), csize_t(entry.svalue.len)) + else: + let blob = entry.value + if blob.isfile: #TODO ? + curl_mime_filedata(part, cstring(WebFile(blob).path)) else: - let fd = readFile(entry.filename) - curl_mime_data(part, cstring(fd), csize_t(fd.len)) + curl_mime_data(part, blob.buffer, csize_t(blob.size)) # may be overridden by curl_mime_filedata, so set it here curl_mime_filename(part, cstring(entry.filename)) - else: - curl_mime_data(part, cstring(entry.content), csize_t(entry.content.len)) curl.setopt(CURLOPT_MIMEPOST, handleData.mime) elif request.body.issome: curl.setopt(CURLOPT_POSTFIELDS, cstring(request.body.get)) diff --git a/src/io/request.nim b/src/io/request.nim index 531a2fc3..76fd9fc4 100644 --- a/src/io/request.nim +++ b/src/io/request.nim @@ -4,6 +4,7 @@ import strutils import tables import bindings/quickjs +import types/formdata import types/url import js/javascript import utils/twtstr @@ -66,7 +67,7 @@ type url*: Url headers* {.jsget.}: Headers body*: Option[string] - multipart*: Option[MimeData] + multipart*: Option[FormData] referer*: URL mode* {.jsget.}: RequestMode destination* {.jsget.}: RequestDestination @@ -92,19 +93,6 @@ type Headers* = ref object table* {.jsget.}: Table[string, seq[string]] -# Originally from the stdlib - MimePart* = object - name*, content*: string - case isFile*: bool - of true: - filename*, contentType*: string - fileSize*: int64 - isStream*: bool - else: discard - - MimeData* = object - content*: seq[MimePart] - proc Request_url(ctx: JSContext, this: JSValue, magic: cint): JSValue {.cdecl.} = let op = getOpaque0(this) if unlikely(not ctx.isInstanceOf(this, "Request") or op == nil): @@ -213,31 +201,30 @@ func newHeaders*(table: Table[string, string]): Headers = result.table[k] = @[v] func newRequest*(url: URL, httpmethod = HTTP_GET, headers = newHeaders(), - body = none(string), # multipart = none(MimeData), - mode = RequestMode.NO_CORS, - credentialsMode = CredentialsMode.SAME_ORIGIN, - destination = RequestDestination.NO_DESTINATION, - proxy: URL = nil): Request = + body = none(string), multipart = none(FormData), mode = RequestMode.NO_CORS, + credentialsMode = CredentialsMode.SAME_ORIGIN, + destination = RequestDestination.NO_DESTINATION, proxy: URL = nil): Request = return Request( url: url, httpmethod: httpmethod, headers: headers, body: body, - #multipart: multipart, + multipart: multipart, mode: mode, credentialsMode: credentialsMode, destination: destination, proxy: proxy ) -func newRequest*(url: URL, httpmethod = HTTP_GET, headers: seq[(string, string)] = @[], - body = none(string), # multipart = none(MimeData), TODO TODO TODO multipart - mode = RequestMode.NO_CORS, proxy: URL = nil): Request = +func newRequest*(url: URL, httpmethod = HTTP_GET, + headers: seq[(string, string)] = @[], body = none(string), + multipart = none(FormData), mode = RequestMode.NO_CORS, proxy: URL = nil): + Request = let hl = newHeaders() for pair in headers: let (k, v) = pair hl.table[k] = @[v] - return newRequest(url, httpmethod, hl, body, mode, proxy = proxy) + return newRequest(url, httpmethod, hl, body, multipart, mode, proxy = proxy) func createPotentialCORSRequest*(url: URL, destination: RequestDestination, cors: CORSAttribute, fallbackFlag = false): Request = var mode = if cors == NO_CORS: @@ -257,16 +244,23 @@ func createPotentialCORSRequest*(url: URL, destination: RequestDestination, cors func newRequest*(resource: string, init: JSObject): Request {.jserr, jsctor.} = let x = parseURL(resource) if x.isNone: - JS_ERR JS_TypeError, resource & " is not a valid URL" + JS_ERR JS_TypeError, resource & " is not a valid URL." if x.get.username != "" or x.get.password != "": - JS_ERR JS_TypeError, resource & " is not a valid URL" + JS_ERR JS_TypeError, resource & " is not a valid URL." let url = x.get let ctx = init.ctx let fallbackMode = some(RequestMode.CORS) #TODO none if resource is request #TODO fallback mode, origin, window, request mode, ... let httpMethod = fromJS[HttpMethod](ctx, JS_GetPropertyStr(ctx, init.val, "method")).get(HTTP_GET) - let body = fromJS[string](ctx, JS_GetPropertyStr(ctx, init.val, "body")) + let bodyProp = JS_GetPropertyStr(ctx, init.val, "body") + let multipart = fromJS[FormData](ctx, bodyProp) + var body: Option[string] + if multipart.isNone: + body = fromJS[string](ctx, bodyProp) + #TODO inputbody + if (multipart.isSome or body.isSome) and httpMethod in {HTTP_GET, HTTP_HEAD}: + JS_ERR JS_TypeError, "HEAD or GET Request cannot have a body." let jheaders = JS_GetPropertyStr(ctx, init.val, "headers") let hl = newHeaders() hl.fill(ctx, jheaders) @@ -276,10 +270,7 @@ func newRequest*(resource: string, init: JSObject): Request {.jserr, jsctor.} = .get(fallbackMode.get(RequestMode.NO_CORS)) #TODO find a standard compatible way to implement this let proxyUrl = fromJS[URL](ctx, JS_GetPropertyStr(ctx, init.val, "proxyUrl")) - return newRequest(url, httpMethod, hl, body, mode, credentials, proxy = proxyUrl.get(nil)) - -proc `[]=`*(multipart: var MimeData, k, v: string) = - multipart.content.add(MimePart(name: k, content: v)) + return newRequest(url, httpMethod, hl, body, multipart, mode, credentials, proxy = proxyUrl.get(nil)) proc add*(headers: var Headers, k, v: string) = let k = k.toHeaderCase() diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim index 0636e2e9..c2a6f5a6 100644 --- a/src/ips/serialize.nim +++ b/src/ips/serialize.nim @@ -7,7 +7,9 @@ import tables import io/request import js/regex +import types/blob import types/buffersource +import types/formdata import types/url proc swrite*(stream: Stream, n: SomeNumber) @@ -54,9 +56,13 @@ proc swrite*(stream: Stream, obj: ref object) proc sread*(stream: Stream, obj: var ref object) func slen*(obj: ref object): int -proc swrite*(stream: Stream, part: MimePart) -proc sread*(stream: Stream, part: var MimePart) -func slen*(part: MimePart): int +proc swrite*(stream: Stream, part: FormDataEntry) +proc sread*(stream: Stream, part: var FormDataEntry) +func slen*(part: FormDataEntry): int + +proc swrite*(stream: Stream, blob: Blob) +proc sread*(stream: Stream, blob: var Blob) +func slen*(blob: Blob): int proc swrite*[T](stream: Stream, o: Option[T]) proc sread*[T](stream: Stream, o: var Option[T]) @@ -242,40 +248,57 @@ func slen*(obj: ref object): int = if obj != nil: result += slen(obj[]) -proc swrite*(stream: Stream, part: MimePart) = - stream.swrite(part.isFile) +proc swrite*(stream: Stream, part: FormDataEntry) = + stream.swrite(part.isstr) stream.swrite(part.name) - stream.swrite(part.content) - if part.isFile: - stream.swrite(part.filename) - stream.swrite(part.contentType) - stream.swrite(part.fileSize) - stream.swrite(part.isStream) - -proc sread*(stream: Stream, part: var MimePart) = - var isFile: bool - stream.sread(isFile) - if isFile: - part = MimePart(isFile: true) + stream.swrite(part.filename) + if part.isstr: + stream.swrite(part.svalue) + else: + stream.swrite(part.value) + +proc sread*(stream: Stream, part: var FormDataEntry) = + var isstr: bool + stream.sread(isstr) + if isstr: + part = FormDataEntry(isstr: true) else: - part = MimePart(isFile: false) + part = FormDataEntry(isstr: false) stream.sread(part.name) - stream.sread(part.content) - if part.isFile: - stream.sread(part.filename) - stream.sread(part.contentType) - stream.sread(part.fileSize) - stream.sread(part.isStream) - -func slen*(part: MimePart): int = - result += slen(part.isFile) + stream.sread(part.filename) + if part.isstr: + stream.sread(part.svalue) + else: + stream.sread(part.value) + +func slen*(part: FormDataEntry): int = + result += slen(part.isstr) result += slen(part.name) - result += slen(part.content) - if part.isFile: - result += slen(part.filename) - result += slen(part.contentType) - result += slen(part.fileSize) - result += slen(part.isStream) + result += slen(part.filename) + if part.isstr: + result += slen(part.svalue) + else: + result += slen(part.value) + +proc swrite*(stream: Stream, blob: Blob) = + stream.swrite(blob.ctype) + stream.swrite(blob.size) + #TODO ?? + stream.writeData(blob.buffer, int(blob.size)) + +proc sread*(stream: Stream, blob: var Blob) = + new(blob) + stream.sread(blob.ctype) + stream.sread(blob.size) + blob.buffer = alloc(blob.size) + blob.deallocFun = dealloc + #TODO ?? + assert stream.readData(blob.buffer, int(blob.size)) == int(blob.size) + +func slen*(blob: Blob): int = + result += slen(blob.ctype) + result += slen(blob.size) + result += int(blob.size) #TODO ?? proc swrite*[T](stream: Stream, o: Option[T]) = stream.swrite(o.issome) diff --git a/src/js/intl.nim b/src/js/intl.nim new file mode 100644 index 00000000..28cc42d5 --- /dev/null +++ b/src/js/intl.nim @@ -0,0 +1,46 @@ +# Very minimal Intl module... TODO make it more complete + +import bindings/quickjs +import js/javascript + +type + NumberFormat = ref object + +#TODO ...yeah +proc newNumberFormat(name: string = "en-US", + options = none(JSObject)): NumberFormat {.jsctor.} = + return NumberFormat() + +#TODO: this should accept string/BigInt too +proc format(nf: NumberFormat, num: float64): string {.jsfunc.} = + let s = $num + var i = 0 + var L = s.len + for k in countdown(s.high, 0): + if s[k] == '.': + L = k + break + if L mod 3 != 0: + while i < L mod 3: + result &= s[i] + inc i + if i < L: + result &= ',' + let j = i + while i < L: + if j != i and i mod 3 == j: + result &= ',' + result &= s[i] + inc i + if i + 1 < s.len and s[i] == '.': + if not (s[i + 1] == '0' and s.len == i + 2): + while i < s.len: + result &= s[i] + inc i + +proc addIntlModule*(ctx: JSContext) = + let global = JS_GetGlobalObject(ctx) + let intl = JS_NewObject(ctx) + ctx.registerType(NumberFormat, namespace = intl) + ctx.defineProperty(global, "Intl", intl) + JS_FreeValue(ctx, global) diff --git a/src/js/javascript.nim b/src/js/javascript.nim index 18ff3850..adf58ee4 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -5,7 +5,7 @@ # around each bound function call, so it shouldn't be too difficult to get it # working. (This would involve generating JS functions in registerType.) # Now for the pragmas: -# {.jsctr.} for constructors. These need no `this' value, and are bound as +# {.jsctor.} for constructors. These need no `this' value, and are bound as # regular constructors in JS. They must return a ref object, which will have # a JS counterpart too. (Other functions can return ref objects too, which # will either use the existing JS counterpart, if exists, or create a new @@ -293,7 +293,8 @@ proc definePropertyCWE*[T](ctx: JSContext, this: JSValue, name: string, func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, tname: string, ctor: JSCFunction, funcs: JSFunctionList, nimt: pointer, parent: JSClassID, asglobal: bool, nointerface: bool, - finalizer: proc(val: JSValue)): JSClassID {.discardable.} = + finalizer: proc(val: JSValue), + namespace: JSValue): JSClassID {.discardable.} = let rt = JS_GetRuntime(ctx) discard JS_NewClassID(addr result) var ctxOpaque = ctx.getOpaque() @@ -334,9 +335,12 @@ func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, tname: string, JS_SetConstructor(ctx, jctor, proto) ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor) if not nointerface: - let global = JS_GetGlobalObject(ctx) - ctx.defineProperty(global, $cdef.class_name, jctor) - JS_FreeValue(ctx, global) + if namespace == JS_NULL: + let global = JS_GetGlobalObject(ctx) + ctx.defineProperty(global, $cdef.class_name, jctor) + JS_FreeValue(ctx, global) + else: + ctx.defineProperty(namespace, $cdef.class_name, jctor) type FuncParam = tuple[name: string, t: NimNode, val: Option[NimNode], generic: Option[NimNode]] @@ -576,10 +580,12 @@ proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] = proc toJS*(ctx: JSContext, s: cstring): JSValue proc toJS*(ctx: JSContext, s: string): JSValue proc toJS(ctx: JSContext, r: Rune): JSValue -proc toJS(ctx: JSContext, n: int64): JSValue -proc toJS(ctx: JSContext, n: int32): JSValue +proc toJS*(ctx: JSContext, n: int64): JSValue +proc toJS*(ctx: JSContext, n: int32): JSValue proc toJS*(ctx: JSContext, n: int): JSValue -proc toJS(ctx: JSContext, n: uint32): JSValue +proc toJS*(ctx: JSContext, n: uint16): JSValue +proc toJS*(ctx: JSContext, n: uint32): JSValue +proc toJS*(ctx: JSContext, n: uint64): JSValue proc toJS(ctx: JSContext, n: SomeFloat): JSValue proc toJS*(ctx: JSContext, b: bool): JSValue proc toJS[U, V](ctx: JSContext, t: Table[U, V]): JSValue @@ -666,7 +672,7 @@ proc fromJS*[T](ctx: JSContext, val: JSValue): Option[T] = return fromJSInt[T](ctx, val) elif T is SomeFloat: if JS_IsNumber(val): - let f64: float64 + var f64: float64 if JS_ToFloat64(ctx, addr f64, val) < 0: return none(T) return some(cast[T](f64)) @@ -724,19 +730,26 @@ proc toJS*(ctx: JSContext, s: string): JSValue = proc toJS(ctx: JSContext, r: Rune): JSValue = return toJS(ctx, $r) -proc toJS(ctx: JSContext, n: int32): JSValue = +proc toJS*(ctx: JSContext, n: int32): JSValue = return JS_NewInt32(ctx, n) -proc toJS(ctx: JSContext, n: int64): JSValue = +proc toJS*(ctx: JSContext, n: int64): JSValue = return JS_NewInt64(ctx, n) # Always int32, so we don't risk 32-bit only breakage. proc toJS*(ctx: JSContext, n: int): JSValue = return toJS(ctx, int32(n)) -proc toJS(ctx: JSContext, n: uint32): JSValue = +proc toJS*(ctx: JSContext, n: uint16): JSValue = + return JS_NewUint32(ctx, uint32(n)) + +proc toJS*(ctx: JSContext, n: uint32): JSValue = return JS_NewUint32(ctx, n) +proc toJS*(ctx: JSContext, n: uint64): JSValue = + #TODO this is incorrect + return JS_NewFloat64(ctx, float64(n)) + proc toJS(ctx: JSContext, n: SomeFloat): JSValue = return JS_NewFloat64(ctx, float64(n)) @@ -837,9 +850,10 @@ type funcName: string generics: Table[string, seq[NimNode]] funcParams: seq[FuncParam] + passCtx: bool thisType: string returnType: Option[NimNode] - newName: string + newName: NimNode newBranchList: seq[NimNode] errval: NimNode # JS_EXCEPTION or -1 dielabel: NimNode # die: didn't match parameters, but could still match other ones @@ -922,8 +936,7 @@ proc getParams(fun: NimNode): seq[FuncParam] = t = quote do: typeof(`x`) else: - eprint treeRepr it - error("??") + error("?? " & treeRepr(it)) let val = if it[^1].kind != nnkEmpty: let x = it[^1] some(newPar(x)) @@ -1182,6 +1195,9 @@ proc addOptionalParams(gen: var JSFuncGenerator) = ) )) else: + if gen.funcParams[gen.i][2].isNone: + error("No fallback value. Maybe a non-optional parameter follows an " & + "optional parameter?") let fallback = gen.funcParams[gen.i][2].get if tt.typeKind == ntyGenericParam: gen.addUnionParam(tt, s, fallback) @@ -1208,11 +1224,11 @@ proc registerFunction(typ: string, t: BoundFunctionType, name: string, id: NimNo existing_funcs.incl(id.strVal) proc registerConstructor(gen: JSFuncGenerator) = - registerFunction(gen.thisType, gen.t, gen.funcName, ident(gen.newName)) + registerFunction(gen.thisType, gen.t, gen.funcName, gen.newName) js_funcs[gen.funcName] = gen proc registerFunction(gen: JSFuncGenerator) = - registerFunction(gen.thisType, gen.t, gen.funcName, ident(gen.newName)) + registerFunction(gen.thisType, gen.t, gen.funcName, gen.newName) var js_errors {.compileTime.}: Table[string, seq[string]] @@ -1222,13 +1238,18 @@ export JS_ThrowTypeError, JS_ThrowRangeError, JS_ThrowSyntaxError, proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode = let tt = gen.thisType let fn = gen.funcName - let ma = if gen.thisname.isSome: gen.minArgs - 1 else: gen.minArgs + var ma = gen.minArgs + if gen.thisname.isSome: + ma -= 1 + if gen.passCtx: + ma -= 1 + let pctx = gen.passCtx assert ma >= 0 result = newStmtList() if isva: result.add(quote do: if argc < `ma`: - return JS_ThrowTypeError(ctx, "At least %d arguments required, but only %d passed", `ma`, argc) + return JS_ThrowTypeError(ctx, "At least %d arguments required, but only %d passed %d", `ma`, argc, `pctx`) ) if gen.thisname.isSome: let tn = ident(gen.thisname.get) @@ -1258,7 +1279,7 @@ proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode = proc newJSProc(gen: var JSFuncGenerator, params: openArray[NimNode], isva = true): NimNode = let jsBody = gen.newJSProcBody(isva) let jsPragmas = newNimNode(nnkPragma).add(ident("cdecl")) - result = newProc(ident(gen.newName), params, jsBody, pragmas = jsPragmas) + result = newProc(gen.newName, params, jsBody, pragmas = jsPragmas) gen.res = result # WARNING: for now, this only works correctly when the .jserr pragma was @@ -1271,35 +1292,60 @@ macro JS_ERR*(a: typed, b: string) = block when_js: raise newException(`a`, `b`) -proc setupGenerator(fun: NimNode, t: BoundFunctionType, thisname = some("this")): JSFuncGenerator = - result.t = t - result.funcName = $fun[0] - if result.funcName == "$": +func getFuncName(fun: NimNode, jsname: string): string = + if jsname != "": + return jsname + let x = $fun[0] + if x == "$": # stringifier - result.funcName = "toString" - result.generics = getGenerics(fun) - result.funcParams = getParams(fun) - result.returnType = getReturn(fun) - result.minArgs = result.funcParams.getMinArgs() - result.original = fun - result.thisname = thisname + return "toString" + return x + +func getErrVal(t: BoundFunctionType): NimNode = if t in {PROPERTY_GET, PROPERTY_HAS}: - result.errval = quote do: cint(-1) - else: - result.errval = quote do: JS_EXCEPTION - result.dielabel = ident("ondie") - result.jsFunCallList = newStmtList() - result.jsFunCallLists.add(result.jsFunCallList) - result.jsFunCall = newCall(fun[0]) + return quote do: cint(-1) + return quote do: JS_EXCEPTION + +proc addJSContext(gen: var JSFuncGenerator) = + if gen.funcParams.len > gen.i and + gen.funcParams[gen.i].t.eqIdent(ident("JSContext")): + gen.passCtx = true + gen.jsFunCall.add(ident("ctx")) + inc gen.i + +proc addThisName(gen: var JSFuncGenerator, thisname: Option[string]) = if thisname.isSome: - result.thisType = $result.funcParams[0][1] - result.newName = $t & "_" & result.thisType & "_" & result.funcName + gen.thisType = $gen.funcParams[gen.i][1] + gen.newName = ident($gen.t & "_" & gen.thisType & "_" & gen.funcName) else: - if result.returnType.get.kind == nnkRefTy: - result.thisType = result.returnType.get[0].strVal + if gen.returnType.get.kind == nnkRefTy: + gen.thisType = gen.returnType.get[0].strVal else: - result.thisType = result.returnType.get.strVal - result.newName = $t & "_" & result.funcName + gen.thisType = gen.returnType.get.strVal + gen.newName = ident($gen.t & "_" & gen.funcName) + +proc setupGenerator(fun: NimNode, t: BoundFunctionType, + thisname = some("this"), jsname: string = ""): JSFuncGenerator = + let jsFunCallList = newStmtList() + let funcParams = getParams(fun) + var gen = JSFuncGenerator( + t: t, + funcName: getFuncName(fun, jsname), + generics: getGenerics(fun), + funcParams: funcParams, + returnType: getReturn(fun), + minArgs: funcParams.getMinArgs(), + original: fun, + thisname: thisname, + errval: getErrVal(t), + dielabel: ident("ondie"), + jsFunCallList: jsFunCallList, + jsFunCallLists: @[jsFunCallList], + jsFunCall: newCall(fun[0]) + ) + gen.addJSContext() + gen.addThisName(thisname) + return gen # this might be pretty slow... #TODO ideally we wouldn't need separate functions at all. Not sure how that @@ -1354,7 +1400,7 @@ macro jserr*(fun: untyped) = macro jsctor*(fun: typed) = var gen = setupGenerator(fun, CONSTRUCTOR, thisname = none(string)) - if gen.newName in existing_funcs: + if gen.newName.strVal in existing_funcs: #TODO TODO TODO implement function overloading error("Function overloading hasn't been implemented yet...") gen.addRequiredParams() @@ -1372,7 +1418,7 @@ macro jsctor*(fun: typed) = macro jsgctor*(fun: typed) = var gen = setupGenerator(fun, CONSTRUCTOR, thisname = none(string)) - if gen.newName in existing_funcs: + if gen.newName.strVal in existing_funcs: #TODO TODO TODO implement function overloading error("Function overloading hasn't been implemented yet...") gen.addFixParam("this") @@ -1391,7 +1437,7 @@ macro jsgctor*(fun: typed) = macro jshasprop*(fun: typed) = var gen = setupGenerator(fun, PROPERTY_HAS, thisname = some("obj")) - if gen.newName in existing_funcs: + if gen.newName.strVal in existing_funcs: #TODO TODO TODO ditto error("Function overloading hasn't been implemented yet...") gen.addFixParam("obj") @@ -1409,7 +1455,7 @@ macro jshasprop*(fun: typed) = macro jsgetprop*(fun: typed) = var gen = setupGenerator(fun, PROPERTY_GET, thisname = some("obj")) - if gen.newName in existing_funcs: + if gen.newName.strVal in existing_funcs: #TODO TODO TODO ditto error("Function overloading hasn't been implemented yet...") gen.addFixParam("obj") @@ -1431,13 +1477,13 @@ macro jsgetprop*(fun: typed) = gen.registerFunction() result = newStmtList(fun, jsProc) -macro jsfget*(fun: typed) = - var gen = setupGenerator(fun, GETTER) +macro jsfgetn(jsname: static string, fun: typed) = + var gen = setupGenerator(fun, GETTER, jsname = jsname) if gen.minArgs != 1 or gen.funcParams.len != gen.minArgs: error("jsfget functions must only have one parameter.") if gen.returnType.isnone: error("jsfget functions must have a return type.") - if gen.newName in existing_funcs: + if gen.newName.strVal in existing_funcs: #TODO TODO TODO ditto error("Function overloading hasn't been implemented yet...") gen.addFixParam("this") @@ -1451,8 +1497,17 @@ macro jsfget*(fun: typed) = gen.registerFunction() result = newStmtList(fun, jsProc) -macro jsfset*(fun: typed) = - var gen = setupGenerator(fun, SETTER) +# "Why?" So the compiler doesn't cry. +macro jsfget*(fun: typed) = + quote do: + jsfgetn("", `fun`) + +macro jsfget*(jsname: static string, fun: typed) = + quote do: + jsfgetn(`jsname`, `fun`) + +macro jsfsetn(jsname: static string, fun: typed) = + var gen = setupGenerator(fun, SETTER, jsname = jsname) if gen.minArgs != 2 or gen.funcParams.len != gen.minArgs: error("jsfset functions must accept two parameters") if gen.returnType.issome: @@ -1472,8 +1527,16 @@ macro jsfset*(fun: typed) = gen.registerFunction() result = newStmtList(fun, jsProc) -macro jsfunc*(fun: typed) = - var gen = setupGenerator(fun, FUNCTION) +macro jsfset*(fun: typed) = + quote do: + jsfsetn("", `fun`) + +macro jsfset*(jsname: static string, fun: typed) = + quote do: + jsfsetn(`jsname`, `fun`) + +macro jsfuncn*(jsname: static string, fun: typed) = + var gen = setupGenerator(fun, FUNCTION, jsname = jsname) if gen.minArgs == 0: error("Zero-parameter functions are not supported. (Maybe pass Window or Client?)") gen.addFixParam("this") @@ -1496,15 +1559,25 @@ macro jsfunc*(fun: typed) = gen.registerFunction() result = newStmtList(fun, jsProc) +macro jsfunc*(fun: typed) = + quote do: + jsfuncn("", `fun`) + +macro jsfunc*(jsname: static string, fun: typed) = + quote do: + jsfuncn(`jsname`, `fun`) + macro jsfin*(fun: typed) = var gen = setupGenerator(fun, FINALIZER, thisname = some("fin")) - registerFunction(gen.thisType, FINALIZER, gen.funcName, ident(gen.newName)) + registerFunction(gen.thisType, FINALIZER, gen.funcName, gen.newName) fun # Having the same names for these and the macros leads to weird bugs, so the # macros get an additional f. template jsget*() {.pragma.} +template jsget*(name: string) {.pragma.} template jsset*() {.pragma.} +template jsset*(name: string) {.pragma.} proc nim_finalize_for_js[T](obj: T) = for rt in runtimes: @@ -1543,9 +1616,26 @@ proc nim_finalize_for_js[T](obj: T) = proc js_illegal_ctor*(ctx: JSContext, this: JSValue, argc: cint, argv: ptr JSValue): JSValue {.cdecl.} = return JS_ThrowTypeError(ctx, "Illegal constructor") -type JSObjectPragmas = object - jsget: seq[NimNode] - jsset: seq[NimNode] +type + JSObjectPragma = object + name: string + varsym: NimNode + + JSObjectPragmas = object + jsget: seq[JSObjectPragma] + jsset: seq[JSObjectPragma] + jsinclude: seq[JSObjectPragma] + +func getPragmaName(varPragma: NimNode): string = + if varPragma.kind == nnkExprColonExpr: + return $varPragma[0] + return $varPragma + +func getStringFromPragma(varPragma: NimNode): Option[string] = + if varPragma.kind == nnkExprColonExpr: + if not varPragma.len == 1 and varPragma[1].kind == nnkStrLit: + error("Expected string as pragma argument") + return some($varPragma[1]) proc findPragmas(t: NimNode): JSObjectPragmas = let typ = t.getTypeInst()[1] # The type, as declared. @@ -1557,7 +1647,6 @@ proc findPragmas(t: NimNode): JSObjectPragmas = for i in 0..<identDefsStack.len: identDefsStack[i] = impl[2][i] while identDefsStack.len > 0: var identDefs = identDefsStack.pop() - case identDefs.kind of nnkRecList: for child in identDefs.children: @@ -1576,10 +1665,17 @@ proc findPragmas(t: NimNode): JSObjectPragmas = if varName.kind == nnkPostfix: # This is a public field. We are skipping the postfix * varName = varName[1] - for pragma in varNode[1]: - case $pragma - of "jsget": result.jsget.add(varName) - of "jsset": result.jsset.add(varName) + var varPragmas = varNode[1] + for varPragma in varPragmas: + let pragmaName = getPragmaName(varPragma) + let op = JSObjectPragma( + name: getStringFromPragma(varPragma).get($varName), + varsym: varName + ) + case pragmaName + of "jsget": result.jsget.add(op) + of "jsset": result.jsset.add(op) + of "jsinclude": result.jsinclude.add(op) type TabGetSet* = object @@ -1595,12 +1691,13 @@ type macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = false, nointerface = false, name: static string = "", extra_getset: static openarray[TabGetSet] = [], - extra_funcs: static openarray[TabFunc] = []): JSClassID = + extra_funcs: static openarray[TabFunc] = [], + namespace: JSValue = JS_NULL): JSClassID = result = newStmtList() let tname = t.strVal # the nim type's name. let name = if name == "": tname else: name # possibly a different name, e.g. Buffer for Container var sctr = ident("js_illegal_ctor") - var sfin = ident("js_" & t.strVal & "ClassFin") + var sfin = ident("js_" & tname & "ClassFin") # constructor var ctorFun: NimNode var ctorImpl: NimNode @@ -1614,9 +1711,10 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = var setters, getters: Table[string, NimNode] let tabList = newNimNode(nnkBracket) let pragmas = findPragmas(t) - for node in pragmas.jsget: - let id = ident($GETTER & "_" & t.strVal & "_" & $node) - let fn = $node + for op in pragmas.jsget: + let node = op.varsym + let fn = op.name + let id = ident($GETTER & "_" & tname & "_" & fn) result.add(quote do: proc `id`(ctx: JSContext, this: JSValue): JSValue {.cdecl.} = if not (JS_IsUndefined(this) or ctx.isGlobal(`tname`)) and not ctx.isInstanceOf(this, `tname`): @@ -1625,10 +1723,11 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = let arg_0 = fromJS_or_return(`t`, ctx, this) return toJS(ctx, arg_0.`node`) ) - registerFunction(t.strVal, GETTER, fn, id) - for node in pragmas.jsset: - let id = ident($SETTER & "_" & t.strVal & "_" & $node) - let fn = $node + registerFunction(tname, GETTER, fn, id) + for op in pragmas.jsset: + let node = op.varsym + let fn = op.name + let id = ident($SETTER & "_" & tname & "_" & fn) result.add(quote do: proc `id`(ctx: JSContext, this: JSValue, val: JSValue): JSValue {.cdecl.} = if not (JS_IsUndefined(this) or ctx.isGlobal(`tname`)) and not ctx.isInstanceOf(this, `tname`): @@ -1639,10 +1738,10 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = arg_0.`node` = fromJS_or_return(typeof(arg_0.`node`), ctx, arg_1) return JS_DupValue(ctx, arg_1) ) - registerFunction(t.strVal, SETTER, fn, id) + registerFunction(tname, SETTER, fn, id) - if t.strVal in BoundFunctions: - for fun in BoundFunctions[t.strVal].mitems: + if tname in BoundFunctions: + for fun in BoundFunctions[tname].mitems: var f0 = fun.name let f1 = fun.id if fun.name.endsWith("_exceptions"): @@ -1655,7 +1754,7 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = of CONSTRUCTOR: ctorImpl = js_funcs[$f0].res if ctorFun != nil: - error("Class " & t.strVal & " has 2+ constructors.") + error("Class " & tname & " has 2+ constructors.") ctorFun = f1 of GETTER: getters[f0] = f1 @@ -1765,7 +1864,8 @@ static JSClassDef """, `cdname`, """ = { # any associated JS object from all relevant runtimes. var x: `t` new(x, nim_finalize_for_js) - `ctx`.newJSClass(`classDef`, `tname`, `sctr`, `tabList`, getTypePtr(x), `parent`, `asglobal`, `nointerface`, `finName`) + `ctx`.newJSClass(`classDef`, `tname`, `sctr`, `tabList`, getTypePtr(x), + `parent`, `asglobal`, `nointerface`, `finName`, `namespace`) ) result.add(newBlockStmt(endstmts)) diff --git a/src/types/blob.nim b/src/types/blob.nim new file mode 100644 index 00000000..677fb037 --- /dev/null +++ b/src/types/blob.nim @@ -0,0 +1,61 @@ +import js/javascript +import types/mime +import utils/twtstr + +type + DeallocFun = proc(buffer: pointer) {.noconv.} + + Blob* = ref object of RootObj + isfile*: bool + size* {.jsget.}: uint64 + ctype* {.jsget: "type".}: string + buffer*: pointer + deallocFun*: DeallocFun + + WebFile* = ref object of Blob + webkitRelativePath {.jsget.}: string + path*: string + file: File #TODO maybe use fd? + +proc newBlob*(buffer: pointer, size: int, ctype: string, + deallocFun: DeallocFun): Blob = + return Blob( + buffer: buffer, + size: uint64(size), + ctype: ctype, + deallocFun: deallocFun + ) + +proc finalize(blob: Blob) {.jsfin.} = + if blob.deallocFun != nil and blob.buffer != nil: + blob.deallocFun(blob.buffer) + +proc newWebFile*(path: string, webkitRelativePath = ""): WebFile = + var file: File + doAssert open(file, path, fmRead) #TODO bleh + return WebFile( + isfile: true, + path: path, + file: file, + webkitRelativePath: webkitRelativePath + ) + +#TODO File, Blob constructors + +func size*(this: WebFile): uint64 {.jsfget.} = + #TODO use stat instead + return uint64(this.file.getFileSize()) + +func ctype*(this: WebFile): string {.jsfget: "type".} = + return guessContentType(this.path) + +func name*(this: WebFile): string {.jsfget.} = + if this.path.len > 0 and this.path[^1] != '/': + return this.path.afterLast('/') + return this.path.afterLast('/', 2) + +#TODO lastModified + +proc addBlobModule*(ctx: JSContext) = + ctx.registerType(Blob) + ctx.registerType(WebFile, name = "File") diff --git a/src/types/color.nim b/src/types/color.nim index 02f56a56..818d4f5a 100644 --- a/src/types/color.nim +++ b/src/types/color.nim @@ -1,3 +1,4 @@ +import math import options import sequtils import strutils @@ -21,6 +22,8 @@ converter toRGBColor*(i: RGBAColor): RGBColor = converter toRGBAColor*(i: RGBColor): RGBAColor = return RGBAColor(uint32(i) or 0xFF000000u32) +func `==`*(a, b: RGBAColor): bool {.borrow.} + func rgbcolor*(color: CellColor): RGBColor = cast[RGBColor](color.n) @@ -220,10 +223,99 @@ func b*(c: RGBAColor): int = func a*(c: RGBAColor): int = return int(uint32(c) shr 24 and 0xff) +# https://html.spec.whatwg.org/#serialisation-of-a-color +func serialize*(color: RGBAColor): string = + if color.a == 255: + let r = toHex(cast[uint8](color.r)) + let g = toHex(cast[uint8](color.g)) + let b = toHex(cast[uint8](color.b)) + return "#" & r & g & b + let a = float64(color.a) / 255 + return "rgba(" & $color.r & ", " & $color.g & ", " & $color.b & ", " & $a & + ")" + +func `$`*(rgbacolor: RGBAColor): string = + return rgbacolor.serialize() + +# https://arxiv.org/pdf/2202.02864.pdf +func fastmul(c, ca: uint32): uint32 = + let u = c or 0xFF000000u32 + var rb = u and 0x00FF00FFu32 + rb *= ca + rb += 0x00800080 + rb += (rb shr 8) and 0x00FF00FFu32 + rb = rb and 0xFF00FF00u32 + var ga = (u shr 8) and 0x00FF00FFu32 + ga *= ca + ga += 0x00800080 + ga += (ga shr 8) and 0x00FF00FFu32 + ga = ga and 0xFF00FF00u32 + return ga or (rb shr 8) + +# fastmul, but preserves alpha +func fastmul1(c, ca: uint32): uint32 = + let u = c + var rb = u and 0x00FF00FFu32 + rb *= ca + rb += 0x00800080 + rb += (rb shr 8) and 0x00FF00FFu32 + rb = rb and 0xFF00FF00u32 + var ga = (u shr 8) and 0x00FF00FFu32 + ga *= ca + ga += 0x00800080 + ga += (ga shr 8) and 0x00FF00FFu32 + ga = ga and 0xFF00FF00u32 + return ga or (rb shr 8) + +func fastmul(c: RGBAColor, ca: uint32): uint32 = + return fastmul(uint32(c), ca) + +func fastmul1(c: RGBAColor, ca: uint32): uint32 = + return fastmul1(uint32(c), ca) + +func rgba*(r, g, b, a: int): RGBAColor + +func premul(c: RGBAColor): RGBAColor = + return RGBAColor(fastmul(c, uint32(c.a))) + +const straightAlphaTable = (func(): auto = + var table: array[256, array[256, uint8]] + for a in 0 ..< 256: + let multiplier = if a > 0: (255 / a.float32) else: 0 + for c in 0 ..< 256: + table[a][c] = min(round((c.float32 * multiplier)), 255).uint8 + return table)() + +proc straight*(c: RGBAColor): RGBAColor = + let r = straightAlphaTable[c.a][c.r] + let g = straightAlphaTable[c.a][c.g] + let b = straightAlphaTable[c.a][c.b] + return rgba(int(r), int(g), int(b), int(c.a)) + +func blend*(c0, c1: RGBAColor): RGBAColor = + let pc0 = c0.premul() + let pc1 = c1.premul() + let k = 255 - pc1.a + let mc = RGBAColor(fastmul1(pc0, uint32(k))) + let rr = pc1.r + mc.r + let rg = pc1.g + mc.g + let rb = pc1.b + mc.b + let ra = pc1.a + mc.a + let pres = rgba(rr, rg, rb, ra) + let res = straight(pres) + return res + +#func blend*(c0, c1: RGBAColor): RGBAColor = +# const norm = 1f64 / 255f64 +# let c0a = float64(c0.a) * norm +# let c1a = float64(c1.a) * norm +# let a0 = c0a + c1a * (1 - c0a) + func rgb*(r, g, b: int): RGBColor = return RGBColor((r shl 16) or (g shl 8) or b) -func `==`*(a, b: RGBAColor): bool {.borrow.} +func rgb*(r, g, b: uint8): RGBColor {.inline.} = + return rgb(int(r), int(g), int(b)) func r*(c: RGBColor): int = return int(uint32(c) shr 16 and 0xff) @@ -256,12 +348,15 @@ func YUV*(Y, U, V: int): RGBColor = func rgba*(r, g, b, a: int): RGBAColor = return RGBAColor((uint32(a) shl 24) or (uint32(r) shl 16) or (uint32(g) shl 8) or uint32(b)) +func gray*(n: int): RGBColor = + return rgb(n, n, n) #TODO use yuv instead? + +func gray*(n: uint8): RGBColor = + return gray(int(n)) + template `$`*(rgbcolor: RGBColor): string = "rgb(" & $rgbcolor.r & ", " & $rgbcolor.g & ", " & $rgbcolor.b & ")" -template `$`*(rgbacolor: RGBAColor): string = - "rgba(" & $rgbacolor.r & ", " & $rgbacolor.g & ", " & $rgbacolor.b & ", " & $rgbacolor.a & ")" - template `$`*(color: CellColor): string = if color.rgb: $color.rgbcolor diff --git a/src/types/cookie.nim b/src/types/cookie.nim index 41de0f4c..9dc50550 100644 --- a/src/types/cookie.nim +++ b/src/types/cookie.nim @@ -157,7 +157,9 @@ proc newCookie*(str: string): Cookie {.jsctor.} = if date.issome: cookie.expires = date.get.toTime().toUnix() of "max-age": - cookie.expires = now().toTime().toUnix() + parseInt64(val) + let x = parseInt64(val) + if x.isSome: + cookie.expires = now().toTime().toUnix() + x.get of "secure": cookie.secure = true of "httponly": cookie.httponly = true of "samesite": cookie.samesite = true diff --git a/src/types/formdata.nim b/src/types/formdata.nim new file mode 100644 index 00000000..983508bf --- /dev/null +++ b/src/types/formdata.nim @@ -0,0 +1,18 @@ +import types/blob + +type + FormDataEntry* = object + name*: string + filename*: string + case isstr*: bool + of true: + svalue*: string + of false: + value*: Blob + + FormData* = ref object + entries*: seq[FormDataEntry] + +iterator items*(this: FormData): FormDataEntry {.inline.} = + for entry in this.entries: + yield entry diff --git a/src/types/line.nim b/src/types/line.nim new file mode 100644 index 00000000..31a639f5 --- /dev/null +++ b/src/types/line.nim @@ -0,0 +1,66 @@ +import types/vector + +type + Line* = object + p0*: Vector2D + p1*: Vector2D + + LineSegment* = object + line: Line + miny*: float64 + maxy*: float64 + minyx*: float64 + islope*: float64 + +func minx*(line: Line): float64 = + return min(line.p0.x, line.p1.x) + +func maxx*(line: Line): float64 = + return max(line.p0.x, line.p1.x) + +func minyx*(line: Line): float64 = + if line.p0.y < line.p1.y: + return line.p0.x + return line.p1.x + +func maxyx*(line: Line): float64 = + if line.p0.y > line.p1.y: + return line.p0.x + return line.p1.x + +func miny*(line: Line): float64 = + return min(line.p0.y, line.p1.y) + +func maxy*(line: Line): float64 = + return max(line.p0.y, line.p1.y) + +func slope*(line: Line): float64 = + let xdiff = (line.p0.x - line.p1.x) + if xdiff == 0: + return 0 + return (line.p0.y - line.p1.y) / xdiff + +# inverse slope +func islope*(line: Line): float64 = + let ydiff = (line.p0.y - line.p1.y) + if ydiff == 0: + return 0 + return (line.p0.x - line.p1.x) / ydiff + +proc cmpLineSegmentY*(l1, l2: LineSegment): int = + return cmp(l1.miny, l2.miny) + +proc cmpLineSegmentX*(l1, l2: LineSegment): int = + return cmp(l1.minyx, l2.minyx) + +func p0*(ls: LineSegment): Vector2D {.inline.} = ls.line.p0 +func p1*(ls: LineSegment): Vector2D {.inline.} = ls.line.p1 + +converter toLineSegment*(line: Line): LineSegment = + LineSegment( + line: line, + miny: line.miny, + maxy: line.maxy, + minyx: line.minyx, + islope: line.islope + ) diff --git a/src/types/matrix.nim b/src/types/matrix.nim new file mode 100644 index 00000000..17ff3c0d --- /dev/null +++ b/src/types/matrix.nim @@ -0,0 +1,46 @@ +type Matrix* = object + me*: seq[float64] + w: int + h: int + +proc newMatrix*(me: seq[float64], w: int, h: int): Matrix = + return Matrix( + me: me, + w: w, + h: h + ) + +proc newIdentityMatrix*(n: int): Matrix = + var me = newSeq[float64](n * n) + for i in 0 ..< n: + me[n * i + i] = 1 + return Matrix( + me: me, + w: n, + h: n + ) + +proc newMatrixUninitialized*(w, h: int): Matrix = + return Matrix( + me: newSeqUninitialized[float64](w * h), + w: w, + h: h + ) + +#TODO this is extremely inefficient +proc `*`*(a: Matrix, b: Matrix): Matrix = + assert a.w == b.h + let h = a.h + let w = b.w + let n = a.w + var c = newMatrixUninitialized(w, h) + for x in 0 ..< w: + for y in 0 ..< h: + var val: float64 = 0 + for i in 0 ..< n: + val += a.me[y * a.w + i] * b.me[i * b.w + x] + c.me[y * c.w + x] = val + return c + +proc `*=`*(a: var Matrix, b: Matrix) = + a = a * b diff --git a/src/types/mime.nim b/src/types/mime.nim index 2db567ad..cd4c2d51 100644 --- a/src/types/mime.nim +++ b/src/types/mime.nim @@ -12,12 +12,12 @@ const DefaultGuess = [ ("", "text/plain") ].toTable() -proc guessContentType*(path: string): string = +proc guessContentType*(path: string, def = DefaultGuess[""]): string = var i = path.len - 1 var n = 0 while i > 0: if path[i] == '/': - return DefaultGuess[""] + return def if path[i] == '.': n = i break @@ -26,7 +26,7 @@ proc guessContentType*(path: string): string = let ext = path.substr(n + 1) if ext in DefaultGuess: return DefaultGuess[ext] - return DefaultGuess[""] + return def const JavaScriptTypes = [ "application/ecmascript", diff --git a/src/types/url.nim b/src/types/url.nim index 10b91fe5..cba83adb 100644 --- a/src/types/url.nim +++ b/src/types/url.nim @@ -6,6 +6,7 @@ import unicode import math import js/javascript +import types/blob import utils/twtstr type @@ -17,10 +18,8 @@ type RELATIVE_SLASH_STATE, QUERY_STATE, HOST_STATE, HOSTNAME_STATE, FILE_HOST_STATE, PORT_STATE, PATH_START_STATE, FILE_SLASH_STATE - Blob* = object - BlobUrlEntry* = object - obj: Blob #TODO + obj: Blob #TODO blob urls UrlPath* = object case opaque*: bool @@ -570,10 +569,11 @@ proc basicParseUrl*(input: string, base = none(Url), url: Url = Url(), stateOver (url.is_special and c == '\\') or override: if buffer != "": let i = parseInt32(buffer) - if i notin 0..65535: + if i.isNone or i.get notin 0..65535: + eprint "validation error???", i #TODO validation error return none(Url) - let port = cast[uint16](i).some + let port = cast[uint16](i.get).some url.port = if url.is_special and url.default_port == port: none(uint16) else: port buffer = "" if override: diff --git a/src/types/vector.nim b/src/types/vector.nim new file mode 100644 index 00000000..e200cc98 --- /dev/null +++ b/src/types/vector.nim @@ -0,0 +1,53 @@ +import math + +type Vector2D* = object + x*: float64 + y*: float64 + +func `-`*(v1, v2: Vector2D): Vector2D = + return Vector2D(x: v1.x - v2.x, y: v1.y - v2.y) + +func `+`*(v1, v2: Vector2D): Vector2D = + return Vector2D(x: v1.x + v2.x, y: v1.y + v2.y) + +proc `+=`*(v1: var Vector2D, v2: Vector2D) = + v1.x += v2.x + v1.y += v2.y + +proc `-=`*(v1: var Vector2D, v2: Vector2D) = + v1.x -= v2.x + v1.y -= v2.y + +# scalar multiplication +func `*`*(v: Vector2D, s: float64): Vector2D = + return Vector2D(x: v.x * s, y: v.y * s) + +func `/`*(v: Vector2D, s: float64): Vector2D = + return Vector2D(x: v.x / s, y: v.y / s) + +# dot product +func `*`*(v1, v2: Vector2D): float64 = + return v1.x * v2.x + v1.y * v2.y + +func norm*(v: Vector2D): float64 = + return sqrt(v.x * v.x + v.y * v.y) + +# kind of a cross product? +func cross*(v1, v2: Vector2D): float64 = + return v1.x * v2.y - v1.y * v2.x + +# https://en.wikipedia.org/wiki/Inner_product_space +func innerAngle*(v1, v2: Vector2D): float64 = + return arccos((v1 * v2) / (v1.norm() * v2.norm())) + +func rotate*(v: Vector2D, alpha: float64): Vector2D = + let sa = sin(alpha) + let ca = cos(alpha) + return Vector2D( + x: v.x * ca - v.y * sa, + y: v.x * sa + v.y * ca + ) + +func collinear*(v1, v2, v3: Vector2D): bool = + return almostEqual((v1.y - v2.y) * (v1.x - v3.x), + (v1.y - v3.y) * (v1.x - v2.x)) diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index d67e1ab7..93fce06f 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -122,6 +122,11 @@ func snakeToKebabCase*(str: string): string = if c == '_': c = '-' +func normalizeLocale*(s: string): string = + for i in 0 ..< s.len: + if cast[uint8](s[i]) > 0x20 and s[i] != '_' and s[i] != '-': + result &= s[i].toLowerAscii() + func isAscii*(r: Rune): bool = return cast[uint32](r) < 128 @@ -168,6 +173,9 @@ func toHex*(c: char): string = result[0] = HexChars[(uint8(c) shr 4)] result[1] = HexChars[(uint8(c) and 0xF)] +func toHex*(i: uint8): string = + return toHex(cast[char](i)) + func equalsIgnoreCase*(s1: seq[Rune], s2: string): bool = var i = 0 while i < min(s1.len, s2.len): @@ -236,6 +244,17 @@ func after*(s: string, c: set[char]): string = func after*(s: string, c: char): string = s.after({c}) +func afterLast*(s: string, c: set[char], n = 1): string = + var j = 0 + for i in countdown(s.high, 0): + if s[i] in c: + inc j + if j == n: + return s.substr(i + 1) + return s + +func afterLast*(s: string, c: char, n = 1): string = s.afterLast({c}, n) + proc c_sprintf(buf, fm: cstring): cint {.header: "<stdio.h>", importc: "sprintf", varargs} # From w3m @@ -345,72 +364,59 @@ func japaneseNumber*(i: int): string = result &= ss[n] dec n -func parseInt32*(s: string): int = - var sign = 1 - var t = 1 - var integer: int = 0 - var e: int = 0 - +# Implements https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#signed-integers +#TODO TODO TODO handle overflow defects +func parseInt32*(s: string): Option[int32] = + var sign: int32 = 1 var i = 0 if i < s.len and s[i] == '-': sign = -1 inc i elif i < s.len and s[i] == '+': inc i - + if i == s.len or s[i] notin AsciiDigit: + return none(int32) + var integer = int32(decValue(s[i])) + inc i while i < s.len and isDigit(s[i]): integer *= 10 - integer += decValue(s[i]) + integer += int32(decValue(s[i])) inc i + return some(sign * integer) - if i < s.len and (s[i] == 'e' or s[i] == 'E'): - inc i - if i < s.len and s[i] == '-': - t = -1 - inc i - elif i < s.len and s[i] == '+': - inc i - - while i < s.len and isDigit(s[i]): - e *= 10 - e += decValue(s[i]) - inc i - - return sign * integer * 10 ^ (t * e) - -func parseInt64*(s: string): int64 = - var sign = 1 - var t = 1 - var integer: int64 = 0 - var e: int64 = 0 - +func parseInt64*(s: string): Option[int64] = + var sign: int64 = 1 var i = 0 if i < s.len and s[i] == '-': sign = -1 inc i elif i < s.len and s[i] == '+': inc i - + if i == s.len or s[i] notin AsciiDigit: + return none(int64) + var integer = int64(decValue(s[i])) + inc i while i < s.len and isDigit(s[i]): integer *= 10 - integer += decValue(s[i]) + integer += int64(decValue(s[i])) inc i + return some(sign * integer) - if i < s.len and (s[i] == 'e' or s[i] == 'E'): +func parseUInt32*(s: string): Option[uint32] = + var i = 0 + if i < s.len and s[i] == '+': inc i - if i < s.len and s[i] == '-': - t = -1 - inc i - elif i < s.len and s[i] == '+': - inc i - - while i < s.len and isDigit(s[i]): - e *= 10 - e += decValue(s[i]) - inc i - - return sign * integer * 10 ^ (t * e) + if i == s.len or s[i] notin AsciiDigit: + return none(uint32) + var integer = uint32(decValue(s[i])) + inc i + while i < s.len and isDigit(s[i]): + integer *= 10 + integer += uint32(decValue(s[i])) + inc i + return some(integer) +#TODO not sure where this algorithm is from... func parseFloat64*(s: string): float64 = var sign = 1 var t = 1 diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim new file mode 100644 index 00000000..9b45e7ba --- /dev/null +++ b/src/xhr/formdata.nim @@ -0,0 +1,159 @@ +import html/dom +import html/tags +import js/javascript +import types/blob +import types/formdata +import utils/twtstr + +proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil, + encoding: string = ""): Option[seq[FormDataEntry]] + +proc newFormData*(form: HTMLFormElement = nil, + submitter: HTMLElement = nil): FormData {.jserr, jsctor.} = + let this = FormData() + if form != nil: + if submitter != nil: + if not submitter.isSubmitButton(): + JS_ERR JS_TypeError, "Submitter must be a submit button" + if FormAssociatedElement(submitter).form != form: + #TODO should be DOMException + JS_ERR JS_TypeError, "InvalidStateError" + this.entries = constructEntryList(form, submitter).get(@[]) + return this + +#TODO as jsfunc +proc append*(this: FormData, name: string, svalue: string, filename = "") = + this.entries.add(FormDataEntry( + name: name, + isstr: true, + svalue: svalue, + filename: filename + )) + +proc append*(this: FormData, name: string, value: Blob, + filename = "blob") = + this.entries.add(FormDataEntry( + name: name, + isstr: false, + value: value, + filename: filename + )) + +#TODO hack +proc append(this: FormData, name: string, value: JSObject, + filename = none(string)) {.jsfunc.} = + let blob = fromJS[Blob](value.ctx, value.val) + if blob.isSome: + this.append(name, blob.get, filename.get("blob")) + else: + let s = fromJS[string](value.ctx, value.val) + # toString should never fail (?) + this.append(name, s.get, filename.get("")) + +proc delete(this: FormData, name: string) {.jsfunc.} = + for i in countdown(this.entries.high, 0): + if this.entries[i].name == name: + this.entries.delete(i) + +proc get(ctx: JSContext, this: FormData, name: string): JSValue {.jsfunc.} = + for entry in this.entries: + if entry.name == name: + if entry.isstr: + return toJS(ctx, entry.svalue) + else: + return toJS(ctx, entry.value) + return JS_NULL + +proc getAll(this: FormData, name: string): seq[Blob] {.jsfunc.} = + for entry in this.entries: + if entry.name == name: + result.add(entry.value) # may be null + +proc add(list: var seq[FormDataEntry], entry: tuple[name, value: string]) = + list.add(FormDataEntry( + name: entry.name, + isstr: true, + svalue: entry.value + )) + +func toNameValuePairs*(list: seq[FormDataEntry]): + seq[tuple[name, value: string]] = + for entry in list: + if entry.isstr: + result.add((entry.name, entry.svalue)) + else: + result.add((entry.name, entry.name)) + +# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set +proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil, + encoding: string = ""): Option[seq[FormDataEntry]] = + if form.constructingentrylist: + return + form.constructingentrylist = true + + var entrylist: seq[FormDataEntry] + for field in form.controls: + if field.findAncestor({TAG_DATALIST}) != nil or + field.attrb("disabled") or + field.isButton() and Element(field) != submitter: + continue + + if field.tagType == TAG_INPUT: + let field = HTMLInputElement(field) + if field.inputType == INPUT_IMAGE: + let name = if field.attr("name") != "": + field.attr("name") & '.' + else: + "" + entrylist.add((name & 'x', $field.xcoord)) + entrylist.add((name & 'y', $field.ycoord)) + continue + + #TODO custom elements + + let name = field.attr("name") + + if name == "": + continue + + if field.tagType == TAG_SELECT: + let field = HTMLSelectElement(field) + for option in field.options: + if option.selected or option.disabled: + entrylist.add((name, option.value)) + elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}: + let value = if field.attr("value") != "": + field.attr("value") + else: + "on" + entrylist.add((name, value)) + elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE: + #TODO file + discard + elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"): + let charset = if encoding != "": + encoding + else: + "UTF-8" + entrylist.add((name, charset)) + else: + case field.tagType + of TAG_INPUT: + entrylist.add((name, HTMLInputElement(field).value)) + of TAG_BUTTON: + entrylist.add((name, HTMLButtonElement(field).value)) + of TAG_TEXTAREA: + entrylist.add((name, HTMLTextAreaElement(field).value)) + else: assert false, "Tag type " & $field.tagType & " not accounted for in constructEntryList" + if field.tagType == TAG_TEXTAREA or + field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}: + if field.attr("dirname") != "": + let dirname = field.attr("dirname") + let dir = "ltr" #TODO bidi + entrylist.add((dirname, dir)) + + form.constructingentrylist = false + return some(entrylist) + +proc addFormDataModule*(ctx: JSContext) = + ctx.registerType(FormData) |