about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-08-02 00:29:45 +0200
committerbptato <nincsnevem662@gmail.com>2024-08-02 00:44:19 +0200
commit8f88e5f05b76dd9b16ba91c9e28f07bd1549ae93 (patch)
treeaf65e0743bc92a0dd312b0f0f09b36ec8fa14d70
parent22625453ef3275adab3a47990c242dc92397b02b (diff)
downloadchawan-8f88e5f05b76dd9b16ba91c9e28f07bd1549ae93.tar.gz
cssvalues, twtstr, mediaquery: refactor & fixes
* cssvalues, twtstr: unify enum parsing code paths, parse enums by
  bisearch instead of hash tables
* mediaquery: refactor (long overdue), fix range comparison syntax
  parsing, make ident comparisons case-insensitive (as they should be)
-rw-r--r--doc/architecture.md4
-rw-r--r--src/css/cascade.nim16
-rw-r--r--src/css/cssvalues.nim37
-rw-r--r--src/css/mediaquery.nim661
-rw-r--r--src/js/domexception.nim13
-rw-r--r--src/utils/twtstr.nim53
-rw-r--r--test/layout/media-query.color.expected1
-rw-r--r--test/layout/media-query.html9
8 files changed, 354 insertions, 440 deletions
diff --git a/doc/architecture.md b/doc/architecture.md
index ae3d95d3..ae21e181 100644
--- a/doc/architecture.md
+++ b/doc/architecture.md
@@ -276,8 +276,8 @@ css/ contains everything related to styling: CSS parsing and cascading.
 
 The parser is not very interesting, it's just an implementation of the CSS 3
 parsing module. The latest iteration of the selector parser is pretty good. The
-media query parser is horrible and should be rewritten. And the CSS value parser
-works OK, but is missing features like variables.
+media query parser and the CSS value parser both work OK, but are missing
+some commonly used features like variables.
 
 Cascading is slow, though it could be slower. Chawan has style caching, so
 re-styles are normally very fast. Also, a hash map is used for reducing initial
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 8a8d8c44..56430d52 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -26,17 +26,12 @@ type
     user: RuleList
     author: seq[RuleList]
 
-func appliesLR(feature: MediaFeature; window: Window;
-    n: LayoutUnit): bool =
-  let a = px(feature.lengthrange.a, window.attrs, 0)
-  let b = px(feature.lengthrange.b, window.attrs, 0)
-  if not feature.lengthaeq and a == n:
+func appliesLR(feature: MediaFeature; window: Window; n: LayoutUnit): bool =
+  let a = feature.lengthrange.s.a.px(window.attrs, 0)
+  let b = feature.lengthrange.s.b.px(window.attrs, 0)
+  if not feature.lengthrange.aeq and a == n or a > n:
     return false
-  if a > n:
-    return false
-  if not feature.lengthbeq and b == n:
-    return false
-  if b < n:
+  if not feature.lengthrange.beq and b == n or b < n:
     return false
   return true
 
@@ -66,7 +61,6 @@ func applies(mq: MediaQuery; window: Window): bool =
     of mtScreen: return true
     of mtSpeech: return false
     of mtTty: return true
-    of mtUnknown: return false
   of mctNot:
     return not mq.n.applies(window)
   of mctAnd:
diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim
index 0c699806..9bb063b3 100644
--- a/src/css/cssvalues.nim
+++ b/src/css/cssvalues.nim
@@ -1,3 +1,4 @@
+import std/algorithm
 import std/macros
 import std/options
 import std/strutils
@@ -582,8 +583,7 @@ func ic_to_px(ic: float64; window: WindowAttributes): LayoutUnit =
 func ex_to_px(ex: float64; window: WindowAttributes): LayoutUnit =
   (ex * float64(window.ppc) / 2).toLayoutUnit()
 
-func px*(l: CSSLength; window: WindowAttributes; p: LayoutUnit): LayoutUnit
-    {.inline.} =
+func px*(l: CSSLength; window: WindowAttributes; p: LayoutUnit): LayoutUnit =
   case l.unit
   of cuEm, cuRem: em_to_px(l.num, window)
   of cuCh: ch_to_px(l.num, window)
@@ -785,45 +785,26 @@ const Colors: Table[string, ARGBColor] = ((func (): Table[string, ARGBColor] =
   result["transparent"] = rgba(0x00, 0x00, 0x00, 0x00)
 )())
 
-func isToken(cval: CSSComponentValue): bool {.inline.} =
+template isToken(cval: CSSComponentValue): bool =
   cval of CSSToken
 
-func getToken(cval: CSSComponentValue): CSSToken {.inline.} =
+template getToken(cval: CSSComponentValue): CSSToken =
   CSSToken(cval)
 
