about summary refs log blame commit diff stats
path: root/src/css/mediaquery.nim
blob: 73649e8fd55a33fa2bd1b0b533d62ac489125ef4 (plain) (tree)
1
2
3
4
5
6
                  
 
                    
                    
                
                   






                                 




                       

                            
                                               

                          











                                                  


                             
                
                        

                                                
              
                           
                               


                               
                
                       
                  
                            
              
                    
             

                      
              




                                   
                             
                                     
 



                                                                   
               

                                     
              
                                                       
             
                           
              
                            
                           
                                           
                         
                                 
                  
                          
                   
                                
                          

                   
                                 
                  
                                                            
 

                                   




                                                              
 
                                                     
 
                                                 

                                         
                                                               


                                  






                                                                            

               
                                                               

                                    
                                               

                            
                                                                      



                   
                                                                 
              
                                              



                                                
              



                                                            
       
                
 
                                                                  
                     


                  
 
                                                                
                             



                            
 





                                                                
 






















                                                                                
       











                                                                             
       
























                                                                               
 



















                                                                     
                             
                        
 

                                                                        
           





















                                                                             
                      
             
                                    
                            
              
                                              
                            
                           
                                              
                            
              
                                                   
                                    
                         

                                                      
                  
                      

                                                                
                            
                    
 


















































                                                                             
 
                                                                   
                                    

               


                                                
                  



                                                        
                     
                      

                                  
                     






                                        
 

                                                                   
                             



                                                                          
                     



                                   
 
                                                                     

                      
                
                 












                                                                
         



                                       

                      










                                                               
         





                                      

                      

                                  

                                                                          
              
                                                     
                       
                                              
                                        

                           
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)