import std/options
import std/unicode
import js/domexception
import types/opt
import utils/twtstr
type
CSSTokenType* = enum
cttIdent, cttFunction, cttAtKeyword, cttHash, cttString, cttBadString,
cttUrl, cttBadUrl, cttDelim, cttNumber, cttPercentage, cttDimension,
cttWhitespace, cttCdo, cttCdc, cttColon, cttSemicolon, cttComma,
cttRbracket, cttLbracket, cttLparen, cttRparen, cttLbrace, cttRbrace
CSSTokenizerState = object
at: int
buf: string
curr: char
CSSParseState = object
tokens: seq[CSSParsedItem]
at: int
tflaga = enum
tflagaUnrestricted, tflagaId
tflagb* = enum
tflagbInteger, tflagbNumber
CSSParsedItem* = ref object of RootObj
CSSComponentValue* = ref object of CSSParsedItem
CSSToken* = ref object of CSSComponentValue
case tokenType*: CSSTokenType
of cttIdent, cttFunction, cttAtKeyword,
cttHash, cttString, cttUrl:
value*: string
tflaga*: tflaga
of cttDelim:
cvalue*: char
of cttNumber, cttPercentage, cttDimension:
nvalue*: float64
tflagb*: tflagb
unit*: string
else: discard
CSSRule* = ref object of CSSParsedItem
prelude*: seq[CSSComponentValue]
oblock*: CSSSimpleBlock
CSSAtRule* = ref object of CSSRule
name*: string
CSSQualifiedRule* = ref object of CSSRule
CSSDeclaration* = ref object of CSSComponentValue
name*: string
value*: seq[CSSComponentValue]
important*: bool
CSSFunction* = ref object of CSSComponentValue
name*: string
value*: seq[CSSComponentValue]
CSSSimpleBlock* = ref object of CSSComponentValue
token*: CSSToken
value*: seq[CSSComponentValue]
CSSRawStylesheet* = object
value*: seq[CSSRule]
CSSAnB* = tuple[A, B: int]
# For debugging
proc `$`*(c: CSSParsedItem): string =
if c of CSSToken:
let c = CSSToken(c)
case c.tokenType:
of cttFunction, cttAtKeyword:
result &= $c.tokenType & c.value & '\n'
of cttUrl:
result &= "url(" & c.value & ")"
of cttHash:
result &= '#' & c.value
of cttIdent:
result &= c.value
of cttString:
result &= ("\"" & c.value & "\"")
of cttDelim:
if c.cvalue != char(128):
result &= c.cvalue
else:
result &= "<UNICODE>"
of cttDimension:
case c.tflagb
of tflagbNumber:
result &= $c.nvalue & c.unit
of tflagbInteger:
result &= $int64(c.nvalue) & c.unit
of cttNumber:
result &= $c.nvalue & c.unit
of cttPercentage:
result &= $c.nvalue & "%"
of cttColon:
result &= ":"
of cttWhitespace:
result &= " "
of cttSemicolon:
result &= ";\n"
of cttComma:
result &= ","
else:
result &= $c.tokenType & '\n'
elif c of CSSDeclaration:
let decl = CSSDeclaration(c)
result &= decl.name
result &= ": "
for s in decl.value:
result &= $s
if decl.important:
result &= " !important"
result &= ";\n"
elif c of CSSFunction:
result &= CSSFunction(c).name & "("
for s in CSSFunction(c).value:
result &= $s
result &= ")"
elif c of CSSSimpleBlock:
case CSSSimpleBlock(c).token.tokenType
of cttLbrace: result &= "{\n"
of cttLparen: result &= "("
of cttLbracket: result &= "["
else: discard
for s in CSSSimpleBlock(c).value:
result &= $s
case CSSSimpleBlock(c).token.tokenType
of cttLbrace: result &= "\n}"
of cttLparen: result &= ")"
of cttLbracket: result &= "]"
else: discard
elif c of CSSRule:
if c of CSSAtRule:
result &= CSSAtRule(c).name & " "
result &= $CSSRule(c).prelude & "\n"
result &= $CSSRule(c).oblock
func `==`*(a: CSSParsedItem, b: CSSTokenType): bool =
return a of CSSToken and CSSToken(a).tokenType == b
const IdentStart = AsciiAlpha + NonAscii + {'_'}
const Ident = IdentStart + AsciiDigit + {'-'}
proc consume(state: var CSSTokenizerState): char =
state.curr = state.buf[state.at]
inc state.at
return state.curr
proc consumeRChar(state: var CSSTokenizerState): char =
var r: Rune
fastRuneAt(state.buf, state.at, r)
if int32(r) < 0x80:
return char(r)
return char(128)
proc reconsume(state: var CSSTokenizerState) =
dec state.at
func peek(state: CSSTokenizerState; i: int = 0): char =
return state.buf[state.at + i]
proc has(state: var CSSTokenizerState; i: int = 0): bool =
return state.at + i < state.buf.len
proc isValidEscape(a, b: char): bool =
return a == '\\' and b != '\n'
proc isValidEscape(state: var CSSTokenizerState): bool =
return state.has() and isValidEscape(state.curr, state.peek())
# current + next + next(1)
proc startsWithIdentSequence(state: var CSSTokenizerState): bool =
case state.curr
of '-':
return state.has() and state.peek() in IdentStart + {'-'} or
state.has(1) and state.isValidEscape()
of IdentStart:
return true
of '\\':
return state.isValidEscape()
else:
return false
# next, next(1), next(2)
proc next3startsWithIdentSequence(state: var CSSTokenizerState): bool =
if not state.has():
return false
case state.peek()
of '-':
return state.has(1) and state.peek(1) in IdentStart + {'-'} or
state.has(2) and isValidEscape(state.peek(1), state.peek(2))
of IdentStart:
return true
of '\\':
return state.has(1) and isValidEscape(state.peek(), state.peek(1))
else:
return false
proc startsWithNumber(state: var CSSTokenizerState): bool =
if state.has():
case state.peek()
of '+', '-':
if state.has(1):
if state.peek(1) in AsciiDigit:
return true
elif state.peek(1) == '.':
if state.has(2) and state.peek(2) in AsciiDigit:
return true
of '.':
if state.has(1) and state.peek(1) in AsciiDigit:
return true
elif state.peek() in AsciiDigit:
return true
else:
return false
return false
proc skipWhitespace(state: var CSSTokenizerState) =
while state.has() and state.peek() in AsciiWhitespace:
discard state.consume()
proc consumeEscape(state: var CSSTokenizerState): string =
if not state.has():
return $Rune(0xFFFD)
let c = state.consume()
if c in AsciiHexDigit:
var num = hexValue(c)
var i = 0
while i <= 5 and state.has():
let c = state.consume()
if hexValue(c) == -1:
state.reconsume()
break
num *= 0x10
num += hexValue(c)
inc i
if state.has() and state.peek() in AsciiWhitespace:
discard state.consume()
if num == 0 or num > 0x10FFFF or num in 0xD800..0xDFFF:
return $Rune(0xFFFD)
else:
return $Rune(num)
else:
return $c #NOTE this assumes the caller doesn't care about non-ascii
proc consumeString(state: var CSSTokenizerState): CSSToken =
var s = ""
let ending = state.curr
while state.has():
let c = state.consume()
case c
of '\n':
state.reconsume()
return CSSToken(tokenType: cttBadString)
of '\\':
if not state.has():
continue
elif state.peek() == '\n':
discard state.consume()
else:
s &= consumeEscape(state)
elif c == ending:
break
else:
s &= c
return CSSToken(tokenType: cttString, value: s)
proc consumeIdentSequence(state: var CSSTokenizerState): string =
var s = ""
while state.has():
let c = state.consume()
if state.isValidEscape():
s &= state.consumeEscape()
elif c in Ident:
s &= c
else:
state.reconsume()
return s
return s
proc consumeNumber(state: var CSSTokenizerState): (tflagb, float64) =
var t = tflagbInteger
var repr = ""
if state.has() and state.peek() in {'+', '-'}:
repr &= state.consume()
while state.has() and state.peek() in AsciiDigit:
repr &= state.consume()
if state.has(1) and state.peek() == '.' and state.peek(1) in AsciiDigit:
repr &= state.consume()
repr &= state.consume()
t = tflagbNumber
while state.has() and state.peek() in AsciiDigit:
repr &= state.consume()
if state.has(1) and state.peek() in {'E', 'e'} and
state.peek(1) in AsciiDigit or
state.has(2) and state.peek() in {'E', 'e'} and
state.peek(1) in {'-', '+'} and state.peek(2) in AsciiDigit:
repr &= state.consume()
if state.peek() in {'-', '+'}:
repr &= state.consume()
repr &= state.consume()
else:
repr &= state.consume()
t = tflagbNumber
while state.has() and state.peek() in AsciiDigit:
repr &= state.consume()
let val = parseFloat64(repr)
return (t, val)
proc consumeNumericToken(state: var CSSTokenizerState): CSSToken =
let (t, val) = state.consumeNumber()
if state.next3startsWithIdentSequence():
result = CSSToken(tokenType: cttDimension, nvalue: val, tflagb: t)
result.unit = state.consumeIdentSequence()
elif state.has() and state.peek() == '%':
discard state.consume()
result = CSSToken(tokenType: cttPercentage, nvalue: val)
else:
result = CSSToken(tokenType: cttNumber, nvalue: val, tflagb: t)
proc consumeBadURL(state: var CSSTokenizerState) =
while state.has():
let c = state.consume()
case c
of ')':
return
elif state.isValidEscape():
discard state.consumeEscape()
else: discard
const NonPrintable = {
char(0x00)..char(0x08), char(0x0B),
char(0x0E)..char(0x1F), char(0x7F)
}
proc consumeURL(state: var CSSTokenizerState): CSSToken =
result = CSSToken(tokenType: cttUrl)
state.skipWhitespace()
while state.has():
let c = state.consume()
case c
of ')':
return result
of '"', '\'', '(', NonPrintable:
state.consumeBadURL()
return CSSToken(tokenType: cttBadUrl)
of AsciiWhitespace:
state.skipWhitespace()
if not state.has():
return result
if state.peek() == ')':
discard state.consume()
return result
state.consumeBadURL()
return CSSToken(tokenType: cttBadUrl)
of '\\':
state.reconsume()
if state.isValidEscape():
result.value &= state.consumeEscape()
else:
state.consumeBadURL()
return CSSToken(tokenType: cttBadUrl)
else:
result.value &= c
proc consumeIdentLikeToken(state: var CSSTokenizerState): CSSToken =
let s = state.consumeIdentSequence()
if s.equalsIgnoreCase("url") and state.has() and state.peek() == '(':
discard state.consume()
while state.has(1) and state.peek() in AsciiWhitespace and
state.peek(1) in AsciiWhitespace:
discard state.consume()
if state.has() and state.peek() in {'"', '\''} or
state.has(1) and state.peek() in {'"', '\''} + AsciiWhitespace and
state.peek(1) in {'"', '\''}:
return CSSToken(tokenType: cttFunction, value: s)
else:
return state.consumeURL()
elif state.has() and state.peek() == '(':
discard state.consume()
return CSSToken(tokenType: cttFunction, value: s)
return CSSToken(tokenType: cttIdent, value: s)
proc consumeComments(state: var CSSTokenizerState) =
if state.has(1) and state.peek() == '/' and state.peek(1) == '*':
discard state.consume()
discard state.consume()
while state.has() and not (state.has(1) and state.peek() == '*' and
state.peek(1) == '/'):
discard state.consume()
if state.has(1):
discard state.consume()
if state.has():
discard state.consume()
proc consumeToken(state: var CSSTokenizerState): CSSToken =
state.consumeComments()
if not state.has():
return
let c = state.consume()
case c
of AsciiWhitespace:
state.skipWhitespace()
return CSSToken(tokenType: cttWhitespace)
of '"', '\'':
return consumeString(state)
of '#':
if state.has() and state.peek() in Ident or state.isValidEscape():
result = CSSToken(tokenType: cttHash)
if state.startsWithIdentSequence():
result.tflaga = tflagaId
result.value = consumeIdentSequence(state)
else:
state.reconsume()
return CSSToken(tokenType: cttDelim, cvalue: state.consumeRChar())
of '(': return CSSToken(tokenType: cttLparen)
of ')': return CSSToken(tokenType: cttRparen)
of '{': return CSSToken(tokenType: cttLbrace)
of '}': return CSSToken(tokenType: cttRbrace)
of '+':
if state.startsWithNumber():
state.reconsume()
return state.consumeNumericToken()
else:
return CSSToken(tokenType: cttDelim, cvalue: c)
of ',': return CSSToken(tokenType: cttComma)
of '-':
if state.startsWithNumber():
state.reconsume()
return state.consumeNumericToken()
else:
if state.has(1) and state.peek() == '-' and state.peek(1) == '>':
discard state.consume()
discard state.consume()
return CSSToken(tokenType: cttCdc)
elif state.startsWithIdentSequence():
state.reconsume()
return state.consumeIdentLikeToken()
else:
return CSSToken(tokenType: cttDelim, cvalue: c)
of '.':
if state.startsWithNumber():
state.reconsume()
return state.consumeNumericToken()
else:
return CSSToken(tokenType: cttDelim, cvalue: c)
of ':': return CSSToken(tokenType: cttColon)
of ';': return CSSToken(tokenType: cttSemicolon)
of '<':
if state.has(2) and state.peek() == '!' and state.peek(1) == '-' and
state.peek(2) == '-':
discard state.consume()
discard state.consume()
discard state.consume()
return CSSToken(tokenType: cttCdo)
else:
return CSSToken(tokenType: cttDelim, cvalue: c)
of '@':
if state.next3startsWithIdentSequence():
let name = state.consumeIdentSequence()
return CSSToken(tokenType: cttAtKeyword, value: name)
else:
return CSSToken(tokenType: cttDelim, cvalue: c)
of '[': return CSSToken(tokenType: cttLbracket)
of '\\':
if state.isValidEscape():
state.reconsume()
return state.consumeIdentLikeToken()
else:
return CSSToken(tokenType: cttDelim, cvalue: c)
of ']': return CSSToken(tokenType: cttRbracket)
of AsciiDigit:
state.reconsume()
return state.consumeNumericToken()
of IdentStart:
state.reconsume()
return state.consumeIdentLikeToken()
else:
state.reconsume()
return CSSToken(tokenType: cttDelim, cvalue: state.consumeRChar())
proc tokenizeCSS*(ibuf: string): seq[CSSParsedItem] =
var state = CSSTokenizerState(buf: ibuf)
while state.has():
let tok = state.consumeToken()
if tok != nil:
result.add(tok)
proc consume(state: var CSSParseState): CSSParsedItem =
result = state.tokens[state.at]
inc state.at
proc reconsume(state: var CSSParseState) =
dec state.at
func has(state: CSSParseState; i: int = 0): bool =
return state.at + i < state.tokens.len
func peek(state: CSSParseState): CSSParsedItem =
return state.tokens[state.at]
proc skipWhitespace(state: var CSSParseState) =
while state.has() and state.peek() == cttWhitespace:
discard state.consume()
proc consumeComponentValue(state: var CSSParseState): CSSComponentValue
proc consumeSimpleBlock(state: var CSSParseState): CSSSimpleBlock =
state.reconsume()
let t = CSSToken(state.consume())
var ending: CSSTokenType
case t.tokenType
of cttLbrace: ending = cttRbrace
of cttLparen: ending = cttRparen
of cttLbracket: ending = cttRbracket
else: doAssert false
result = CSSSimpleBlock(token: t)
while state.at < state.tokens.len:
let t = state.consume()
if t == ending:
return result
else:
if t == cttLbrace or t == cttLbracket or t == cttLparen:
result.value.add(state.consumeSimpleBlock())
else:
state.reconsume()
result.value.add(state.consumeComponentValue())
return result
proc consumeFunction(state: var CSSParseState): CSSFunction =
let t = (CSSToken)state.consume()
result = CSSFunction(name: t.value)
while state.at < state.tokens.len:
let t = state.consume()
if t == cttRparen:
return result
else:
state.reconsume()
result.value.add(state.consumeComponentValue())
proc consumeComponentValue(state: var CSSParseState): CSSComponentValue =
let t = state.consume()
if t == cttLbrace or t == cttLbracket or t == cttLparen:
return state.consumeSimpleBlock()
elif t == cttFunction:
state.reconsume()
return state.consumeFunction()
return CSSComponentValue(t)
proc consumeQualifiedRule(state: var CSSParseState): Option[CSSQualifiedRule] =
var r = CSSQualifiedRule()
while state.has():
let t = state.consume()
if t of CSSSimpleBlock and CSSSimpleBlock(t).token == cttLbrace:
r.oblock = CSSSimpleBlock(t)
return some(r)
elif t == cttLbrace:
r.oblock = state.consumeSimpleBlock()
return some(r)
else:
state.reconsume()
r.prelude.add(state.consumeComponentValue())
return none(CSSQualifiedRule)
proc consumeAtRule(state: var CSSParseState): CSSAtRule =
let t = CSSToken(state.consume())
result = CSSAtRule(name: t.value)
while state.at < state.tokens.len:
let t = state.consume()
if t of CSSSimpleBlock:
result.oblock = CSSSimpleBlock(t)
elif t == cttSemicolon:
return result
elif t == cttLbrace:
result.oblock = state.consumeSimpleBlock()
return result
else:
state.reconsume()
result.prelude.add(state.consumeComponentValue())
proc consumeDeclaration(state: var CSSParseState): Option[CSSDeclaration] =
let t = CSSToken(state.consume())
var decl = CSSDeclaration(name: t.value)
state.skipWhitespace()
if not state.has() or state.peek() != cttColon:
return none(CSSDeclaration)
discard state.consume()
state.skipWhitespace()
while state.has():
decl.value.add(state.consumeComponentValue())
var i = decl.value.len - 1
var j = 2
var k = 0
var l = 0
while i >= 0 and j > 0:
if decl.value[i] != cttWhitespace:
dec j
if decl.value[i] == cttIdent and k == 0:
if CSSToken(decl.value[i]).value.equalsIgnoreCase("important"):
inc k
l = i
elif k == 1 and decl.value[i] == cttDelim:
if CSSToken(decl.value[i]).cvalue == '!':
decl.important = true
decl.value.delete(l)
decl.value.delete(i)
break
dec i
while decl.value.len > 0 and decl.value[^1] == cttWhitespace:
decl.value.setLen(decl.value.len - 1)
return some(decl)
#> Note: Despite the name, this actually parses a mixed list of declarations
#> and at-rules, as CSS 2.1 does for @page. Unexpected at-rules (which could be
#> all of them, in a given context) are invalid and should be ignored by the
#> consumer.
#So we have two versions, one with at rules and one without.
proc consumeDeclarations(state: var CSSParseState): seq[CSSParsedItem] =
while state.has():
let t = state.consume()
if t == cttWhitespace or t == cttSemicolon:
continue
elif t == cttAtKeyword:
state.reconsume()
result.add(state.consumeAtRule())
elif t == cttIdent:
var tempList: seq[CSSParsedItem]
tempList.add(CSSToken(t))
while state.has() and state.peek() != cttSemicolon:
tempList.add(state.consumeComponentValue())
var tempState = CSSParseState(at: 0, tokens: tempList)
let decl = tempState.consumeDeclaration()
if decl.isSome:
result.add(decl.get)
else:
state.reconsume()
if state.peek() != cttSemicolon:
discard state.consumeComponentValue()
proc consumeDeclarations2(state: var CSSParseState): seq[CSSDeclaration] =
while state.has():
let t = state.consume()
if t == cttWhitespace or t == cttSemicolon:
continue
elif t == cttAtKeyword:
state.reconsume()
discard state.consumeAtRule()
elif t == cttIdent:
var tempList: seq[CSSParsedItem]
let tok = CSSToken(t)
tempList.add(tok)
while state.has() and state.peek() != cttSemicolon:
tempList.add(state.consumeComponentValue())
var tempState = CSSParseState(at: 0, tokens: tempList)
let decl = tempState.consumeDeclaration()
if decl.isSome:
result.add(decl.get)
else:
state.reconsume()
if state.peek() != cttSemicolon:
discard state.consumeComponentValue()
proc consumeListOfRules(state: var CSSParseState; topLevel = false):
seq[CSSRule] =
while state.at < state.tokens.len:
let t = state.consume()
if t == cttWhitespace:
continue
elif t == cttCdo or t == cttCdc:
if topLevel:
continue
else:
state.reconsume()
let q = state.consumeQualifiedRule()
if q.isSome:
result.add(q.get)
elif t == cttAtKeyword:
state.reconsume()
result.add(state.consumeAtRule())
else:
state.reconsume()
let q = state.consumeQualifiedRule()
if q.isSome:
result.add(q.get)
proc parseStylesheet(state: var CSSParseState): CSSRawStylesheet =
return CSSRawStylesheet(value: state.consumeListOfRules(true))
proc parseStylesheet*(ibuf: string): CSSRawStylesheet =
var state = CSSParseState()
state.tokens = tokenizeCSS(ibuf)
return state.parseStylesheet()
proc parseListOfRules(state: var CSSParseState): seq[CSSRule] =
return state.consumeListOfRules()
proc parseListOfRules*(cvals: seq[CSSComponentValue]): seq[CSSRule] =
var state = CSSParseState()
state.tokens = cast[seq[CSSParsedItem]](cvals)
return state.parseListOfRules()
proc parseRule(state: var CSSParseState): DOMResult[CSSRule] =
state.skipWhitespace()
if not state.has():
return errDOMException("Unexpected EOF", "SyntaxError")
var res = if state.peek() == cttAtKeyword:
state.consumeAtRule()
else:
let q = state.consumeQualifiedRule()
if q.isNone:
return errDOMException("No qualified rule found!", "SyntaxError")
q.get
state.skipWhitespace()
if state.has():
return errDOMException("EOF not reached", "SyntaxError")
return ok(res)
proc parseRule*(ibuf: string): DOMResult[CSSRule] =
var state = CSSParseState()
state.tokens = tokenizeCSS(ibuf)
return state.parseRule()
proc parseDeclaration(state: var CSSParseState): DOMResult[CSSDeclaration] =
state.skipWhitespace()
if not state.has() or state.peek() != cttIdent:
return errDOMException("No ident token found", "SyntaxError")
let d = state.consumeDeclaration()
if d.isSome:
return ok(d.get)
return errDOMException("No declaration found", "SyntaxError")
proc parseDeclaration*(ibuf: string): DOMResult[CSSDeclaration] =
var state = CSSParseState()
state.tokens = tokenizeCSS(ibuf)
return state.parseDeclaration()
proc parseDeclarations(state: var CSSParseState): seq[CSSParsedItem] =
return state.consumeDeclarations()
proc parseDeclarations*(cvals: seq[CSSComponentValue]):
seq[CSSParsedItem] =
var state = CSSParseState(tokens: cast[seq[CSSParsedItem]](cvals))
return state.consumeDeclarations()
proc parseDeclarations*(ibuf: string): seq[CSSParsedItem] =
var state = CSSParseState()
state.tokens = tokenizeCSS(ibuf)
return state.parseDeclarations()
proc parseDeclarations2(state: var CSSParseState): seq[CSSDeclaration] =
return state.consumeDeclarations2()
proc parseDeclarations2*(cvals: seq[CSSComponentValue]):
seq[CSSDeclaration] =
var state = CSSParseState(tokens: cast[seq[CSSParsedItem]](cvals))
return state.consumeDeclarations2()
proc parseDeclarations2*(ibuf: string): seq[CSSDeclaration] =
var state = CSSParseState(tokens: tokenizeCSS(ibuf))
return state.parseDeclarations2()
proc parseComponentValue(state: var CSSParseState):
DOMResult[CSSComponentValue] =
state.skipWhitespace()
if not state.has():
return errDOMException("Unexpected EOF", "SyntaxError")
let res = state.consumeComponentValue()
state.skipWhitespace()
if state.has():
return errDOMException("EOF not reached", "SyntaxError")
return ok(res)
proc parseComponentValue*(ibuf: string): DOMResult[CSSComponentValue] =
var state = CSSParseState(tokens: tokenizeCSS(ibuf))
return state.parseComponentValue()
proc parseComponentValues(state: var CSSParseState): seq[CSSComponentValue] =
result = @[]
while state.has():
result.add(state.consumeComponentValue())
proc parseComponentValues*(ibuf: string): seq[CSSComponentValue] =
var state = CSSParseState(tokens: tokenizeCSS(ibuf))
return state.parseComponentValues()
proc parseCommaSepComponentValues(state: var CSSParseState):
seq[seq[CSSComponentValue]] =
result = @[]
if state.has():
result.add(newSeq[CSSComponentValue]())
while state.has():
let cvl = state.consumeComponentValue()
if cvl != cttComma:
result[^1].add(cvl)
else:
result.add(newSeq[CSSComponentValue]())
proc parseCommaSepComponentValues*(cvals: seq[CSSComponentValue]):
seq[seq[CSSComponentValue]] =
var state = CSSParseState(tokens: cast[seq[CSSParsedItem]](cvals))
return state.parseCommaSepComponentValues()
proc parseAnB*(state: var CSSParseState): Option[CSSAnB] =
template is_eof: bool =
not state.has() or not (state.peek() of CSSToken)
template fail_eof =
if is_eof:
return none(CSSAnB)
template get_plus: bool =
let tok = state.peek()
if tok == cttDelim and CSSToken(tok).cvalue == '+':
discard state.consume()
true
else:
false
template get_tok: CSSToken =
state.skipWhitespace()
fail_eof
CSSToken(state.consume())
template get_tok_nows: CSSToken =
fail_eof
CSSToken(state.consume())
template fail_plus =
if is_plus:
return none(CSSAnB)
template parse_sub_int(sub: string; skip: int): int =
let s = sub.substr(skip)
let x = parseInt32(s)
if x.isNone:
return none(CSSAnB)
x.get
template fail_non_integer(tok: CSSToken; res: Option[CSSAnB]) =
if tok.tokenType != cttNumber:
state.reconsume()
return res
if tok.tflagb != tflagbInteger:
state.reconsume()
return res
if int64(tok.nvalue) > high(int):
state.reconsume()
return res
template fail_non_signless_integer(tok: CSSToken; res: Option[CSSAnB]) =
fail_non_integer tok, res #TODO check if signless?
fail_eof
state.skipWhitespace()
fail_eof
let is_plus = get_plus
let tok = get_tok_nows
case tok.tokenType
of cttIdent:
case tok.value
of "odd":
fail_plus
return some((2, 1))
of "even":
fail_plus
return some((2, 0))
of "n", "N":
state.skipWhitespace()
if is_eof:
return some((1, 0))
let tok2 = get_tok_nows
if tok2.tokenType == cttDelim:
let sign = case tok2.cvalue
of '+': 1
of '-': -1
else: return none(CSSAnB)
let tok3 = get_tok
fail_non_signless_integer tok3, some((1, 0))
return some((1, sign * int(tok3.nvalue)))
else:
fail_non_integer tok2, some((1, 0))
return some((1, int(tok2.nvalue)))
of "-n", "-N":
fail_plus
state.skipWhitespace()
if is_eof:
return some((-1, 0))
let tok2 = get_tok_nows
if tok2.tokenType == cttDelim:
let sign = case tok2.cvalue
of '+': 1
of '-': -1
else: return none(CSSAnB)
let tok3 = get_tok
fail_non_signless_integer tok3, some((-1, 0))
return some((-1, sign * int(tok3.nvalue)))
else:
fail_non_integer tok2, some((-1, 0))
return some((-1, int(tok2.nvalue)))
of "n-", "N-":
let tok2 = get_tok
fail_non_signless_integer tok2, none(CSSAnB)
return some((1, -int(tok2.nvalue)))
of "-n-", "-N-":
fail_plus
let tok2 = get_tok
fail_non_signless_integer tok2, none(CSSAnB)
return some((-1, -int(tok2.nvalue)))
elif tok.value.startsWithIgnoreCase("n-"):
return some((1, -parse_sub_int(tok.value, "n-".len)))
elif tok.value.startsWithIgnoreCase("-n-"):
fail_plus
return some((-1, -parse_sub_int(tok.value, "n-".len)))
else:
return none(CSSAnB)
of cttNumber:
fail_plus
if tok.tflagb != tflagbInteger:
return none(CSSAnB)
if int64(tok.nvalue) > high(int):
return none(CSSAnB)
# <integer>
return some((0, int(tok.nvalue)))
of cttDimension:
fail_plus
if int64(tok.nvalue) > high(int):
return none(CSSAnB)
if tok.tflagb != tflagbInteger:
return none(CSSAnB)
case tok.unit
of "n", "N":
# <n-dimension>
state.skipWhitespace()
if is_eof:
return some((int(tok.nvalue), 0))
let tok2 = get_tok_nows
if tok2.tokenType == cttDelim:
let sign = case tok2.cvalue
of '+': 1
of '-': -1
else: return none(CSSAnB)
let tok3 = get_tok
fail_non_signless_integer tok3, some((int(tok.nvalue), 0))
return some((int(tok.nvalue), sign * int(tok3.nvalue)))
else:
fail_non_integer tok2, some((int(tok.nvalue), 0))
return some((int(tok.nvalue), int(tok2.nvalue)))
of "n-", "N-":
# <ndash-dimension>
let tok2 = get_tok
fail_non_signless_integer tok2, none(CSSAnB)
return some((int(tok.nvalue), -int(tok2.nvalue)))
elif tok.unit.startsWithIgnoreCase("n-"):
# <ndashdigit-dimension>
return some((int(tok.nvalue), -parse_sub_int(tok.unit, "n-".len)))
else:
return none(CSSAnB)
else:
return none(CSSAnB)
proc parseAnB*(cvals: seq[CSSComponentValue]): (Option[CSSAnB], int) =
var state = CSSParseState(tokens: cast[seq[CSSParsedItem]](cvals))
let anb = state.parseAnB()
return (anb, state.at)