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

                 
                
                  

    
                                                           
 


























                                                  
                                             




                    

                                                                 
                                                                               
                       
                                                    
                                      


                                                
                                                                               
                      
                                     
                                                                          
                                      
 
                      




                                              

                                                                   
                                  

                     
                 


                                                       
                           


                                                        
                            


                                                       
                           


                                                        
                            


                                                     
                                      

                                              



                                                                                              

                                             


                                                                     


                                                  
                           


                                                   












                                             

                      

             
               


                     




                     
             

               


                                             
              
             

             


              

               
            

                                               

                                                                          

              
               
 
                                                                                                                                    
                          
                                               
 

                                                  
                     
                                                  
                            

                                          

                                                                          
                                                    
                                                         
 
                                                     

                                          

                                                                                         
                                                              
 

                                                  
                     
                                                  
 

                                                                                 
 
                                                        
                          



                                               
 

                                                                                                                                                                            

                                                  
                     
                                                       
 
                                                                                                                         




                                        
                                                                               
 






                           
                                                                            
                                                                                                  


                          
                                
 
                                      
                                    
                                                                
 
                            
                                                                      
 
                               
                                              

                                     

              
                               

                                                            
                                         
                                             

                                                                              

                                                                                                                           


                                                        

              

                                         
                             
                      


                                              
         
                                                                         
                                        
                       
                               
                                                                              

                                       
                                                                                          

                                                   


                                                                                   

                                                                         
                             
                                                                

                                                                              

                                                 





                                                                               




                                                                     




                                                                     
                               



                                            
                                                                       
                                                  

                                                                                 

                                           
                                                          


                                                  


                                                                               

                              

                                                                                          




                                                        


                                                                    


                                            









                                                                       



                                            
                               

                         
                                                                               


                                                                               


                                                            

                                                            


                                                       
                                          
                                         

                                               
                                 






                                                                     

                                                     










                                                                          



                                                     



                                                                

                                                
           


                                                                     

                                                     



                                                                                                                 

                                       
                                                          
import algorithm
import options
import streams
import strutils
import sugar

import css/cssparser
import css/match
import css/mediaquery
import css/selectorparser
import css/sheet
import css/stylednode
import css/values
import html/dom
import html/tags
import types/color

type
  DeclarationList* = array[PseudoElem, seq[CSSDeclaration]]

func applies(mq: MediaQuery): 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()
  of CONDITION_AND:
    return mq.anda.applies() and mq.andb.applies()
  of CONDITION_OR:
    return mq.ora.applies() or mq.orb.applies()
  of CONDITION_FEATURE:
    case mq.feature.t
    of FEATURE_COLOR:
      return true #TODO
    of FEATURE_GRID:
      return mq.feature.b
    of FEATURE_HOVER:
      return mq.feature.b
    of FEATURE_PREFERS_COLOR_SCHEME:
      return mq.feature.b

func applies*(mqlist: MediaQueryList): bool =
  for mq in mqlist:
    if mq.applies():
      return true
  return false

type ToSorts = array[PseudoElem, seq[(int, seq[CSSDeclaration])]]

proc calcRule(tosorts: var ToSorts, styledNode: StyledNode, rule: CSSRuleDef) =
  for sel in rule.sels:
    #TODO we shouldn't need backtracking for this...
    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.gen_rules(elem.tagType, elem.id, elem.classList.toks):
    tosorts.calcRule(styledNode, rule)

  for i in PseudoElem:
    tosorts[i].sort((x, y) => cmp(x[0], y[0]))
    result[i] = collect(newSeq):
      for item in tosorts[i]:
        for dl in item[1]:
          dl

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("width"))
    if s.isSome:
      set_cv "width", s.get
  template map_height =
    let s = parseDimensionValues(element.attr("height"))
    if s.isSome:
      set_cv "height", s.get
  template map_width_nozero =
    let s = parseDimensionValues(element.attr("width"))
    if s.isSome and s.get.num != 0:
      set_cv "width", s.get
  template map_height_nozero =
    let s = parseDimensionValues(element.attr("height"))
    if s.isSome and s.get.num != 0:
      set_cv "height", s.get
  template map_bgcolor =
    let c = parseLegacyColor(element.attr("bgcolor"))
    if c.isSome:
      set_cv "background-color", c.get
  template map_valign =
    case element.attr("valign").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("align").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_text =
    let c = parseLegacyColor(element.attr("text"))
    if c.isSome:
      set_cv "color", c.get
  template map_color =
    let c = parseLegacyColor(element.attr("color"))
    if c.isSome:
      set_cv "color", c.get
  template map_colspan =
    let colspan = element.attrigz("colspan")
    if colspan.isSome:
      let i = colspan.get
      if i <= 1000:
        set_cv "-cha-colspan", i
  template map_rowspan =
    let rowspan = element.attrigez("rowspan")
    if rowspan.isSome:
      let i = rowspan.get
      if i <= 65534:
        set_cv "-cha-rowspan", i

  case element.tagType
  of TAG_DIV:
    map_align
  of TAG_TABLE:
    map_height_nozero
    map_width_nozero
    map_bgcolor
  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:
    map_width
    map_height
  of TAG_BODY:
    map_bgcolor
    map_text
  of TAG_TEXTAREA:
    let textarea = HTMLTextAreaElement(element)
    set_cv "width", CSSLength(unit: UNIT_CH, num: float64(textarea.cols))
    set_cv "height", CSSLength(unit: UNIT_EM, num: float64(textarea.rows))
  of TAG_FONT:
    map_color
  else: discard
 