-func parseIdent0[T](map: static openArray[(string, T)]; s: string): Opt[T] =
-  # cmp when len is small enough, otherwise lowercase & hashmap
-  when map.len <= 4:
-    for (k, v) in map:
-      if k.equalsIgnoreCase(s):
-        return ok(v)
-  else:
-    const MapTable = map.toTable()
-    let val = s.toLowerAscii()
-    if val in MapTable:
-      return ok(MapTable[val])
-  return err()
-
-func parseIdent[T](map: static openArray[(string, T)]; cval: CSSComponentValue):
-    Opt[T] =
+func parseIdent(map: openArray[IdentMapItem]; cval: CSSComponentValue):
+    Opt[int] =
   if isToken(cval):
     let tok = getToken(cval)
     if tok.tokenType == cttIdent:
-      return parseIdent0[T](map, tok.value)
+      return map.parseEnumNoCase0(tok.value)
   return err()
 
-func getIdentMap[T: enum](e: typedesc[T]): seq[(string, T)] =
-  result = @[]
-  for e in T.low .. T.high:
-    result.add(($e, e))
-
 func parseIdent[T: enum](cval: CSSComponentValue): Opt[T] =
   const IdentMap = getIdentMap(T)
-  return IdentMap.parseIdent(cval)
+  return ok(cast[T](?IdentMap.parseIdent(cval)))
 
 func cssLength(val: float64; unit: string): Opt[CSSLength] =
-  const UnitMap = getIdentMap(CSSUnit)
-  let u = ?UnitMap.parseIdent0(unit)
+  let u = ?parseEnumNoCase[CSSUnit](unit)
   return ok(CSSLength(num: val, unit: u))
 
 const CSSLengthAuto* = CSSLength(auto: true)
diff --git a/src/css/mediaquery.nim b/src/css/mediaquery.nim
index cd7e9b5e..277813b4 100644
--- a/src/css/mediaquery.nim
+++ b/src/css/mediaquery.nim
@@ -1,5 +1,4 @@
-import std/strutils
-import std/tables
+import std/options
 
 import css/cssparser
 import css/cssvalues
@@ -12,7 +11,6 @@ type
     cvals: seq[CSSComponentValue]
 
   MediaType* = enum
-    mtUnknown = "unknown"
     mtAll = "all"
     mtPrint = "print"
     mtScreen = "screen"
@@ -23,8 +21,18 @@ type
     mctNot, mctAnd, mctOr, mctFeature, mctMedia
 
   MediaFeatureType* = enum
-    mftColor, mftGrid, mftHover, mftPrefersColorScheme, mftWidth, mftHeight,
-    mftScripting
+    mftColor = "color"
+    mftGrid = "grid"
+    mftHover = "hover"
+    mftPrefersColorScheme = "prefers-color-scheme"
+    mftWidth = "width"
+    mftHeight = "height"
+    mftScripting = "scripting"
+
+  LengthRange* = object
+    s*: Slice[CSSLength]
+    aeq*: bool
+    beq*: bool
 
   MediaFeature* = object
     case t*: MediaFeatureType
@@ -34,9 +42,7 @@ type
         mftScripting:
       b*: bool
     of mftWidth, mftHeight:
-      lengthrange*: Slice[CSSLength]
-      lengthaeq*: bool
-      lengthbeq*: bool
+      lengthrange*: LengthRange
 
   MediaQuery* = ref object
     case t*: MediaConditionType
@@ -58,6 +64,10 @@ type
   MediaQueryComparison = enum
     mqcEq, mqcGt, mqcLt, mqcGe, mqcLe
 
+# Forward declarations
+proc parseMediaCondition(parser: var MediaQueryParser; non = false;
+  noor = false): Opt[MediaQuery]
+
 # for debugging
 func `$`*(mf: MediaFeature): string =
   case mf.t
@@ -70,26 +80,26 @@ func `$`*(mf: MediaFeature): string =
   of mftPrefersColorScheme:
     return "prefers-color-scheme: " & $mf.b
   of mftWidth:
-    result &= $mf.lengthrange.a
+    result &= $mf.lengthrange.s.a
     result &= " <"
-    if mf.lengthaeq:
+    if mf.lengthrange.aeq:
       result &= "="
     result &= " width <"
-    if mf.lengthbeq:
+    if mf.lengthrange.beq:
       result &= "="
     result &= " "
-    result &= $mf.lengthrange.b
+    result &= $mf.lengthrange.s.b
   of mftHeight:
-    result &= $mf.lengthrange.a
+    result &= $mf.lengthrange.s.a
     result &= " <"
-    if mf.lengthaeq:
+    if mf.lengthrange.aeq:
       result &= "="
     result &= " width "
     result &= "<"
-    if mf.lengthbeq:
+    if mf.lengthrange.beq:
       result &= "="
     result &= " "
-    result &= $mf.lengthrange.b
+    result &= $mf.lengthrange.s.b
   of mftScripting:
     return "scripting: " & (if mf.b: "enabled" else: "none")
 
@@ -101,30 +111,28 @@ func `$`*(mq: MediaQuery): string =
   of mctOr: return "(" & $mq.ora & ") or (" & $mq.orb & ")"
   of mctAnd: return "(" & $mq.anda & ") or (" & $mq.andb & ")"
 
