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

                    
                 
 
                 
                    
                    
                
                     
                         
                
                     
                 
               
                 
                 
                        
                  
                

    
                                                
 



                             
 



                                                                            
                
                                                     


                
                                                           
                
              
                             
             
                    
              
                    
                           
                    
              
                                                                       
               
                                                                        
                  
                                                 
 
                                                    
           
              
                 




                             
            
                                   
            
                                                              
           
                                                           
                

                                     
                                                             
                   
                          


                 

                        
    
                                                     
 
                                                                               
                       
                                      
                                    
                                           
 
                                                                        
                      




















                                                     
                                      
                      
                                                         

                         
                                                       

                            

                                                                   
                                  

                     
                 
                      
                                                        
                
                           
                       
                                                         
                
                            
                             
                                                        
                                   
                           
                              
                                                         
                                   
                            
                        
                                    
               

                                 
                                                    
                     
                                   
                
                                                                
                     
                                 
               

                                 
                                         
                      
                                  
               

                                 
                                         
                        
                                              


                         
                                     
                        
                                            


                         
                                     

                                         

                      
               


                     



                     

               




                                             


              
                

              

                         
              
                         
               
            

                                               

                                               

                                                              

              



                                            
               
 





















































                                                                           
                                                                         




                                        
                         
                                     
                            
                                          
                                   
                    













                                                                       

                                          
                                                                    



                                        
                         




                                                                     
 
                                                                        



                           
                      
                                

                                                  
 
                                                               
                                              
                                         
                         
                 
                                           
                                
                     
                                                
                     




                       
                                                                   



                                        
                                                   
 



                          
                         
                                 
                            






                                                              
                                                                

                                       







                                                                            
                         

                                          
 
                                                                         
                                                                       
                                   


                                       
                            
               
                         

                                                                        

                                                                          

                                                                        
                                               
                   


                                                               



                                                                          
                                             
                      


                                                                     

                                                            
                                             
               
                                                       




                                                       
                                                                         
                                           





                                                        
                                                                         
                                           

                                               
                                                                         
                                           

                                               
                                                                         
                                           

                                                 
                                                                         
                                           
                           


                           
                          

                                                            
                                              

                                                         
                         

                                                      
                                              

                    

                                             



                                                       

                                                                         



                                      
                          
                 

                   





                               
 

                                                                         
                                       

                                                                                
                              


                                      

                             
                 












                                                                         








                                  
                                                                            
                                                          
                                                    



                                                            
                                                                          
                      


                                                                       



                                                                           
       




                                                               

                                                                           
 
                                                                            

                                                            

                          
          


                                         
                   

                           
                            
                                 
                            
                                 
                            
                                         
                                                                               
                               
                                  
         
                                                                         
                                        


                                                             
                             

                          
                                                                
                                                                
                                                               
                         
                            
             
 
                                                                       


                                                      
                                                          
import std/algorithm
import std/options
import std/tables

import chame/tags
import css/cssparser
import css/cssvalues
import css/match
import css/mediaquery
import css/selectorparser
import css/sheet
import css/stylednode
import html/catom
import html/dom
import html/enums
import js/jscolor
import layout/layoutunit
import types/color
import types/opt

type
  RuleList* = array[PseudoElem, seq[CSSRuleDef]]

  RuleListMap* = ref object
    ua: RuleList # user agent
    user: RuleList
    author: seq[RuleList]

func appliesLR(feature: MediaFeature; window: Window; n: LayoutUnit): bool =
  let a = feature.lengthrange.s.a.px(window.attrs, 0)
  let b = feature.lengthrange.s.b.px(window.attrs, 0)
  if not feature.lengthrange.aeq and a == n or a > n:
    return false
  if not feature.lengthrange.beq and b == n or b < n:
    return false
  return true

func applies(feature: MediaFeature; window: Window): bool =
  case feature.t
  of mftColor:
    return 8 in feature.range
  of mftGrid:
    return feature.b
  of mftHover:
    return feature.b
  of mftPrefersColorScheme:
    return feature.b
  of mftWidth:
    return feature.appliesLR(window, window.attrs.widthPx.toLayoutUnit)
  of mftHeight:
    return feature.appliesLR(window, window.attrs.heightPx.toLayoutUnit)
  of mftScripting:
    return feature.b == window.settings.scripting

