diff options
Diffstat (limited to 'src/css/cssvalues.nim')
-rw-r--r-- | src/css/cssvalues.nim | 1640 |
1 files changed, 1640 insertions, 0 deletions
diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim new file mode 100644 index 00000000..86d5630c --- /dev/null +++ b/src/css/cssvalues.nim @@ -0,0 +1,1640 @@ +import std/macros +import std/options +import std/strutils +import std/tables +import std/unicode + +import css/cssparser +import css/selectorparser +import img/bitmap +import layout/layoutunit +import types/color +import types/opt +import types/winattrs +import utils/twtstr + +export selectorparser.PseudoElem + +type + CSSShorthandType = enum + cstNone = "" + cstAll = "all" + cstMargin = "margin" + cstPadding = "padding" + cstBackground = "background" + cstListStyle = "list-style" + cstFlex = "flex" + cstFlexFlow = "flex-flow" + + CSSUnit* = enum + cuCm = "cm" + cuMm = "mm" + cuIn = "in" + cuPx = "px" + cuPt = "pt" + cuPc = "pc" + cuEm = "em" + cuEx = "ex" + cuCh = "ch" + cuRem = "rem" + cuVw = "vw" + cuVh = "vh" + cuVmin = "vmin" + cuVmax = "vmax" + cuPerc = "%" + cuIc = "ic" + + CSSPropertyType* = enum + cptNone = "" + cptColor = "color" + cptMarginTop = "margin-top" + cptMarginLeft = "margin-left" + cptMarginRight = "margin-right" + cptMarginBottom = "margin-bottom" + cptFontStyle = "font-style" + cptDisplay = "display" + cptContent = "content" + cptWhiteSpace = "white-space" + cptFontWeight = "font-weight" + cptTextDecoration = "text-decoration" + cptWordBreak = "word-break" + cptWidth = "width" + cptHeight = "height" + cptListStyleType = "list-style-type" + cptPaddingTop = "padding-top" + cptPaddingLeft = "padding-left" + cptPaddingRight = "padding-right" + cptPaddingBottom = "padding-bottom" + cptWordSpacing = "word-spacing" + cptVerticalAlign = "vertical-align" + cptLineHeight = "line-height" + cptTextAlign = "text-align" + cptListStylePosition = "list-style-position" + cptBackgroundColor = "background-color" + cptPosition = "position" + cptLeft = "left" + cptRight = "right" + cptTop = "top" + cptBottom = "bottom" + cptCaptionSide = "caption-side" + cptBorderSpacing = "border-spacing" + cptBorderCollapse = "border-collapse" + cptQuotes = "quotes" + cptCounterReset = "counter-reset" + cptMaxWidth = "max-width" + cptMaxHeight = "max-height" + cptMinWidth = "min-width" + cptMinHeight = "min-height" + cptBackgroundImage = "background-image" + cptChaColspan = "-cha-colspan" + cptChaRowspan = "-cha-rowspan" + cptFloat = "float" + cptVisibility = "visibility" + cptBoxSizing = "box-sizing" + cptClear = "clear" + cptTextTransform = "text-transform" + cptBgcolorIsCanvas = "-cha-bgcolor-is-canvas" + cptFlexDirection = "flex-direction" + cptFlexWrap = "flex-wrap" + cptFlexGrow = "flex-grow" + cptFlexShrink = "flex-shrink" + cptFlexBasis = "flex-basis" + + CSSValueType* = enum + cvtNone = "" + cvtLength = "length" + cvtColor = "color" + cvtContent = "content" + cvtDisplay = "display" + cvtFontStyle = "fontStyle" + cvtWhiteSpace = "whiteSpace" + cvtInteger = "integer" + cvtTextDecoration = "textDecoration" + cvtWordBreak = "wordBreak" + cvtListStyleType = "listStyleType" + cvtVerticalAlign = "verticalAlign" + cvtTextAlign = "textAlign" + cvtListStylePosition = "listStylePosition" + cvtPosition = "position" + cvtCaptionSide = "captionSide" + cvtLength2 = "length2" + cvtBorderCollapse = "borderCollapse" + cvtQuotes = "quotes" + cvtCounterReset = "counterReset" + cvtImage = "image" + cvtFloat = "float" + cvtVisibility = "visibility" + cvtBoxSizing = "boxSizing" + cvtClear = "clear" + cvtTextTransform = "textTransform" + cvtBgcolorIsCanvas = "bgcolorIsCanvas" + cvtFlexDirection = "flexDirection" + cvtFlexWrap = "flexWrap" + cvtNumber = "number" + + CSSGlobalType = enum + cgtNoglobal = "" + cgtInitial = "initial" + cgtInherit = "inherit" + cgtRevert = "revert" + cgtUnset = "unset" + + CSSDisplay* = enum + DisplayNone = "none" + DisplayInline = "inline" + DisplayBlock = "block" + DisplayListItem = "list-item" + DisplayInlineBlock = "inline-block" + DisplayTable = "table" + DisplayInlineTable = "inline-table" + DisplayTableRowGroup = "table-row-group" + DisplayTableHeaderGroup = "table-header-group" + DisplayTableFooterGroup = "table-footer-group" + DisplayTableColumnGroup = "table-column-group" + DisplayTableRow = "table-row" + DisplayTableColumn = "table-column" + DisplayTableCell = "table-cell" + DisplayTableCaption = "table-caption" + DisplayFlowRoot = "flow-root" + DisplayFlex = "flex" + DisplayInlineFlex = "inline-flex" + + CSSWhiteSpace* = enum + WhitespaceNormal = "normal" + WhitespaceNowrap = "nowrap" + WhitespacePre = "pre" + WhitespacePreLine = "pre-line" + WhitespacePreWrap = "pre-wrap" + + CSSFontStyle* = enum + FontStyleNormal = "normal" + FontStyleItalic = "italic" + FontStyleOblique = "oblique" + + CSSPosition* = enum + PositionStatic = "static" + PositionRelative = "relative" + PositionAbsolute = "absolute" + PositionFixed = "fixed" + PositionSticky = "sticky" + + CSSTextDecoration* = enum + TextDecorationNone = "none" + TextDecorationUnderline = "underline" + TextDecorationOverline = "overline" + TextDecorationLineThrough = "line-through" + TextDecorationBlink = "blink" + + CSSWordBreak* = enum + WordBreakNormal = "normal" + WordBreakBreakAll = "break-all" + WordBreakKeepAll = "keep-all" + + CSSListStyleType* = enum + ListStyleTypeNone = "none" + ListStyleTypeDisc = "disc" + ListStyleTypeCircle = "circle" + ListStyleTypeSquare = "square" + ListStyleTypeDecimal = "decimal" + ListStyleTypeDisclosureClosed = "disclosure-closed" + ListStyleTypeDisclosureOpen = "disclosure-open" + ListStyleTypeCjkEarthlyBranch = "cjk-earthly-branch" + ListStyleTypeCjkHeavenlyStem = "cjk-heavenly-stem" + ListStyleTypeLowerRoman = "lower-roman" + ListStyleTypeUpperRoman = "upper-roman" + ListStyleTypeLowerAlpha = "lower-alpha" + ListStyleTypeUpperAlpha = "upper-alpha" + ListStyleTypeLowerGreek = "lower-greek" + ListStyleTypeHiragana = "hiragana" + ListStyleTypeHiraganaIroha = "hiragana-iroha" + ListStyleTypeKatakana = "katakana" + ListStyleTypeKatakanaIroha = "katakana-iroha" + ListStyleTypeJapaneseInformal = "japanese-informal" + + CSSVerticalAlign2* = enum + VerticalAlignBaseline = "baseline" + VerticalAlignSub = "sub" + VerticalAlignSuper = "super" + VerticalAlignTextTop = "text-top" + VerticalAlignTextBottom = "text-bottom" + VerticalAlignMiddle = "middle" + VerticalAlignTop = "top" + VerticalAlignBottom = "bottom" + + CSSTextAlign* = enum + TextAlignStart = "start" + TextAlignEnd = "end" + TextAlignLeft = "left" + TextAlignRight = "right" + TextAlignCenter = "center" + TextAlignJustify = "justify" + TextAlignChaCenter = "-cha-center" + TextAlignChaLeft = "-cha-left" + TextAlignChaRight = "-cha-right" + + CSSListStylePosition* = enum + ListStylePositionOutside = "outside" + ListStylePositionInside = "inside" + + CSSCaptionSide* = enum + CaptionSideTop = "top" + CaptionSideBottom = "bottom" + CaptionSideBlockStart = "block-start" + CaptionSideBlockEnd = "block-end" + + CSSBorderCollapse* = enum + BorderCollapseSeparate = "separate" + BorderCollapseCollapse = "collapse" + + CSSContentType* = enum + ContentString, ContentOpenQuote, ContentCloseQuote, + ContentNoOpenQuote, ContentNoCloseQuote, ContentImage, + ContentVideo, ContentAudio, ContentNewline + + CSSFloat* = enum + FloatNone = "none" + FloatLeft = "left" + FloatRight = "right" + + CSSVisibility* = enum + VisibilityVisible = "visible" + VisibilityHidden = "hidden" + VisibilityCollapse = "collapse" + + CSSBoxSizing* = enum + BoxSizingContentBox = "content-box" + BoxSizingBorderBox = "border-box" + + CSSClear* = enum + ClearNone = "none" + ClearLeft = "left" + ClearRight = "right" + ClearBoth = "both" + ClearInlineStart = "inline-start" + ClearInlineEnd = "inline-end" + + CSSTextTransform* = enum + TextTransformNone = "none" + TextTransformCapitalize = "capitalize" + TextTransformUppercase = "uppercase" + TextTransformLowercase = "lowercase" + TextTransformFullWidth = "full-width" + TextTransformFullSizeKana = "full-size-kana" + TextTransformChaHalfWidth = "-cha-half-width" + + CSSFlexDirection* = enum + FlexDirectionRow = "row" + FlexDirectionRowReverse = "row-reverse" + FlexDirectionColumn = "column" + FlexDirectionColumnReverse = "column-reverse" + + CSSFlexWrap* = enum + FlexWrapNowrap = "nowrap" + FlexWrapWrap = "wrap" + FlexWrapWrapReverse = "wrap-reverse" + +type + CSSLength* = object + num*: float64 + unit*: CSSUnit + auto*: bool + + CSSVerticalAlign* = object + length*: CSSLength + keyword*: CSSVerticalAlign2 + + CSSContent* = object + t*: CSSContentType + s*: string + bmp*: Bitmap + + CSSQuotes* = object + auto*: bool + qs*: seq[tuple[s, e: string]] + + CSSCounterReset* = object + name*: string + num*: int + + CSSComputedValue* = ref object + case v*: CSSValueType + of cvtColor: + color*: CellColor + of cvtLength: + length*: CSSLength + of cvtFontStyle: + fontStyle*: CSSFontStyle + of cvtDisplay: + display*: CSSDisplay + of cvtContent: + content*: seq[CSSContent] + of cvtQuotes: + quotes*: CSSQuotes + of cvtWhiteSpace: + whiteSpace*: CSSWhiteSpace + of cvtInteger: + integer*: int + of cvtNumber: + number*: float64 + of cvtTextDecoration: + textDecoration*: set[CSSTextDecoration] + of cvtWordBreak: + wordBreak*: CSSWordBreak + of cvtListStyleType: + listStyleType*: CSSListStyleType + of cvtVerticalAlign: + verticalAlign*: CSSVerticalAlign + of cvtTextAlign: + textAlign*: CSSTextAlign + of cvtListStylePosition: + listStylePosition*: CSSListStylePosition + of cvtPosition: + position*: CSSPosition + of cvtCaptionSide: + captionSide*: CSSCaptionSide + of cvtLength2: + length2*: tuple[a, b: CSSLength] + of cvtBorderCollapse: + borderCollapse*: CSSBorderCollapse + of cvtCounterReset: + counterReset*: seq[CSSCounterReset] + of cvtImage: + image*: CSSContent + of cvtFloat: + float*: CSSFloat + of cvtVisibility: + visibility*: CSSVisibility + of cvtBoxSizing: + boxSizing*: CSSBoxSizing + of cvtClear: + clear*: CSSClear + of cvtTextTransform: + textTransform*: CSSTextTransform + of cvtBgcolorIsCanvas: + bgcolorIsCanvas*: bool + of cvtFlexDirection: + flexDirection*: CSSFlexDirection + of cvtFlexWrap: + flexWrap*: CSSFlexWrap + of cvtNone: discard + + CSSComputedValues* = ref array[CSSPropertyType, CSSComputedValue] + + CSSOrigin* = enum + coUserAgent + coUser + coAuthor + + CSSComputedEntry* = tuple + t: CSSPropertyType + val: CSSComputedValue + global: CSSGlobalType + + CSSComputedEntries = seq[CSSComputedEntry] + + CSSComputedValuesBuilder* = object + parent*: CSSComputedValues + normalProperties: array[CSSOrigin, CSSComputedEntries] + importantProperties: array[CSSOrigin, CSSComputedEntries] + preshints*: CSSComputedValues + +const ShorthandNames = block: + var tab = initTable[string, CSSShorthandType]() + for t in CSSShorthandType: + if $t != "": + tab[$t] = t + tab + +const PropertyNames = block: + var tab = initTable[string, CSSPropertyType]() + for t in CSSPropertyType: + if $t != "": + tab[$t] = t + tab + +const ValueTypes = [ + cptNone: cvtNone, + cptColor: cvtColor, + cptMarginTop: cvtLength, + cptMarginLeft: cvtLength, + cptMarginRight: cvtLength, + cptMarginBottom: cvtLength, + cptFontStyle: cvtFontStyle, + cptDisplay: cvtDisplay, + cptContent: cvtContent, + cptWhiteSpace: cvtWhiteSpace, + cptFontWeight: cvtInteger, + cptTextDecoration: cvtTextDecoration, + cptWordBreak: cvtWordBreak, + cptWidth: cvtLength, + cptHeight: cvtLength, + cptListStyleType: cvtListStyleType, + cptPaddingTop: cvtLength, + cptPaddingLeft: cvtLength, + cptPaddingRight: cvtLength, + cptPaddingBottom: cvtLength, + cptWordSpacing: cvtLength, + cptVerticalAlign: cvtVerticalAlign, + cptLineHeight: cvtLength, + cptTextAlign: cvtTextAlign, + cptListStylePosition: cvtListStylePosition, + cptBackgroundColor: cvtColor, + cptPosition: cvtPosition, + cptLeft: cvtLength, + cptRight: cvtLength, + cptTop: cvtLength, + cptBottom: cvtLength, + cptCaptionSide: cvtCaptionSide, + cptBorderSpacing: cvtLength2, + cptBorderCollapse: cvtBorderCollapse, + cptQuotes: cvtQuotes, + cptCounterReset: cvtCounterReset, + cptMaxWidth: cvtLength, + cptMaxHeight: cvtLength, + cptMinWidth: cvtLength, + cptMinHeight: cvtLength, + cptBackgroundImage: cvtImage, + cptChaColspan: cvtInteger, + cptChaRowspan: cvtInteger, + cptFloat: cvtFloat, + cptVisibility: cvtVisibility, + cptBoxSizing: cvtBoxSizing, + cptClear: cvtClear, + cptTextTransform: cvtTextTransform, + cptBgcolorIsCanvas: cvtBgcolorIsCanvas, + cptFlexDirection: cvtFlexDirection, + cptFlexWrap: cvtFlexWrap, + cptFlexGrow: cvtNumber, + cptFlexShrink: cvtNumber, + cptFlexBasis: cvtLength +] + +const InheritedProperties = { + cptColor, cptFontStyle, cptWhiteSpace, cptFontWeight, cptTextDecoration, + cptWordBreak, cptListStyleType, cptWordSpacing, cptLineHeight, cptTextAlign, + cptListStylePosition, cptCaptionSide, cptBorderSpacing, cptBorderCollapse, + cptQuotes, cptVisibility, cptTextTransform +} + +func getPropInheritedArray(): array[CSSPropertyType, bool] = + for prop in CSSPropertyType: + if prop in InheritedProperties: + result[prop] = true + else: + result[prop] = false + +const InheritedArray = getPropInheritedArray() + +func shorthandType(s: string): CSSShorthandType = + return ShorthandNames.getOrDefault(s.toLowerAscii(), cstNone) + +func propertyType(s: string): CSSPropertyType = + return PropertyNames.getOrDefault(s.toLowerAscii(), cptNone) + +func valueType(prop: CSSPropertyType): CSSValueType = + return ValueTypes[prop] + +func isSupportedProperty*(s: string): bool = + return s in PropertyNames + +func `$`*(length: CSSLength): string = + if length.auto: + return "auto" + return $length.num & $length.unit + +func `$`*(content: CSSContent): string = + if content.s != "": + return "url(" & content.s & ")" + return "none" + +func `$`(quotes: CSSQuotes): string = + if quotes.auto: + return "auto" + return "auto" #TODO + +func `$`(counterreset: seq[CSSCounterReset]): string = + result = "" + for it in counterreset: + result &= $it.name + result &= ' ' + result &= $it.num + +func `$`*(val: CSSComputedValue): string = + case val.v + of cvtNone: return "none" + of cvtColor: return $val.color + of cvtImage: return $val.image + of cvtLength: return $val.length + of cvtDisplay: return $val.display + of cvtFontStyle: return $val.fontStyle + of cvtWhiteSpace: return $val.whiteSpace + of cvtInteger: return $val.integer + of cvtTextDecoration: return $val.textDecoration + of cvtWordBreak: return $val.wordBreak + of cvtListStyleType: return $val.listStyleType + of cvtVerticalAlign: return $val.verticalAlign + of cvtTextAlign: return $val.textAlign + of cvtListStylePosition: return $val.listStylePosition + of cvtPosition: return $val.position + of cvtCaptionSide: return $val.captionSide + of cvtLength2: return $val.length2.a & " " & $val.length2.b + of cvtBorderCollapse: return $val.borderCollapse + of cvtContent: return $val.content + of cvtQuotes: return $val.quotes + of cvtCounterReset: return $val.counterReset + of cvtFloat: return $val.float + of cvtVisibility: return $val.visibility + of cvtBoxSizing: return $val.boxSizing + of cvtClear: return $val.clear + of cvtTextTransform: return $val.textTransform + of cvtBgcolorIsCanvas: return $val.bgcolorIsCanvas + of cvtFlexDirection: return $val.flexDirection + of cvtFlexWrap: return $val.flexWrap + of cvtNumber: return $val.number + +macro `{}`*(vals: CSSComputedValues; s: static string): untyped = + let t = propertyType(s) + let vs = ident($valueType(t)) + return quote do: + `vals`[CSSPropertyType(`t`)].`vs` + +macro `{}=`*(vals: CSSComputedValues; s: static string, val: typed) = + let t = propertyType(s) + let v = valueType(t) + let vs = ident($v) + return quote do: + `vals`[CSSPropertyType(`t`)] = CSSComputedValue( + v: CSSValueType(`v`), + `vs`: `val` + ) + +func inherited(t: CSSPropertyType): bool = + return InheritedArray[t] + +func em_to_px(em: float64; window: WindowAttributes): LayoutUnit = + em * float64(window.ppl) + +func ch_to_px(ch: float64; window: WindowAttributes): LayoutUnit = + ch * float64(window.ppc) + +# 水 width, we assume it's 2 chars +func ic_to_px(ic: float64; window: WindowAttributes): LayoutUnit = + ic * float64(window.ppc) * 2 + +# x-letter height, we assume it's em/2 +func ex_to_px(ex: float64; window: WindowAttributes): LayoutUnit = + ex * float64(window.ppc) / 2 + +func px*(l: CSSLength; window: WindowAttributes; p: LayoutUnit): LayoutUnit + {.inline.} = + case l.unit + of cuEm, cuRem: em_to_px(l.num, window) + of cuCh: ch_to_px(l.num, window) + of cuIc: ic_to_px(l.num, window) + of cuEx: ex_to_px(l.num, window) + of cuPerc: toLayoutUnit(toFloat64(p) * l.num / 100) + of cuPx: toLayoutUnit(l.num) + of cuCm: toLayoutUnit(l.num * 37.8) + of cuMm: toLayoutUnit(l.num * 3.78) + of cuIn: toLayoutUnit(l.num * 96) + of cuPc: toLayoutUnit(l.num * 16) + of cuPt: toLayoutUnit(l.num * 4 / 3) + of cuVw: toLayoutUnit(float64(window.width_px) * l.num / 100) + of cuVh: toLayoutUnit(float64(window.height_px) * l.num / 100) + of cuVmin: + toLayoutUnit(min(window.width_px, window.width_px) / 100 * l.num) + of cuVmax: + toLayoutUnit(max(window.width_px, window.width_px) / 100 * l.num) + +func blockify*(display: CSSDisplay): CSSDisplay = + case display + of DisplayBlock, DisplayTable, DisplayListItem, DisplayNone, DisplayFlowRoot, + DisplayFlex: + #TODO grid + return display + of DisplayInline, DisplayInlineBlock, DisplayTableRow, + DisplayTableRowGroup, DisplayTableColumn, + DisplayTableColumnGroup, DisplayTableCell, DisplayTableCaption, + DisplayTableHeaderGroup, DisplayTableFooterGroup: + return DisplayBlock + of DisplayInlineTable: + return DisplayTable + of DisplayInlineFlex: + return DisplayFlex + +const UpperAlphaMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toRunes() +const LowerAlphaMap = "abcdefghijklmnopqrstuvwxyz".toRunes() +const LowerGreekMap = "αβγδεζηθικλμνξοπρστυφχψω".toRunes() +const HiraganaMap = ("あいうえおかきくけこさしすせそたちつてとなにぬねの" & + "はひふへほまみむめもやゆよらりるれろわゐゑをん").toRunes() +const HiraganaIrohaMap = ("いろはにほへとちりぬるをわかよたれそつねならむ" & + "うゐのおくやまけふこえてあさきゆめみしゑひもせす").toRunes() +const KatakanaMap = ("アイウエオカキクケコサシスセソタチツテトナニヌネノ" & + "ハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン").toRunes() +const KatakanaIrohaMap = ("イロハニホヘトチリヌルヲワカヨタレソツネナラム" & + "ウヰノオクヤマケフコエテアサキユメミシヱヒモセス").toRunes() +const EarthlyBranchMap = "子丑寅卯辰巳午未申酉戌亥".toRunes() +const HeavenlyStemMap = "甲乙丙丁戊己庚辛壬癸".toRunes() + +func numToBase(n: int; map: openArray[Rune]): string = + if n <= 0: + return $n + var tmp: seq[Rune] + var n = n + while n != 0: + n -= 1 + tmp &= map[n mod map.len] + n = n div map.len + result = "" + for i in countdown(tmp.high, 0): + result &= $tmp[i] + +func numToFixed(n: int; map: openArray[Rune]): string = + let n = n - 1 + if n notin 0 .. map.high: + return $n + return $map[n] + +func numberAdditive(i: int; range: HSlice[int, int]; + symbols: openArray[(int, string)]): string = + if i notin range: + return $i + var n = i + var at = 0 + while n > 0: + if n >= symbols[at][0]: + n -= symbols[at][0] + result &= symbols[at][1] + continue + inc at + return result + +const romanNumbers = [ + (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"), + (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I") +] + +const romanNumbersLower = block: + var res: seq[(int, string)] + for (n, s) in romanNumbers: + res.add((n, s.toLowerAscii())) + res + +func romanNumber(i: int): string = + return numberAdditive(i, 1..3999, romanNumbers) + +func romanNumberLower(i: int): string = + return numberAdditive(i, 1..3999, romanNumbersLower) + +func japaneseNumber(i: int): string = + if i == 0: + return "〇" + var n = i + if i < 0: + result &= "マイナス" + n *= -1 + let o = n + var ss: seq[string] = @[] + var d = 0 + while n > 0: + let m = n mod 10 + if m != 0: + case d + of 1: ss.add("十") + of 2: ss.add("百") + of 3: ss.add("千") + of 4: + ss.add("万") + ss.add("一") + of 5: + ss.add("万") + ss.add("十") + of 6: + ss.add("万") + ss.add("百") + of 7: + ss.add("万") + ss.add("千") + ss.add("一") + of 8: + ss.add("億") + ss.add("一") + of 9: + ss.add("億") + ss.add("十") + else: discard + case m + of 0: + inc d + n = n div 10 + of 1: + if o == n: + ss.add("一") + of 2: ss.add("二") + of 3: ss.add("三") + of 4: ss.add("四") + of 5: ss.add("五") + of 6: ss.add("六") + of 7: ss.add("七") + of 8: ss.add("八") + of 9: ss.add("九") + else: discard + n -= m + n = ss.len - 1 + while n >= 0: + result &= ss[n] + dec n + +func listMarker*(t: CSSListStyleType; i: int): string = + case t + of ListStyleTypeNone: return "" + of ListStyleTypeDisc: return "• " # U+2022 + of ListStyleTypeCircle: return "○ " # U+25CB + of ListStyleTypeSquare: return "□ " # U+25A1 + of ListStyleTypeDisclosureOpen: return "▶ " # U+25B6 + of ListStyleTypeDisclosureClosed: return "▼ " # U+25BC + of ListStyleTypeDecimal: return $i & ". " + of ListStyleTypeUpperRoman: return romanNumber(i) & ". " + of ListStyleTypeLowerRoman: return romanNumberLower(i) & ". " + of ListStyleTypeUpperAlpha: return numToBase(i, UpperAlphaMap) & ". " + of ListStyleTypeLowerAlpha: return numToBase(i, LowerAlphaMap) & ". " + of ListStyleTypeLowerGreek: return numToBase(i, LowerGreekMap) & ". " + of ListStyleTypeHiragana: return numToBase(i, HiraganaMap) & "、" + of ListStyleTypeHiraganaIroha: + return numToBase(i, HiraganaIrohaMap) & "、" + of ListStyleTypeKatakana: return numToBase(i, KatakanaMap) & "、" + of ListStyleTypeKatakanaIroha: + return numToBase(i, KatakanaIrohaMap) & "、" + of ListStyleTypeCjkEarthlyBranch: + return numToFixed(i, EarthlyBranchMap) & "、" + of ListStyleTypeCjkHeavenlyStem: + return numToFixed(i, HeavenlyStemMap) & "、" + of ListStyleTypeJapaneseInformal: return japaneseNumber(i) & "、" + +#TODO this should change by language +func quoteStart*(level: int): string = + if level == 0: + return "“" + return "‘" + +func quoteEnd*(level: int): string = + if level == 0: + return "“" + return "‘" + +const Colors: Table[string, ARGBColor] = ((func (): Table[string, ARGBColor] = + for name, rgb in ColorsRGB: + result[name] = rgb + result["transparent"] = rgba(0x00, 0x00, 0x00, 0x00) +)()) + +func isToken(cval: CSSComponentValue): bool {.inline.} = + cval of CSSToken + +func getToken(cval: CSSComponentValue): CSSToken {.inline.} = + CSSToken(cval) + +func parseIdent0[T](map: static openArray[(string, T)]; s: string): Opt[T] = + # cmp when len is small enough, otherwise lowercase & hashmap + when map.len <= 4: + for (k, v) in map: + if k.equalsIgnoreCase(s): + return ok(v) + else: + const MapTable = map.toTable() + let val = s.toLowerAscii() + if val in MapTable: + return ok(MapTable[val]) + return err() + +func parseIdent[T](map: static openArray[(string, T)]; cval: CSSComponentValue): + Opt[T] = + if isToken(cval): + let tok = getToken(cval) + if tok.tokenType == cttIdent: + return parseIdent0[T](map, tok.value) + return err() + +func getIdentMap[T: enum](e: typedesc[T]): seq[(string, T)] = + result = @[] + for e in T.low .. T.high: + result.add(($e, e)) + +func parseIdent[T: enum](cval: CSSComponentValue): Opt[T] = + const IdentMap = getIdentMap(T) + return IdentMap.parseIdent(cval) + +func cssLength(val: float64; unit: string): Opt[CSSLength] = + const UnitMap = getIdentMap(CSSUnit) + let u = ?UnitMap.parseIdent0(unit) + return ok(CSSLength(num: val, unit: u)) + +const CSSLengthAuto* = CSSLength(auto: true) + +func parseDimensionValues*(s: string): Option[CSSLength] = + if s == "": return + var i = 0 + while s[i] in AsciiWhitespace: inc i + if i >= s.len or s[i] notin AsciiDigit: return + var n: float64 + while s[i] in AsciiDigit: + n *= 10 + n += float64(decValue(s[i])) + inc i + if i >= s.len: return some(CSSLength(num: n, unit: cuPx)) + if s[i] == '.': + inc i + if i >= s.len: return some(CSSLength(num: n, unit: cuPx)) + var d = 1 + while i < s.len and s[i] in AsciiDigit: + n += float64(decValue(s[i])) / float64(d) + inc d + inc i + if i >= s.len: return some(CSSLength(num: n, unit: cuPx)) + if s[i] == '%': return some(CSSLength(num: n, unit: cuPerc)) + return some(CSSLength(num: n, unit: cuPx)) + +func skipWhitespace(vals: openArray[CSSComponentValue]; i: var int) = + while i < vals.len: + if vals[i] != cttWhitespace: + break + inc i + +func parseARGB(value: openArray[CSSComponentValue]): Opt[CellColor] = + var i = 0 + var commaMode = false + template check_err(slash: bool) = + #TODO calc, percentages, etc (cssnumber function or something) + if not slash and i >= value.len or i < value.len and + value[i] != cttNumber: + return err() + template next_value(first = false, slash = false) = + inc i + value.skipWhitespace(i) + if i < value.len: + if value[i] == cttComma and (commaMode or first): + # legacy compatibility + inc i + value.skipWhitespace(i) + commaMode = true + elif commaMode: + return err() + elif slash: + let tok = value[i] + if tok != cttDelim or CSSToken(tok).cvalue != '/': + return err() + inc i + value.skipWhitespace(i) + check_err slash + value.skipWhitespace(i) + check_err false + let r = CSSToken(value[i]).nvalue + next_value true + let g = CSSToken(value[i]).nvalue + next_value + let b = CSSToken(value[i]).nvalue + next_value false, true + let a = if i < value.len: + CSSToken(value[i]).nvalue + else: + 1 + value.skipWhitespace(i) + if i < value.len: + return err() + return ok(rgba(int(r), int(g), int(b), int(a * 255)).cellColor()) + +# syntax: -cha-ansi( number | ident ) +# where number is an ANSI color (0..255) +# and ident is in NameTable and may start with "bright-" +func parseANSI(value: openArray[CSSComponentValue]): Opt[CellColor] = + var i = 0 + value.skipWhitespace(i) + if i != value.high or not (value[i] of CSSToken): # only 1 param is valid + #TODO numeric functions + return err() + let tok = CSSToken(value[i]) + if tok.tokenType == cttNumber: + if tok.nvalue notin 0..255: + return err() # invalid numeric ANSI color + return ok(ANSIColor(tok.nvalue).cellColor()) + elif tok.tokenType == cttIdent: + var name = tok.value + if name.equalsIgnoreCase("default"): + return ok(defaultColor) + var bright = false + if name.startsWithIgnoreCase("bright-"): + bright = true + name = name.substr("bright-".len) + const NameTable = [ + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white" + ] + for i, it in NameTable: + if it.equalsIgnoreCase(name): + var i = int(i) + if bright: + i += 8 + return ok(ANSIColor(i).cellColor()) + return err() + +func cssColor*(val: CSSComponentValue): Opt[CellColor] = + if val of CSSToken: + let tok = CSSToken(val) + case tok.tokenType + of cttHash: + let c = parseHexColor(tok.value) + if c.isSome: + return ok(c.get.cellColor()) + of cttIdent: + let s = tok.value.toLowerAscii() + if s in Colors: + return ok(Colors[s].cellColor()) + else: discard + elif val of CSSFunction: + let f = CSSFunction(val) + if f.name.equalsIgnoreCase("rgb") or f.name.equalsIgnoreCase("rgba"): + return parseARGB(f.value) + elif f.name.equalsIgnoreCase("-cha-ansi"): + return parseANSI(f.value) + return err() + +func cssLength*(val: CSSComponentValue; has_auto = true; allow_negative = true): + Opt[CSSLength] = + if val of CSSToken: + let tok = CSSToken(val) + case tok.tokenType + of cttNumber: + if tok.nvalue == 0: + return ok(CSSLength(num: 0, unit: cuPx)) + of cttPercentage: + if not allow_negative: + if tok.nvalue < 0: + return err() + return cssLength(tok.nvalue, "%") + of cttDimension: + if not allow_negative: + if tok.nvalue < 0: + return err() + return cssLength(tok.nvalue, tok.unit) + of cttIdent: + if has_auto: + if tok.value.equalsIgnoreCase("auto"): + return ok(CSSLengthAuto) + else: discard + return err() + +func cssAbsoluteLength(val: CSSComponentValue): Opt[CSSLength] = + if val of CSSToken: + let tok = CSSToken(val) + case tok.tokenType + of cttNumber: + if tok.nvalue == 0: + return ok(CSSLength(num: 0, unit: cuPx)) + of cttDimension: + if tok.nvalue >= 0: + return cssLength(tok.nvalue, tok.unit) + else: discard + return err() + +func cssWordSpacing(cval: CSSComponentValue): Opt[CSSLength] = + if cval of CSSToken: + let tok = CSSToken(cval) + case tok.tokenType + of cttDimension: + return cssLength(tok.nvalue, tok.unit) + of cttIdent: + if tok.value.equalsIgnoreCase("normal"): + return ok(CSSLengthAuto) + else: discard + return err() + +func cssGlobal(cval: CSSComponentValue): CSSGlobalType = + return parseIdent[CSSGlobalType](cval).get(cgtNoglobal) + +func cssQuotes(cvals: openArray[CSSComponentValue]): Opt[CSSQuotes] = + template die = + return err() + if cvals.len == 0: + die + var res: CSSQuotes + var sa = false + var pair: tuple[s, e: string] + for cval in cvals: + if res.auto: die + if isToken(cval): + let tok = getToken(cval) + case tok.tokenType + of cttIdent: + if res.qs.len > 0: die + if tok.value.equalsIgnoreCase("auto"): + res.auto = true + elif tok.value.equalsIgnoreCase("none"): + if cvals.len != 1: + die + die + of cttString: + if sa: + pair.e = tok.value + res.qs.add(pair) + sa = false + else: + pair.s = tok.value + sa = true + of cttWhitespace: discard + else: die + if sa: + die + return ok(res) + +func cssContent(cvals: openArray[CSSComponentValue]): seq[CSSContent] = + for cval in cvals: + if isToken(cval): + let tok = getToken(cval) + case tok.tokenType + of cttIdent: + if tok.value == "/": + break + elif tok.value.equalsIgnoreCase("open-quote"): + result.add(CSSContent(t: ContentOpenQuote)) + elif tok.value.equalsIgnoreCase("no-open-quote"): + result.add(CSSContent(t: ContentNoOpenQuote)) + elif tok.value.equalsIgnoreCase("close-quote"): + result.add(CSSContent(t: ContentCloseQuote)) + elif tok.value.equalsIgnoreCase("no-close-quote"): + result.add(CSSContent(t: ContentNoCloseQuote)) + of cttString: + result.add(CSSContent(t: ContentString, s: tok.value)) + else: return + +func cssFontWeight(cval: CSSComponentValue): Opt[int] = + if isToken(cval): + let tok = getToken(cval) + if tok.tokenType == cttIdent: + const FontWeightMap = { + "normal": 400, + "bold": 700, + "lighter": 400, + "bolder": 700 + } + return FontWeightMap.parseIdent(cval) + elif tok.tokenType == cttNumber: + if tok.nvalue in 1f64..1000f64: + return ok(int(tok.nvalue)) + return err() + +func cssTextDecoration(cvals: openArray[CSSComponentValue]): + Opt[set[CSSTextDecoration]] = + var s: set[CSSTextDecoration] = {} + for cval in cvals: + if not isToken(cval): + continue + let tok = getToken(cval) + if tok.tokenType == cttIdent: + let td = ?parseIdent[CSSTextDecoration](tok) + if td == TextDecorationNone: + if cvals.len != 1: + return err() + return ok(s) + s.incl(td) + return ok(s) + +func cssVerticalAlign(cval: CSSComponentValue): Opt[CSSVerticalAlign] = + if isToken(cval): + let tok = getToken(cval) + if tok.tokenType == cttIdent: + let va2 = ?parseIdent[CSSVerticalAlign2](cval) + return ok(CSSVerticalAlign(keyword: va2)) + else: + return ok(CSSVerticalAlign( + keyword: VerticalAlignBaseline, + length: ?cssLength(tok, has_auto = false) + )) + return err() + +func cssLineHeight(cval: CSSComponentValue): Opt[CSSLength] = + if cval of CSSToken: + let tok = CSSToken(cval) + case tok.tokenType + of cttNumber: + return cssLength(tok.nvalue * 100, "%") + of cttIdent: + if tok.value == "normal": + return ok(CSSLengthAuto) + else: + return cssLength(tok, has_auto = false) + return err() + +func cssCounterReset(cvals: openArray[CSSComponentValue]): + Opt[seq[CSSCounterReset]] = + template die = + return err() + var r = CSSCounterReset() + var s = false + var res: seq[CSSCounterReset] = @[] + for cval in cvals: + if isToken(cval): + let tok = getToken(cval) + case tok.tokenType + of cttWhitespace: discard + of cttIdent: + if s: + die + r.name = tok.value + s = true + of cttNumber: + if not s: + die + r.num = int(tok.nvalue) + res.add(r) + s = false + else: + die + return ok(res) + +func cssMaxMinSize(cval: CSSComponentValue): Opt[CSSLength] = + if isToken(cval): + let tok = getToken(cval) + case tok.tokenType + of cttIdent: + if tok.value.equalsIgnoreCase("none"): + return ok(CSSLengthAuto) + of cttNumber, cttDimension, cttPercentage: + return cssLength(tok, allow_negative = false) + else: discard + return err() + +#TODO should be URL (parsed with baseurl of document...) +func cssURL(cval: CSSComponentValue): Option[string] = + if isToken(cval): + let tok = getToken(cval) + if tok == cttUrl: + return some(tok.value) + elif cval of CSSFunction: + let fun = CSSFunction(cval) + if fun.name.equalsIgnoreCase("url") or fun.name.equalsIgnoreCase("src"): + for x in fun.value: + if not isToken(x): + break + let x = getToken(x) + if x == cttWhitespace: + discard + elif x == cttString: + return some(x.value) + else: + break + +#TODO this should be bg-image, add gradient, etc etc +func cssImage(cval: CSSComponentValue): Opt[CSSContent] = + if isToken(cval): + #TODO bg-image only + let tok = getToken(cval) + if tok.tokenType == cttIdent and tok.value == "none": + return ok(CSSContent(t: ContentImage, s: "")) + let url = cssURL(cval) + if url.isSome: + return ok(CSSContent(t: ContentImage, s: url.get)) + return err() + +func cssInteger(cval: CSSComponentValue; range: Slice[int]): Opt[int] = + if isToken(cval): + let tok = getToken(cval) + if tok.tokenType == cttNumber: + if tok.nvalue in float64(range.a)..float64(range.b): + return ok(int(tok.nvalue)) + return err() + +func cssNumber(cval: CSSComponentValue; positive: bool): Opt[float64] = + if isToken(cval): + let tok = getToken(cval) + if tok.tokenType == cttNumber: + if not positive or tok.nvalue >= 0: + return ok(tok.nvalue) + return err() + +proc parseValue(cvals: openArray[CSSComponentValue]; t: CSSPropertyType): + Opt[CSSComputedValue] = + var i = 0 + cvals.skipWhitespace(i) + if i >= cvals.len: + return err() + let cval = cvals[i] + inc i + let v = valueType(t) + template return_new(prop, val: untyped) = + return ok(CSSComputedValue(v: v, prop: val)) + case v + of cvtColor: return_new color, ?cssColor(cval) + of cvtLength: + case t + of cptWordSpacing: + return_new length, ?cssWordSpacing(cval) + of cptLineHeight: + return_new length, ?cssLineHeight(cval) + of cptMaxWidth, cptMaxHeight, cptMinWidth, cptMinHeight: + return_new length, ?cssMaxMinSize(cval) + of cptPaddingLeft, cptPaddingRight, cptPaddingTop, cptPaddingBottom: + return_new length, ?cssLength(cval, has_auto = false) + #TODO content for flex-basis + else: + return_new length, ?cssLength(cval) + of cvtFontStyle: return_new fontStyle, ?parseIdent[CSSFontStyle](cval) + of cvtDisplay: return_new display, ?parseIdent[CSSDisplay](cval) + of cvtContent: return_new content, cssContent(cvals) + of cvtWhiteSpace: return_new whiteSpace, ?parseIdent[CSSWhiteSpace](cval) + of cvtInteger: + case t + of cptFontWeight: return_new integer, ?cssFontWeight(cval) + of cptChaColspan: return_new integer, ?cssInteger(cval, 1 .. 1000) + of cptChaRowspan: return_new integer, ?cssInteger(cval, 0 .. 65534) + else: assert false + of cvtTextDecoration: return_new textdecoration, ?cssTextDecoration(cvals) + of cvtWordBreak: return_new wordBreak, ?parseIdent[CSSWordBreak](cval) + of cvtListStyleType: + return_new liststyletype, ?parseIdent[CSSListStyleType](cval) + of cvtVerticalAlign: return_new verticalAlign, ?cssVerticalAlign(cval) + of cvtTextAlign: return_new textAlign, ?parseIdent[CSSTextAlign](cval) + of cvtListStylePosition: + return_new listStylePosition, ?parseIdent[CSSListStylePosition](cval) + of cvtPosition: return_new position, ?parseIdent[CSSPosition](cval) + of cvtCaptionSide: return_new captionSide, ?parseIdent[CSSCaptionSide](cval) + of cvtBorderCollapse: + return_new borderCollapse, ?parseIdent[CSSBorderCollapse](cval) + of cvtLength2: + let a = ?cssAbsoluteLength(cval) + cvals.skipWhitespace(i) + let b = if i >= cvals.len: a else: ?cssAbsoluteLength(cvals[i]) + return_new length2, (a, b) + of cvtQuotes: return_new quotes, ?cssQuotes(cvals) + of cvtCounterReset: return_new counterReset, ?cssCounterReset(cvals) + of cvtImage: return_new image, ?cssImage(cval) + of cvtFloat: return_new float, ?parseIdent[CSSFloat](cval) + of cvtVisibility: return_new visibility, ?parseIdent[CSSVisibility](cval) + of cvtBoxSizing: return_new boxSizing, ?parseIdent[CSSBoxSizing](cval) + of cvtClear: return_new clear, ?parseIdent[CSSClear](cval) + of cvtTextTransform: + return_new textTransform, ?parseIdent[CSSTextTransform](cval) + of cvtBgcolorIsCanvas: return err() # internal value + of cvtFlexDirection: + return_new flexDirection, ?parseIdent[CSSFlexDirection](cval) + of cvtFlexWrap: return_new flexWrap, ?parseIdent[CSSFlexWrap](cval) + of cvtNumber: return_new number, ?cssNumber(cval, t == cptFlexGrow) + of cvtNone: return err() + +func getInitialColor(t: CSSPropertyType): CellColor = + if t == cptBackgroundColor: + return Colors["transparent"].cellColor() + return defaultColor + +func getInitialLength(t: CSSPropertyType): CSSLength = + case t + of cptWidth, cptHeight, cptWordSpacing, cptLineHeight, cptLeft, cptRight, + cptTop, cptBottom, cptMaxWidth, cptMaxHeight, cptMinWidth, cptMinHeight, + cptFlexBasis: + return CSSLengthAuto + else: + return CSSLength(auto: false, unit: cuPx, num: 0) + +func getInitialInteger(t: CSSPropertyType): int = + case t + of cptChaColspan, cptChaRowspan: + return 1 + of cptFontWeight: + return 400 # normal + else: + return 0 + +func getInitialNumber(t: CSSPropertyType): float64 = + if t == cptFlexShrink: + return 1 + return 0 + +func calcInitial(t: CSSPropertyType): CSSComputedValue = + let v = valueType(t) + var nv: CSSComputedValue + case v + of cvtColor: + nv = CSSComputedValue(v: v, color: getInitialColor(t)) + of cvtDisplay: + nv = CSSComputedValue(v: v, display: DisplayInline) + of cvtWordBreak: + nv = CSSComputedValue(v: v, wordbreak: WordBreakNormal) + of cvtLength: + nv = CSSComputedValue(v: v, length: getInitialLength(t)) + of cvtInteger: + nv = CSSComputedValue(v: v, integer: getInitialInteger(t)) + of cvtQuotes: + nv = CSSComputedValue(v: v, quotes: CSSQuotes(auto: true)) + of cvtNumber: + nv = CSSComputedValue(v: v, number: getInitialNumber(t)) + else: + nv = CSSComputedValue(v: v) + return nv + +func getInitialTable(): array[CSSPropertyType, CSSComputedValue] = + for t in CSSPropertyType: + result[t] = calcInitial(t) + +let defaultTable = getInitialTable() + +template getDefault(t: CSSPropertyType): CSSComputedValue = + {.cast(noSideEffect).}: + defaultTable[t] + +func lengthShorthand(cvals: openArray[CSSComponentValue]; + props: array[4, CSSPropertyType]; global: CSSGlobalType; has_auto = true): + Opt[seq[CSSComputedEntry]] = + var res: seq[CSSComputedEntry] = @[] + if global != cgtNoglobal: + for t in props: + res.add((t, nil, global)) + return ok(res) + var lengths: seq[CSSComputedValue] = @[] + var i = 0 + while i < cvals.len: + cvals.skipWhitespace(i) + let length = ?cssLength(cvals[i], has_auto = has_auto) + let val = CSSComputedValue(v: cvtLength, length: length) + lengths.add(val) + inc i + case lengths.len + of 1: # top, bottom, left, right + for i, t in props: + res.add((t, lengths[0], cgtNoglobal)) + of 2: # top, bottom | left, right + for i, t in props: + res.add((t, lengths[i mod 2], cgtNoglobal)) + of 3: # top | left, right | bottom + for i, t in props: + let j = if i == 0: + 0 # top + elif i == 3: + 2 # bottom + else: + 1 # left, right + res.add((t, lengths[j], cgtNoglobal)) + of 4: # top | right | bottom | left + for i, t in props: + res.add((t, lengths[i], cgtNoglobal)) + else: + return err() + return ok(res) + +const PropertyMarginSpec = [ + cptMarginTop, cptMarginRight, cptMarginBottom, cptMarginLeft +] + +const PropertyPaddingSpec = [ + cptPaddingTop, cptPaddingRight, cptPaddingBottom, cptPaddingLeft +] + +proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string; + cvals: openArray[CSSComponentValue]): Err[void] = + var i = 0 + cvals.skipWhitespace(i) + if i >= cvals.len: + return err() + let global = cssGlobal(cvals[i]) + case shorthandType(name) + of cstNone: + let t = propertyType(name) + if global != cgtNoglobal: + res.add((t, nil, global)) + else: + res.add((t, ?cvals.parseValue(t), global)) + of cstAll: + if global == cgtNoglobal: + return err() + for t in CSSPropertyType: + res.add((t, nil, global)) + of cstMargin: + res.add(?lengthShorthand(cvals, PropertyMarginSpec, global)) + of cstPadding: + res.add(?lengthShorthand(cvals, PropertyPaddingSpec, global, + has_auto = false)) + of cstBackground: + var bgcolorval = getDefault(cptBackgroundColor) + var bgimageval = getDefault(cptBackgroundImage) + var valid = true + if global == cgtNoglobal: + for tok in cvals: + if tok == cttWhitespace: + continue + if (let r = cssImage(tok); r.isSome): + bgimageval = CSSComputedValue(v: cvtImage, image: r.get) + elif (let r = cssColor(tok); r.isSome): + bgcolorval = CSSComputedValue(v: cvtColor, color: r.get) + else: + #TODO when we implement the other shorthands too + #valid = false + discard + if valid: + res.add((cptBackgroundColor, bgcolorval, global)) + res.add((cptBackgroundImage, bgimageval, global)) + of cstListStyle: + var positionVal = getDefault(cptListStylePosition) + var typeVal = getDefault(cptListStyleType) + var valid = true + if global == cgtNoglobal: + for tok in cvals: + if tok == cttWhitespace: + continue + if (let r = parseIdent[CSSListStylePosition](tok); r.isSome): + positionVal = CSSComputedValue( + v: cvtListStylePosition, + liststyleposition: r.get + ) + elif (let r = parseIdent[CSSListStyleType](tok); r.isSome): + typeVal = CSSComputedValue( + v: cvtListStyleType, + liststyletype: r.get + ) + else: + #TODO list-style-image + #valid = false + discard + if valid: + res.add((cptListStylePosition, positionVal, global)) + res.add((cptListStyleType, typeVal, global)) + of cstFlex: + if global == cgtNoglobal: + var i = 0 + cvals.skipWhitespace(i) + if i >= cvals.len: + return err() + if (let r = cssNumber(cvals[i], positive = true); r.isSome): + # flex-grow + let val = CSSComputedValue(v: cvtNumber, number: r.get) + res.add((cptFlexGrow, val, global)) + inc i + cvals.skipWhitespace(i) + if i < cvals.len: + if not cvals[i].isToken: + return err() + if (let r = cssNumber(cvals[i], positive = true); r.isSome): + # flex-shrink + let val = CSSComputedValue(v: cvtNumber, number: r.get) + res.add((cptFlexShrink, val, global)) + inc i + cvals.skipWhitespace(i) + if res.len < 1: # flex-grow omitted, default to 1 + let val = CSSComputedValue(v: cvtNumber, number: 1) + res.add((cptFlexGrow, val, global)) + if res.len < 2: # flex-shrink omitted, default to 1 + let val = CSSComputedValue(v: cvtNumber, number: 1) + res.add((cptFlexShrink, val, global)) + if i < cvals.len: + # flex-basis + let val = CSSComputedValue(v: cvtLength, length: ?cssLength(cvals[i])) + res.add((cptFlexBasis, val, global)) + else: # omitted, default to 0px + let val = CSSComputedValue( + v: cvtLength, + length: CSSLength(unit: cuPx, num: 0) + ) + res.add((cptFlexBasis, val, global)) + else: + res.add((cptFlexGrow, getDefault(cptFlexGrow), global)) + res.add((cptFlexShrink, getDefault(cptFlexShrink), global)) + res.add((cptFlexBasis, getDefault(cptFlexBasis), global)) + of cstFlexFlow: + if global == cgtNoglobal: + var i = 0 + cvals.skipWhitespace(i) + if i >= cvals.len: + return err() + if (let dir = parseIdent[CSSFlexDirection](cvals[i]); dir.isSome): + # flex-direction + let val = CSSComputedValue(v: cvtFlexDirection, flexdirection: dir.get) + res.add((cptFlexDirection, val, global)) + inc i + cvals.skipWhitespace(i) + if i < cvals.len: + let wrap = ?parseIdent[CSSFlexWrap](cvals[i]) + let val = CSSComputedValue(v: cvtFlexWrap, flexwrap: wrap) + res.add((cptFlexWrap, val, global)) + else: + res.add((cptFlexDirection, getDefault(cptFlexDirection), global)) + res.add((cptFlexWrap, getDefault(cptFlexWrap), global)) + return ok() + +proc parseComputedValues*(name: string; value: seq[CSSComponentValue]): + seq[CSSComputedEntry] = + var res: seq[CSSComputedEntry] = @[] + if res.parseComputedValues(name, value).isSome: + return res + return @[] + +proc addValues*(builder: var CSSComputedValuesBuilder; + decls: seq[CSSDeclaration]; origin: CSSOrigin) = + for decl in decls: + let vals = parseComputedValues(decl.name, decl.value) + if decl.important: + builder.importantProperties[origin].add(vals) + else: + builder.normalProperties[origin].add(vals) + +proc applyValue(vals: CSSComputedValues; entry: CSSComputedEntry; + parent: CSSComputedValues; previousOrigin: CSSComputedValues) = + let parentVal = if parent != nil: + parent[entry.t] + else: + nil + case entry.global + of cgtInherit: + if parentVal != nil: + vals[entry.t] = parentVal + else: + vals[entry.t] = getDefault(entry.t) + of cgtInitial: + vals[entry.t] = getDefault(entry.t) + of cgtUnset: + if inherited(entry.t): + # inherit + if parentVal != nil: + vals[entry.t] = parentVal + else: + vals[entry.t] = getDefault(entry.t) + else: + # initial + vals[entry.t] = getDefault(entry.t) + of cgtRevert: + if previousOrigin != nil: + vals[entry.t] = previousOrigin[entry.t] + else: + vals[entry.t] = getDefault(entry.t) + of cgtNoglobal: + vals[entry.t] = entry.val + +func inheritProperties*(parent: CSSComputedValues): CSSComputedValues = + new(result) + for prop in CSSPropertyType: + if inherited(prop) and parent[prop] != nil: + result[prop] = parent[prop] + else: + result[prop] = getDefault(prop) + +func copyProperties*(props: CSSComputedValues): CSSComputedValues = + new(result) + result[] = props[] + +func rootProperties*(): CSSComputedValues = + new(result) + for prop in CSSPropertyType: + result[prop] = getDefault(prop) + +func hasValues*(builder: CSSComputedValuesBuilder): bool = + for origin in CSSOrigin: + if builder.normalProperties[origin].len > 0: + return true + if builder.importantProperties[origin].len > 0: + return true + return false + +func buildComputedValues*(builder: CSSComputedValuesBuilder): + CSSComputedValues = + new(result) + var previousOrigins: array[CSSOrigin, CSSComputedValues] + for entry in builder.normalProperties[coUserAgent]: # user agent + result.applyValue(entry, builder.parent, nil) + previousOrigins[coUserAgent] = result.copyProperties() + # Presentational hints override user agent style, but respect user/author + # style. + if builder.preshints != nil: + for prop in CSSPropertyType: + if builder.preshints[prop] != nil: + result[prop] = builder.preshints[prop] + for entry in builder.normalProperties[coUser]: # user + result.applyValue(entry, builder.parent, previousOrigins[coUserAgent]) + # save user origins so author can use them + previousOrigins[coUser] = result.copyProperties() + for entry in builder.normalProperties[coAuthor]: # author + result.applyValue(entry, builder.parent, previousOrigins[coUser]) + # no need to save user origins + for entry in builder.importantProperties[coAuthor]: # author important + result.applyValue(entry, builder.parent, previousOrigins[coUser]) + # important, so no need to save origins + for entry in builder.importantProperties[coUser]: # user important + result.applyValue(entry, builder.parent, previousOrigins[coUserAgent]) + # important, so no need to save origins + for entry in builder.importantProperties[coUserAgent]: # user agent important + result.applyValue(entry, builder.parent, nil) + # important, so no need to save origins + # set defaults + for prop in CSSPropertyType: + if result[prop] == nil: + if inherited(prop) and builder.parent != nil and + builder.parent[prop] != nil: + result[prop] = builder.parent[prop] + else: + result[prop] = getDefault(prop) + if result{"float"} != FloatNone: + #TODO it may be better to handle this in layout + let display = result{"display"}.blockify() + if display != result{"display"}: + result{"display"} = display |