-const MediaTypes = {
-  "all": mtAll,
-  "print": mtPrint,
-  "screen": mtScreen,
-  "speech": mtSpeech,
-  "tty": mtTty
-}.toTable()
-
 const RangeFeatures = {mftColor, mftWidth, mftHeight}
 
-proc has(parser: MediaQueryParser; i = 0): bool {.inline.} =
+proc has(parser: MediaQueryParser; i = 0): bool =
   return parser.cvals.len > parser.at + i
 
-proc consume(parser: var MediaQueryParser): CSSComponentValue {.inline.} =
+proc consume(parser: var MediaQueryParser): CSSComponentValue =
   result = parser.cvals[parser.at]
   inc parser.at
 
-proc reconsume(parser: var MediaQueryParser) {.inline.} =
+proc consumeSimpleBlock(parser: var MediaQueryParser): Opt[CSSSimpleBlock] =
+  let res = parser.consume()
+  if res of CSSSimpleBlock:
+    return ok(CSSSimpleBlock(res))
+  return err()
+
+proc reconsume(parser: var MediaQueryParser) =
   dec parser.at
 
-proc peek(parser: MediaQueryParser; i = 0): CSSComponentValue {.inline.} =
+proc peek(parser: MediaQueryParser; i = 0): CSSComponentValue =
   return parser.cvals[parser.at + i]
 
-proc skipBlanks(parser: var MediaQueryParser) {.inline.} =
+proc skipBlanks(parser: var MediaQueryParser) =
   while parser.has():
     let cval = parser.peek()
     if cval of CSSToken and CSSToken(cval).tokenType == cttWhitespace:
@@ -132,397 +140,318 @@ proc skipBlanks(parser: var MediaQueryParser) {.inline.} =
     else:
       break
 
-proc getBoolFeature(feature: MediaFeatureType): MediaQuery =
-  result = MediaQuery(t: mctFeature)
+proc getBoolFeature(feature: MediaFeatureType): Opt[MediaQuery] =
   case feature
   of mftGrid, mftHover, mftPrefersColorScheme:
-    result.feature = MediaFeature(t: feature, b: true)
+    return ok(MediaQuery(
+      t: mctFeature,
+      feature: MediaFeature(t: feature, b: true)
+    ))
   of mftColor:
-    result.feature = MediaFeature(t: feature, range: 1..high(int))
+    return ok(MediaQuery(
+      t: mctFeature,
+      feature: MediaFeature(t: feature, range: 1..high(int))
+    ))
   else:
-    return nil
+    return err()
 
-template skip_has(): bool =
+proc skipBlanksCheckHas(parser: var MediaQueryParser): Err[void] =
   parser.skipBlanks()
-  parser.has()
-
-template get_tok(tok: untyped) =
-  if not (cval of CSSToken): return nil
-  tok = CSSToken(cval)
-
-template get_idtok(tok: untyped) =
-  get_tok(tok)
-  if tok.tokenType != cttIdent: return nil
+  if parser.has():
+    return ok()
+  return err()
 
-template consume_token(): CSSToken =
+proc consumeToken(parser: var MediaQueryParser): Opt[CSSToken] =
   let cval = parser.consume()
-  if not (cval of CSSToken): return nil
-  CSSToken(cval)
+  if not (cval of CSSToken):
+    parser.reconsume()
+    return err()
+  return ok(CSSToken(cval))
 
-template skip_consume(): CSSToken =
-  parser.skipBlanks()
-  consume_token()
+proc consumeIdent(parser: var MediaQueryParser): Opt[CSSToken] =
+  let tok = ?parser.consumeToken()
+  if tok.tokenType != cttIdent:
+    parser.reconsume()
+    return err()
+  return ok(tok)
 
-template expect_int(i: var int) =
-  let cval = parser.consume()
-  if not (cval of CSSToken): return nil
-  let tok = CSSToken(cval)
-  if tok.tokenType == cttNumber and tok.tflagb == tflagbInteger:
-    i = int(tok.nvalue)
+proc consumeInt(parser: var MediaQueryParser): Opt[int] =
+  let tok = ?parser.consumeToken()
+  if tok.tokenType != cttNumber or tok.tflagb == tflagbInteger:
+    parser.reconsume()
+    return err()
+  return ok(int(tok.nvalue))
+
+proc parseMqInt(parser: var MediaQueryParser; ifalse, itrue: int): Opt[bool] =
+  let i = ?parser.consumeInt()
+  if i == ifalse:
+    return ok(false)
+  elif i == itrue:
+    return ok(true)
+  return err()
+
+proc parseBool(parser: var MediaQueryParser; sfalse, strue: string): Opt[bool] =
+  let tok = ?parser.consumeToken()
+  if tok.tokenType != cttIdent:
+    return err()
+  if tok.value.equalsIgnoreCase(strue):
+    return ok(true)
+  elif tok.value.equalsIgnoreCase(sfalse):
+    return ok(false)
   else:
