about summary refs log blame commit diff stats
path: root/src/css/cascade.nim
blob: 6c56810fe0cf42a1bc9031148c01f0f654d2bd47 (plain) (tree)
1
2
3
4
5
6
7
8
9


                    
 
                    
                
                     
                         
                
                     
                 
                 
               
                 
                        
                  
                
 

                 
    
                                                           
 
                                  



                                    













                                                     










                                                           
                                                                         
                    
                                                                          

                                                 

                                                    









                                       
                                   
                   
                                                              
                  
                                                           
                       


                                                             
                   
                          


                 

                        

                                                              
 
                                                                               
                       
                                      
                                    
                                                 
 
                                                                               
                      
                                     
                                                                           
                                      
                      



                                                                  

                            

                                                                   
                                  

                     
                 
                      
                                                        
                
                           
                       
                                                         
                
                            
                             
                                                        
                                   
                           
                              
                                                         
                                   
                            
                        
                                    
               

                                 
                                                    
                     
                                   

                                                                   
                       
                                               



                                                                                              
                      
                                              


                                                                     
                            
                                              
              
                               
               
                                 


                                                                      
                     
                                 
               

                                 
                                         
                      
                                  
               

                                 
                                         
                        
                                              


                         
                                     
                        
                                            


                         
                                     
                             
                                     
                     







                                                                   
                                     





                                                                   

                                         

                      

             
               


                     
                   




                     
             

               


                                             
              
             

             
                         

              

                         
              
                         
               
            

                                               

                                               

                                                                 

              



                                            



                    
               


                                                                         
                          
                                                        


                                                      
                                                  
                            
                                          


                                                   
                                                         
                                                     

                                          

                                                                    
                                                                       


                                                      
                                                  
                         

                                                         
 
                                                                        
                          

                 
                      

                                                     
 



                                                               
                 

                                           
                     







                                                                          



                                        
                                                   
 



                          
                         










                                                              
                                                       

                                                    













                                                                       















                                                                               
                                               






                                                                          
                                             




                                                                           
                                             
                    
                                                       


                                                                 
                                           









                                                                 


                                                                 
                                           
                                



                                
                          
                                                                   
                                              

                                                         
                         

                                                      
                                              














































































                                                                               



                                                                  



                                                                    
                                                                   



                                                                                
 
                                                                            
                                                                                                  

                          
          





                                         
                      
                            




                                                                        
                                  
         
                                                                         
                                        


                                                             
                             




                                                                     
             
 



                                                                       
                                                          
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)