about summary refs log tree commit diff stats
path: root/src/css/mediaquery.nim
blob: 73649e8fd55a33fa2bd1b0b533d62ac489125ef4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
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)