-    return nil
-
-template expect_mq_int(b: bool; ifalse, itrue: int) =
-  var i: int
-  expect_int(i)
-  if i == ifalse: b = false
-  elif i == itrue: b = true
-  else: return nil
-
-template expect_bool(b: bool; sfalse, strue: string) =
-  let tok = consume_token()
-  if tok.tokenType != cttIdent: return nil
-  let s = tok.value
-  case s
-  of strue: b = true
-  of sfalse: b = false
-  else: return nil
-
-template expect_bool(b: bool; sfalse, sfalse2, strue: string) =
-  let tok = consume_token()
-  if tok.tokenType != cttIdent: return nil
-  let s = tok.value
-  case s
-  of strue: b = true
-  of sfalse, sfalse2: b = false
-  else: return nil
-
-template expect_comparison(comparison: var MediaQueryComparison) =
-  let tok = consume_token()
-  if tok != cttDelim: return nil
-  let c = tok.cvalue
-  if c notin {'=', '<', '>'}: return nil
-  block parse:
-    case c
-    of '<':
-      if parser.has():
-        let tok = skip_consume()
-        if tok == cttDelim and tok.cvalue == '=':
-          comparison = mqcLe
-          break parse
-        parser.reconsume()
-      comparison = mqcLt
-    of '>':
-      if parser.has():
-        let tok = skip_consume()
-        if tok == cttDelim and tok.cvalue == '=':
-          comparison = mqcGe
-          break parse
-        parser.reconsume()
-      comparison = mqcGt
-    of '=':
-      comparison = mqcEq
-    else: return nil
-
-template expect_int_range(range: var Slice[int]; ismin, ismax: bool) =
-  if ismin:
-    expect_int(range.a)
-  elif ismax:
-    expect_int(range.b)
+    return err()
+
+proc parseBool(parser: var MediaQueryParser; sfalse, sfalse2, strue: string):
+    Opt[bool] =
+  let tok = ?parser.consumeToken()
+  if tok.tokenType != cttIdent:
+    return err()
+  if tok.value.equalsIgnoreCase(strue):
+    return ok(true)
+  elif tok.value.equalsIgnoreCase(sfalse) or
+      tok.value.equalsIgnoreCase(sfalse2):
+    return ok(false)
   else:
-    let tok = consume_token
-    parser.reconsume()
-    if tok.tokenType == cttDelim:
-      var comparison: MediaQueryComparison
-      expect_comparison(comparison)
-      if not skip_has: return nil
-      case comparison
-      of mqcEq:
-        expect_int(range.a) #TODO should be >= 0 (for color at least)
-        range.b = range.a
-      of mqcGt:
-        expect_int(range.a)
-        range.b = high(int)
-      of mqcGe:
-        expect_int(range.a)
-        range.b = high(int)
-      of mqcLt:
-        expect_int(range.b)
-      of mqcLe:
-        expect_int(range.b)
-    else:
-      return nil
+    return err()
+
+proc parseComparison(parser: var MediaQueryParser): Opt[MediaQueryComparison] =
+  let tok = ?parser.consumeToken()
+  if tok != cttDelim or tok.cvalue notin {'=', '<', '>'}:
+    return err()
+  case tok.cvalue
+  of '<':
+    if parser.has():
+      parser.skipBlanks()
+      let tok = ?parser.consumeToken()
+      if tok == cttDelim and tok.cvalue == '=':
+        return ok(mqcLe)
+      parser.reconsume()
+    return ok(mqcLt)
+  of '>':
+    if parser.has():
+      parser.skipBlanks()
+      let tok = ?parser.consumeToken()
+      if tok == cttDelim and tok.cvalue == '=':
+        return ok(mqcGe)
+      parser.reconsume()
+    return ok(mqcGt)
+  of '=': return ok(mqcEq)
+  else: return err()
 
-template expect_length(length: var CSSLength) =
+proc parseIntRange(parser: var MediaQueryParser; ismin, ismax: bool):
+    Opt[Slice[int]] =
+  if ismin:
+    let a = ?parser.consumeInt()
+    return ok(a .. int.high)
+  if ismax:
+    let b = ?parser.consumeInt()
+    return ok(0 .. b)
+  let comparison = ?parser.parseComparison()
+  ?parser.skipBlanksCheckHas()
+  let n = ?parser.consumeInt()
+  case comparison
+  of mqcEq: #TODO should be >= 0 (for color at least)
+    return ok(n .. n)
+  of mqcGt, mqcGe:
+    return ok(n .. int.high)
+  of mqcLt, mqcLe:
+    return ok(0 .. n)
+
+proc parseLength(parser: var MediaQueryParser): Opt[CSSLength] =
   let cval = parser.consume()
-  let r = cssLength(cval)
-  if r.isNone:
-    return nil
-  length = r.get
+  return cssLength(cval)
 
-template expect_length_range(range: var Slice[CSSLength];
-    lengthaeq, lengthbeq: var bool; ismin, ismax: bool) =
+proc parseLengthRange(parser: var MediaQueryParser; ismin, ismax: bool):
+    Opt[LengthRange] =
   if ismin:
