about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-11-19 21:42:39 +0100
committerbptato <nincsnevem662@gmail.com>2021-11-19 21:52:31 +0100
commit8bbff1f79920fa8175da7425f8f23ad08b97f79e (patch)
tree830abcc6918f8c96bce5538eff5384518876e378 /src
parent42aacf6bf1a52a8ebad902d8ee5adeef57f8822a (diff)
downloadchawan-8bbff1f79920fa8175da7425f8f23ad08b97f79e.tar.gz
User stylesheets and applyStylesheets optimizations
Diffstat (limited to 'src')
-rw-r--r--src/config/config.nim61
-rw-r--r--src/css/style.nim38
-rw-r--r--src/html/dom.nim142
-rw-r--r--src/io/buffer.nim60
-rw-r--r--src/utils/twtstr.nim26
5 files changed, 216 insertions, 111 deletions
diff --git a/src/config/config.nim b/src/config/config.nim
index d055af53..89f80fae 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -37,10 +37,12 @@ type
   StaticConfig = object
     nmap: ActionMap
     lemap: ActionMap
+    stylesheet*: string
 
   Config = object
     nmap*: ActionMap
     lemap*: ActionMap
+    stylesheet*: string
 
 func getConfig(s: StaticConfig): Config =
   return Config(nmap: s.nmap, lemap: s.lemap)
@@ -105,15 +107,53 @@ func constructActionTable*(origTable: ActionMap): ActionMap =
     newTable[realk] = v
   return newTable
 
+proc readUserStylesheet(dir: string, file: string): string =
+  if file.len == 0:
+    return ""
+  if file[0] == '~' or file[0] == '/':
+    var f: File
+    if f.open(expandPath(file)):
+      result = f.readAll()
+      f.close()
+  else:
+    var f: File
+    if f.open(dir / file):
+      result = f.readAll()
+      f.close()
+
 proc parseConfigLine[T](line: string, config: var T) =
   if line.len == 0 or line[0] == '#':
     return
-  let cmd = line.split(' ')
+  var cmd: seq[string]
+  var s = ""
+  var quote = false
+  var escape = false
+  for c in line:
+    if escape:
+      escape = false
+      s &= c
+      continue
+
+    if not quote and c == ' ' and s.len > 0:
+      cmd.add(s)
+      s = ""
+    elif c == '"':
+      quote = not quote
+    elif c == '\\' and not quote:
+      escape = true
+    else:
+      s &= c
+  if s.len > 0:
+    cmd.add(s)
+
   if cmd.len == 3:
     if cmd[0] == "nmap":
       config.nmap[getRealKey(cmd[1])] = parseEnum[TwtAction]("ACTION_" & cmd[2])
     elif cmd[0] == "lemap":
       config.lemap[getRealKey(cmd[1])] = parseEnum[TwtAction]("ACTION_" & cmd[2])
+  elif cmd.len == 2:
+    if cmd[0] == "stylesheet":
+      config.stylesheet = cmd[1]
 
 proc staticReadConfig(): StaticConfig =
   let default = staticRead"res/config"
@@ -124,25 +164,26 @@ proc staticReadConfig(): StaticConfig =
   result.lemap = constructActionTable(result.lemap)
 
 const defaultConfig = staticReadConfig()
-var gconfig* = getConfig(defaultConfig)
+var gconfig*: Config
 
-proc readConfig(filename: string) =
+proc readConfig(dir: string) =
   var f: File
-  let status = f.open(filename, fmRead)
-  var nmap: ActionMap
-  var lemap: ActionMap
+  let status = f.open(dir / "config", fmRead)
   if status:
     var line: TaintedString
     while f.readLine(line):
       parseConfigLine(line, gconfig)
 
-    gconfig.nmap = constructActionTable(nmap)
-    gconfig.lemap = constructActionTable(lemap)
+    gconfig.nmap = constructActionTable(gconfig.nmap)
+    gconfig.lemap = constructActionTable(gconfig.lemap)
+    gconfig.stylesheet = readUserStylesheet(dir, gconfig.stylesheet)
+    f.close()
 
 proc readConfig*() =
