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/painter import img/png import img/path import io/loader import io/request import io/window 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 FormMethod* = enum FORM_METHOD_GET, FORM_METHOD_POST, FORM_METHOD_DIALOG FormEncodingType* = enum FORM_ENCODING_TYPE_URLENCODED = "application/x-www-form-urlencoded", FORM_ENCODING_TYPE_MULTIPART = "multipart/form-data", FORM_ENCODING_TYPE_TEXT_PLAIN = "text/plain" QuirksMode* = enum NO_QUIRKS, QUIRKS, LIMITED_QUIRKS Namespace* = enum NO_NAMESPACE = "", HTML = "http://www.w3.org/1999/xhtml", MATHML = "http://www.w3.org/1998/Math/MathML", SVG = "http://www.w3.org/2000/svg", XLINK = "http://www.w3.org/1999/xlink", XML = "http://www.w3.org/XML/1998/namespace", XMLNS = "http://www.w3.org/2000/xmlns/" ScriptType = enum NO_SCRIPTTYPE, CLASSIC, MODULE, IMPORTMAP ParserMetadata = enum PARSER_INSERTED, NOT_PARSER_INSERTED ScriptResultType = enum RESULT_NULL, RESULT_UNINITIALIZED, RESULT_SCRIPT, RESULT_IMPORT_MAP_PARSE type Script = object #TODO setings baseURL: URL options: ScriptOptions mutedErrors: bool #TODO parse error/error to rethrow record: string #TODO should be a record... ScriptOptions = object nonce: string integrity: string parserMetadata: ParserMetadata credentialsMode: CredentialsMode referrerPolicy: Option[ReferrerPolicy] renderBlocking: bool ScriptResult = object case t: ScriptResultType of RESULT_NULL, RESULT_UNINITIALIZED: discard of RESULT_SCRIPT: script: Script of RESULT_IMPORT_MAP_PARSE: discard #TODO type Window* = ref object attrs*: WindowAttributes console* {.jsget.}: console navigator* {.jsget.}: Navigator settings*: EnvironmentSettings loader*: Option[FileLoader] jsrt*: JSRuntime jsctx*: JSContext document* {.jsget.}: Document timeouts*: TimeoutState[int] # Navigator stuff Navigator* = ref object plugins: PluginArray PluginArray* = ref object MimeTypeArray* = ref object # "For historical reasons, console is lowercased." # Also, for a more practical reason: so the javascript macros don't confuse # this and the Client console. # TODO: merge those two console* = ref object err*: Stream NamedNodeMap = ref object element: Element attrlist: seq[Attr] EnvironmentSettings* = object scripting*: bool EventTarget* = ref object of RootObj Collection = ref CollectionObj CollectionObj = object of RootObj islive: bool childonly: bool root: Node match: proc(node: Node): bool {.noSideEffect.} snapshot: seq[Node] livelen: int id: int NodeList = ref object of Collection HTMLCollection = ref object of Collection DOMTokenList = ref object toks*: seq[string] element: Element localName: string Node* = ref object of EventTarget nodeType*: NodeType childList*: seq[Node] parentNode* {.jsget.}: Node parentElement* {.jsget.}: Element root: Node document*: Document index*: int # Index in parents children. -1 for nodes without a parent. # Live collection cache: ids of live collections are saved in all # nodes they refer to. These are removed when the collection is destroyed, # and invalidated when the owner node's children or attributes change. # (We can't just store pointers, because those may be invalidated by # the JavaScript finalizers.) liveCollections: HashSet[int] Attr* = ref object of Node namespaceURI* {.jsget.}: string prefix* {.jsget.}: string localName* {.jsget.}: string value* {.jsget.}: string ownerElement* {.jsget.}: Element DOMImplementation = ref object document: Document Document* = ref object of Node charset*: Charset window*: Window url*: URL #TODO expose as URL (capitalized) location* {.jsget.}: URL #TODO should be location mode*: QuirksMode currentScript: HTMLScriptElement isxml*: bool implementation {.jsget.}: DOMImplementation origin: Origin scriptsToExecSoon*: seq[HTMLScriptElement] scriptsToExecInOrder*: Deque[HTMLScriptElement] scriptsToExecOnLoad*: Deque[HTMLScriptElement] parserBlockingScript*: HTMLScriptElement parser_cannot_change_the_mode_flag*: bool is_iframe_srcdoc*: bool focus*: Element contentType* {.jsget.}: string renderBlockingElements: seq[Element] invalidCollections: HashSet[int] # collection ids colln: int cachedSheets: seq[CSSStylesheet] cachedSheetsInvalid: bool CharacterData* = ref object of Node data* {.jsget.}: string Text* = ref object of CharacterData Comment* = ref object of CharacterData CDATASection = ref object of CharacterData ProcessingInstruction = ref object of CharacterData target {.jsget.}: string DocumentFragment* = ref object of Node host*: Element DocumentType* = ref object of Node name*: string publicId*: string systemId*: string Element* = ref object of Node namespace*: Namespace namespacePrefix*: Option[string] prefix*: string localName*: string tagType*: TagType id* {.jsget.}: string classList* {.jsget.}: DOMTokenList attrs*: Table[string, string] attributes* {.jsget.}: NamedNodeMap hover*: bool invalid*: bool HTMLElement* = ref object of Element FormAssociatedElement* = ref object of HTMLElement parserInserted*: bool HTMLInputElement* = ref object of FormAssociatedElement form* {.jsget.}: HTMLFormElement inputType*: InputType value* {.jsget.}: string checked*: bool xcoord*: int ycoord*: int file*: Option[Url] HTMLAnchorElement* = ref object of HTMLElement HTMLSelectElement* = ref object of FormAssociatedElement form* {.jsget.}: HTMLFormElement HTMLSpanElement* = ref object of HTMLElement HTMLOptGroupElement* = ref object of HTMLElement HTMLOptionElement* = ref object of HTMLElement selected*: bool HTMLHeadingElement* = ref object of HTMLElement rank*: uint16 HTMLBRElement* = ref object of HTMLElement HTMLMenuElement* = ref object of HTMLElement HTMLUListElement* = ref object of HTMLElement HTMLOListElement* = ref object of HTMLElement start*: Option[int] HTMLLIElement* = ref object of HTMLElement value* {.jsget.}: Option[int] HTMLStyleElement* = ref object of HTMLElement sheet*: CSSStylesheet HTMLLinkElement* = ref object of HTMLElement sheet*: CSSStylesheet HTMLFormElement* = ref object of HTMLElement name*: string smethod*: string enctype*: string novalidate*: bool constructingentrylist*: bool controls*: seq[FormAssociatedElement] HTMLTemplateElement* = ref object of HTMLElement content*: DocumentFragment HTMLUnknownElement* = ref object of HTMLElement HTMLScriptElement* = ref object of HTMLElement parserDocument*: Document preparationTimeDocument*: Document forceAsync*: bool fromAnExternalFile*: bool readyForParserExec*: bool alreadyStarted*: bool delayingTheLoadEvent: bool ctype: ScriptType internalNonce: string scriptResult*: ScriptResult onReady: (proc()) HTMLBaseElement* = ref object of HTMLElement HTMLAreaElement* = ref object of HTMLElement HTMLButtonElement* = ref object of FormAssociatedElement form* {.jsget.}: HTMLFormElement ctype*: ButtonType value* {.jsget, jsset.}: string HTMLTextAreaElement* = ref object of FormAssociatedElement form* {.jsget.}: HTMLFormElement value* {.jsget.}: string 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*(jctx: JSContext, target: HTMLCanvasElement, options: Option[JSValue]): 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_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] for tag in ts: tags.incl(tag) tags func makes(name: string, ts: set[TagType]): ReflectEntry = ReflectEntry( attrname: name, funcname: name, t: REFLECT_STR, tags: ts ) func makes(attrname: string, funcname: string, ts: set[TagType]): ReflectEntry = ReflectEntry( attrname: attrname, funcname: funcname, t: REFLECT_STR, tags: ts ) func makes(name: string, ts: varargs[TagType]): ReflectEntry = makes(name, toset(ts)) func makes(attrname, funcname: string, ts: varargs[TagType]): ReflectEntry = makes(attrname, funcname, toset(ts)) template makeb(name: string, ts: varargs[TagType]): ReflectEntry = ReflectEntry( attrname: name, funcname: name, t: REFLECT_BOOL, tags: toset(ts) ) 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 makes("target", TAG_A, TAG_AREA, TAG_LABEL, TAG_LINK), makes("href", TAG_LINK), makeb("required", TAG_INPUT, TAG_SELECT, TAG_TEXTAREA), makes("rel", "relList", TAG_A, TAG_LINK, TAG_LABEL), makes("for", "htmlFor", TAG_LABEL), makeul("cols", TAG_TEXTAREA, 20u32), makeul("rows", TAG_TEXTAREA, 1u32), #