-    expect_length(range.a)
-    range.b = CSSLength(num: Inf, unit: cuPx)
-    lengthaeq = true
-  elif ismax:
-    range.a = CSSLength(num: 0, unit: cuPx)
-    expect_length(range.b)
-    lengthbeq = true
-  else:
-    let tok = consume_token
-    parser.reconsume()
-    if tok.tokenType == cttDelim:
-      var comparison: MediaQueryComparison
-      expect_comparison(comparison)
-      if not skip_has: return nil
-      expect_length(range.a)
-      if not skip_has: return nil
-      expect_length(range.b)
-      case comparison
-      of mqcEq:
-        expect_length(range.a)
-        range.b = range.a
-        lengthaeq = true
-        lengthbeq = true
-      of mqcGt:
-        expect_length(range.a)
-        range.b = CSSLength(num: Inf, unit: cuPx)
-      of mqcGe:
-        expect_length(range.a)
-        range.b = CSSLength(num: Inf, unit: cuPx)
-        lengthaeq = true
-      of mqcLt:
-        range.a = CSSLength(num: 0, unit: cuPx)
-        expect_length(range.b)
-      of mqcLe:
-        range.a = CSSLength(num: 0, unit: cuPx)
-        expect_length(range.b)
-        lengthbeq = true
-    else:
-      return nil
-
-proc parseFeature(parser: var MediaQueryParser; t: MediaFeatureType;
-    ismin, ismax: bool): MediaQuery =
-  if not parser.has(): return getBoolFeature(t)
-  let cval = parser.consume()
-  var tok: CSSToken
-  get_tok(tok)
-  if tok.tokenType != cttColon: return nil
-  parser.skipBlanks()
-  if (ismin or ismax) and t notin RangeFeatures:
-    return nil
-  if not parser.has(): return nil
+    let a = ?parser.parseLength()
+    let b = CSSLength(num: Inf, unit: cuPx)
+    return ok(LengthRange(s: a .. b, aeq: true, beq: false))
+  if ismax:
+    let a = CSSLength(num: 0, unit: cuPx)
+    let b = ?parser.parseLength()
+    return ok(LengthRange(s: a .. b, aeq: false, beq: true))
+  let comparison = ?parser.parseComparison()
+  ?parser.skipBlanksCheckHas()
+  let len = ?parser.parseLength()
+  case comparison
+  of mqcEq:
+    return ok(LengthRange(s: len .. len, aeq: true, beq: true))
+  of mqcGt, mqcGe:
+    let b = CSSLength(num: Inf, unit: cuPx)
+    return ok(LengthRange(s: len .. b, aeq: comparison == mqcGe, beq: false))
+  of mqcLt, mqcLe:
+    let a = CSSLength(num: 0, unit: cuPx)
+    return ok(LengthRange(s: a .. len, aeq: false, beq: comparison == mqcLe))
+
+proc parseFeature0(parser: var MediaQueryParser; t: MediaFeatureType;
+    ismin, ismax: bool): Opt[MediaFeature] =
   let feature = case t
   of mftGrid:
-    var b: bool
-    expect_mq_int(b, 0, 1)
+    let b = ?parser.parseMqInt(0, 1)
     MediaFeature(t: t, b: b)
   of mftHover:
-    var b: bool
-    expect_bool(b, "none", "hover")
+    let b = ?parser.parseBool("none", "hover")
     MediaFeature(t: t, b: b)
   of mftPrefersColorScheme:
-    var b: bool
-    expect_bool(b, "light", "dark")
+    let b = ?parser.parseBool("light", "dark")
     MediaFeature(t: t, b: b)
   of mftColor:
-    var range: Slice[int]
-    expect_int_range(range, ismin, ismax)
+    let range = ?parser.parseIntRange(ismin, ismax)
     MediaFeature(t: t, range: range)
   of mftWidth, mftHeight:
-    var range: Slice[CSSLength]
-    var lengthaeq: bool
-    var lengthbeq: bool
-    expect_length_range(range, lengthaeq, lengthbeq, ismin, ismax)
-    MediaFeature(
-      t: t,
-      lengthrange: range,
-      lengthaeq: lengthaeq,
-      lengthbeq: lengthbeq
-    )
+    let range = ?parser.parseLengthRange(ismin, ismax)
+    MediaFeature(t: t, lengthrange: range)
   of mftScripting:
     if ismin or ismax:
-      return nil
-    var b: bool
-    expect_bool(b, "none", "initial-only", "enabled")
+      return err()
+    let b = ?parser.parseBool("none", "initial-only", "enabled")
     MediaFeature(t: t, b: b)
-  parser.skipBlanks()
-  if parser.has():
-    return nil
-  return MediaQuery(t: mctFeature, feature: feature)
+  return ok(feature)
 