+  gconfig = getConfig(defaultConfig)
   when defined(debug):
-    readConfig("res" / "config")
-  readConfig(getConfigDir() / "twt" / "config")
+    readConfig(getCurrentDir() / "res")
+  readConfig(getConfigDir() / "twt")
 
 proc getNormalAction*(s: string): TwtAction =
   if gconfig.nmap.hasKey(s):
diff --git a/src/css/style.nim b/src/css/style.nim
index 9611f862..f018f077 100644
--- a/src/css/style.nim
+++ b/src/css/style.nim
@@ -12,11 +12,9 @@ type
     unit*: CSSUnit
     auto*: bool
 
-  CSSComputedValues* = array[low(CSSPropertyType)..high(CSSPropertyType), CSSComputedValue]
-
   CSSColor* = tuple[r: uint8, g: uint8, b: uint8, a: uint8]
   
-  CSSComputedValue* = object of RootObj
+  CSSComputedValue* = ref object of RootObj
     t*: CSSPropertyType
     case v*: CSSValueType
     of VALUE_COLOR:
@@ -37,6 +35,8 @@ type
       textdecoration*: CSSTextDecoration
     of VALUE_NONE: discard
 
+  CSSComputedValues* = array[low(CSSPropertyType)..high(CSSPropertyType), CSSComputedValue]
+
   CSSSpecifiedValue* = object of CSSComputedValue
     globalValue: CSSGlobalValueType
 
@@ -59,22 +59,22 @@ const PropertyNames = {
   "text-decoration": PROPERTY_TEXT_DECORATION,
 }.toTable()
 
