diff options
author | bptato <nincsnevem662@gmail.com> | 2022-12-19 21:03:15 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2022-12-19 21:03:15 +0100 |
commit | ea9df035a294bf1cfa715c140d0d22aa018e262e (patch) | |
tree | 9f1ec79e96003494666970c16f905c0b0c9608ff /src/html | |
parent | dad0c1c04b6d4f67da407f69cec98221d178c194 (diff) | |
download | chawan-ea9df035a294bf1cfa715c140d0d22aa018e262e.tar.gz |
More DOM work
Diffstat (limited to 'src/html')
-rw-r--r-- | src/html/dom.nim | 777 | ||||
-rw-r--r-- | src/html/env.nim | 2 | ||||
-rw-r--r-- | src/html/htmlparser.nim | 15 | ||||
-rw-r--r-- | src/html/tags.nim | 4 |
4 files changed, 548 insertions, 250 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim index 4dfe5c3a..ed96173d 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -81,9 +81,13 @@ type jsrt*: JSRuntime jsctx*: JSContext document* {.jsget.}: Document - console* {.jsget.}: Console + console* {.jsget.}: console - Console* = ref object + # "For historical reasons, console is lowercased." + # Also, for a more practical reason: so the javascript macros don't confuse + # this and the Client console. + # TODO: merge those two + console* = ref object err: Stream NamedNodeMap = ref object @@ -95,15 +99,35 @@ type EventTarget* = ref object of RootObj + #TODO this has caching, but invalidation is pretty expensive... not sure if + # it's worth the trouble at all... + Collection = ref CollectionObj + CollectionObj = object of RootObj + islive: bool + invalid: bool + childonly: bool + root: Node + match: proc(node: Node): bool {.noSideEffect.} + snapshot: seq[Node] + livelen: int + + NodeList = ref object of Collection + + HTMLCollection = ref object of Collection + Node* = ref object of EventTarget nodeType* {.jsget.}: NodeType - childNodes* {.jsget.}: seq[Node] - nextSibling*: Node - previousSibling*: Node + childList*: seq[Node] + nextSibling* {.jsget.}: Node + previousSibling* {.jsget.}: Node parentNode* {.jsget.}: Node parentElement* {.jsget.}: Element root: Node document*: Document + # Live collection cache: if parentHasCollections is true, recursively + # invalidate all of them on insert. + parentHasCollections: bool + liveCollections: seq[Collection] Attr* = ref object of Node namespace: Namespace @@ -156,8 +180,8 @@ type localName*: string tagType*: TagType - id*: string - classList*: seq[string] + id* {.jsget.}: string + classList* {.jsget.}: seq[string] #TODO should be DomTokenList attrs*: Table[string, string] attributes* {.jsget.}: NamedNodeMap hover*: bool @@ -174,7 +198,6 @@ type autofocus*: bool required*: bool value* {.jsget.}: string - size*: int checked*: bool xcoord*: int ycoord*: int @@ -184,7 +207,6 @@ type HTMLSelectElement* = ref object of FormAssociatedElement form* {.jsget.}: HTMLFormElement - size*: int HTMLSpanElement* = ref object of HTMLElement @@ -253,10 +275,17 @@ type HTMLTextAreaElement* = ref object of FormAssociatedElement form* {.jsget.}: HTMLFormElement - rows*: int - cols*: int value* {.jsget.}: string +proc `=destroy`(collection: var CollectionObj) = + var i = -1 + for j in 0 ..< collection.root.liveCollections.len: + if cast[pointer](collection.root.liveCollections[j]) == addr collection: + i = j + break + assert i != -1 + collection.root.liveCollections.del(i) + const NamespaceMap = (func(): Table[string, Namespace] = for ns in Namespace: result[$ns] = ns @@ -290,7 +319,7 @@ func escapeText(s: string, attribute_mode = false): string = result &= c func `$`*(node: Node): string = - if node == nil: return "nil" #TODO this isn't standard compliant but helps debugging + if node == nil: return "null" #TODO this isn't standard compliant but helps debugging case node.nodeType of ELEMENT_NODE: let element = Element(node) @@ -298,7 +327,7 @@ func `$`*(node: Node): string = for k, v in element.attrs: result &= ' ' & k & "=\"" & v.escapeText(true) & "\"" result &= ">\n" - for node in element.childNodes: + for node in element.childList: for line in ($node).split('\n'): result &= "\t" & line & "\n" result &= "</" & $element.tagType.tostr() & ">" @@ -314,22 +343,17 @@ func `$`*(node: Node): string = else: result = "Node of " & $node.nodeType -iterator children*(node: Node): Element {.inline.} = - for child in node.childNodes: +iterator elementList*(node: Node): Element {.inline.} = + for child in node.childList: if child.nodeType == ELEMENT_NODE: yield Element(child) -iterator children_rev*(node: Node): Element {.inline.} = - for i in countdown(node.childNodes.high, 0): - let child = node.childNodes[i] +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: yield Element(child) -#TODO TODO TODO this should return a live view instead -proc children*(node: Node): seq[Element] {.jsfget.} = - for child in node.children: - result.add(child) - # Returns the node's ancestors iterator ancestors*(node: Node): Element {.inline.} = var element = node.parentElement @@ -350,9 +374,9 @@ iterator descendants*(node: Node): Node {.inline.} = stack.add(node) while stack.len > 0: let node = stack.pop() - for i in countdown(node.childNodes.high, 0): - yield node.childNodes[i] - stack.add(node.childNodes[i]) + for i in countdown(node.childList.high, 0): + yield node.childList[i] + stack.add(node.childList[i]) iterator elements*(node: Node): Element {.inline.} = for child in node.descendants: @@ -389,16 +413,16 @@ iterator radiogroup*(input: HTMLInputElement): HTMLInputElement {.inline.} = yield input iterator textNodes*(node: Node): Text {.inline.} = - for node in node.childNodes: + for node in node.childList: if node.nodeType == TEXT_NODE: yield Text(node) iterator options*(select: HTMLSelectElement): HTMLOptionElement {.inline.} = - for child in select.children: + for child in select.elementList: if child.tagType == TAG_OPTION: yield HTMLOptionElement(child) elif child.tagType == TAG_OPTGROUP: - for opt in child.children: + for opt in child.elementList: if opt.tagType == TAG_OPTION: yield HTMLOptionElement(child) @@ -406,6 +430,75 @@ iterator items(attributes: NamedNodeMap): Attr {.inline.} = for attr in attributes.attrlist: yield attr +proc populateCollection(collection: Collection) = + if collection.childonly: + for child in collection.root.childList: + if collection.match == nil or collection.match(child): + collection.snapshot.add(child) + else: + for desc in collection.root.descendants: + if collection.match == nil or collection.match(desc): + collection.snapshot.add(desc) + +proc refreshCollection(collection: Collection) = + if collection.invalid: + collection.snapshot.setLen(0) + collection.populateCollection() + collection.invalid = false + +func ownerDocument(node: Node): Document {.jsfget.} = + if node.nodeType == DOCUMENT_NODE: + return nil + return node.document + +func hasChildNodes(node: Node): bool {.jsfget.} = + return node.childList.len > 0 + +func len(collection: Collection): int = + collection.refreshCollection() + return collection.snapshot.len + +func newCollection[T: Collection](root: Node, match: proc(node: Node): bool {.noSideEffect.}, islive: bool): T = + result = T( + islive: islive, + match: match, + root: root + ) + result.populateCollection() + if islive: + root.liveCollections.add(result) + for desc in root.descendants: + desc.parentHasCollections = true + +func isElement(node: Node): bool = + return node.nodeType == ELEMENT_NODE + +func children*(node: Node): HTMLCollection {.jsfget.} = + return newCollection[HTMLCollection](node, isElement, true) + +func childNodes(node: Node): NodeList {.jsfget.} = + return newCollection[NodeList](node, nil, true) + +func length(nodeList: NodeList): int {.jsfget.} = + return nodeList.len + +func hasprop(nodeList: NodeList, i: int): bool {.jshasprop.} = + return i < nodeList.len + +func getter(nodeList: NodeList, i: int): Option[Node] {.jsgetprop.} = + if i < nodeList.len: + return some(nodeList.snapshot[i]) + +func length(collection: HTMLCollection): int {.jsfget.} = + return collection.len + +func hasprop(collection: HTMLCollection, i: int): bool {.jshasprop.} = + return i < collection.len + +func getter(collection: HTMLCollection, i: int): Option[Element] {.jsgetprop.} = + if i < collection.len: + return some(Element(collection.snapshot[i])) + func newAttr(parent: Element, localName, value: string, prefix = "", namespace = NO_NAMESPACE): Attr = return Attr( nodeType: ATTRIBUTE_NODE, @@ -569,12 +662,12 @@ func select*(option: HTMLOptionElement): HTMLSelectElement = return nil func countChildren(node: Node, nodeType: NodeType): int = - for child in node.childNodes: + for child in node.childList: if child.nodeType == nodeType: inc result func hasChild(node: Node, nodeType: NodeType): bool = - for child in node.childNodes: + for child in node.childList: if child.nodeType == nodeType: return false @@ -592,37 +685,40 @@ func hasPreviousSibling(node: Node, nodeType: NodeType): bool = node = node.previousSibling return false +func nodeValue(node: Node): Option[string] {.jsfget.} = + case node.nodeType + of CharacterDataNodes: + return some(CharacterData(node).data) + of ATTRIBUTE_NODE: + return some(Attr(node).value) + else: discard + +func textContent*(node: Node): string {.jsfget.} = + case node.nodeType + of DOCUMENT_NODE, DOCUMENT_TYPE_NODE: + return "" #TODO null + of CharacterDataNodes: + return CharacterData(node).data + else: + for child in node.childList: + if child.nodeType != COMMENT_NODE: + result &= child.textContent + +func childTextContent*(node: Node): string = + for child in node.childList: + if child.nodeType == TEXT_NODE: + result &= Text(child).data + func rootNode*(node: Node): Node = if node.root == nil: return node return node.root -func connected*(node: Node): bool = +func isConnected*(node: Node): bool {.jsfget.} = return node.rootNode.nodeType == DOCUMENT_NODE #TODO shadow root func inSameTree*(a, b: Node): bool = a.rootNode == b.rootNode -func filterDescendants*(element: Element, predicate: (proc(child: Element): bool)): seq[Element] = - var stack: seq[Element] - for child in element.children_rev: - stack.add(child) - while stack.len > 0: - let child = stack.pop() - if predicate(child): - result.add(child) - for child in element.children_rev: - stack.add(child) - -func all_descendants*(element: Element): seq[Element] = - var stack: seq[Element] - for child in element.children_rev: - stack.add(child) - while stack.len > 0: - let child = stack.pop() - result.add(child) - for child in element.children_rev: - stack.add(child) - # a == b or b in a's ancestors func contains*(a, b: Node): bool = for node in a.branch: @@ -630,41 +726,108 @@ func contains*(a, b: Node): bool = return false func firstChild*(node: Node): Node {.jsfget.} = - if node.childNodes.len == 0: + if node.childList.len == 0: return nil - return node.childNodes[0] + return node.childList[0] func lastChild*(node: Node): Node {.jsfget.} = - if node.childNodes.len == 0: + if node.childList.len == 0: return nil - return node.childNodes[^1] + return node.childList[^1] func firstElementChild*(node: Node): Element {.jsfget.} = - for child in node.children: + for child in node.elementList: return child return nil func lastElementChild*(node: Node): Element {.jsfget.} = - for child in node.children: + for child in node.elementList: return child return nil +func findAncestor*(node: Node, tagTypes: set[TagType]): Element = + for element in node.ancestors: + if element.tagType in tagTypes: + return element + return nil + +func getElementById*(node: Node, id: string): Element {.jsfunc.} = + if id.len == 0: + return nil + for child in node.elements: + if child.id == id: + return child + +func getElementsByTag*(node: Node, tag: TagType): seq[Element] = + for element in node.elements(tag): + result.add(element) + +func getElementsByTagName(node: Node, tagName: string): HTMLCollection {.jsfunc.} = + if tagName == "*": + return newCollection[HTMLCollection](node, func(node: Node): bool = node.isElement, true) + let t = tagType(tagName) + if t != TAG_UNKNOWN: + return newCollection[HTMLCollection](node, func(node: Node): bool = node.isElement and Element(node).tagType == t, true) + +func getElementsByClassName(node: Node, classNames: string): HTMLCollection {.jsfunc.} = + var classes = classNames.split(AsciiWhitespace) + let isquirks = node.document.mode == QUIRKS + if isquirks: + for i in 0 .. classes.high: + classes[i].toLowerAsciiInPlace() + return newCollection[HTMLCollection](node, + func(node: Node): bool = + if node.nodeType == ELEMENT_NODE: + if isquirks: + var cl = Element(node).classList + for i in 0 .. cl.high: + cl[i].toLowerAsciiInPlace() + for class in classes: + if class notin cl: + return false + else: + for class in classes: + if class notin Element(node).classList: + return false + return true, true) + +func filterDescendants*(element: Element, predicate: (proc(child: Element): bool)): seq[Element] = + var stack: seq[Element] + for child in element.elementList_rev: + stack.add(child) + while stack.len > 0: + let child = stack.pop() + if predicate(child): + result.add(child) + for child in element.elementList_rev: + stack.add(child) + +func all_descendants*(element: Element): seq[Element] = + var stack: seq[Element] + for child in element.elementList_rev: + stack.add(child) + while stack.len > 0: + let child = stack.pop() + result.add(child) + for child in element.elementList_rev: + stack.add(child) + func previousElementSibling*(elem: Element): Element {.jsfget.} = if elem.parentNode == nil: return nil - var i = elem.parentNode.childNodes.find(elem) + var i = elem.parentNode.childList.find(elem) dec i while i >= 0: - if elem.parentNode.childNodes[i].nodeType == ELEMENT_NODE: + if elem.parentNode.childList[i].nodeType == ELEMENT_NODE: return elem dec i return nil func nextElementSibling*(elem: Element): Element {.jsfget.} = if elem.parentNode == nil: return nil - var i = elem.parentNode.childNodes.find(elem) + var i = elem.parentNode.childList.find(elem) inc i - while i < elem.parentNode.childNodes.len: - if elem.parentNode.childNodes[i].nodeType == ELEMENT_NODE: + while i < elem.parentNode.childList.len: + if elem.parentNode.childList[i].nodeType == ELEMENT_NODE: return elem inc i return nil @@ -682,34 +845,44 @@ func attri*(element: Element, s: string): Option[int] = except ValueError: return none(int) +func attrigz*(element: Element, s: string): Option[int] = + let a = element.attr(s) + try: + let i = parseInt(a) + if i > 0: + return some(i) + except ValueError: + discard + func attrb*(element: Element, s: string): bool = if s in element.attrs: return true return false +# Element attribute reflection (getters) +func className(element: Element): string {.jsfget.} = + element.attr("class") + +#TODO implement JS union types for ref object... +func size*(element: HTMLInputElement): int {.jsfget.} = + element.attrigz("size").get(20) + +func size*(element: HTMLSelectElement): int {.jsfget.} = + element.attrigz("size").get(20) + +func cols*(element: HTMLTextAreaElement): int {.jsfget.} = + element.attrigz("cols").get(20) + +func rows*(element: HTMLTextAreaElement): int {.jsfget.} = + element.attrigz("rows").get(1) + func innerHTML*(element: Element): string {.jsfget.} = - for child in element.childNodes: + for child in element.childList: result &= $child func outerHTML*(element: Element): string {.jsfget.} = return $element -func textContent*(node: Node): string {.jsfget.} = - case node.nodeType - of DOCUMENT_NODE, DOCUMENT_TYPE_NODE: - return "" #TODO null - of CDATA_SECTION_NODE, COMMENT_NODE, PROCESSING_INSTRUCTION_NODE, TEXT_NODE: - return CharacterData(node).data - else: - for child in node.childNodes: - if child.nodeType != COMMENT_NODE: - result &= child.textContent - -func childTextContent*(node: Node): string = - for child in node.childNodes: - if child.nodeType == TEXT_NODE: - result &= Text(child).data - func crossorigin(element: HTMLScriptElement): CORSAttribute = if not element.attrb("crossorigin"): return NO_CORS @@ -724,7 +897,7 @@ func referrerpolicy(element: HTMLScriptElement): Option[ReferrerPolicy] = getReferrerPolicy(element.attr("referrerpolicy")) proc sheets*(element: Element): seq[CSSStylesheet] = - for child in element.children: + for child in element.elementList: if child.tagType == TAG_STYLE: let child = HTMLStyleElement(child) if child.sheet_invalid: @@ -845,39 +1018,29 @@ func target*(element: Element): string {.jsfunc.} = return base.attr("target") return "" -func findAncestor*(node: Node, tagTypes: set[TagType]): Element = - for element in node.ancestors: - if element.tagType in tagTypes: - return element - return nil - func newText*(document: Document, data: string = ""): Text {.jsctor.} = new(result) result.nodeType = TEXT_NODE result.document = document result.data = data -func textContent*(node: Node, data: string) {.jsfset.} = - node.childNodes.setLen(0) - node.childNodes.add(node.document.newText(data)) - func newComment*(document: Document = nil, data: string = ""): Comment {.jsctor.} = new(result) result.nodeType = COMMENT_NODE result.document = document result.data = data +proc attr*(element: Element, name, value: string) + #TODO custom elements -func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace.HTML, prefix = none[string]()): HTMLElement = +func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace.HTML, prefix = none[string](), attrs = Table[string, string]()): HTMLElement = case tagType of TAG_INPUT: result = new(HTMLInputElement) - HTMLInputElement(result).size = 20 of TAG_A: result = new(HTMLAnchorElement) of TAG_SELECT: result = new(HTMLSelectElement) - HTMLSelectElement(result).size = 1 of TAG_OPTGROUP: result = new(HTMLOptGroupElement) of TAG_OPTION: @@ -919,16 +1082,20 @@ func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace result = new(HTMLTextAreaElement) else: result = new(HTMLElement) - result.nodeType = ELEMENT_NODE result.tagType = tagType result.namespace = namespace result.namespacePrefix = prefix result.document = document result.attributes = NamedNodeMap(element: result) - -func newHTMLElement*(document: Document, localName: string, namespace = Namespace.HTML, prefix = none[string](), tagType = tagType(localName)): Element = - result = document.newHTMLElement(tagType, namespace, prefix) + {.cast(noSideEffect).}: + for k, v in attrs: + result.attr(k, v) + if tagType == TAG_SCRIPT: + HTMLScriptElement(result).internalNonce = result.attr("nonce") + +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 @@ -940,32 +1107,12 @@ func newDocument*(): Document {.jsctor.} = func newDocumentType*(document: Document, name: string, publicId = "", systemId = ""): DocumentType {.jsctor.} = new(result) + result.nodeType = DOCUMENT_TYPE_NODE result.document = document result.name = name result.publicId = publicId result.systemId = systemId -func getElementById*(node: Node, id: string): Element {.jsfunc.} = - if id.len == 0: - return nil - for child in node.elements: - if child.id == id: - return child - -func getElementsByTag*(node: Node, tag: TagType): seq[Element] = - for element in node.elements(tag): - result.add(element) - -func getElementsByTagName(node: Node, tagName: string): seq[Element] {.jsfunc.} = - let tagName = tagType(tagName) - if tagName != TAG_UNKNOWN: - return node.getElementsByTag(tagName) - -func getElementsByClassName(node: Node, class: string): seq[Element] {.jsfunc.} = - for element in node.elements: - if class in element.classList: - result.add(element) - func inHTMLNamespace*(element: Element): bool = element.namespace == Namespace.HTML func inMathMLNamespace*(element: Element): bool = element.namespace == Namespace.MATHML func inSVGNamespace*(element: Element): bool = element.namespace == Namespace.SVG @@ -1040,59 +1187,136 @@ func value*(option: HTMLOptionElement): string {.jsfget.} = return option.attr("value") return option.childTextContent.stripAndCollapse() -# WARNING the ordering of the arguments in the standard is whack so this doesn't match that -func preInsertionValidity*(parent, node, before: Node): bool = - if parent.nodeType notin {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE}: - # HierarchyRequestError - return false - if node.isHostIncludingInclusiveAncestor(parent): - # HierarchyRequestError - return false - if before != nil and before.parentNode != parent: - # NotFoundError - return false - if node.nodeType notin {DOCUMENT_FRAGMENT_NODE, DOCUMENT_TYPE_NODE, ELEMENT_NODE, CDATA_SECTION_NODE}: - # HierarchyRequestError - return false - if (node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE) or - (node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE): - # HierarchyRequestError - return false - 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): - # HierarchyRequestError - return false - elif elems == 1 and (parent.hasChild(ELEMENT_NODE) or before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or before.hasNextSibling(DOCUMENT_TYPE_NODE))): - # HierarchyRequestError - return false - of ELEMENT_NODE: - if parent.hasChild(ELEMENT_NODE) or before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or before.hasNextSibling(DOCUMENT_TYPE_NODE)): - # HierarchyRequestError - return false - 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): - # HierarchyRequestError - return false - else: discard - return true # no exception reached +proc invalidateCollections(node: Node): bool {.discardable.} = + for collection in node.liveCollections: + collection.invalid = true + if node.parentHasCollections: + if not node.parentNode.invalidateCollections(): + node.parentHasCollections = false + return node.liveCollections.len != 0 or node.parentHasCollections + +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.attributes.attrlist.delete(i) + element.invalidateCollections() + element.invalid = true + +proc reflectAttrs(element: Element, name, value: string) = + template reflect_str(element: Element, n: static string, val: untyped) = + if name == n: + element.val = value + return + template reflect_str(element: Element, n: static string, val, fun: untyped) = + if name == n: + element.val = fun(value) + template reflect_bool(element: Element, name: static string, val: untyped) = + if name in element.attrs: + element.val = true + element.reflect_str "id", id + if name == "class": + element.classList.setLen(0) + let classList = value.split(AsciiWhitespace) + for x in classList: + if x != "" and x notin element.classList: + element.classList.add(x) + return + case element.tagType + of TAG_INPUT: + let input = HTMLInputElement(element) + input.reflect_str "value", value + input.reflect_str "type", inputType, inputType + input.reflect_bool "checked", checked + of TAG_OPTION: + let option = HTMLOptionElement(element) + option.reflect_bool "selected", selected + of TAG_BUTTON: + let button = HTMLButtonElement(element) + button.reflect_str "type", ctype, (func(s: string): ButtonType = + case s + of "submit": return BUTTON_SUBMIT + of "reset": return BUTTON_RESET + of "button": return BUTTON_BUTTON) + else: discard -proc attr(element: Element, name, value: string) = - if name in element.attrs: - element.delAttr(name) - element.attrs[name] = value - element.attributes.attrlist.add(element.newAttr(name, value)) +proc attr0(element: Element, name, value: string) = + element.attrs.withValue(name, val): + val[] = value + element.invalidateCollections() + element.invalid = true + do: # else + element.attrs[name] = value + element.reflectAttrs(name, value) + +proc attr*(element: Element, name, value: string) = + let i = element.attributes.findAttr(name) + if i != -1: + element.attributes.attrlist[i].value = value + else: + element.attributes.attrlist.add(element.newAttr(name, value)) + element.attr0(name, value) + +proc attrigz(element: Element, name: string, value: int) = + if value > 0: + element.attr(name, $value) proc setAttribute(element: Element, qualifiedName, value: string) {.jsfunc.} = element.attr(qualifiedName, value) +proc setAttributeNS(element: Element, namespace, qualifiedName, value: string) {.jsfunc.} = + if namespace == "" or namespace == $Namespace.HTML: + element.attr(qualifiedName, value) + if namespace notin NamespaceMap: + return + #TODO validate and extract + element.attr0(qualifiedName, value) + let ns = NamespaceMap[namespace] + let i = element.attributes.findAttr(qualifiedName) + if i == -1: + let s = qualifiedName.until(':') + if s.len < qualifiedName.len: + element.attributes.attrlist.add(element.newAttr(qualifiedName.substr(s.len), value, s, ns)) + else: + element.attributes.attrlist.add(element.newAttr(qualifiedName, value, "", ns)) + else: + element.attributes.attrlist[i].value = value + +proc removeAttribute(element: Element, qualifiedName: string) {.jsfunc.} = + element.delAttr(qualifiedName) + +proc removeAttributeNS(element: Element, namespace, localName: string) {.jsfunc.} = + #TODO use namespace + element.delAttr(localName) + +proc value(attr: Attr, s: string) {.jsfset.} = + attr.value = s + if attr.ownerElement != nil: + attr.ownerElement.attr0(attr.name, s) + +# Element attribute reflection (setters) +proc className(element: Element, s: string) {.jsfset.} = + element.attr("class", s) + +proc size(element: HTMLInputElement, n: int) {.jsfset.} = + element.attrigz("size", n) + +proc size(element: HTMLSelectElement, n: int) {.jsfset.} = + element.attrigz("size", n) + +proc cols(element: HTMLTextAreaElement, n: int) {.jsfset.} = + element.attrigz("cols", n) + +proc rows(element: HTMLTextAreaElement, n: int) {.jsfset.} = + element.attrigz("rows", n) + proc setNamedItem*(map: NamedNodeMap, attr: Attr): Option[Attr] {.jserr, jsfunc.} = if attr.ownerElement != nil and attr.ownerElement != map.element: #TODO should be DOMException @@ -1109,20 +1333,39 @@ proc setNamedItem*(map: NamedNodeMap, attr: Attr): Option[Attr] {.jserr, jsfunc. proc setNamedItemNS*(map: NamedNodeMap, attr: Attr): Option[Attr] {.jsfunc.} = map.setNamedItem(attr) -proc remove*(node: Node) = +proc removeNamedItem*(map: NamedNodeMap, qualifiedName: string): Attr {.jserr, jsfunc.} = + let i = map.findAttr(qualifiedName) + if i != -1: + let attr = map.attrlist[i] + map.element.delAttr(i) + return attr + #TODO should be DOMException + JS_ERR JS_TypeError, "Not found" + +proc removeNamedItemNS*(map: NamedNodeMap, namespace, localName: string): Attr = + #TODO TODO TODO + map.removeNamedItem(localName) + +proc id(element: Element, id: string) {.jsfset.} = + element.id = id + element.attr("id", id) + +# Pass an index to avoid searching for it. +proc remove*(node: Node, index: int, suppressObservers: bool) = let parent = node.parentNode assert parent != nil - let index = parent.childNodes.find(node) assert index != -1 #TODO live ranges #TODO NodeIterator let oldPreviousSibling = node.previousSibling let oldNextSibling = node.nextSibling - parent.childNodes.delete(index) + parent.childList.delete(index) if oldPreviousSibling != nil: oldPreviousSibling.nextSibling = oldNextSibling if oldNextSibling != nil: oldNextSibling.previousSibling = oldPreviousSibling + discard node.parentNode.invalidateCollections() + node.parentHasCollections = false node.parentNode = nil node.parentElement = nil node.root = nil @@ -1130,6 +1373,10 @@ proc remove*(node: Node) = #TODO assigned, shadow root, shadow root again, custom nodes, registered observers #TODO not suppress observers => queue tree mutation record +proc remove*(node: Node, suppressObservers = false) = + let index = node.parentNode.childList.find(node) + node.remove(index, suppressObservers) + proc adopt(document: Document, node: Node) = if node.parentNode != nil: remove(node) @@ -1141,11 +1388,14 @@ proc applyChildInsert(parent, child: Node, index: int) = if parent.nodeType == ELEMENT_NODE: child.parentElement = Element(parent) if index - 1 >= 0: - child.previousSibling = parent.childNodes[index - 1] + child.previousSibling = parent.childList[index - 1] child.previousSibling.nextSibling = child - if index + 1 < parent.childNodes.len: - child.nextSibling = parent.childNodes[index + 1] + if index + 1 < parent.childList.len: + child.nextSibling = parent.childList[index + 1] child.nextSibling.previousSibling = child + child.invalidateCollections() + child.parentHasCollections = parent.liveCollections.len > 0 or parent.parentHasCollections + child.invalidateCollections() proc resetElement*(element: Element) = case element.tagType @@ -1217,7 +1467,7 @@ proc resetFormOwner(element: FormAssociatedElement) = element.findAncestor({TAG_FORM}) == element.form: return element.form = nil - if element.tagType in ListedElements and element.attrb("form") and element.connected: + if element.tagType in ListedElements and element.attrb("form") and element.isConnected: let form = element.attr("form") for desc in element.elements(TAG_FORM): if desc.id == form: @@ -1245,114 +1495,135 @@ proc insertionSteps(insertedNode: Node) = return element.resetFormOwner() +# WARNING the ordering of the arguments in the standard is whack so this doesn't match that +func preInsertionValidity*(parent, node, before: Node): bool = + if parent.nodeType notin {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE}: + # HierarchyRequestError + return false + if node.isHostIncludingInclusiveAncestor(parent): + # HierarchyRequestError + return false + if before != nil and before.parentNode != parent: + # NotFoundError + return false + if node.nodeType notin {DOCUMENT_FRAGMENT_NODE, DOCUMENT_TYPE_NODE, ELEMENT_NODE} + CharacterDataNodes: + # HierarchyRequestError + return false + if (node.nodeType == TEXT_NODE and parent.nodeType == DOCUMENT_NODE) or + (node.nodeType == DOCUMENT_TYPE_NODE and parent.nodeType != DOCUMENT_NODE): + # HierarchyRequestError + return false + 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): + # HierarchyRequestError + return false + elif elems == 1 and (parent.hasChild(ELEMENT_NODE) or before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or before.hasNextSibling(DOCUMENT_TYPE_NODE))): + # HierarchyRequestError + return false + of ELEMENT_NODE: + if parent.hasChild(ELEMENT_NODE) or before != nil and (before.nodeType == DOCUMENT_TYPE_NODE or before.hasNextSibling(DOCUMENT_TYPE_NODE)): + # HierarchyRequestError + return false + 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): + # HierarchyRequestError + return false + else: discard + return true # no exception reached + # WARNING ditto proc insert*(parent, node, before: Node) = - let nodes = if node.nodeType == DOCUMENT_FRAGMENT_NODE: node.childNodes + let nodes = if node.nodeType == DOCUMENT_FRAGMENT_NODE: node.childList else: @[node] let count = nodes.len if count == 0: return if node.nodeType == DOCUMENT_FRAGMENT_NODE: - for child in node.childNodes: - child.remove() + for i in countdown(node.childList.high, 0): + node.childList[i].remove(i, true) #TODO tree mutation record if before != nil: #TODO live ranges discard - #let previousSibling = if before == nil: parent.lastChild - #else: before.previousSibling for node in nodes: parent.document.adopt(node) if before == nil: - parent.childNodes.add(node) - parent.applyChildInsert(node, parent.childNodes.high) + parent.childList.add(node) + parent.applyChildInsert(node, parent.childList.high) else: - let index = parent.childNodes.find(before) - parent.childNodes.insert(node, index) + let index = parent.childList.find(before) + parent.childList.insert(node, index) parent.applyChildInsert(node, index) if node.nodeType == ELEMENT_NODE: #TODO shadow root insertionSteps(node) -# WARNING ditto -proc preInsert*(parent, node, before: Node) = +proc insertBefore(parent, node, before: Node): Node {.jserr, jsfunc.} = if parent.preInsertionValidity(node, before): - let referenceChild = if before == node: node.nextSibling - else: before + let referenceChild = if before == node: + node.nextSibling + else: + before parent.insert(node, referenceChild) + return node + #TODO use preInsertionValidity result + JS_ERR JS_TypeError, "Pre-insertion validity violated" + +proc appendChild(parent, node: Node): Node {.jsfunc.} = + return parent.insertBefore(node, nil) proc append*(parent, node: Node) = - parent.preInsert(node, nil) + discard parent.appendChild(node) + +#TODO replaceChild + +proc removeChild(parent, node: Node): Node {.jsfunc.} = + #TODO should be DOMException + if node.parentNode != parent: + JS_ERR JS_TypeError, "NotFoundError" + node.remove() + +proc replaceAll(parent, node: Node) = + for i in countdown(parent.childList.high, 0): + parent.childList[i].remove(i, true) + if node != nil: + if node.nodeType == DOCUMENT_FRAGMENT_NODE: + for child in node.childList: + parent.append(child) + else: + parent.append(node) + #TODO tree mutation record + +proc textContent*(node: Node, data: Option[string]) {.jsfset.} = + case node.nodeType + of DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE: + let x = if data.isSome: + node.document.newText(data.get) + else: + nil + node.replaceAll(x) + of ATTRIBUTE_NODE: + value(Attr(node), data.get("")) + of TEXT_NODE, COMMENT_NODE: + CharacterData(node).data = data.get("") + else: discard proc reset*(form: HTMLFormElement) = for control in form.controls: control.resetElement() control.invalid = true -proc appendAttributes*(element: Element, attrs: Table[string, string]) = - for k, v in attrs: - element.attr(k, v) - template reflect_str(element: Element, name: static string, val: untyped) = - element.attrs.withValue(name, val): - element.val = val[] - template reflect_str(element: Element, name: static string, val, fun: untyped) = - element.attrs.withValue(name, val): - element.val = fun(val[]) - template reflect_nonzero_int(element: Element, name: static string, val: untyped, default: int) = - element.attrs.withValue(name, val): - if val[].isValidNonZeroInt(): - element.val = parseInt(val[]) - else: - element.val = default - do: - element.val = default - template reflect_bool(element: Element, name: static string, val: untyped) = - if name in element.attrs: - element.val = true - element.reflect_str "id", id - let classList = element.attr("class").split(' ') - for x in classList: - if x != "" and x notin element.classList: - element.classList.add(x) - case element.tagType - of TAG_INPUT: - let input = HTMLInputElement(element) - input.reflect_str "value", value - input.reflect_str "type", inputType, inputType - input.reflect_nonzero_int "size", size, 20 - input.reflect_bool "checked", checked - of TAG_OPTION: - let option = HTMLOptionElement(element) - option.reflect_bool "selected", selected - of TAG_SELECT: - let select = HTMLSelectElement(element) - select.reflect_nonzero_int "size", size, (if "multiple" in element.attrs: 4 else: 1) - of TAG_BUTTON: - let button = HTMLButtonElement(element) - button.reflect_str "type", ctype, (func(s: string): ButtonType = - case s - of "submit": return BUTTON_SUBMIT - of "reset": return BUTTON_RESET - of "button": return BUTTON_BUTTON) - of TAG_TEXTAREA: - let textarea = HTMLTextAreaElement(element) - textarea.reflect_nonzero_int "cols", cols, 20 - textarea.reflect_nonzero_int "rows", rows, 1 - of TAG_SCRIPT: - let element = HTMLScriptElement(element) - element.reflect_str "nonce", internalNonce - else: discard - proc renderBlocking*(element: Element): bool = if "render" in element.attr("blocking").split(AsciiWhitespace): return true - case element.tagType - of TAG_SCRIPT: + if element.tagType == TAG_SCRIPT: let element = HTMLScriptElement(element) if element.ctype == CLASSIC and element.parserDocument != nil and not element.attrb("async") and not element.attrb("defer"): return true - else: discard return false proc blockRendering*(element: Element) = @@ -1398,6 +1669,19 @@ proc fetchClassicScript(element: HTMLScriptElement, url: URL, let script = createClassicScript(source, url, options, false) element.markAsReady(ScriptResult(t: RESULT_SCRIPT, script: script)) +#TODO TODO TODO do something with this (redirect stderr?) +proc log*(console: console, ss: varargs[string]) {.jsfunc.} = + var s = "" + for i in 0..<ss.len: + s &= ss[i] + #console.err.write(ss[i]) + if i != ss.high: + s &= ' ' + #console.err.write(' ') + eprint s + #console.err.write('\n') + #console.err.flush() + proc execute*(element: HTMLScriptElement) = let document = element.document if document != element.preparationTimeDocument: @@ -1419,7 +1703,7 @@ proc execute*(element: HTMLScriptElement) = let ss = newStringStream() document.window.jsctx.writeException(ss) ss.setPosition(0) - eprint "Exception in document", document.url, ss.readAll() + document.window.console.log("Exception in document", $document.url, ss.readAll()) document.currentScript = oldCurrentScript else: discard #TODO @@ -1434,7 +1718,7 @@ proc prepare*(element: HTMLScriptElement) = let sourceText = element.childTextContent if not element.attrb("src") and sourceText == "": return - if not element.connected: + if not element.isConnected: return let typeString = if element.attr("type") != "": element.attr("type").strip(chars = AsciiWhitespace).toLowerAscii() @@ -1557,9 +1841,15 @@ proc querySelectorAll*(node: Node, q: string): seq[Element] {.jsfunc.} = proc querySelector*(node: Node, q: string): Element {.jsfunc.} = return doqs(node, q) +proc addconsoleModule*(ctx: JSContext) = + #TODO console should not have a prototype + ctx.registerType(console, nointerface = true) + proc addDOMModule*(ctx: JSContext) = let eventTargetCID = ctx.registerType(EventTarget) let nodeCID = ctx.registerType(Node, parent = eventTargetCID) + ctx.registerType(NodeList) + ctx.registerType(HTMLCollection) ctx.registerType(Document, parent = nodeCID) let characterDataCID = ctx.registerType(CharacterData, parent = nodeCID) ctx.registerType(Comment, parent = characterDataCID) @@ -1588,3 +1878,4 @@ proc addDOMModule*(ctx: JSContext) = ctx.registerType(HTMLUnknownElement, parent = htmlElementCID) ctx.registerType(HTMLScriptElement, parent = htmlElementCID) ctx.registerType(HTMLButtonElement, parent = htmlElementCID) + ctx.registerType(HTMLTextAreaElement, parent = htmlElementCID) diff --git a/src/html/env.nim b/src/html/env.nim index 6db018c9..29b1de09 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -6,6 +6,7 @@ import types/url proc newWindow*(scripting: bool, loader = none(FileLoader)): Window = result = Window( + console: console(), loader: loader, settings: EnvironmentSettings( scripting: scripting @@ -21,6 +22,7 @@ proc newWindow*(scripting: bool, loader = none(FileLoader)): Window = ctx.setOpaque(global, result) ctx.setProperty(global, "window", global) JS_FreeValue(ctx, global) + ctx.addconsoleModule() ctx.addDOMModule() ctx.addURLModule() ctx.addHTMLModule() diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim index 999625a8..f306d5f4 100644 --- a/src/html/htmlparser.nim +++ b/src/html/htmlparser.nim @@ -205,8 +205,7 @@ func createElement(parser: HTML5Parser, token: Token, namespace: Namespace, inte #TODO custom elements let document = intendedParent.document let localName = token.tagname - let element = document.newHTMLElement(localName, namespace, tagType = token.tagtype) - element.appendAttributes(token.attrs) + let element = document.newHTMLElement(localName, namespace, tagType = token.tagtype, attrs = token.attrs) if element.isResettable(): element.resetElement() @@ -1086,10 +1085,12 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = let token = parser.activeFormatting[formattingIndex][1] let element = parser.createElement(token, Namespace.HTML, furthestBlock) var tomove: seq[Node] - for j in countdown(furthestBlock.childNodes.high, 0): - let child = furthestBlock.childNodes[j] - child.remove() + j = furthestBlock.childList.high + while j >= 0: + let child = furthestBlock.childList[j] + child.remove(j, true) tomove.add(child) + dec j for child in tomove: element.append(child) furthestBlock.append(element) @@ -1139,7 +1140,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = else: for k, v in token.attrs: if k notin parser.openElements[0].attrs: - parser.openElements[0].attrs[k] = v + parser.openElements[0].attr(k, v) ) ("<base>", "<basefont>", "<bgsound>", "<link>", "<meta>", "<noframes>", "<script>", "<style>", "<template>", "<title>", "</template>") => (block: parser.processInHTMLContent(token, IN_HEAD)) @@ -1151,7 +1152,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode = parser.framesetOk = false for k, v in token.attrs: if k notin parser.openElements[1].attrs: - parser.openElements[1].attrs[k] = v + parser.openElements[1].attr(k, v) ) "<frameset>" => (block: parse_error diff --git a/src/html/tags.nim b/src/html/tags.nim index ff6a3a30..8b0713d7 100644 --- a/src/html/tags.nim +++ b/src/html/tags.nim @@ -142,6 +142,10 @@ const LabelableElements* = { TAG_BUTTON, TAG_INPUT, TAG_METER, TAG_OUTPUT, TAG_PROGRESS, TAG_SELECT, TAG_TEXTAREA } +const CharacterDataNodes* = { + TEXT_NODE, CDATA_SECTION_NODE, PROCESSING_INSTRUCTION_NODE, COMMENT_NODE +} + #https://html.spec.whatwg.org/multipage/parsing.html#the-stack-of-open-elements #NOTE MathML not implemented #TODO SVG foreignObject, SVG desc, SVG title |