require 'digest'
require 'singleton'
require 'guilded/exceptions'
module Guilded
# Guilder is the worker for the entire Guilded framework. It collects all of the necessary components for a page
# through its add() method. When the g_apply_behavior() method is called at the end of a page, the Guilder writes
# HTML to include all of the necessary asset files (caching them in production). It also writes a JavaScript initialization
# function and fires the initialization function on page load and a before and after initialization callback allowing for
# custom initializtion to occur.
#
# This initialization function calls the initialization function for each Guilded component that was added to the current
# page. For example, if a Guilded component named 'g_load_alerter' was added to a page, the Guilder would include this line
# in the initialization function it writes: g.initLoadAlerter( /* passing options hash here */ ); The g before the function
# is a JavaScript namespace that Guilded automatically creates to facilitate avoiding name collisions with other JavaScript
# libraries and code.
#
# Th options hash that is passed to the init functions for each Guilded component is simply the options hash from the
# component's view helper. The Guilder calls .to_json() on the options hash. Thus, if there are pairs in the options hash
# that need not go to the JavaScript init method they should be removed within the view helper.
#
class Guilder
include Singleton
GUILDED_NS = "guilded."
attr_reader :initialized_at, :jquery_js, :mootools_js
def initialize #:nodoc:
if defined?( GUILDED_CONFIG )
@config = GUILDED_CONFIG
else
raise Guilded::Exceptions::MissingConfiguration
end
configure_guilded
@initialized_at = Time.now
@g_elements = Hash.new
@g_data_elements = Hash.new
@combined_js_srcs = Array.new
@combined_css_srcs = Array.new
@assets_combined = false
# Make sure that the css reset file is first so that other files can override the reset,
# unless the user specified no reset to be included.
init_sources
end
# Adds an element with its options to the @g_elements hash to be used later.
#
def add( element, options={}, libs=[], styles=[] )
raise Guilded::Exceptions::IdMissing.new unless options.has_key?( :id )
raise Guilded::Exceptions::DuplicateElementId.new( options[:id] ) if @g_elements.has_key?( options[:id] )
@need_mootools = true if options[:mootools]
@g_elements[ options[:id].to_sym ] = Guilded::ComponentDef.new( element, options, libs, styles )
end
# Adds a data structure to be passed to the Guilded JavaScript environment for use on the client
# side. The data is passed using the ruby to_json method on the data structure provided.
#
# === Parameters
# * +name+ - The desired name of the variable on the client side.
# * +data+ - The data to pass to the Guilded JavaScript environment.
#
def add_data( name, data )
@g_data_elements.merge!( name.to_sym => data )
end
# Adds JavaScript sources to the libs collection by resolving them to the normal or min version
# based on the current running environment, development, production, etc.
#
def add_js_sources( *sources )
resolve_js_libs( *sources )
end
def count #:nodoc:
@g_elements.size
end
# The number of Guilded components to be renderred.
#
def component_count
count
end
# The current number of CSS assets necessary for the Guilded component set.
#
def style_count
@combined_css_srcs.size
end
# The current number of JavaScript assets necessary for the Guilded component set.
#
def script_count
@combined_js_srcs.size
end
# Returns true if the component type is included, otherwise false.
#
def include_component?( type )
@g_elements.has_key?( type.to_sym )
end
# The collection of JavaScript assets for the current Guilded component set.
#
def combined_js_srcs
#generate_asset_lists unless @assets_combined
@combined_js_srcs
end
# The collection of CSS assets for the current Guilded component set.
#
def combined_css_srcs
#generate_asset_lists unless @assets_combined
@combined_css_srcs
end
# Clears out all but the reset CSS and the base JavaScripts
#
def reset!
@combined_css_srcs.clear
@combined_js_srcs.clear
@g_elements.clear
@assets_combined = false
init_sources
@default_css_count = @combined_css_srcs.size
@default_js_count = @combined_js_srcs.size
end
def inject_css( *sources )
@combined_css_srcs.insert( @default_css_count, *sources )
end
def inject_js( *sources )
@combined_js_srcs.insert( @default_js_count, *sources )
end
# Generates the markup required to include all the assets necessary for the Guilded compoents in
# @g_elements collection. Use this if you are not interested in caching asset files.
#
def apply #:nodoc:
to_init = ""
generate_asset_lists unless @assets_combined
@combined_css_srcs.each { |css| to_init << "" }
@combined_js_srcs.each { |js| to_init << "" }
to_init << generate_javascript_init
reset!
end
# Writes an initialization method that calls each Guilded components initialization method. This
# method will exceute on document load finish.
#
def generate_javascript_init #:nodoc:
code = ""
end
# Generates a name to use when caching the current set of Guilded component JavaScript assets. Sorts and concatenates
# the name of each JavaScript asset in @combined_js_srcs. Then hashes this string to generate a reproducible, unique
# and shorter string.
#
def js_cache_name
generate_js_cache_name( @combined_js_srcs )
end
# Generates a name to use when caching the current set of Guilded component CSS assets. Sorts and concatenates
# the name of each JavaScript asset in @combined_js_srcs. Then hashes this string to generate a reproducible, unique
# and shorter string.
#
def css_cache_name
generate_css_cache_name( @combined_css_srcs )
end
def generate_js_cache_name( sources ) #:nodoc:
generate_asset_lists unless @assets_combined
#return"#{controller.class.to_s.underscore}_#{controller.action_name}" if development?
sorted_srcs = sources.sort
stripped_srcs = sorted_srcs.map { |src| src.gsub( /.js/, '' ).gsub( /\//, '_') }
joined = stripped_srcs.join( "+" )
"#{Digest::MD5.hexdigest( joined )}"
end
def generate_css_cache_name( sources ) #:nodoc:
generate_asset_lists unless @assets_combined
#return "#{controller.class.to_s.underscore}_#{controller.action_name}" if development?
sorted_srcs = sources.sort
stripped_srcs = sorted_srcs.map { |src| src.gsub( /.css/, '' ).gsub( /\//, '_') }
joined = stripped_srcs.join( "+" )
"#{Digest::MD5.hexdigest( joined )}"
end
protected
def configure_guilded #:nodoc:
@js_path = @config[:js_path]
@js_folder = @config[:js_folder]
@jquery_js = @config[:jquery_js]
@mootools_js = @config[:mootools_js]
@jquery_folder = @config[:jquery_folder] || 'jquery/'
@mootools_folder = @config[:mootools_folder] || 'mootools/'
@guilded_js = 'guilded.min.js'
@url_js = 'jquery-url.min.js'
@css_path = @config[:css_path]
@css_folder = @config[:css_folder]
@reset_css = @config[:reset_css]
#@do_reset_css = @config[:do_reset_css]
@env = @config[:environment]
@env ||= :production
@js_path.freeze
@css_path.freeze
@js_folder.freeze
@guilded_js.freeze
@url_js.freeze
@jquery_js.freeze
@jquery_folder.freeze
@mootols_js.freeze
@mootools_folder.freeze
@guilded_js.freeze
@css_folder.freeze
@reset_css.freeze
#@do_reset_css.freeze
@env.freeze
end
# Adds the Guilded reset CSS file and the guilded.js and jQuery files to the respective sources
# collections.
#
def init_sources #:nodoc:
@combined_css_srcs << "#{@reset_css}" unless @reset_css.nil? || @reset_css.empty?
resolve_js_libs( "#{@jquery_js}", "#{@jquery_folder}#{@url_js}", "#{@js_folder}#{@guilded_js}" )
#TODO include the jQuery lib from Google server in production
end
# Combines all JavaScript and CSS files into lists to include based on what Guilded components are on
# the current page.
#
def generate_asset_lists #:nodoc:
@assets_combined = true
@g_elements.each_value do |defi|
#TODO get stylesheet (skin) stuff using rails caching
combine_css_sources( defi.kind, defi.options[:skin], defi.styles ) unless defi.exclude_css?
# Combine all JavaScript sources so that the caching option can be used on
# the javascript_incldue_tag helper.
combine_js_sources( defi.kind, defi.libs ) unless defi.exclude_js?
end
end
# Helper method that takes the libs and component specific js files and puts them
# into one array so that the javascript_include_tag can correctly cache them. Automatically
# ignores files that have already been inlcuded.
#
# *parameters*
# combined_src (required) An array of the combined sorces for the page being renderred.
# component (required) The name of a guilded component.
# libs An array of JavaScript libraries that this component depends on. More than likely
# a jQuery plugin, etc.
#
def combine_js_sources( component, libs=[] ) #:nodoc:
libs << @mootools_js if @need_mootools
resolve_js_libs( *libs )
comp_src = add_guilded_js_path( component )
@combined_js_srcs.push( comp_src ) unless @combined_js_srcs.include?( comp_src )
end
# Helper method that adds the aditional JavaScript library icludes to the include set.
#
# If running development mode, it will try to remove any .pack, .min, or.compressed
# parts fo the name to try and get the debug version of the library. If it cannot
# find the debug version of the file, it will just remain what was initially provded.
#
def resolve_js_libs( *libs ) #:nodoc:
if development?
# Try to use an unpacked or unminimized version
libs.each do |lib|
debug_lib = lib.gsub( /.pack/, '' ).gsub( /.min/, '' ).gsub( /.compressed/, '' )
path = "#{RAILS_ROOT}/public/javascripts/#{debug_lib}"
if File.exist?( path )
@combined_js_srcs.push( debug_lib ) unless @combined_js_srcs.include?( debug_lib )
else
@combined_js_srcs.push( lib ) unless @combined_js_srcs.include?( lib )
end
end
else
libs.each { |lib| @combined_js_srcs.push( lib ) unless @combined_js_srcs.include?( lib ) }
end
end
# Helper method that takes an array of js sources and adds the correct guilded
# path to them. Returns an array with the new path resolved sources.
#
def map_guilded_js_paths( *sources ) #:nodoc:
sources.map { |source| add_guilded_js_path( source ) }
end
# Adds the guilded JS path to the the source name passed in. When not in development mode,
# it looks for a .pack.js, .min.jsm .compressed.js and chooses one of these over the
# development version.
#
def add_guilded_js_path( source ) #:nodoc:
part = "#{@js_folder}#{GUILDED_NS}#{source.to_s}"
ext = 'js'
return "#{part}.#{ext}" unless production?
possibles = [ "#{@js_path}#{part}.min.#{ext}", "#{@js_path}#{part}.pack.#{ext}", "#{@js_path}#{part}.compressed.#{ext}",
"#{@js_path}#{part}.#{ext}" ]
parts = [ "#{part}.min.#{ext}", "{part}.pack.#{ext}", "#{part}.compressed.#{ext}", "#{part}.#{ext}" ]
possibles.each_with_index do |full_path, i|
return parts[i] if File.exists?( full_path )
end
"" # Should never reach here
end
def combine_css_sources( component, skin, styles=[] ) #:nodoc:
# Get all of this components defined external styles
styles.each do |style|
@combined_css_srcs.push( style ) unless @combined_css_srcs.include?( style )
end
#Get the default or guilded skin styles for this component
comp_src = add_guilded_css_path( component, skin )
@combined_css_srcs.push( comp_src ) unless @combined_css_srcs.include?( comp_src ) || comp_src.empty?
user_src = add_guilded_css_path( component, "user" )
@combined_css_srcs.push( user_src ) unless @combined_css_srcs.include?( user_src ) || user_src.empty?
skin_user_src = add_guilded_css_path( component, "#{skin || 'default'}_user" )
@combined_css_srcs.push( skin_user_src ) unless @combined_css_srcs.include?( skin_user_src ) || skin_user_src.empty?
end
def add_guilded_css_path( source, skin ) #:nodoc:
skin = 'default' if skin.nil? || skin.empty?
part = "#{@css_folder}#{source.to_s}/#{skin}.css"
path = "#{@css_path}#{part}"
File.exists?( path ) ? part : ''
end
def development? #:nodoc:
@env.to_sym == :development
end
def production? #:nodoc:
@env.to_sym == :production
end
def test? #:nodoc:
@env.to_sym == :test
end
end
end