import options import streams import sugar import unicode import utils/twtstr type CSSTokenType* = enum CSS_NO_TOKEN, CSS_IDENT_TOKEN, CSS_FUNCTION_TOKEN, CSS_AT_KEYWORD_TOKEN, CSS_HASH_TOKEN, CSS_STRING_TOKEN, CSS_BAD_STRING_TOKEN, CSS_URL_TOKEN, CSS_BAD_URL_TOKEN, CSS_DELIM_TOKEN, CSS_NUMBER_TOKEN, CSS_PERCENTAGE_TOKEN, CSS_DIMENSION_TOKEN, CSS_WHITESPACE_TOKEN, CSS_CDO_TOKEN, CSS_CDC_TOKEN, CSS_COLON_TOKEN, CSS_SEMICOLON_TOKEN, CSS_COMMA_TOKEN, CSS_RBRACKET_TOKEN, CSS_LBRACKET_TOKEN, CSS_LPAREN_TOKEN, CSS_RPAREN_TOKEN, CSS_LBRACE_TOKEN, CSS_RBRACE_TOKEN CSSTokenizerState = object at: int stream: Stream buf: seq[Rune] CSSParseState = object tokens: seq[CSSParsedItem] at: int top_level: bool tflaga* = enum TFLAGA_UNRESTRICTED, TFLAGA_ID tflagb* = enum TFLAGB_INTEGER, TFLAGB_NUMBER CSSParsedItem* = ref object of RootObj CSSComponentValue* = ref object of CSSParsedItem CSSToken* = ref object of CSSComponentValue case tokenType*: CSSTokenType of CSS_IDENT_TOKEN, CSS_FUNCTION_TOKEN, CSS_AT_KEYWORD_TOKEN, CSS_HASH_TOKEN, CSS_STRING_TOKEN, CSS_URL_TOKEN: value*: seq[Rune] tflaga*: tflaga of CSS_DELIM_TOKEN: rvalue*: Rune of CSS_NUMBER_TOKEN, CSS_PERCENTAGE_TOKEN, CSS_DIMENSION_TOKEN: nvalue*: float64 tflagb*: tflagb unit*: seq[Rune] else: discard CSSRule* = ref object of CSSParsedItem prelude*: seq[CSSComponentValue] oblock*: CSSSimpleBlock CSSAtRule* = ref object of CSSRule name*: seq[Rune] CSSQualifiedRule* = ref object of CSSRule CSSDeclaration* = ref object of CSSComponentValue name*: seq[Rune] value*: seq[CSSComponentValue] important*: bool CSSFunction* = ref object of CSSComponentValue name*: seq[Rune] value*: seq[CSSComponentValue] CSSSimpleBlock* = ref object of CSSComponentValue token*: CSSToken value*: seq[CSSComponentValue] CSSRawStylesheet* = object value*: seq[CSSRule] SyntaxError = object of ValueError # For debugging template `$`*(c: CSSParsedItem): string = if c of CSSToken: case CSSToken(c).tokenType: of CSS_FUNCTION_TOKEN, CSS_AT_KEYWORD_TOKEN, CSS_URL_TOKEN: result &= $CSSToken(c).tokenType & $CSSToken(c).value & '\n' of CSS_HASH_TOKEN: result &= '#' & $CSSToken(c).value of CSS_IDENT_TOKEN: result &= $CSSToken(c).value of CSS_STRING_TOKEN: result &= ("\"" & $CSSToken(c).value & "\"") of CSS_DELIM_TOKEN: result &= $CSSToken(c).rvalue of CSS_DIMENSION_TOKEN: result &= $CSSToken(c).tokenType & $CSSToken(c).nvalue & "unit" & $CSSToken(c).unit & $CSSToken(c).tflagb of CSS_NUMBER_TOKEN: result &= $CSSToken(c).nvalue & $CSSToken(c).unit of CSS_PERCENTAGE_TOKEN: result &= $CSSToken(c).nvalue & "%" of CSS_COLON_TOKEN: result &= ":" of CSS_WHITESPACE_TOKEN: result &= " " of CSS_SEMICOLON_TOKEN: result &= ";\n" of CSS_COMMA_TOKEN: result &= "," else: result &= $CSSToken(c).tokenType & '\n' elif c of CSSDeclaration: result &= $CSSDeclaration(c).name result &= ": " for s in CSSDeclaration(c).value: result &= $s 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 CSS_LBRACE_TOKEN: result &= "{\n" of CSS_LPAREN_TOKEN: result &= "(" of CSS_LBRACKET_TOKEN: result &= "[" else: discard for s in CSSSimpleBlock(c).value: result &= $s case CSSSimpleBlock(c).token.tokenType of CSS_LBRACE_TOKEN: result &= "\n}" of CSS_LPAREN_TOKEN: result &= ")" of CSS_LBRACKET_TOKEN: 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 func isNameStartCodePoint(r: Rune): bool = return not isAscii(r) or r == Rune('_') or isAlphaAscii(r) func isNameCodePoint(r: Rune): bool = return isNameStartCodePoint(r) or isDigitAscii(r) or r == Rune('-') proc consume(state: var CSSTokenizerState): Rune = result = state.buf[state.at] inc state.at proc reconsume(state: var CSSTokenizerState) = dec state.at func peek(state: CSSTokenizerState, i: int): Rune = return state.buf[state.at + i] proc has(state: var CSSTokenizerState, i: int = 0): bool = if state.at + i >= state.buf.len and not state.stream.atEnd(): state.buf &= state.stream.readLine().toRunes() & Rune('\n') return state.at + i < state.buf.len func curr(state: CSSTokenizerState): Rune = return state.buf[state.at] proc isValidEscape(state: var CSSTokenizerState): bool = return state.has(1) and state.curr() == Rune('\\') and state.peek(1) != Rune('\n') proc startsWithIdentifier(state: var CSSTokenizerState): bool = if not state.has(): return false if isNameStartCodePoint(state.curr()): return true if state.curr() == Rune('-'): if state.has(1) and state.peek(1).isNameStartCodePoint(): return true if state.isValidEscape(): return true return false elif state.curr() == Rune('\\'): return state.isValidEscape() return false proc startsWithNumber(state: var CSSTokenizerState): bool = if state.has(): case state.curr() of Rune('+'), Rune('-'): if state.has(1): if isDigitAscii(state.peek(1)): return true elif state.peek(1) == Rune('.'): if state.has(2) and isDigitAscii(state.peek(2)): return true of Rune('.'): if isDigitAscii(state.peek(1)): return true elif isDigitAscii(state.curr()): return true else: return false return false proc consumeEscape(state: var CSSTokenizerState): Rune = let r = state.consume() var num = hexValue(r) if num != -1: var i = 0 while state.has() and i <= 5: let r = state.consume() if hexValue(r) == -1: state.reconsume() break num *= 0x10 num += hexValue(r) inc i if num == 0 or num > 0x10FFFF or num in {0xD800..0xDFFF}: return Rune(0xFFFD) else: return Rune(num) else: return r proc consumeString(state: var CSSTokenizerState): CSSToken = var s: seq[Rune] state.reconsume() let ending = state.consume() while state.has(): let r = state.consume() case r of Rune('\n'): return CSSToken(tokenType: CSS_BAD_STRING_TOKEN) of Rune('\\'): s &= consumeEscape(state) elif r == ending: break else: s &= r return CSSToken(tokenType: CSS_STRING_TOKEN, value: s) proc consumeName(state: var CSSTokenizerState): seq[Rune] = while state.has(): let r = state.consume() if state.isValidEscape(): result &= state.consumeEscape() elif isNameCodePoint(r): result &= r else: state.reconsume() return result proc consumeNumberSign(state: var CSSTokenizerState): CSSToken = if state.has(): let r = state.consume() if isNameCodePoint(r) or state.isValidEscape(): result = CSSToken(tokenType: CSS_HASH_TOKEN) if state.startsWithIdentifier(): result.tflaga = TFLAGA_ID state.reconsume() result.value = consumeName(state) else: let r = state.consume() result = CSSToken(tokenType: CSS_DELIM_TOKEN, rvalue: r) proc consumeNumber(state: var CSSTokenizerState): tuple[t: tflagb, val: float64] = var t = TFLAGB_INTEGER var repr: seq[Rune] if state.has(): if state.curr() == Rune('+') or state.curr() == Rune('-'): repr &= state.consume() while state.has() and isDigitAscii(state.curr()): repr &= state.consume() if state.has(1): if state.curr() == Rune('.') and isDigitAscii(state.peek(1)): repr &= state.consume() repr &= state.consume() t = TFLAGB_NUMBER while state.has() and isDigitAscii(state.curr()): repr &= state.consume() if state.has(1): if state.curr() == Rune('E') or state.curr() == Rune('e'): var j = 2 if state.peek(1) == Rune('-') or state.peek(1) == Rune('+'): inc j if state.has(j) and isDigitAscii(state.peek(j)): while j > 0: repr &= state.consume() dec j while state.has() and isDigitAscii(state.curr()): repr &= state.consume() let val = parseFloat64($repr) return (t, val) proc consumeNumericToken(state: var CSSTokenizerState): CSSToken = let num = state.consumeNumber() if state.startsWithIdentifier(): result = CSSToken(tokenType: CSS_DIMENSION_TOKEN, nvalue: num.val, tflagb: num.t) result.unit = state.consumeName() elif state.has() and state.curr() == Rune('%'): discard state.consume() result = CSSToken(tokenType: CSS_PERCENTAGE_TOKEN, nvalue: num.val) else: result = CSSToken(tokenType: CSS_NUMBER_TOKEN, nvalue: num.val, tflagb: num.t) proc consumeBadURL(state: var CSSTokenizerState) = while state.has(1): let r = state.consume() case r of Rune(')'): return elif state.isValidEscape(): discard state.consumeEscape() else: discard proc consumeURL(state: var CSSTokenizerState): CSSToken = result = CSSToken(tokenType: CSS_URL_TOKEN) while state.has(1) and state.peek(1).isWhitespace(): discard state.consume() while state.has(1): let r = state.consume() case r of Rune(')'): return result of Rune('"'), Rune('\''), Rune('('): state.consumeBadURL() return CSSToken(tokenType: CSS_BAD_URL_TOKEN) of Rune('\\'): state.reconsume() if state.isValidEscape(): result.value &= state.consumeEscape() else: state.consumeBadURL() return CSSToken(tokenType: CSS_BAD_URL_TOKEN) elif r.isWhitespace(): while state.has(1) and state.peek(1).isWhitespace(): discard state.consume() else: result.value &= r proc consumeIdentLikeToken(state: var CSSTokenizerState): CSSToken = let s = state.consumeName() if s.equalsIgnoreCase("url") and state.has() and state.curr() == Rune('('): discard state.consume() while state.has(1) and state.curr().isWhitespace() and state.peek(1).isWhitespace(): discard state.consume() if state.curr() == Rune('\'') or state.curr() == Rune('"') or state.curr().isWhitespace(): return CSSToken(tokenType: CSS_FUNCTION_TOKEN, value: s) else: return state.consumeURL() elif state.has() and state.curr() == Rune('('): discard state.consume() return CSSToken(tokenType: CSS_FUNCTION_TOKEN, value: s) return CSSToken(tokenType: CSS_IDENT_TOKEN, value: s) proc consumeComments(state: var CSSTokenizerState) = if state.has(1) and state.curr() == Rune('/') and state.peek(1) == Rune('*'): discard state.consume() discard state.consume() while state.has(1) and not (state.curr() == Rune('*') and state.peek(1) == Rune('/')): 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 r = state.consume() case r of Rune('\n'), Rune('\t'), Rune(' '), Rune('\f'), Rune('\r'): while state.has() and state.curr().isWhitespace(): discard state.consume() return CSSToken(tokenType: CSS_WHITESPACE_TOKEN) of Rune('"'), Rune('\''): return consumeString(state) of Rune('#'): return consumeNumberSign(state) of Rune('('): return CSSToken(tokenType: CSS_LPAREN_TOKEN) of Rune(')'): return CSSToken(tokenType: CSS_RPAREN_TOKEN) of Rune('['): return CSSToken(tokenType: CSS_LBRACKET_TOKEN) of Rune(']'): return CSSToken(tokenType: CSS_RBRACKET_TOKEN) of Rune('{'): return CSSToken(tokenType: CSS_LBRACE_TOKEN) of Rune('}'): return CSSToken(tokenType: CSS_RBRACE_TOKEN) of Rune(','): return CSSToken(tokenType: CSS_COMMA_TOKEN) of Rune(':'): return CSSToken(tokenType: CSS_COLON_TOKEN) of Rune(';'): return CSSToken(tokenType: CSS_SEMICOLON_TOKEN) of Rune('+'): if state.startsWithNumber(): state.reconsume() return state.consumeNumericToken() else: return CSSToken(tokenType: CSS_DELIM_TOKEN, rvalue: r) of Rune('-'): if state.startsWithNumber(): state.reconsume() return state.consumeNumericToken() else: if state.has(2) and state.peek(1) == Rune('-') and state.peek(2) == Rune('>'): discard state.consume() discard state.consume() return CSSToken(tokenType: CSS_CDC_TOKEN) elif state.startsWithIdentifier(): state.reconsume() result = state.consumeIdentLikeToken() else: return CSSToken(tokenType: CSS_DELIM_TOKEN, rvalue: r) of Rune('.'): if state.startsWithNumber(): state.reconsume() return state.consumeNumericToken() else: return CSSToken(tokenType: CSS_DELIM_TOKEN, rvalue: r) of Rune('<'): if state.has(3) and state.peek(1) == Rune('!') and state.peek(2) == Rune('-') and state.peek(3) == Rune('-'): discard state.consume() discard state.consume() discard state.consume() return CSSToken(tokenType: CSS_CDO_TOKEN) else: return CSSToken(tokenType: CSS_DELIM_TOKEN, rvalue: r) of Rune('@'): if state.startsWithIdentifier(): let name = state.consumeName() return CSSToken(tokenType: CSS_AT_KEYWORD_TOKEN, value: name) else: return CSSToken(tokenType: CSS_DELIM_TOKEN, rvalue: r) elif isDigitAscii(r): state.reconsume() return state.consumeNumericToken() elif isNameStartCodePoint(r): state.reconsume() return state.consumeIdentLikeToken() else: return CSSToken(tokenType: CSS_DELIM_TOKEN, rvalue: r) proc tokenizeCSS*(inputStream: Stream): seq[CSSParsedItem] = var state: CSSTokenizerState state.stream = inputStream state.buf = state.stream.readLine().toRunes() while state.has(): let tok = state.consumeToken() if tok != nil: result.add(tok) inputStream.close() 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): bool = return state.at + i < state.tokens.len func curr(state: CSSParseState): CSSParsedItem = return state.tokens[state.at] func has(state: CSSParseState): bool = return state.at < state.tokens.len 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 CSS_LBRACE_TOKEN: ending = CSS_RBRACE_TOKEN of CSS_LPAREN_TOKEN: ending = CSS_RPAREN_TOKEN of CSS_LBRACKET_TOKEN: ending = CSS_RBRACKET_TOKEN else: raise newException(Exception, "Parse error!") result = CSSSimpleBlock(token: t) while state.at < state.tokens.len: let t = state.consume() if t == ending: return result else: if t == CSS_LBRACE_TOKEN or t == CSS_LBRACKET_TOKEN or t == CSS_LPAREN_TOKEN: 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 == CSS_RPAREN_TOKEN: return result else: state.reconsume() result.value.add(state.consumeComponentValue()) proc consumeComponentValue(state: var CSSParseState): CSSComponentValue = let t = state.consume() if t == CSS_LBRACE_TOKEN or t == CSS_LBRACKET_TOKEN or t == CSS_LPAREN_TOKEN: return state.consumeSimpleBlock() elif t == CSS_FUNCTION_TOKEN: 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: r.oblock = CSSSimpleBlock(t) return some(r) elif t == CSS_LBRACE_TOKEN: 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 == CSS_SEMICOLON_TOKEN: return result elif t == CSS_LBRACE_TOKEN: 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) while state.has() and state.curr() == CSS_WHITESPACE_TOKEN: discard state.consume() if not state.has() or state.curr() != CSS_COLON_TOKEN: return none(CSSDeclaration) discard state.consume() while state.has() and state.curr() == CSS_WHITESPACE_TOKEN: discard state.consume() 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] != CSS_WHITESPACE_TOKEN: dec j if decl.value[i] == CSS_IDENT_TOKEN and k == 0: if CSSToken(decl.value[i]).value.equalsIgnoreCase("important"): inc k l = i elif k == 1 and decl.value[i] == CSS_DELIM_TOKEN: if CSSToken(decl.value[i]).rvalue == Rune('!'): decl.important = true decl.value.del(l) decl.value.del(i) break dec i while decl.value.len > 0 and decl.value[^1] == CSS_WHITESPACE_TOKEN: decl.value.del(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 consumeListOfDeclarations(state: var CSSParseState): seq[CSSParsedItem] = while state.has(): let t = state.consume() if t == CSS_wHITESPACE_TOKEN or t == CSS_SEMICOLON_TOKEN: continue elif t == CSS_AT_KEYWORD_TOKEN: state.reconsume() result.add(state.consumeAtRule()) elif t == CSS_IDENT_TOKEN: var tempList: seq[CSSParsedItem] tempList.add(CSSToken(t)) while state.has() and state.curr() != CSS_SEMICOLON_TOKEN: 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.curr() != CSS_SEMICOLON_TOKEN: discard state.consumeComponentValue() proc consumeListOfDeclarations2(state: var CSSParseState): seq[CSSDeclaration] = while state.has(): let t = state.consume() if t == CSS_wHITESPACE_TOKEN or t == CSS_SEMICOLON_TOKEN: continue elif t == CSS_AT_KEYWORD_TOKEN: state.reconsume() discard state.consumeAtRule() elif t == CSS_IDENT_TOKEN: var tempList: seq[CSSParsedItem] tempList.add(CSSToken(t)) while state.has() and state.curr() != CSS_SEMICOLON_TOKEN: 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.curr() != CSS_SEMICOLON_TOKEN: discard state.consumeComponentValue() proc consumeListOfRules(state: var CSSParseState): seq[CSSRule] = while state.at < state.tokens.len: let t = state.consume() if t == CSS_WHITESPACE_TOKEN: continue elif t == CSS_CDO_TOKEN or t == CSS_CDC_TOKEN: if state.top_level: continue else: state.reconsume() let q = state.consumeQualifiedRule() if q.isSome: result.add(q.get) elif t == CSS_AT_KEYWORD_TOKEN: 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 = state.top_level = true result.value.add(state.consumeListOfRules()) proc parseStylesheet(inputStream: Stream): CSSRawStylesheet = var state = CSSParseState() state.tokens = tokenizeCSS(inputStream) inputStream.close() 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 = collect(newSeq): for cval in cvals: CSSParsedItem(cval) return state.parseListOfRules() proc parseRule(state: var CSSParseState): CSSRule = while state.has() and state.curr() == CSS_WHITESPACE_TOKEN: discard state.consume() if not state.has(): raise newException(SyntaxError, "EOF reached!") if state.curr() == CSS_AT_KEYWORD_TOKEN: result = state.consumeAtRule() else: let q = state.consumeQualifiedRule() if q.isSome: result = q.get else: raise newException(SyntaxError, "No qualified rule found!") while state.has() and state.curr() == CSS_WHITESPACE_TOKEN: discard state.consume() if state.has(): raise newException(SyntaxError, "EOF not reached!") proc parseRule(inputStream: Stream): CSSRule = var state = CSSParseState() state.tokens = tokenizeCSS(inputStream) return state.parseRule() proc parseDeclaration(state: var CSSParseState): CSSDeclaration = while state.has() and state.curr() == CSS_WHITESPACE_TOKEN: discard state.consume() if not state.has() or state.curr() != CSS_IDENT_TOKEN: raise newException(SyntaxError, "No ident token found!") let d = state.consumeDeclaration() if d.isSome: return d.get raise newException(SyntaxError, "No declaration found!") proc parseDeclaration*(inputStream: Stream): CSSDeclaration = var state = CSSParseState() state.tokens = tokenizeCSS(inputStream) return state.parseDeclaration() proc parseListOfDeclarations(state: var CSSParseState): seq[CSSParsedItem] = return state.consumeListOfDeclarations() proc parseListOfDeclarations*(cvals: seq[CSSComponentValue]): seq[CSSParsedItem] = var state: CSSParseState state.tokens = collect(newSeq): for cval in cvals: CSSParsedItem(cval) return state.consumeListOfDeclarations() proc parseListOfDeclarations*(inputStream: Stream): seq[CSSParsedItem] = var state: CSSParseState state.tokens = tokenizeCSS(inputStream) return state.parseListOfDeclarations() proc parseListOfDeclarations2(state: var CSSParseState): seq[CSSDeclaration] = return state.consumeListOfDeclarations2() proc parseListOfDeclarations2*(cvals: seq[CSSComponentValue]): seq[CSSDeclaration] = var state: CSSParseState state.tokens = collect(newSeq): for cval in cvals: CSSParsedItem(cval) return state.consumeListOfDeclarations2() proc parseListOfDeclarations2*(inputStream: Stream): seq[CSSDeclaration] = var state: CSSParseState state.tokens = tokenizeCSS(inputStream) return state.parseListOfDeclarations2() proc parseComponentValue(state: var CSSParseState): CSSComponentValue = while state.has() and state.curr() == CSS_WHITESPACE_TOKEN: discard state.consume() if not state.has(): raise newException(SyntaxError, "EOF reached!") result = state.consumeComponentValue() while state.has() and state.curr() == CSS_WHITESPACE_TOKEN: discard state.consume() if state.has(): raise newException(SyntaxError, "EOF not reached!") proc parseComponentValue*(inputStream: Stream): CSSComponentValue = var state: CSSParseState state.tokens = tokenizeCSS(inputStream) return state.parseComponentValue() proc parseListOfComponentValues(state: var CSSParseState): seq[CSSComponentValue] = while state.has(): result.add(state.consumeComponentValue()) proc parseListOfComponentValues*(inputStream: Stream): seq[CSSComponentValue] = var state = CSSParseState() state.tokens = tokenizeCSS(inputStream) return state.parseListOfComponentValues() proc parseCommaSeparatedListOfComponentValues(state: var CSSParseState): seq[seq[CSSComponentValue]] = if state.has(): result.add(newSeq[CSSComponentValue]()) while state.has(): let cvl = state.consumeComponentValue() if cvl != CSS_COMMA_TOKEN: result[^1].add(cvl) else: result.add(newSeq[CSSComponentValue]()) proc parseCommaSeparatedListOfComponentValues*(cvals: seq[CSSComponentValue]): seq[seq[CSSComponentValue]] = var state: CSSParseState state.tokens = collect(newSeq): for cval in cvals: CSSParsedItem(cval) return state.parseCommaSeparatedListOfComponentValues() proc parseCommaSeparatedListOfComponentValues(inputStream: Stream): seq[seq[CSSComponentValue]] = var state = CSSParseState() state.tokens = tokenizeCSS(inputStream) return state.parseCommaSeparatedListOfComponentValues() proc parseCSS*(inputStream: Stream): CSSRawStylesheet = if inputStream.atEnd(): return CSSRawStylesheet() return inputstream.parseStylesheet()