func applies(mq: MediaQuery; window: Window): bool =
  case mq.t
  of mctMedia:
    case mq.media
    of mtAll: return true
    of mtPrint: return false
    of mtScreen: return true
    of mtSpeech: return false
    of mtTty: return true
  of mctNot:
    return not mq.n.applies(window)
  of mctAnd:
    return mq.anda.applies(window) and mq.andb.applies(window)
  of mctOr:
    return mq.ora.applies(window) or mq.orb.applies(window)
  of mctFeature:
    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, CSSRuleDef)]]

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))

func calcRules(styledNode: StyledNode; sheet: CSSStylesheet): RuleList =
  var tosorts: ToSorts
  let element = Element(styledNode.node)
  var rules: seq[CSSRuleDef] = @[]
  sheet.tagTable.withValue(element.localName, v):
    for rule in v[]:
      rules.add(rule)
  if element.id != CAtomNull:
    sheet.idTable.withValue(element.id, v):
      for rule in v[]:
        rules.add(rule)
  for class in element.classList.toks:
    sheet.classTable.withValue(class, v):
      for rule in v[]:
        rules.add(rule)
  for attr in element.attrs:
    sheet.attrTable.withValue(attr.qualifiedName, v):
      for rule in v[]:
        rules.add(rule)
  for rule in sheet.generalList:
    rules.add(rule)
  rules.sort(ruleDefCmp, order = Ascending)
  for rule in rules:
    tosorts.calcRule(styledNode, rule)
  for i in PseudoElem:
    tosorts[i].sort((proc(x, y: (int, CSSRuleDef)): int =
      cmp(x[0], y[0])
    ), order = Ascending)
    result[i] = newSeqOfCap[CSSRuleDef](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: cuCh)
  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 set_bgcolor_is_canvas =
    set_cv "-cha-bgcolor-is-canvas", true

  case element.tagType
  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_colspan
    map_rowspan
  of TAG_THEAD, TAG_TBODY, TAG_TFOOT, TAG_TR:
    map_height
    map_bgcolor
  of TAG_COL:
    map_width
  of TAG_IMG:
    map_width
    map_height
  of 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: cuCh, num: float64(cols))
    set_cv "height", CSSLength(unit: cuEm, num: float64(rows))
  of TAG_FONT:
    map_color
  of TAG_INPUT:
    let input = HTMLInputElement(element)
    if input.inputType in InputTypeWithSize:
      map_size
  else: discard

type
  CSSValueEntryObj = object
    normal: seq[CSSComputedEntry]
    important: seq[CSSComputedEntry]

  CSSValueEntryMap = array[CSSOrigin, CSSValueEntryObj]

func buildComputedValues(rules: CSSValueEntryMap; presHints, parent:
    CSSComputedValues): CSSComputedValues =
  new(result)
  var previousOrigins: array[CSSOrigin, CSSComputedValues]
  for entry in rules[coUserAgent].normal: # user agent
    result.applyValue(entry, parent, nil)
  previousOrigins[coUserAgent] = result.copyProperties()
  # Presentational hints override user agent style, but respect user/author
  # style.
  if presHints != nil:
    for prop in CSSPropertyType:
      if presHints[prop] != nil:
        result[prop] = presHints[prop]
  for entry in rules[coUser].normal: # user
    result.applyValue(entry, parent, previousOrigins[coUserAgent])
  # save user origins so author can use them
  previousOrigins[coUser] = result.copyProperties()
  for entry in rules[coAuthor].normal: # author
    result.applyValue(entry, parent, previousOrigins[coUser])
  # no need to save user origins
  for entry in rules[coAuthor].important: # author important
    result.applyValue(entry, parent, previousOrigins[coUser])
  # important, so no need to save origins
  for entry in rules[coUser].important: # user important
    result.applyValue(entry, parent, previousOrigins[coUserAgent])
  # important, so no need to save origins
  for entry in rules[coUserAgent].important: # user agent important
    result.applyValue(entry, parent, nil)
  # important, so no need to save origins
  # set defaults
  for prop in CSSPropertyType:
    if result[prop] == nil:
      if prop.inherited and parent != nil and parent[prop] != nil:
        result[prop] = 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

proc add(map: var CSSValueEntryObj; rules: seq[CSSRuleDef]) =
  for rule in rules:
    map.normal.add(rule.normalVals)
    map.important.add(rule.importantVals)