-proc parseMediaCondition(parser: var MediaQueryParser; non = false;
-  noor = false): MediaQuery
-
-proc parseMediaInParens(parser: var MediaQueryParser): MediaQuery =
-  var fparser: MediaQueryParser
-  block:
-    let cval = parser.consume()
-    if not (cval of CSSSimpleBlock): return nil
-
-    let sb = CSSSimpleBlock(cval)
-    if sb.token.tokenType != cttLparen: return nil
-
-    fparser.cvals = sb.value
-    fparser.skipBlanks()
-
-  block:
-    let cval = fparser.consume()
-    var tok: CSSToken
-    get_tok(tok)
-    fparser.skipBlanks()
-    if tok.tokenType == cttIdent:
-      var tokval = tok.value
-      let ismin = tokval.startsWith("min-")
-      let ismax = tokval.startsWith("max-")
-      if ismin or ismax:
-        tokval = tokval.substr(4)
-      case tokval
-      of "not":
-        return fparser.parseMediaCondition(true)
-      of "color":
-        return fparser.parseFeature(mftColor, ismin, ismax)
-      of "width":
-        return fparser.parseFeature(mftWidth, ismin, ismax)
-      of "grid":
-        return fparser.parseFeature(mftGrid, ismin, ismax)
-      of "hover":
-        return fparser.parseFeature(mftHover, ismin, ismax)
-      of "prefers-color-scheme":
-        return fparser.parseFeature(mftPrefersColorScheme, ismin, ismax)
-      of "scripting":
-        return fparser.parseFeature(mftScripting, ismin, ismax)
-      else: discard
-  return nil
-
-proc parseMediaOr(parser: var MediaQueryParser; left: MediaQuery): MediaQuery =
-  let right = parser.parseMediaCondition()
-  if right != nil:
-    return MediaQuery(t: mctOr, ora: left, orb: right)
-  return nil
-
-proc parseMediaAnd(parser: var MediaQueryParser; left: MediaQuery): MediaQuery =
-  let right = parser.parseMediaCondition()
-  if right != nil:
-    return MediaQuery(t: mctAnd, anda: left, andb: right)
-  return nil
+proc parseFeature(parser: var MediaQueryParser; t: MediaFeatureType;
+    ismin, ismax: bool): Opt[MediaQuery] =
+  if not parser.has():
+    return getBoolFeature(t)
+  let tok = ?parser.consumeToken()
+  if t notin RangeFeatures and (tok.tokenType != cttColon or ismin or ismax):
+    return err()
+  if tok.tokenType != cttColon:
+    # for range parsing; e.g. we might have gotten a delim or similar
+    parser.reconsume()
+  ?parser.skipBlanksCheckHas()
+  let feature = ?parser.parseFeature0(t, ismin, ismax)
+  parser.skipBlanks()
+  if parser.has(): # die if there's still something left to parse
+    return err()
+  return ok(MediaQuery(t: mctFeature, feature: feature))
+
+proc parseMediaInParens(parser: var MediaQueryParser): Opt[MediaQuery] =
+  let sb = ?parser.consumeSimpleBlock()
+  if sb.token.tokenType != cttLparen:
+    return err()
+  var fparser = MediaQueryParser(cvals: sb.value)
+  fparser.skipBlanks()
+  let tok = ?fparser.consumeIdent()
+  fparser.skipBlanks()
+  if tok.value.equalsIgnoreCase("not"):
+    return fparser.parseMediaCondition(non = true)
+  var tokval = tok.value
+  let ismin = tokval.startsWithIgnoreCase("min-")
+  let ismax = tokval.startsWithIgnoreCase("max-")
+  if ismin or ismax:
+    tokval = tokval.substr(4)
+  let x = parseEnumNoCase[MediaFeatureType](tokval)
+  if x.isNone:
+    return err()
+  return fparser.parseFeature(x.get, ismin, ismax)
+
+proc parseMediaOr(parser: var MediaQueryParser; left: MediaQuery):
+    Opt[MediaQuery] =
+  let right = ?parser.parseMediaCondition()
+  return ok(MediaQuery(t: mctOr, ora: left, orb: right))
+
+proc parseMediaAnd(parser: var MediaQueryParser; left: MediaQuery):
+    Opt[MediaQuery] =
+  let right = ?parser.parseMediaCondition()
+  return ok(MediaQuery(t: mctAnd, anda: left, andb: right))
+
+func negateIf(mq: MediaQuery; non: bool): MediaQuery =
+  if non:
+    return MediaQuery(t: mctNot, n: mq)
+  return mq
 
 proc parseMediaCondition(parser: var MediaQueryParser; non = false;
-    noor = false): MediaQuery =
+    noor = false): Opt[MediaQuery] =
   var non = non
   if not non:
-    let cval = parser.consume()
-    if cval of CSSToken and CSSToken(cval).tokenType == cttIdent:
-      if CSSToken(cval).value == "not":
+    let tokx = parser.consumeIdent()
+    if tokx.isSome:
+      if tokx.get.value.equalsIgnoreCase("not"):
         non = true
