import std/options
import css/cssparser
import css/cssvalues
import types/opt
import utils/twtstr
type
MediaQueryParser = object
at: int
cvals: seq[CSSComponentValue]
MediaType* = enum
mtAll = "all"
mtPrint = "print"
mtScreen = "screen"
mtSpeech = "speech"
mtTty = "tty"
MediaConditionType* = enum
mctNot, mctAnd, mctOr, mctFeature, mctMedia
MediaFeatureType* = enum
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
of mftColor:
range*: Slice[int]
of mftGrid, mftHover, mftPrefersColorScheme,
mftScripting:
b*: bool
of mftWidth, mftHeight:
lengthrange*: LengthRange
MediaQuery* = ref object
case t*: MediaConditionType
of mctMedia:
media*: MediaType
of mctFeature:
feature*: MediaFeature
of mctNot:
n*: MediaQuery
of mctOr:
ora*: MediaQuery
orb*: MediaQuery
of mctAnd:
anda*: MediaQuery
andb*: MediaQuery
MediaQueryList* = seq[MediaQuery]
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
of mftColor:
return "color: " & $mf.range.a & ".." & $mf.range.b
of mftGrid:
return "grid: " & $mf.b
of mftHover:
return "hover: " & $mf.b
of mftPrefersColorScheme:
return "prefers-color-scheme: " & $mf.b
of mftWidth, mftHeight:
result &= $mf.lengthrange.s.a
result &= " <"
if mf.lengthrange.aeq:
result &= "="
result &= ' ' & $mf.t & " <"
if mf.lengthrange.beq:
result &= "="
result &= " "
result &= $mf.lengthrange.s.b
of mftScripting:
return "scripting: " & (if mf.b: "enabled" else: "none")
func `$`*(mq: MediaQuery): string =
case mq.t
of mctMedia: return $mq.media
of mctFeature: return $mq.feature
of mctNot: return "not (" & $mq.n
of mctOr: return "(" & $mq.ora & ") or (" & $mq.orb & ")"
of mctAnd: return "(" & $mq.anda & ") or (" & $mq.andb & ")"
const RangeFeatures = {mftColor, mftWidth, mftHeight}
proc has(parser: MediaQueryParser; i = 0): bool =
return parser.cvals.len > parser.at + i
proc consume(parser: var MediaQueryParser): CSSComponentValue =
result = parser.cvals[parser.at]
inc parser.at
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 =
return parser.cvals[parser.at + i]
proc skipBlanks(parser: var MediaQueryParser) =
while parser.has():
let cval = parser.peek()
if cval of CSSToken and CSSToken(cval).tokenType == cttWhitespace:
inc parser.at
else:
break
proc getBoolFeature(feature: MediaFeatureType): Opt[MediaQuery] =
case feature
of mftGrid, mftHover, mftPrefersColorScheme:
return ok(MediaQuery(
t: mctFeature,
feature: MediaFeature(t: feature, b: true)
))
of mftColor:
return ok(MediaQuery(
t: mctFeature,
feature: MediaFeature(t: feature, range: 1..high(int))
))
else:
return err()
proc skipBlanksCheckHas(parser: var MediaQueryParser): Err[void] =
parser.skipBlanks()
if parser.has():
return ok()
return err()
proc consumeToken(parser: var MediaQueryParser): Opt[CSSToken] =
let cval = parser.consume()
if not (cval of CSSToken):
parser.reconsume()
return err()
return ok(CSSToken(cval))
proc consumeIdent(parser: var MediaQueryParser): Opt[CSSToken] =
let tok = ?parser.consumeToken()
if tok.tokenType != cttIdent:
parser.reconsume()
return err()
return ok(tok)
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 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:
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()
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()
return cssLength(cval)
proc parseLengthRange(parser: var MediaQueryParser; ismin, ismax: bool):
Opt[LengthRange] =
if ismin:
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:
let b = ?parser.parseMqInt(0, 1)
MediaFeature(t: t, b: b)
of mftHover:
let b = ?parser.parseBool("none", "hover")
MediaFeature(t: t, b: b)
of mftPrefersColorScheme:
let b = ?parser.parseBool("light", "dark")
MediaFeature(t: t, b: b)
of mftColor:
let range = ?parser.parseIntRange(ismin, ismax)
MediaFeature(t: t, range: range)
of mftWidth, mftHeight:
let range = ?parser.parseLengthRange(ismin, ismax)
MediaFeature(t: t, lengthrange: range)
of mftScripting:
if ismin or ismax:
return err()
let b = ?parser.parseBool("none", "initial-only", "enabled")
MediaFeature(t: t, b: b)
return ok(feature)
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): Opt[MediaQuery] =
var non = non
if not non:
let tokx = parser.consumeIdent()
if tokx.isSome:
if tokx.get.value.equalsIgnoreCase("not"):
non = true
else:
parser.reconsume()
?parser.skipBlanksCheckHas()
let res = (?parser.parseMediaInParens()).negateIf(non)
parser.skipBlanks()
if not parser.has():
return ok(res)
let tok = ?parser.consumeIdent()
parser.skipBlanks()
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()
if cval of CSSToken:
let tok = CSSToken(cval)
if tok.tokenType != cttIdent or not tok.value.equalsIgnoreCase("and"):
return err()
parser.skipBlanks()
if not parser.has():
return err()
parser.reconsume()
return parser.parseMediaAnd(left)
proc parseMediaQuery(parser: var MediaQueryParser): Opt[MediaQuery] =
parser.skipBlanks()
if not parser.has():
return err()
var non = false
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:
return err()
else:
parser.reconsume()
return parser.parseMediaCondition()
parser.skipBlanks()
if not parser.has():
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:
return err()
elif tokval.equalsIgnoreCase("and"):
parser.reconsume()
return parser.parseMediaAnd(res)
else:
return err()
parser.skipBlanks()
if not parser.has():
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.isSome:
result.add(query.get)