From f0241b2fec3e41744aa0c900fc4e6d3c46fe4757 Mon Sep 17 00:00:00 2001 From: bptato Date: Tue, 10 May 2022 22:57:39 +0200 Subject: Rename conflicting source files Nim can't really differentiate between them, unfortunately. --- src/css/cascade.nim | 4 +- src/css/cssparser.nim | 817 +++++++++++++++++++++++++++++++++++++++++++++ src/css/mediaquery.nim | 2 +- src/css/parser.nim | 817 --------------------------------------------- src/css/select.nim | 4 +- src/css/selectorparser.nim | 340 +++++++++++++++++++ src/css/selparser.nim | 340 ------------------- src/css/sheet.nim | 4 +- src/css/values.nim | 6 +- 9 files changed, 1167 insertions(+), 1167 deletions(-) create mode 100644 src/css/cssparser.nim delete mode 100644 src/css/parser.nim create mode 100644 src/css/selectorparser.nim delete mode 100644 src/css/selparser.nim (limited to 'src/css') diff --git a/src/css/cascade.nim b/src/css/cascade.nim index 761280f5..abf8ba73 100644 --- a/src/css/cascade.nim +++ b/src/css/cascade.nim @@ -3,9 +3,9 @@ import streams import sugar import css/mediaquery -import css/parser +import css/cssparser import css/select -import css/selparser +import css/selectorparser import css/sheet import css/values import html/dom diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim new file mode 100644 index 00000000..1db94958 --- /dev/null +++ b/src/css/cssparser.nim @@ -0,0 +1,817 @@ +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() diff --git a/src/css/mediaquery.nim b/src/css/mediaquery.nim index 4e8c1e9f..b589a355 100644 --- a/src/css/mediaquery.nim +++ b/src/css/mediaquery.nim @@ -1,7 +1,7 @@ import tables import unicode -import css/parser +import css/cssparser type MediaQueryParser = object diff --git a/src/css/parser.nim b/src/css/parser.nim deleted file mode 100644 index 1db94958..00000000 --- a/src/css/parser.nim +++ /dev/null @@ -1,817 +0,0 @@ -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() diff --git a/src/css/select.nim b/src/css/select.nim index 015dbcc3..10f87113 100644 --- a/src/css/select.nim +++ b/src/css/select.nim @@ -5,8 +5,8 @@ import sequtils import sugar import streams -import css/selparser -import css/parser +import css/selectorparser +import css/cssparser import html/dom func attrSelectorMatches(elem: Element, sel: Selector): bool = diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim new file mode 100644 index 00000000..d44d7168 --- /dev/null +++ b/src/css/selectorparser.nim @@ -0,0 +1,340 @@ +import unicode + +import css/cssparser +import html/tags + +type + SelectorType* = enum + TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, CLASS_SELECTOR, + UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR, FUNC_SELECTOR, + COMBINATOR_SELECTOR + + QueryMode = enum + QUERY_TYPE, QUERY_CLASS, QUERY_ATTR, QUERY_DELIM, QUERY_VALUE, + QUERY_PSEUDO, QUERY_PSELEM, QUERY_DESC_COMBINATOR, QUERY_CHILD_COMBINATOR, + QUERY_NEXT_SIBLING_COMBINATOR, QUERY_SUBSEQ_SIBLING_COMBINATOR + + PseudoElem* = enum + PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER + + PseudoClass* = enum + PSEUDO_FIRST_CHILD, PSEUDO_LAST_CHILD, PSEUDO_ONLY_CHILD, PSEUDO_HOVER, + PSEUDO_ROOT, PSEUDO_NTH_CHILD + + CombinatorType* = enum + DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR, + SUBSEQ_SIBLING_COMBINATOR + + SelectorParser = object + selectors: seq[SelectorList] + query: QueryMode + combinator: Selector + + #TODO combinators + Selector* = ref object of RootObj + case t*: SelectorType + of TYPE_SELECTOR: + tag*: TagType + of ID_SELECTOR: + id*: string + of ATTR_SELECTOR: + attr*: string + value*: string + rel*: char + of CLASS_SELECTOR: + class*: string + of UNIVERSAL_SELECTOR: #TODO namespaces? + discard + of PSEUDO_SELECTOR: + pseudo*: PseudoClass + pseudonum*: float64 + of PSELEM_SELECTOR: + elem*: PseudoElem + of FUNC_SELECTOR: + name*: string + fsels*: seq[SelectorList] + of COMBINATOR_SELECTOR: + ct*: CombinatorType + csels*: seq[SelectorList] + + SelectorList* = ref object + sels*: seq[Selector] + pseudo*: PseudoElem + +proc add(sellist: SelectorList, sel: Selector) = sellist.sels.add(sel) +proc add(sellist: SelectorList, sels: SelectorList) = sellist.sels.add(sels.sels) +proc `[]`*(sellist: SelectorList, i: int): Selector = sellist.sels[i] +proc len*(sellist: SelectorList): int = sellist.sels.len + +func getSpecificity*(sels: SelectorList): int + +func getSpecificity(sel: Selector): int = + case sel.t + of ID_SELECTOR: + result += 1000000 + of CLASS_SELECTOR, ATTR_SELECTOR, PSEUDO_SELECTOR: + result += 1000 + of TYPE_SELECTOR, PSELEM_SELECTOR: + result += 1 + of FUNC_SELECTOR: + case sel.name + of "is": + var best = 0 + for child in sel.fsels: + let s = getSpecificity(child) + if s > best: + best = s + result += best + of "not": + for child in sel.fsels: + result += getSpecificity(child) + else: discard + of UNIVERSAL_SELECTOR: + discard + of COMBINATOR_SELECTOR: + for child in sel.csels: + result += getSpecificity(child) + +func getSpecificity*(sels: SelectorList): int = + for sel in sels.sels: + result += getSpecificity(sel) + +func optimizeSelectorList*(selectors: SelectorList): SelectorList = + new(result) + #pass 1: check for invalid sequences + var i = 1 + while i < selectors.len: + let sel = selectors[i] + if sel.t == TYPE_SELECTOR or sel.t == UNIVERSAL_SELECTOR: + return SelectorList() + inc i + + #pass 2: move selectors in combination + if selectors.len > 1: + var i = 0 + var slow = SelectorList() + if selectors[0].t == UNIVERSAL_SELECTOR: + inc i + + while i < selectors.len: + if selectors[i].t in {ATTR_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR}: + slow.add(selectors[i]) + else: + result.add(selectors[i]) + inc i + + result.add(slow) + else: + result.add(selectors[0]) + +proc addSelector(state: var SelectorParser, sel: Selector) = + if state.combinator != nil: + if sel.t == PSELEM_SELECTOR: + state.combinator.csels[^1].pseudo = sel.elem + state.combinator.csels[^1].add(sel) + else: + if sel.t == PSELEM_SELECTOR: + state.selectors[^1].pseudo = sel.elem + state.selectors[^1].add(sel) + +proc getLastSel(state: SelectorParser): Selector = + if state.combinator != nil: + return state.combinator.csels[^1].sels[^1] + else: + return state.selectors[^1].sels[^1] + +proc addSelectorList(state: var SelectorParser) = + if state.combinator != nil: + state.selectors[^1].add(state.combinator) + state.combinator = nil + state.selectors.add(SelectorList()) + +proc parseSelectorCombinator(state: var SelectorParser, ct: CombinatorType, csstoken: CSSToken) = + if csstoken.tokenType in {CSS_IDENT_TOKEN, CSS_HASH_TOKEN, + CSS_COLON_TOKEN}: + if state.combinator != nil and state.combinator.ct != ct: + let nc = Selector(t: COMBINATOR_SELECTOR, ct: ct) + nc.csels.add(SelectorList()) + nc.csels[^1].add(state.combinator) + state.combinator = nc + + if state.combinator == nil: + state.combinator = Selector(t: COMBINATOR_SELECTOR, ct: ct) + + state.combinator.csels.add(state.selectors[^1]) + if state.combinator.csels[^1].len > 0: + state.combinator.csels.add(SelectorList()) + state.selectors[^1] = SelectorList() + state.query = QUERY_TYPE + +proc parseSelectorToken(state: var SelectorParser, csstoken: CSSToken) = + case state.query + of QUERY_DESC_COMBINATOR: + state.parseSelectorCombinator(DESCENDANT_COMBINATOR, csstoken) + of QUERY_CHILD_COMBINATOR: + if csstoken.tokenType == CSS_WHITESPACE_TOKEN: + return + state.parseSelectorCombinator(CHILD_COMBINATOR, csstoken) + of QUERY_NEXT_SIBLING_COMBINATOR: + if csstoken.tokenType == CSS_WHITESPACE_TOKEN: + return + state.parseSelectorCombinator(NEXT_SIBLING_COMBINATOR, csstoken) + of QUERY_SUBSEQ_SIBLING_COMBINATOR: + if csstoken.tokenType == CSS_WHITESPACE_TOKEN: + return + state.parseSelectorCombinator(SUBSEQ_SIBLING_COMBINATOR, csstoken) + else: discard + + case csstoken.tokenType + of CSS_IDENT_TOKEN: + case state.query + of QUERY_CLASS: + state.addSelector(Selector(t: CLASS_SELECTOR, class: $csstoken.value)) + of QUERY_TYPE: + state.addSelector(Selector(t: TYPE_SELECTOR, tag: tagType($csstoken.value))) + of QUERY_PSEUDO: + case $csstoken.value + of "before": + state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_BEFORE)) + of "after": + state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_AFTER)) + of "first-child": + state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_FIRST_CHILD)) + of "last-child": + state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_LAST_CHILD)) + of "only-child": + state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_ONLY_CHILD)) + of "hover": + state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_HOVER)) + of "root": + state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_ROOT)) + of QUERY_PSELEM: + case $csstoken.value + of "before": + state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_BEFORE)) + of "after": + state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_AFTER)) + else: discard + else: discard + state.query = QUERY_TYPE + of CSS_DELIM_TOKEN: + case csstoken.rvalue + of Rune('.'): + state.query = QUERY_CLASS + of Rune('>'): + if state.selectors[^1].len > 0 or state.combinator != nil: + state.query = QUERY_CHILD_COMBINATOR + of Rune('+'): + if state.selectors[^1].len > 0 or state.combinator != nil: + state.query = QUERY_NEXT_SIBLING_COMBINATOR + of Rune('~'): + if state.selectors[^1].len > 0 or state.combinator != nil: + state.query = QUERY_SUBSEQ_SIBLING_COMBINATOR + of Rune('*'): + state.addSelector(Selector(t: UNIVERSAL_SELECTOR)) + else: discard + of CSS_HASH_TOKEN: + state.addSelector(Selector(t: ID_SELECTOR, id: $csstoken.value)) + of CSS_COMMA_TOKEN: + if state.selectors[^1].len > 0: + state.addSelectorList() + of CSS_WHITESPACE_TOKEN: + if state.selectors[^1].len > 0 or state.combinator != nil: + state.query = QUERY_DESC_COMBINATOR + of CSS_COLON_TOKEN: + if state.query == QUERY_PSEUDO: + state.query = QUERY_PSELEM + else: + state.query = QUERY_PSEUDO + else: discard + +proc parseSelectorSimpleBlock(state: var SelectorParser, cssblock: CSSSimpleBlock) = + case cssblock.token.tokenType + of CSS_LBRACKET_TOKEN: + state.query = QUERY_ATTR + for cval in cssblock.value: + if cval of CSSToken: + let csstoken = (CSSToken)cval + case csstoken.tokenType + of CSS_IDENT_TOKEN: + case state.query + of QUERY_ATTR: + state.query = QUERY_DELIM + state.addSelector(Selector(t: ATTR_SELECTOR, attr: $csstoken.value, rel: ' ')) + of QUERY_VALUE: + state.getLastSel().value = $csstoken.value + break + else: discard + of CSS_STRING_TOKEN: + case state.query + of QUERY_VALUE: + state.getLastSel().value = $csstoken.value + break + else: discard + of CSS_DELIM_TOKEN: + case csstoken.rvalue + of Rune('~'), Rune('|'), Rune('^'), Rune('$'), Rune('*'): + if state.query == QUERY_DELIM: + state.getLastSel().rel = char(csstoken.rvalue) + of Rune('='): + if state.query == QUERY_DELIM: + if state.getLastSel().rel == ' ': + state.getLastSel().rel = '=' + state.query = QUERY_VALUE + else: discard + else: discard + state.query = QUERY_TYPE + else: discard + +proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction) = + case $cssfunction.name + of "not", "is": + if state.query != QUERY_PSEUDO: + return + state.query = QUERY_TYPE + of "nth-child": + if state.query != QUERY_PSEUDO: + return + if cssfunction.value.len != 1 or not (cssfunction.value[0] of CSSToken): + return + if CSSToken(cssfunction.value[0]).tokenType != CSS_NUMBER_TOKEN: + return + let num = CSSToken(cssfunction.value[0]).nvalue + if num == float64(int64(num)): + state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_NTH_CHILD, pseudonum: num)) + state.query = QUERY_TYPE + return + else: return + var fun = Selector(t: FUNC_SELECTOR, name: $cssfunction.name) + state.addSelector(fun) + + let osels = state.selectors + let ocomb = state.combinator + state.combinator = nil + state.selectors = newSeq[SelectorList]() + state.addSelectorList() + for cval in cssfunction.value: + if cval of CSSToken: + state.parseSelectorToken(CSSToken(cval)) + elif cval of CSSSimpleBlock: + state.parseSelectorSimpleBlock(CSSSimpleBlock(cval)) + elif cval of CSSFunction: + state.parseSelectorFunction(CSSFunction(cval)) + fun.fsels = state.selectors + state.selectors = osels + state.combinator = ocomb + +func parseSelectors*(cvals: seq[CSSComponentValue]): seq[SelectorList] = + var state = SelectorParser() + state.addSelectorList() + for cval in cvals: + if cval of CSSToken: + state.parseSelectorToken(CSSToken(cval)) + elif cval of CSSSimpleBlock: + state.parseSelectorSimpleBlock(CSSSimpleBlock(cval)) + elif cval of CSSFunction: + state.parseSelectorFunction(CSSFunction(cval)) + if state.combinator != nil: + state.selectors[^1].add(state.combinator) + state.combinator = nil + return state.selectors diff --git a/src/css/selparser.nim b/src/css/selparser.nim deleted file mode 100644 index dd38adad..00000000 --- a/src/css/selparser.nim +++ /dev/null @@ -1,340 +0,0 @@ -import unicode - -import css/parser -import html/tags - -type - SelectorType* = enum - TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, CLASS_SELECTOR, - UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR, FUNC_SELECTOR, - COMBINATOR_SELECTOR - - QueryMode = enum - QUERY_TYPE, QUERY_CLASS, QUERY_ATTR, QUERY_DELIM, QUERY_VALUE, - QUERY_PSEUDO, QUERY_PSELEM, QUERY_DESC_COMBINATOR, QUERY_CHILD_COMBINATOR, - QUERY_NEXT_SIBLING_COMBINATOR, QUERY_SUBSEQ_SIBLING_COMBINATOR - - PseudoElem* = enum - PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER - - PseudoClass* = enum - PSEUDO_FIRST_CHILD, PSEUDO_LAST_CHILD, PSEUDO_ONLY_CHILD, PSEUDO_HOVER, - PSEUDO_ROOT, PSEUDO_NTH_CHILD - - CombinatorType* = enum - DESCENDANT_COMBINATOR, CHILD_COMBINATOR, NEXT_SIBLING_COMBINATOR, - SUBSEQ_SIBLING_COMBINATOR - - SelectorParser = object - selectors: seq[SelectorList] - query: QueryMode - combinator: Selector - - #TODO combinators - Selector* = ref object of RootObj - case t*: SelectorType - of TYPE_SELECTOR: - tag*: TagType - of ID_SELECTOR: - id*: string - of ATTR_SELECTOR: - attr*: string - value*: string - rel*: char - of CLASS_SELECTOR: - class*: string - of UNIVERSAL_SELECTOR: #TODO namespaces? - discard - of PSEUDO_SELECTOR: - pseudo*: PseudoClass - pseudonum*: float64 - of PSELEM_SELECTOR: - elem*: PseudoElem - of FUNC_SELECTOR: - name*: string - fsels*: seq[SelectorList] - of COMBINATOR_SELECTOR: - ct*: CombinatorType - csels*: seq[SelectorList] - - SelectorList* = ref object - sels*: seq[Selector] - pseudo*: PseudoElem - -proc add(sellist: SelectorList, sel: Selector) = sellist.sels.add(sel) -proc add(sellist: SelectorList, sels: SelectorList) = sellist.sels.add(sels.sels) -proc `[]`*(sellist: SelectorList, i: int): Selector = sellist.sels[i] -proc len*(sellist: SelectorList): int = sellist.sels.len - -func getSpecificity*(sels: SelectorList): int - -func getSpecificity(sel: Selector): int = - case sel.t - of ID_SELECTOR: - result += 1000000 - of CLASS_SELECTOR, ATTR_SELECTOR, PSEUDO_SELECTOR: - result += 1000 - of TYPE_SELECTOR, PSELEM_SELECTOR: - result += 1 - of FUNC_SELECTOR: - case sel.name - of "is": - var best = 0 - for child in sel.fsels: - let s = getSpecificity(child) - if s > best: - best = s - result += best - of "not": - for child in sel.fsels: - result += getSpecificity(child) - else: discard - of UNIVERSAL_SELECTOR: - discard - of COMBINATOR_SELECTOR: - for child in sel.csels: - result += getSpecificity(child) - -func getSpecificity*(sels: SelectorList): int = - for sel in sels.sels: - result += getSpecificity(sel) - -func optimizeSelectorList*(selectors: SelectorList): SelectorList = - new(result) - #pass 1: check for invalid sequences - var i = 1 - while i < selectors.len: - let sel = selectors[i] - if sel.t == TYPE_SELECTOR or sel.t == UNIVERSAL_SELECTOR: - return SelectorList() - inc i - - #pass 2: move selectors in combination - if selectors.len > 1: - var i = 0 - var slow = SelectorList() - if selectors[0].t == UNIVERSAL_SELECTOR: - inc i - - while i < selectors.len: - if selectors[i].t in {ATTR_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR}: - slow.add(selectors[i]) - else: - result.add(selectors[i]) - inc i - - result.add(slow) - else: - result.add(selectors[0]) - -proc addSelector(state: var SelectorParser, sel: Selector) = - if state.combinator != nil: - if sel.t == PSELEM_SELECTOR: - state.combinator.csels[^1].pseudo = sel.elem - state.combinator.csels[^1].add(sel) - else: - if sel.t == PSELEM_SELECTOR: - state.selectors[^1].pseudo = sel.elem - state.selectors[^1].add(sel) - -proc getLastSel(state: SelectorParser): Selector = - if state.combinator != nil: - return state.combinator.csels[^1].sels[^1] - else: - return state.selectors[^1].sels[^1] - -proc addSelectorList(state: var SelectorParser) = - if state.combinator != nil: - state.selectors[^1].add(state.combinator) - state.combinator = nil - state.selectors.add(SelectorList()) - -proc parseSelectorCombinator(state: var SelectorParser, ct: CombinatorType, csstoken: CSSToken) = - if csstoken.tokenType in {CSS_IDENT_TOKEN, CSS_HASH_TOKEN, - CSS_COLON_TOKEN}: - if state.combinator != nil and state.combinator.ct != ct: - let nc = Selector(t: COMBINATOR_SELECTOR, ct: ct) - nc.csels.add(SelectorList()) - nc.csels[^1].add(state.combinator) - state.combinator = nc - - if state.combinator == nil: - state.combinator = Selector(t: COMBINATOR_SELECTOR, ct: ct) - - state.combinator.csels.add(state.selectors[^1]) - if state.combinator.csels[^1].len > 0: - state.combinator.csels.add(SelectorList()) - state.selectors[^1] = SelectorList() - state.query = QUERY_TYPE - -proc parseSelectorToken(state: var SelectorParser, csstoken: CSSToken) = - case state.query - of QUERY_DESC_COMBINATOR: - state.parseSelectorCombinator(DESCENDANT_COMBINATOR, csstoken) - of QUERY_CHILD_COMBINATOR: - if csstoken.tokenType == CSS_WHITESPACE_TOKEN: - return - state.parseSelectorCombinator(CHILD_COMBINATOR, csstoken) - of QUERY_NEXT_SIBLING_COMBINATOR: - if csstoken.tokenType == CSS_WHITESPACE_TOKEN: - return - state.parseSelectorCombinator(NEXT_SIBLING_COMBINATOR, csstoken) - of QUERY_SUBSEQ_SIBLING_COMBINATOR: - if csstoken.tokenType == CSS_WHITESPACE_TOKEN: - return - state.parseSelectorCombinator(SUBSEQ_SIBLING_COMBINATOR, csstoken) - else: discard - - case csstoken.tokenType - of CSS_IDENT_TOKEN: - case state.query - of QUERY_CLASS: - state.addSelector(Selector(t: CLASS_SELECTOR, class: $csstoken.value)) - of QUERY_TYPE: - state.addSelector(Selector(t: TYPE_SELECTOR, tag: tagType($csstoken.value))) - of QUERY_PSEUDO: - case $csstoken.value - of "before": - state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_BEFORE)) - of "after": - state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_AFTER)) - of "first-child": - state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_FIRST_CHILD)) - of "last-child": - state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_LAST_CHILD)) - of "only-child": - state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_ONLY_CHILD)) - of "hover": - state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_HOVER)) - of "root": - state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_ROOT)) - of QUERY_PSELEM: - case $csstoken.value - of "before": - state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_BEFORE)) - of "after": - state.addSelector(Selector(t: PSELEM_SELECTOR, elem: PSEUDO_AFTER)) - else: discard - else: discard - state.query = QUERY_TYPE - of CSS_DELIM_TOKEN: - case csstoken.rvalue - of Rune('.'): - state.query = QUERY_CLASS - of Rune('>'): - if state.selectors[^1].len > 0 or state.combinator != nil: - state.query = QUERY_CHILD_COMBINATOR - of Rune('+'): - if state.selectors[^1].len > 0 or state.combinator != nil: - state.query = QUERY_NEXT_SIBLING_COMBINATOR - of Rune('~'): - if state.selectors[^1].len > 0 or state.combinator != nil: - state.query = QUERY_SUBSEQ_SIBLING_COMBINATOR - of Rune('*'): - state.addSelector(Selector(t: UNIVERSAL_SELECTOR)) - else: discard - of CSS_HASH_TOKEN: - state.addSelector(Selector(t: ID_SELECTOR, id: $csstoken.value)) - of CSS_COMMA_TOKEN: - if state.selectors[^1].len > 0: - state.addSelectorList() - of CSS_WHITESPACE_TOKEN: - if state.selectors[^1].len > 0 or state.combinator != nil: - state.query = QUERY_DESC_COMBINATOR - of CSS_COLON_TOKEN: - if state.query == QUERY_PSEUDO: - state.query = QUERY_PSELEM - else: - state.query = QUERY_PSEUDO - else: discard - -proc parseSelectorSimpleBlock(state: var SelectorParser, cssblock: CSSSimpleBlock) = - case cssblock.token.tokenType - of CSS_LBRACKET_TOKEN: - state.query = QUERY_ATTR - for cval in cssblock.value: - if cval of CSSToken: - let csstoken = (CSSToken)cval - case csstoken.tokenType - of CSS_IDENT_TOKEN: - case state.query - of QUERY_ATTR: - state.query = QUERY_DELIM - state.addSelector(Selector(t: ATTR_SELECTOR, attr: $csstoken.value, rel: ' ')) - of QUERY_VALUE: - state.getLastSel().value = $csstoken.value - break - else: discard - of CSS_STRING_TOKEN: - case state.query - of QUERY_VALUE: - state.getLastSel().value = $csstoken.value - break - else: discard - of CSS_DELIM_TOKEN: - case csstoken.rvalue - of Rune('~'), Rune('|'), Rune('^'), Rune('$'), Rune('*'): - if state.query == QUERY_DELIM: - state.getLastSel().rel = char(csstoken.rvalue) - of Rune('='): - if state.query == QUERY_DELIM: - if state.getLastSel().rel == ' ': - state.getLastSel().rel = '=' - state.query = QUERY_VALUE - else: discard - else: discard - state.query = QUERY_TYPE - else: discard - -proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction) = - case $cssfunction.name - of "not", "is": - if state.query != QUERY_PSEUDO: - return - state.query = QUERY_TYPE - of "nth-child": - if state.query != QUERY_PSEUDO: - return - if cssfunction.value.len != 1 or not (cssfunction.value[0] of CSSToken): - return - if CSSToken(cssfunction.value[0]).tokenType != CSS_NUMBER_TOKEN: - return - let num = CSSToken(cssfunction.value[0]).nvalue - if num == float64(int64(num)): - state.addSelector(Selector(t: PSEUDO_SELECTOR, pseudo: PSEUDO_NTH_CHILD, pseudonum: num)) - state.query = QUERY_TYPE - return - else: return - var fun = Selector(t: FUNC_SELECTOR, name: $cssfunction.name) - state.addSelector(fun) - - let osels = state.selectors - let ocomb = state.combinator - state.combinator = nil - state.selectors = newSeq[SelectorList]() - state.addSelectorList() - for cval in cssfunction.value: - if cval of CSSToken: - state.parseSelectorToken(CSSToken(cval)) - elif cval of CSSSimpleBlock: - state.parseSelectorSimpleBlock(CSSSimpleBlock(cval)) - elif cval of CSSFunction: - state.parseSelectorFunction(CSSFunction(cval)) - fun.fsels = state.selectors - state.selectors = osels - state.combinator = ocomb - -func parseSelectors*(cvals: seq[CSSComponentValue]): seq[SelectorList] = - var state = SelectorParser() - state.addSelectorList() - for cval in cvals: - if cval of CSSToken: - state.parseSelectorToken(CSSToken(cval)) - elif cval of CSSSimpleBlock: - state.parseSelectorSimpleBlock(CSSSimpleBlock(cval)) - elif cval of CSSFunction: - state.parseSelectorFunction(CSSFunction(cval)) - if state.combinator != nil: - state.selectors[^1].add(state.combinator) - state.combinator = nil - return state.selectors diff --git a/src/css/sheet.nim b/src/css/sheet.nim index aab602ec..be8ac80c 100644 --- a/src/css/sheet.nim +++ b/src/css/sheet.nim @@ -3,8 +3,8 @@ import tables import unicode import css/mediaquery -import css/parser -import css/selparser +import css/cssparser +import css/selectorparser import html/tags type diff --git a/src/css/values.nim b/src/css/values.nim index 88efc84a..0e8923aa 100644 --- a/src/css/values.nim +++ b/src/css/values.nim @@ -6,13 +6,13 @@ import options import macros import strutils -import css/parser -import css/selparser +import css/cssparser +import css/selectorparser import io/term import types/color import utils/twtstr -export selparser.PseudoElem +export selectorparser.PseudoElem type CSSUnit* = enum -- cgit 1.4.1-2-gfad0