-    else:
-      parser.reconsume()
-
+      else:
+        parser.reconsume()
+  ?parser.skipBlanksCheckHas()
+  let res = (?parser.parseMediaInParens()).negateIf(non)
   parser.skipBlanks()
   if not parser.has():
-    return nil
-
-  result = parser.parseMediaInParens()
-
-  if result == nil:
-    return nil
-
-  if non:
-    result = MediaQuery(t: mctNot, n: result)
-
+    return ok(res)
+  let tok = ?parser.consumeIdent()
   parser.skipBlanks()
-  if not parser.has():
-    return result
+  if tok.value.equalsIgnoreCase("and"):
+    return parser.parseMediaAnd(res)
+  elif tok.value.equalsIgnoreCase("or"):
+    if noor:
+      return err()
+    return parser.parseMediaOr(res)
+  return ok(res)
 
+proc maybeParseAnd(parser: var MediaQueryParser; left: MediaQuery):
+    Opt[MediaQuery] =
   let cval = parser.consume()
-  var tok: CSSToken
-  get_idtok(tok)
+  if cval of CSSToken:
+    let tok = CSSToken(cval)
+    if tok.tokenType != cttIdent or not tok.value.equalsIgnoreCase("and"):
+      return err()
   parser.skipBlanks()
-  let tokval = tok.value
-  case tokval
-  of "and":
-    return parser.parseMediaAnd(result)
-  of "or":
-    if noor:
-      return nil
-    return parser.parseMediaOr(result)
-  else: discard
+  if not parser.has():
+    return err()
+  parser.reconsume()
+  return parser.parseMediaAnd(left)
 
-proc parseMediaQuery(parser: var MediaQueryParser): MediaQuery =
+proc parseMediaQuery(parser: var MediaQueryParser): Opt[MediaQuery] =
   parser.skipBlanks()
   if not parser.has():
-    return nil
+    return err()
   var non = false
-  block:
-    let cval = parser.consume()
-    if cval of CSSToken:
-      let tok = CSSToken(cval)
-      if tok.tokenType == cttIdent:
-        let tokval = tok.value
-        case tokval
-        of "not":
-          non = true
-        of "only":
-          discard
-        elif tokval in MediaTypes:
-          result = MediaQuery(t: mctMedia, media: MediaTypes[tokval])
-        else:
-          return nil
-      else:
-        return nil
+  let cval = parser.consume()
+  var res: MediaQuery = nil
+  if cval of CSSToken:
+    let tok = CSSToken(cval)
+    if tok.tokenType != cttIdent:
+      return err()
+    let tokval = tok.value
+    if tokval.equalsIgnoreCase("not"):
+      non = true
+    elif tokval.equalsIgnoreCase("only"):
+      discard
+    elif (let x = parseEnumNoCase[MediaType](tokval); x.isSome):
+      res = MediaQuery(t: mctMedia, media: x.get)
     else:
-      parser.reconsume()
-      return parser.parseMediaCondition()
+      return err()
+  else:
+    parser.reconsume()
+    return parser.parseMediaCondition()
   parser.skipBlanks()
   if not parser.has():
-    return result
-  block:
-    let cval = parser.consume()
-    if cval of CSSToken:
-      let tok = CSSToken(cval)
-      if tok.tokenType == cttIdent:
-        let tokval = tok.value
-        if result == nil:
-          if tokval in MediaTypes:
-            let mq = MediaQuery(t: mctMedia, media: MediaTypes[tokval])
-            if non:
-              result = MediaQuery(t: mctNot, n: mq)
-            else:
-              result = mq
-          else:
-            return nil
-        else:
-          if tokval == "and":
-            parser.reconsume()
-            return parser.parseMediaAnd(result)
-          else:
-            return nil
-      else:
-        return nil
+    return ok(res)
+  let tokx = parser.consumeToken()
+  if tokx.isNone:
+    return parser.parseMediaCondition(non)
+  let tok = tokx.get
+  if tok.tokenType != cttIdent:
+    return err()
+  let tokval = tok.value
+  if res == nil:
+    if (let x = parseEnumNoCase[MediaType](tokval); x.isSome):
+      res = MediaQuery(t: mctMedia, media: x.get).negateIf(non)
     else:
-      parser.reconsume()
-      return parser.parseMediaCondition(non)
+      return err()
+  elif tokval.equalsIgnoreCase("and"):
+    parser.reconsume()
+    return parser.parseMediaAnd(res)
+  else:
+    return err()
   parser.skipBlanks()
   if not parser.has():
-    return result
-  block:
-    let cval = parser.consume()
-    if cval of CSSToken:
-      let tok = CSSToken(cval)
-      if tok.tokenType != cttIdent or tok.value != "and":
-        return nil
-    parser.skipBlanks()
-    if not parser.has():
-      return nil
-    parser.reconsume()
-    return parser.parseMediaAnd(result)
+    return ok(res)
+  return parser.maybeParseAnd(res)
 
 proc parseMediaQueryList*(cvals: seq[CSSComponentValue]): MediaQueryList =
