#!/usr/bin/env ruby
#--
# Copyright &169;2001-2008 Integrallis Software, LLC.
# All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require 'trellis/utils'
require 'trellis/logging'
require 'rubygems'
require 'rack'
require 'radius'
require 'builder'
require 'nokogiri'
require 'extensions/string'
require 'haml'
require 'markaby'
require 'redcloth'
require 'bluecloth'
require 'facets'
require 'directory_watcher'
require 'erubis'
require 'ostruct'
module Trellis
TEMPLATE_FORMATS = [:html, :xhtml, :haml, :textile, :markdown, :eruby]
# -- Application --
# Represents a Trellis Web Application. An application can define one or more
# pages and it must define a home page or entry point into the application
class Application
include Logging
include Rack::Utils
include Nokogiri::XML
@@partials = Hash.new
@@layouts = Hash.new
# descendant application classes get a singleton class level instances for
# holding homepage, dependent pages, static resource routing paths
def self.inherited(child) #:nodoc:
child.class_attr_reader(:homepage)
child.attr_array(:persistents)
child.class_attr_reader(:session_config)
child.attr_array(:static_routes)
child.attr_array(:routers)
child.meta_def(:logger) { Application.logger }
child.instance_variable_set(:@session_config, OpenStruct.new({:impl => :cookie}))
super
end
# class method that defines the homepage or entry point of the application
# the entry point is the URL pattern / where the application is mounted
def self.home(sym)
@homepage = sym
end
def self.session(sym, options={})
@session_config = OpenStruct.new({:impl => sym, :options => options})
end
# define url paths for static resources
def self.map_static(urls = [], root = File.expand_path("#{File.dirname($0)}/../html/"))
@static_routes << {:urls => urls, :root => root}
end
# application-wide persistent fields
def self.persistent(*fields)
instance_attr_accessor fields
@persistents = @persistents | fields
end
def self.partials
@@partials
end
def self.partial(name, body = nil, options = nil, &block)
store_template(name, :partial, body, options, &block)
end
def self.layouts
@@layouts
end
def self.layout(name, body = nil, options = nil, &block)
store_template(name, :layout, body, options, &block)
end
# bootstrap the application
def start(port = 3000)
Application.logger.info "Starting Trellis Application #{self.class} on port #{port}"
# only in development mode
directory_watcher = configure_directory_watcher
directory_watcher.start
Rack::Handler::Mongrel.run configured, :Port => port do |server|
trap(:INT) do
Application.logger.info "Exiting Trellis Application #{self.class}"
directory_watcher.stop
server.stop
end
end
rescue Exception => e
Application.logger.warn "#{ e } (#{ e.class })!"
end
def configured
# configure rack middleware
application = Rack::ShowStatus.new(self)
application = Rack::ShowExceptions.new(application)
application = Rack::Reloader.new(application) # only in development mode
application = Rack::CommonLogger.new(application, Application.logger)
# configure rack session
session_config = self.class.session_config
case session_config.impl
when :pool
application = Rack::Session::Pool.new(application, session_config.options)
when :memcached
application = Rack::Session::Memcache.new(application, session_config.options)
else
application = Rack::Session::Cookie.new(application)
end
# set all static resource paths
self.class.static_routes.each do |path|
application = Rack::Static.new(application, path)
end
application
end
def self.routers
unless @routers
@routers = Page.subclasses.values.collect { |page| page.router }.compact.sort {|a,b| b.score <=> a.score }
end
@routers
end
# find the first page with a suitable router, if none is found use the default router
def find_router_for(request)
match = Application.routers.find { |router| router.matches?(request) }
match || DefaultRouter.new(:application => self)
end
# rack call interface.
def call(env)
dup.call!(env)
end
# implements the rack specification
def call!(env)
response = Rack::Response.new
request = Rack::Request.new(env)
Application.logger.debug "request received with url_root of #{request.script_name}" if request.script_name
session = env['rack.session'] ||= {}
router = find_router_for(request)
route = router.route(request)
page = route.destination.new if route.destination
if page
load_persistent_fields_data(session)
page.application = self
page.class.url_root = request.script_name
page.path = request.path_info.sub(/^\//, '')
page.inject_dependent_pages
page.call_if_provided(:before_load)
page.load_page_session_information(session)
page.call_if_provided(:after_load)
page.params = request.params.keys_to_symbols
router.inject_parameters_into_page_instance(page, request)
result = route.event ? page.process_event(route.event, route.value, route.source, session) : page
Application.logger.debug "response is #{result} an instance of #{result.class}"
# -------------------------
# prepare the http response
# -------------------------
if result.kind_of?(Trellis::Redirect)
# redirect short circuits
result.apply_to(request, response)
Application.logger.debug "redirecting to ==> #{request.script_name}/#{result.target}"
elsif (request.post? || route.event) && result.kind_of?(Trellis::Page)
# for action events of posts then use redirect after post pattern
# remove the events path and just return to the page
path = result.path ? result.path.gsub(/\/events\/.*/, '') : result.class.class_to_sym
response.status = 302
response.headers["Location"] = "#{request.script_name}/#{path}"
Application.logger.debug "redirecting to ==> #{request.script_name}/#{path}"
else
# handle the get method
if result.kind_of?(Trellis::Page) && result.respond_to?(:get)
get = result.get
if get.kind_of?(Trellis::Redirect)
# redirect short circuits
get.apply_to(request, response)
Application.logger.debug "redirecting to ==> #{request.script_name}/#{get.target}"
elsif (get.class == result.class) || !get.kind_of?(Trellis::Page)
response.body = get.kind_of?(Trellis::Page) ? get.render : get
response.status = 200
else
path = get.path ? get.path.gsub(/\/events\/.*/, '') : get.class.class_to_sym
response.status = 302
response.headers["Location"] = "#{request.script_name}/#{path}"
Application.logger.debug "redirecting to ==> #{request.script_name}/#{path}"
end
else
# for render requests simply render the page
response.body = result.kind_of?(Trellis::Page) ? result.render : result
response.status = 200
end
end
else
response.status = 404
end
save_persistent_fields_data(session)
response.finish
end
private
def self.store_template(name, type, body = nil, options = nil, &block)
format = (options[:format] if options) || :html
if block_given?
mab = Markaby::Builder.new({}, self, &block)
html = mab.to_s
else
case format
when :haml
html = Haml::Engine.new(body).render
when :textile
html = RedCloth.new(body).to_html
when :markdown
if type == :partial
html = BlueCloth.new(body).to_html
else
html = Markaby.build { thtml { body { text "#{BlueCloth.new(body).to_html}" } }}
end
else # assume the body is (x)html, also eruby is treated as (x)html at this point
html = body
end
end
template = Nokogiri::XML(html)
case type
when :layout
@@layouts[name] = OpenStruct.new({:name => name,
:template => template,
:to_xml => template.to_xml,
:format => format})
when :partial
@@partials[name] = OpenStruct.new({:name => name,
:template => template,
:to_xml => template.to_xml(:save_with => Node::SaveOptions::NO_DECLARATION),
:format => format})
end
end
def load_persistent_fields_data(session)
self.class.persistents.each do |persistent_field|
field = "@#{persistent_field}".to_sym
current_value = instance_variable_get(field)
new_value = session[persistent_field]
if current_value != new_value && new_value != nil
instance_variable_set(field, new_value)
end
end
end
def save_persistent_fields_data(session)
self.class.persistents.each do |persistent_field|
session[persistent_field] = instance_variable_get("@#{persistent_field}".to_sym)
end
end
def configure_directory_watcher(directory = nil)
# set directory watcher to reload templates
glob = []
Page::TEMPLATE_FORMATS.each do |format|
glob << "*.#{format}"
end
templates_directory = directory || "#{File.dirname($0)}/../html/"
directory_watcher = DirectoryWatcher.new templates_directory, :glob => glob, :pre_load => true
directory_watcher.add_observer do |*args|
args.each do |event|
Application.logger.debug "directory watcher: #{event}"
event_type = event.type.to_s
if (event_type == 'modified' || event_type == 'stable')
template = event.path
format = File.extname(template).delete('.').to_sym
page_locator = Page.template_registry[template]
page = Page.get_page(page_locator)
Application.logger.info "reloading template for page => #{page}: #{template}"
File.open(template, "r") { |f| page.template(f.read, :format => format) }
end
end
end
Application.logger.info "watching #{templates_directory} for template changes..."
directory_watcher
end
end
# -- Route --
# A route object encapsulates an event, the event destination (target), the
# event source and an optional event value
class Route
attr_reader :destination, :event, :source, :value
def initialize(destination, event = nil, source = nil, value = nil)
@destination, @event, @source, @value = destination, event, source, value
end
end
# -- Router --
# A Router returns a Route in response to an HTTP request
class Router
EVENT_REGEX = %r{^(?:.+)/events/(?:([^/\.]+)(?:\.([^/\.]+)?)?)(?:/(?:([^\.]+)?))?}
attr_reader :application, :pattern, :keys, :path, :page, :score
def initialize(options={})
@application = options[:application]
@path = options[:path]
@page = options[:page]
if @path
compile_path
compute_score
else
@score = 3 # since "/*" scores at 2
end
end
def route(request = nil)
# get the event information if any
value, source, event = request.path_info.match(EVENT_REGEX).to_a.reverse if request
Route.new(@page, event, source, value)
end
def matches?(request)
request.path_info.gsub(/\/events\/.*/, '').match(@pattern) != nil
end
def inject_parameters_into_page_instance(page, request)
# extract parameters and named parameters from request
if @pattern && @page && match = @pattern.match(request.path_info.gsub(/\/events\/.*/, ''))
values = match.captures.to_a
params =
if @keys.any?
@keys.zip(values).inject({}) do |hash,(k,v)|
if k == 'splat'
(hash[k] ||= []) << v
else
hash[k] = v
end
hash
end
elsif values.any?
{'captures' => values}
else
{}
end
params << request.params
params.each_pair { |name, value| page.instance_variable_set("@#{name}".to_sym, value) }
end
end
def to_s
@path
end
private
# borrowed (stolen) from Sinatra!
def compile_path
@keys = []
if @path.respond_to? :to_str
special_chars = %w{. + ( )}
pattern =
@path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
case match
when "*"
@keys << 'splat'
"(.*?)"
when *special_chars
Regexp.escape(match)
else
@keys << $2[1..-1]
"([^/?]+)"
end
end
@pattern = /^#{pattern}$/
elsif @path.respond_to?(:keys) && @path.respond_to?(:match)
@pattern = @path
@keys = @path.keys
elsif @path.respond_to? :match
@pattern = path
else
raise TypeError, @path
end
end
def compute_score
score = 0
parts = @path.split('/').delete_if {|part| part.empty? }
parts.each_index do |index|
part = parts[index]
power = parts.size - index
factor = part.match('\*') ? 1 : (part.match(':') ? 2 : 3)
score = score + (factor * (2**index))
end
@score = score
end
end
# -- DefaultRouter --
# The default routing scheme is in the form /page[.event[_source]][/value][?query_params]
class DefaultRouter < Router
ROUTE_REGEX = %r{^/([^/]+)(?:/(?:events/(?:([^/\.]+)(?:\.([^/\.]+)?)?)(?:/(?:([^\.]+)?))?)?)?}
def route(request)
value, source, event, destination = request.path_info.match(ROUTE_REGEX).to_a.reverse
destination = @application.class.homepage unless destination
page = Page.get_page(destination.to_sym)
Route.new(page, event, source, value)
end
def matches?(request)
request.path_info.match(ROUTE_REGEX) != nil
end
def self.to_uri(options={})
# get options
url_root = options[:url_root]
page = options[:page]
event = options[:event]
source = options[:source]
value = options[:value]
destination = page
url_root = "/"
if page.kind_of?(Trellis::Page)
destination = page.path || page.class.class_to_sym
root = page.class.url_root
url_root = (root && !root.empty?) ? "/#{root}" : '/'
end
source = source ? ".#{source}" : ''
value = value ? "/#{value}" : ''
event_info = event ? "/events/#{event}#{source}#{value}" : ''
"#{url_root}#{destination}#{event_info}"
end
end
# -- Redirect --
# Encapsulates an HTTP redirect (is the object returned by Page#redirect method)
class Redirect
attr_reader :target, :status
def initialize(target, status=nil)
status = 302 unless status
raise ArgumentError.new("#{status} is not a valid redirect status") unless status >= 300 && status < 400
@target, @status = target, status
end
def apply_to(request, response)
response.status = status
response["Location"] = "#{request.script_name}#{target.starts_with?('/') ? '' : '/'}#{target}"
end
end
# -- Page --
# Represents a Web Page in a Trellis Application. A Page can contain multiple
# components and it defines a template or view either as an external file
# (xml, xhtml, other) or programmatically using Markaby or HAML
# A Trellis Page contains listener methods to respond to events trigger by
# components in the same page or other pages
class Page
include Nokogiri::XML
@@subclasses = Hash.new
@@template_registry = Hash.new
attr_accessor :application, :params, :path, :logger
def self.inherited(child) #:nodoc:
sym = child.class_to_sym
@@subclasses[sym] = child if sym
child.instance_variable_set(:@name, child.underscore_class_name)
child.attr_array(:pages, :create_accessor => false)
child.attr_array(:components)
child.attr_array(:stateful_components)
child.attr_array(:persistents)
child.class_attr_accessor :url_root
child.class_attr_accessor :name
child.class_attr_accessor :router
child.meta_def(:add_stateful_component) { |tid,clazz| @stateful_components << [tid,clazz] }
locate_template child
super
end
def self.layout
@layout
end
def self.template(body = nil, options = nil, &block)
@format = (options[:format] if options) || :html
@layout = (options[:layout] if options)
if block_given?
mab = Markaby::Builder.new({}, self, &block)
html = mab.to_s
else
case @format
when :haml
html = Haml::Engine.new(body).render
when :textile
html = RedCloth.new(body).to_html
when :markdown
if @layout
html = BlueCloth.new(body).to_html
else
html = Markaby.build { thtml { body { text "#{BlueCloth.new(body).to_html}" } }}
end
else # assume the body is (x)html, also eruby is treated as (x)html at this point
html = body
end
end
# hack to prevent nokogiri form stripping namespace prefix on xml fragments
if @layout
html = %[
#{html}
]
end
@template = Nokogiri::XML(html)
find_components
end
def self.dom
# try to reload the template if it wasn't found on during inherited
# since it could have failed if the app was not mounted as root
unless @template
Application.logger.debug "parsed template was no loaded, attempting to load..."
locate_template(self)
end
@template
end
def self.to_xml(options = {})
options[:no_declaration] ? dom.to_xml(:save_with => Node::SaveOptions::NO_DECLARATION) : dom.to_xml
end
def self.format
@format
end
def self.pages(*syms)
@pages = @pages | syms
end
def self.route(path)
router = Router.new(:path => path, :page => self)
self.instance_variable_set(:@router, router)
end
def self.persistent(*fields)
instance_attr_accessor fields
@persistents = @persistents | fields
end
def self.get_page(sym)
@@subclasses[sym]
end
def self.subclasses
@@subclasses
end
def initialize # TODO this is Ugly.... should not do it in initialize since it'll require super in child classes
self.class.stateful_components.each do |id_component|
id_component[1].enhance_page(self, id_component[0])
end
@logger = Application.logger
end
def redirect(path, status=nil)
Redirect.new(path, status)
end
def process_event(event, value, source, session)
method = source ? "on_#{event}_from_#{source}" : "on_#{event}"
# execute the method passing the value if necessary
method_result = value ? send(method.to_sym, Rack::Utils.unescape(value)) : send(method.to_sym)
# determine navigation flow based on the return value of the method call
if method_result
if method_result.kind_of?(Trellis::Page)
page = method_result
# save the current page persistent information
if self != method_result
save_page_session_information(session)
page.inject_dependent_pages
page.call_if_provided(:before_load)
end
# save the persistent information before rendering a response
page.save_page_session_information(session)
end
end
method_result
end
def load_page_session_information(session)
load_persistent_fields_data(session)
load_stateful_components_data(session)
end
def save_page_session_information(session)
save_persistent_fields_data(session)
save_stateful_components_data(session)
end
def render
call_if_provided(:before_render)
result = Renderer.new(self).render
call_if_provided(:after_render)
result
end
def render_partial(name, locals={})
Renderer.new(self).render_partial(name, locals)
end
# inject an instance of each of the injected pages classes as instance variables
# of the current page
def inject_dependent_pages
self.class.inject_dependent_pages(self)
end
def self.inject_dependent_pages(target)
@pages.each do |sym|
clazz = Page.get_page(sym)
# if the injected page class is not found
# throw an exception in production mode or
# dynamically generate a page in development mode
unless clazz
target_class = sym.to_s.camelcase(:upper)
Application.logger.debug "creating stand in page class #{target_class} for symbol #{sym}"
clazz = Page.create_child(target_class) do
def method_missing(sym, *args, &block)
Application.logger.debug "faking response to #{sym}(#{args}) from #{self} an instance of #{self.class}"
self
end
template do
thtml {
head { title "Stand-in Page" }
body { h1 { text %[Stand-in Page for ] }}
}
end
end
Page.subclasses[sym] = clazz
end
Application.logger.debug "injecting an instance of #{clazz} for #{sym}"
target.instance_variable_set("@#{sym}".to_sym, clazz.new)
target.meta_def(sym) { instance_variable_get("@#{sym}") }
end
end
def self.template_registry
@@template_registry
end
private
def self.locate_template(clazz)
begin
if clazz.url_root.nil? || clazz.url_root.empty?
dir = "#{File.dirname($0)}/../html/"
else
dir = "#{File.dirname($0)}#{clazz.url_root}/html/".gsub("Rack: ", '')
end
base = "#{clazz.underscore_class_name}"
Application.logger.debug "looking for template #{base} in #{dir}"
TEMPLATE_FORMATS.each do |format|
filename = "#{base}.#{format}"
file = File.find_first(dir, filename)
if file
Application.logger.debug "found template for page => #{clazz}: #{filename}"
File.open(file, "r") { |f| clazz.template(f.read, :format => format) }
# add the template file to the external template registry so that we can reload it
@@template_registry["#{dir}#{filename}"] = clazz.class_to_sym
Application.logger.debug "registering template file for #{clazz.class_to_sym} => #{dir}#{filename}"
break
end
end
rescue Exception => e
Application.logger.debug "no template found for page => #{clazz}: #{base} : #{e}"
end
end
def self.find_components
@components.clear
classes_processed = []
# look for component declarations in the template
@template.xpath("//trellis:*", 'trellis' => "http://trellisframework.org/schema/trellis_1_0_0.xsd").each do |element|
# retrieve the component class
component = Component.get_component(element.name.to_sym)
# for components that are contained in other components
# pass the parent information (parent tid)
unless component.containers.empty?
parent = nil
# loop over all the container types until we find the matching parent
component.containers.each do |container|
parent = element.xpath("ancestor::trellis:#{container}", 'trellis' => "http://trellisframework.org/schema/trellis_1_0_0.xsd").first
break if parent
end
element['parent_tid'] = parent['tid'] if parent
end
tid = element['tid']
unless component
Application.logger.info "could not find #{element.name} in component hash"
else
# add component class to the page component list
components << component
add_stateful_component(tid, component) #should I always do this?
process_component_contributions(component, classes_processed, element.attributes)
# also process any component dependencies - this should be recursive
component.dependencies.each do |dependency|
components << dependency
process_component_contributions(dependency, classes_processed)
end
end
end
end
def self.process_component_contributions(component, classes_processed, attributes=nil)
unless classes_processed.include?(component)
component.add_style_links_to_page(self, attributes)
component.add_script_links_to_page(self, attributes)
component.add_class_styles_to_page(self, attributes)
component.add_class_scripts_to_page(self, attributes)
component.add_document_modifications_to_page(self)
end
component.add_styles_to_page(self, attributes)
component.add_scripts_to_page(self, attributes)
classes_processed << component unless classes_processed.include?(component)
end
def load_persistent_fields_data(session)
self.class.persistents.each do |persistent_field|
field = "@#{persistent_field}".to_sym
current_value = instance_variable_get(field)
new_value = session["#{self.class}_#{persistent_field}"]
if current_value != new_value && new_value != nil
instance_variable_set(field, new_value)
end
end
end
def load_stateful_components_data(session)
self.instance_variables.each do |instance_variable_name|
instance_variable = self.instance_variable_get(instance_variable_name.to_sym)
instance_variable.load_component_session_information(self, instance_variable_name, session) if instance_variable.respond_to?(:load_component_session_information)
end
end
def save_persistent_fields_data(session)
self.class.persistents.each do |persistent_field|
session["#{self.class}_#{persistent_field}"] = instance_variable_get("@#{persistent_field}".to_sym)
end
end
def save_stateful_components_data(session)
self.instance_variables.each do |instance_variable_name|
instance_variable = self.instance_variable_get(instance_variable_name.to_sym)
instance_variable.save_component_session_information(self, instance_variable_name, session) if instance_variable.respond_to?(:save_component_session_information)
end
end
end # page
# -- Renderer --
# Responsible for processing tags/components in the page templates
# Uses the Radius context object onto which components registered themselves
# (the tags that they respond to)
class Renderer
include Radius
SKIP_METHODS = ['before_load', 'after_load', 'before_render', 'after_render', 'get']
INCLUDE_METHODS = ['render_partial']
def initialize(page)
@page = page
@context = Context.new
# context for erubis templates
@eruby_context = Erubis::Context.new #if @page.class.format == :eruby
# add all instance variables in the page as values accesible from the tags
page.instance_variables.each do |var|
value = page.instance_variable_get(var)
unless value.kind_of?(Trellis::Page)
sym = "#{var}=".split('@').last.to_sym
@context.globals.send(sym, value)
@eruby_context["#{var}".split('@').last] = value #if @eruby_context
end
end
# add other useful values to the tag context
@context.globals.send(:page_name=, page.class.to_s)
@eruby_context[:page_name] = page.class.to_s #if @eruby_context
# add public page methods to the context
page.public_methods(false).each do |method_name|
# skip event handlers and the 'get' method
unless method_name.starts_with?('on_') || SKIP_METHODS.include?(method_name)
@eruby_context.meta_def(method_name) do |*args|
page.send(method_name.to_sym, *args)
end #if @eruby_context
@context.globals.meta_def(method_name) do |*args|
page.send(method_name.to_sym, *args)
end
end
end
# add page helper methods to the context
INCLUDE_METHODS.each do |method_name|
@eruby_context.meta_def(method_name) do |*args|
page.send(method_name.to_sym, *args)
end #if @eruby_context
@context.globals.meta_def(method_name) do |*args|
page.send(method_name.to_sym, *args)
end
end
# add public application methods to the context
page.application.public_methods(false).each do |method_name|
@eruby_context.meta_def(method_name) do |*args|
page.application.send(method_name.to_sym, *args)
end #if @eruby_context
@context.globals.meta_def(method_name) do |*args|
page.application.send(method_name.to_sym, *args)
end
end
# add the page to the context too
@context.globals.page = page
@eruby_context[:page] = page #if @eruby_context
# register the components contained in the page with the renderer's context
page.class.components.each do |component|
component.register_with_tag_context(@context)
end
@parser = Parser.new(@context, :tag_prefix => 'trellis')
end
def render
preprocessed = ""
layout_id = @page.class.layout
template = layout_id ? @page.class.to_xml(:no_declaration => true) : @page.class.to_xml
if layout_id
# page has a layout
# retrieve the layout from the application
layout = Application.layouts[layout_id]
# render the page template to a variable
if @page.class.format == :eruby
body = Erubis::PI::Eruby.new(template, :trim => false).evaluate(@eruby_context)
@eruby_context[:body] = body
else
@eruby_context[:body] = template
end
# render the layout around the page template
preprocessed = Erubis::PI::Eruby.new(layout.to_xml, :trim => false).evaluate(@eruby_context)
# clean up nokogiri namespace hack, see Page#template
doc = Nokogiri::XML(preprocessed)
to_be_removed = doc.at_css(%[div[id="trellis_remove"]])
parent = to_be_removed.parent
to_be_removed.children.each { |child| child.parent = parent }
to_be_removed.remove
preprocessed = doc.to_xml
else
# page has no layout
if @page.class.format == :eruby
preprocessed = Erubis::PI::Eruby.new(template, :trim => false).evaluate(@eruby_context)
else
preprocessed = template
end
end
# radius parsing
@parser.parse(preprocessed)
end
def render_partial(name, locals={})
partial = Application.partials[name]
if partial
if partial.format == :eruby
locals.each_pair { |n,v| @eruby_context[n] = v }
preprocessed = Erubis::PI::Eruby.new(partial.to_xml, :trim => false).evaluate(@eruby_context)
@parser.parse(preprocessed)
else
@parser.parse(partial.to_xml)
end
end
end
end # renderer
# -- Component --
# The component represents a stateless (tag) or a stateful components. Trellis
# components can provide contributions to the page. The contributions can be
# javascript, css stylesheets either at the class level or on a per instance
# basis. Components contain parameters that can be coerced or casted to a
# particular type before being handed to the event handling code
class Component
# the page instance containing the component
attr_accessor :page, :logger
@@components = {}
def initialize()
@logger = Application.logger
end
def self.inherited(child) #:nodoc:
# component registration
@@components[child.class_to_sym] = child
child.class_attr_accessor(:body)
child.class_attr_accessor(:cname)
child.cname = child.underscore_class_name
child.meta_def(:stateful?) { @stateful }
child.attr_array(:fields, :create_accessor => false)
child.attr_array(:style_links)
child.attr_array(:script_links)
child.attr_array(:scripts)
child.attr_array(:class_scripts)
child.attr_array(:styles)
child.attr_array(:class_styles)
child.attr_array(:persistents)
child.attr_array(:dependencies)
child.attr_array(:document_modifications)
child.attr_array(:containers)
Application.logger.debug "registered component for tag #{child.cname} => class #{child}"
super
end
def self.tag_name(name)
@cname = name
end
def self.contained_in(*args)
@containers = @containers | args
end
def self.field(sym, options=nil)
# extract options
coherce_to = options[:coherce_to] if options
default_value = options[:defaults_to] if options
persistent = options[:persistent] if options
@fields << sym
# add an instance field to the component
attr_accessor sym
# store in array of persistent fields
persistents << sym if persistent
# castings
if coherce_to
meta_def("#{sym}=") do |value|
Application.logger.debug "casting value #{sym} to #{coherce_to}"
self.instance_variable_set("@#{sym}", value)
end
end
send("#{sym}=", default_value) if default_value
end
def self.add_style_links_to_page(page, attributes)
style_links.each do |href|
href = href.replace_ant_style_properties(attributes) if attributes
builder = Builder::XmlMarkup.new
link = builder.link(:rel => "stylesheet", :type => "text/css", :href => href)
page.dom.at_css("html/head").children.last.after("\n#{link}")
end
end
def self.add_script_links_to_page(page, attributes)
script_links.each do |src|
src = src.replace_ant_style_properties(attributes) if attributes
builder = Builder::XmlMarkup.new
script = builder.script('', :type => "text/javascript", :src => src)
page.dom.at_css("html/head").children.last.after("\n#{script}")
end
end
def self.add_class_styles_to_page(page, attributes)
class_styles.each do |body|
body = body.replace_ant_style_properties(attributes) if attributes
builder = Builder::XmlMarkup.new
style = builder.style(:type => "text/css") do |builder|
builder << body
end
page.dom.at_css("html/head").children.last.after("\n#{style}")
end
end
def self.add_class_scripts_to_page(page, attributes)
class_scripts.each do |body|
body = body.replace_ant_style_properties(attributes) if attributes
builder = Builder::XmlMarkup.new
script = builder.script(:type => "text/javascript") do |builder|
builder << body
end
page.dom.at_css("html/body").children.last.after("\n#{script}")
end
end
def self.add_styles_to_page(page, attributes)
styles.each do |body|
body = body.replace_ant_style_properties(attributes) if attributes
builder = Builder::XmlMarkup.new
style = builder.style(:type => "text/css") do |builder|
builder << body
end
page.dom.at_css("html/head").children.last.after("\n#{style}")
end
end
def self.add_scripts_to_page(page, attributes)
scripts.each do |body|
body = body.replace_ant_style_properties(attributes) if attributes
builder = Builder::XmlMarkup.new
script = builder.script(:type => "text/javascript") do |builder|
builder << body
end
page.dom.at_css("html/body").children.last.after("\n#{script}")
end
end
def self.add_document_modifications_to_page(page)
document_modifications.each do |block|
page.dom.instance_eval(&block)
end
end
def self.page_contribution(sym, contribution=nil, options=nil, &block)
unless (sym == :dom && block_given?)
# add the contribution to the appropriate array of contributions
# scripts, class_scripts, styles, class_styles, script_links, style_links
scope = options[:scope] || :class if options
receiver = sym.to_s.plural
receiver = "class_#{receiver}" if scope == :class
instance_variable_get("@#{receiver}").send(:<<, contribution)
else
@document_modifications << block
end
end
def self.get_component(sym)
@@components[sym]
end
def self.render(&body)
@body = body
end
def self.register_with_tag_context(context)
Application.logger.debug "registering #{self} with tag context"
if @containers.empty?
context.define_tag("#{@cname}", {}, &@body)
else
@containers.each do |container|
Application.logger.debug "=> registering tag name #{container}:#{@cname}"
context.define_tag("#{container}:#{@cname}", {}, &@body)
end
end
end
def self.is_stateful
instance_variable_set "@stateful".to_sym, true
end
def self.depends_on(*syms)
syms.each do |sym|
component = Component.get_component(sym)
dependencies << component if component
end
end
def save_component_session_information(page, instance_variable_name, session_data)
self.class.persistents.each do |field|
key = "#{page.class}_#{self.class}_#{instance_variable_name}_#{field}"
session_data[key] = instance_variable_get("@#{field}".to_sym) if session_data
end
end
def load_component_session_information(page, instance_variable_name, session_data)
self.class.persistents.each do |field|
field_sym = "@#{field}".to_sym
current_value = instance_variable_get(field_sym)
new_value = session_data["#{page.class}_#{self.class}_#{instance_variable_name}_#{field}"] if session_data
if current_value != new_value && new_value != nil
instance_variable_set(field_sym, new_value)
end
end
end
private
# - takes a page parameter
# - adds an instance of the component with the given id to the page
# - adds a wrapper method that
# - is named on_event_from_source where source is the specific component id
# - calls the component instance on_event method passing any params
# - responds with a redirect to a page or a value
def self.enhance_page(page, id)
if @stateful
cname = @cname
# create an instance of the component
component_instance = self.new #TODO maybe this constructor can take the page
# set the page on the instance of the component
component_instance.page = page
# set the component instance by id on the page
page.instance_variable_set("@#{cname}_#{id}".to_sym, component_instance)
# add an accessor method to get to the component instance
page.meta_def("#{cname}_#{id}") do
self.instance_variable_get("@#{cname}_#{id}".to_sym)
end
# create pass-through methods for each event handler in the component (on_something methods)
self.public_instance_methods.each do |method_name|
if method_name.starts_with?('on_')
page.meta_def("#{method_name}_from_#{cname}_#{id}") do |*args|
result = page.instance_variable_get("@#{cname}_#{id}".to_sym).send(method_name.to_sym, *args)
# if the method returns a page, navigate to that page, otherwise navigate to the source page
(result && result.kind_of?(String)) ? result : page
end
end
end
end
end
end
# load trellis core components
require 'trellis/component_library/core_components'
require 'trellis/component_library/grid'
require 'trellis/component_library/object_editor'
end