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