about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2025-01-15 23:43:45 +0100
committerbptato <nincsnevem662@gmail.com>2025-01-16 01:58:42 +0100
commite76118ea080646cff4ed3017a9423ca299806e05 (patch)
treece1edad9782ff2d9b235add33e6e53ce175f87dc
parent5209268ce43e78dabdddeec761f431fb0f23fe5b (diff)
downloadchawan-e76118ea080646cff4ed3017a9423ca299806e05.tar.gz
cascade: basic CSS variable support
and once again, there was light...

Well, it barely works, but it's enough to get colors back on most sites.
Nested variables aren't supported for now, and shorthand expansion only
"works" for `background' (it's a hack).

Props to the W3C for introducing the C preprocessor to CSS - I was
starting to confuse it with JSSS after calc().
-rw-r--r--doc/troubleshooting.md2
-rw-r--r--src/css/cascade.nim177
-rw-r--r--src/css/cssvalues.nim247
-rw-r--r--src/css/sheet.nim25
-rw-r--r--test/layout/variables.color.expected2
-rw-r--r--test/layout/variables.html7
6 files changed, 296 insertions, 164 deletions
diff --git a/doc/troubleshooting.md b/doc/troubleshooting.md
index 6fce6fe7..d5ed7401 100644
--- a/doc/troubleshooting.md
+++ b/doc/troubleshooting.md
@@ -83,7 +83,7 @@ To do the same for HTML and ANSI text, use `plaintext, pre`.
 ## Why does `$WEBSITE` look awful?
 
 Usually, this is because it uses some CSS features that are not yet implemented
-in Chawan. The most common offenders are grid and CSS variables.
+in Chawan. The most common offenders are grid and calc().
 
 There are three ways of dealing with this:
 
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 0bdd4116..0b23a9a3 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -24,6 +24,8 @@ type
   RuleListEntry = object
     normal: seq[CSSComputedEntry]
     important: seq[CSSComputedEntry]
+    normalVars: seq[CSSVariable]
+    importantVars: seq[CSSVariable]
 
   RuleList = array[CSSOrigin, RuleListEntry]
 
@@ -36,6 +38,16 @@ type
 
   ToSorts = array[PseudoElement, seq[RulePair]]
 
+  InitType = enum
+    itUserAgent, itUser, itOther
+
+  InitMap = array[CSSPropertyType, set[InitType]]
+
+# Forward declarations
+proc applyValue(vals: CSSValues; entry: CSSComputedEntry;
+  parentComputed, previousOrigin: CSSValues; initMap: var InitMap;
+  initType: InitType; nextInitType: set[InitType]; window: Window)
+
 proc calcRule(tosorts: var ToSorts; element: Element;
     depends: var DependencyInfo; rule: CSSRuleDef) =
   for sel in rule.sels:
@@ -46,9 +58,14 @@ proc calcRule(tosorts: var ToSorts; element: Element;
       else:
         tosorts[sel.pseudo].add((sel.specificity, rule))
 
-func calcRules0(map: RuleListMap; styledNode: StyledNode; sheet: CSSStylesheet;
+proc add(entry: var RuleListEntry; rule: CSSRuleDef) =
+  entry.normal.add(rule.normalVals)
+  entry.important.add(rule.importantVals)
+  entry.normalVars.add(rule.normalVars)
+  entry.importantVars.add(rule.importantVars)
+
+proc calcRules0(map: RuleListMap; styledNode: StyledNode; sheet: CSSStylesheet;
     origin: CSSOrigin) =
-  var tosorts = ToSorts.default
   let element = styledNode.element
   var rules: seq[CSSRuleDef] = @[]
   sheet.tagTable.withValue(element.localName, v):
@@ -64,29 +81,95 @@ func calcRules0(map: RuleListMap; styledNode: StyledNode; sheet: CSSStylesheet;
       rules.add(v[])
   for rule in sheet.generalList:
     rules.add(rule)
+  var tosorts = ToSorts.default
   for rule in rules:
     tosorts.calcRule(element, styledNode.depends, rule)
   for pseudo, it in tosorts.mpairs:
-    it.sort(proc(x, y: (int, CSSRuleDef)): int =
-      let n = cmp(x[0], y[0])
+    it.sort(proc(x, y: RulePair): int =
+      let n = cmp(x.specificity, y.specificity)
       if n != 0:
         return n
-      return cmp(x[1].idx, y[1].idx), order = Ascending)
+      return cmp(x.rule.idx, y.rule.idx), order = Ascending)
     for item in it:
-      map.rules[pseudo][origin].normal.add(item[1].normalVals)
-      map.rules[pseudo][origin].important.add(item[1].importantVals)
+      map.rules[pseudo][origin].add(item.rule)
+
+proc findVariable(computed: CSSValues; varName: CAtom): CSSVariable =
+  var vars = computed.vars
+  while vars != nil:
+    let cvar = vars.table.getOrDefault(varName)
+    if cvar != nil:
+      return cvar
+    vars = vars.parent
+  return nil
+
+proc applyVariable(vals: CSSValues; t: CSSPropertyType; varName: CAtom;
+    fallback: ref CSSComputedEntry; parentComputed, previousOrigin: CSSValues;
+    initMap: var InitMap; initType: InitType; nextInitType: set[InitType];
+    window: Window) =
+  let v = t.valueType
+  let cvar = vals.findVariable(varName)
+  if cvar == nil:
+    if fallback != nil:
+      vals.applyValue(fallback[], parentComputed, previousOrigin, initMap,
+        initType, nextInitType, window)
+    return
+  for (iv, entry) in cvar.resolved.mitems:
+    if iv == v:
+      entry.t = t # must override, same var can be used for different props
+      vals.applyValue(entry, parentComputed, previousOrigin, initMap, initType,
+        nextInitType, window)
+      return
+  var entries: seq[CSSComputedEntry] = @[]
+  assert window != nil
+  if entries.parseComputedValues($t, cvar.cvals, window.attrsp[],
+      window.factory).isSome:
+    if entries[0].et != ceVar:
+      cvar.resolved.add((v, entries[0]))
+      vals.applyValue(entries[0], parentComputed, previousOrigin, initMap,
+        initType, nextInitType, window)
+
+proc applyGlobal(vals: CSSValues; t: CSSPropertyType; global: CSSGlobalType;
+    parentComputed, previousOrigin: CSSValues; initMap: InitMap;
+    initType: InitType) =
+  case global
+  of cgtInherit:
+    vals.initialOrCopyFrom(parentComputed, t)
+  of cgtInitial:
+    vals.setInitial(t)
+  of cgtUnset:
+    vals.initialOrInheritFrom(parentComputed, t)
+  of cgtRevert:
+    if previousOrigin != nil and initType in initMap[t]:
+      vals.copyFrom(previousOrigin, t)
+    else:
+      vals.initialOrInheritFrom(parentComputed, t)
+
+proc applyValue(vals: CSSValues; entry: CSSComputedEntry;
+    parentComputed, previousOrigin: CSSValues; initMap: var InitMap;
+    initType: InitType; nextInitType: set[InitType]; window: Window) =
+  case entry.et
+  of ceBit: vals.bits[entry.t].dummy = entry.bit
+  of ceWord: vals.words[entry.t] = entry.word
+  of ceObject: vals.objs[entry.t] = entry.obj
+  of ceGlobal:
+    vals.applyGlobal(entry.t, entry.global, parentComputed, previousOrigin,
+      initMap, initType)
+  of ceVar:
+    vals.applyVariable(entry.t, entry.cvar, entry.fallback, parentComputed,
+      previousOrigin, initMap, initType, nextInitType, window)
+    return # maybe it applies, maybe it doesn't...
+  initMap[entry.t] = initMap[entry.t] + nextInitType
 
 proc applyPresHints(computed: CSSValues; element: Element;
-    attrs: WindowAttributes; initMap: var InitMap) =
+    attrs: WindowAttributes; initMap: var InitMap, window: Window) =
   template set_cv(t, x, b: untyped) =
-    computed.applyValue(makeEntry(t, CSSValueWord(x: b)), nil, nil, nil,
-      initMap, itUserAgent)
-    initMap[t].incl(itUser)
+    computed.applyValue(makeEntry(t, CSSValueWord(x: b)), nil, nil, initMap,
+      itUserAgent, {itUser}, window)
   template set_cv_new(t, x, b: untyped) =
     const v = valueType(t)
     let val = CSSValue(v: v, x: b)
-    computed.applyValue(makeEntry(t, val), nil, nil, nil, initMap, itUserAgent)
-    initMap[t].incl(itUser)
+    computed.applyValue(makeEntry(t, val), nil, nil, initMap, itUserAgent,
+      {itUser}, window)
   template map_width =
     let s = parseDimensionValues(element.attr(satWidth))
     if s.isSome:
@@ -140,8 +223,8 @@ proc applyPresHints(computed: CSSValues; element: Element;
   template set_bgcolor_is_canvas =
     let t = cptBgcolorIsCanvas
     let val = CSSValueBit(bgcolorIsCanvas: true)
-    computed.applyValue(makeEntry(t, val), nil, nil, nil, initMap, itUserAgent)
-    initMap[t].incl(itUser)
+    computed.applyValue(makeEntry(t, val), nil, nil, initMap, itUserAgent,
+      {itUser}, window)
   template map_cellspacing =
     let s = element.attrul(satCellspacing)
     if s.isSome:
@@ -196,39 +279,60 @@ proc applyPresHints(computed: CSSValues; element: Element;
       set_cv cptHeight, length, resolveLength(cuEm, float32(size), attrs)
   else: discard
 
-func applyDeclarations0(rules: RuleList; parent: CSSValues; element: Element;
+proc applyDeclarations0(rules: RuleList; parent, element: Element;
     window: Window): CSSValues =
   result = CSSValues()
+  var parentComputed: CSSValues = nil
+  var parentVars: CSSVariableMap = nil
+  if parent != nil:
+    parentComputed = parent.computed
+    parentVars = parentComputed.vars
+  for origin in CSSOrigin:
+    if rules[origin].importantVars.len > 0:
+      if result.vars == nil:
+        result.vars = newCSSVariableMap(parentVars)
+      for i in countdown(rules[origin].importantVars.high, 0):
+        let cvar = rules[origin].importantVars[i]
+        result.vars.putIfAbsent(cvar.name, cvar)
+  for origin in countdown(CSSOrigin.high, CSSOrigin.low):
+    if rules[origin].normalVars.len > 0:
+      if result.vars == nil:
+        result.vars = newCSSVariableMap(parentVars)
+      for i in countdown(rules[origin].normalVars.high, 0):
+        let cvar = rules[origin].normalVars[i]
+        result.vars.putIfAbsent(cvar.name, cvar)
+  if result.vars == nil:
+    result.vars = parentVars # inherit parent
   var initMap = InitMap.default
   for entry in rules[coUserAgent].normal: # user agent
-    result.applyValue(entry, nil, parent, nil, initMap, itOther)
-    initMap[entry.t] = {itUserAgent, itUser}
+    result.applyValue(entry, parentComputed, nil, initMap, itOther,
+      {itUserAgent, itUser}, window)
   let uaProperties = result.copyProperties()
   # Presentational hints override user agent style, but respect user/author
   # style.
   if element != nil:
-    result.applyPresHints(element, window.attrsp[], initMap)
+    result.applyPresHints(element, window.attrsp[], initMap, window)
   for entry in rules[coUser].normal: # user
-    result.applyValue(entry, nil, parent, uaProperties, initMap, itUserAgent)
-    initMap[entry.t].incl(itUser)
+    result.applyValue(entry, parentComputed, uaProperties, initMap, itUserAgent,
+      {itUser}, window)
   # save user properties so author can use them
   let userProperties = result.copyProperties()
   for entry in rules[coAuthor].normal: # author
-    result.applyValue(entry, nil, parent, userProperties, initMap, itUser)
-    initMap[entry.t].incl(itOther)
+    result.applyValue(entry, parentComputed, userProperties, initMap, itUser,
+      {itOther}, window)
   for entry in rules[coAuthor].important: # author important
-    result.applyValue(entry, nil, parent, userProperties, initMap, itUser)
-    initMap[entry.t].incl(itOther)
+    result.applyValue(entry, parentComputed, userProperties, initMap, itUser,
+      {itOther}, window)
   for entry in rules[coUser].important: # user important
-    result.applyValue(entry, nil, parent, uaProperties, initMap, itUserAgent)
-    initMap[entry.t].incl(itOther)
+    result.applyValue(entry, parentComputed, uaProperties, initMap, itUserAgent,
+      {itOther}, window)
   for entry in rules[coUserAgent].important: # user agent important
-    result.applyValue(entry, nil, parent, nil, initMap, itUserAgent)
-    initMap[entry.t].incl(itOther)
+    result.applyValue(entry, parentComputed, nil, initMap, itUserAgent,
+      {itOther}, window)
   # set defaults
   for t in CSSPropertyType:
     if initMap[t] == {}:
-      result.initialOrInheritFrom(parent, t)
+      result.initialOrInheritFrom(parentComputed, t)
   # Quirk: it seems others aren't implementing what the spec says about
   # blockification.
   # Well, neither will I, because the spec breaks on actual websites.
@@ -243,12 +347,12 @@ func applyDeclarations0(rules: RuleList; parent: CSSValues; element: Element;
     result{"overflow-x"} = result{"overflow-x"}.bfcify()
     result{"overflow-y"} = result{"overflow-y"}.bfcify()
 
-proc applyDeclarations(styledNode: StyledNode; parent: CSSValues;
+proc applyDeclarations(styledNode: StyledNode; parent: Element;
     map: RuleListMap; window: Window; pseudo = peNone) =
   let element = if styledNode.pseudo == peNone: styledNode.element else: nil
   styledNode.computed = map.rules[pseudo].applyDeclarations0(parent, element,
     window)
-  if element != nil and window.settings.scripting == smApp:
+  if element != nil:
     element.computed = styledNode.computed
 
 func hasValues(rules: RuleList): bool =
@@ -267,7 +371,7 @@ func applyMediaQuery(ss: CSSStylesheet; window: Window): CSSStylesheet =
       res.add(mq.children.applyMediaQuery(window))
   return res
 
-func calcRules(styledNode: StyledNode; ua, user: CSSStylesheet;
+proc calcRules(styledNode: StyledNode; ua, user: CSSStylesheet;
     author: seq[CSSStylesheet]; window: Window): RuleListMap =
   let map = RuleListMap()
   map.calcRules0(styledNode, ua, coUserAgent)
@@ -278,6 +382,7 @@ func calcRules(styledNode: StyledNode; ua, user: CSSStylesheet;
   let style = styledNode.element.cachedStyle
   if window.styling and style != nil:
     for decl in style.decls:
+      #TODO variables
       let vals = parseComputedValues(decl.name, decl.value, window.attrsp[],
         window.factory)
       if decl.important:
@@ -328,10 +433,10 @@ proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
       let styledChild = newStyledElement(element)
       map = styledChild.calcRules(ua, user, author, window)
       if styledParent == nil: # root element
-        styledChild.applyDeclarations(rootProperties(), map, window)
+        styledChild.applyDeclarations(nil, map, window)
       else:
         styledParent.children.add(styledChild)
-        styledChild.applyDeclarations(styledParent.computed, map, window)
+        styledChild.applyDeclarations(styledParent.element, map, window)
       return styledChild
     elif child of Text:
       let text = Text(child)
@@ -342,7 +447,7 @@ proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
     let map = frame.parentMap
     if map.rules[pseudo].hasValues():
       let styledPseudo = styledParent.newStyledElement(pseudo)
-      styledPseudo.applyDeclarations(styledParent.computed, map, nil, pseudo)
+      styledPseudo.applyDeclarations(styledParent.element, map, window, pseudo)
       if styledPseudo.computed{"content"}.len > 0:
         for content in styledPseudo.computed{"content"}:
           let child = styledPseudo.newStyledReplacement(content, peNone)
diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim
index 8fba787b..d6763372 100644
--- a/src/css/cssvalues.nim
+++ b/src/css/cssvalues.nim
@@ -146,8 +146,7 @@ type
     cvtNumber = "number"
     cvtOverflow = "overflow"
 
-  CSSGlobalType = enum
-    cgtNone = ""
+  CSSGlobalType* = enum
     cgtInitial = "initial"
     cgtInherit = "inherit"
     cgtRevert = "revert"
@@ -355,7 +354,7 @@ type
     b*: CSSLength
 
   CSSValueBit* {.union.} = object
-    dummy: uint8
+    dummy*: uint8
     bgcolorIsCanvas*: bool
     borderCollapse*: CSSBorderCollapse
     boxSizing*: CSSBoxSizing
@@ -399,10 +398,17 @@ type
       image*: NetworkBitmap
     else: discard
 
+  # Linked list of variable maps, except empty maps are skipped.
+  # Must not be nil for CSSValues, at least during cascade.
+  CSSVariableMap* = ref object
+    parent*: CSSVariableMap
+    table*: Table[CAtom, CSSVariable]
+
   CSSValues* = ref object
     bits*: array[CSSPropertyType.low..LastBitPropType, CSSValueBit]
     words*: array[FirstWordPropType..LastWordPropType, CSSValueWord]
     objs*: array[FirstObjPropType..CSSPropertyType.high, CSSValue]
+    vars*: CSSVariableMap
 
   CSSOrigin* = enum
     coUserAgent
@@ -410,11 +416,12 @@ type
     coAuthor
 
   CSSEntryType* = enum
-    ceBit, ceObject, ceWord, ceVar
+    ceBit, ceObject, ceWord, ceVar, ceGlobal
 
   CSSComputedEntry* = object
+    # put it here, so ComputedEntry remains 2 words wide
+    cvar*: CAtom
     t*: CSSPropertyType
-    global*: CSSGlobalType
     case et*: CSSEntryType
     of ceBit:
       bit*: uint8
@@ -423,17 +430,21 @@ type
     of ceObject:
       obj*: CSSValue
     of ceVar:
-      cvar*: CAtom
+      fallback*: ref CSSComputedEntry
+    of ceGlobal:
+      global*: CSSGlobalType
 
   CSSVariable* = ref object
     name*: CAtom
+    important*: bool
     cvals*: seq[CSSComponentValue]
-    resolved: seq[tuple[v: CSSValueType; entry: CSSComputedEntry]]
+    resolved*: seq[tuple[v: CSSValueType; entry: CSSComputedEntry]]
 
 static:
   doAssert sizeof(CSSValueBit) == 1
   doAssert sizeof(CSSValueWord) <= 8
   doAssert sizeof(CSSValue()[]) <= 16
+  doAssert sizeof(CSSComputedEntry()) <= 16
 
 const ValueTypes = [
   # bits
@@ -508,6 +519,17 @@ const OverflowScrollLike* = {OverflowScroll, OverflowAuto, OverflowOverlay}
 const OverflowHiddenLike* = {OverflowHidden, OverflowClip}
 const FlexReverse* = {FlexDirectionRowReverse, FlexDirectionColumnReverse}
 
+# Forward declarations
+proc parseValue(cvals: openArray[CSSComponentValue]; t: CSSPropertyType;
+  entry: var CSSComputedEntry; attrs: WindowAttributes; factory: CAtomFactory):
+  Opt[void]
+
+proc newCSSVariableMap*(parent: CSSVariableMap): CSSVariableMap =
+  return CSSVariableMap(parent: parent)
+
+proc putIfAbsent*(map: CSSVariableMap; name: CAtom; cvar: CSSVariable) =
+  discard map.table.hasKeyOrPut(name, cvar)
+
 type CSSPropertyReprType* = enum
   cprtBit, cprtWord, cprtObject
 
@@ -904,7 +926,15 @@ func parseDimensionValues*(s: string): Option[CSSLength] =
 func skipBlanks*(vals: openArray[CSSComponentValue]; i: int): int =
   var i = i
   while i < vals.len:
-    if not (vals[i] of CSSToken) or CSSToken(vals[i]).t != cttWhitespace:
+    if vals[i] != cttWhitespace:
+      break
+    inc i
+  return i
+
+func findBlank(vals: openArray[CSSComponentValue]; i: int): int =
+  var i = i
+  while i < vals.len:
+    if vals[i] == cttWhitespace:
       break
     inc i
   return i
@@ -1055,8 +1085,8 @@ func cssAbsoluteLength(val: CSSComponentValue; attrs: WindowAttributes):
     else: discard
   return err()
 
-func cssGlobal(cval: CSSComponentValue): CSSGlobalType =
-  return parseIdent[CSSGlobalType](cval).get(cgtNone)
+func parseGlobal(cval: CSSComponentValue): Opt[CSSGlobalType] =
+  return parseIdent[CSSGlobalType](cval)
 
 func parseQuotes(cvals: openArray[CSSComponentValue]): Opt[CSSQuotes] =
   var i = cvals.skipBlanks(0)
@@ -1256,25 +1286,23 @@ func cssNumber(cval: CSSComponentValue; positive: bool): Opt[float32] =
         return ok(tok.nvalue)
   return err()
 
-proc makeEntry*(t: CSSPropertyType; obj: CSSValue; global = cgtNone):
-    CSSComputedEntry =
-  return CSSComputedEntry(et: ceObject, t: t, obj: obj, global: global)
+proc makeEntry*(t: CSSPropertyType; obj: CSSValue): CSSComputedEntry =
+  return CSSComputedEntry(et: ceObject, t: t, obj: obj)
 
-proc makeEntry*(t: CSSPropertyType; word: CSSValueWord; global = cgtNone):
-    CSSComputedEntry =
-  return CSSComputedEntry(et: ceWord, t: t, word: word, global: global)
+proc makeEntry*(t: CSSPropertyType; word: CSSValueWord): CSSComputedEntry =
+  return CSSComputedEntry(et: ceWord, t: t, word: word)
 
-proc makeEntry*(t: CSSPropertyType; bit: CSSValueBit; global = cgtNone):
-    CSSComputedEntry =
-  return CSSComputedEntry(et: ceBit, t: t, bit: bit.dummy, global: global)
+proc makeEntry*(t: CSSPropertyType; bit: CSSValueBit): CSSComputedEntry =
+  return CSSComputedEntry(et: ceBit, t: t, bit: bit.dummy)
 
 proc makeEntry*(t: CSSPropertyType; global: CSSGlobalType): CSSComputedEntry =
-  return CSSComputedEntry(et: ceObject, t: t, global: global)
+  return CSSComputedEntry(et: ceGlobal, t: t, global: global)
 
-proc parseVar(fun: CSSFunction; entry: var CSSComputedEntry;
-    attrs: WindowAttributes; factory: CAtomFactory): Opt[void] =
-  let i = fun.value.skipBlanks(0)
-  if i >= fun.value.len or fun.value.skipBlanks(i + 1) < fun.value.len:
+proc parseVariable(fun: CSSFunction; t: CSSPropertyType;
+    entry: var CSSComputedEntry; attrs: WindowAttributes;
+    factory: CAtomFactory): Opt[void] =
+  var i = fun.value.skipBlanks(0)
+  if i >= fun.value.len:
     return err()
   let cval = fun.value[i]
   if not (cval of CSSToken):
@@ -1284,11 +1312,22 @@ proc parseVar(fun: CSSFunction; entry: var CSSComputedEntry;
     return err()
   entry = CSSComputedEntry(
     et: ceVar,
+    t: t,
     cvar: factory.toAtom(tok.value.substr(2))
   )
+  i = fun.value.skipBlanks(i + 1)
+  if i < fun.value.len:
+    if fun.value[i] != cttComma:
+      return err()
+    i = fun.value.skipBlanks(i + 1)
+    if i < fun.value.len:
+      entry.fallback = (ref CSSComputedEntry)()
+      if fun.value.toOpenArray(i, fun.value.high).parseValue(t,
+          entry.fallback[], attrs, factory).isNone:
+        entry.fallback = nil
   return ok()
 
-proc parseValue(cvals: openArray[CSSComponentValue];
+proc parseValue(cvals: openArray[CSSComponentValue]; t: CSSPropertyType;
     entry: var CSSComputedEntry; attrs: WindowAttributes;
     factory: CAtomFactory): Opt[void] =
   var i = cvals.skipBlanks(0)
@@ -1301,23 +1340,22 @@ proc parseValue(cvals: openArray[CSSComponentValue];
     if fun.name == cftVar:
       if cvals.skipBlanks(i) < cvals.len:
         return err()
-      return fun.parseVar(entry, attrs, factory)
-  let t = entry.t
+      return fun.parseVariable(t, entry, attrs, factory)
   let v = valueType(t)
   template set_new(prop, val: untyped) =
     entry = CSSComputedEntry(
-      t: entry.t,
+      t: t,
       et: ceObject,
       obj: CSSValue(v: v, prop: val)
     )
   template set_word(prop, val: untyped) =
     entry = CSSComputedEntry(
-      t: entry.t,
+      t: t,
       et: ceWord,
       word: CSSValueWord(prop: val)
     )
   template set_bit(prop, val: untyped) =
-    entry = CSSComputedEntry(t: entry.t, et: ceBit, bit: cast[uint8](val))
+    entry = CSSComputedEntry(t: t, et: ceBit, bit: cast[uint8](val))
   case v
   of cvtDisplay: set_bit display, ?parseIdent[CSSDisplay](cval)
   of cvtWhiteSpace: set_bit whiteSpace, ?parseIdent[CSSWhiteSpace](cval)
@@ -1422,10 +1460,11 @@ proc getDefaultWord(t: CSSPropertyType): CSSValueWord =
   else: return CSSValueWord(dummy: 0)
 
 func lengthShorthand(cvals: openArray[CSSComponentValue];
-    props: array[4, CSSPropertyType]; global: CSSGlobalType;
+    props: array[4, CSSPropertyType]; global: Opt[CSSGlobalType];
     attrs: WindowAttributes; hasAuto = true): Opt[seq[CSSComputedEntry]] =
   var res: seq[CSSComputedEntry] = @[]
-  if global != cgtNone:
+  if global.isSome:
+    let global = global.get
     for t in props:
       res.add(makeEntry(t, global))
     return ok(res)
@@ -1474,25 +1513,20 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
   var i = cvals.skipBlanks(0)
   if i >= cvals.len:
     return err()
-  let global = cssGlobal(cvals[i])
+  let global = parseGlobal(cvals[i])
   case shorthandType(name)
   of cstNone:
     let t = propertyType(name)
     if t.isSome:
       let t = t.get
-      if global != cgtNone:
-        res.add(makeEntry(t, global))
+      if global.isSome:
+        res.add(makeEntry(t, global.get))
       else:
-        let et = case t.reprType
-        of cprtBit: ceBit
-        of cprtObject: ceObject
-        of cprtWord: ceWord
-        var entry = CSSComputedEntry(t: t, et: et)
-        ?cvals.parseValue(entry, attrs, factory)
+        var entry = CSSComputedEntry()
+        ?cvals.parseValue(t, entry, attrs, factory)
         res.add(entry)
   of cstAll:
-    if global == cgtNone:
-      return err()
+    let global = ?global
     for t in CSSPropertyType:
       res.add(makeEntry(t, global))
   of cstMargin:
@@ -1501,29 +1535,42 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
     res.add(?lengthShorthand(cvals, PropertyPaddingSpec, global, attrs,
       hasAuto = false))
   of cstBackground:
-    var bgcolorval = getDefaultWord(cptBackgroundColor)
-    var bgimageval = getDefault(cptBackgroundImage)
-    var valid = true
-    if global == cgtNone:
-      for tok in cvals:
-        if tok == cttWhitespace:
-          continue
-        if (let r = parseImage(tok); r.isSome):
-          bgimageval = CSSValue(v: cvtImage, image: r.get)
-        elif (let r = cssColor(tok); r.isSome):
-          bgcolorval.color = r.get
+    if global.isSome:
+      let global = global.get
+      res.add(makeEntry(cptBackgroundColor, global))
+      res.add(makeEntry(cptBackgroundImage, global))
+    else:
+      var bgcolor = makeEntry(cptBackgroundColor,
+        getDefaultWord(cptBackgroundColor))
+      var bgimage = makeEntry(cptBackgroundImage,
+        getDefault(cptBackgroundImage))
+      var valid = true
+      var i = cvals.skipBlanks(0)
+      while i < cvals.len:
+        let j = cvals.findBlank(i)
+        if cvals.toOpenArray(i, j - 1).parseValue(bgcolor.t, bgcolor, attrs,
+            factory).isSome:
+          discard
+        elif cvals.toOpenArray(i, j - 1).parseValue(bgimage.t, bgimage, attrs,
+            factory).isSome:
+          discard
         else:
           #TODO when we implement the other shorthands too
           #valid = false
           discard
-    if valid:
-      res.add(makeEntry(cptBackgroundColor, bgcolorval, global))
-      res.add(makeEntry(cptBackgroundImage, bgimageval, global))
+        i = cvals.skipBlanks(j)
+      if valid:
+        res.add(bgcolor)
+        res.add(bgimage)
   of cstListStyle:
-    var positionVal = CSSValueBit()
     var typeVal = CSSValueBit()
-    var valid = true
-    if global == cgtNone:
+    if global.isSome:
+      let global = global.get
+      res.add(makeEntry(cptListStylePosition, global))
+      res.add(makeEntry(cptListStyleType, global))
+    else:
+      var valid = true
+      var positionVal = CSSValueBit()
       for tok in cvals:
         if tok == cttWhitespace:
           continue
@@ -1535,11 +1582,16 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
           #TODO list-style-image
           #valid = false
           discard
-    if valid:
-      res.add(makeEntry(cptListStylePosition, positionVal, global))
-      res.add(makeEntry(cptListStyleType, typeVal, global))
+      if valid:
+        res.add(makeEntry(cptListStylePosition, positionVal))
+        res.add(makeEntry(cptListStyleType, typeVal))
   of cstFlex:
-    if global == cgtNone:
+    if global.isSome:
+      let global = global.get
+      res.add(makeEntry(cptFlexGrow, global))
+      res.add(makeEntry(cptFlexShrink, global))
+      res.add(makeEntry(cptFlexBasis, global))
+    else:
       var i = cvals.skipBlanks(0)
       if i >= cvals.len:
         return err()
@@ -1568,13 +1620,13 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
         res.add(makeEntry(cptFlexBasis, val))
       else: # omitted, default to 0px
         let val = CSSValueWord(length: cssLength(0))
-        res.add(makeEntry(cptFlexBasis, val, global))
-    else:
-      res.add(makeEntry(cptFlexGrow, global))
-      res.add(makeEntry(cptFlexShrink, global))
-      res.add(makeEntry(cptFlexBasis, global))
+        res.add(makeEntry(cptFlexBasis, val))
   of cstFlexFlow:
-    if global == cgtNone:
+    if global.isSome:
+      let global = global.get
+      res.add(makeEntry(cptFlexDirection, global))
+      res.add(makeEntry(cptFlexWrap, global))
+    else:
       var i = cvals.skipBlanks(0)
       if i >= cvals.len:
         return err()
@@ -1587,11 +1639,12 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
         let wrap = ?parseIdent[CSSFlexWrap](cvals[i])
         var val = CSSValueBit(flexWrap: wrap)
         res.add(makeEntry(cptFlexWrap, val))
-    else:
-      res.add(makeEntry(cptFlexDirection, global))
-      res.add(makeEntry(cptFlexWrap, global))
   of cstOverflow:
-    if global == cgtNone:
+    if global.isSome:
+      let global = global.get
+      res.add(makeEntry(cptOverflowX, global))
+      res.add(makeEntry(cptOverflowY, global))
+    else:
       var i = cvals.skipBlanks(0)
       if i >= cvals.len:
         return err()
@@ -1603,9 +1656,6 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
           y.overflow = ?parseIdent[CSSOverflow](cvals[i])
         res.add(makeEntry(cptOverflowX, x))
         res.add(makeEntry(cptOverflowY, y))
-    else:
-      res.add(makeEntry(cptOverflowX, global))
-      res.add(makeEntry(cptOverflowY, global))
   return ok()
 
 proc parseComputedValues*(name: string; value: seq[CSSComponentValue];
@@ -1615,13 +1665,13 @@ proc parseComputedValues*(name: string; value: seq[CSSComponentValue];
     return res
   return @[]
 
-proc copyFrom(a, b: CSSValues; t: CSSPropertyType) =
+proc copyFrom*(a, b: CSSValues; t: CSSPropertyType) =
   case t.reprType
   of cprtBit: a.bits[t] = b.bits[t]
   of cprtWord: a.words[t] = b.words[t]
   of cprtObject: a.objs[t] = b.objs[t]
 
-proc setInitial(a: CSSValues; t: CSSPropertyType) =
+proc setInitial*(a: CSSValues; t: CSSPropertyType) =
   case t.reprType
   of cprtBit: a.bits[t].dummy = 0
   of cprtWord: a.words[t] = getDefaultWord(t)
@@ -1633,40 +1683,11 @@ proc initialOrInheritFrom*(a, b: CSSValues; t: CSSPropertyType) =
   else:
     a.setInitial(t)
 
-type
-  InitType* = enum
-    itUserAgent, itUser, itOther
-
-  InitMap* = array[CSSPropertyType, set[InitType]]
-
-  CSSVariableMap* = ref object
-    parent*: CSSVariableMap
-    vars*: seq[CSSVariable]
-
-proc applyValue*(vals: CSSValues; entry: CSSComputedEntry;
-    varMap: CSSVariableMap; parent, previousOrigin: CSSValues; inited: InitMap;
-    initType: InitType) =
-  case entry.global
-  of cgtInherit:
-    if parent != nil:
-      vals.copyFrom(parent, entry.t)
-    else:
-      vals.setInitial(entry.t)
-  of cgtInitial:
-    vals.setInitial(entry.t)
-  of cgtUnset:
-    vals.initialOrInheritFrom(parent, entry.t)
-  of cgtRevert:
-    if previousOrigin != nil and initType in inited[entry.t]:
-      vals.copyFrom(previousOrigin, entry.t)
-    else:
-      vals.initialOrInheritFrom(parent, entry.t)
-  of cgtNone:
-    case entry.et
-    of ceBit: vals.bits[entry.t].dummy = entry.bit
-    of ceWord: vals.words[entry.t] = entry.word
-    of ceObject: vals.objs[entry.t] = entry.obj
-    of ceVar: discard #eprint "TODO"
+proc initialOrCopyFrom*(a, b: CSSValues; t: CSSPropertyType) =
+  if b != nil:
+    a.copyFrom(b, t)
+  else:
+    a.setInitial(t)
 
 func inheritProperties*(parent: CSSValues): CSSValues =
   result = CSSValues()
diff --git a/src/css/sheet.nim b/src/css/sheet.nim
index c0963d2b..f574bd4d 100644
--- a/src/css/sheet.nim
+++ b/src/css/sheet.nim
@@ -185,31 +185,28 @@ proc addRule(sheet: CSSStylesheet; rule: CSSQualifiedRule) =
   var sels = parseSelectors(rule.prelude, sheet.factory)
   if sels.len > 0:
     let decls = rule.oblock.value.parseDeclarations()
-    var pass2 = newSeqOfCap[CSSDeclaration](decls.len)
     let rule = CSSRuleDef(sels: move(sels), idx: sheet.len)
     for decl in decls:
       if decl.name.startsWith("--"):
         let cvar = CSSVariable(
-          # case sensitive, it seems
           name: sheet.factory.toAtom(decl.name.substr(2)),
-          cvals: move(decl.value)
+          cvals: decl.value
         )
         if decl.important:
           rule.importantVars.add(cvar)
         else:
           rule.normalVars.add(cvar)
-      pass2.add(decl)
-    for decl in pass2:
-      if decl.important:
-        let olen = rule.importantVals.len
-        if rule.importantVals.parseComputedValues(decl.name, decl.value,
-            sheet.attrs[], sheet.factory).isNone:
-          rule.importantVals.setLen(olen)
       else:
-        let olen = rule.normalVals.len
-        if rule.normalVals.parseComputedValues(decl.name, decl.value,
-            sheet.attrs[], sheet.factory).isNone:
-          rule.normalVals.setLen(olen)
+        if decl.important:
+          let olen = rule.importantVals.len
+          if rule.importantVals.parseComputedValues(decl.name, decl.value,
+              sheet.attrs[], sheet.factory).isNone:
+            rule.importantVals.setLen(olen)
+        else:
+          let olen = rule.normalVals.len
+          if rule.normalVals.parseComputedValues(decl.name, decl.value,
+              sheet.attrs[], sheet.factory).isNone:
+            rule.normalVals.setLen(olen)
     sheet.add(rule)
     inc sheet.len
 
diff --git a/test/layout/variables.color.expected b/test/layout/variables.color.expected
new file mode 100644
index 00000000..59986144
--- /dev/null
+++ b/test/layout/variables.color.expected
@@ -0,0 +1,2 @@
+purple                                                                          
+
diff --git a/test/layout/variables.html b/test/layout/variables.html
new file mode 100644
index 00000000..cd6d3696
--- /dev/null
+++ b/test/layout/variables.html
@@ -0,0 +1,7 @@
+<style>
+a { --purple: purple; color: var(--purple) }
+div { color: white; --pink: yellow !important; background-color: var(--purple) }
+</style>
+<a>
+<div>purple</div>
+</a>