lib/trellis/trellis.rb in trellis-0.0.6 vs lib/trellis/trellis.rb in trellis-0.0.7
- old
+ new
@@ -28,12 +28,11 @@
require 'trellis/logging'
require 'rubygems'
require 'rack'
require 'radius'
require 'builder'
-require 'hpricot'
-require 'rexml/document'
+require 'nokogiri'
require 'extensions/string'
require 'haml'
require 'markaby'
require 'redcloth'
require 'bluecloth'
@@ -41,22 +40,29 @@
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.meta_def(:logger) { Application.logger }
child.instance_variable_set(:@session_config, OpenStruct.new({:impl => :cookie}))
super
@@ -75,14 +81,37 @@
# 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
@@ -97,11 +126,11 @@
def configured
# configure rack middleware
application = Rack::ShowStatus.new(self)
application = Rack::ShowExceptions.new(application)
- application = Rack::Reloader.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
@@ -124,11 +153,11 @@
def find_router_for(request)
match = Page.subclasses.values.find { |page| page.router && page.router.matches?(request) }
match ? match.router : DefaultRouter.new(:application => self)
end
- # Rack call interface.
+ # rack call interface.
def call(env)
dup.call!(env)
end
# implements the rack specification
@@ -143,10 +172,13 @@
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)
@@ -154,31 +186,110 @@
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 (request.post? || route.event) && result.kind_of?(Trellis::Page)
+ # -------------------------
+
+ 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
- # for render requests simply render the page
- response.body = result.kind_of?(Trellis::Page) ? result.render : result
- response.status = 200
+ # 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|
@@ -314,38 +425,64 @@
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.kind_of?(Trellis::Page) ? (page.path || page.class.class_to_sym) : page
- url_root = page.kind_of?(Trellis::Page) && page.class.url_root ? "/#{page.class.url_root}" : '/' unless url_root
+
+ 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
-
- TEMPLATE_FORMATS = [:html, :xhtml, :haml, :textile, :markdown, :eruby]
+ include Nokogiri::XML
@@subclasses = Hash.new
@@template_registry = Hash.new
- attr_accessor :params, :path, :logger
+ attr_accessor :application, :params, :path, :logger
def self.inherited(child) #:nodoc:
sym = child.class_to_sym
@@subclasses[sym] = child if sym
@@ -355,48 +492,67 @@
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.class_attr_accessor :layout
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
- html = "<html><body>#{BlueCloth.new(body).to_html}</body></html>"
+ 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
- @template = Hpricot.XML(html)
+
+ # hack to prevent nokogiri form stripping namespace prefix on xml fragments
+ if @layout
+ html = %[<div id="trellis_remove" xmlns:trellis="http://trellisframework.org/schema/trellis_1_0_0.xsd">#{html}</div>]
+ end
+
+ @template = Nokogiri::XML(html)
+
find_components
end
- def self.parsed_template
+ 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)
@@ -419,26 +575,26 @@
def self.subclasses
@@subclasses
end
- def initialize # TODO this is Ugly.... should no do it in initialize since it'll require super in child classes
+ 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
- unless value
- method_result = send method.to_sym
- else
- method_result = send method.to_sym, Rack::Utils.unescape(value)
- end
+ # 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
@@ -472,10 +628,14 @@
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
@@ -496,11 +656,11 @@
Application.logger.debug "faking response to #{sym}(#{args}) from #{self} an instance of #{self.class}"
self
end
template do
- xhtml_strict {
+ thtml {
head { title "Stand-in Page" }
body { h1 { text %[Stand-in Page for <trellis:value name="page_name"/>] }}
}
end
end
@@ -548,28 +708,27 @@
end
def self.find_components
@components.clear
classes_processed = []
- doc = REXML::Document.new(@template.to_html)
# look for component declarations in the template
- doc.elements.each('//trellis:*') do |element|
+ @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 = REXML::XPath.first(element, "ancestor::trellis:#{container}")
+ parent = element.xpath("ancestor::trellis:#{container}", 'trellis' => "http://trellisframework.org/schema/trellis_1_0_0.xsd").first
break if parent
end
- element.attributes['parent_tid'] = parent.attributes['tid'] if parent
+ element['parent_tid'] = parent['tid'] if parent
end
- tid = element.attributes['tid']
+ 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
@@ -636,53 +795,130 @@
# 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 = {} if @page.class.format == :eruby
+ @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
+ @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
+ @eruby_context[:page_name] = page.class.to_s #if @eruby_context
- #TODO add public page methods to the 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
+ @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
- unless @page.class.format == :eruby
- @parser.parse(@page.class.parsed_template.to_html)
+ 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
- preprocessed = Erubis::PI::Eruby.new(@page.class.parsed_template.to_html, :trim => false).evaluate(@eruby_context)
- @parser.parse(preprocessed)
+ # 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
@@ -760,70 +996,70 @@
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.parsed_template.at("html/head").containers.last.after("\n#{link}")
+ 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.parsed_template.at("html/head").containers.last.after("\n#{script}")
+ 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.parsed_template.at("html/head").containers.last.after("\n#{style}")
+ 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.parsed_template.at("html/body").containers.last.after("\n#{script}")
+ 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.parsed_template.at("html/head").containers.last.after("\n#{style}")
+ 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.parsed_template.at("html/body").containers.last.after("\n#{script}")
+ 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.parsed_template.instance_eval(&block)
+ page.dom.instance_eval(&block)
end
end
def self.page_contribution(sym, contribution=nil, options=nil, &block)
unless (sym == :dom && block_given?)
@@ -924,6 +1160,6 @@
# load trellis core components
require 'trellis/component_library/core_components'
require 'trellis/component_library/grid'
require 'trellis/component_library/object_editor'
-end
\ No newline at end of file
+end