diff options
author | bptato <nincsnevem662@gmail.com> | 2024-08-02 00:29:45 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-08-02 00:44:19 +0200 |
commit | 8f88e5f05b76dd9b16ba91c9e28f07bd1549ae93 (patch) | |
tree | af65e0743bc92a0dd312b0f0f09b36ec8fa14d70 | |
parent | 22625453ef3275adab3a47990c242dc92397b02b (diff) | |
download | chawan-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.md | 4 | ||||
-rw-r--r-- | src/css/cascade.nim | 16 | ||||
-rw-r--r-- | src/css/cssvalues.nim | 37 | ||||
-rw-r--r-- | src/css/mediaquery.nim | 661 | ||||
-rw-r--r-- | src/js/domexception.nim | 13 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 53 | ||||
-rw-r--r-- | test/layout/media-query.color.expected | 1 | ||||
-rw-r--r-- | test/layout/media-query.html | 9 |
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 @@ +[38;2;255;40;40mred[39m 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> |