import std/deques import std/math import std/options import std/sets import std/streams import std/strutils import std/tables import css/cssparser import css/mediaquery import css/sheet import css/values import html/catom import html/enums import html/event import html/script import img/bitmap import img/painter import img/path import img/png import io/promise import js/console import js/domexception import js/error import js/fromjs import js/javascript import js/opaque import js/propertyenumlist import js/timeout import js/tojs import loader/loader import loader/request import types/blob import types/color import types/matrix import types/referrer import types/url import types/vector import types/winattrs import utils/mimeguess import utils/strwidth import utils/twtstr import chagashi/charset import chagashi/decoder import chagashi/validator 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" type DocumentReadyState* = enum rsLoading = "loading" rsInteractive = "interactive" rsComplete = "complete" type Location = ref object window: Window Window* = ref object of EventTarget attrs*: WindowAttributes console* {.jsget.}: Console navigator* {.jsget.}: Navigator screen* {.jsget.}: Screen settings*: EnvironmentSettings loader*: Option[FileLoader] location* {.jsget.}: Location jsrt*: JSRuntime jsctx*: JSContext document* {.jsufget.}: Document timeouts*: TimeoutState navigate*: proc(url: URL) importMapsAllowed*: bool factory*: CAtomFactory loadingResourcePromises*: seq[EmptyPromise] images*: bool # Navigator stuff Navigator* = object plugins: PluginArray PluginArray* = object MimeTypeArray* = object Screen* = object NamedNodeMap = ref object element: Element attrlist: seq[Attr] 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 HTMLAllCollection = ref object of Collection DOMTokenList = ref object toks*: seq[CAtom] element: Element localName: CAtom DOMStringMap = object target {.cursor.}: HTMLElement Node* = ref object of EventTarget childList*: seq[Node] parentNode* {.jsget.}: Node root: Node 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] childNodes_cached: NodeList document_internal: Document # not nil Attr* = ref object of Node dataIdx: int ownerElement*: Element DOMImplementation = object document: Document DocumentWriteBuffer* = ref object data*: string i*: int Document* = ref object of Node factory*: CAtomFactory charset*: Charset window* {.jsget: "defaultView".}: Window url* {.jsget: "URL".}: URL mode*: QuirksMode currentScript: HTMLScriptElement isxml*: bool implementation {.jsget.}: DOMImplementation origin: Origin readyState* {.jsget.}: DocumentReadyState # document.write ignoreDestructiveWrites: int throwOnDynamicMarkupInsertion: int activeParserWasAborted: bool writeBuffers*: seq[DocumentWriteBuffer] 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 all_cached: HTMLAllCollection cachedSheets: seq[CSSStylesheet] cachedSheetsInvalid*: bool children_cached: HTMLCollection #TODO I hate this but I really don't want to put chadombuilder into dom too parser*: pointer 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 children_cached*: HTMLCollection DocumentType* = ref object of Node name*: string publicId*: string systemId*: string AttrData* = object qualifiedName*: CAtom localName*: CAtom prefix*: CAtom namespace*: CAtom value*: string Element* = ref object of Node namespace*: Namespace namespacePrefix*: NamespacePrefix prefix*: string localName*: CAtom id*: CAtom name*: CAtom classList* {.jsget.}: DOMTokenList attrs: seq[AttrData] attributesInternal: NamedNodeMap hover*: bool invalid*: bool style_cached*: CSSStyleDeclaration children_cached: HTMLCollection AttrDummyElement = ref object of Element CSSStyleDeclaration* = ref object decls*: seq[CSSDeclaration] element: Element HTMLElement* = ref object of Element dataset {.jsget.}: DOMStringMap FormAssociatedElement* = ref object of HTMLElement form*: HTMLFormElement parserInserted*: bool HTMLInputElement* = ref object of FormAssociatedElement inputType*: InputType value* {.jsget.}: string checked* {.jsget.}: bool xcoord*: int ycoord*: int file*: Option[URL] HTMLAnchorElement* = ref object of HTMLElement relList {.jsget.}: DOMTokenList HTMLSelectElement* = ref object of FormAssociatedElement 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 relList {.jsget.}: DOMTokenList fetchStarted: bool HTMLFormElement* = ref object of HTMLElement smethod*: string enctype*: string novalidate*: bool constructingEntryList*: bool controls*: seq[FormAssociatedElement] relList {.jsget.}: DOMTokenList HTMLTemplateElement* = ref object of HTMLElement content*: DocumentFragment HTMLUnknownElement* = ref object of HTMLElement HTMLScriptElement* = ref object of HTMLElement parserDocument*: Document preparationTimeDocument*: Document forceAsync*: bool external*: 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 relList {.jsget.}: DOMTokenList HTMLButtonElement* = ref object of FormAssociatedElement ctype*: ButtonType value* {.jsget, jsset.}: string HTMLTextAreaElement* = ref object of FormAssociatedElement 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 fetchStarted: bool HTMLVideoElement* = ref object of HTMLElement HTMLAudioElement* = ref object of HTMLElement jsDestructor(Navigator) jsDestructor(PluginArray) jsDestructor(MimeTypeArray) jsDestructor(Screen) jsDestructor(Window) 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(HTMLVideoElement) jsDestructor(HTMLAudioElement) jsDestructor(Node) jsDestructor(NodeList) jsDestructor(HTMLCollection) jsDestructor(HTMLAllCollection) jsDestructor(Location) jsDestructor(Document) jsDestructor(DOMImplementation) jsDestructor(DOMTokenList) jsDestructor(DOMStringMap) jsDestructor(Comment) jsDestructor(CDATASection) jsDestructor(DocumentFragment) jsDestructor(ProcessingInstruction) jsDestructor(CharacterData) jsDestructor(Text) jsDestructor(DocumentType) jsDestructor(Attr) jsDestructor(NamedNodeMap) jsDestructor(CanvasRenderingContext2D) jsDestructor(TextMetrics) jsDestructor(CSSStyleDeclaration) 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: StaticAtom funcname: string tags: set[TagType] case t: ReflectType of REFLECT_LONG: i: int32 of REFLECT_ULONG, REFLECT_ULONG_GZ: u: uint32 else: discard func attrType0(s: static string): StaticAtom = return parseEnum[StaticAtom](s) template toset(ts: openarray[TagType]): set[TagType] = var tags: system.set[TagType] for tag in ts: tags.incl(tag) tags func makes(name: static string, ts: set[TagType]): ReflectEntry = const attrname = attrType0(name) ReflectEntry( attrname: attrname, funcname: name, t: REFLECT_STR, tags: ts ) func makes(attrname, funcname: static string, ts: set[TagType]): ReflectEntry = const attrname = attrType0(attrname) ReflectEntry( attrname: attrname, funcname: funcname, t: REFLECT_STR, tags: ts ) func makes(name: static string, ts: varargs[TagType]): ReflectEntry = makes(name, toset(ts)) func makes(attrname, funcname: static string, ts: varargs[TagType]): ReflectEntry = makes(attrname, funcname, toset(ts)) func makeb(attrname, funcname: static string, ts: varargs[TagType]): ReflectEntry = const attrname = attrType0(attrname) ReflectEntry( attrname: attrname, funcname: funcname, t: REFLECT_BOOL, tags: toset(ts) ) func makeb(name: static string, ts: varargs[TagType]): ReflectEntry = makeb(name, name, ts) func makeul(name: static string, ts: varargs[TagType], default = 0u32): ReflectEntry = const attrname = attrType0(name) ReflectEntry( attrname: attrname, funcname: name, t: REFLECT_ULONG, tags: toset(ts), u: default ) func makeulgz(name: static string, ts: varargs[TagType], default = 0u32): ReflectEntry = const attrname = attrType0(name) ReflectEntry( attrname: attrname, 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", TAG_A, TAG_LINK, TAG_LABEL), makes("for", "htmlFor", TAG_LABEL), makeul("cols", TAG_TEXTAREA, 20u32), makeul("rows", TAG_TEXTAREA, 1u32), # func jsForm(this: HTMLInputElement): HTMLFormElement {.jsfget: "form".} = return this.form #