import std/algorithm
import std/options
import std/strutils
import css/cssparser
import css/match
import css/mediaquery
import css/selectorparser
import css/sheet
import css/stylednode
import css/values
import html/catom
import html/dom
import html/enums
import layout/layoutunit
import types/color
import types/opt
import chame/tags
type
DeclarationList* = array[PseudoElem, seq[CSSDeclaration]]
DeclarationListMap* = ref object
ua: DeclarationList # user agent
user: DeclarationList
author: seq[DeclarationList]
func appliesLR(feature: MediaFeature, window: Window,
n: LayoutUnit): bool =
let a = px(feature.lengthrange.a, window.attrs, 0)
let b = px(feature.lengthrange.b, window.attrs, 0)
if not feature.lengthaeq and a == n:
return false
if a > n:
return false
if not feature.lengthbeq and b == n:
return false
if b < n:
return false
return true
func applies(feature: MediaFeature, window: Window): bool =
case feature.t
of FEATURE_COLOR:
return 8 in feature.range
of FEATURE_GRID:
return feature.b
of FEATURE_HOVER:
return feature.b
of FEATURE_PREFERS_COLOR_SCHEME:
return feature.b
of FEATURE_WIDTH:
return feature.appliesLR(window, toLayoutUnit(window.attrs.width_px))
of FEATURE_HEIGHT:
return feature.appliesLR(window, toLayoutUnit(window.attrs.height_px))
of FEATURE_SCRIPTING:
return feature.b == window.settings.scripting
func applies(mq: MediaQuery, window: Window): bool =
case mq.t
of CONDITION_MEDIA:
case mq.media
of MEDIA_TYPE_ALL: return true
of MEDIA_TYPE_PRINT: return false
of MEDIA_TYPE_SCREEN: return true
of MEDIA_TYPE_SPEECH: return false
of MEDIA_TYPE_TTY: return true
of MEDIA_TYPE_UNKNOWN: return false
of CONDITION_NOT:
return not mq.n.applies(window)
of CONDITION_AND:
return mq.anda.applies(window) and mq.andb.applies(window)
of CONDITION_OR:
return mq.ora.applies(window) or mq.orb.applies(window)
of CONDITION_FEATURE:
return mq.feature.applies(window)
func applies*(mqlist: MediaQueryList, window: Window): bool =
for mq in mqlist:
if mq.applies(window):
return true
return false
appliesFwdDecl = applies
type
ToSorts = array[PseudoElem, seq[(int, seq[CSSDeclaration])]]
proc calcRule(tosorts: var ToSorts, styledNode: StyledNode, rule: CSSRuleDef) =
for sel in rule.sels:
if styledNode.selectorsMatch(sel):
let spec = getSpecificity(sel)
tosorts[sel.pseudo].add((spec, rule.decls))
func calcRules(styledNode: StyledNode, sheet: CSSStylesheet): DeclarationList =
var tosorts: ToSorts
let elem = Element(styledNode.node)
for rule in sheet.genRules(elem.localName, elem.id, elem.classList.toks):
tosorts.calcRule(styledNode, rule)
for i in PseudoElem:
tosorts[i].sort((proc(x, y: (int, seq[CSSDeclaration])): int =
cmp(x[0], y[0])
), order = Ascending)
result[i] = newSeqOfCap[CSSDeclaration](tosorts[i].len)
for item in tosorts[i]:
result[i].add(item[1])
func calcPresentationalHints(element: Element): CSSComputedValues =
template set_cv(a, b: untyped) =
if result == nil:
new(result)
result{a} = b
template map_width =
let s = parseDimensionValues(element.attr(satWidth))
if s.isSome:
set_cv "width", s.get
template map_height =
let s = parseDimensionValues(element.attr(satHeight))
if s.isSome:
set_cv "height", s.get
template map_width_nozero =
let s = parseDimensionValues(element.attr(satWidth))
if s.isSome and s.get.num != 0:
set_cv "width", s.get
template map_height_nozero =
let s = parseDimensionValues(element.attr(satHeight))
if s.isSome and s.get.num != 0:
set_cv "height", s.get
template map_bgcolor =
let s = element.attr(satBgcolor)
if s != "":
let c = parseLegacyColor(s)
if c.isSome:
set_cv "background-color", c.get.cellColor()
template map_size =
let s = element.attrul(satSize)
if s.isSome:
set_cv "width", CSSLength(num: float64(s.get), unit: UNIT_CH)
template map_valign =
case element.attr(satValign).toLowerAscii()
of "top": set_cv "vertical-align", CSSVerticalAlign(keyword: VERTICAL_ALIGN_TOP)
of "middle": set_cv "vertical-align", CSSVerticalAlign(keyword: VERTICAL_ALIGN_MIDDLE)
of "bottom": set_cv "vertical-align", CSSVerticalAlign(keyword: VERTICAL_ALIGN_BOTTOM)
of "baseline": set_cv "vertical-align", CSSVerticalAlign(keyword: VERTICAL_ALIGN_BASELINE)
template map_align =
case element.attr(satAlign).toLowerAscii()
of "center", "middle": set_cv "text-align", TEXT_ALIGN_CHA_CENTER
of "left": set_cv "text-align", TEXT_ALIGN_CHA_LEFT
of "right": set_cv "text-align", TEXT_ALIGN_CHA_RIGHT
template map_table_align =
case element.attr(satAlign).toLowerAscii()
of "left":
set_cv "float", FLOAT_LEFT
of "right":
set_cv "float", FLOAT_RIGHT
of "center":
set_cv "margin-left", CSSLengthAuto #TODO should be inline-start
set_cv "margin-right", CSSLengthAuto #TODO should be inline-end
template map_text =
let s = element.attr(satText)
if s != "":
let c = parseLegacyColor(s)
if c.isSome:
set_cv "color", c.get.cellColor()
template map_color =
let s = element.attr(satColor)
if s != "":
let c = parseLegacyColor(s)
if c.isSome:
set_cv "color", c.get.cellColor()
template map_colspan =
let colspan = element.attrulgz(satColspan)
if colspan.isSome:
let i = colspan.get
if i <= 1000:
set_cv "-cha-colspan", int(i)
template map_rowspan =
let rowspan = element.attrul(satRowspan)
if rowspan.isSome:
let i = rowspan.get
if i <= 65534:
set_cv "-cha-rowspan", int(i)
template map_list_type_ol =
let ctype = element.attr(satType)
if ctype.len > 0:
case ctype[0]
of '1': set_cv "list-style-type", LIST_STYLE_TYPE_DECIMAL
of 'a': set_cv "list-style-type", LIST_STYLE_TYPE_LOWER_ALPHA
of 'A': set_cv "list-style-type", LIST_STYLE_TYPE_UPPER_ALPHA
of 'i': set_cv "list-style-type", LIST_STYLE_TYPE_LOWER_ROMAN
of 'I': set_cv "list-style-type", LIST_STYLE_TYPE_UPPER_ROMAN
else: discard
template map_list_type_ul =
let ctype = element.attr(satType)
if ctype.len > 0:
case ctype.toLowerAscii()
of "none": set_cv "list-style-type", LIST_STYLE_TYPE_NONE
of "disc": set_cv "list-style-type", LIST_STYLE_TYPE_DISC
of "circle": set_cv "list-style-type", LIST_STYLE_TYPE_CIRCLE
of "square": set_cv "list-style-type", LIST_STYLE_TYPE_SQUARE
template set_bgcolor_is_canvas =
set_cv "-cha-bgcolor-is-canvas", true
case element.tagType
of TAG_DIV:
map_align
of TAG_TABLE:
map_height_nozero
map_width_nozero
map_bgcolor
map_table_align
of TAG_TD, TAG_TH:
map_height_nozero
map_width_nozero
map_bgcolor
map_valign
map_align
map_colspan
map_rowspan
of TAG_THEAD, TAG_TBODY, TAG_TFOOT, TAG_TR:
map_height
map_bgcolor
map_valign
map_align
of TAG_COL:
map_width
of TAG_IMG, TAG_CANVAS:
map_width
map_height
of TAG_HTML:
set_bgcolor_is_canvas
of TAG_BODY:
set_bgcolor_is_canvas
map_bgcolor
map_text
of TAG_TEXTAREA:
let textarea = HTMLTextAreaElement(element)
let cols = textarea.attrul(satCols).get(20)
let rows = textarea.attrul(satRows).get(1)
set_cv "width", CSSLength(unit: UNIT_CH, num: float64(cols))
set_cv "height", CSSLength(unit: UNIT_EM, num: float64(rows))
of TAG_FONT:
map_color
of TAG_INPUT:
let input = HTMLInputElement(element)
if input.inputType in InputTypeWithSize:
map_size
of TAG_OL:
map_list_type_ol
of TAG_UL:
map_list_type_ul
else: discard
proc applyDeclarations(styledNode: StyledNode, parent: CSSComputedValues,
map: DeclarationListMap) =
let pseudo = PSEUDO_NONE
var builder = CSSComputedValuesBuilder(parent: parent)
builder.addValues(map.ua[pseudo], ORIGIN_USER_AGENT)
builder.addValues(map.user[pseudo], ORIGIN_USER)
for rule in map.author:
builder.addValues(rule[pseudo], ORIGIN_AUTHOR)
if styledNode.node != nil:
let element = Element(styledNode.node)
let style = element.style_cached
if style != nil:
builder.addValues(style.decls, ORIGIN_AUTHOR)
builder.preshints = element.calcPresentationalHints()
styledNode.computed = builder.buildComputedValues()
# Either returns a new styled node or nil.
proc applyDeclarations(pseudo: PseudoElem, styledParent: StyledNode,
map: DeclarationListMap): StyledNode =
var builder = CSSComputedValuesBuilder(parent: styledParent.computed)
builder.addValues(map.ua[pseudo], ORIGIN_USER_AGENT)
builder.addValues(map.user[pseudo], ORIGIN_USER)
for rule in map.author:
builder.addValues(rule[pseudo], ORIGIN_AUTHOR)
if builder.hasValues():
let cvals = builder.buildComputedValues()
result = styledParent.newStyledElement(pseudo, cvals)
func applyMediaQuery(ss: CSSStylesheet, window: Window): CSSStylesheet =
if ss == nil: return nil
new(result)
result[] = ss[]
for mq in ss.mqList:
if mq.query.applies(window):
result.add(mq.children.applyMediaQuery(window))
func calcRules(styledNode: StyledNode, ua, user: CSSStylesheet,
author: seq[CSSStylesheet]): DeclarationListMap =
let uadecls = calcRules(styledNode, ua)
var userdecls: DeclarationList
if user != nil:
userdecls = calcRules(styledNode, user)
var authordecls: seq[DeclarationList]
for rule in author:
authordecls.add(calcRules(styledNode, rule))
return DeclarationListMap(
ua: uadecls,
user: userdecls,
author: authordecls
)
proc applyStyle(parent, styledNode: StyledNode, map: DeclarationListMap) =
let parentComputed = if parent != nil:
parent.computed
else:
rootProperties()
styledNode.applyDeclarations(parentComputed, map)
type CascadeFrame = object
styledParent: StyledNode
child: Node
pseudo: PseudoElem
cachedChild: StyledNode
parentDeclMap: DeclarationListMap
proc getAuthorSheets(document: Document): seq[CSSStylesheet] =
var author: seq[CSSStylesheet]
for sheet in document.sheets():
author.add(sheet.applyMediaQuery(document.window))
return author
proc applyRulesFrameValid(frame: CascadeFrame): StyledNode =
let styledParent = frame.styledParent
let cachedChild = frame.cachedChild
let styledChild = if cachedChild.t == STYLED_ELEMENT:
if cachedChild.pseudo != PSEUDO_NONE:
# Pseudo elements can't have invalid children.
cachedChild
else:
# We can't just copy cachedChild.children from the previous pass,
# as any child could be invalid.
let element = Element(cachedChild.node)
styledParent.newStyledElement(element, cachedChild.computed,
cachedChild.depends)
else:
# Text
cachedChild
styledChild.parent = styledParent
if styledParent != nil:
styledParent.children.add(styledChild)
return styledChild
proc applyRulesFrameInvalid(frame: CascadeFrame, ua, user: CSSStylesheet,
author: seq[CSSStylesheet], declmap: var DeclarationListMap): StyledNode =
var styledChild: StyledNode
let pseudo = frame.pseudo
let styledParent = frame.styledParent
let child = frame.child
if frame.pseudo != PSEUDO_NONE:
case pseudo
of PSEUDO_BEFORE, PSEUDO_AFTER:
let declmap = frame.parentDeclMap
let styledPseudo = pseudo.applyDeclarations(styledParent, declmap)
if styledPseudo != nil:
let contents = styledPseudo.computed{"content"}
for content in contents:
styledPseudo.children.add(styledPseudo.newStyledReplacement(content))
styledParent.children.add(styledPseudo)
of PSEUDO_INPUT_TEXT:
let content = HTMLInputElement(styledParent.node).inputString()
if content.len > 0:
let styledText = styledParent.newStyledText(content)
# Note: some pseudo-elements (like input text) generate text nodes
# directly, so we have to cache them like this.
styledText.pseudo = pseudo
styledParent.children.add(styledText)
of PSEUDO_TEXTAREA_TEXT:
let content = HTMLTextAreaElement(styledParent.node).textAreaString()
if content.len > 0:
let styledText = styledParent.newStyledText(content)
styledText.pseudo = pseudo
styledParent.children.add(styledText)
of PSEUDO_IMAGE:
let src = Element(styledParent.node).attr(satSrc)
let content = CSSContent(t: CONTENT_IMAGE, s: src)
let styledText = styledParent.newStyledReplacement(content)
styledText.pseudo = pseudo
styledParent.children.add(styledText)
of PSEUDO_VIDEO:
let content = CSSContent(t: CONTENT_VIDEO)
let styledText = styledParent.newStyledReplacement(content)
styledText.pseudo = pseudo
styledParent.children.add(styledText)
of PSEUDO_AUDIO:
let content = CSSContent(t: CONTENT_AUDIO)
let styledText = styledParent.newStyledReplacement(content)
styledText.pseudo = pseudo
styledParent.children.add(styledText)
of PSEUDO_NEWLINE:
let content = CSSContent(t: CONTENT_NEWLINE)
let styledText = styledParent.newStyledReplacement(content)
styledParent.children.add(styledText)
styledText.pseudo = pseudo
of PSEUDO_NONE: assert false
else:
assert child != nil
if styledParent != nil:
if child of Element:
styledChild = styledParent.newStyledElement(Element(child))
styledParent.children.add(styledChild)
declmap = styledChild.calcRules(ua, user, author)
applyStyle(styledParent, styledChild, declmap)
elif child of Text:
let text = Text(child)
styledChild = styledParent.newStyledText(text)
styledParent.children.add(styledChild)
else:
# Root element
styledChild = newStyledElement(Element(child))
declmap = styledChild.calcRules(ua, user, author)
applyStyle(styledParent, styledChild, declmap)
return styledChild
proc stackAppend(styledStack: var seq[CascadeFrame], frame: CascadeFrame,
styledParent: StyledNode, child: Node, i: var int) =
if frame.cachedChild != nil:
var cached: StyledNode
while i >= 0:
let it = frame.cachedChild.children[i]
dec i
if it.node == child:
cached = it
break
styledStack.add(CascadeFrame(
styledParent: styledParent,
child: child,
pseudo: PSEUDO_NONE,
cachedChild: cached
))
else:
styledStack.add(CascadeFrame(
styledParent: styledParent,
child: child,
pseudo: PSEUDO_NONE,
cachedChild: nil
))
proc stackAppend(styledStack: var seq[CascadeFrame], frame: CascadeFrame,
styledParent: StyledNode, pseudo: PseudoElem, i: var int,
parentDeclMap: DeclarationListMap = nil) =
if frame.cachedChild != nil:
var cached: StyledNode
let oldi = i
while i >= 0:
let it = frame.cachedChild.children[i]
dec i
if it.pseudo == pseudo:
cached = it
break
# When calculating pseudo-element rules, their dependencies are added
# to their parent's dependency list; so invalidating a pseudo-element
# invalidates its parent too, which in turn automatically rebuilds
# the pseudo-element.
# In other words, we can just do this:
if cached != nil:
styledStack.add(CascadeFrame(
styledParent: styledParent,
pseudo: pseudo,
cachedChild: cached,
parentDeclMap: parentDeclMap
))
else:
i = oldi # move pointer back to where we started
else:
styledStack.add(CascadeFrame(
styledParent: styledParent,
pseudo: pseudo,
cachedChild: nil,
parentDeclMap: parentDeclMap
))
# Append children to styledChild.
proc appendChildren(styledStack: var seq[CascadeFrame], frame: CascadeFrame,
styledChild: StyledNode, parentDeclMap: DeclarationListMap) =
# i points to the child currently being inspected.
var idx = if frame.cachedChild != nil:
frame.cachedChild.children.len - 1
else:
-1
let elem = Element(styledChild.node)
styledStack.stackAppend(frame, styledChild, PSEUDO_AFTER, idx, parentDeclMap)
if elem.tagType == TAG_TEXTAREA:
styledStack.stackAppend(frame, styledChild, PSEUDO_TEXTAREA_TEXT, idx)
elif elem.tagType == TAG_IMG or elem.tagType == TAG_IMAGE:
styledStack.stackAppend(frame, styledChild, PSEUDO_IMAGE, idx)
elif elem.tagType == TAG_VIDEO:
styledStack.stackAppend(frame, styledChild, PSEUDO_VIDEO, idx)
elif elem.tagType == TAG_AUDIO:
styledStack.stackAppend(frame, styledChild, PSEUDO_AUDIO, idx)
elif elem.tagType == TAG_BR:
styledStack.stackAppend(frame, styledChild, PSEUDO_NEWLINE, idx)
else:
for i in countdown(elem.childList.high, 0):
if elem.childList[i] of Element or elem.childList[i] of Text:
styledStack.stackAppend(frame, styledChild, elem.childList[i], idx)
if elem.tagType == TAG_INPUT:
styledStack.stackAppend(frame, styledChild, PSEUDO_INPUT_TEXT, idx)
styledStack.stackAppend(frame, styledChild, PSEUDO_BEFORE, idx, parentDeclMap)
# Builds a StyledNode tree, optionally based on a previously cached version.
proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledNode): StyledNode =
let html = document.html
if html == nil:
return
let author = document.getAuthorSheets()
var styledStack = @[CascadeFrame(
child: html,
pseudo: PSEUDO_NONE,
cachedChild: cachedTree
)]
var root: StyledNode
while styledStack.len > 0:
var frame = styledStack.pop()
var declmap: DeclarationListMap
let styledParent = frame.styledParent
let valid = frame.cachedChild != nil and frame.cachedChild.isValid()
let styledChild = if valid:
frame.applyRulesFrameValid()
else:
# From here on, computed values of this node's children are invalid
# because of property inheritance.
frame.cachedChild = nil
frame.applyRulesFrameInvalid(ua, user, author, declmap)
if styledChild != nil:
if styledParent == nil:
# Root element
root = styledChild
if styledChild.t == STYLED_ELEMENT and styledChild.node != nil:
styledChild.applyDependValues()
styledStack.appendChildren(frame, styledChild, declmap)
return root
proc applyStylesheets*(document: Document, uass, userss: CSSStylesheet,
previousStyled: StyledNode): StyledNode =
let uass = uass.applyMediaQuery(document.window)
let userss = userss.applyMediaQuery(document.window)
return document.applyRules(uass, userss, previousStyled)