proc applyDeclarations(styledNode: StyledNode; parent: CSSComputedValues;
    map: RuleListMap) =
  var rules: CSSValueEntryMap
  var presHints: CSSComputedValues = nil
  rules[coUserAgent].add(map.ua[peNone])
  rules[coUser].add(map.user[peNone])
  for rule in map.author:
    rules[coAuthor].add(rule[peNone])
  if styledNode.node != nil:
    let element = Element(styledNode.node)
    let style = element.cachedStyle
    if style != nil:
      for decl in style.decls:
        let vals = parseComputedValues(decl.name, decl.value)
        if decl.important:
          rules[coAuthor].important.add(vals)
        else:
          rules[coAuthor].normal.add(vals)
    presHints = element.calcPresentationalHints()
  styledNode.computed = rules.buildComputedValues(presHints, parent)

func hasValues(rules: CSSValueEntryMap): bool =
  for origin in CSSOrigin:
    if rules[origin].normal.len > 0 or rules[origin].important.len > 0:
      return true
  return false

# Either returns a new styled node or nil.
proc applyDeclarations(pseudo: PseudoElem; styledParent: StyledNode;
    map: RuleListMap): StyledNode =
  var rules: CSSValueEntryMap
  rules[coUserAgent].add(map.ua[pseudo])
  rules[coUser].add(map.user[pseudo])
  for rule in map.author:
    rules[coAuthor].add(rule[pseudo])
  if rules.hasValues():
    let cvals = rules.buildComputedValues(nil, styledParent.computed)
    return styledParent.newStyledElement(pseudo, cvals)
  return nil

func applyMediaQuery(ss: CSSStylesheet; window: Window): CSSStylesheet =
  if ss == nil:
    return nil
  var res = CSSStylesheet()
  res[] = ss[]
  for mq in ss.mqList:
    if mq.query.applies(window):
      res.add(mq.children.applyMediaQuery(window))
  return res

func calcRules(styledNode: StyledNode; ua, user: CSSStylesheet;
    author: seq[CSSStylesheet]): RuleListMap =
  let uadecls = calcRules(styledNode, ua)
  var userdecls: RuleList
  if user != nil:
    userdecls = calcRules(styledNode, user)
  var authordecls: seq[RuleList]
  for rule in author:
    authordecls.add(calcRules(styledNode, rule))
  return RuleListMap(
    ua: uadecls,
    user: userdecls,
    author: authordecls
  )

proc applyStyle(parent, styledNode: StyledNode; map: RuleListMap) =
  let parentComputed = if parent != nil:
    parent.computed
  else:
    rootProperties()
  styledNode.applyDeclarations(parentComputed, map)

type CascadeFrame = object
  styledParent: StyledNode
  child: Node
  pseudo: PseudoElem
  cachedChild: StyledNode
  cachedChildren: seq[StyledNode]
  parentDeclMap: RuleListMap

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: var CascadeFrame): StyledNode =
  let styledParent = frame.styledParent
  let cachedChild = frame.cachedChild
  # Pseudo elements can't have invalid children.
  if cachedChild.t == stElement and cachedChild.pseudo == peNone:
    # Refresh child nodes:
    # * move old seq to a temporary location in frame
    # * create new seq, assuming capacity == len of the previous pass
    frame.cachedChildren = move(cachedChild.children)
    cachedChild.children = newSeqOfCap[StyledNode](frame.cachedChildren.len)
  cachedChild.parent = styledParent
  if styledParent != nil:
    styledParent.children.add(cachedChild)
  return cachedChild

proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
    author: seq[CSSStylesheet]; declmap: var RuleListMap): StyledNode =
  var styledChild: StyledNode = nil
  let pseudo = frame.pseudo
  let styledParent = frame.styledParent
  let child = frame.child
  if frame.pseudo != peNone:
    case pseudo
    of peBefore, peAfter:
      let declmap = frame.parentDeclMap
      let styledPseudo = pseudo.applyDeclarations(styledParent, declmap)
      if styledPseudo != nil and styledPseudo.computed{"content"}.len > 0:
        for content in styledPseudo.computed{"content"}:
          let child = styledPseudo.newStyledReplacement(content, peNone)
          styledPseudo.children.add(child)
        styledParent.children.add(styledPseudo)
    of peInputText:
      let s = HTMLInputElement(styledParent.node).inputString()
      if s.len > 0:
        let content = styledParent.node.document.newText(s)
        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 peTextareaText:
      let s = HTMLTextAreaElement(styledParent.node).textAreaString()
      if s.len > 0:
        let content = styledParent.node.document.newText(s)
        let styledText = styledParent.newStyledText(content)
        styledText.pseudo = pseudo
        styledParent.children.add(styledText)
    of peImage:
      let src = Element(styledParent.node).attr(satSrc)
      let content = CSSContent(
        t: ContentImage,
        s: src,
        bmp: HTMLImageElement(styledParent.node).bitmap
      )
      let styledText = styledParent.newStyledReplacement(content, pseudo)
      styledParent.children.add(styledText)
    of peCanvas:
      let content = CSSContent(
        t: ContentImage,
        s: "canvas://",
        bmp: HTMLCanvasElement(styledParent.node).bitmap
      )
      let styledText = styledParent.newStyledReplacement(content, pseudo)
      styledParent.children.add(styledText)
    of peVideo:
      let content = CSSContent(t: ContentVideo)
      let styledText = styledParent.newStyledReplacement(content, pseudo)
      styledParent.children.add(styledText)
    of peAudio:
      let content = CSSContent(t: ContentAudio)
      let styledText = styledParent.newStyledReplacement(content, pseudo)
      styledParent.children.add(styledText)
    of peNewline:
      let content = CSSContent(t: ContentNewline)
      let styledText = styledParent.newStyledReplacement(content, pseudo)
      styledParent.children.add(styledText)
    of peNone: assert false
  else:
    assert child != nil
    if styledParent != nil:
      if child of Element:
        let element = Element(child)
        styledChild = styledParent.newStyledElement(element)
        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
      let element = Element(child)
      styledChild = newStyledElement(element)
      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) =
  var cached: StyledNode = nil
  if frame.cachedChildren.len > 0:
    for j in countdown(i, 0):
      let it = frame.cachedChildren[j]
      if it.node == child:
        i = j - 1
        cached = it
        break
  styledStack.add(CascadeFrame(
    styledParent: styledParent,
    child: child,
    pseudo: peNone,
    cachedChild: cached
  ))

proc stackAppend(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
    styledParent: StyledNode; pseudo: PseudoElem; i: var int;
    parentDeclMap: RuleListMap = nil) =
  # Can't check for cachedChildren.len here, because we assume that we only have
  # cached pseudo elems when the parent is also cached.
  if frame.cachedChild != nil:
    var cached: StyledNode = nil
    for j in countdown(i, 0):
      let it = frame.cachedChildren[j]
      if it.pseudo == pseudo:
        cached = it
        i = j - 1
        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:
    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: RuleListMap) =
  # i points to the child currently being inspected.
  var idx = frame.cachedChildren.len - 1
  let element = Element(styledChild.node)
  # reset invalid flag here to avoid a type conversion above
  element.invalid = false
  styledStack.stackAppend(frame, styledChild, peAfter, idx, parentDeclMap)
  case element.tagType
  of TAG_TEXTAREA:
    styledStack.stackAppend(frame, styledChild, peTextareaText, idx)
  of TAG_IMG: styledStack.stackAppend(frame, styledChild, peImage, idx)
  of TAG_VIDEO: styledStack.stackAppend(frame, styledChild, peVideo, idx)
  of TAG_AUDIO: styledStack.stackAppend(frame, styledChild, peAudio, idx)
  of TAG_BR: styledStack.stackAppend(frame, styledChild, peNewline, idx)
  of TAG_CANVAS: styledStack.stackAppend(frame, styledChild, peCanvas, idx)
  else:
    for i in countdown(element.childList.high, 0):
      let child = element.childList[i]
      if child of Element or child of Text:
        styledStack.stackAppend(frame, styledChild, child, idx)
    if element.tagType == TAG_INPUT:
      styledStack.stackAppend(frame, styledChild, peInputText, idx)
  styledStack.stackAppend(frame, styledChild, peBefore, 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: peNone,
    cachedChild: cachedTree
  )]
  var root: StyledNode = nil
  var toReset: seq[Element] = @[]
  while styledStack.len > 0:
    var frame = styledStack.pop()
    var declmap: RuleListMap
    let styledParent = frame.styledParent
    let valid = frame.cachedChild != nil and frame.cachedChild.isValid(toReset)
    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 == stElement and styledChild.node != nil:
        # note: following resets styledChild.node's invalid flag
        styledStack.appendChildren(frame, styledChild, declmap)
  for element in toReset:
    element.invalidDeps = {}
  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)