proc applyDeclarations(styledNode: StyledNode, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]) =
  let pseudo = PSEUDO_NONE
  var builder = newComputedValueBuilder(parent)

  builder.addValues(ua[pseudo], ORIGIN_USER_AGENT)
  builder.addValues(user[pseudo], ORIGIN_USER)
  for rule in author:
    builder.addValues(rule[pseudo], ORIGIN_AUTHOR)
  if styledNode.node != nil:
    let element = Element(styledNode.node)
    let style = element.attr("style")
    if style.len > 0:
      let inline_rules = newStringStream(style).parseListOfDeclarations2()
      builder.addValues(inline_rules, ORIGIN_AUTHOR)
    builder.preshints = element.calcPresentationalHints()

  styledNode.computed = builder.buildComputedValues()

# Either returns a new styled node or nil.
proc applyDeclarations(pseudo: PseudoElem, styledParent: StyledNode, ua,
                       user: DeclarationList, author: seq[DeclarationList]): StyledNode =
  var builder = newComputedValueBuilder(styledParent.computed)

  builder.addValues(ua[pseudo], ORIGIN_USER_AGENT)
  builder.addValues(user[pseudo], ORIGIN_USER)
  for rule in author:
    builder.addValues(rule[pseudo], ORIGIN_AUTHOR)

  if builder.hasValues():
    result = styledParent.newStyledElement(pseudo, builder.buildComputedValues())

func applyMediaQuery(ss: CSSStylesheet): CSSStylesheet =
  if ss == nil: return nil
  result = ss
  for mq in ss.mq_list:
    if mq.query.applies():
      result.add(mq.children.applyMediaQuery())

func calcRules(styledNode: StyledNode, ua, user: CSSStylesheet, author: seq[CSSStylesheet]): tuple[uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]] =
  result.uadecls = calcRules(styledNode, ua)
  if user != nil:
    result.userdecls = calcRules(styledNode, user)
  for rule in author:
    result.authordecls.add(calcRules(styledNode, rule))

proc applyStyle(parent, styledNode: StyledNode, uadecls, userdecls: DeclarationList, authordecls: seq[DeclarationList]) =
  let parentComputed = if parent != nil:
    parent.computed
  else:
    rootProperties()

  styledNode.applyDeclarations(parentComputed, uadecls, userdecls, authordecls)

type CascadeLevel = tuple[
  styledParent: StyledNode,
  child: Node,
  pseudo: PseudoElem,
  cachedChild: StyledNode
]

