module Pakyow module Presenter class View class << self attr_accessor :binders, :default_view_path, :default_is_root_view def view_store Pakyow.app.presenter.current_view_lookup_store end def binder_for_scope(scope, bindable) bindings = Pakyow.app.presenter.bindings(scope) bindings.bindable = bindable return bindings end def view_path(dvp, dirv=false) self.default_view_path = dvp self.default_is_root_view = dirv end def self_closing_tag?(tag) %w[area base basefont br hr input img link meta].include? tag end def form_field?(tag) %w[input select textarea button].include? tag end def tag_without_value?(tag) %w[select].include? tag end def at_path(view_path) v = self.new(self.view_store.root_path(view_path), true) v.compile(view_path) end def root_at_path(view_path) self.new(self.view_store.root_path(view_path), true) end end attr_accessor :doc, :scoped_as, :scopes attr_writer :bindings def dup v = self.class.new(@doc.dup) v.scoped_as = self.scoped_as v end def initialize(arg=nil, is_root_view=false) arg = self.class.default_view_path if arg.nil? && self.class.default_view_path is_root_view = self.class.default_is_root_view if arg.nil? && self.class.default_is_root_view if arg.is_a?(Nokogiri::XML::Element) || arg.is_a?(Nokogiri::XML::Document) || arg.is_a?(Nokogiri::HTML::DocumentFragment) @doc = arg elsif arg.is_a?(Pakyow::Presenter::ViewCollection) @doc = arg.first.doc.dup elsif arg.is_a?(Pakyow::Presenter::View) @doc = arg.doc.dup elsif arg.is_a?(String) view_path = self.class.view_store.real_path(arg) # run parsers format = StringUtils.split_at_last_dot(view_path)[1].to_sym content = parse_content(File.read(view_path), format) if is_root_view then @doc = Nokogiri::HTML::Document.parse(content) else @doc = Nokogiri::HTML.fragment(content) end else raise ArgumentError, "No View for you! Come back, one year." end end def compile(view_path) return unless view_info = self.class.view_store.view_info(view_path) self.populate_view(self, view_info[:views]) end def parse_content(content, format) begin Pakyow.app.presenter.parser_store[format].call(content) rescue Log.warn("No parser defined for extension #{format}") unless format.to_sym == :html content end end def title=(title) if @doc if o = @doc.css('title').first o.inner_html = Nokogiri::HTML::fragment(title) else if o = @doc.css('head').first o.add_child(Nokogiri::HTML::fragment("#{title}")) end end end end def title o = @doc.css('title').first o.inner_html if o end def to_html(container = nil) if container if o = @doc.css("*[#{Configuration::Presenter.container_attribute}='#{container}']").first o.inner_html else '' end else @doc.to_html end end alias :to_s :to_html # Allows multiple attributes to be set at once. # root_view.find(selector).attributes(:class => my_class, :style => my_style) # def attributes(*args) if args.empty? return Attributes.new(self) else #TODO mass assign attributes (if we still want to do this) #TODO use this instead of (or combine with) bind_attributes_to_doc? end # if args.empty? # @previous_method = :attributes # return self # else # args[0].each_pair { |name, value| # @previous_method = :attributes # self.send(name.to_sym, value) # } # end end alias :attrs :attributes def remove self.doc.remove end alias :delete :remove #TODO replace this with a different syntax (?): view.attributes.class.add/remove/has?(:foo) # def add_class(val) # self.doc['class'] = "#{self.doc['class']} #{val}".strip # end # def remove_class(val) # self.doc['class'] = self.doc['class'].gsub(val.to_s, '').strip if self.doc['class'] # end # def has_class(val) # self.doc['class'].include? val # end def clear return if self.doc.blank? self.doc.inner_html = '' end def text self.doc.inner_text end def content self.doc.inner_html end alias :html :content def content=(content) self.doc.inner_html = Nokogiri::HTML.fragment(content.to_s) end alias :html= :content= def append(view) self.doc.add_child(view.doc) end def after(view) self.doc.after(view.doc) end def before(view) self.doc.before(view.doc) end def scope(name) name = name.to_sym views = ViewCollection.new self.bindings.select{|b| b[:scope] == name}.each{|s| v = self.view_from_path(s[:path]) v.bindings = self.bindings_for_child_view(v) v.scoped_as = s[:scope] views << v } views end def prop(name) name = name.to_sym views = ViewCollection.new self.bindings.each {|binding| binding[:props].each {|prop| if prop[:prop] == name v = self.view_from_path(prop[:path]) v.bindings = self.bindings_for_child_view(v) views << v end } } views end # call-seq: # with {|view| block} # # Creates a context in which view manipulations can be performed. # # Unlike previous versions, the context can only be referenced by the # block argument. No `context` method will be available.s # def with yield(self) end # call-seq: # for {|view, datum| block} # # Yields a view and its matching dataum. This is driven by the view, # meaning datums are yielded until no more views are available. For # the single View case, only one view/datum pair is yielded. # # (this is basically Bret's `map` function) # def for(data, &block) data = [data] unless data.instance_of?(Array) block.call(self, data[0]) end # call-seq: # match(data) => ViewCollection # # Returns a ViewCollection object that has been manipulated to match the data. # For the single View case, the ViewCollection collection will consist n copies # of self, where n = data.length. # def match(data) data = [data] unless data.instance_of?(Array) views = ViewCollection.new data.each {|datum| d_v = self.doc.dup self.doc.before(d_v) v = View.new(d_v) v.bindings = self.bindings #TODO set view scope views << v } self.remove views end # call-seq: # repeat(data) {|view, datum| block} # # Matches self with data and yields a view/datum pair. # def repeat(data, &block) self.match(data).for(data, &block) end # call-seq: # bind(data) # # Binds data across existing scopes. # def bind(data, bindings = nil, &block) scope = self.bindings.first binder = View.binder_for_scope(scope[:scope], data) binder.merge(bindings) self.bind_data_to_scope(data, scope, binder) yield(self, data) if block_given? end # call-seq: # apply(data) # # Matches self to data then binds data to the view. # def apply(data, bindings = nil, &block) views = self.match(data).bind(data, bindings, &block) end def container(name) matches = self.containers.select{|c| c[:name].to_sym == name.to_sym} vs = ViewCollection.new matches.each{|m| vs << view_from_path(m[:path])} vs end def containers @containers ||= self.find_containers end def bindings @bindings ||= self.find_bindings end protected def add_content_to_container(content, container) content = content.doc unless content.class == String || content.class == Nokogiri::HTML::DocumentFragment || content.class == Nokogiri::XML::Element container.add_child(content) end def reset_container(container) container.inner_html = '' end # populates the root_view using view_store data by recursively building # and substituting in child views named in the structure def populate_view(root_view, view_info) root_view.containers.each {|e| next unless path = view_info[e[:name]] v = self.populate_view(View.new(path), view_info) self.reset_container(e[:doc]) self.add_content_to_container(v, e[:doc]) } root_view end # returns an array of hashes, each with the container name and doc def find_containers elements = [] @doc.traverse {|e| if name = e.attr(Configuration::Presenter.container_attribute) elements << { :name => name, :doc => e, :path => path_to(e)} end } elements end # returns an array of hashes that describe each scope def find_bindings bindings = [] breadth_first(@doc) {|o| next unless scope = o[Configuration::Presenter.scope_attribute] # find props props = [] breadth_first(o) {|so| # don't go into deeper scopes throw :reject if so != o && so[Configuration::Presenter.scope_attribute] next unless prop = so[Configuration::Presenter.prop_attribute] props << {:prop => prop.to_sym, :path => path_to(so)} } bindings << {:scope => scope.to_sym, :path => path_to(o), :props => props} } # determine nestedness (currently unused; leaving in case needed) # bindings.each {|b| # nested = [] # bindings.each {|b2| # b_doc = doc_from_path(b[:path]) # b2_doc = doc_from_path(b2[:path]) # nested << b2 if b2_doc.ancestors.include? b_doc # } # b[:nested_scopes] = nested # } return bindings end def bindings_for_child_view(child) child_path = self.path_to(child.doc) child_path_len = child_path.length child_bindings = [] self.bindings.each {|binding| # we want paths within the child path if (child_path - binding[:path]).empty? # update paths relative to child dup = Marshal.load(Marshal.dump(binding)) [dup].concat(dup[:props]).each{|p| p[:path] = p[:path][child_path_len..-1] } child_bindings << dup end } child_bindings end def breadth_first(doc) queue = [doc] until queue.empty? node = queue.shift catch(:reject) { yield node queue.concat(node.children) } end end def path_to(child) path = [] return path if child == @doc child.ancestors.each {|a| # since ancestors goes all the way to doc root, stop when we get to the level of @doc break if a.children.include?(@doc) path.unshift(a.children.index(child)) child = a } return path end def doc_from_path(path) o = @doc # if path is empty we're at self return o if path.empty? path.each {|i| if child = o.children[i] o = child else break end } return o end def view_from_path(path) View.new(doc_from_path(path)) end def bind_data_to_scope(data, scope, binder = nil) return unless data # handle root binding if binder && v = binder.value_for_prop(:_root) v.is_a?(Hash) ? self.bind_attributes_to_doc(v, self.doc) : self.bind_value_to_doc(v, self.doc) end scope[:props].each {|p| k = p[:prop] v = binder ? binder.value_for_prop(k) : data[k] doc = doc_from_path(p[:path]) # handle form field self.bind_to_form_field(doc, scope, k, v, binder) if View.form_field?(doc.name) # bind attributes or value v.is_a?(Hash) ? self.bind_attributes_to_doc(v, doc) : self.bind_value_to_doc(v, doc) } end def bind_value_to_doc(value, doc) return unless value tag = doc.name return if View.tag_without_value?(tag) View.self_closing_tag?(tag) ? doc['value'] = value : doc.inner_html = value end def bind_attributes_to_doc(attrs, doc) attrs.each do |attr, v| if attr == :content v = v.call(doc.inner_html) if v.is_a?(Proc) bind_value_to_doc(v, doc) next end attr = attr.to_s v = v.call(doc[attr]) if v.is_a?(Proc) v.nil? ? doc.remove_attribute(attr) : doc[attr] = v.to_s end end #TODO refactor to use new options_for def bind_to_form_field(doc, scope, prop, value, binder) return unless !doc['name'] || doc['name'].empty? # set name on form element doc['name'] = "#{scope[:scope]}[#{prop}]" # special binding for checkboxes and radio buttons if doc.name == 'input' && (doc[:type] == 'checkbox' || doc[:type] == 'radio') if value == true || (doc[:value] && doc[:value] == value.to_s) doc[:checked] = 'checked' else doc.delete('checked') end # coerce to string since booleans are often used # and fail when binding to a view value = value.to_s # special binding for selects elsif doc.name == 'select' && binder && options = binder.options_for_prop(prop) option_nodes = Nokogiri::HTML::DocumentFragment.parse "" Nokogiri::HTML::Builder.with(option_nodes) do |h| until options.length == 0 catch :optgroup do options.each_with_index { |o,i| # an array containing value/content if o.is_a?(Array) h.option o[1], :value => o[0] options.delete_at(i) # likely an object (e.g. string); start a group else h.optgroup(:label => o) { options.delete_at(i) options[i..-1].each_with_index { |o2,i2| # starting a new group throw :optgroup if !o2.is_a?(Array) h.option o2[1], :value => o2[0] options.delete_at(i) } } end } end end end doc.add_child(option_nodes) end # select appropriate option if o = doc.css('option[value="' + value.to_s + '"]').first o[:selected] = 'selected' end end end end end