diff options
author | bptato <nincsnevem662@gmail.com> | 2021-07-30 20:23:34 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2021-07-30 20:25:38 +0200 |
commit | 34b023515599bc746c10c597467ecb07f53c49fe (patch) | |
tree | 38e77bb205bf94c63387dae4f7d859cda7bb9ce6 /src/css | |
parent | 94a10242dca6181ef8f15a37e7083069ead09559 (diff) | |
download | chawan-34b023515599bc746c10c597467ecb07f53c49fe.tar.gz |
CSS selectors and re-organization
Diffstat (limited to 'src/css')
-rw-r--r-- | src/css/cssparser.nim | 813 | ||||
-rw-r--r-- | src/css/selector.nim | 151 | ||||
-rw-r--r-- | src/css/style.nim | 321 |
3 files changed, 1285 insertions, 0 deletions
diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim new file mode 100644 index 00000000..5ecb470a --- /dev/null +++ b/src/css/cssparser.nim @@ -0,0 +1,813 @@ +# CSS tokenizer and parser. The tokenizer is a mess, and may or may not work +# correctly. The parser should work, though the outputted object model is +# questionable at best. + +import unicode +import streams +import math +import options + +import ../io/twtio + +import ../utils/twtstr + +import ../types/enums + +type + 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] + + CSSStylesheet* = object + value*: seq[CSSRule] + + SyntaxError = object of ValueError + +func `==`*(a: CSSParsedItem, b: CSSTokenType): bool = + return a of CSSToken and CSSToken(a).tokenType == b + +func toNumber(s: seq[Rune]): float64 = + var sign = 1 + var t = 1 + var d = 0 + var integer: float64 = 0 + var f: float64 = 0 + var e: float64 = 0 + + var i = 0 + if i < s.len and s[i] == Rune('-'): + sign = -1 + inc i + elif i < s.len and s[i] == Rune('+'): + inc i + + while i < s.len and isDigitAscii(s[i]): + integer *= 10 + integer += float64(decValue(s[i])) + inc i + + if i < s.len and s[i] == Rune('.'): + inc i + while i < s.len and isDigitAscii(s[i]): + f *= 10 + f += float64(decValue(s[i])) + inc i + inc d + + if i < s.len and (s[i] == Rune('e') or s[i] == Rune('E')): + inc i + if i < s.len and s[i] == Rune('-'): + t = -1 + inc i + elif i < s.len and s[i] == Rune('+'): + inc i + + while i < s.len and isDigitAscii(s[i]): + e *= 10 + e += float64(decValue(s[i])) + inc i + + return float64(sign) * (integer + f * pow(10, float64(-d))) * pow(10, (float64(t) * e)) + +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): 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 has(state: var CSSTokenizerState): bool = + if state.at >= state.buf.len and not state.stream.atEnd(): + state.buf &= state.stream.readLine().toRunes() & Rune('\n') + return state.at < state.buf.len + +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 = toNumber(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.toAsciiLower() == "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(2) and state.peek(1) == Rune('/') and state.peek(2) == Rune('*'): + discard state.consume() + discard state.consume() + while state.has(2) and not (state.peek(1) == Rune('*') and state.peek(2) == Rune('/')): + discard state.consume() + + if state.has(2): + discard state.consume() + if state.has(1): + discard state.consume() + +proc consumeToken(state: var CSSTokenizerState): CSSToken = + state.consumeComments() + 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() + eprint result.value + 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(): + result.add(state.consumeToken()) + + 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 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: + result.value.add(CSSComponentValue(t)) + return result + +proc consumeComponentValue*(state: var CSSParseState): CSSComponentValue + +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 = state.consumeSimpleBlock() + 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 = state.consumeSimpleBlock() + 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.toAsciiLower() == "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. +#Wow this is ugly. +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 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): CSSStylesheet = + state.top_level = true + result.value.add(state.consumeListOfRules()) + +proc parseStylesheet(inputStream: Stream): CSSStylesheet = + var state = CSSParseState() + state.tokens = tokenizeCSS(inputStream) + return state.parseStylesheet() + +proc parseListOfRules(state: var CSSParseState): seq[CSSRule] = + return state.consumeListOfRules() + +proc parseListOfRules(inputStream: Stream): seq[CSSRule] = + var state = CSSParseState() + state.tokens = tokenizeCSS(inputStream) + 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 parseCSSDeclaration*(inputStream: Stream): CSSDeclaration = + var state = CSSParseState() + state.tokens = tokenizeCSS(inputStream) + return state.parseDeclaration() + +proc parseListOfDeclarations(state: var CSSParseState): seq[CSSParsedItem] = + return state.consumeListOfDeclarations() + +proc parseCSSListOfDeclarations*(inputStream: Stream): seq[CSSParsedItem] = + var state = CSSParseState() + state.tokens = tokenizeCSS(inputStream) + return state.parseListOfDeclarations() + +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 parseCSSComponentValue*(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 parseCSSListOfComponentValues*(inputStream: Stream): seq[CSSComponentValue] = + var state = CSSParseState() + state.tokens = tokenizeCSS(inputStream) + return state.parseListOfComponentValues() + +proc parseCommaSeparatedListOfComponentValues(state: var CSSParseState): seq[CSSComponentValue] = + while state.has(1): + let cvl = state.consumeComponentValue() + if cvl != CSS_COMMA_TOKEN: + result.add(state.consumeComponentValue()) + +proc parseCommaSeparatedListOfComponentValues(inputStream: Stream): seq[CSSComponentValue] = + var state = CSSParseState() + state.tokens = tokenizeCSS(inputStream) + return state.parseCommaSeparatedListOfComponentValues() + +proc printc*(c: CSSComponentValue) = + if c of CSSToken: + case CSSToken(c).tokenType: + of CSS_FUNCTION_TOKEN, CSS_AT_KEYWORD_TOKEN, CSS_URL_TOKEN: + eprint CSSToken(c).tokenType, CSSToken(c).value + of CSS_HASH_TOKEN: + stderr.write('#' & $CSSToken(c).value) + of CSS_IDENT_TOKEN: + stderr.write(CSSToken(c).value) + of CSS_STRING_TOKEN: + stderr.write("\"" & $CSSToken(c).value & "\"") + of CSS_DELIM_TOKEN: + stderr.write(CSSToken(c).rvalue) + of CSS_DIMENSION_TOKEN: + eprint CSSToken(c).tokenType, CSSToken(c).nvalue, "unit", CSSToken(c).unit, CSSToken(c).tflagb + of CSS_NUMBER_TOKEN: + stderr.write($CSSToken(c).nvalue & $CSSToken(c).unit) + of CSS_PERCENTAGE_TOKEN: + stderr.write($CSSToken(c).nvalue & "%") + of CSS_COLON_TOKEN: + stderr.write(":") + of CSS_WHITESPACE_TOKEN: + stderr.write(" ") + of CSS_SEMICOLON_TOKEN: + stderr.write(";\n") + of CSS_COMMA_TOKEN: + stderr.write(",") + else: + eprint CSSToken(c).tokenType + elif c of CSSDeclaration: + stderr.write(CSSDeclaration(c).name) + stderr.write(": ") + for s in CSSDeclaration(c).value: + printc(s) + stderr.write(";\n") + elif c of CSSFunction: + stderr.write($CSSFunction(c).name & "(") + for s in CSSFunction(c).value: + printc(s) + stderr.write(")") + elif c of CSSSimpleBlock: + case CSSSimpleBlock(c).token.tokenType + of CSS_LBRACE_TOKEN: eprint "{" + of CSS_LPAREN_TOKEN: stderr.write("(") + of CSS_LBRACKET_TOKEN: stderr.write("[") + else: discard + for s in CSSSimpleBlock(c).value: + printc(s) + case CSSSimpleBlock(c).token.tokenType + of CSS_LBRACE_TOKEN: eprint "}" + of CSS_LPAREN_TOKEN: stderr.write(")") + of CSS_LBRACKET_TOKEN: stderr.write("]") + else: discard + +proc parseCSS*(inputStream: Stream): CSSStylesheet = + return inputstream.parseStylesheet() + +proc debugparseCSS*(inputStream: Stream) = + let ss = inputStream.parseStylesheet() + for v in ss.value: + if v of CSSAtRule: + eprint CSSAtRule(v).name + else: + for c in CSSQualifiedRule(v).prelude: + printc(c) + case v.oblock.token.tokenType + of CSS_LBRACE_TOKEN: eprint "\n{" + of CSS_LPAREN_TOKEN: eprint "(" + of CSS_LBRACKET_TOKEN: eprint "[" + else: discard + for s in v.oblock.value: + printc(s) + case v.oblock.token.tokenType + of CSS_LBRACE_TOKEN: eprint "\n}" + of CSS_LPAREN_TOKEN: eprint ")" + of CSS_LBRACKET_TOKEN: eprint "]" + else: discard diff --git a/src/css/selector.nim b/src/css/selector.nim new file mode 100644 index 00000000..1ca417dd --- /dev/null +++ b/src/css/selector.nim @@ -0,0 +1,151 @@ +import unicode + +import ../types/enums +import ../types/tagtypes + +import cssparser + +type + SelectorType* = enum + TYPE_SELECTOR, ID_SELECTOR, ATTR_SELECTOR, CLASS_SELECTOR, + UNIVERSAL_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR, FUNC_SELECTOR + + QueryMode* = enum + QUERY_TYPE, QUERY_CLASS, QUERY_ATTR, QUERY_DELIM, QUERY_VALUE, + QUERY_PSEUDO, QUERY_PSELEM + + SelectorParser = object + selectors: seq[SelectorList] + query: QueryMode + negate: bool + + #TODO combinators + Selector* = object + 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*: string + of PSELEM_SELECTOR: + elem*: string + of FUNC_SELECTOR: + name*: string + selectors*: SelectorList + + SelectorList* = ref object + sels*: seq[Selector] + parent*: SelectorList + +proc add*(sellist: SelectorList, sel: Selector) = sellist.sels.add(sel) +proc add*(sellist: SelectorList, sels: SelectorList) = sellist.sels.add(sels.sels) +proc setLen*(sellist: SelectorList, i: int) = sellist.sels.setLen(i) +proc `[]`*(sellist: SelectorList, i: int): Selector = sellist.sels[i] +proc len*(sellist: SelectorList): int = sellist.sels.len + +proc parseSelectorToken(state: var SelectorParser, csstoken: CSSToken) = + case csstoken.tokenType + of CSS_IDENT_TOKEN: + var sel: Selector + case state.query + of QUERY_CLASS: + state.selectors[^1].add(Selector(t: CLASS_SELECTOR, class: $csstoken.value)) + of QUERY_TYPE: + state.selectors[^1].add(Selector(t: TYPE_SELECTOR, tag: tagType($csstoken.value))) + of QUERY_PSEUDO: + state.selectors[^1].add(Selector(t: PSEUDO_SELECTOR, pseudo: $csstoken.value)) + of QUERY_PSELEM: + state.selectors[^1].add(Selector(t: PSELEM_SELECTOR, elem: $csstoken.value)) + else: discard + state.query = QUERY_TYPE + of CSS_DELIM_TOKEN: + if csstoken.rvalue == Rune('.'): + state.query = QUERY_CLASS + of CSS_HASH_TOKEN: + state.selectors[^1].add(Selector(t: ID_SELECTOR, id: $csstoken.value)) + of CSS_COMMA_TOKEN: + if state.selectors[^1].len > 0: + state.selectors.add(SelectorList()) + 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.selectors[^1].add(Selector(t: ATTR_SELECTOR, attr: $csstoken.value, rel: ' ')) + of QUERY_VALUE: + state.selectors[^1].sels[^1].value = $csstoken.value + break + else: discard + of CSS_STRING_TOKEN: + case state.query + of QUERY_VALUE: + state.selectors[^1].sels[^1].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.selectors[^1].sels[^1].rel = char(csstoken.rvalue) + of Rune('='): + if state.query == QUERY_DELIM: + 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": + if state.query != QUERY_PSEUDO: + return + state.query = QUERY_TYPE + else: return + var fun = Selector(t: FUNC_SELECTOR, name: $cssfunction.name) + fun.selectors = SelectorList(parent: state.selectors[^1]) + state.selectors[^1].add(fun) + state.selectors[^1] = fun.selectors + 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) + state.selectors[^1] = fun.selectors.parent + +func parseSelectors*(cvals: seq[CSSComponentValue]): seq[SelectorList] = + var state = SelectorParser() + state.selectors.add(SelectorList()) + 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) + return state.selectors diff --git a/src/css/style.nim b/src/css/style.nim new file mode 100644 index 00000000..56e6b00b --- /dev/null +++ b/src/css/style.nim @@ -0,0 +1,321 @@ +import streams +import unicode +import terminal +import tables + +import ../io/twtio + +import ../utils/twtstr + +import ../types/enums + +import cssparser + +type + CSSLength* = object + num*: float64 + unit*: CSSUnit + auto*: bool + + CSS2Properties* = ref object + rawtext*: string + fmttext*: seq[string] + x*: int + y*: int + ex*: int + ey*: int + width*: int + height*: int + hidden*: bool + before*: CSS2Properties + after*: CSS2Properties + margintop*: CSSLength + marginbottom*: CSSLength + marginleft*: CSSLength + marginright*: CSSLength + centered*: bool + display*: DisplayType + bold*: bool + italic*: bool + underscore*: bool + islink*: bool + selected*: bool + indent*: int + color*: CSSColor + position*: CSSPosition + + CSSCanvas* = object + rootBox*: CSSBox + width*: int + height*: int + + CSSRect* = object + x1*: int + y1*: int + x2*: int + y2*: int + + CSSBox* = ref object + display*: DisplayType + x*: int + y*: int + innerEdge*: CSSRect + paddingEdge*: CSSRect + borderEdge*: CSSRect + marginEdge*: CSSRect + color*: CSSColor + props*: CSS2Properties + content*: seq[Rune] + dispcontent*: string + children*: seq[CSSBox] + + CSSColor* = tuple[r: uint8, g: uint8, b: uint8, a: uint8] + +func `+`(a: CSSRect, b: CSSRect): CSSRect = + result.x1 = a.x1 + b.x1 + result.y1 = a.y1 + b.y1 + result.x2 = a.x2 + b.x2 + result.y2 = a.y2 + b.y2 + +proc `+=`(a: var CSSRect, b: CSSRect) = + a = a + b + +func cells(l: CSSLength): int = + case l.unit + of EM_UNIT: + return int(l.num) + else: + #TODO + return int(l.num / 8) + +const colors = { + "maroon": (0x80u8, 0x00u8, 0x00u8, 0x00u8), + "red": (0xffu8, 0x00u8, 0x00u8, 0x00u8), + "orange": (0xffu8, 0xa5u8, 0x00u8, 0x00u8), + "yellow": (0xffu8, 0xffu8, 0x00u8, 0x00u8), + "olive": (0x80u8, 0x80u8, 0x00u8, 0x00u8), + "purple": (0x80u8, 0x00u8, 0x80u8, 0x00u8), + "fuchsia": (0xffu8, 0x00u8, 0x00u8, 0x00u8), + "white": (0xffu8, 0xffu8, 0xffu8, 0x00u8), + "lime": (0x00u8, 0xffu8, 0x00u8, 0x00u8), + "green": (0x00u8, 0x80u8, 0x00u8, 0x00u8), + "navy": (0x00u8, 0x00u8, 0x80u8, 0x00u8), + "blue": (0x00u8, 0x00u8, 0xffu8, 0x00u8), + "aqua": (0x00u8, 0xffu8, 0xffu8, 0x00u8), + "teal": (0x00u8, 0x80u8, 0x80u8, 0x00u8), + "black": (0x00u8, 0x00u8, 0x00u8, 0x00u8), + "silver": (0xc0u8, 0xc0u8, 0xc0u8, 0x00u8), + "gray": (0x80u8, 0x80u8, 0x80u8, 0x00u8), +}.toTable() + +const defaultColor = (0xffu8, 0xffu8, 0xffu8, 0x00u8) + +func cssLength(val: float64, unit: string): CSSLength = + case unit + of "%": return CSSLength(num: val, unit: PERC_UNIT) + of "cm": return CSSLength(num: val, unit: CM_UNIT) + of "mm": return CSSLength(num: val, unit: MM_UNIT) + of "in": return CSSLength(num: val, unit: IN_UNIT) + of "px": return CSSLength(num: val, unit: PX_UNIT) + of "pt": return CSSLength(num: val, unit: PT_UNIT) + of "pc": return CSSLength(num: val, unit: PC_UNIT) + of "em": return CSSLength(num: val, unit: EM_UNIT) + of "ex": return CSSLength(num: val, unit: EX_UNIT) + of "ch": return CSSLength(num: val, unit: CH_UNIT) + of "rem": return CSSLength(num: val, unit: REM_UNIT) + of "vw": return CSSLength(num: val, unit: VW_UNIT) + of "vh": return CSSLength(num: val, unit: VH_UNIT) + of "vmin": return CSSLength(num: val, unit: VMIN_UNIT) + of "vmax": return CSSLength(num: val, unit: VMAX_UNIT) + else: return CSSLength(num: 0, unit: EM_UNIT) + +func cssColor*(d: CSSDeclaration): CSSColor = + if d.value.len > 0: + if d.value[0] of CSSToken: + let tok = CSSToken(d.value[0]) + case tok.tokenType + of CSS_HASH_TOKEN: + let s = tok.value + if s.len == 3: + for r in s: + if hexValue(r) == -1: + return + let r = hexValue(s[0]) * 0x10 + hexValue(s[0]) + let g = hexValue(s[1]) * 0x10 + hexValue(s[1]) + let b = hexValue(s[2]) * 0x10 + hexValue(s[2]) + + return (uint8(r), uint8(g), uint8(b), 0x00u8) + elif s.len == 6: + for r in s: + if hexValue(r) == -1: + return + let r = hexValue(s[0]) * 0x10 + hexValue(s[1]) + let g = hexValue(s[2]) * 0x10 + hexValue(s[3]) + let b = hexValue(s[4]) * 0x10 + hexValue(s[5]) + return (uint8(r), uint8(g), uint8(b), 0x00u8) + else: + return defaultColor + of CSS_IDENT_TOKEN: + let s = tok.value + eprint "ident", s + if $s in colors: + return colors[$s] + else: + return defaultColor + else: + eprint "else", tok.tokenType + return defaultColor + elif d of CSSFunction: + let f = CSSFunction(d.value[0]) + eprint "func", f.name + #todo calc etc (cssnumber function or something) + case $f.name + of "rgb": + if f.value.len != 3: + return defaultColor + for c in f.value: + if c != CSS_NUMBER_TOKEN: + return defaultColor + let r = CSSToken(f.value[0]).nvalue + let g = CSSToken(f.value[1]).nvalue + let b = CSSToken(f.value[2]).nvalue + return (uint8(r), uint8(g), uint8(b), 0x00u8) + of "rgba": + if f.value.len != 4: + eprint "too few args" + return defaultColor + for c in f.value: + if c != CSS_NUMBER_TOKEN: + eprint "not number" + return defaultColor + let r = CSSToken(f.value[0]).nvalue + let g = CSSToken(f.value[1]).nvalue + let b = CSSToken(f.value[2]).nvalue + let a = CSSToken(f.value[3]).nvalue + return (uint8(r), uint8(g), uint8(b), uint8(a)) + else: + eprint "not rgba" + return defaultColor + + return defaultColor + +func cssLength(d: CSSDeclaration): CSSLength = + if d.value.len > 0 and d.value[0] of CSSToken: + let tok = CSSToken(d.value[0]) + case tok.tokenType + of CSS_PERCENTAGE_TOKEN: + return cssLength(tok.nvalue, "%") + of CSS_DIMENSION_TOKEN: + return cssLength(tok.nvalue, $tok.unit) + of CSS_IDENT_TOKEN: + if $tok.value == "auto": + return CSSLength(num: 0, unit: EM_UNIT, auto: true) + else: + return CSSLength(num: 0, unit: EM_UNIT) + + return CSSLength(num: 0, unit: EM_UNIT) + +func hasColor*(style: CSS2Properties): bool = + return style.color.r != 0 or style.color.b != 0 or style.color.g != 0 or style.color.a != 0 + +func termColor*(style: CSS2Properties): ForegroundColor = + if style.color.r > 120: + return fgRed + elif style.color.b > 120: + return fgBlue + elif style.color.g > 120: + return fgGreen + else: + return fgWhite + +proc applyProperties*(box: CSSBox, s: string) = + let decls = parseCSSListOfDeclarations(newStringStream(s)) + if box.props == nil: + box.props = CSS2Properties() + let props = box.props + + for item in decls: + if item of CSSDeclaration: + let d = CSSDeclaration(item) + case $d.name + of "color": + props.color = cssColor(d) + eprint props.color #TODO + of "margin": + let l = cssLength(d) + props.margintop = l + props.marginbottom = l + props.marginleft = l + props.marginright = l + of "margin-top": + props.margintop = cssLength(d) + of "margin-left": + props.marginleft = cssLength(d) + of "margin-right": + props.marginright = cssLength(d) + of "margin-bottom": + props.marginbottom = cssLength(d) + else: + printc(d) #TODO + +func getLength(s: seq[Rune], start: int, wlimit: int): tuple[wrap: bool, len: int, width: int] = + var len = 0 + var width = 0 + var i = start + while i < s.len: + let r = s[i] + let cw = r.width() + if width + cw > wlimit: + return (wrap: true, len: len, width: width) + width += cw + len += 1 + + return (wrap: false, len: len, width: width) + +proc arrangeBoxes*(canvas: CSSCanvas) = + var stack: seq[CSSBox] + stack.add(canvas.rootBox) + var x = 0 + var y = 0 + + while stack.len > 0: + let box = stack.pop() + + #arrange box + box.marginEdge.x1 = x + box.marginEdge.y1 = y + x += box.props.marginleft.cells() + y += box.props.margintop.cells() + + if box.display == DISPLAY_BLOCK: + x = 0 + inc y + + if x > canvas.width: + x = 0 + inc y + + box.x = x + box.y = y + + var l = 0 + while l < box.content.len: + let (wrap, wraplen, wrapwidth) = box.content.getLength(l, canvas.width - x) + var wrapbox = new(CSSBox) + wrapbox.content = box.content.substr(l, l + wraplen) + box.children.add(wrapbox) + l += wraplen + x += wrapwidth + if wrap: + inc y + x = 0 + + x += box.props.marginright.cells() + y += box.props.marginbottom.cells() + box.marginEdge.x2 = x + box.marginEdge.y2 = y + + var i = box.children.len - 1 + while i >= 0: + stack.add(box.children[i]) + i -= 1 |