diff options
author | bptato <nincsnevem662@gmail.com> | 2024-01-15 20:48:29 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-02-07 22:21:48 +0100 |
commit | 168bd542d989c76ce3ff09a29b8d77af448c3c12 (patch) | |
tree | e71ccd2bbce8da91caae46edf5df57f157a45d15 | |
parent | 0beb8385d88c20efbd00b7f9e6ac57bf19a109c7 (diff) | |
download | chawan-168bd542d989c76ce3ff09a29b8d77af448c3c12.tar.gz |
Update chame
* Update chame to the latest version * Get rid of nodeType usage * Add atoms * Re-implement DOM attributes * document.write
m--------- | lib/chame | 0 | ||||
-rw-r--r-- | src/css/cascade.nim | 8 | ||||
-rw-r--r-- | src/css/match.nim | 9 | ||||
-rw-r--r-- | src/css/selectorparser.nim | 58 | ||||
-rw-r--r-- | src/css/sheet.nim | 60 | ||||
-rw-r--r-- | src/html/catom.nim | 60 | ||||
-rw-r--r-- | src/html/chadombuilder.nim | 371 | ||||
-rw-r--r-- | src/html/dom.nim | 1022 | ||||
-rw-r--r-- | src/html/enums.nim | 22 | ||||
-rw-r--r-- | src/html/env.nim | 8 | ||||
-rw-r--r-- | src/server/buffer.nim | 78 | ||||
-rw-r--r-- | src/xhr/formdata.nim | 2 | ||||
-rw-r--r-- | todo | 5 |
13 files changed, 1092 insertions, 611 deletions
diff --git a/lib/chame b/lib/chame -Subproject 661017722195cb87a92ccd5123a4f919210d7a1 +Subproject 946af631dc066b4cd291fc5a96ee0db4b075866 diff --git a/src/css/cascade.nim b/src/css/cascade.nim index f9422a19..4df558c0 100644 --- a/src/css/cascade.nim +++ b/src/css/cascade.nim @@ -91,7 +91,7 @@ proc calcRule(tosorts: var ToSorts, styledNode: StyledNode, rule: CSSRuleDef) = func calcRules(styledNode: StyledNode, sheet: CSSStylesheet): DeclarationList = var tosorts: ToSorts let elem = Element(styledNode.node) - for rule in sheet.genRules(elem.tagType, elem.id, elem.classList.toks): + for rule in sheet.genRules(elem.localName, elem.id, elem.classList.toks): tosorts.calcRule(styledNode, rule) for i in PseudoElem: tosorts[i].sort((proc(x, y: (int, seq[CSSDeclaration])): int = @@ -383,12 +383,12 @@ proc applyRulesFrameInvalid(frame: CascadeFrame, ua, user: CSSStylesheet, else: assert child != nil if styledParent != nil: - if child.nodeType == ELEMENT_NODE: + if child of Element: styledChild = styledParent.newStyledElement(Element(child)) styledParent.children.add(styledChild) declmap = styledChild.calcRules(ua, user, author) applyStyle(styledParent, styledChild, declmap) - elif child.nodeType == TEXT_NODE: + elif child of Text: let text = Text(child) styledChild = styledParent.newStyledText(text) styledParent.children.add(styledChild) @@ -475,7 +475,7 @@ proc appendChildren(styledStack: var seq[CascadeFrame], frame: CascadeFrame, styledStack.stackAppend(frame, styledChild, PSEUDO_NEWLINE, idx) else: for i in countdown(elem.childList.high, 0): - if elem.childList[i].nodeType in {ELEMENT_NODE, TEXT_NODE}: + if elem.childList[i] of Element or elem.childList[i] of Text: styledStack.stackAppend(frame, styledChild, elem.childList[i], idx) if elem.tagType == TAG_INPUT: styledStack.stackAppend(frame, styledChild, PSEUDO_INPUT_TEXT, idx) diff --git a/src/css/match.nim b/src/css/match.nim index bf78f6b5..0d1f507f 100644 --- a/src/css/match.nim +++ b/src/css/match.nim @@ -6,6 +6,7 @@ import std/tables import css/cssparser import css/selectorparser import css/stylednode +import html/catom import html/dom import utils/twtstr @@ -148,9 +149,7 @@ func selectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T = n let elem = Element(selem.node) case sel.t of TYPE_SELECTOR: - return elem.tagType == sel.tag - of UNKNOWN_TYPE_SELECTOR: - return elem.localName == sel.tagstr + return elem.localName == sel.tag of CLASS_SELECTOR: return sel.class in elem.classList of ID_SELECTOR: @@ -232,14 +231,14 @@ func selectorsMatch*[T: Element|StyledNode](elem: T, cxsel: ComplexSelector, fel return elem.complexSelectorMatches(cxsel, felem) proc querySelectorAll(node: Node, q: string): seq[Element] = - let selectors = parseSelectors(newStringStream(q)) + let selectors = parseSelectors(newStringStream(q), node.document.factory) for element in node.elements: if element.selectorsMatch(selectors): result.add(element) doqsa = (proc(node: Node, q: string): seq[Element] = querySelectorAll(node, q)) proc querySelector(node: Node, q: string): Element = - let selectors = parseSelectors(newStringStream(q)) + let selectors = parseSelectors(newStringStream(q), node.document.factory) for element in node.elements: if element.selectorsMatch(selectors): return element diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim index 75f6182a..9e79a36d 100644 --- a/src/css/selectorparser.nim +++ b/src/css/selectorparser.nim @@ -3,14 +3,13 @@ import std/streams import std/strutils import css/cssparser +import html/catom import utils/twtstr -import chame/tags - type SelectorType* = enum - TYPE_SELECTOR, UNKNOWN_TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, - CLASS_SELECTOR, UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR + TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, CLASS_SELECTOR, + UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR PseudoElem* = enum PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER, @@ -32,6 +31,7 @@ type cvals: seq[CSSComponentValue] at: int failed: bool + factory: CAtomFactory RelationType* {.size: sizeof(int) div 2.} = enum RELATION_EXISTS, RELATION_EQUALS, RELATION_TOKEN, RELATION_BEGIN_DASH, @@ -47,9 +47,9 @@ type Selector* = ref object # Simple selector case t*: SelectorType of TYPE_SELECTOR: - tag*: TagType - of UNKNOWN_TYPE_SELECTOR: - tagstr*: string + tag*: CAtom + when defined(debug): + tags: string of ID_SELECTOR: id*: string of ATTR_SELECTOR: @@ -109,9 +109,10 @@ func `$`*(cxsel: ComplexSelector): string func `$`*(sel: Selector): string = case sel.t of TYPE_SELECTOR: - return tagName(sel.tag) - of UNKNOWN_TYPE_SELECTOR: - return sel.tagstr + when defined(debug): + return sel.tags + else: + return "tagt " & $int(sel.tag) of ID_SELECTOR: return '#' & sel.id of ATTR_SELECTOR: @@ -205,7 +206,7 @@ func getSpecificity(sel: Selector): int = result += 1000 of PSEUDO_WHERE: discard else: result += 1000 - of TYPE_SELECTOR, UNKNOWN_TYPE_SELECTOR, PSELEM_SELECTOR: + of TYPE_SELECTOR, PSELEM_SELECTOR: result += 1 of UNIVERSAL_SELECTOR: discard @@ -242,15 +243,17 @@ template get_tok(cval: CSSComponentValue): CSSToken = if not (c of CSSToken): fail CSSToken(c) -proc parseSelectorList(cvals: seq[CSSComponentValue]): SelectorList +proc parseSelectorList(cvals: seq[CSSComponentValue], factory: CAtomFactory): + SelectorList # Functions that may contain other selectors, functions, etc. -proc parseRecursiveSelectorFunction(state: var SelectorParser, class: PseudoClass, body: seq[CSSComponentValue]): Selector = +proc parseRecursiveSelectorFunction(state: var SelectorParser, + class: PseudoClass, body: seq[CSSComponentValue]): Selector = var fun = Selector( t: PSEUDO_SELECTOR, pseudo: PseudoData(t: class), ) - fun.pseudo.fsels = parseSelectorList(body) + fun.pseudo.fsels = parseSelectorList(body, state.factory) if fun.pseudo.fsels.len == 0: fail return fun @@ -266,7 +269,8 @@ proc parseNthChild(state: var SelectorParser, cssfunction: CSSFunction, data: Ps return nthchild if not (get_tok cssfunction.value[i]).value.equalsIgnoreCase("of"): fail if i == cssfunction.value.len: fail - nthchild.pseudo.ofsels = parseSelectorList(cssfunction.value[i..^1]) + nthchild.pseudo.ofsels = parseSelectorList(cssfunction.value[i..^1], + state.factory) if nthchild.pseudo.ofsels.len == 0: fail return nthchild @@ -396,6 +400,7 @@ proc parseClassSelector(state: var SelectorParser): Selector = return Selector(t: CLASS_SELECTOR, class: tok.value) proc parseCompoundSelector(state: var SelectorParser): CompoundSelector = + result = CompoundSelector() while state.has(): let cval = state.peek() if cval of CSSToken: @@ -403,12 +408,8 @@ proc parseCompoundSelector(state: var SelectorParser): CompoundSelector = case tok.tokenType of CSS_IDENT_TOKEN: inc state.at - let s = tok.value.toLowerAscii() - let tag = tagType(s) - if tag == TAG_UNKNOWN: - result.add(Selector(t: UNKNOWN_TYPE_SELECTOR, tagstr: s)) - else: - result.add(Selector(t: TYPE_SELECTOR, tag: tag)) + let tag = state.factory.toAtom(tok.value.toLowerAscii()) + result.add(Selector(t: TYPE_SELECTOR, tag: tag)) of CSS_COLON_TOKEN: inc state.at result.add(state.parsePseudoSelector()) @@ -466,16 +467,19 @@ proc parseComplexSelector(state: var SelectorParser): ComplexSelector = if result.len == 0 or result[^1].ct != NO_COMBINATOR: fail -proc parseSelectorList(cvals: seq[CSSComponentValue]): SelectorList = - var state = SelectorParser(cvals: cvals) +proc parseSelectorList(cvals: seq[CSSComponentValue], factory: CAtomFactory): + SelectorList = + var state = SelectorParser(cvals: cvals, factory: factory) var res: SelectorList while state.has(): res.add(state.parseComplexSelector()) if not state.failed: return res -func parseSelectors*(cvals: seq[CSSComponentValue]): seq[ComplexSelector] = {.cast(noSideEffect).}: - return parseSelectorList(cvals) +proc parseSelectors*(cvals: seq[CSSComponentValue], factory: CAtomFactory): + seq[ComplexSelector] = + return parseSelectorList(cvals, factory) -proc parseSelectors*(stream: Stream): seq[ComplexSelector] = - return parseSelectors(parseListOfComponentValues(stream)) +proc parseSelectors*(stream: Stream, factory: CAtomFactory): + seq[ComplexSelector] = + return parseSelectors(parseListOfComponentValues(stream), factory) diff --git a/src/css/sheet.nim b/src/css/sheet.nim index 2db7b47a..4990d991 100644 --- a/src/css/sheet.nim +++ b/src/css/sheet.nim @@ -2,11 +2,10 @@ import std/algorithm import std/streams import std/tables -import css/mediaquery import css/cssparser +import css/mediaquery import css/selectorparser - -import chame/tags +import html/catom type CSSRuleBase* = ref object of RootObj @@ -26,23 +25,27 @@ type CSSStylesheet* = ref object mqList*: seq[CSSMediaQueryDef] - tagTable: array[TagType, seq[CSSRuleDef]] + #TODO maybe just array[TagType] would be more efficient + tagTable: Table[CAtom, seq[CSSRuleDef]] idTable: Table[string, seq[CSSRuleDef]] classTable: Table[string, seq[CSSRuleDef]] generalList: seq[CSSRuleDef] len: int + factory: CAtomFactory type SelectorHashes = object - tag: TagType + tag: CAtom id: string class: string -func newStylesheet*(cap: int): CSSStylesheet = +func newStylesheet*(cap: int, factory: CAtomFactory): CSSStylesheet = let bucketsize = cap div 2 return CSSStylesheet( + tagTable: initTable[CAtom, seq[CSSRuleDef]](bucketsize), idTable: initTable[string, seq[CSSRuleDef]](bucketsize), classTable: initTable[string, seq[CSSRuleDef]](bucketsize), - generalList: newSeqOfCap[CSSRuleDef](bucketsize) + generalList: newSeqOfCap[CSSRuleDef](bucketsize), + factory: factory ) proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool @@ -66,7 +69,7 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool = of ID_SELECTOR: hashes.id = sel.id return true - of ATTR_SELECTOR, PSELEM_SELECTOR, UNIVERSAL_SELECTOR, UNKNOWN_TYPE_SELECTOR: + of ATTR_SELECTOR, PSELEM_SELECTOR, UNIVERSAL_SELECTOR: return false of PSEUDO_SELECTOR: if sel.pseudo.t in {PSEUDO_IS, PSEUDO_WHERE}: @@ -88,9 +91,9 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool = while i < sel.pseudo.fsels.len: var nhashes: SelectorHashes nhashes.getSelectorIds(sel.pseudo.fsels[i]) - if hashes.tag == TAG_UNKNOWN: + if hashes.tag == CAtomNull: hashes.tag = nhashes.tag - elif not cancel_tag and nhashes.tag != TAG_UNKNOWN and nhashes.tag != hashes.tag: + elif not cancel_tag and nhashes.tag != CAtomNull and nhashes.tag != hashes.tag: cancel_tag = true if hashes.id == "": @@ -106,23 +109,24 @@ proc getSelectorIds(hashes: var SelectorHashes, sel: Selector): bool = inc i if cancel_tag: - hashes.tag = TAG_UNKNOWN + hashes.tag = CAtomNull if cancel_id: hashes.id = "" if cancel_class: hashes.class = "" - if hashes.tag != TAG_UNKNOWN or hashes.id != "" or hashes.class != "": + if hashes.tag != CAtomNull or hashes.id != "" or hashes.class != "": return true proc ruleDefCmp(a, b: CSSRuleDef): int = cmp(a.idx, b.idx) -iterator genRules*(sheet: CSSStylesheet, tag: TagType, id: string, +iterator genRules*(sheet: CSSStylesheet, tag: CAtom, id: string, classes: seq[string]): CSSRuleDef = var rules: seq[CSSRuleDef] - for rule in sheet.tagTable[tag]: - rules.add(rule) + sheet.tagTable.withValue(tag, v): + for rule in v[]: + rules.add(rule) if id != "": sheet.idTable.withValue(id, v): for rule in v[]: @@ -141,8 +145,11 @@ proc add(sheet: var CSSStylesheet, rule: CSSRuleDef) = var hashes: SelectorHashes for cxsel in rule.sels: hashes.getSelectorIds(cxsel) - if hashes.tag != TAG_UNKNOWN: - sheet.tagTable[hashes.tag].add(rule) + if hashes.tag != CAtomNull: + sheet.tagTable.withValue(hashes.tag, p): + p[].add(rule) + do: + sheet.tagTable[hashes.tag] = @[rule] elif hashes.id != "": sheet.idTable.withValue(hashes.id, p): p[].add(rule) @@ -158,8 +165,11 @@ proc add(sheet: var CSSStylesheet, rule: CSSRuleDef) = proc add*(sheet: var CSSStylesheet, sheet2: CSSStylesheet) = sheet.generalList.add(sheet2.generalList) - for tag in TagType: - sheet.tagTable[tag].add(sheet2.tagTable[tag]) + for key, value in sheet2.tagTable.pairs: + sheet.tagTable.withValue(key, p): + p[].add(value) + do: + sheet.tagTable[key] = value for key, value in sheet2.idTable.pairs: sheet.idTable.withValue(key, p): p[].add(value) @@ -172,7 +182,7 @@ proc add*(sheet: var CSSStylesheet, sheet2: CSSStylesheet) = sheet.classTable[key] = value proc addRule(stylesheet: var CSSStylesheet, rule: CSSQualifiedRule) = - let sels = parseSelectors(rule.prelude) + let sels = parseSelectors(rule.prelude, stylesheet.factory) if sels.len > 0: let r = CSSRuleDef( sels: sels, @@ -192,7 +202,7 @@ proc addAtRule(stylesheet: var CSSStylesheet, atrule: CSSAtRule) = let rules = atrule.oblock.value.parseListOfRules() if rules.len > 0: var media = CSSMediaQueryDef() - media.children = newStylesheet(rules.len) + media.children = newStylesheet(rules.len, stylesheet.factory) media.children.len = stylesheet.len media.query = query for rule in rules: @@ -204,13 +214,13 @@ proc addAtRule(stylesheet: var CSSStylesheet, atrule: CSSAtRule) = stylesheet.len = media.children.len else: discard #TODO -proc parseStylesheet*(s: Stream): CSSStylesheet = +proc parseStylesheet*(s: Stream, factory: CAtomFactory): CSSStylesheet = let css = parseCSS(s) - result = newStylesheet(css.value.len) + result = newStylesheet(css.value.len, factory) for v in css.value: if v of CSSAtRule: result.addAtRule(CSSAtRule(v)) else: result.addRule(CSSQualifiedRule(v)) s.close() -proc parseStylesheet*(s: string): CSSStylesheet = - return newStringStream(s).parseStylesheet() +proc parseStylesheet*(s: string, factory: CAtomFactory): CSSStylesheet = + return newStringStream(s).parseStylesheet(factory) diff --git a/src/html/catom.nim b/src/html/catom.nim new file mode 100644 index 00000000..62066663 --- /dev/null +++ b/src/html/catom.nim @@ -0,0 +1,60 @@ +import std/hashes + +import chame/tags + +#TODO use a better hash map +const CAtomFactoryStrMapLength = 1024 # must be a power of 2 +static: + doAssert (CAtomFactoryStrMapLength and (CAtomFactoryStrMapLength - 1)) == 0 + +type + CAtom* = distinct int + + #TODO could be a ptr probably + CAtomFactory* = ref object of RootObj + strMap: array[CAtomFactoryStrMapLength, seq[CAtom]] + atomMap: seq[string] + +const CAtomNull* = CAtom(0) + +# Mandatory Atom functions +func `==`*(a, b: CAtom): bool {.borrow.} +func hash*(atom: CAtom): Hash {.borrow.} + +func toAtom*(factory: CAtomFactory, s: string): CAtom + +proc newCAtomFactory*(): CAtomFactory = + const minCap = int(TagType.high) + 1 + let factory = CAtomFactory( + atomMap: newSeqOfCap[string](minCap), + ) + factory.atomMap.add("") # skip TAG_UNKNOWN + for tagType in TagType(int(TAG_UNKNOWN) + 1) .. TagType.high: + discard factory.toAtom($tagType) + return factory + +func toAtom*(factory: CAtomFactory, s: string): CAtom = + let h = s.hash() + let i = h and (factory.strMap.len - 1) + for atom in factory.strMap[i]: + if factory.atomMap[int(atom)] == s: + # Found + return atom + # Not found + let atom = CAtom(factory.atomMap.len) + factory.atomMap.add(s) + factory.strMap[i].add(atom) + return atom + +func toAtom*(factory: CAtomFactory, tagType: TagType): CAtom = + assert tagType != TAG_UNKNOWN + return CAtom(tagType) + +func toStr*(factory: CAtomFactory, atom: CAtom): string = + return factory.atomMap[int(atom)] + +func toTagType*(factory: CAtomFactory, atom: CAtom): TagType = + let i = int(atom) + if i > 0 and i <= int(high(TagType)): + return TagType(atom) + return TAG_UNKNOWN diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim index fa3b761d..21e94901 100644 --- a/src/html/chadombuilder.nim +++ b/src/html/chadombuilder.nim @@ -2,6 +2,7 @@ import std/deques import std/options import std/streams +import html/catom import html/dom import html/enums import js/error @@ -10,138 +11,167 @@ import js/javascript import types/url import chakasu/charset +import chakasu/decoderstream +import chakasu/encoderstream import chame/htmlparser -import chame/htmltokenizer import chame/tags # DOMBuilder implementation for Chawan. +type CharsetConfidence = enum + CONFIDENCE_TENTATIVE, CONFIDENCE_CERTAIN, CONFIDENCE_IRRELEVANT + type - ChaDOMBuilder = ref object of DOMBuilder[Node] + ChaDOMBuilder = ref object of DOMBuilder[Node, CAtom] + charset: Charset + confidence: CharsetConfidence + document: Document isFragment: bool + factory: CAtomFactory + poppedScript: HTMLScriptElement + +type + DOMBuilderImpl = ChaDOMBuilder + HandleImpl = Node + AtomImpl = CAtom + +include chame/htmlparseriface type DOMParser = ref object # JS interface jsDestructor(DOMParser) -template getDocument(dombuilder: ChaDOMBuilder): Document = - cast[Document](dombuilder.document) +proc getDocumentImpl(builder: ChaDOMBuilder): Node = + return builder.document + +proc atomToTagTypeImpl(builder: ChaDOMBuilder, atom: CAtom): TagType = + return builder.factory.toTagType(atom) + +proc tagTypeToAtomImpl(builder: ChaDOMBuilder, tagType: TagType): CAtom = + return builder.factory.toAtom(tagType) -proc finish(builder: DOMBuilder[Node]) = - let builder = cast[ChaDOMBuilder](builder) - let document = builder.getDocument() - while document.scriptsToExecOnLoad.len > 0: +proc strToAtomImpl(builder: ChaDOMBuilder, s: string): CAtom = + return builder.factory.toAtom(s) + +proc finish(builder: ChaDOMBuilder) = + while builder.document.scriptsToExecOnLoad.len > 0: #TODO spin event loop - let script = document.scriptsToExecOnLoad.popFirst() + let script = builder.document.scriptsToExecOnLoad.popFirst() script.execute() #TODO events -proc restart(builder: DOMBuilder[Node]) = - let document = newDocument() +proc restart(builder: ChaDOMBuilder) = + let document = newDocument(builder.factory) document.contentType = "text/html" - let oldDocument = cast[Document](builder.document) + let oldDocument = builder.document document.url = oldDocument.url let window = oldDocument.window if window != nil: document.window = window window.document = document builder.document = document + assert document.factory != nil + +proc setQuirksModeImpl(builder: ChaDOMBuilder, quirksMode: QuirksMode) = + if not builder.document.parser_cannot_change_the_mode_flag: + builder.document.mode = quirksMode + +proc setEncodingImpl(builder: ChaDOMBuilder, encoding: string): + SetEncodingResult = + let charset = getCharset(encoding) + if charset == CHARSET_UNKNOWN: + return SET_ENCODING_CONTINUE + if builder.charset in {CHARSET_UTF_16_LE, CHARSET_UTF_16_BE}: + builder.confidence = CONFIDENCE_CERTAIN + return SET_ENCODING_CONTINUE + builder.confidence = CONFIDENCE_CERTAIN + if charset == builder.charset: + return SET_ENCODING_CONTINUE + if charset == CHARSET_X_USER_DEFINED: + builder.charset = CHARSET_WINDOWS_1252 + else: + builder.charset = charset + return SET_ENCODING_STOP -proc parseError(builder: DOMBuilder[Node], message: string) = - discard - -proc setQuirksMode(builder: DOMBuilder[Node], quirksMode: QuirksMode) = - let builder = cast[ChaDOMBuilder](builder) - let document = builder.getDocument() - if not document.parser_cannot_change_the_mode_flag: - document.mode = quirksMode - -proc setCharacterSet(builder: DOMBuilder[Node], charset: Charset) = - let builder = cast[ChaDOMBuilder](builder) - let document = builder.getDocument() - document.charset = charset - -proc getTemplateContent(builder: DOMBuilder[Node], handle: Node): Node = +proc getTemplateContentImpl(builder: ChaDOMBuilder, handle: Node): Node = return HTMLTemplateElement(handle).content -proc getTagType(builder: DOMBuilder[Node], handle: Node): TagType = - return Element(handle).tagType - -proc getParentNode(builder: DOMBuilder[Node], handle: Node): Option[Node] = +proc getParentNodeImpl(builder: ChaDOMBuilder, handle: Node): Option[Node] = return option(handle.parentNode) -proc getLocalName(builder: DOMBuilder[Node], handle: Node): string = +proc getLocalNameImpl(builder: ChaDOMBuilder, handle: Node): CAtom = return Element(handle).localName -proc getNamespace(builder: DOMBuilder[Node], handle: Node): Namespace = +proc getNamespaceImpl(builder: ChaDOMBuilder, handle: Node): Namespace = return Element(handle).namespace -proc createElement(builder: DOMBuilder[Node], localName: string, - namespace: Namespace, tagType: TagType, - attrs: Table[string, string]): Node = - let builder = cast[ChaDOMBuilder](builder) - let document = builder.getDocument() - let element = document.newHTMLElement(localName, namespace, - tagType = tagType, attrs = attrs) - if element.isResettable(): +proc createHTMLElementImpl(builder: ChaDOMBuilder): Node = + return builder.document.newHTMLElement(TAG_HTML) + +proc createElementForTokenImpl(builder: ChaDOMBuilder, localName: CAtom, + namespace: Namespace, intendedParent: Node, htmlAttrs: Table[CAtom, string], + xmlAttrs: seq[ParsedAttr[CAtom]]): Node = + let document = builder.document + let element = document.newHTMLElement(localName, namespace) + for k, v in htmlAttrs: + element.attr(k, v) + for attr in xmlAttrs: + element.attrns(attr.name, attr.prefix, attr.namespace, attr.value) + if element.tagType in ResettableElements: element.resetElement() - if tagType == TAG_SCRIPT: + if element of HTMLScriptElement: let script = HTMLScriptElement(element) script.parserDocument = document script.forceAsync = false - if builder.isFragment: - script.alreadyStarted = true - #TODO document.write (?) + # Note: per standard, we could set already started to true here when we + # are parsing from document.write, but that sounds like a horrible idea. return element -proc createComment(builder: DOMBuilder[Node], text: string): Node = - let builder = cast[ChaDOMBuilder](builder) - return builder.getDocument().createComment(text) +proc createCommentImpl(builder: ChaDOMBuilder, text: string): Node = + return builder.document.createComment(text) -proc createDocumentType(builder: DOMBuilder[Node], name, publicId, +proc createDocumentTypeImpl(builder: ChaDOMBuilder, name, publicId, systemId: string): Node = - let builder = cast[ChaDOMBuilder](builder) - return builder.getDocument().newDocumentType(name, publicId, systemId) - -proc insertBefore(builder: DOMBuilder[Node], parent, child, - before: Node) = - discard parent.insertBefore(child, before) - -proc insertText(builder: DOMBuilder[Node], parent: Node, text: string, - before: Node) = - let builder = cast[ChaDOMBuilder](builder) - let prevSibling = if before != nil: - before.previousSibling + return builder.document.newDocumentType(name, publicId, systemId) + +proc insertBeforeImpl(builder: ChaDOMBuilder, parent, child: Node, + before: Option[Node]) = + discard parent.insertBefore(child, before.get(nil)) + +proc insertTextImpl(builder: ChaDOMBuilder, parent: Node, text: string, + before: Option[Node]) = + let prevSibling = if before.isSome: + before.get.previousSibling else: parent.lastChild - if prevSibling != nil and prevSibling.nodeType == TEXT_NODE: + if prevSibling != nil and prevSibling of Text: Text(prevSibling).data &= text else: - let text = builder.getDocument().createTextNode(text) - discard parent.insertBefore(text, before) + let text = builder.document.createTextNode(text) + discard parent.insertBefore(text, before.get(nil)) -proc remove(builder: DOMBuilder[Node], child: Node) = +proc removeImpl(builder: ChaDOMBuilder, child: Node) = child.remove(suppressObservers = true) -proc moveChildren(builder: DOMBuilder[Node], fromNode, toNode: Node) = +proc moveChildrenImpl(builder: ChaDOMBuilder, fromNode, toNode: Node) = var tomove = fromNode.childList for node in tomove: node.remove(suppressObservers = true) for child in tomove: toNode.insert(child, nil) -proc addAttrsIfMissing(builder: DOMBuilder[Node], element: Node, - attrs: Table[string, string]) = - let element = Element(element) +proc addAttrsIfMissingImpl(builder: ChaDOMBuilder, handle: Node, + attrs: Table[CAtom, string]) = + let element = Element(handle) for k, v in attrs: if not element.attrb(k): element.attr(k, v) -proc setScriptAlreadyStarted(builder: DOMBuilder[Node], script: Node) = +proc setScriptAlreadyStartedImpl(builder: ChaDOMBuilder, script: Node) = HTMLScriptElement(script).alreadyStarted = true -proc associateWithForm(builder: DOMBuilder[Node], element, form, +proc associateWithFormImpl(builder: ChaDOMBuilder, element, form, intendedParent: Node) = if form.inSameTree(intendedParent): #TODO remove following test eventually @@ -150,26 +180,17 @@ proc associateWithForm(builder: DOMBuilder[Node], element, form, element.setForm(HTMLFormElement(form)) element.parserInserted = true -proc elementPopped(builder: DOMBuilder[Node], element: Node) = - let builder = cast[ChaDOMBuilder](builder) - let document = builder.getDocument() +proc elementPoppedImpl(builder: ChaDOMBuilder, element: Node) = let element = Element(element) - if element.tagType == TAG_TEXTAREA: + if element of HTMLTextAreaElement: element.resetElement() - elif element.tagType == TAG_SCRIPT: - #TODO microtask (maybe it works here too?) - let script = HTMLScriptElement(element) - #TODO document.write() (?) - script.prepare() - while document.parserBlockingScript != nil: - let script = document.parserBlockingScript - document.parserBlockingScript = nil - #TODO style sheet - script.execute() - -proc newChaDOMBuilder(url: URL, window: Window, isFragment = false): - ChaDOMBuilder = - let document = newDocument() + elif element of HTMLScriptElement: + assert builder.poppedScript == nil or not builder.document.scriptingEnabled + builder.poppedScript = HTMLScriptElement(element) + +proc newChaDOMBuilder(url: URL, window: Window, factory: CAtomFactory, + isFragment = false): ChaDOMBuilder = + let document = newDocument(factory) document.contentType = "text/html" document.url = url if window != nil: @@ -177,36 +198,18 @@ proc newChaDOMBuilder(url: URL, window: Window, isFragment = false): window.document = document return ChaDOMBuilder( document: document, - finish: finish, - restart: restart, - setQuirksMode: setQuirksMode, - setCharacterSet: setCharacterSet, - elementPopped: elementPopped, - getTemplateContent: getTemplateContent, - getTagType: getTagType, - getParentNode: getParentNode, - getLocalName: getLocalName, - getNamespace: getNamespace, - createElement: createElement, - createComment: createComment, - createDocumentType: createDocumentType, - insertBefore: insertBefore, - insertText: insertText, - remove: remove, - moveChildren: moveChildren, - addAttrsIfMissing: addAttrsIfMissing, - setScriptAlreadyStarted: setScriptAlreadyStarted, - associateWithForm: associateWithForm, - #TODO isSVGIntegrationPoint (SVG support) - isFragment: isFragment + isFragment: isFragment, + factory: factory ) # https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments proc parseHTMLFragment*(element: Element, s: string): seq[Node] = let url = parseURL("about:blank").get - let builder = newChaDOMBuilder(url, nil) + let factory = element.document.factory + let builder = newChaDOMBuilder(url, nil, factory) + let inputStream = newStringStream(s) builder.isFragment = true - let document = Document(builder.document) + let document = builder.document document.mode = element.document.mode let state = case element.tagType of TAG_TITLE, TAG_TEXTAREA: RCDATA @@ -222,31 +225,139 @@ proc parseHTMLFragment*(element: Element, s: string): seq[Node] = else: DATA let root = document.newHTMLElement(TAG_HTML) document.append(root) - let opts = HTML5ParserOpts[Node]( + let opts = HTML5ParserOpts[Node, CAtom]( isIframeSrcdoc: false, #TODO? scripting: false, - canReinterpret: false, - charsets: @[CHARSET_UTF_8], - ctx: some(Node(element)), + ctx: some((Node(element), element.localName)), initialTokenizerState: state, - openElementsInit: @[Node(root)], + openElementsInit: @[(Node(root), root.localName)], pushInTemplate: element.tagType == TAG_TEMPLATE ) - let inputStream = newStringStream(s) - parseHTML(inputStream, builder, opts) + var parser = initHTML5Parser(builder, opts) + var buffer: array[4096, char] + while true: + let n = inputStream.readData(addr buffer[0], buffer.len) + if n == 0: break + let res = parser.parseChunk(buffer.toOpenArray(0, n - 1)) + assert res == PRES_CONTINUE # scripting is false, so this must be continue + parser.finish() + builder.finish() return root.childList +#TODO this should be handled by decoderstream +proc bomSniff(inputStream: Stream): Charset = + let bom = inputStream.readStr(2) + if bom == "\xFE\xFF": + return CHARSET_UTF_16_BE + if bom == "\xFF\xFE": + return CHARSET_UTF_16_LE + if bom == "\xEF\xBB": + if inputStream.readChar() == '\xBF': + return CHARSET_UTF_8 + inputStream.setPosition(0) + return CHARSET_UNKNOWN + proc parseHTML*(inputStream: Stream, window: Window, url: URL, - charsets: seq[Charset] = @[], canReinterpret = true): Document = - let builder = newChaDOMBuilder(url, window) - let opts = HTML5ParserOpts[Node]( + factory: CAtomFactory, charsets: seq[Charset] = @[], + seekable = true): Document = + let opts = HTML5ParserOpts[Node, CAtom]( isIframeSrcdoc: false, #TODO? - scripting: window != nil and window.settings.scripting, - canReinterpret: canReinterpret, - charsets: charsets + scripting: window != nil and window.settings.scripting ) - parseHTML(inputStream, builder, opts) - return Document(builder.document) + let builder = newChaDOMBuilder(url, window, factory) + var charsetStack: seq[Charset] + for i in countdown(charsets.high, 0): + charsetStack.add(charsets[i]) + var seekable = seekable + var inputStream = inputStream + if seekable: + let scs = inputStream.bomSniff() + if scs != CHARSET_UNKNOWN: + charsetStack.add(scs) + builder.confidence = CONFIDENCE_CERTAIN + seekable = false + if charsetStack.len == 0: + charsetStack.add(DefaultCharset) # UTF-8 + while true: + builder.charset = charsetStack.pop() + if seekable: + builder.confidence = CONFIDENCE_TENTATIVE # used in the next iteration + else: + builder.confidence = CONFIDENCE_CERTAIN + let em = if charsetStack.len == 0 or not seekable: + DECODER_ERROR_MODE_REPLACEMENT + else: + DECODER_ERROR_MODE_FATAL + let decoder = newDecoderStream(inputStream, builder.charset, errormode = em) + let encoder = newEncoderStream(decoder, CHARSET_UTF_8, + errormode = ENCODER_ERROR_MODE_FATAL) + var parser = initHTML5Parser(builder, opts) + let document = builder.document + var buffer: array[4096, char] + while true: + let n = encoder.readData(addr buffer[0], buffer.len) + if n == 0: break + var res = parser.parseChunk(buffer.toOpenArray(0, n - 1)) + # set insertion point for when it's needed + var ip = parser.getInsertionPoint() + while res == PRES_SCRIPT: + if builder.poppedScript != nil: + #TODO microtask + document.writeBuffers.add(DocumentWriteBuffer()) + builder.poppedScript.prepare() + while document.parserBlockingScript != nil: + let script = document.parserBlockingScript + document.parserBlockingScript = nil + #TODO style sheet + script.execute() + assert document.parserBlockingScript != script + builder.poppedScript = nil + if document.writeBuffers.len == 0: + if ip == n: + # nothing left to re-parse. + break + # parse rest of input buffer + res = parser.parseChunk(buffer.toOpenArray(ip, n - 1)) + ip += parser.getInsertionPoint() # move insertion point + else: + let writeBuffer = document.writeBuffers[^1] + let p = writeBuffer.i + let n = writeBuffer.data.len + res = parser.parseChunk(writeBuffer.data.toOpenArray(p, n - 1)) + case res + of PRES_CONTINUE: + discard document.writeBuffers.pop() + res = PRES_SCRIPT + of PRES_SCRIPT: + let pp = p + parser.getInsertionPoint() + if pp == writeBuffer.data.len: + discard document.writeBuffers.pop() + else: + writeBuffer.i = pp + of PRES_STOP: + break + {.linearScanEnd.} + # PRES_STOP is returned when we return SET_ENCODING_STOP from + # setEncodingImpl. We immediately stop parsing in this case. + if res == PRES_STOP: + break + parser.finish() + if builder.confidence == CONFIDENCE_CERTAIN and seekable: + # A meta tag describing the charset has been found; force use of this + # charset. + builder.restart() + inputStream.setPosition(0) + charsetStack.add(builder.charset) + seekable = false + continue + if decoder.failed and seekable: + # Retry with another charset. + builder.restart() + inputStream.setPosition(0) + continue + break + builder.finish() + return builder.document proc newDOMParser(): DOMParser {.jsctor.} = return DOMParser() @@ -265,7 +376,9 @@ proc parseFromString(ctx: JSContext, parser: DOMParser, str, t: string): window.document.url else: newURL("about:blank").get - let res = parseHTML(newStringStream(str), Window(nil), url) + #TODO this is probably broken in client (or at least sub-optimal) + let factory = if window != nil: window.factory else: newCAtomFactory() + let res = parseHTML(newStringStream(str), Window(nil), url, factory) return ok(res) of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml": return err(newInternalError("XML parsing is not supported yet")) diff --git a/src/html/dom.nim b/src/html/dom.nim index ac709ba1..114634e6 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -1,5 +1,4 @@ import std/deques -import std/macros import std/math import std/options import std/sets @@ -11,6 +10,7 @@ import css/cssparser import css/sheet import css/values import display/winattrs +import html/catom import html/enums import html/event import html/script @@ -76,6 +76,7 @@ type timeouts*: TimeoutState navigate*: proc(url: URL) importMapsAllowed*: bool + factory*: CAtomFactory # Navigator stuff Navigator* = object @@ -108,13 +109,12 @@ type DOMTokenList = ref object toks*: seq[string] element: Element - localName: string + localName: CAtom DOMStringMap = object target {.cursor.}: HTMLElement Node* = ref object of EventTarget - nodeType*: NodeType childList*: seq[Node] parentNode* {.jsget.}: Node root: Node @@ -129,16 +129,18 @@ type document_internal: Document # not nil Attr* = ref object of Node - namespaceURI* {.jsget.}: string - prefix* {.jsget.}: string - localName* {.jsget.}: string - value* {.jsget.}: string - ownerElement* {.jsget.}: Element + 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 @@ -148,6 +150,11 @@ type 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] @@ -190,22 +197,30 @@ type publicId*: string systemId*: string + AttrData* = object + qualifiedName*: CAtom + localName*: CAtom + prefix*: CAtom + namespace*: CAtom + value*: string + Element* = ref object of Node namespace*: Namespace - namespacePrefix*: Option[string] + namespacePrefix*: NamespacePrefix prefix*: string - localName*: string - tagType*: TagType + localName*: CAtom id* {.jsget.}: string classList* {.jsget.}: DOMTokenList - attrs: Table[string, string] - attributes* {.jsget.}: NamedNodeMap + 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 @@ -277,7 +292,7 @@ type parserDocument*: Document preparationTimeDocument*: Document forceAsync*: bool - fromAnExternalFile*: bool + external*: bool readyForParserExec*: bool alreadyStarted*: bool delayingTheLoadEvent: bool @@ -782,13 +797,64 @@ const ReflectTable0 = [ ] # Forward declarations -func attr*(element: Element, s: string): string {.inline.} -func attrb*(element: Element, s: string): bool +func attr*(element: Element, s: CAtom): string +func attr*(element: Element, s: string): string +func attrb*(element: Element, s: CAtom): bool +proc attr*(element: Element, name: CAtom, value: string) proc attr*(element: Element, name, value: string) func baseURL*(document: Document): URL +proc delAttr(element: Element, i: int, keep = false) +proc reflectAttrs(element: Element, name: CAtom, value: string) + +func document*(node: Node): Document = + if node of Document: + return Document(node) + return node.document_internal + +proc toAtom*(document: Document, s: string): CAtom = + return document.factory.toAtom(s) + +proc toStr(document: Document, atom: CAtom): string = + return document.factory.toStr(atom) + +proc toTagType*(document: Document, atom: CAtom): TagType = + return document.factory.toTagType(atom) + +proc toAtom*(document: Document, tagType: TagType): CAtom = + return document.factory.toAtom(tagType) + +proc toAtom(document: Document, namespace: Namespace): CAtom = + #TODO optimize + assert namespace != NO_NAMESPACE + return document.toAtom($namespace) + +proc toAtom(document: Document, prefix: NamespacePrefix): CAtom = + #TODO optimize + assert prefix != NO_PREFIX + return document.toAtom($prefix) + +func tagTypeNoNS(element: Element): TagType = + return element.document.toTagType(element.localName) + +func tagType*(element: Element): TagType = + if element.namespace != Namespace.HTML: + return TAG_UNKNOWN + return element.tagTypeNoNS + +func localNameStr*(element: Element): string = + return element.document.toStr(element.localName) + +func findAttr(element: Element, qualifiedName: CAtom): int = + for i, attr in element.attrs: + if attr.qualifiedName == qualifiedName: + return i + return -1 -proc tostr(ftype: enum): string = - return ($ftype).split('_')[1..^1].join('-').toLowerAscii() +func findAttrNS(element: Element, namespace, qualifiedName: CAtom): int = + for i, attr in element.attrs: + if attr.namespace == namespace and attr.qualifiedName == qualifiedName: + return i + return -1 func escapeText(s: string, attribute_mode = false): string = var nbsp_mode = false @@ -816,33 +882,30 @@ func escapeText(s: string, attribute_mode = false): string = func `$`*(node: Node): string = if node == nil: return "null" #TODO this isn't standard compliant but helps debugging - case node.nodeType - of ELEMENT_NODE: + if node of Element: let element = Element(node) - result = "<" & $element.tagType.tostr() - for k, v in element.attrs: - result &= ' ' & k & "=\"" & v.escapeText(true) & "\"" + result = "<" & element.localNameStr + for attr in element.attrs: + let k = element.document.toStr(attr.localName) + result &= ' ' & k & "=\"" & attr.value.escapeText(true) & "\"" result &= ">\n" for node in element.childList: for line in ($node).split('\n'): result &= "\t" & line & "\n" - result &= "</" & $element.tagType.tostr() & ">" - of TEXT_NODE: + result &= "</" & element.localNameStr & ">" + elif node of Text: let text = Text(node) result = text.data.escapeText() - of COMMENT_NODE: + elif node of Comment: result = "<!-- " & Comment(node).data & "-->" - of PROCESSING_INSTRUCTION_NODE: + elif node of ProcessingInstruction: result = "" #TODO - of DOCUMENT_TYPE_NODE: + elif node of DocumentType: result = "<!DOCTYPE" & ' ' & DocumentType(node).name & ">" + elif node of Document: + result = "Node of Document" else: - result = "Node of " & $node.nodeType - -func document*(node: Node): Document = - if node of Document: - return Document(node) - return node.document_internal + result = "Unknown node" func parentElement*(node: Node): Element {.jsfget.} = let p = node.parentNode @@ -852,13 +915,13 @@ func parentElement*(node: Node): Element {.jsfget.} = iterator elementList*(node: Node): Element {.inline.} = for child in node.childList: - if child.nodeType == ELEMENT_NODE: + if child of Element: yield Element(child) iterator elementList_rev*(node: Node): Element {.inline.} = for i in countdown(node.childList.high, 0): let child = node.childList[i] - if child.nodeType == ELEMENT_NODE: + if child of Element: yield Element(child) # Returns the node's ancestors @@ -888,7 +951,7 @@ iterator descendants*(node: Node): Node {.inline.} = iterator elements*(node: Node): Element {.inline.} = for child in node.descendants: - if child.nodeType == ELEMENT_NODE: + if child of Element: yield Element(child) iterator elements*(node: Node, tag: TagType): Element {.inline.} = @@ -927,7 +990,7 @@ iterator radiogroup*(input: HTMLInputElement): HTMLInputElement {.inline.} = iterator textNodes*(node: Node): Text {.inline.} = for node in node.childList: - if node.nodeType == TEXT_NODE: + if node of Text: yield Text(node) iterator options*(select: HTMLSelectElement): HTMLOptionElement {.inline.} = @@ -980,7 +1043,7 @@ proc finalize(collection: HTMLAllCollection) {.jsfin.} = collection.finalize0() func ownerDocument(node: Node): Document {.jsfget.} = - if node.nodeType == DOCUMENT_NODE: + if node of Document: return nil return node.document @@ -1005,11 +1068,34 @@ func newCollection[T: Collection](root: Node, match: CollectionMatchFun, inc root.document.colln result.populateCollection() -func nodeType(node: Node): uint16 {.jsfget.} = - return uint16(node.nodeType) +func jsNodeType0(node: Node): NodeType = + if node of CharacterData: + if node of Text: + return TEXT_NODE + elif node of Comment: + return COMMENT_NODE + elif node of CDATASection: + return CDATA_SECTION_NODE + elif node of ProcessingInstruction: + return PROCESSING_INSTRUCTION_NODE + assert false + elif node of Element: + return ELEMENT_NODE + elif node of Document: + return DOCUMENT_NODE + elif node of DocumentType: + return DOCUMENT_TYPE_NODE + elif node of Attr: + return ATTRIBUTE_NODE + elif node of DocumentFragment: + return DOCUMENT_FRAGMENT_NODE + assert false + +func jsNodeType(node: Node): uint16 {.jsfget: "nodeType".} = + return uint16(node.jsNodeType0) func isElement(node: Node): bool = - return node.nodeType == ELEMENT_NODE + return node of Element template parentNodeChildrenImpl(parentNode: typed) = if parentNode.children_cached == nil: @@ -1052,7 +1138,8 @@ func contains*(tokenList: DOMTokenList, s: string): bool {.jsfunc.} = return s in tokenList.toks proc update(tokenList: DOMTokenList) = - if not tokenList.element.attrb(tokenList.localName) and tokenList.toks.len == 0: + if not tokenList.element.attrb(tokenList.localName) and + tokenList.toks.len == 0: return tokenList.element.attr(tokenList.localName, tokenList.toks.join(' ')) @@ -1111,18 +1198,22 @@ proc replace(tokenList: DOMTokenList, token, newToken: string): return ok(true) const SupportedTokensMap = { - "rel": @["alternate", "dns-prefetch", "icon", "manifest", "modulepreload", + "rel": @[ + "alternate", "dns-prefetch", "icon", "manifest", "modulepreload", "next", "pingback", "preconnect", "prefetch", "preload", "search", - "stylesheet"] + "stylesheet" + ] }.toTable() func supports(tokenList: DOMTokenList, token: string): JSResult[bool] {.jsfunc.} = - if tokenList.localName in SupportedTokensMap: + #TODO atomize SupportedTokensMap (or preferably add an attribute name enum) + let localName = tokenList.element.document.toStr(tokenList.localName) + if localName in SupportedTokensMap: let lowercase = token.toLowerAscii() - return ok(lowercase in SupportedTokensMap[tokenList.localName]) + return ok(lowercase in SupportedTokensMap[localName]) return err(newTypeError("No supported tokens defined for attribute " & - tokenList.localName)) + localName)) func `$`(tokenList: DOMTokenList): string {.jsfunc.} = return tokenList.toks.join(' ') @@ -1134,31 +1225,35 @@ func getter(tokenList: DOMTokenList, i: int): Option[string] {.jsgetprop.} = return tokenList.item(i) # DOMStringMap -func validateAttributeName(name: string, isq: static bool = false): - Err[DOMException] = - when isq: - if name.matchNameProduction(): - return ok() - else: - if name.matchQNameProduction(): - return ok() +func validateAttributeName(name: string): Err[DOMException] = + if name.matchNameProduction(): + return ok() + return errDOMException("Invalid character in attribute name", + "InvalidCharacterError") + +func validateAttributeQName(name: string): Err[DOMException] = + if name.matchQNameProduction(): + return ok() return errDOMException("Invalid character in attribute name", "InvalidCharacterError") func hasprop(map: ptr DOMStringMap, name: string): bool {.jshasprop.} = - return "data-" & name in map[].target.attrs + let name = map[].target.document.toAtom("data-" & name) + return map[].target.attrb(name) proc delete(map: ptr DOMStringMap, name: string): bool {.jsfunc.} = - let name = "data-" & name.camelToKebabCase() - let res = name in map[].target.attrs - map[].target.attrs.del(name) - return res + let name = map[].target.document.toAtom("data-" & name.camelToKebabCase()) + let i = map[].target.findAttr(name) + if i != -1: + map[].target.delAttr(i) + return i != -1 func getter(map: ptr DOMStringMap, name: string): Option[string] {.jsgetprop.} = - let name = "data-" & name.camelToKebabCase() - map[].target.attrs.withValue(name, p): - return some(p[]) + let name = map[].target.document.toAtom("data-" & name.camelToKebabCase()) + let i = map[].target.findAttr(name) + if i != -1: + return some(map[].target.attrs[i].value) return none(string) proc setter(map: ptr DOMStringMap, name, value: string): Err[DOMException] @@ -1172,13 +1267,15 @@ proc setter(map: ptr DOMStringMap, name, value: string): Err[DOMException] "InvalidCharacterError") let name = "data-" & name.camelToKebabCase() ?name.validateAttributeName() - map.target.attr(name, value) + let aname = map[].target.document.toAtom(name) + map.target.attr(aname, value) return ok() func names(ctx: JSContext, map: ptr DOMStringMap): JSPropertyEnumList {.jspropnames.} = var list = newJSPropertyEnumList(ctx, uint32(map[].target.attrs.len)) - for k, v in map[].target.attrs: + for attr in map[].target.attrs: + let k = map[].target.document.toStr(attr.localName) if k.startsWith("data-") and AsciiUpperAlpha notin k: list.add(k["data-".len .. ^1].kebabToCamelCase()) return list @@ -1231,6 +1328,7 @@ func getter[T: uint32|string](collection: HTMLCollection, u: T): func names(ctx: JSContext, collection: HTMLCollection): JSPropertyEnumList {.jspropnames.} = + let aName = collection.root.document.toAtom("name") #TODO enumize let L = collection.length var list = newJSPropertyEnumList(ctx, L) var ids: OrderedSet[string] @@ -1240,7 +1338,7 @@ func names(ctx: JSContext, collection: HTMLCollection): JSPropertyEnumList if elem.id != "": ids.incl(elem.id) if elem.namespace == Namespace.HTML: - let name = elem.attr("name") + let name = elem.attr(aName) ids.incl(name) for id in ids: list.add(id) @@ -1423,100 +1521,127 @@ proc setHash(location: Location, s: string) {.jsfset: "hash".} = copyURL.setHash(s) document.window.navigate(copyURL) -func newAttr(document: Document, localName, value, prefix, - namespaceURI: string): Attr = - return Attr( - nodeType: ATTRIBUTE_NODE, - document_internal: document, - namespaceURI: namespaceURI, - localName: localName, - prefix: prefix, - value: value, - index: -1 - ) +func jsOwnerElement(attr: Attr): Element {.jsfget: "ownerElement".} = + if attr.ownerElement of AttrDummyElement: + return nil + return attr.ownerElement -func newAttr(parent: Element, localName, value: string, prefix = "", - namespaceURI = ""): Attr = - return Attr( - nodeType: ATTRIBUTE_NODE, - document_internal: parent.document, - namespaceURI: namespaceURI, - ownerElement: parent, - localName: localName, - prefix: prefix, - value: value, - index: -1 - ) +func data(attr: Attr): lent AttrData = + return attr.ownerElement.attrs[attr.dataIdx] + +proc jsNamespaceURI(attr: Attr): string {.jsfget: "namespaceURI".} = + return attr.ownerElement.document.toStr(attr.data.namespace) + +proc jsPrefix(attr: Attr): string {.jsfget: "prefix".} = + return attr.ownerElement.document.toStr(attr.data.prefix) + +proc jsLocalName(attr: Attr): string {.jsfget: "localName".} = + return attr.ownerElement.document.toStr(attr.data.localName) + +proc jsValue(attr: Attr): string {.jsfget: "value".} = + return attr.data.value func name(attr: Attr): string {.jsfget.} = - if attr.prefix == "": - return attr.localName - return attr.prefix & ':' & attr.localName + return attr.ownerElement.document.toStr(attr.data.qualifiedName) -func findAttr(map: NamedNodeMap, name: string): int = - for i in 0 ..< map.attrlist.len: - if map.attrlist[i].name == name: +func findAttr(map: NamedNodeMap, dataIdx: int): int = + for i, attr in map.attrlist: + if attr.dataIdx == dataIdx: return i return -1 -func findAttrNS(map: NamedNodeMap, namespace, localName: string): int = - for i in 0 ..< map.attrlist.len: - if map.attrlist[i].namespaceURI == namespace and map.attrlist[i].localName == localName: - return i - return -1 +proc getAttr(map: NamedNodeMap, dataIdx: int): Attr = + let i = map.findAttr(dataIdx) + if i != -1: + return map.attrlist[i] + let attr = Attr( + document_internal: map.element.document, + index: -1, + dataIdx: dataIdx, + ownerElement: map.element + ) + map.attrlist.add(attr) + return attr + +func normalizeAttrQName(element: Element, qualifiedName: string): CAtom = + if element.namespace == Namespace.HTML and not element.document.isxml: + return element.document.toAtom(qualifiedName.toLowerAscii()) + return element.document.toAtom(qualifiedName) + +func hasAttributes(element: Element): bool {.jsfunc.} = + return element.attrs.len > 0 + +func attributes(element: Element): NamedNodeMap {.jsfget.} = + if element.attributesInternal != nil: + return element.attributesInternal + element.attributesInternal = NamedNodeMap(element: element) + for i, attr in element.attrs: + element.attributesInternal.attrlist.add(Attr( + document_internal: element.document, + index: -1, + dataIdx: i, + ownerElement: element + )) + return element.attributesInternal + +func findAttr(element: Element, qualifiedName: string): int = + return element.findAttr(element.normalizeAttrQName(qualifiedName)) + +func findAttrNS(element: Element, namespace, localName: string): int = + let namespace = element.document.toAtom(namespace) + let localName = element.document.toAtom(localName) + return element.findAttrNS(namespace, localName) func hasAttribute(element: Element, qualifiedName: string): bool {.jsfunc.} = - let qualifiedName = if element.namespace == Namespace.HTML and - not element.document.isxml: - qualifiedName.toLowerAscii() - else: - qualifiedName - if qualifiedName in element.attrs: - return true + return element.findAttr(qualifiedName) != -1 func hasAttributeNS(element: Element, namespace, localName: string): bool {.jsfunc.} = - return element.attributes.findAttrNS(namespace, localName) != -1 + return element.findAttrNS(namespace, localName) != -1 func getAttribute(element: Element, qualifiedName: string): Option[string] {.jsfunc.} = - let qualifiedName = if element.namespace == Namespace.HTML and - not element.document.isxml: - qualifiedName.toLowerAscii() - else: - qualifiedName - element.attrs.withValue(qualifiedName, val): - return some(val[]) + let i = element.findAttr(qualifiedName) + if i != -1: + return some(element.attrs[i].value) + return none(string) -func getAttributeNS(element: Element, namespace, localName: string): Option[string] {.jsfunc.} = - let i = element.attributes.findAttrNS(namespace, localName) +func getAttributeNS(element: Element, namespace, localName: string): + Option[string] {.jsfunc.} = + let i = element.findAttrNS(namespace, localName) if i != -1: - return some(element.attributes.attrlist[i].value) + return some(element.attrs[i].value) + return none(string) -func getNamedItem(map: NamedNodeMap, qualifiedName: string): Option[Attr] {.jsfunc.} = - if map.element.hasAttribute(qualifiedName): - let i = map.findAttr(qualifiedName) - if i != -1: - return some(map.attrlist[i]) +proc getNamedItem(map: NamedNodeMap, qualifiedName: string): Option[Attr] + {.jsfunc.} = + let i = map.element.findAttr(qualifiedName) + if i != -1: + return some(map.getAttr(i)) + return none(Attr) -func getNamedItemNS(map: NamedNodeMap, namespace, localName: string): Option[Attr] {.jsfunc.} = - let i = map.findAttrNS(namespace, localName) +proc getNamedItemNS(map: NamedNodeMap, namespace, localName: string): + Option[Attr] {.jsfunc.} = + let i = map.element.findAttrNS(namespace, localName) if i != -1: - return some(map.attrlist[i]) + return some(map.getAttr(i)) + return none(Attr) func length(map: NamedNodeMap): uint32 {.jsfget.} = return uint32(map.element.attrs.len) -func item(map: NamedNodeMap, i: int): Option[Attr] {.jsfunc.} = - if i < map.attrlist.len: - return some(map.attrlist[i]) +proc item(map: NamedNodeMap, i: uint32): Option[Attr] {.jsfunc.} = + if int(i) < map.element.attrs.len: + return some(map.getAttr(int(i))) + return none(Attr) -func hasprop[T: int|string](map: NamedNodeMap, i: T): bool {.jshasprop.} = - when T is int: - return i < map.attrlist.len +func hasprop[T: uint32|string](map: NamedNodeMap, i: T): bool {.jshasprop.} = + when T is uint32: + return int(i) < map.element.attrs.len else: return map.getNamedItem(i).isSome -func getter[T: int|string](map: NamedNodeMap, i: T): Option[Attr] {.jsgetprop.} = - when T is int: +func getter[T: uint32|string](map: NamedNodeMap, i: T): Option[Attr] + {.jsgetprop.} = + when T is uint32: return map.item(i) else: return map.getNamedItem(i) @@ -1530,9 +1655,16 @@ func names(ctx: JSContext, map: NamedNodeMap): JSPropertyEnumList var list = newJSPropertyEnumList(ctx, len) for u in 0 ..< len: list.add(u) - if map.element.namespace == Namespace.HTML: - for name in map.element.attrs.keys: - list.add(name) + var names: HashSet[string] + let element = map.element + for attr in element.attrs: + let name = element.document.toStr(attr.qualifiedName) + if element.namespace == Namespace.HTML and AsciiUpperAlpha in name: + continue + if name in names: + continue + names.incl(name) + list.add(name) return list func length(characterData: CharacterData): uint32 {.jsfget.} = @@ -1590,10 +1722,28 @@ func canSubmitImplicitly*(form: HTMLFormElement): bool = return true func qualifiedName*(element: Element): string = - if element.namespacePrefix.isSome: - element.namespacePrefix.get & ':' & element.localName + if element.namespacePrefix != NO_PREFIX: + $element.namespacePrefix & ':' & element.localNameStr else: - element.localName + element.localNameStr + +# https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-write-steps +proc write(document: Document, text: varargs[string]): Err[DOMException] + {.jsfunc.} = + if document.isxml: + return errDOMException("document.write not supported in XML documents", + "InvalidStateError") + if document.throwOnDynamicMarkupInsertion > 0: + return errDOMException("throw-on-dynamic-markup-insertion counter > 0", + "InvalidStateError") + if document.activeParserWasAborted: + return ok() + #TODO if insertion point is undefined... (open document) + if document.writeBuffers.len == 0: + return ok() #TODO (probably covered by open above) + for s in text: + document.writeBuffers[^1].data &= s + return ok() func html*(document: Document): HTMLElement = for element in document.elements(TAG_HTML): @@ -1617,21 +1767,21 @@ func select*(option: HTMLOptionElement): HTMLSelectElement = return HTMLSelectElement(anc) return nil -func countChildren(node: Node, nodeType: NodeType): int = +func countChildren(node: Node, nodeType: type): int = for child in node.childList: - if child.nodeType == nodeType: + if child of nodeType: inc result -func hasChild(node: Node, nodeType: NodeType): bool = +func hasChild(node: Node, nodeType: type): bool = for child in node.childList: - if child.nodeType == nodeType: + if child of nodeType: return true -func hasChildExcept(node: Node, nodeType: NodeType, ex: Node): bool = +func hasChildExcept(node: Node, nodeType: type, ex: Node): bool = for child in node.childList: if child == ex: continue - if child.nodeType == nodeType: + if child of nodeType: return true return false @@ -1647,42 +1797,46 @@ func nextSibling*(node: Node): Node {.jsfget.} = return nil return node.parentNode.childList[i] -func hasNextSibling(node: Node, nodeType: NodeType): bool = +func hasNextSibling(node: Node, nodeType: type): bool = var node = node.nextSibling while node != nil: - if node.nodeType == nodeType: return true + if node of nodeType: + return true node = node.nextSibling return false -func hasPreviousSibling(node: Node, nodeType: NodeType): bool = +func hasPreviousSibling(node: Node, nodeType: type): bool = var node = node.previousSibling while node != nil: - if node.nodeType == nodeType: return true + if node of nodeType: + return true node = node.previousSibling return false func nodeValue(node: Node): Option[string] {.jsfget.} = - case node.nodeType - of CharacterDataNodes: + if node of CharacterData: return some(CharacterData(node).data) - of ATTRIBUTE_NODE: - return some(Attr(node).value) - else: discard + elif node of Attr: + return some(Attr(node).data.value) + return none(string) -func textContent*(node: Node): string {.jsfget.} = - case node.nodeType - of DOCUMENT_NODE, DOCUMENT_TYPE_NODE: - return "" #TODO null - of CharacterDataNodes: - return CharacterData(node).data +func textContent*(node: Node): string = + if node of CharacterData: + result = CharacterData(node).data else: + result = "" for child in node.childList: - if child.nodeType != COMMENT_NODE: + if not (child of Comment): result &= child.textContent +func jsTextContent(node: Node): Opt[string] {.jsfget: "textContent".} = + if node of Document or node of DocumentType: + return err() # null + return ok(node.textContent) + func childTextContent*(node: Node): string = for child in node.childList: - if child.nodeType == TEXT_NODE: + if child of Text: result &= Text(child).data func rootNode*(node: Node): Node = @@ -1690,7 +1844,7 @@ func rootNode*(node: Node): Node = return node.root func isConnected(node: Node): bool {.jsfget.} = - return node.rootNode.nodeType == DOCUMENT_NODE #TODO shadow root + return node.rootNode of Document #TODO shadow root func inSameTree*(a, b: Node): bool = a.rootNode == b.rootNode @@ -1735,13 +1889,30 @@ func getElementById(node: Node, id: string): Element {.jsfunc.} = for child in node.elements: if child.id == id: return child + return nil func getElementsByTagName0(root: Node, tagName: string): HTMLCollection = if tagName == "*": - return newCollection[HTMLCollection](root, func(node: Node): bool = node.isElement, true, false) - let t = tagType(tagName) - if t != TAG_UNKNOWN: - return newCollection[HTMLCollection](root, func(node: Node): bool = node.isElement and Element(node).tagType == t, true, false) + return newCollection[HTMLCollection]( + root, + isElement, + islive = true, + childonly = false + ) + let localName = root.document.toAtom(tagName) + let localNameLower = root.document.toAtom(tagName.toLowerAscii()) + return newCollection[HTMLCollection]( + root, + func(node: Node): bool = + if node of Element: + let element = Element(node) + if element.namespace == Namespace.HTML: + return element.localName == localNameLower + return element.localName == localName + return false, + islive = true, + childonly = false + ) func getElementsByTagName(document: Document, tagName: string): HTMLCollection {.jsfunc.} = return document.getElementsByTagName0(tagName) @@ -1758,7 +1929,7 @@ func getElementsByClassName0(node: Node, classNames: string): HTMLCollection = c = c.toLowerAscii() return newCollection[HTMLCollection](node, func(node: Node): bool = - if node.nodeType == ELEMENT_NODE: + if node of Element: if isquirks: var cl = Element(node).classList for tok in cl.toks.mitems: @@ -1784,7 +1955,7 @@ func previousElementSibling*(elem: Element): Element {.jsfget.} = if p == nil: return nil for i in countdown(elem.index - 1, 0): let node = p.childList[i] - if node.nodeType == ELEMENT_NODE: + if node of Element: return Element(node) return nil @@ -1793,15 +1964,21 @@ func nextElementSibling*(elem: Element): Element {.jsfget.} = if p == nil: return nil for i in elem.index + 1 .. p.childList.high: let node = p.childList[i] - if node.nodeType == ELEMENT_NODE: + if node of Element: return Element(node) return nil func documentElement(document: Document): Element {.jsfget.} = document.firstElementChild() -func attr*(element: Element, s: string): string {.inline.} = - return element.attrs.getOrDefault(s, "") +func attr*(element: Element, s: CAtom): string = + let i = element.findAttr(s) + if i != -1: + return element.attrs[i].value + return "" + +func attr*(element: Element, s: string): string = + return element.attr(element.document.toAtom(s)) func attrl*(element: Element, s: string): Option[int32] = return parseInt32(element.attr(s)) @@ -1816,10 +1993,12 @@ func attrul*(element: Element, s: string): Option[uint32] = if x.isSome and x.get >= 0: return x +func attrb*(element: Element, s: CAtom): bool = + return element.findAttr(s) != -1 + func attrb*(element: Element, s: string): bool = - if s in element.attrs: - return true - return false + let atom = element.document.toAtom(s) + return element.attrb(atom) # https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments func serializesAsVoid(element: Element): bool = @@ -1832,23 +2011,19 @@ func serializeFragmentInner(child: Node, parentType: TagType): string = result = "" if child of Element: let element = Element(child) + let tags = element.localNameStr result &= '<' #TODO qualified name if not HTML, SVG or MathML - if element.tagType == TAG_UNKNOWN: - result &= element.localName - else: - result &= tagName(element.tagType) + result &= tags #TODO custom elements - for k, v in element.attrs: + for attr in element.attrs: #TODO namespaced attrs - result &= ' ' & k & "=\"" & v.escapeText(true) & "\"" + let k = element.document.toStr(attr.localName) + result &= ' ' & k & "=\"" & attr.value.escapeText(true) & "\"" result &= '>' result &= element.serializeFragment() result &= "</" - if element.tagType == TAG_UNKNOWN: - result &= element.localName - else: - result &= tagName(element.tagType) + result &= tags result &= '>' elif child of Text: let text = Text(child) @@ -1924,7 +2099,7 @@ proc sheets*(document: Document): seq[CSSStylesheet] = case elem.tagType of TAG_STYLE: let style = HTMLStyleElement(elem) - style.sheet = parseStylesheet(newStringStream(style.textContent)) + style.sheet = parseStylesheet(style.textContent, document.factory) if style.sheet != nil: document.cachedSheets.add(style.sheet) of TAG_LINK: @@ -2162,7 +2337,6 @@ func jsForm(this: HTMLTextAreaElement): HTMLFormElement {.jsfget: "form".} = func newText(document: Document, data: string): Text = return Text( - nodeType: TEXT_NODE, document_internal: document, data: data, index: -1 @@ -2174,7 +2348,6 @@ func newText(ctx: JSContext, data = ""): Text {.jsctor.} = func newCDATASection(document: Document, data: string): CDATASection = return CDATASection( - nodeType: CDATA_SECTION_NODE, document_internal: document, data: data, index: -1 @@ -2183,7 +2356,6 @@ func newCDATASection(document: Document, data: string): CDATASection = func newProcessingInstruction(document: Document, target, data: string): ProcessingInstruction = return ProcessingInstruction( - nodeType: PROCESSING_INSTRUCTION_NODE, document_internal: document, target: target, data: data, @@ -2192,7 +2364,6 @@ func newProcessingInstruction(document: Document, target, data: string): func newDocumentFragment(document: Document): DocumentFragment = return DocumentFragment( - nodeType: DOCUMENT_FRAGMENT_NODE, document_internal: document, index: -1 ) @@ -2203,7 +2374,6 @@ func newDocumentFragment(ctx: JSContext): DocumentFragment {.jsctor.} = func newComment(document: Document, data: string): Comment = return Comment( - nodeType: COMMENT_NODE, document_internal: document, data: data, index: -1 @@ -2214,15 +2384,17 @@ func newComment(ctx: JSContext, data: string = ""): Comment {.jsctor.} = 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 = +proc newHTMLElement*(document: Document, localName: CAtom, + namespace = Namespace.HTML, prefix = NO_PREFIX, + attrs = newSeq[AttrData]()): HTMLElement = + let tagType = document.toTagType(localName) case tagType of TAG_INPUT: result = HTMLInputElement() of TAG_A: let anchor = HTMLAnchorElement() - anchor.relList = DOMTokenList(element: anchor, localName: "rel") + let localName = document.toAtom("rel") + anchor.relList = DOMTokenList(element: anchor, localName: localName) result = anchor of TAG_SELECT: result = HTMLSelectElement() @@ -2248,11 +2420,13 @@ func newHTMLElement*(document: Document, tagType: TagType, result = HTMLStyleElement() of TAG_LINK: let link = HTMLLinkElement() - link.relList = DOMTokenList(element: link, localName: "rel") + let localName = document.toAtom("rel") #TODO enumize + link.relList = DOMTokenList(element: link, localName: localName) result = link of TAG_FORM: let form = HTMLFormElement() - form.relList = DOMTokenList(element: form, localName: "rel") + let localName = document.toAtom("rel") #TODO enumize + form.relList = DOMTokenList(element: form, localName: localName) result = form of TAG_TEMPLATE: result = HTMLTemplateElement( @@ -2279,52 +2453,52 @@ func newHTMLElement*(document: Document, tagType: TagType, result = HTMLImageElement() of TAG_AREA: let area = HTMLAreaElement() - area.relList = DOMTokenList(element: result, localName: "rel") + let localName = document.toAtom("rel") #TODO enumize + area.relList = DOMTokenList(element: result, localName: localName) result = area else: result = HTMLElement() - result.nodeType = ELEMENT_NODE - result.tagType = tagType + result.localName = localName result.namespace = namespace result.namespacePrefix = prefix result.document_internal = document - result.attributes = NamedNodeMap(element: result) - result.classList = DOMTokenList(element: result, localName: "classList") + let localName = document.toAtom("classList") #TODO enumize + result.classList = DOMTokenList(element: result, localName: localName) result.index = -1 result.dataset = DOMStringMap(target: result) - {.cast(noSideEffect).}: - for k, v in attrs: - result.attr(k, v) - 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 + result.attrs = attrs -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 +proc newHTMLElement*(document: Document, tagType: TagType, + namespace = Namespace.HTML, prefix = NO_PREFIX, + attrs = newSeq[AttrData]()): HTMLElement = + let localName = document.toAtom(tagType) + return document.newHTMLElement(localName, namespace, prefix, attrs) -func newDocument*(): Document {.jsctor.} = +func newDocument*(factory: CAtomFactory): Document = + assert factory != nil let document = Document( - nodeType: DOCUMENT_NODE, url: newURL("about:blank").get, - index: -1 + index: -1, + factory: factory ) document.implementation = DOMImplementation(document: document) document.contentType = "application/xml" return document -func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType = +func newDocument(ctx: JSContext): Document {.jsctor.} = + let global = JS_GetGlobalObject(ctx) + let window = if ctx.hasClass(Window): + fromJS[Window](ctx, global).get(nil) + else: + Window(nil) + JS_FreeValue(ctx, global) + #TODO this is probably broken in client (or at least sub-optimal) + let factory = if window != nil: window.factory else: newCAtomFactory() + return newDocument(factory) + +func newDocumentType*(document: Document, name, publicId, systemId: string): + DocumentType = return DocumentType( - nodeType: DOCUMENT_TYPE_NODE, document_internal: document, name: name, publicId: publicId, @@ -2332,15 +2506,13 @@ func newDocumentType*(document: Document, name: string, publicId = "", systemId index: -1 ) -func isResettable*(element: Element): bool = - return element.tagType in {TAG_INPUT, TAG_OUTPUT, TAG_SELECT, TAG_TEXTAREA} - func isHostIncludingInclusiveAncestor*(a, b: Node): bool = for parent in b.branch: if parent == a: return true - if b.rootNode.nodeType == DOCUMENT_FRAGMENT_NODE and DocumentFragment(b.rootNode).host != nil: - for parent in b.rootNode.branch: + let root = b.rootNode + if root of DocumentFragment and DocumentFragment(root).host != nil: + for parent in root.branch: if parent == a: return true return false @@ -2375,39 +2547,55 @@ func title*(document: Document): string = return title.childTextContent.stripAndCollapse() return "" -func disabled*(option: HTMLOptionElement): bool = - if option.parentElement.tagType == TAG_OPTGROUP and option.parentElement.attrb("disabled"): +# https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-disabled +func isDisabled*(option: HTMLOptionElement): bool = + if option.parentElement.tagType == TAG_OPTGROUP and + option.parentElement.attrb("disabled"): return true return option.attrb("disabled") -func text*(option: HTMLOptionElement): string = +func text(option: HTMLOptionElement): string {.jsfget.} = + var s = "" for child in option.descendants: - if child.nodeType == TEXT_NODE: - let child = Text(child) - if child.parentElement.tagType != TAG_SCRIPT: #TODO svg - result &= child.data.stripAndCollapse() + let parent = child.parentElement + if child of Text and (parent.tagTypeNoNS != TAG_SCRIPT or + parent.namespace notin {Namespace.HTML, Namespace.SVG}): + s &= Text(child).data + return s.stripAndCollapse() func value*(option: HTMLOptionElement): string {.jsfget.} = if option.attrb("value"): return option.attr("value") - return option.childTextContent.stripAndCollapse() + return option.text proc invalidateCollections(node: Node) = for id in node.liveCollections: node.document.invalidCollections.incl(id) -proc delAttr(element: Element, i: int) = - if i != -1: - let attr = element.attributes.attrlist[i] - element.attrs.del(attr.name) - element.attributes.attrlist.delete(i) - element.invalidateCollections() - element.invalid = true - -proc delAttr(element: Element, name: string) = - let i = element.attributes.findAttr(name) - if i != -1: - element.delAttr(i) +proc delAttr(element: Element, i: int, keep = false) = + let map = element.attributesInternal + element.attrs.delete(i) # ordering matters + if map != nil: + # delete from attrlist + adjust indices invalidated + var j = -1 + for i, attr in map.attrlist.mpairs: + if attr.dataIdx == i: + j = i + elif attr.dataIdx > i: + dec attr.dataIdx + if j != -1: + if keep: + let attr = map.attrlist[j] + let data = attr.data + attr.ownerElement = AttrDummyElement( + document_internal: attr.ownerElement.document, + index: -1, + attrs: @[data] + ) + attr.dataIdx = 0 + map.attrlist.del(j) # ordering does not matter + element.invalidateCollections() + element.invalid = true proc newCSSStyleDeclaration(element: Element, value: string): CSSStyleDeclaration = @@ -2463,7 +2651,9 @@ proc style*(element: Element): CSSStyleDeclaration {.jsfget.} = element.style_cached = CSSStyleDeclaration(element: element) return element.style_cached -proc reflectAttrs(element: Element, name, value: string) = +proc reflectAttrs(element: Element, name: CAtom, value: string) = + #TODO enumize + let name = element.document.toStr(name) template reflect_str(element: Element, n: static string, val: untyped) = if name == n: element.val = value @@ -2486,6 +2676,7 @@ proc reflectAttrs(element: Element, name, value: string) = return element.reflect_str "id", id element.reflect_domtoklist "class", classList + #TODO internalNonce if name == "style": element.style_cached = newCSSStyleDeclaration(element, value) return @@ -2514,24 +2705,64 @@ proc reflectAttrs(element: Element, name, value: string) = of TAG_AREA: let area = HTMLAreaElement(element) area.reflect_domtoklist "rel", relList + of TAG_CANVAS: + if name == "width" or name == "height": + let w = element.attrul("width").get(300) + let h = element.attrul("height").get(150) + let canvas = HTMLCanvasElement(element) + if canvas.bitmap.width != w or canvas.bitmap.height != h: + canvas.bitmap = newBitmap(w, h) else: discard -proc attr0(element: Element, name, value: string) = - element.attrs.withValue(name, val): - val[] = value +proc attr*(element: Element, name: CAtom, value: string) = + let i = element.findAttr(name) + if i != -1: + element.attrs[i].value = value element.invalidateCollections() element.invalid = true - do: # else - element.attrs[name] = value + else: + #TODO sort? + element.attrs.add(AttrData( + qualifiedName: name, + localName: name, + value: value + )) element.reflectAttrs(name, value) -proc attr*(element: Element, name, value: string) = - let i = element.attributes.findAttr(name) +proc attrns*(element: Element, localName: CAtom, prefix: NamespacePrefix, + namespace: Namespace, value: sink string) = + if prefix == NO_PREFIX and namespace == NO_NAMESPACE: + element.attr(localName, value) + return + let namespace = element.document.toAtom(namespace) + let i = element.findAttrNS(namespace, localName) + var prefixAtom, qualifiedName: CAtom + if prefix != NO_PREFIX: + prefixAtom = element.document.toAtom(prefix) + let tmp = $prefix & ':' & element.document.toStr(localName) + qualifiedName = element.document.toAtom(tmp) + else: + qualifiedName = localName if i != -1: - element.attributes.attrlist[i].value = value + element.attrs[i].prefix = prefixAtom + element.attrs[i].qualifiedName = qualifiedName + element.attrs[i].value = value + element.invalidateCollections() + element.invalid = true else: - element.attributes.attrlist.add(element.newAttr(name, value)) - element.attr0(name, value) + #TODO sort? + element.attrs.add(AttrData( + prefix: prefixAtom, + localName: localName, + qualifiedName: qualifiedName, + namespace: namespace, + value: value + )) + element.reflectAttrs(qualifiedName, value) + +proc attr*(element: Element, name, value: string) = + let name = element.document.toAtom(name) + element.attr(name, value) proc attrl(element: Element, name: string, value: int32) = element.attr(name, $value) @@ -2555,72 +2786,76 @@ proc setAttribute(element: Element, qualifiedName, value: string): proc setAttributeNS(element: Element, namespace, qualifiedName, value: string): Err[DOMException] {.jsfunc.} = - ?validateAttributeName(qualifiedName, isq = true) + ?validateAttributeQName(qualifiedName) let ps = qualifiedName.until(':') let prefix = if ps.len < qualifiedName.len: ps else: "" - let localName = qualifiedName.substr(prefix.len) + let localName = element.document.toAtom(qualifiedName.substr(prefix.len)) + #TODO atomize here if prefix != "" and namespace == "" or prefix == "xml" and namespace != $Namespace.XML or (qualifiedName == "xmlns" or prefix == "xmlns") and namespace != $Namespace.XMLNS or namespace == $Namespace.XMLNS and qualifiedName != "xmlns" and prefix != "xmlns": return errDOMException("Unexpected namespace", "NamespaceError") - element.attr0(qualifiedName, value) - let i = element.attributes.findAttrNS(namespace, localName) + let qualifiedName = element.document.toAtom(qualifiedName) + let namespace = element.document.toAtom(namespace) + let i = element.findAttrNS(namespace, localName) if i != -1: - element.attributes.attrlist[i].value = value + element.attrs[i].value = value else: - element.attributes.attrlist.add(element.newAttr(localName, value, prefix, namespace)) + element.attrs.add(AttrData( + localName: localName, + namespace: namespace, + qualifiedName: qualifiedName, + value: value + )) return ok() proc removeAttribute(element: Element, qualifiedName: string) {.jsfunc.} = - let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml: - qualifiedName.toLowerAscii() - else: - qualifiedName - element.delAttr(qualifiedName) + let i = element.findAttr(qualifiedName) + if i != -1: + element.delAttr(i) proc removeAttributeNS(element: Element, namespace, localName: string) {.jsfunc.} = - let i = element.attributes.findAttrNS(namespace, localName) + let i = element.findAttrNS(namespace, localName) if i != -1: element.delAttr(i) proc toggleAttribute(element: Element, qualifiedName: string, force = none(bool)): DOMResult[bool] {.jsfunc.} = ?validateAttributeName(qualifiedName) - let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml: - qualifiedName.toLowerAscii() - else: - qualifiedName + let qualifiedName = element.normalizeAttrQName(qualifiedName) if not element.attrb(qualifiedName): if force.get(true): element.attr(qualifiedName, "") return ok(true) return ok(false) if not force.get(false): - element.delAttr(qualifiedName) + let i = element.findAttr(qualifiedName) + if i != -1: + element.delAttr(i) return ok(false) return ok(true) proc value(attr: Attr, s: string) {.jsfset.} = - attr.value = s - if attr.ownerElement != nil: - attr.ownerElement.attr0(attr.name, s) + attr.ownerElement.attr(attr.name, s) proc setNamedItem(map: NamedNodeMap, attr: Attr): DOMResult[Attr] {.jsfunc.} = - if attr.ownerElement != nil and attr.ownerElement != map.element: + if attr.ownerElement == map.element: + # Setting attr on its owner element does nothing, since the "get an + # attribute by namespace and local name" step is used for retrieval + # (which will always return self). + return + if attr.jsOwnerElement != nil: return errDOMException("Attribute is currently in use", "InUseAttributeError") - if attr.name in map.element.attrs: - return ok(attr) - let i = map.findAttr(attr.name) + let i = map.element.findAttrNS(attr.data.namespace, attr.data.localName) + attr.ownerElement = map.element if i != -1: - result = ok(map.attrlist[i]) - map.attrlist.delete(i) - else: - result = ok(nil) - map.element.attrs[attr.name] = attr.value - map.attrlist.add(attr) + map.element.attrs[i] = attr.data + return ok(attr) + map.element.attrs.add(attr.data) + return ok(nil) proc setNamedItemNS(map: NamedNodeMap, attr: Attr): DOMResult[Attr] {.jsfunc.} = @@ -2628,19 +2863,19 @@ proc setNamedItemNS(map: NamedNodeMap, attr: Attr): DOMResult[Attr] proc removeNamedItem(map: NamedNodeMap, qualifiedName: string): DOMResult[Attr] {.jsfunc.} = - let i = map.findAttr(qualifiedName) + let i = map.element.findAttr(qualifiedName) if i != -1: - let attr = map.attrlist[i] - map.element.delAttr(i) + let attr = map.getAttr(i) + map.element.delAttr(i, keep = true) return ok(attr) return errDOMException("Item not found", "NotFoundError") proc removeNamedItemNS(map: NamedNodeMap, namespace, localName: string): DOMResult[Attr] {.jsfunc.} = - let i = map.findAttrNS(namespace, localName) + let i = map.element.findAttrNS(namespace, localName) if i != -1: - let attr = map.attrlist[i] - map.element.delAttr(i) + let attr = map.getAttr(i) + map.element.delAttr(i, keep = true) return ok(attr) return errDOMException("Item not found", "NotFoundError") @@ -2663,7 +2898,7 @@ proc remove*(node: Node, suppressObservers: bool) = node.parentNode = nil node.root = nil node.index = -1 - if node.nodeType == ELEMENT_NODE: + if node of Element: if Element(node).tagType in {TAG_STYLE, TAG_LINK} and node.document != nil: node.document.cachedSheetsInvalid = true @@ -2681,7 +2916,7 @@ proc adopt(document: Document, node: Node) = #TODO shadow root for desc in node.descendants: desc.document_internal = document - if desc.nodeType == ELEMENT_NODE: + if desc of Element: for attr in Element(desc).attributes.attrlist: attr.document_internal = document #TODO custom elements @@ -2766,7 +3001,7 @@ proc resetFormOwner(element: FormAssociatedElement) = element.setForm(HTMLFormElement(form)) proc insertionSteps(insertedNode: Node) = - if insertedNode.nodeType == ELEMENT_NODE: + if insertedNode of Element: let element = Element(insertedNode) let tagType = element.tagType case tagType @@ -2787,8 +3022,14 @@ proc insertionSteps(insertedNode: Node) = return element.resetFormOwner() +func isValidParent(node: Node): bool = + return node of Element or node of Document or node of DocumentFragment + +func isValidChild(node: Node): bool = + return node.isValidParent or node of DocumentType or node of CharacterData + func checkParentValidity(parent: Node): Err[DOMException] = - if parent.nodeType in {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE}: + if parent.isValidParent(): return ok() const msg = "Parent must be a document, a document fragment, or an element." return errDOMException(msg, "HierarchyRequestError") @@ -2803,40 +3044,37 @@ func preInsertionValidity*(parent, node, before: Node): Err[DOMException] = if before != nil and before.parentNode != parent: return errDOMException("Reference node is not a child of parent", "NotFoundError") - if node.nodeType notin {DOCUMENT_FRAGMENT_NODE, DOCUMENT_TYPE_NODE, - ELEMENT_NODE} + CharacterDataNodes: - return errDOMException("Cannot insert node type", - "HierarchyRequestError") - if node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE: + if not node.isValidChild(): + return errDOMException("Node is not a valid child", "HierarchyRequestError") + if node of Text and parent of Document: return errDOMException("Cannot insert text into document", "HierarchyRequestError") - if node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE: + if node of DocumentType and not (parent of Document): return errDOMException("Document type can only be inserted into document", "HierarchyRequestError") - if parent.nodeType == DOCUMENT_NODE: - case node.nodeType - of DOCUMENT_FRAGMENT_NODE: - let elems = node.countChildren(ELEMENT_NODE) - if elems > 1 or node.hasChild(TEXT_NODE): + if parent of Document: + if node of DocumentFragment: + let elems = node.countChildren(Element) + if elems > 1 or node.hasChild(Text): return errDOMException("Document fragment has invalid children", "HierarchyRequestError") - elif elems == 1 and (parent.hasChild(ELEMENT_NODE) or - before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or - before.hasNextSibling(DOCUMENT_TYPE_NODE))): + elif elems == 1 and (parent.hasChild(Element) or + before != nil and (before of DocumentType or + before.hasNextSibling(DocumentType))): return errDOMException("Document fragment has invalid children", "HierarchyRequestError") - of ELEMENT_NODE: - if parent.hasChild(ELEMENT_NODE): + elif node of Element: + if parent.hasChild(Element): return errDOMException("Document already has an element child", "HierarchyRequestError") - elif before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or - before.hasNextSibling(DOCUMENT_TYPE_NODE)): + elif before != nil and (before of DocumentType or + before.hasNextSibling(DocumentType)): return errDOMException("Cannot insert element before document type", "HierarchyRequestError") - of DOCUMENT_TYPE_NODE: - if parent.hasChild(DOCUMENT_TYPE_NODE) or - before != nil and before.hasPreviousSibling(ELEMENT_NODE) or - before == nil and parent.hasChild(ELEMENT_NODE): + elif node of DocumentType: + if parent.hasChild(DocumentType) or + before != nil and before.hasPreviousSibling(Element) or + before == nil and parent.hasChild(Element): const msg = "Cannot insert document type before an element node" return errDOMException(msg, "HierarchyRequestError") else: discard @@ -2857,28 +3095,29 @@ proc insertNode(parent, node, before: Node) = node.parentNode = parent node.invalidateCollections() parent.invalidateCollections() - if node.nodeType == ELEMENT_NODE: + if node of Element: if Element(node).tagType in {TAG_STYLE, TAG_LINK} and node.document != nil: node.document.cachedSheetsInvalid = true - if node.nodeType == ELEMENT_NODE: #TODO shadow root insertionSteps(node) # WARNING ditto proc insert*(parent, node, before: Node, suppressObservers = false) = - let nodes = if node.nodeType == DOCUMENT_FRAGMENT_NODE: node.childList - else: @[node] + let nodes = if node of DocumentFragment: + node.childList + else: + @[node] let count = nodes.len if count == 0: return - if node.nodeType == DOCUMENT_FRAGMENT_NODE: + if node of DocumentFragment: for i in countdown(node.childList.high, 0): node.childList[i].remove(true) #TODO tree mutation record if before != nil: #TODO live ranges discard - if parent.nodeType == ELEMENT_NODE: + if parent of Element: Element(parent).invalid = true for node in nodes: insertNode(parent, node, before) @@ -2918,43 +3157,36 @@ proc replace(parent, child, node: Node): Err[DOMException] = if child.parentNode != parent: return errDOMException("Node to replace is not a child of parent", "NotFoundError") - if node.nodeType notin {DOCUMENT_NODE, DOCUMENT_TYPE_NODE, ELEMENT_NODE} + - CharacterDataNodes: - return errDOMException("Replacement is not a valid replacement node type", - "HierarchyRequesError") - if node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE or - node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE: + if not node.isValidChild(): + return errDOMException("Node is not a valid child", "HierarchyRequesError") + if node of Text and parent of Document or + node of DocumentType and not (parent of Document): return errDOMException("Replacement cannot be placed in parent", "HierarchyRequesError") let childNextSibling = child.nextSibling let childPreviousSibling = child.previousSibling - if parent.nodeType == DOCUMENT_NODE: - case node.nodeType - of DOCUMENT_FRAGMENT_NODE: - let elems = node.countChildren(ELEMENT_NODE) - if elems > 1 or node.hasChild(TEXT_NODE): + if parent of Document: + if node of DocumentFragment: + let elems = node.countChildren(Element) + if elems > 1 or node.hasChild(Text): return errDOMException("Document fragment has invalid children", "HierarchyRequestError") - elif elems == 1 and (parent.hasChildExcept(ELEMENT_NODE, child) or - childNextSibling != nil and - childNextSibling.nodeType == DOCUMENT_TYPE_NODE): + elif elems == 1 and (parent.hasChildExcept(Element, child) or + childNextSibling != nil and childNextSibling of DocumentType): return errDOMException("Document fragment has invalid children", "HierarchyRequestError") - of ELEMENT_NODE: - if parent.hasChildExcept(ELEMENT_NODE, child): + elif node of Element: + if parent.hasChildExcept(Element, child): return errDOMException("Document already has an element child", "HierarchyRequestError") - elif childNextSibling != nil and - childNextSibling.nodeType == DOCUMENT_TYPE_NODE: + elif childNextSibling != nil and childNextSibling of DocumentType: return errDOMException("Cannot insert element before document type ", "HierarchyRequestError") - of DOCUMENT_TYPE_NODE: - if parent.hasChildExcept(DOCUMENT_TYPE_NODE, child) or - childPreviousSibling != nil and - childPreviousSibling.nodeType == DOCUMENT_TYPE_NODE: + elif node of DocumentType: + if parent.hasChildExcept(DocumentType, child) or + childPreviousSibling != nil and childPreviousSibling of DocumentType: const msg = "Cannot insert document type before an element node" return errDOMException(msg, "HierarchyRequestError") - else: discard let referenceChild = if childNextSibling == node: node.nextSibling else: @@ -2972,7 +3204,7 @@ proc replaceAll(parent, node: Node) = child.remove(true) assert parent != node if node != nil: - if node.nodeType == DOCUMENT_FRAGMENT_NODE: + if node of DocumentFragment: var addedNodes = node.childList # copy for child in addedNodes: parent.append(child) @@ -2984,18 +3216,16 @@ proc createTextNode*(document: Document, data: string): Text {.jsfunc.} = return newText(document, data) proc textContent*(node: Node, data: Option[string]) {.jsfset.} = - case node.nodeType - of DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE: + if node of Element or node of DocumentFragment: let x = if data.isSome: node.document.createTextNode(data.get) else: nil node.replaceAll(x) - of ATTRIBUTE_NODE: - value(Attr(node), data.get("")) - of TEXT_NODE, COMMENT_NODE: + elif node of CharacterData: CharacterData(node).data = data.get("") - else: discard + elif node of Attr: + value(Attr(node), data.get("")) proc reset*(form: HTMLFormElement) = for control in form.controls: @@ -3135,6 +3365,9 @@ proc execute*(element: HTMLScriptElement) = if element.scriptResult.t == RESULT_NULL: #TODO fire error event return + let needsInc = element.external or element.ctype == MODULE + if needsInc: + inc document.ignoreDestructiveWrites case element.ctype of CLASSIC: let oldCurrentScript = document.currentScript @@ -3153,6 +3386,8 @@ proc execute*(element: HTMLScriptElement) = ss.readAll()) document.currentScript = oldCurrentScript else: discard #TODO + if needsInc: + dec document.ignoreDestructiveWrites # https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element proc prepare*(element: HTMLScriptElement) = @@ -3218,7 +3453,7 @@ proc prepare*(element: HTMLScriptElement) = if src == "": #TODO fire error event return - element.fromAnExternalFile = true + element.external = true let url = element.document.parseURL(src) if url.isNone: #TODO fire error event @@ -3285,9 +3520,9 @@ proc createElement(document: Document, localName: string): return errDOMException("Invalid character in element name", "InvalidCharacterError") let localName = if not document.isxml: - localName.toLowerAscii() + document.toAtom(localName.toLowerAscii()) else: - localName + document.toAtom(localName) let namespace = if not document.isxml: #TODO or content type is application/xhtml+xml Namespace.HTML else: @@ -3307,11 +3542,11 @@ proc createDocumentType(implementation: ptr DOMImplementation, qualifiedName, let document = implementation.document return ok(document.newDocumentType(qualifiedName, publicId, systemId)) -proc createHTMLDocument(implementation: ptr DOMImplementation, title = - none(string)): Document {.jsfunc.} = - let doc = newDocument() +proc createHTMLDocument(ctx: JSContext, implementation: ptr DOMImplementation, + title = none(string)): Document {.jsfunc.} = + let doc = newDocument(ctx) doc.contentType = "text/html" - doc.append(doc.newDocumentType("html")) + doc.append(doc.newDocumentType("html", "", "")) let html = doc.newHTMLElement(TAG_HTML, Namespace.HTML) doc.append(html) let head = doc.newHTMLElement(TAG_HEAD, Namespace.HTML) @@ -3347,14 +3582,13 @@ proc createProcessingInstruction(document: Document, target, data: string): "InvalidCharacterError") return ok(newProcessingInstruction(document, target, data)) -func clone(node: Node, document = none(Document), deep = false): Node = +proc clone(node: Node, document = none(Document), deep = false): Node = let document = document.get(node.document) - let copy = case node.nodeType - of ELEMENT_NODE: + let copy = if node of Element: #TODO is value let element = Element(node) let x = document.newHTMLElement(element.localName, element.namespace, - element.namespacePrefix, element.tagType, element.attrs) + element.namespacePrefix, element.attrs) #TODO namespaced attrs? # Cloning steps if x.tagType == TAG_SCRIPT: @@ -3369,43 +3603,50 @@ func clone(node: Node, document = none(Document), deep = false): Node = x.checked = element.checked #TODO dirty checkedness flag Node(x) - of ATTRIBUTE_NODE: + elif node of Attr: let attr = Attr(node) - let x = document.newAttr(attr.localName, attr.value, attr.prefix, - attr.namespaceURI) + let data = attr.data + let x = Attr( + ownerElement: AttrDummyElement( + document_internal: attr.ownerElement.document, + index: -1, + attrs: @[data] + ), + dataIdx: 0 + ) Node(x) - of TEXT_NODE: + elif node of Text: let text = Text(node) let x = document.newText(text.data) Node(x) - of CDATA_SECTION_NODE: + elif node of CDATASection: let x = document.newCDATASection("") #TODO is this really correct?? # really, I don't know. only relevant with xhtml anyway... Node(x) - of COMMENT_NODE: + elif node of Comment: let comment = Comment(node) let x = document.newComment(comment.data) Node(x) - of PROCESSING_INSTRUCTION_NODE: + elif node of ProcessingInstruction: let procinst = ProcessingInstruction(node) let x = document.newProcessingInstruction(procinst.target, procinst.data) Node(x) - of DOCUMENT_NODE: + elif node of Document: let document = Document(node) - let x = newDocument() + let x = newDocument(document.factory) x.charset = document.charset x.contentType = document.contentType x.url = document.url x.isxml = document.isxml x.mode = document.mode Node(x) - of DOCUMENT_TYPE_NODE: + elif node of DocumentType: let doctype = DocumentType(node) let x = document.newDocumentType(doctype.name, doctype.publicId, doctype.systemId) Node(x) - of DOCUMENT_FRAGMENT_NODE: + elif node of DocumentFragment: let x = document.newDocumentFragment() Node(x) else: @@ -3416,7 +3657,7 @@ func clone(node: Node, document = none(Document), deep = false): Node = copy.append(child.clone(deep = true)) return copy -func cloneNode(node: Node, deep = false): Node {.jsfunc.} = +proc cloneNode(node: Node, deep = false): Node {.jsfunc.} = #TODO shadow root return node.clone(deep = deep) @@ -3495,7 +3736,9 @@ proc jsReflectSet(ctx: JSContext, this, val: JSValue, magic: cint): JSValue {.cd if x.get: element.attr(entry.attrname, "") else: - element.delAttr(entry.attrname) + let i = element.findAttr(entry.attrname) + if i != -1: + element.delAttr(i) of REFLECT_LONG: let x = fromJS[int32](ctx, val) if x.isSome: @@ -3575,11 +3818,11 @@ proc outerHTML(element: Element, s: string): Err[DOMException] {.jsfset.} = let parent0 = element.parentNode if parent0 == nil: return ok() - if parent0.nodeType == DOCUMENT_NODE: + if parent0 of Document: let ex = newDOMException("outerHTML is disallowed for Document children", "NoModificationAllowedError") return err(ex) - let parent = if parent0.nodeType == DOCUMENT_FRAGMENT_NODE: + let parent = if parent0 of DocumentFragment: element.document.newHTMLElement(TAG_BODY) else: # neither a document, nor a document fragment => parent must be an @@ -3594,8 +3837,7 @@ proc insertAdjacentHTML(element: Element, position, text: string): #TODO enumize position let ctx0 = case position of "beforebegin", "afterend": - if element.parentNode.nodeType == DOCUMENT_NODE or - element.parentNode == nil: + if element.parentNode of Document or element.parentNode == nil: return errDOMException("Parent is not a valid element", "NoModificationAllowedError") element.parentNode @@ -3604,7 +3846,7 @@ proc insertAdjacentHTML(element: Element, position, text: string): else: return errDOMException("Invalid position", "SyntaxError") let document = ctx0.document - let ctx = if ctx0.nodeType != ELEMENT_NODE or not document.isxml or + let ctx = if not (ctx0 of Element) or not document.isxml or Element(ctx0).namespace == Namespace.HTML: document.newHTMLElement(TAG_BODY) else: diff --git a/src/html/enums.nim b/src/html/enums.nim index 24f496bb..67ae4e76 100644 --- a/src/html/enums.nim +++ b/src/html/enums.nim @@ -16,6 +16,21 @@ type ButtonType* = enum BUTTON_SUBMIT, BUTTON_RESET, BUTTON_BUTTON + NodeType* = enum + ELEMENT_NODE = 1, + ATTRIBUTE_NODE = 2, + TEXT_NODE = 3, + CDATA_SECTION_NODE = 4, + ENTITY_REFERENCE_NODE = 5, + ENTITY_NODE = 6 + PROCESSING_INSTRUCTION_NODE = 7, + COMMENT_NODE = 8, + DOCUMENT_NODE = 9, + DOCUMENT_TYPE_NODE = 10, + DOCUMENT_FRAGMENT_NODE = 11, + NOTATION_NODE = 12 + + #TODO support all the other ones const SupportedFormAssociatedElements* = { TAG_BUTTON, TAG_INPUT, TAG_SELECT, TAG_TEXTAREA @@ -31,7 +46,8 @@ const AutocapitalizeInheritingElements* = { const LabelableElements* = { # input only if type not hidden - TAG_BUTTON, TAG_INPUT, TAG_METER, TAG_OUTPUT, TAG_PROGRESS, TAG_SELECT, TAG_TEXTAREA + TAG_BUTTON, TAG_INPUT, TAG_METER, TAG_OUTPUT, TAG_PROGRESS, TAG_SELECT, + TAG_TEXTAREA } # https://html.spec.whatwg.org/multipage/syntax.html#void-elements @@ -40,6 +56,10 @@ const VoidElements* = { TAG_LINK, TAG_META, TAG_SOURCE, TAG_TRACK, TAG_WBR } +const ResettableElements* = { + TAG_INPUT, TAG_OUTPUT, TAG_SELECT, TAG_TEXTAREA +} + func getInputTypeMap(): Table[string, InputType] = for i in InputType: let enumname = $InputType(i) diff --git a/src/html/env.nim b/src/html/env.nim index f462c347..9eff5e03 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -3,6 +3,7 @@ import std/streams import bindings/quickjs import display/winattrs +import html/catom import html/chadombuilder import html/dom import html/event @@ -176,8 +177,8 @@ proc runJSJobs*(window: Window) = window.jsrt.runJSJobs(window.console.err) proc newWindow*(scripting: bool, selector: Selector[int], - attrs: WindowAttributes, navigate: proc(url: URL) = nil, - loader = none(FileLoader)): Window = + attrs: WindowAttributes, factory: CAtomFactory, + navigate: proc(url: URL) = nil, loader = none(FileLoader)): Window = let err = newFileStream(stderr) let window = Window( attrs: attrs, @@ -187,7 +188,8 @@ proc newWindow*(scripting: bool, selector: Selector[int], settings: EnvironmentSettings( scripting: scripting ), - navigate: navigate + navigate: navigate, + factory: factory ) window.location = window.newLocation() if scripting: diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 093bc2d7..abb116c6 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -18,6 +18,7 @@ import css/sheet import css/stylednode import css/values import display/winattrs +import html/catom import html/chadombuilder import html/dom import html/enums @@ -120,6 +121,9 @@ type estream: Stream # error stream ishtml: bool ssock: ServerSocket + factory: CAtomFactory + uastyle: CSSStylesheet + quirkstyle: CSSStylesheet InterfaceOpaque = ref object stream: Stream @@ -626,19 +630,14 @@ proc gotoAnchor*(buffer: Buffer): Opt[tuple[x, y: int]] {.proxy.} = return ok((format.pos, y)) return err() -const css = staticRead"res/ua.css" -let uastyle = css.parseStylesheet() -const quirk = css & staticRead"res/quirk.css" -let quirkstyle = quirk.parseStylesheet() - proc do_reshape(buffer: Buffer) = if buffer.ishtml: if buffer.document == nil: return # not parsed yet, nothing to render let uastyle = if buffer.document.mode != QUIRKS: - uastyle + buffer.uastyle else: - quirkstyle + buffer.quirkstyle let styledRoot = buffer.document.applyStylesheets(uastyle, buffer.userstyle, buffer.prevstyled) buffer.lines = renderDocument(styledRoot, buffer.attrs) @@ -725,7 +724,7 @@ proc loadResource(buffer: Buffer, elem: HTMLLinkElement): EmptyPromise = #TODO non-utf-8 css let ds = newDecoderStream(ss, cs = CHARSET_UTF_8) let source = newEncoderStream(ds, cs = CHARSET_UTF_8) - elem.sheet = parseStylesheet(source)) + elem.sheet = parseStylesheet(source, buffer.factory)) proc loadResource(buffer: Buffer, elem: HTMLImageElement): EmptyPromise = let document = buffer.document @@ -783,6 +782,33 @@ type ConnectResult* = object referrerpolicy*: Option[ReferrerPolicy] charset*: Charset +proc setHTML(buffer: Buffer, ishtml: bool) = + buffer.ishtml = ishtml + if ishtml: + let factory = newCAtomFactory() + buffer.factory = factory + if buffer.config.scripting: + buffer.window = newWindow( + buffer.config.scripting, + buffer.selector, + buffer.attrs, + factory, + proc(url: URL) = buffer.navigate(url), + some(buffer.loader) + ) + else: + buffer.window = newWindow( + buffer.config.scripting, + buffer.selector, + buffer.attrs, + buffer.factory + ) + const css = staticRead"res/ua.css" + const quirk = css & staticRead"res/quirk.css" + buffer.uastyle = css.parseStylesheet(factory) + buffer.quirkstyle = quirk.parseStylesheet(factory) + buffer.userstyle = parseStylesheet(buffer.config.userstyle, factory) + proc connect*(buffer: Buffer): ConnectResult {.proxy.} = if buffer.connected: return ConnectResult(invalid: true) @@ -841,7 +867,7 @@ proc connect*(buffer: Buffer): ConnectResult {.proxy.} = buffer.loader.setReferrerPolicy(referrerpolicy.get) buffer.connected = true let contentType = buffer.source.contentType.get("") - buffer.ishtml = contentType == "text/html" + buffer.setHTML(contentType == "text/html") return ConnectResult( charset: charset, needsAuth: needsAuth, @@ -904,7 +930,7 @@ proc readFromFd*(buffer: Buffer, fd: FileHandle, ishtml: bool) {.proxy.} = contentType: some(contentType), charset: buffer.source.charset ) - buffer.ishtml = ishtml + buffer.setHTML(ishtml) discard fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) or O_NONBLOCK) buffer.istream = newPosixStream(fd) buffer.fd = fd @@ -912,7 +938,7 @@ proc readFromFd*(buffer: Buffer, fd: FileHandle, ishtml: bool) {.proxy.} = proc setContentType*(buffer: Buffer, contentType: string) {.proxy.} = buffer.source.contentType = some(contentType) - buffer.ishtml = contentType == "text/html" + buffer.setHTML(contentType == "text/html") # As defined in std/selectors: this determines whether kqueue is being used. # On these platforms, we must not close the selector after fork, since kqueue @@ -1114,11 +1140,13 @@ proc finishLoad(buffer: Buffer): EmptyPromise = if buffer.ishtml: buffer.sstream.setPosition(0) buffer.available = 0 - if buffer.window == nil: - buffer.window = newWindow(buffer.config.scripting, buffer.selector, - buffer.attrs) - let document = parseHTML(buffer.sstream, charsets = buffer.charsets, - window = buffer.window, url = buffer.url) + let document = parseHTML( + buffer.sstream, + charsets = buffer.charsets, + window = buffer.window, + url = buffer.url, + factory = buffer.factory + ) buffer.document = document document.readyState = READY_STATE_INTERACTIVE buffer.state = LOADING_RESOURCES @@ -1211,12 +1239,14 @@ proc cancel*(buffer: Buffer): int {.proxy.} = if buffer.ishtml: buffer.sstream.setPosition(0) buffer.available = 0 - if buffer.window == nil: - buffer.window = newWindow(buffer.config.scripting, buffer.selector, - buffer.attrs) - buffer.document = parseHTML(buffer.sstream, - charsets = buffer.charsets, window = buffer.window, - url = buffer.url, canReinterpret = false) + buffer.document = parseHTML( + buffer.sstream, + charsets = buffer.charsets, + window = buffer.window, + url = buffer.url, + factory = buffer.factory, + seekable = false + ) buffer.do_reshape() return buffer.lines.len @@ -1811,7 +1841,6 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource, let socks = ssock.acceptSocketStream() let buffer = Buffer( alive: true, - userstyle: parseStylesheet(config.userstyle), attrs: attrs, config: config, loader: loader, @@ -1835,9 +1864,6 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource, buffer.selector.registerHandle(fd, {Read}, 0) loader.unregisterFun = proc(fd: int) = buffer.selector.unregister(fd) - if buffer.config.scripting: - buffer.window = newWindow(buffer.config.scripting, buffer.selector, - buffer.attrs, proc(url: URL) = buffer.navigate(url), some(buffer.loader)) buffer.selector.registerHandle(buffer.rfd, {Read}, 0) buffer.runBuffer() buffer.cleanup() diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim index e5cc5aa0..7d83d5b2 100644 --- a/src/xhr/formdata.nim +++ b/src/xhr/formdata.nim @@ -137,7 +137,7 @@ proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil, if field.tagType == TAG_SELECT: let field = HTMLSelectElement(field) for option in field.options: - if option.selected or option.disabled: + if option.selected or option.isDisabled: 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") != "": diff --git a/todo b/todo index eed500d4..8f2b6d1b 100644 --- a/todo +++ b/todo @@ -56,6 +56,10 @@ external: - history, bookmarks (w3m format) - save buffer (source, output) - edit buffer (local file, or whatever we downloaded) +dom: +- atomize `id' + * maybe also add `name' attr atom? +- add attr name enum (like TagType) javascript: - important: callbacks should not leak memory - add support for JS mixins @@ -66,6 +70,7 @@ javascript: - implement ReadableStream, XHR - separate console for each buffer - buffer selection +- errTypeError etc templates layout engine: - box borders * will probably need special treatment, as borders must round to |