diff options
author | bptato <nincsnevem662@gmail.com> | 2022-07-27 22:28:14 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2022-07-27 23:26:24 +0200 |
commit | dd9cb39c7d6adfa02feb181dd629bb9a93b7250c (patch) | |
tree | 4d4f840847a20577729f12a879c95c7a29a859b7 | |
parent | cc92092fe8053f77d0c63e57c3990272b36be4ef (diff) | |
download | chawan-dd9cb39c7d6adfa02feb181dd629bb9a93b7250c.tar.gz |
Fix StyledNode invalidation & others
-rw-r--r-- | readme.md | 10 | ||||
-rw-r--r-- | src/css/cascade.nim | 84 | ||||
-rw-r--r-- | src/css/select.nim | 223 | ||||
-rw-r--r-- | src/css/stylednode.nim | 115 | ||||
-rw-r--r-- | src/css/values.nim | 2 | ||||
-rw-r--r-- | src/html/dom.nim | 7 | ||||
-rw-r--r-- | src/io/buffer.nim | 57 | ||||
-rw-r--r-- | src/io/cell.nim | 4 | ||||
-rw-r--r-- | src/js/javascript.nim | 1 | ||||
-rw-r--r-- | src/layout/box.nim | 13 | ||||
-rw-r--r-- | src/layout/engine.nim | 102 | ||||
-rw-r--r-- | src/types/url.nim | 22 |
12 files changed, 419 insertions, 221 deletions
diff --git a/readme.md b/readme.md index 10f4eab5..27c8cf94 100644 --- a/readme.md +++ b/readme.md @@ -18,18 +18,16 @@ It includes its own parsers, pager, and layout engine. Currently implemented features are: -* an unicode, double-width capable pager -* a (basic) CSS-capable layout engine -* incomplete support for forms -* incomplete support for remote resources (i.e. css) +* a unicode, double-width capable pager +* a CSS-capable layout engine +* forms **Planned** features, roughly in order of importance: * partial re-rendering of page layout -* full support for forms * unicode normalization * non-unicode charsets (they currently crash the browser) -* fast streams (aka pipes; use library?) +* fast streams (aka pipelines; use library?) * standard-compliant support for remote resources * incremental page loading (to function as a proper pager) * table diff --git a/src/css/cascade.nim b/src/css/cascade.nim index 8b93e8d0..25866ac0 100644 --- a/src/css/cascade.nim +++ b/src/css/cascade.nim @@ -21,9 +21,6 @@ type proc applyProperty(styledNode: StyledNode, parent: CSSComputedValues, d: CSSDeclaration) = styledNode.computed.applyValue(parent, d) - if styledNode.node != nil: - Element(styledNode.node).cssapplied = true - func applies(mq: MediaQuery): bool = case mq.t of CONDITION_MEDIA: @@ -59,16 +56,17 @@ func applies(mqlist: MediaQueryList): bool = type ToSorts = array[PseudoElem, seq[(int, seq[CSSDeclaration])]] -proc calcRule(tosorts: var ToSorts, elem: Element, rule: CSSRuleDef) = +proc calcRule(tosorts: var ToSorts, styledNode: StyledNode, rule: CSSRuleDef) = for sel in rule.sels: - if elem.selectorsMatch(sel): + if styledNode.selectorsMatch(sel): let spec = getSpecificity(sel) tosorts[sel.pseudo].add((spec,rule.decls)) -func calcRules(elem: Element, sheet: CSSStylesheet): DeclarationList = +func calcRules(styledNode: StyledNode, sheet: CSSStylesheet): DeclarationList = var tosorts: ToSorts + let elem = Element(styledNode.node) for rule in sheet.gen_rules(elem.tagType, elem.id, elem.classList): - tosorts.calcRule(elem, rule) + tosorts.calcRule(styledNode, rule) for i in PseudoElem: tosorts[i].sort((x, y) => cmp(x[0], y[0])) @@ -88,8 +86,7 @@ proc applyImportant(ares: var ApplyResult, decls: seq[CSSDeclaration]) = if decl.important: ares.important.add(decl) -# Always returns a new styled node, with the passed declarations applied. -proc applyDeclarations(elem: Element, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]): StyledNode = +proc applyDeclarations(styledNode: StyledNode, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]) = let pseudo = PSEUDO_NONE var ares: ApplyResult @@ -101,21 +98,22 @@ proc applyDeclarations(elem: Element, parent: CSSComputedValues, ua, user: Decla for rule in author: ares.applyImportant(rule[pseudo]) - let style = elem.attr("style") - if style.len > 0: - let inline_rules = newStringStream(style).parseListOfDeclarations2() - ares.applyNormal(inline_rules) - ares.applyImportant(inline_rules) + if styledNode.node != nil: + let style = Element(styledNode.node).attr("style") + if style.len > 0: + let inline_rules = newStringStream(style).parseListOfDeclarations2() + ares.applyNormal(inline_rules) + ares.applyImportant(inline_rules) ares.applyImportant(user[pseudo]) ares.applyImportant(ua[pseudo]) - result = StyledNode(t: STYLED_ELEMENT, node: elem, computed: parent.inheritProperties()) + styledNode.computed = parent.inheritProperties() for rule in ares.normal: - result.applyProperty(parent, rule) + styledNode.applyProperty(parent, rule) for rule in ares.important: - result.applyProperty(parent, rule) + styledNode.applyProperty(parent, rule) # Either returns a new styled node or nil. proc applyDeclarations(pseudo: PseudoElem, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]): StyledNode = @@ -146,23 +144,21 @@ func applyMediaQuery(ss: CSSStylesheet): CSSStylesheet = if mq.query.applies(): result.add(mq.children.applyMediaQuery()) -func calcRules(elem: Element, ua, user: CSSStylesheet, author: seq[CSSStylesheet]): tuple[uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]] = - result.uadecls = calcRules(elem, ua) - result.userdecls = calcRules(elem, user) +func calcRules(styledNode: StyledNode, ua, user: CSSStylesheet, author: seq[CSSStylesheet]): tuple[uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]] = + result.uadecls = calcRules(styledNode, ua) + result.userdecls = calcRules(styledNode, user) for rule in author: - result.authordecls.add(calcRules(elem, rule)) + result.authordecls.add(calcRules(styledNode, rule)) -proc applyStyle(parent: StyledNode, elem: Element, uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]): StyledNode = +proc applyStyle(parent, styledNode: StyledNode, uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]) = let parentComputed = if parent != nil: parent.computed else: rootProperties() - result = elem.applyDeclarations(parentComputed, uadecls, userdecls, authordecls) - assert result != nil + styledNode.applyDeclarations(parentComputed, uadecls, userdecls, authordecls) # Builds a StyledNode tree, optionally based on a previously cached version. -# This was originally a recursive algorithm; it had to be rewritten iteratively proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledNode): StyledNode = if document.html == nil: return @@ -187,14 +183,17 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN continue var styledChild: StyledNode - if cachedChild != nil and (cachedChild.node == nil or cachedChild.node.nodeType != ELEMENT_NODE or Element(cachedChild.node).cssapplied): + if cachedChild != nil and cachedChild.isValid(): if cachedChild.t == STYLED_ELEMENT: - styledChild = StyledNode(t: STYLED_ELEMENT, pseudo: cachedChild.pseudo, computed: cachedChild.computed, node: cachedChild.node) - if cachedChild.pseudo != PSEUDO_NONE: + if cachedChild.pseudo == PSEUDO_NONE: + styledChild = styledParent.newStyledElement(Element(cachedChild.node), cachedChild.computed, cachedChild.depends) + else: + styledChild = styledParent.newStyledElement(cachedChild.pseudo, cachedChild.computed, cachedChild.depends) styledChild.children = cachedChild.children #TODO does this actually refresh pseudo elems when needed? else: # Text - styledChild = StyledNode(t: STYLED_TEXT, text: cachedChild.text, node: cachedChild.node) + styledChild = styledParent.newStyledText(cachedChild.text) + styledChild.node = cachedChild.node if styledParent == nil: # Root element result = styledChild @@ -202,7 +201,7 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN styledParent.children.add(styledChild) else: if pseudo != PSEUDO_NONE: - let (ua, user, authordecls) = Element(styledParent.node).calcRules(ua, user, author) + let (ua, user, authordecls) = styledParent.calcRules(ua, user, author) case pseudo of PSEUDO_BEFORE, PSEUDO_AFTER: let styledPseudo = pseudo.applyDeclarations(styledParent.computed, ua, user, authordecls) @@ -210,32 +209,34 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN styledParent.children.add(styledPseudo) let content = styledPseudo.computed{"content"} if content.len > 0: - styledPseudo.children.add(StyledNode(t: STYLED_TEXT, text: content)) + styledPseudo.children.add(styledPseudo.newStyledText(content)) of PSEUDO_INPUT_TEXT: let content = HTMLInputElement(styledParent.node).inputString() if content.len > 0: - styledChild = StyledNode(t: STYLED_TEXT, text: content) + styledChild = styledParent.newStyledText(content) styledParent.children.add(styledChild) of PSEUDO_NONE: discard else: assert child != nil if styledParent != nil: if child.nodeType == ELEMENT_NODE: - let (ua, user, authordecls) = Element(child).calcRules(ua, user, author) - styledChild = applyStyle(styledParent, Element(child), ua, user, authordecls) + styledChild = styledParent.newStyledElement(Element(child)) + let (ua, user, authordecls) = styledChild.calcRules(ua, user, author) + applyStyle(styledParent, styledChild, ua, user, authordecls) styledParent.children.add(styledChild) elif child.nodeType == TEXT_NODE: let text = Text(child) - styledChild = StyledNode(t: STYLED_TEXT, node: child, text: text.data) + styledChild = styledParent.newStyledText(text) styledParent.children.add(styledChild) else: # Root element - assert result == nil - let (ua, user, authordecls) = Element(child).calcRules(ua, user, author) - styledChild = applyStyle(styledParent, Element(child), ua, user, authordecls) + styledChild = newStyledElement(Element(child)) + let (ua, user, authordecls) = styledChild.calcRules(ua, user, author) + applyStyle(styledParent, styledChild, ua, user, authordecls) result = styledChild - if styledChild != nil and styledChild.node != nil and styledChild.node.nodeType == ELEMENT_NODE: + if styledChild != nil and styledChild.t == STYLED_ELEMENT and styledChild.node != nil: + styledChild.applyDependValues() template stack_append(styledParent: StyledNode, child: Node) = if cachedChild != nil: var cached: StyledNode @@ -280,8 +281,3 @@ proc applyStylesheets*(document: Document, uass, userss: CSSStylesheet, previous let uass = uass.applyMediaQuery() let userss = userss.applyMediaQuery() return document.applyRules(uass, userss, previousStyled) - -proc refreshStyle*(elem: Element) = - elem.cssapplied = false - for child in elem.children: - child.refreshStyle() diff --git a/src/css/select.nim b/src/css/select.nim index b43604c4..f1140d1e 100644 --- a/src/css/select.nim +++ b/src/css/select.nim @@ -1,12 +1,9 @@ -import unicode import tables import strutils -import sequtils -import sugar -import streams -import css/selectorparser import css/cssparser +import css/selectorparser +import css/stylednode import html/dom import html/tags @@ -14,7 +11,7 @@ func attrSelectorMatches(elem: Element, sel: Selector): bool = case sel.rel of ' ': return sel.attr in elem.attributes of '=': return elem.attr(sel.attr) == sel.value - of '~': return sel.value in unicode.split(elem.attr(sel.attr)) + of '~': return sel.value in elem.attr(sel.attr).split(Whitespace) of '|': let val = elem.attr(sel.attr) return val == sel.value or sel.value.startsWith(val & '-') @@ -23,12 +20,17 @@ func attrSelectorMatches(elem: Element, sel: Selector): bool = of '*': return elem.attr(sel.attr).contains(sel.value) else: return false -func pseudoSelectorMatches(elem: Element, sel: Selector): bool = +func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool = + let selem = elem + when elem is StyledNode: + let elem = Element(elem.node) case sel.pseudo of PSEUDO_FIRST_CHILD: return elem.parentNode.firstElementChild == elem of PSEUDO_LAST_CHILD: return elem.parentNode.lastElementChild == elem of PSEUDO_ONLY_CHILD: return elem.parentNode.firstElementChild == elem and elem.parentNode.lastElementChild == elem - of PSEUDO_HOVER: return elem.hover + of PSEUDO_HOVER: + when selem is StyledNode: felem.depends.nodes[DEPEND_HOVER].add(selem) + return elem.hover of PSEUDO_ROOT: return elem == elem.document.html of PSEUDO_NTH_CHILD: let n = int64(sel.pseudonum - 1) @@ -39,68 +41,101 @@ func pseudoSelectorMatches(elem: Element, sel: Selector): bool = inc i return false of PSEUDO_CHECKED: + when selem is StyledNode: felem.depends.nodes[DEPEND_CHECKED].add(selem) if elem.tagType == TAG_INPUT: return HTMLInputElement(elem).checked elif elem.tagType == TAG_OPTION: return HTMLOptionElement(elem).selected return false -func selectorsMatch*(elem: Element, selectors: SelectorList): bool +func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: SelectorList, felem: T = nil): bool -func funcSelectorMatches(elem: Element, sel: Selector): bool = +func funcSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool = case sel.name of "not": for slist in sel.fsels: - if elem.selectorsMatch(slist): + if elem.selectorsMatch(slist, felem): return false return true of "is", "where": for slist in sel.fsels: - if elem.selectorsMatch(slist): + if elem.selectorsMatch(slist, felem): return true return false else: discard -func combinatorSelectorMatches(elem: Element, sel: Selector): bool = +func combinatorSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool = + let selem = elem #combinator without at least two members makes no sense assert sel.csels.len > 1 - if elem.selectorsMatch(sel.csels[^1]): + if selem.selectorsMatch(sel.csels[^1], felem): var i = sel.csels.len - 2 case sel.ct of DESCENDANT_COMBINATOR: - var e = elem.parentElement + when selem is StyledNode: + var e = elem.parent + else: + var e = elem.parentElement while e != nil and i >= 0: - if e.selectorsMatch(sel.csels[i]): + if e.selectorsMatch(sel.csels[i], felem): dec i - e = e.parentElement + when elem is StyledNode: + e = e.parent + else: + e = e.parentElement of CHILD_COMBINATOR: - var e = elem.parentElement + when elem is StyledNode: + var e = elem.parent + else: + var e = elem.parentElement while e != nil and i >= 0: - if not e.selectorsMatch(sel.csels[i]): + if not e.selectorsMatch(sel.csels[i], felem): return false dec i - e = e.parentElement + when elem is StyledNode: + e = e.parent + else: + e = e.parentElement of NEXT_SIBLING_COMBINATOR: var found = false - for child in elem.parentElement.children_rev: + when elem is StyledNode: + var parent = elem.parent + else: + var parent = elem.parentElement + for child in parent.children_rev: + when elem is StyledNode: + if child.t != STYLED_ELEMENT or child.node == nil: continue if found: - if not child.selectorsMatch(sel.csels[i]): + if not child.selectorsMatch(sel.csels[i], felem): return false dec i + if i < 0: + return true if child == elem: found = true of SUBSEQ_SIBLING_COMBINATOR: var found = false - for child in elem.parentElement.children_rev: + when selem is StyledNode: + var parent = selem.parent + else: + var parent = elem.parentElement + for child in parent.children_rev: + when selem is StyledNode: + if child.t != STYLED_ELEMENT or child.node == nil: continue if found: - if child.selectorsMatch(sel.csels[i]): + if child.selectorsMatch(sel.csels[i], felem): dec i - if child == elem: + if i < 0: + return true + if child == selem: found = true return i == -1 return false -func selectorMatches(elem: Element, sel: Selector): bool = +func selectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem: T): bool = + let selem = elem + when elem is StyledNode: + let elem = Element(selem.node) case sel.t of TYPE_SELECTOR: return elem.tagType == sel.tag @@ -111,78 +146,86 @@ func selectorMatches(elem: Element, sel: Selector): bool = of ATTR_SELECTOR: return elem.attrSelectorMatches(sel) of PSEUDO_SELECTOR: - return pseudoSelectorMatches(elem, sel) + return pseudoSelectorMatches(selem, sel, felem) of PSELEM_SELECTOR: return true of UNIVERSAL_SELECTOR: return true of FUNC_SELECTOR: - return funcSelectorMatches(elem, sel) + return funcSelectorMatches(selem, sel, felem) of COMBINATOR_SELECTOR: - return combinatorSelectorMatches(elem, sel) + return combinatorSelectorMatches(selem, sel, felem) + +# WARNING for StyledNode, this has the side effect of modifying depends. +#TODO make that an explicit flag or something, also get rid of the Element case +func selectorsMatch*[T: Element|StyledNode](elem: T, selectors: SelectorList, felem: T = nil): bool = + let felem = if felem != nil: + felem + else: + elem -func selectorsMatch*(elem: Element, selectors: SelectorList): bool = for sel in selectors.sels: - if not selectorMatches(elem, sel): + if not selectorMatches(elem, sel, felem): return false return true -func selectElems(element: Element, sel: Selector): seq[Element] = - case sel.t - of TYPE_SELECTOR: - return element.filterDescendants((elem) => elem.tagType == sel.tag) - of ID_SELECTOR: - return element.filterDescendants((elem) => elem.id == sel.id) - of CLASS_SELECTOR: - return element.filterDescendants((elem) => sel.class in elem.classList) - of UNIVERSAL_SELECTOR: - return element.all_descendants - of ATTR_SELECTOR: - return element.filterDescendants((elem) => attrSelectorMatches(elem, sel)) - of PSEUDO_SELECTOR: - return element.filterDescendants((elem) => pseudoSelectorMatches(elem, sel)) - of PSELEM_SELECTOR: - return element.all_descendants - of FUNC_SELECTOR: - return element.filterDescendants((elem) => selectorMatches(elem, sel)) - of COMBINATOR_SELECTOR: - return element.filterDescendants((elem) => selectorMatches(elem, sel)) - -func selectElems(element: Element, selectors: SelectorList): seq[Element] = - assert(selectors.len > 0) - let sellist = optimizeSelectorList(selectors) - result = element.selectElems(selectors[0]) - var i = 1 - - while i < sellist.len: - result = result.filter((elem) => selectorMatches(elem, sellist[i])) - inc i - -proc querySelectorAll*(document: Document, q: string): seq[Element] = - let ss = newStringStream(q) - let cvals = parseListOfComponentValues(ss) - let selectors = parseSelectors(cvals) - - if document.html != nil: - for sel in selectors: - result.add(document.html.selectElems(sel)) - -proc querySelector*(document: Document, q: string): Element = - let elems = document.querySelectorAll(q) - if elems.len > 0: - return elems[0] - return nil - -proc querySelectorAll*(element: Element, q: string): seq[Element] = - let ss = newStringStream(q) - let cvals = parseListOfComponentValues(ss) - let selectors = parseSelectors(cvals) - - for sel in selectors: - result.add(element.selectElems(sel)) - -proc querySelector*(element: Element, q: string): Element = - let elems = element.querySelectorAll(q) - if elems.len > 0: - return elems[0] - return nil +#TODO idk, it's not like we have JS anyways +#func selectElems[T: Element|StyledNode](element: T, sel: Selector, felem: T): seq[T] = +# case sel.t +# of TYPE_SELECTOR: +# return element.filterDescendants((elem) => elem.tagType == sel.tag) +# of ID_SELECTOR: +# return element.filterDescendants((elem) => elem.id == sel.id) +# of CLASS_SELECTOR: +# return element.filterDescendants((elem) => sel.class in elem.classList) +# of UNIVERSAL_SELECTOR: +# return element.all_descendants +# of ATTR_SELECTOR: +# return element.filterDescendants((elem) => attrSelectorMatches(elem, sel)) +# of PSEUDO_SELECTOR: +# return element.filterDescendants((elem) => pseudoSelectorMatches(elem, sel, felem)) +# of PSELEM_SELECTOR: +# return element.all_descendants +# of FUNC_SELECTOR: +# return element.filterDescendants((elem) => selectorMatches(elem, sel)) +# of COMBINATOR_SELECTOR: +# return element.filterDescendants((elem) => selectorMatches(elem, sel)) +# +#func selectElems(element: Element, selectors: SelectorList): seq[Element] = +# assert(selectors.len > 0) +# let sellist = optimizeSelectorList(selectors) +# result = element.selectElems(selectors[0], element) +# var i = 1 +# +# while i < sellist.len: +# result = result.filter((elem) => selectorMatches(elem, sellist[i], elem)) +# inc i +# +#proc querySelectorAll*(document: Document, q: string): seq[Element] = +# let ss = newStringStream(q) +# let cvals = parseListOfComponentValues(ss) +# let selectors = parseSelectors(cvals) +# +# if document.html != nil: +# for sel in selectors: +# result.add(document.html.selectElems(sel)) +# +#proc querySelector*(document: Document, q: string): Element = +# let elems = document.querySelectorAll(q) +# if elems.len > 0: +# return elems[0] +# return nil +# +#proc querySelectorAll*(element: Element, q: string): seq[Element] = +# let ss = newStringStream(q) +# let cvals = parseListOfComponentValues(ss) +# let selectors = parseSelectors(cvals) +# +# for sel in selectors: +# result.add(element.selectElems(sel)) +# +#proc querySelector*(element: Element, q: string): Element = +# let elems = element.querySelectorAll(q) +# if elems.len > 0: +# return elems[0] +# return nil diff --git a/src/css/stylednode.nim b/src/css/stylednode.nim index d6293187..6c306f21 100644 --- a/src/css/stylednode.nim +++ b/src/css/stylednode.nim @@ -1,18 +1,123 @@ +import css/selectorparser import css/values import html/dom +import html/tags # Container to hold a style and a node. -# Pseudo elements are implemented using StyledNode objects without nodes. +# Pseudo-elements are implemented using StyledNode objects without nodes. Input +# elements are implemented as internal "pseudo-elements." +# +# To avoid having to invalidate the entire tree on pseudo-class changes, each +# node holds a list of nodes their CSS values depend on. (This list may include +# the node itself.) In addition, nodes also store each value valid for +# dependency d. These are then used for checking the validity of StyledNodes. +# +# In other words - say we have to apply the author stylesheets of the following +# document: +# +# <style> +# div:hover { color: red; } +# :not(input:checked) + p { display: none; } +# </style> +# <div>This div turns red on hover.</div> +# <input type=checkbox> +# <p>This paragraph is only shown when the checkbox above is checked. +# +# That produces the following dependency graph (simplified): +# div -> div (hover) +# p -> input (checked) +# +# Then, to check if a node has been invalidated, we just iterate over all +# recorded dependencies of each StyledNode, and check if their registered value +# of the pseudo-class still matches that of its associated element. +# +# So in our example, for div we check if div's :hover pseudo-class has changed, +# for p we check whether input's :checked pseudo-class has changed. + type StyledType* = enum STYLED_ELEMENT, STYLED_TEXT + DependencyType* = enum + DEPEND_HOVER, DEPEND_CHECKED + + InvalidationRegistry* = set[DependencyType] + + DependencyInfo* = object + # All nodes we depend on, for each dependency type d. + nodes*: array[DependencyType, seq[StyledNode]] + # Previous value. Node is marked invalid when one of these no longer + # matches the DOM value. + prev: array[DependencyType, bool] + StyledNode* = ref object + parent*: StyledNode + node*: Node case t*: StyledType + of STYLED_TEXT: + text*: string of STYLED_ELEMENT: pseudo*: PseudoElem computed*: CSSComputedValues - of STYLED_TEXT: - text*: string - node*: Node - children*: seq[StyledNode] + children*: seq[StyledNode] + depends*: DependencyInfo + +iterator branch*(node: StyledNode): StyledNode {.inline.} = + var node = node + while node != nil: + yield node + node = node.parent + +iterator children_rev*(node: StyledNode): StyledNode {.inline.} = + for i in countdown(node.children.high, 0): + yield node.children[i] + +func checked(element: Element): bool = + if element.tagType == TAG_INPUT: + let input = HTMLInputElement(element) + result = input.checked + +func isValid*(styledNode: StyledNode): bool = + if styledNode.t == STYLED_TEXT: + return true + if styledNode.node != nil and Element(styledNode.node).invalid: + return false + for d in DependencyType: + for child in styledNode.depends.nodes[d]: + assert child.node != nil + let elem = Element(child.node) + case d + of DEPEND_HOVER: + if child.depends.prev[d] != elem.hover: + return false + of DEPEND_CHECKED: + if child.depends.prev[d] != elem.checked: + return false + return styledNode.parent == nil or styledNode.parent.isValid() + +proc applyDependValues*(styledNode: StyledNode) = + let elem = Element(styledNode.node) + styledNode.depends.prev[DEPEND_HOVER] = elem.hover + styledNode.depends.prev[DEPEND_CHECKED] = elem.checked + elem.invalid = false + +func newStyledElement*(parent: StyledNode, element: Element, computed: CSSComputedValues, reg: sink DependencyInfo): StyledNode = + result = StyledNode(t: STYLED_ELEMENT, computed: computed, node: element, parent: parent) + result.depends = reg + +func newStyledElement*(parent: StyledNode, element: Element): StyledNode = + result = StyledNode(t: STYLED_ELEMENT, node: element, parent: parent) + +# Root +func newStyledElement*(element: Element): StyledNode = + result = StyledNode(t: STYLED_ELEMENT, node: element) + +func newStyledElement*(parent: StyledNode, pseudo: PseudoElem, computed: CSSComputedValues, reg: sink DependencyInfo): StyledNode = + result = StyledNode(t: STYLED_ELEMENT, computed: computed, pseudo: pseudo, parent: parent) + result.depends = reg + +func newStyledText*(parent: StyledNode, text: string): StyledNode = + result = StyledNode(t: STYLED_TEXT, text: text, parent: parent) + +func newStyledText*(parent: StyledNode, text: Text): StyledNode = + result = StyledNode(t: STYLED_TEXT, text: text.data, node: text, parent: parent) diff --git a/src/css/values.nim b/src/css/values.nim index be8f5305..ec172e8d 100644 --- a/src/css/values.nim +++ b/src/css/values.nim @@ -589,7 +589,7 @@ func cssDisplay(d: CSSDeclaration): CSSDisplay = of "inline": return DISPLAY_INLINE of "list-item": return DISPLAY_LIST_ITEM of "inline-block": return DISPLAY_INLINE_BLOCK - of "table": return DISPLAY_TABLE + #of "table": return DISPLAY_TABLE # of "table-row": return DISPLAY_TABLE_ROW # of "table-cell": return DISPLAY_TABLE_CELL # of "table-column": return DISPLAY_TABLE_COLUMN diff --git a/src/html/dom.nim b/src/html/dom.nim index 98ed023b..2c6b9134 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -84,8 +84,7 @@ type classList*: seq[string] attributes*: Table[string, string] hover*: bool - cssapplied*: bool - rendered*: bool + invalid*: bool HTMLElement* = ref object of Element @@ -778,7 +777,7 @@ proc resetElement*(element: Element) = of INPUT_FILE: input.file = none(Url) else: discard - input.rendered = false + input.invalid = true of TAG_SELECT: let select = HTMLSelectElement(element) if not select.attrb("multiple"): @@ -896,7 +895,7 @@ proc append*(parent, node: Node) = proc reset*(form: HTMLFormElement) = for control in form.controls: control.resetElement() - control.rendered = false + control.invalid = true proc appendAttribute*(element: Element, k, v: string) = case k diff --git a/src/io/buffer.nim b/src/io/buffer.nim index b3022615..dc8df009 100644 --- a/src/io/buffer.nim +++ b/src/io/buffer.nim @@ -6,7 +6,6 @@ import tables import terminal import unicode -import css/cascade import css/sheet import css/stylednode import html/dom @@ -61,7 +60,7 @@ type istream*: Stream streamclosed*: bool source*: string - prevnode*: Node + prevnode*: StyledNode sourcepair*: Buffer prev*: Buffer next*: Buffer @@ -219,25 +218,27 @@ func currentDisplayCell(buffer: Buffer): FixedCell = let row = (buffer.cursory - buffer.fromy) * buffer.width return buffer.display[row + buffer.currentCellOrigin()] -func getLink(node: Node): HTMLAnchorElement = +func getLink(node: StyledNode): HTMLAnchorElement = if node == nil: return nil - if node.nodeType == ELEMENT_NODE and Element(node).tagType == TAG_A: - return HTMLAnchorElement(node) - return HTMLAnchorElement(node.findAncestor({TAG_A})) + if node.t == STYLED_ELEMENT and node.node != nil and Element(node.node).tagType == TAG_A: + return HTMLAnchorElement(node.node) + if node.node != nil: + return HTMLAnchorElement(node.node.findAncestor({TAG_A})) + #TODO ::before links? const ClickableElements = { TAG_A, TAG_INPUT } -func getClickable(node: Node): Element = - if node == nil: +func getClickable(styledNode: StyledNode): Element = + if styledNode == nil or styledNode.node == nil: return nil - if node.nodeType == ELEMENT_NODE: - let element = Element(node) + if styledNode.t == STYLED_ELEMENT: + let element = Element(styledNode.node) if element.tagType in ClickableElements: return element - return node.findAncestor(ClickableElements) + styledNode.node.findAncestor(ClickableElements) func getCursorClickable(buffer: Buffer): Element = return buffer.currentDisplayCell().node.getClickable() @@ -693,7 +694,7 @@ proc gotoAnchor*(buffer: Buffer) = var i = 0 while i < line.formats.len: let format = line.formats[i] - if anchor in format.node: + if format.node != nil and anchor in format.node.node: buffer.setCursorY(y) buffer.centerLine() buffer.setCursorX(format.pos) @@ -794,14 +795,13 @@ proc updateHover(buffer: Buffer) = let thisnode = buffer.currentDisplayCell().node let prevnode = buffer.prevnode - if thisnode != prevnode: - for node in thisnode.branch: - if node.nodeType == ELEMENT_NODE: - let elem = Element(node) - if not elem.hover and node notin prevnode: + if thisnode != prevnode and (thisnode == nil or prevnode == nil or thisnode.node != prevnode.node): + for styledNode in thisnode.branch: + if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: + let elem = Element(styledNode.node) + if not elem.hover: elem.hover = true buffer.reshape = true - elem.refreshStyle() let link = thisnode.getLink() if link != nil: @@ -809,13 +809,12 @@ proc updateHover(buffer: Buffer) = else: buffer.hovertext = "" - for node in prevnode.branch: - if node.nodeType == ELEMENT_NODE: - let elem = Element(node) - if elem.hover and node notin thisnode: + for styledNode in prevnode.branch: + if styledNode.t == STYLED_ELEMENT and styledNode.node != nil: + let elem = Element(styledNode.node) + if elem.hover: elem.hover = false buffer.reshape = true - elem.refreshStyle() buffer.prevnode = thisnode @@ -1121,7 +1120,7 @@ proc click*(buffer: Buffer): Option[ClickAction] = let status = readLine("SEARCH: ", value, buffer.width, {'\r', '\n'}) if status: input.value = value - input.rendered = false + input.invalid = true buffer.reshape = true if input.form != nil: let submitaction = submitForm(input.form, input) @@ -1133,7 +1132,7 @@ proc click*(buffer: Buffer): Option[ClickAction] = let status = readLine("TEXT: ", value, buffer.width, {'\r', '\n'}) if status: input.value = value - input.rendered = false + input.invalid = true buffer.reshape = true of INPUT_FILE: var path = if input.file.issome: @@ -1148,18 +1147,18 @@ proc click*(buffer: Buffer): Option[ClickAction] = let path = parseUrl(path, cdir) if path.issome: input.file = path - input.rendered = false + input.invalid = true buffer.reshape = true of INPUT_CHECKBOX: input.checked = not input.checked - input.rendered = false + input.invalid = true buffer.reshape = true of INPUT_RADIO: for radio in input.radiogroup: radio.checked = false - radio.rendered = false + radio.invalid = true input.checked = true - input.rendered = false + input.invalid = true buffer.reshape = true of INPUT_RESET: if input.form != nil: diff --git a/src/io/cell.nim b/src/io/cell.nim index 4d808dda..80ada720 100644 --- a/src/io/cell.nim +++ b/src/io/cell.nim @@ -4,7 +4,7 @@ import strutils import sugar import unicode -import html/dom +import css/stylednode import layout/box import types/color import utils/twtstr @@ -26,7 +26,7 @@ type Cell* = object of RootObj format*: Format - node*: Node + node*: StyledNode FormatCell* = object of Cell pos*: int diff --git a/src/js/javascript.nim b/src/js/javascript.nim index 95e37110..c728283a 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -98,7 +98,6 @@ proc setFunctionProperty*(obj: JSObject, name: string, fun: JSCFunction) = #), cstring(name), 1)) proc free*(ctx: var JSContext) = - eprint "free" let opaque = ctx.getOpaque() if opaque != nil: dealloc(opaque) diff --git a/src/layout/box.nim b/src/layout/box.nim index 7e0ac948..1626791d 100644 --- a/src/layout/box.nim +++ b/src/layout/box.nim @@ -1,5 +1,6 @@ import options +import css/stylednode import css/values import html/dom import io/term @@ -28,8 +29,7 @@ type children*: seq[BoxBuilder] inlinelayout*: bool computed*: CSSComputedValues - node*: Node - element*: Element + node*: StyledNode InlineBoxBuilder* = ref object of BoxBuilder text*: seq[string] @@ -48,9 +48,12 @@ type marker*: MarkerBoxBuilder content*: BlockBoxBuilder + TableRowGroupBoxBuilder* = ref object of BoxBuilder + + TableRowBoxBuilder* = ref object of BoxBuilder + TableBoxBuilder* = ref object of BoxBuilder - inline*: bool - content*: BlockBoxBuilder + rowgroups*: seq[TableRowGroupBoxBuilder] InlineAtom* = ref object of RootObj offset*: Offset @@ -66,7 +69,7 @@ type fontweight*: int textdecoration*: CSSTextDecoration color*: CSSColor - node*: Node + node*: StyledNode InlineSpacing* = ref object of InlineAtom format*: ComputedFormat diff --git a/src/layout/engine.nim b/src/layout/engine.nim index 8ec1c668..18af9e7b 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -16,7 +16,7 @@ func px(l: CSSLength, viewport: Viewport, p = 0): int {.inline.} = type InlineState = object ictx: InlineContext skip: bool - node: Node + node: StyledNode word: InlineWord maxwidth: int computed: CSSComputedValues @@ -284,7 +284,7 @@ proc processWhitespace(state: var InlineState, c: char) = else: inc state.ictx.whitespacenum -proc renderText*(ictx: InlineContext, str: string, maxwidth: int, computed: CSSComputedValues, node: Node) = +proc renderText*(ictx: InlineContext, str: string, maxwidth: int, computed: CSSComputedValues, node: StyledNode) = var state: InlineState state.computed = computed state.ictx = ictx @@ -421,7 +421,7 @@ proc positionInlines(bctx: BlockBox) = proc buildBlock(box: BlockBoxBuilder, parent: BlockBox): BlockBox proc buildInlines(bctx: BlockBox, inlines: seq[BoxBuilder]): InlineContext -proc buildBlocks(bctx: BlockBox, blocks: seq[BoxBuilder], node: Node) +proc buildBlocks(bctx: BlockBox, blocks: seq[BoxBuilder], node: StyledNode) proc applyInlineDimensions(bctx: BlockBox) = bctx.height += bctx.inline.height @@ -436,7 +436,7 @@ proc buildInlineLayout(bctx: BlockBox, children: seq[BoxBuilder]) = bctx.positionInlines() # Builder only contains block boxes. -proc buildBlockLayout(bctx: BlockBox, children: seq[BoxBuilder], node: Node) = +proc buildBlockLayout(bctx: BlockBox, children: seq[BoxBuilder], node: StyledNode) = bctx.buildBlocks(children, node) func baseline(bctx: BlockBox): int = @@ -609,13 +609,13 @@ proc positionBlocks(bctx: BlockBox) = bctx.width += bctx.padding_left bctx.width += bctx.padding_right -proc buildBlocks(bctx: BlockBox, blocks: seq[BoxBuilder], node: Node) = +proc buildBlocks(bctx: BlockBox, blocks: seq[BoxBuilder], node: StyledNode) = for child in blocks: var cblock: BlockBox case child.computed{"display"} of DISPLAY_BLOCK: cblock = buildBlock(BlockBoxBuilder(child), bctx) of DISPLAY_LIST_ITEM: cblock = buildListItem(ListItemBoxBuilder(child), bctx) - of DISPLAY_TABLE: cblock = buildBlock(TableBoxBuilder(child).content, bctx) + of DISPLAY_TABLE: cblock = buildBlock(BlockBoxBuilder(child), bctx) else: assert false, "child.t is " & $child.computed{"display"} bctx.nested.add(cblock) bctx.positionBlocks() @@ -682,9 +682,19 @@ proc getTableBox(computed: CSSComputedValues): TableBoxBuilder = new(result) result.computed = computed.copyProperties() +# Also known as <tbody>. +proc getTableRowGroupBox(computed: CSSComputedValues): TableRowGroupBoxBuilder = + new(result) + result.computed = computed.copyProperties() + +proc getTableRowBox(computed: CSSComputedValues): TableRowBoxBuilder = + new(result) + result.computed = computed.copyProperties() + type BlockGroup = object parent: BlockBoxBuilder boxes: seq[BoxBuilder] + listItemCounter: int proc add(blockgroup: var BlockGroup, box: BoxBuilder) {.inline.} = blockgroup.boxes.add(box) @@ -703,8 +713,6 @@ func canGenerateAnonymousInline(blockgroup: BlockGroup, computed: CSSComputedVal computed{"white-space"} in {WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP} or not str.onlyWhitespace() -proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(MarkerBoxBuilder)): BlockBoxBuilder - template flush_ibox() = if ibox != nil: assert ibox.computed{"display"} in {DISPLAY_INLINE, DISPLAY_INLINE_BLOCK} @@ -713,10 +721,13 @@ template flush_ibox() = proc newBlockGroup(parent: BlockBoxBuilder): BlockGroup = result.parent = parent + result.listItemCounter = 1 + +proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(MarkerBoxBuilder)): BlockBoxBuilder -proc generateInlineBoxes(box: BlockBoxBuilder, styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport, listItemCounter: var int) +proc generateInlineBoxes(box: BlockBoxBuilder, styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport) -proc generateFromElem(styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport, ibox: var InlineBoxBuilder, listItemCounter: var int) = +proc generateFromElem(styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport, ibox: var InlineBoxBuilder) = let box = blockgroup.parent if styledNode.node != nil: let elem = Element(styledNode.node) @@ -732,17 +743,17 @@ proc generateFromElem(styledNode: StyledNode, blockgroup: var BlockGroup, viewpo box.children.add(childbox) of DISPLAY_LIST_ITEM: blockgroup.flush() - let childbox = getListItemBox(styledNode.computed, listItemCounter) + let childbox = getListItemBox(styledNode.computed, blockgroup.listItemCounter) if childbox.computed{"list-style-position"} == LIST_STYLE_POSITION_INSIDE: childbox.content = styledNode.generateBlockBox(viewport, some(childbox.marker)) childbox.marker = nil else: childbox.content = styledNode.generateBlockBox(viewport) box.children.add(childbox) - inc listItemCounter + inc blockgroup.listItemCounter of DISPLAY_INLINE: flush_ibox - box.generateInlineBoxes(styledNode, blockgroup, viewport, listItemCounter) + box.generateInlineBoxes(styledNode, blockgroup, viewport) of DISPLAY_INLINE_BLOCK: flush_ibox let childbox = getInlineBlockBox(styledNode.computed) @@ -750,25 +761,24 @@ proc generateFromElem(styledNode: StyledNode, blockgroup: var BlockGroup, viewpo blockgroup.add(childbox) of DISPLAY_TABLE: blockgroup.flush() - let childbox = getTableBox(styledNode.computed) - childbox.content = styledNode.generateBlockBox(viewport) - box.children.add(childbox) + #styledNode.generateFromElemTable(blockgroup.parent, viewport) of DISPLAY_TABLE_ROW_GROUP: discard + of DISPLAY_NONE: discard else: discard #TODO -proc generateInlineBoxes(box: BlockBoxBuilder, styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport, listItemCounter: var int) = +proc generateInlineBoxes(box: BlockBoxBuilder, styledNode: StyledNode, blockgroup: var BlockGroup, viewport: Viewport) = var ibox: InlineBoxBuilder = nil for child in styledNode.children: case child.t of STYLED_ELEMENT: - generateFromElem(child, blockgroup, viewport, ibox, listItemCounter) + generateFromElem(child, blockgroup, viewport, ibox) of STYLED_TEXT: if ibox == nil: ibox = getTextBox(styledNode.computed) - ibox.node = styledNode.node + ibox.node = styledNode ibox.text.add(child.text) flush_ibox @@ -777,7 +787,6 @@ proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none( let box = getBlockBox(styledNode.computed) var blockgroup = newBlockGroup(box) var ibox: InlineBoxBuilder = nil - var listItemCounter = 1 # ordinal value of current list if marker.issome: ibox = marker.get @@ -787,12 +796,12 @@ proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none( case child.t of STYLED_ELEMENT: flush_ibox - generateFromElem(child, blockgroup, viewport, ibox, listItemCounter) + generateFromElem(child, blockgroup, viewport, ibox) of STYLED_TEXT: if canGenerateAnonymousInline(blockgroup, box.computed, child.text): if ibox == nil: ibox = getTextBox(styledNode.computed) - ibox.node = styledNode.node + ibox.node = styledNode ibox.text.add(child.text) flush_ibox @@ -805,6 +814,55 @@ proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none( blockgroup.flush() return box +const RowGroupBox = {DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_HEADER_GROUP, + DISPLAY_TABLE_FOOTER_GROUP} +const ProperTableChild = {DISPLAY_TABLE_ROW, DISPLAY_TABLE_COLUMN, + DISPLAY_TABLE_COLUMN_GROUP} + RowGroupBox +const ProperTableRowParent = {DISPLAY_TABLE} + RowGroupBox #TODO inline-table box +const InternalTableBox = {DISPLAY_TABLE_CELL, DISPLAY_TABLE_ROW, DISPLAY_TABLE_COLUMN, DISPLAY_TABLE_COLUMN_GROUP} + RowGroupBox +const TabularContainer = {DISPLAY_TABLE_ROW} + ProperTableRowParent + +proc generateTableBox(styledNode: StyledNode, viewport: Viewport): TableBox = + discard + #let box = getTableBox(styledNode.computed) + #var blockgroup = newBlockGroup(box) + #var ibox: InlineBoxBuilder = nil + #var listItemCounter = 1 + + #for child in styledNode.children: + # if child.t == STYLED_ELEMENT: + # generateFromElem(child, blockgroup, viewport, ibox) + # else: + # if canGenerateAnonymousInline(blockgroup, box.computed, child.text): + # if ibox == nil: + # ibox = getTextBox(styledNode.computed) + # ibox.node = styledNode.node + # ibox.text.add(child.text) + + #flush_ibox + #blockgroup.flush() + + ## Generate missing child wrappers + #var anonRow: TableRowBoxBuilder + #for child in box.children: + # if child.computed{"display"} notin ProperTableChild: + # if anonRow != nil: + # anonRow = getTableRowBox(box.computed.inheritProperties()) + # discard + + +#proc generateFromElemTable(styledNode: StyledNode, viewport: Viewport, table: TableBoxBuilder = nil, parent: BoxBuilder = nil): BlockBox = +# case styledNode.computed{"display"} +# of DISPLAY_TABLE: +# of DISPLAY_TABLE_ROW_GROUP: +# if parent != table: +# # misparented +# let box = getTableRowGroupBox(styledNode.computed) +# let anonymousTable = getTableBox(styledNode.computed.inheritProperties()) +# anonymousTable.children.add(box) +# parent.children.add(anonymousTable) +# else: +# discard proc renderLayout*(viewport: var Viewport, document: Document, root: StyledNode) = let builder = root.generateBlockBox(viewport) diff --git a/src/types/url.nim b/src/types/url.nim index 4099fed9..afe7d83a 100644 --- a/src/types/url.nim +++ b/src/types/url.nim @@ -488,18 +488,16 @@ proc basicParseUrl*(input: string, base = none(Url), url: var Url = Url(), overr #TODO validation error if atsignseen: buffer = "%40" & buffer - atsignseen = true - var i = 0 - while i < buffer.len: - if c == ':' and not passwordtokenseen: - passwordtokenseen = true - inc i - continue - if passwordtokenseen: - url.password.percentEncode(c, UserInfoPercentEncodeSet) - else: - url.username.percentEncode(c, UserInfoPercentEncodeSet) - buffer = "" + atsignseen = true + for c in buffer: + if c == ':' and not passwordtokenseen: + passwordtokenseen = true + continue + if passwordtokenseen: + url.password.percentEncode(c, UserInfoPercentEncodeSet) + else: + url.username.percentEncode(c, UserInfoPercentEncodeSet) + buffer = "" elif not has or c in {'/', '?', '#'} or (url.is_special and c == '\\'): if atsignseen and buffer == "": #TODO validation error |