import unicode import tables import utils/twtstr import types/enums import css/parser import types/color type CSSLength* = object num*: float64 unit*: CSSUnit auto*: bool CSSColor* = tuple[r: uint8, g: uint8, b: uint8, a: uint8] CSSComputedValue* = ref object of RootObj t*: CSSPropertyType case v*: CSSValueType of VALUE_COLOR: color*: CSSColor of VALUE_LENGTH: length*: CSSLength of VALUE_FONT_STYLE: fontstyle*: CSSFontStyle of VALUE_DISPLAY: display*: CSSDisplay of VALUE_CONTENT: content*: seq[Rune] of VALUE_WHITESPACE: whitespace*: CSSWhitespace of VALUE_INTEGER: integer*: int of VALUE_TEXT_DECORATION: textdecoration*: CSSTextDecoration of VALUE_WORD_BREAK: wordbreak*: CSSWordBreak of VALUE_NONE: discard CSSComputedValues* = array[low(CSSPropertyType)..high(CSSPropertyType), CSSComputedValue] CSSSpecifiedValue* = object of CSSComputedValue globalValue: CSSGlobalValueType CSSValueError* = object of ValueError #TODO calculate this during compile time const PropertyNames = { "all": PROPERTY_ALL, "color": PROPERTY_COLOR, "margin": PROPERTY_MARGIN, "margin-top": PROPERTY_MARGIN_TOP, "margin-bottom": PROPERTY_MARGIN_BOTTOM, "margin-left": PROPERTY_MARGIN_LEFT, "margin-right": PROPERTY_MARGIN_RIGHT, "font-style": PROPERTY_FONT_STYLE, "display": PROPERTY_DISPLAY, "content": PROPERTY_CONTENT, "white-space": PROPERTY_WHITE_SPACE, "font-weight": PROPERTY_FONT_WEIGHT, "text-decoration": PROPERTY_TEXT_DECORATION, "word-break": PROPERTY_WORD_BREAK, }.toTable() const ValueTypes = [ PROPERTY_NONE: VALUE_NONE, PROPERTY_ALL: VALUE_NONE, PROPERTY_COLOR: VALUE_COLOR, PROPERTY_MARGIN: VALUE_LENGTH, PROPERTY_MARGIN_TOP: VALUE_LENGTH, PROPERTY_MARGIN_LEFT: VALUE_LENGTH, PROPERTY_MARGIN_RIGHT: VALUE_LENGTH, PROPERTY_MARGIN_BOTTOM: VALUE_LENGTH, PROPERTY_FONT_STYLE: VALUE_FONT_STYLE, PROPERTY_DISPLAY: VALUE_DISPLAY, PROPERTY_CONTENT: VALUE_CONTENT, PROPERTY_WHITE_SPACE: VALUE_WHITE_SPACE, PROPERTY_FONT_WEIGHT: VALUE_INTEGER, PROPERTY_TEXT_DECORATION: VALUE_TEXT_DECORATION, PROPERTY_WORD_BREAK: VALUE_WORD_BREAK, ] const InheritedProperties = { PROPERTY_COLOR, PROPERTY_FONT_STYLE, PROPERTY_WHITE_SPACE, PROPERTY_FONT_WEIGHT, PROPERTY_TEXT_DECORATION, PROPERTY_WORD_BREAK } func getPropInheritedArray(): array[low(CSSPropertyType)..high(CSSPropertyType), bool] = for prop in low(CSSPropertyType)..high(CSSPropertyType): if prop in InheritedProperties: result[prop] = true else: result[prop] = false const InheritedArray = getPropInheritedArray() func propertyType*(s: string): CSSPropertyType = return PropertyNames.getOrDefault(s, PROPERTY_NONE) func valueType*(prop: CSSPropertyType): CSSValueType = return ValueTypes[prop] func inherited(t: CSSPropertyType): bool = return InheritedArray[t] func cells*(l: CSSLength): int = case l.unit of UNIT_EM: return int(l.num) of UNIT_CH: 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() func cssLength(val: float64, unit: string): CSSLength = case unit of "%": return CSSLength(num: val, unit: UNIT_PERC) of "cm": return CSSLength(num: val, unit: UNIT_CM) of "mm": return CSSLength(num: val, unit: UNIT_MM) of "in": return CSSLength(num: val, unit: UNIT_IN) of "px": return CSSLength(num: val, unit: UNIT_PX) of "pt": return CSSLength(num: val, unit: UNIT_PT) of "pc": return CSSLength(num: val, unit: UNIT_PC) of "em": return CSSLength(num: val, unit: UNIT_EM) of "ex": return CSSLength(num: val, unit: UNIT_EX) of "ch": return CSSLength(num: val, unit: UNIT_CH) of "rem": return CSSLength(num: val, unit: UNIT_REM) of "vw": return CSSLength(num: val, unit: UNIT_VW) of "vh": return CSSLength(num: val, unit: UNIT_VH) of "vmin": return CSSLength(num: val, unit: UNIT_VMIN) of "vmax": return CSSLength(num: val, unit: UNIT_VMAX) else: raise newException(CSSValueError, "Invalid 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: raise newException(CSSValueError, "Invalid color") 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: raise newException(CSSValueError, "Invalid color") 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: raise newException(CSSValueError, "Invalid color") of CSS_IDENT_TOKEN: let s = tok.value if $s in colors: return colors[$s] else: raise newException(CSSValueError, "Invalid color") else: raise newException(CSSValueError, "Invalid color") elif d.value[0] of CSSFunction: let f = CSSFunction(d.value[0]) #TODO calc etc (cssnumber function or something) case $f.name of "rgb": if f.value.len != 3: raise newException(CSSValueError, "Invalid color") for c in f.value: if c != CSS_NUMBER_TOKEN: raise newException(CSSValueError, "Invalid color") 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: raise newException(CSSValueError, "Invalid color") for c in f.value: if c != CSS_NUMBER_TOKEN: raise newException(CSSValueError, "Invalid color") 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: discard raise newException(CSSValueError, "Invalid color") func cellColor*(color: CSSColor): CellColor = #TODO better would be to store color names and return term colors on demand #option) return CellColor(rgb: true, rgbcolor: (r: color.r, g: color.g, b: color.b)) 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(auto: true) else: return CSSLength(num: 0, unit: UNIT_EM) return CSSLength(num: 0, unit: UNIT_EM) #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 func isToken(d: CSSDeclaration): bool = d.value.len > 0 and d.value[0] of CSSToken func getToken(d: CSSDeclaration): CSSToken = (CSSToken)d.value[0] func cssGlobal(d: CSSDeclaration): CSSGlobalValueType = if isToken(d): let tok = getToken(d) if tok.tokenType == CSS_IDENT_TOKEN: case $tok.value of "inherit": return VALUE_INHERIT of "initial": return VALUE_INITIAL of "unset": return VALUE_UNSET of "revert": return VALUE_REVERT return VALUE_NOGLOBAL func cssString(d: CSSDeclaration): seq[Rune] = if isToken(d): let tok = getToken(d) case tok.tokenType of CSS_IDENT_TOKEN, CSS_STRING_TOKEN: return tok.value else: return func cssDisplay(d: CSSDeclaration): CSSDisplay = if isToken(d): let tok = getToken(d) if tok.tokenType == CSS_IDENT_TOKEN: case $tok.value of "block": return DISPLAY_BLOCK of "inline": return DISPLAY_INLINE of "inline-block": return DISPLAY_INLINE_BLOCK of "list-item": return DISPLAY_LIST_ITEM of "table": return DISPLAY_TABLE of "table-row-group": return DISPLAY_TABLE_ROW_GROUP of "table-header-group": return DISPLAY_TABLE_HEADER_GROUP of "table-footer-group": return DISPLAY_TABLE_FOOTER_GROUP of "table-column-group": return DISPLAY_TABLE_COLUMN_GROUP of "table-row": return DISPLAY_TABLE_ROW of "table-column": return DISPLAY_TABLE_COLUMN of "table-cell": return DISPLAY_TABLE_CELL of "none": return DISPLAY_NONE else: return DISPLAY_INLINE raise newException(CSSValueError, "Invalid display") func cssFontStyle(d: CSSDeclaration): CSSFontStyle = if isToken(d): let tok = getToken(d) if tok.tokenType == CSS_IDENT_TOKEN: case $tok.value of "normal": return FONTSTYLE_NORMAL of "italic": return FONTSTYLE_ITALIC of "oblique": return FONTSTYLE_OBLIQUE else: raise newException(CSSValueError, "Invalid font style") raise newException(CSSValueError, "Invalid font style") func cssWhiteSpace(d: CSSDeclaration): CSSWhitespace = if isToken(d): let tok = getToken(d) if tok.tokenType == CSS_IDENT_TOKEN: case $tok.value of "normal": return WHITESPACE_NORMAL of "nowrap": return WHITESPACE_NOWRAP of "pre": return WHITESPACE_PRE of "pre-line": return WHITESPACE_PRE_LINE of "pre-wrap": return WHITESPACE_PRE_WRAP else: return WHITESPACE_NORMAL raise newException(CSSValueError, "Invalid whitespace") #TODO func cssFontWeight(d: CSSDeclaration): int = if isToken(d): let tok = getToken(d) if tok.tokenType == CSS_IDENT_TOKEN: case $tok.value of "normal": return 400 of "bold": return 700 of "lighter": return 400 of "bolder": return 700 elif tok.tokenType == CSS_NUMBER_TOKEN: return int(tok.nvalue) raise newException(CSSValueError, "Invalid font weight") func cssTextDecoration(d: CSSDeclaration): CSSTextDecoration = if isToken(d): let tok = getToken(d) if tok.tokenType == CSS_IDENT_TOKEN: case $tok.value of "underline": return TEXT_DECORATION_UNDERLINE of "overline": return TEXT_DECORATION_OVERLINE of "line-through": return TEXT_DECORATION_LINE_THROUGH of "blink": return TEXT_DECORATION_BLINK raise newException(CSSValueError, "Invalid text decoration") func cssWordBreak(d: CSSDeclaration): CSSWordBreak = if isToken(d): let tok = getToken(d) if tok.tokenType == CSS_IDENT_TOKEN: case $tok.value of "normal": return WORD_BREAK_NORMAL of "break-all": return WORD_BREAK_BREAK_ALL of "keep-all": return WORD_BREAK_KEEP_ALL raise newException(CSSValueError, "Invalid text decoration") func getSpecifiedValue*(d: CSSDeclaration): CSSSpecifiedValue = let name = $d.name let ptype = propertyType(name) let vtype = valueType(ptype) result = CSSSpecifiedValue(t: ptype, v: vtype) try: case vtype of VALUE_COLOR: result.color = cssColor(d) of VALUE_LENGTH: result.length = cssLength(d) of VALUE_FONT_STYLE: result.fontstyle = cssFontStyle(d) of VALUE_DISPLAY: result.display = cssDisplay(d) of VALUE_CONTENT: result.content = cssString(d) of VALUE_WHITE_SPACE: result.whitespace = cssWhiteSpace(d) of VALUE_INTEGER: case ptype of PROPERTY_FONT_WEIGHT: result.integer = cssFontWeight(d) else: discard #??? of VALUE_TEXT_DECORATION: result.textdecoration = cssTextDecoration(d) of VALUE_WORD_BREAK: result.wordbreak = cssWordBreak(d) of VALUE_NONE: discard except CSSValueError: result.globalValue = VALUE_UNSET if result.globalValue == VALUE_NOGLOBAL: result.globalValue = cssGlobal(d) func getInitialColor*(t: CSSPropertyType): CSSColor = case t of PROPERTY_COLOR: return (0xffu8, 0xffu8, 0xffu8, 0x00u8) else: return (0u8, 0u8, 0u8, 255u8) func calcDefault(t: CSSPropertyType): CSSComputedValue = let v = valueType(t) var nv: CSSComputedValue case v of VALUE_COLOR: nv = CSSComputedValue(t: t, v: v, color: getInitialColor(t)) of VALUE_DISPLAY: nv = CSSComputedValue(t: t, v: v, display: DISPLAY_INLINE) of VALUE_WORD_BREAK: nv = CSSComputedValue(t: t, v: v, wordbreak: WORD_BREAK_NORMAL) else: nv = CSSComputedValue(t: t, v: v) return nv func getDefaultTable(): array[low(CSSPropertyType)..high(CSSPropertyType), CSSComputedValue] = for i in low(result)..high(result): result[i] = calcDefault(i) let defaultTable = getDefaultTable() func getDefault(t: CSSPropertyType): CSSComputedValue = {.cast(noSideEffect).}: assert defaultTable[t] != nil return defaultTable[t] func getComputedValue*(prop: CSSSpecifiedValue, current: CSSComputedValues): CSSComputedValue = case prop.globalValue of VALUE_INHERIT: if inherited(prop.t): return current[prop.t] of VALUE_INITIAL: return getDefault(prop.t) of VALUE_UNSET: if inherited(prop.t): return current[prop.t] return getDefault(prop.t) of VALUE_REVERT: #TODO discard of VALUE_NOGLOBAL: discard case prop.v of VALUE_COLOR: return CSSComputedValue(t: prop.t, v: VALUE_COLOR, color: prop.color) of VALUE_LENGTH: return CSSComputedValue(t: prop.t, v: VALUE_LENGTH, length: prop.length) of VALUE_DISPLAY: return CSSComputedValue(t: prop.t, v: VALUE_DISPLAY, display: prop.display) of VALUE_FONT_STYLE: return CSSComputedValue(t: prop.t, v: VALUE_FONT_STYLE, fontstyle: prop.fontstyle) of VALUE_CONTENT: return CSSComputedValue(t: prop.t, v: VALUE_CONTENT, content: prop.content) of VALUE_WHITESPACE: return CSSComputedValue(t: prop.t, v: VALUE_WHITESPACE, whitespace: prop.whitespace) of VALUE_INTEGER: return CSSComputedValue(t: prop.t, v: VALUE_INTEGER, integer: prop.integer) of VALUE_TEXT_DECORATION: return CSSComputedValue(t: prop.t, v: VALUE_TEXT_DECORATION, textdecoration: prop.textdecoration) of VALUE_WORD_BREAK: return CSSComputedValue(t: prop.t, v: VALUE_WORD_BREAK, wordbreak: prop.wordbreak) of VALUE_NONE: return CSSComputedValue(t: prop.t, v: VALUE_NONE) func getComputedValue*(d: CSSDeclaration, current: CSSComputedValues): CSSComputedValue = return getComputedValue(getSpecifiedValue(d), current) proc inheritProperties*(vals: var CSSComputedValues, parent: CSSComputedValues) = for prop in low(CSSPropertyType)..high(CSSPropertyType): if vals[prop] == nil: vals[prop] = getDefault(prop) if inherited(prop) and parent[prop] != nil and vals[prop] == getDefault(prop): vals[prop] = parent[prop] proc rootProperties*(vals: var CSSComputedValues) = for prop in low(CSSPropertyType)..high(CSSPropertyType): vals[prop] = getDefault(prop)