+  result = @[]
   let cseplist = cvals.parseCommaSepComponentValues()
   for list in cseplist:
     var parser = MediaQueryParser(cvals: list)
     let query = parser.parseMediaQuery()
-    if query != nil:
-      result.add(query)
+    if query.isSome:
+      result.add(query.get)
diff --git a/src/js/domexception.nim b/src/js/domexception.nim
index 68e313e9..1fab7ed9 100644
--- a/src/js/domexception.nim
+++ b/src/js/domexception.nim
@@ -28,18 +28,24 @@ const NamesTable = {
   "TimeoutError": 23u16,
   "InvalidNodeTypeError": 24u16,
   "DataCloneError": 25u16
-}.toTable()
+}
 
 type
   DOMException* = ref object of JSError
     name* {.jsget.}: string
+    code {.jsget.}: uint16
 
   DOMResult*[T] = Result[T, DOMException]
 
 jsDestructor(DOMException)
 
 proc newDOMException*(message = ""; name = "Error"): DOMException {.jsctor.} =
-  return DOMException(e: jeDOMException, name: name, message: message)
+  let ex = DOMException(e: jeDOMException, name: name, message: message)
+  for it in NamesTable:
+    if it[0] == name:
+      ex.code = it[1]
+      break
+  return ex
 
 template errDOMException*(message, name: string): untyped =
   err(newDOMException(message, name))
@@ -47,8 +53,5 @@ template errDOMException*(message, name: string): untyped =
 func message0(this: DOMException): string {.jsfget: "message".} =
   return this.message
 
-func code(this: DOMException): uint16 {.jsfget.} =
-  return NamesTable.getOrDefault(this.name, 0u16)
-
 proc addDOMExceptionModule*(ctx: JSContext) =
   ctx.registerType(DOMException, JS_CLASS_ERROR, errid = opt(jeDOMException))
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 7cadd41f..a5770042 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -537,37 +537,34 @@ proc makeCRLF*(s: string): string =
     else:
       result &= s[i]
 
+type IdentMapItem* = tuple[s: string; n: int]
+
+func getIdentMap*[T: enum](e: typedesc[T]): seq[IdentMapItem] =
+  result = @[]
+  for e in T.low .. T.high:
+    result.add(($e, int(e)))
+  result.sort(proc(x, y: IdentMapItem): int = cmp(x[0], y[0]))
+
 func strictParseEnum*[T: enum](s: string): Option[T] =
-  # cmp when len is small enough, otherwise hashmap
-  when {T.low..T.high}.len <= 4:
-    for e in T.low .. T.high:
-      if $e == s:
-        return some(e)
-  else:
-    const tab = (func(): Table[string, T] =
-      result = initTable[string, T]()
-      for e in T.low .. T.high:
-        result[$e] = e
-    )()
-    if s in tab:
-      return some(tab[s])
+  const IdentMap = getIdentMap(T)
+  let i = IdentMap.binarySearch(s, proc(x: IdentMapItem; y: string): int =
+    return x[0].cmp(y)
+  )
+  if i != -1:
+    return some(cast[T](IdentMap[i].n))
   return none(T)
 
-func parseEnumNoCase*[T: enum](s: string): Option[T] =
-  # cmp when len is small enough, otherwise hashmap
-  when {T.low..T.high}.len <= 4:
-    for e in T.low .. T.high:
-      if ($e).equalsIgnoreCase(s):
-        return some(e)
-  else:
-    const tab = (func(): Table[string, T] =
-      result = initTable[string, T]()
-      for e in T.low .. T.high:
-        result[$e] = e
-    )()
-    if s in tab:
-      return some(tab[s])
-  return none(T)
+func parseEnumNoCase0*(map: openArray[IdentMapItem]; s: string): Opt[int] =
+  let i = map.binarySearch(s, proc(x: IdentMapItem; y: string): int =
+    return x[0].cmpIgnoreCase(y)
+  )
+  if i != -1:
+    return ok(map[i].n)
+  return err()
+
+func parseEnumNoCase*[T: enum](s: string): Opt[T] =
+  const IdentMap = getIdentMap(T)
+  return ok(cast[T](?IdentMap.parseEnumNoCase0(s)))
 
 proc getContentTypeAttr*(contentType, attrname: string): string =
   var i = contentType.find(';')
diff --git a/test/layout/media-query.color.expected b/test/layout/media-query.color.expected
new file mode 100644
index 00000000..edb263b3
--- /dev/null
+++ b/test/layout/media-query.color.expected
@@ -0,0 +1 @@
+red
diff --git a/test/layout/media-query.html b/test/layout/media-query.html
new file mode 100644
index 00000000..1d09e25f
--- /dev/null
+++ b/test/layout/media-query.html
@@ -0,0 +1,9 @@
+<style>
+@media (WiDtH >= 1px) and (HeiGhT < 10000px) {
+	#red { color: red }
+}
+@media (width >= 1px 2px) {
+	#red { color: blue }
+}
+</style>
+<div id=red>red</div>