# Builds a StyledNode tree, optionally based on a previously cached version.
proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledNode): StyledNode =
  if document.html == nil:
    return

  var author: seq[CSSStylesheet]

  var lenstack = newSeqOfCap[int](256)
  var styledStack: seq[CascadeLevel]
  styledStack.add((nil, document.html, PSEUDO_NONE, cachedTree))

  while styledStack.len > 0:
    var (styledParent, child, pseudo, cachedChild) = styledStack.pop()

    # Remove stylesheets on nil
    if pseudo == PSEUDO_NONE and child == nil:
      let len = lenstack.pop()
      author.setLen(author.len - len)
      continue

    var styledChild: StyledNode
    let valid = cachedChild != nil and cachedChild.isValid()
    if valid:
      if cachedChild.t == STYLED_ELEMENT:
        if cachedChild.pseudo == PSEUDO_NONE:
          # We can't just copy cachedChild.children from the previous pass, as
          # any child could be invalid.
          styledChild = styledParent.newStyledElement(Element(cachedChild.node), cachedChild.computed, cachedChild.depends)
        else:
          # Pseudo elements can't have invalid children.
          styledChild = cachedChild
          styledChild.parent = styledParent
      else:
        # Text
        styledChild = cachedChild
        styledChild.parent = styledParent
      if styledParent == nil:
        # Root element
        result = styledChild
      else:
        styledParent.children.add(styledChild)
    else:
      # From here on, computed values of this node's children are invalid
      # because of property inheritance.
      cachedChild = nil
      if pseudo != PSEUDO_NONE:
        let (ua, user, authordecls) = styledParent.calcRules(ua, user, author)
        case pseudo
        of PSEUDO_BEFORE, PSEUDO_AFTER:
          let styledPseudo = pseudo.applyDeclarations(styledParent, ua, user, authordecls)
          if styledPseudo != nil:
            styledParent.children.add(styledPseudo)
            let contents = styledPseudo.computed{"content"}
            for content in contents:
              styledPseudo.children.add(styledPseudo.newStyledReplacement(content))
        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 content = CSSContent(t: CONTENT_IMAGE, s: "[img]")
          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)
          styledText.pseudo = pseudo
          styledParent.children.add(styledText)
        of PSEUDO_NONE: discard
      else:
        assert child != nil
        if styledParent != nil:
          if child.nodeType == ELEMENT_NODE:
            styledChild = styledParent.newStyledElement(Element(child))
            styledParent.children.add(styledChild)
            let (ua, user, authordecls) = styledChild.calcRules(ua, user, author)
            applyStyle(styledParent, styledChild, ua, user, authordecls)
          elif child.nodeType == TEXT_NODE:
            let text = Text(child)
            styledChild = styledParent.newStyledText(text)
            styledParent.children.add(styledChild)
        else:
          # Root element
          styledChild = newStyledElement(Element(child))
          let (ua, user, authordecls) = styledChild.calcRules(ua, user, author)
          applyStyle(styledParent, styledChild, ua, user, authordecls)
          result = styledChild

    if styledChild != nil and styledChild.t == STYLED_ELEMENT and styledChild.node != nil:
      styledChild.applyDependValues()
      # i points to the child currently being inspected.
      var i = if cachedChild != nil:
        cachedChild.children.len - 1
      else:
        -1
      template stack_append(styledParent: StyledNode, child: Node) =
        if cachedChild != nil:
          var cached: StyledNode
          while i >= 0:
            let it = cachedChild.children[i]
            dec i
            if it.node == child:
              cached = it
              break
          styledStack.add((styledParent, child, PSEUDO_NONE, cached))
        else:
          styledStack.add((styledParent, child, PSEUDO_NONE, nil))

      template stack_append(styledParent: StyledNode, ps: PseudoElem) =
        if cachedChild != nil:
          var cached: StyledNode
          let oldi = i
          while i >= 0:
            let it = cachedChild.children[i]
            dec i
            if it.pseudo == ps:
              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((styledParent, nil, ps, cached))
          else:
            i = oldi # move pointer back to where we started
        else:
          styledStack.add((styledParent, nil, ps, nil))

      let elem = Element(styledChild.node)
      if valid and result != styledChild:
        styledChild.sheets = cachedChild.sheets
      else:
        if result == styledChild:
          #TODO this is ugly. we should cache head sheets separately.
          let head = document.head
          if head != nil:
            if head.invalid or cachedChild == nil:
              let sheets = head.sheets()
              for sheet in sheets:
                styledChild.sheets.add(sheet.applyMediaQuery())
            else:
              styledChild.sheets = cachedChild.sheets
        else:
          let sheets = elem.sheets()
          if sheets.len > 0:
            for sheet in sheets:
              styledChild.sheets.add(sheet.applyMediaQuery())
      if styledChild.sheets.len > 0:
        for sheet in styledChild.sheets:
          author.add(sheet)
        lenstack.add(styledChild.sheets.len)
        # Add a nil before the last element (in-stack), so we know when to
        # remove inline author sheets.
        styledStack.add((nil, nil, PSEUDO_NONE, nil))

      stack_append styledChild, PSEUDO_AFTER

      if elem.tagType == TAG_TEXTAREA:
        stack_append styledChild, PSEUDO_TEXTAREA_TEXT
      elif elem.tagType == TAG_IMG or elem.tagType == TAG_IMAGE:
        stack_append styledChild, PSEUDO_IMAGE
      elif elem.tagType == TAG_BR:
        stack_append styledChild, PSEUDO_NEWLINE
      else:
        for i in countdown(elem.childList.high, 0):
          if elem.childList[i].nodeType in {ELEMENT_NODE, TEXT_NODE}:
            stack_append styledChild, elem.childList[i]
        if elem.tagType == TAG_INPUT:
          stack_append styledChild, PSEUDO_INPUT_TEXT

      stack_append styledChild, PSEUDO_BEFORE

proc applyStylesheets*(document: Document, uass, userss: CSSStylesheet, previousStyled: StyledNode): StyledNode =
  let uass = uass.applyMediaQuery()
  let userss = userss.applyMediaQuery()
  return document.applyRules(uass, userss, previousStyled)