-const ValueTypes = {
+const ValueTypes = [
   PROPERTY_NONE: VALUE_NONE,
   PROPERTY_ALL: VALUE_NONE,
   PROPERTY_COLOR: VALUE_COLOR,
   PROPERTY_MARGIN: VALUE_LENGTH,
   PROPERTY_MARGIN_TOP: VALUE_LENGTH,
-  PROPERTY_MARGIN_BOTTOM: VALUE_LENGTH,
   PROPERTY_MARGIN_LEFT: VALUE_LENGTH,
   PROPERTY_MARGIN_RIGHT: VALUE_LENGTH,
+  PROPERTY_MARGIN_BOTTOM: VALUE_LENGTH,
   PROPERTY_FONT_STYLE: VALUE_FONT_STYLE,
   PROPERTY_DISPLAY: VALUE_DISPLAY,
   PROPERTY_CONTENT: VALUE_CONTENT,
   PROPERTY_WHITE_SPACE: VALUE_WHITE_SPACE,
   PROPERTY_FONT_WEIGHT: VALUE_INTEGER,
   PROPERTY_TEXT_DECORATION: VALUE_TEXT_DECORATION,
-}.toTable()
+]
 
 const InheritedProperties = {
   PROPERTY_COLOR, PROPERTY_FONT_STYLE, PROPERTY_WHITE_SPACE,
@@ -365,7 +365,7 @@ func getInitialColor*(t: CSSPropertyType): CSSColor =
   else:
     return (r: 0u8, g: 0u8, b: 0u8, a: 255u8)
 
-func getDefault(t: CSSPropertyType): CSSComputedValue =
+func calcDefault(t: CSSPropertyType): CSSComputedValue =
   let v = valueType(t)
   var nv: CSSComputedValue
   case v
@@ -377,6 +377,16 @@ func getDefault(t: CSSPropertyType): CSSComputedValue =
     nv = CSSComputedValue(t: t, v: v)
   return nv
 
+func getDefaultTable(): array[low(CSSPropertyType)..high(CSSPropertyType), CSSComputedValue] =
+  for i in low(result)..high(result):
+    result[i] = calcDefault(i)
+
+let defaultTable = getDefaultTable()
+
+func getDefault(t: CSSPropertyType): CSSComputedValue = {.cast(noSideEffect).}:
+  assert defaultTable[t] != nil
+  return defaultTable[t]
+
 func getComputedValue*(prop: CSSSpecifiedValue, current: CSSComputedValues): CSSComputedValue =
   case prop.globalValue
   of VALUE_INHERIT:
@@ -415,13 +425,13 @@ func getComputedValue*(prop: CSSSpecifiedValue, current: CSSComputedValues): CSS
 func getComputedValue*(d: CSSDeclaration, current: CSSComputedValues): CSSComputedValue =
   return getComputedValue(getSpecifiedValue(d), current)
 
-func inheritProperties*(parent: CSSComputedValues): CSSComputedValues =
+proc inheritProperties*(vals: var CSSComputedValues, parent: CSSComputedValues) =
   for prop in low(CSSPropertyType)..high(CSSPropertyType):
-    if inherited(prop):
-      result[prop] = parent[prop]
-    else:
-      result[prop] = getDefault(prop)
+    if vals[prop] == nil:
+      vals[prop] = getDefault(prop)
+    if inherited(prop) and parent[prop] != nil and vals[prop] == getDefault(prop):
+      vals[prop] = parent[prop]
 
-func rootProperties*(): CSSComputedValues =
+proc rootProperties*(vals: var CSSComputedValues) =
   for prop in low(CSSPropertyType)..high(CSSPropertyType):
-    result[prop] = getDefault(prop)
+    vals[prop] = getDefault(prop)
diff --git a/src/html/dom.nim b/src/html/dom.nim
index a807f824..db7b9d89 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -12,9 +12,6 @@ import css/parser
 import css/selector
 import types/enums
 
-const css = staticRead"res/default.css"
-let stylesheet = parseCSS(newStringStream(css))
-
 type
   EventTarget* = ref EventTargetObj
   EventTargetObj = object of RootObj
@@ -31,9 +28,6 @@ type
     parentElement*: Element
     ownerDocument*: Document
 
-    hidden*: bool
-    hover*: bool
-
   Attr* = ref AttrObj
   AttrObj = object of NodeObj
     namespaceURI*: string
@@ -80,6 +74,8 @@ type
     cssvalues*: CSSComputedValues
     cssvalues_before*: CSSComputedValues
     cssvalues_after*: CSSComputedValues
+    hover*: bool
+    cssapplied*: bool
 
   HTMLElement* = ref HTMLElementObj
   HTMLElementObj = object of ElementObj
@@ -140,13 +136,6 @@ func lastElementChild(node: Node): Element =
 func `$`*(element: Element): string =
   return "Element of " & $element.tagType
 
-#TODO
-func nodeAttr*(node: Node): HtmlElement =
-  case node.nodeType
-  of TEXT_NODE: return HtmlElement(node.parentElement)
-  of ELEMENT_NODE: return HtmlElement(node)
-  else: assert(false)
-
 func isTextNode*(node: Node): bool =
   return node.nodeType == TEXT_NODE
 
@@ -194,20 +183,6 @@ func toInputType*(str: string): InputType =
   of "week": INPUT_WEEK
   else: INPUT_UNKNOWN
 
-func toInputSize*(str: string): int =
-  if str.len == 0:
-    return 20
-  for c in str:
-    if not c.isDigit():
-      return 20
-  return str.parseInt()
-
-#TODO
-func ancestor*(htmlNode: Node, tagType: TagType): HtmlElement =
-  result = HtmlElement(htmlNode.parentElement)
-  while result != nil and result.tagType != tagType:
-    result = HtmlElement(result.parentElement)
-
 func newText*(): Text =
   new(result)
   result.nodeType = TEXT_NODE
@@ -260,7 +235,6 @@ func getAttrValue*(element: Element, s: string): string =
 
 #TODO case sensitivity
 
-
 type SelectResult = object
   success: bool
   pseudo: PseudoElem
@@ -377,7 +351,6 @@ proc querySelector*(document: Document, q: string): seq[Element] =
   for sel in selectors:
     result.add(document.selectElems(sel))
 
-
 proc applyProperty(elem: Element, decl: CSSDeclaration, pseudo: PseudoElem) =
   let cval = getComputedValue(decl, elem.cssvalues)
   case pseudo
@@ -387,9 +360,14 @@ proc applyProperty(elem: Element, decl: CSSDeclaration, pseudo: PseudoElem) =
     elem.cssvalues_before[cval.t] = cval
   of PSEUDO_AFTER:
     elem.cssvalues_after[cval.t] = cval
+  elem.cssapplied = true
 
-type ParsedRule = tuple[sels: seq[SelectorList], oblock: CSSSimpleBlock]
-type ParsedStylesheet = seq[ParsedRule]
+type
+  ParsedRule* = tuple[sels: seq[SelectorList], oblock: CSSSimpleBlock]
+  ParsedStylesheet* = seq[ParsedRule]
+  ApplyResult = object
+    normal: seq[tuple[e:Element,d:CSSDeclaration,p:PseudoElem]]
+    important: seq[tuple[e:Element,d:CSSDeclaration,p:PseudoElem]]
 
 func calcRules(elem: Element, rules: ParsedStylesheet):
     array[low(PseudoElem)..high(PseudoElem), seq[CSSSimpleBlock]] =
@@ -405,29 +383,30 @@ func calcRules(elem: Element, rules: ParsedStylesheet):
     tosorts[i].sort((x, y) => cmp(x.s,y.s))
     result[i] = tosorts[i].map((x) => x.b)
 
-proc applyRules*(document: Document, rules: CSSStylesheet): seq[tuple[e:Element,d:CSSDeclaration,p:PseudoElem]] =
+proc applyRules*(document: Document, pss: ParsedStylesheet, reset: bool = false): ApplyResult =
   var stack: seq[Element]
 
   stack.add(document.head)
   stack.add(document.body)
-  document.root.cssvalues = rootProperties()
+  document.root.cssvalues.rootProperties()
 
-  let parsed = rules.value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
   while stack.len > 0:
     let elem = stack.pop()
-    elem.cssvalues = inheritProperties(elem.parentElement.cssvalues)
-    let rules_pseudo = calcRules(elem, parsed)
-    for pseudo in low(PseudoElem)..high(PseudoElem):
-      let rules = rules_pseudo[pseudo]
-      for rule in rules:
-        let decls = parseCSSListOfDeclarations(rule.value)
-        for item in decls:
-          if item of CSSDeclaration:
-            let decl = CSSDeclaration(item)
-            if decl.important:
-              result.add((elem, decl, pseudo))
-            else:
-              elem.applyProperty(decl, pseudo)
+    if not elem.cssapplied:
+      if reset:
+        elem.cssvalues.rootProperties()
+      let rules_pseudo = calcRules(elem, pss)
+      for pseudo in low(PseudoElem)..high(PseudoElem):
+        let rules = rules_pseudo[pseudo]
+        for rule in rules:
+          let decls = parseCSSListOfDeclarations(rule.value)
+          for item in decls:
+            if item of CSSDeclaration:
+              let decl = CSSDeclaration(item)
+              if decl.important:
+                result.important.add((elem, decl, pseudo))
+              else:
+                result.normal.add((elem, decl, pseudo))
 
     var i = elem.children.len - 1
     while i >= 0:
@@ -435,7 +414,7 @@ proc applyRules*(document: Document, rules: CSSStylesheet): seq[tuple[e:Element,
       stack.add(child)
       dec i
 
-proc applyAuthorRules*(document: Document): seq[tuple[e:Element,d:CSSDeclaration,p:PseudoElem]] =
+proc applyAuthorRules*(document: Document): ApplyResult =
   var stack: seq[Element]
   var embedded_rules: seq[ParsedStylesheet]
 
@@ -448,7 +427,6 @@ proc applyAuthorRules*(document: Document): seq[tuple[e:Element,d:CSSDeclaration
         if ct.nodeType == TEXT_NODE:
           rules_head &= Text(ct).data
 
-  
   stack.setLen(0)
 
   stack.add(document.body)
@@ -469,20 +447,22 @@ proc applyAuthorRules*(document: Document): seq[tuple[e:Element,d:CSSDeclaration
     if rules_local.len > 0:
       let parsed = parseCSS(newStringStream(rules_local)).value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
       embedded_rules.add(parsed)
-    let this_rules = embedded_rules.concat()
-    let rules_pseudo = calcRules(elem, this_rules)
-
-    for pseudo in low(PseudoElem)..high(PseudoElem):
-      let rules = rules_pseudo[pseudo]
-      for rule in rules:
-        let decls = parseCSSListOfDeclarations(rule.value)
-        for item in decls:
-          if item of CSSDeclaration:
-            let decl = CSSDeclaration(item)
-            if decl.important:
-              result.add((elem, decl, pseudo))
-            else:
-              elem.applyProperty(decl, pseudo)
+
+    if not elem.cssapplied:
+      let this_rules = embedded_rules.concat()
+      let rules_pseudo = calcRules(elem, this_rules)
+
+      for pseudo in low(PseudoElem)..high(PseudoElem):
+        let rules = rules_pseudo[pseudo]
+        for rule in rules:
+          let decls = parseCSSListOfDeclarations(rule.value)
+          for item in decls:
+            if item of CSSDeclaration:
+              let decl = CSSDeclaration(item)
+              if decl.important:
+                result.important.add((elem, decl, pseudo))
+              else:
+                result.normal.add((elem, decl, pseudo))
 
     var i = elem.children.len - 1
     while i >= 0:
@@ -493,11 +473,37 @@ proc applyAuthorRules*(document: Document): seq[tuple[e:Element,d:CSSDeclaration
     if rules_local.len > 0:
       discard embedded_rules.pop()
 
-proc applyStylesheets*(document: Document) =
-  let important_ua = document.applyRules(stylesheet)
-  let important_author = document.applyAuthorRules()
-  for rule in important_author:
+proc applyStylesheets*(document: Document, uass: ParsedStylesheet, userss: ParsedStylesheet) =
+  let ua = document.applyRules(uass, true)
+  let user = document.applyRules(userss)
+  let author = document.applyAuthorRules()
+  var elems: seq[Element]
+
+  for rule in ua.normal:
+    if not rule.e.cssapplied:
+      elems.add(rule.e)
+    rule.e.applyProperty(rule.d, rule.p)
+  for rule in user.normal:
+    if not rule.e.cssapplied:
+      elems.add(rule.e)
+    rule.e.applyProperty(rule.d, rule.p)
+  for rule in author.normal:
+    if not rule.e.cssapplied:
+      elems.add(rule.e)
+    rule.e.applyProperty(rule.d, rule.p)
+
+  for rule in author.important:
+    if not rule.e.cssapplied:
+      elems.add(rule.e)
+    rule.e.applyProperty(rule.d, rule.p)
+  for rule in user.important:
+    if not rule.e.cssapplied:
+      elems.add(rule.e)
     rule.e.applyProperty(rule.d, rule.p)
-  for rule in important_ua:
+  for rule in ua.important:
+    if not rule.e.cssapplied:
+      elems.add(rule.e)
     rule.e.applyProperty(rule.d, rule.p)
 
+  for elem in elems:
+    elem.cssvalues.inheritProperties(elem.parentElement.cssvalues)
diff --git a/src/io/buffer.nim b/src/io/buffer.nim
index f38d1b24..6b72a401 100644
--- a/src/io/buffer.nim
+++ b/src/io/buffer.nim
@@ -2,17 +2,22 @@ import terminal
 import uri
 import strutils
 import unicode
+import streams
+import sequtils
+import sugar
 
 import types/enums
 import css/style
+import css/parser
+import css/selector
 import utils/twtstr
 import html/dom
 import layout/box
+import layout/engine
 import config/config
 import io/term
 import io/lineedit
 import io/cell
-import layout/engine
 
 type
   Buffer* = ref BufferObj
@@ -32,7 +37,6 @@ type
     fromy*: int
     attrs*: TermAttributes
     document*: Document
-    displaycontrols*: bool
     redraw*: bool
     reshape*: bool
     location*: Uri
@@ -611,28 +615,35 @@ proc updateCursor(buffer: Buffer) =
   if buffer.lines.len == 0:
     buffer.cursory = 0
 
-#TODO this works, but needs rethinking:
-#* reshape is called every time the cursor moves onto or off a line box, which
-#  practically means we're re-interpreting all style-sheets AND re-applying all
-#  rules way too often
-#* reshape also calls redraw so the entire window gets re-painted too which
-#  looks pretty bad (tick)
-#* and finally it re-arranges all CSS boxes too, which is a rather
-#  resource-intensive operation
-#overall the second point is the easiest to solve, then the first and finally
-#the last (there's only so much you can do in a flow layout, especially with
-#the current layout engine)
+#TODO this works, but reshape rearranges all CSS boxes which is a *very*
+#resource-intensive operation, and a significant restructuring of the layout
+#engine is needed to avoid this
 proc updateHover(buffer: Buffer) =
   let nodes = buffer.currentCell().nodes
   if nodes != buffer.prevnodes:
     for node in nodes:
-      if not node.hover:
-        node.hover = true
+      var elem: Element
+      if node of Element:
+        elem = Element(node)
+      else:
+        elem = node.parentElement
+        assert elem != nil
+
+      if not elem.hover:
+        elem.hover = true
         buffer.reshape = true
+        elem.cssapplied = false
     for node in buffer.prevnodes:
-      if node.hover and not (node in nodes):
-        node.hover = false
+      var elem: Element
+      if node of Element:
+        elem = Element(node)
+      else:
+        elem = node.parentElement
+        assert elem != nil
+      if elem.hover and not (node in nodes):
+        elem.hover = false
         buffer.reshape = true
+        elem.cssapplied = false
   buffer.prevnodes = nodes
 
 proc renderPlainText*(buffer: Buffer, text: string) =
@@ -667,9 +678,21 @@ proc renderPlainText*(buffer: Buffer, text: string) =
       buffer.lines.addCell(r)
   buffer.updateCursor()
 
+
+const css = staticRead"res/default.css"
+let ua_stylesheet = parseCSS(newStringStream(css)).value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
+
+#TODO refactor
+var ss_init = false
+var user_stylesheet: ParsedStylesheet
 proc renderDocument*(buffer: Buffer) =
   buffer.clearText()
-  buffer.document.applyStylesheets()
+
+  if not ss_init:
+    user_stylesheet = parseCSS(newStringStream(gconfig.stylesheet)).value.map((x) => (sels: parseSelectors(x.prelude), oblock: x.oblock))
+    ss_init = true
+
+  buffer.document.applyStylesheets(ua_stylesheet, user_stylesheet)
   buffer.rootbox = buffer.document.alignBoxes(buffer.width, buffer.height)
   if buffer.rootbox == nil:
     return
@@ -755,7 +778,6 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
     else:
       feedNext = false
 
-
     let c = getch()
     s &= c
     let action = getNormalAction(s)
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 6291df36..d2a06165 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -4,6 +4,10 @@ import unicode
 import tables
 import json
 import bitops
+import os
+
+when defined(posix):
+  import posix
 
 func ansiStyle*(str: string, style: Style): seq[string] =
   result &= ansiStyleCode(style)
@@ -176,6 +180,28 @@ func skipBlanks*(buf: string, at: int): int =
   while result < buf.len and buf[result].isWhitespace():
     inc result
 
+proc expandPath*(path: string): string =
+  if path.len == 0:
+    return path
+  result = path
+  var i = 0
+  if path[0] == '~':
+    if path.len == 1:
+      result = getHomeDir()
+    elif path[1] == '/':
+      result = getHomeDir() / path.substr(2)
+      inc i
+    else:
+      when defined(posix):
+        i = 1
+        var usr = ""
+        while path[i] != '/':
+          usr &= path[i]
+          inc i
+        let p = getpwnam(usr)
+        if p != nil:
+          result = $p.pw_dir / path.substr(i)
+
 template CSI*(s: varargs[string, `$`]): string =
   var r = "\e["
   var first = true