Forum OpenACS Development: Re: template::head::add_css problem

Collapse
Posted by Dave Bauer on
Don,

I agree. A package might define specific CSS, but it should never be overriding the site wide css settings. It should define its own classes and use them instead of the site wide defined classes.

Collapse
Posted by Gustaf Neumann on
Hmm, this rule would make it quite hard to refine packages 
or to define skins, modifying some properties of the default 
settings. Style sheets are built for cascading (therefore 
the name) and to allow redefinitions / refinements.

Below is a short implementation provided as a study
of what i have sketched above with a small change: 

 - the approach below does not require identifiers, but uses
   the content as identifiers. Therefore it is not
   necessary to invent names when dependent entries are
   defined

The code computes the topological sort of the lattice implied 
by the dependencies and outputs movable elements according to
the optimization rules from the yahoo developer network (css
to the top, javascript to the bottom). The implementation 
is as well more complete in terms of supported html
attributes (e.g. possible to specify rel, rev, type for 
css_files, etc).

Note that the code takes care of the following aspects:
a) It is not depending on the evaluation
   order of tcl/adp-files 
b) Double definitions of the e.g. same css file 
   (e.g. two different included tcl/adp chunks 
   require the same css file) 
   does not lead to double definitions in resulting 
   HTML file. 
c) The code handles as well javascript files and
   javascript junks, which have the same problem.
   The discussion above addresses only css files. 

Below is the code and a few test cases to demonstrate its
usage.

Best regards
-gustaf neumann


namespace eval ::template {
  #
  # Define a Meta Class to create objects in the ::template namespace
  #
  ::xotcl::Class create ::template::Class -superclass ::xotcl::Class
  ::template::Class instproc create {name args} {
    eval next [list ::template::$name] $args
  }

  #
  # Class "header_content" is the most general of the classes below.
  # It defines common attributes (here "requires") and behavior 
  # (most important generate)
  #
  Class create header_content -parameter {
    requires
  }

  header_content proc generate {} {
    # sort all instances accoring to their dependencies
    my topoSort [my allinstances]
    #
    # The entries on the first level are entries requiring
    # nothing else.  These are quite independent of the rest
    # and can be easily moved, if they are not required by other
    # entries. As described in http://developer.yahoo.com/performance/rules.html#css_top
    # movable stylesheets should be moved to the top and 
    # movable script-files should be moved towards the end of the head
    # for performance reasons.
    
    set can_move [list]
    set cannot_move [list]
    foreach e [my set level(1)] {
      if {[my exists is_required([namespace tail $e])]} {
        lappend cannot_move $e
      } else {
        lappend can_move $e
      }
    }
    
    set output ""
    set tail ""
    # Put movable css files from the first level to the top and 
    # put movable js definitions towards the end
    foreach e $can_move {
      if {[$e istype ::template::css_file]} {
        append output [$e as_html] \n
      } else {
        append tail [$e as_html] \n
      }
    }
    # Output the remainder of the entries in the computed order
    foreach e $cannot_move {
      append output [$e as_html] \n
    }
    foreach l [lrange [lsort -integer [my array names level]] 1 end] {
      foreach e [my set level($l)] {
        append output [$e as_html] \n
      }
    }
    append output $tail
    return $output
  }

  header_content proc topoSort {set} {
    #
    # Perform a topological sort of the given set
    # of entries. The code is taken essentially 
    # from the XOTcl serializer.
    #
    if {[my array exists s]} {my array unset s}
    if {[my array exists level]} {my array unset level}
    foreach c $set {my set s($c) 1}
    set stratum 0
    while {1} {
      set set [my array names s]
      if {[llength $set] == 0} break
      incr stratum
      my set level($stratum) {}
      foreach c $set {
        if {[my needs_nothing $c]} {
          my lappend level($stratum) $c
        }
      }
      if {[my set level($stratum)] eq ""} {
        my set level($stratum) $set
        ns_log notice "Cyclic dependency in $set"
      }
      foreach i [my set level($stratum)] {my unset s($i)}
    }
  }
  header_content proc needs_nothing {o} {
    if {![$o exists requires]} {return 1}
    return [my needs_one_of [$o requires]]
  }
  header_content proc needs_one_of list {
    foreach e $list {
      my set is_required($e) 1
      if {[my exists s($e)]} {return 1}
    }
    return 0
  }
  # 
  # a utility function for the subclasses to output specified 
  # attributes, if values are provided.
  #
  header_content instproc options {list} {
    set options ""
    foreach att $list {
      if {[my exists $att]} {append options " $att='[my $att]'"}
    }
    return $options
  }

  #
  # A header_file is a header_content with a href.
  # If the href is not specified, take it from the object name.
  #

  Class create header_file -superclass header_content -parameter {
    href
  }
  header_file instproc init {} {
    if {![my exists href]} {my href [namespace tail [self]]}
  }

  #
  # A js_file is a header_file with javascript definitions
  #
  Class create js_file -superclass header_file -parameter {
    href
    charset
    {defer false}
    {type text/javascript}
  }
  js_file instproc as_html {} {
    set defer_string [expr {[my defer] ? " defer='defer'" : ""}]
    return "<script src='[my href]'[my options {type charset}]$defer_string></script>"
  }

  #
  # The class javascript is used for inline scripts
  #
  Class create javascript -superclass header_content -parameter {
    src
    charset
    {defer false}
    {type text/javascript}
  }
  javascript instproc init {} {
    if {![my exists src]} {my src [namespace tail [self]]}
  }
  javascript instproc as_html {} {
    set defer_string [expr {[my defer] ? " defer='defer'" : ""}]
    return "<script[my options {type charset}]$defer_string>[my src]</script>"
  }
  
  #
  # A css_file is a special kind of header_file
  #
  Class create css_file -superclass header_file -parameter {
    href 
    title 
    {media all} 
    {alternate false}
    {rel stylesheet}
    rev
    {type text/css}
  } 
  css_file instproc as_html {} {
    if {[my alternate]} {my rel "alternate"}
    return "<link[my options {href type media title lang rel rev}]>"
  }

}

###########################################################################
#
# 
# A few test cases
#
# First take a few definitions from typical openacs applications
#
::template::css_file /resources/acs-templating/lists.css
::template::css_file /resources/acs-templating/forms.css 
::template::css_file /resources/acs-subsite/site-master.css
::template::js_file  /resources/acs-subsite/core.js
#
# This shows, how to refine a css file
#
::template::css_file /resources/my_pkg/my-lists.css \
    -requires /resources/acs-templating/lists.css

#
# An example for an inline script
#
::template::javascript {
  collapse_symbol = '<img src="/resources/forums/Collapse16.gif" width="16" height="16" ALT="-" border="0" title="collapse message">';
  expand_symbol = '<img src="/resources/forums/Expand16.gif" width="16" height="16" ALT="+" border="0" title="expand message">';
  loading_symbol = '<img src="/resources/forums/dyn_wait.gif" width="12" height="16" ALT="x" border="0">';
  loading_message = '<i>Loading...</i>';
  rootdir = 'messages-get';
  sid = '1389425';
}

#
# Finally, compute the output in the optimized and required order
#
ns_log notice [::template::header_content generate]