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 html/event import img/bitmap import img/painter import img/path import img/png import io/loader import io/request import io/window import js/domexception import js/error import js/fromjs import js/javascript import js/opaque import js/timeout import js/tojs import types/blob import types/color import types/matrix import types/referer import types/url import types/vector import utils/mimeguess import utils/twtstr import chakasu/charset import chakasu/decoderstream import chame/tags 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" 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 Location = ref object window: Window Window* = ref object of EventTarget attrs*: WindowAttributes console* {.jsget.}: console navigator* {.jsget.}: Navigator settings*: EnvironmentSettings loader*: Option[FileLoader] location* {.jsget.}: Location jsrt*: JSRuntime jsctx*: JSContext document* {.jsufget.}: Document timeouts*: TimeoutState[int] navigate*: proc(url: URL) # Navigator stuff Navigator* = object plugins: PluginArray PluginArray* = object MimeTypeArray* = 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 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] children_cached: HTMLCollection childNodes_cached: NodeList Attr* = ref object of Node namespaceURI* {.jsget.}: string prefix* {.jsget.}: string localName* {.jsget.}: string value* {.jsget.}: string ownerElement* {.jsget.}: Element DOMImplementation = object document: Document Document* = ref object of Node charset*: Charset window*: Window url* {.jsget: "URL".}: URL 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[int32] 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 HTMLImageElement* = ref object of HTMLElement bitmap*: Bitmap jsDestructor(Navigator) jsDestructor(PluginArray) jsDestructor(MimeTypeArray) jsDestructor(Window) jsDestructor(console) jsDestructor(Element) jsDestructor(HTMLElement) jsDestructor(HTMLInputElement) jsDestructor(HTMLAnchorElement) jsDestructor(HTMLSelectElement) jsDestructor(HTMLSpanElement) jsDestructor(HTMLOptGroupElement) jsDestructor(HTMLOptionElement) jsDestructor(HTMLHeadingElement) jsDestructor(HTMLBRElement) jsDestructor(HTMLMenuElement) jsDestructor(HTMLUListElement) jsDestructor(HTMLOListElement) jsDestructor(HTMLLIElement) jsDestructor(HTMLStyleElement) jsDestructor(HTMLLinkElement) jsDestructor(HTMLFormElement) jsDestructor(HTMLTemplateElement) jsDestructor(HTMLUnknownElement) jsDestructor(HTMLScriptElement) jsDestructor(HTMLBaseElement) jsDestructor(HTMLAreaElement) jsDestructor(HTMLButtonElement) jsDestructor(HTMLTextAreaElement) jsDestructor(HTMLLabelElement) jsDestructor(HTMLCanvasElement) jsDestructor(HTMLImageElement) jsDestructor(Node) jsDestructor(NodeList) jsDestructor(HTMLCollection) jsDestructor(Location) jsDestructor(Document) jsDestructor(DOMImplementation) jsDestructor(DOMTokenList) jsDestructor(Comment) jsDestructor(CDATASection) jsDestructor(DocumentFragment) jsDestructor(ProcessingInstruction) jsDestructor(CharacterData) jsDestructor(Text) jsDestructor(DocumentType) jsDestructor(Attr) jsDestructor(NamedNodeMap) jsDestructor(CanvasRenderingContext2D) jsDestructor(TextMetrics) 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): Err[DOMException] {.jsfunc.} = return ctx.state.path.arcTo(x1, y1, x2, y2, radius) proc arc(ctx: CanvasRenderingContext2D, x, y, radius, startAngle, endAngle: float64, counterclockwise = false): Err[DOMException] {.jsfunc.} = return ctx.state.path.arc(x, y, radius, startAngle, endAngle, counterclockwise) proc ellipse(ctx: CanvasRenderingContext2D, x, y, radiusX, radiusY, rotation, startAngle, endAngle: float64, counterclockwise = false): Err[DOMException] {.jsfunc.} = return ctx.state.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) 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)) func makeb(attrname, funcname: string, ts: varargs[TagType]): ReflectEntry = ReflectEntry( attrname: attrname, funcname: funcname, t: REFLECT_BOOL, tags: toset(ts) ) func makeb(name: string, ts: varargs[TagType]): ReflectEntry = makeb(name, name, 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), #