require 'express_templates/components' module ExpressTemplates class Expander cattr_accessor :module_search_space attr_accessor :stack def self.expand(template, source) expanded = new(template).expand(source) compiled = expanded.map(&:compile) return compiled.join("+") end def initialize(template) @template = template @stack = Stack.new end def expand(source=nil, &block) if source modified = source.gsub(/(\W)(yield)(\.*)?/, '\1 (stack << ExpressTemplates::Components::Yielder.new(\3))') modified.gsub!(/(\W)(@\w+)(\W)?/, '\1 (stack << ExpressTemplates::Components::Wrapper.new("\2") )\3') instance_eval(modified, @template.inspect) stack.current else instance_eval &block stack.current end end # define a "macro" method for a component # these methods accept args which are passed to the # initializer for the component # blocks supplied are evaluated and any returned objects are # added as children to the component def self.register_macros_for(*components) components.each do |component| define_method(component.macro_name.to_sym) do |*args, &block| stack << if block begin stack.descend! block.call # anything stored on stack.current or on stack.next is added as a child # this is a bit problematic in the case where we would have # blocks and helpers or locals mixed component.new(*(args.push(*(stack.current)))) ensure stack.ascend! end else component.new(*(args)) end end end end @module_search_space = [ExpressTemplates::Components] @module_search_space.each do |mod| register_macros_for(* mod.constants.map { |const| [mod.to_s, const.to_s].join("::").constantize }. select { |klass| klass.ancestors.include? (ExpressTemplates::Component) } ) end def method_missing(name, *args) stack.current << ExpressTemplates::Components::Wrapper.new(name.to_s, *args) nil end class Stack def initialize @stack = [[]] @frame = 0 end def all @stack end def dump puts "Current frame: #{@frame}" puts all.map(&:inspect).join("\n") end def <<(child) current << child child end def current @stack[@frame] end def next @stack[@frame+1] ||= [] end def descend! @frame += 1 @stack[@frame] ||= [] @stack[@frame].clear @frame end def ascend! raise "Cannot ascend" if @frame <= 0 current.clear ; self.next.clear @frame -= 1 end end end end