pakyow-presenter/lib/presenter/view.rb in pakyow-presenter-0.8.0 vs pakyow-presenter/lib/presenter/view.rb in pakyow-presenter-0.9.0
- old
+ new
@@ -1,232 +1,135 @@
+require 'forwardable'
+
module Pakyow
module Presenter
class View
- include DocHelpers
- include TitleHelpers
+ extend Forwardable
- PARTIAL_REGEX = /<!--\s*@include\s*([a-zA-Z0-9\-_]*)\s*-->/
+ def_delegators :@doc, :title=, :title, :remove, :clear, :text, :html
- class << self
- attr_accessor :binders
+ # The object responsible for parsing, manipulating, and rendering
+ # the underlying HTML document for the view.
+ #
+ attr_reader :doc
- def self_closing_tag?(tag)
- %w[area base basefont br hr input img link meta].include? tag
- end
+ # The scope, if any, that the view belongs to.
+ #
+ attr_accessor :scoped_as
- def form_field?(tag)
- %w[input select textarea button].include? tag
- end
-
- def tag_without_value?(tag)
- %w[select].include? tag
- end
+ # Creates a view, running `contents` through any registered view processors for `format`.
+ #
+ # @param contents [String] the contents of the view
+ # @param format [Symbol] the format of contents
+ #
+ def initialize(contents = '', format: :html)
+ @doc = Config.presenter.view_doc_class.new(Presenter.process(contents, format))
end
- attr_accessor :doc, :scoped_as, :scopes, :related_views, :context, :composer
- attr_writer :bindings
-
- def initialize(contents = '', format = :html)
- @related_views = []
-
- processed = Presenter.process(contents, format)
-
- if processed.match(/<html.*>/)
- @doc = Nokogiri::HTML::Document.parse(processed)
- else
- @doc = Nokogiri::HTML.fragment(processed)
- end
- end
-
def initialize_copy(original_view)
super
@doc = original_view.doc.dup
@scoped_as = original_view.scoped_as
- @context = @context
- @composer = @composer
end
+ # Creates a new view with a soft copy of doc.
+ #
+ def soft_copy
+ copy = View.from_doc(@doc.soft_copy)
+ copy.scoped_as = scoped_as
+ copy
+ end
+
+ # Creates a view from a doc.
+ #
+ # @see StringDoc
+ # @see NokogiriDoc
+ #
def self.from_doc(doc)
- view = self.new
- view.doc = doc
- return view
+ view = new
+ view.instance_variable_set(:@doc, doc)
+ view
end
+ # Creates a view from a file.
+ #
def self.load(path)
- format = Utils::String.split_at_last_dot(path)[-1]
- contents = File.read(path)
+ new(File.read(path), format: File.format(path))
+ end
- return self.new(contents, format)
+ def ==(other)
+ self.class == other.class && @doc == other.doc
end
# Allows multiple attributes to be set at once.
- # root_view.find(selector).attributes(:class => my_class, :style => my_style)
#
- def attributes(attrs = {})
- #TODO this is not invalidating composer
-
- if attrs.empty?
- return Attributes.new(self.doc, @composer)
- else
- self.bind_attributes_to_doc(attrs, doc)
- end
+ # view.attrs(class: '...', style: '...')
+ #
+ def attrs(attrs = {})
+ return Attributes.new(@doc) if attrs.empty?
+ bind_attributes_to_doc(attrs, @doc)
end
- alias :attrs :attributes
-
- def remove
- if doc.parent.nil?
- # best we can do is to remove the children
- doc.children.remove
- else
- doc.remove
- end
-
- invalidate!
- end
-
- alias :delete :remove
-
- def clear
- return if self.doc.blank?
- self.doc.inner_html = ''
- self.invalidate!
- end
-
- def text
- self.doc.inner_text
- end
-
def text=(text)
text = text.call(self.text) if text.is_a?(Proc)
- self.doc.content = text.to_s
- self.invalidate!
+ @doc.text = text
end
- def html
- self.doc.inner_html
- end
-
def html=(html)
html = html.call(self.html) if html.is_a?(Proc)
- self.doc.inner_html = Nokogiri::HTML.fragment(html.to_s)
- self.invalidate!
+ @doc.html = html
end
def append(view)
- doc = view.doc
- num = doc.children.count
- path = self.path_to(doc)
-
- self.doc.add_child(view.doc)
-
- self.update_binding_offset_at_path(num, path)
- self.invalidate!
+ @doc.append(view.doc)
end
def prepend(view)
- doc = view.doc
- num = doc.children.count
- path = self.path_to(doc)
-
- if first_child = self.doc.children.first
- first_child.add_previous_sibling(doc)
- else
- self.doc = doc
- end
-
- self.update_binding_offset_at_path(num, path)
- self.invalidate!
+ @doc.prepend(view.doc)
end
+ #TODO allow strings?
def after(view)
- doc = view.doc
- num = doc.children.count
- path = self.path_to(doc)
-
- self.doc.after(view.doc)
-
- self.update_binding_offset_at_path(num, path)
- self.invalidate!
+ @doc.after(view.doc)
end
def before(view)
- doc = view.doc
- num = doc.children.count
- path = self.path_to(doc)
-
- self.doc.before(view.doc)
-
- self.update_binding_offset_at_path(num, path)
- self.invalidate!
+ @doc.before(view.doc)
end
def replace(view)
- view = view.doc if view.is_a?(View)
-
- if doc.parent.nil?
- doc.children.remove
- doc.inner_html = view
- else
- doc.replace(view)
- end
-
- invalidate!
+ replacement = view.is_a?(View) ? view.doc : view
+ @doc.replace(replacement)
end
def scope(name)
name = name.to_sym
-
- views = ViewCollection.new
- views.context = @context
- views.composer = @composer
- self.bindings.select{|b| b[:scope] == name}.each{|s|
- v = self.view_from_path(s[:path])
-
- v.bindings = self.update_binding_paths_from_path([s].concat(s[:nested_bindings]), s[:path])
- v.scoped_as = s[:scope]
- v.context = @context
- v.composer = @composer
-
- views << v
- }
-
- views
+ @doc.scope(name).inject(ViewCollection.new) do |coll, scope|
+ view = View.from_doc(scope[:doc])
+ view.scoped_as = name
+ coll << view
+ end
end
def prop(name)
name = name.to_sym
-
- views = ViewCollection.new
- views.context = @context
- views.composer = @composer
-
- if binding = self.bindings.select{|binding| binding[:scope] == self.scoped_as}[0]
- binding[:props].each {|prop|
- if prop[:prop] == name
- v = self.view_from_path(prop[:path])
-
- v.scoped_as = self.scoped_as
- v.context = @context
- v.composer = @composer
- views << v
- end
- }
+ @doc.prop(scoped_as, name).inject(ViewCollection.new) do |coll, prop|
+ view = View.from_doc(prop[:doc])
+ view.scoped_as = scoped_as
+ coll << view
end
-
- views
end
# call-seq:
# with {|view| block}
#
# Creates a context in which view manipulations can be performed.
#
def with(&block)
if block.arity == 0
- self.instance_exec(&block)
+ instance_exec(&block)
else
yield(self)
end
self
@@ -240,27 +143,25 @@
# the single View case, only one view/datum pair is yielded.
#
# (this is basically Bret's `map` function)
#
def for(data, &block)
- data = data.to_a if data.is_a?(Enumerator)
- data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
-
+ datum = Array.ensure(data).first
if block.arity == 1
- self.instance_exec(data[0], &block)
+ instance_exec(datum, &block)
else
- block.call(self, data[0])
+ block.call(self, datum)
end
end
# call-seq:
# for_with_index {|view, datum, i| block}
#
# Yields a view, its matching dataum, and the index. See #for.
#
def for_with_index(data, &block)
- self.for(data) do |ctx, datum|
+ self.for(data) do |ctx, datum|
if block.arity == 2
ctx.instance_exec(datum, 0, &block)
else
block.call(ctx, datum, 0)
end
@@ -273,81 +174,77 @@
# 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.to_a if data.is_a?(Enumerator)
- data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
+ data = Array.ensure(data)
+ coll = ViewCollection.new
- views = ViewCollection.new
- views.context = @context
- views.composer = @composer
- data.each {|datum|
- d_v = self.doc.dup
- self.doc.before(d_v)
+ # an empty set always means an empty view
+ if data.empty?
+ remove
+ else
+ # dup for later
+ original_view = dup if data.length > 1
- v = View.from_doc(d_v)
- v.bindings = self.bindings.dup
- v.scoped_as = self.scoped_as
- v.context = @context
- v.composer = @composer
+ # the original view match the first datum
+ coll << self
- views << v
- }
+ # create views for the other datums
+ data[1..-1].inject(coll) { |coll|
+ duped_view = original_view.dup
+ after(duped_view)
+ coll << duped_view
+ }
+ end
- self.remove
- views
+ # return the new collection
+ coll
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)
+ match(data).for(data, &block)
end
# call-seq:
# repeat_with_index(data) {|view, datum, i| block}
#
# Matches self with data and yields a view/datum pair with index.
#
def repeat_with_index(data, &block)
- self.match(data).for_with_index(data, &block)
+ match(data).for_with_index(data, &block)
end
# call-seq:
# bind(data)
#
- # Binds data across existing scopes.
+ # Binds a single datum across existing scopes.
#
- def bind(data, bindings = {}, &block)
- data = data.to_a if data.is_a?(Enumerator)
- data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
-
- scope_info = self.bindings.first
-
- self.bind_data_to_scope(data[0], scope_info, bindings)
- invalidate!(true)
-
+ def bind(data, bindings: {}, context: nil, &block)
+ datum = Array.ensure(data).first
+ bind_data_to_scope(datum, doc.scopes.first, bindings, context)
return if block.nil?
if block.arity == 1
- self.instance_exec(data[0], &block)
+ instance_exec(datum, &block)
else
- block.call(self, data[0])
+ block.call(self, datum)
end
end
# call-seq:
# bind_with_index(data)
#
# Binds data across existing scopes, yielding a view/datum pair with index.
#
- def bind_with_index(data, bindings = {}, &block)
- self.bind(data) do |ctx, datum|
+ def bind_with_index(*a, **k, &block)
+ bind(*a, **k) do |ctx, datum|
if block.arity == 2
ctx.instance_exec(datum, 0, &block)
else
block.call(ctx, datum, 0)
end
@@ -357,335 +254,206 @@
# call-seq:
# apply(data)
#
# Matches self to data then binds data to the view.
#
- def apply(data, bindings = {}, &block)
- self.match(data).bind(data, bindings, &block)
+ def apply(data, bindings: {}, context: nil, &block)
+ match(data).bind(data, bindings: bindings, context: context, &block)
end
- def bindings(refind = false)
- @bindings = (!@bindings || refind) ? self.find_bindings : @bindings
- end
-
def includes(partial_map)
+ partials = @doc.partials
partial_map = partial_map.dup
# mixin all the partials
- partials.each do |partial|
- partial[1].replace(partial_map[partial[0]].to_s)
+ partials.each do |partial_info|
+ partial = partial_map[partial_info[0]]
+ next if partial.nil?
+ partial_info[1].replace(partial.doc.dup)
end
- # now delete them from the map
- partials.each do |partial|
- partial_map.delete(partial[0])
- end
+ # refind the partials
+ partials = @doc.partials
- # we have more partials
- if partial_map.count > 0
- # initiate another build if content contains partials
- includes(partial_map) if partials(true).count > 0
- end
+ # if mixed in partials included partials, we want to run includes again with a new map
+ if partials.count > 0 && (partial_map.keys - partials.keys).count < partial_map.keys.count
+ includes(partial_map)
+ end
- return self
+ self
end
- def invalidate!(composer_only = false)
- self.bindings(true) unless composer_only
- @composer.dirty! unless @composer.nil?
+ def to_html
+ @doc.to_html
+ end
+ alias :to_s :to_html
- @related_views.each {|v|
- v.invalidate!(composer_only)
- }
- end
+ private
- protected
-
- def partials(refind = false)
- @partials = (!@partials || refind) ? find_partials : @partials
- end
-
- def partials_in(content)
- partials = []
-
- content.scan(PARTIAL_REGEX) do |m|
- partials << m[0].to_sym
- end
-
- return partials
- end
-
- def find_partials
- partials = []
-
- @doc.traverse { |e|
- next unless e.is_a?(Nokogiri::XML::Comment)
- next unless match = e.to_html.strip.match(PARTIAL_REGEX)
-
- name = match[1]
- partials << [name.to_sym, e]
- }
-
- return partials
- 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_store, view_info)
- root_view.containers.each {|e|
- next unless path = view_info[e[:name]]
-
- v = self.populate_view(View.new(path, view_store), view_store, view_info)
- v.context = @context
- v.composer = @composer
- self.reset_container(e[:doc])
- self.add_content_to_container(v, e[:doc])
- }
- root_view
- end
-
-
- # returns an array of hashes that describe each scope
- def find_bindings(doc = @doc, ignore_root = false)
- bindings = []
- breadth_first(doc) {|o|
- next if o == doc && ignore_root
- next if !scope = o[Config::Presenter.scope_attribute]
-
- bindings << {
- :scope => scope.to_sym,
- :path => path_to(o),
- :props => find_props(o)
- }
-
- if o == doc
- # this is the root node, which we need as the first hash in the
- # list of bindings, but we don't want to nest other scopes inside
- # of it in this case
- bindings.last[:nested_bindings] = []
- else
- bindings.last[:nested_bindings] = find_bindings(o, true)
- # reject so children aren't traversed
- throw :reject
- end
- }
-
- # find unscoped props
- unless doc[Config::Presenter.scope_attribute]
- bindings.unshift({
- :scope => nil,
- :path => [],
- :props => find_props(doc),
- :nested_bindings => []
- })
- end
-
- return bindings
- end
-
- def find_props(o)
- props = []
- breadth_first(o) {|so|
- # don't go into deeper scopes
- throw :reject if so != o && so[Config::Presenter.scope_attribute]
-
- next unless prop = so[Config::Presenter.prop_attribute]
- props << {:prop => prop.to_sym, :path => path_to(so)}
- }
-
- return props
- end
-
- # returns a new binding set that takes into account the starting point of `path`
- def update_binding_paths_from_path(bindings, path)
- return bindings.collect { |binding|
- dup_binding = binding.dup
- dup_binding[:path] = dup_binding[:path][path.length..-1] || []
-
- dup_binding[:props] = dup_binding[:props].collect {|prop|
- dup_prop = prop.dup
- dup_prop[:path] = dup_prop[:path][path.length..-1]
- dup_prop
- }
-
- dup_binding[:nested_bindings] = update_binding_paths_from_path(dup_binding[:nested_bindings], path)
-
- dup_binding
- }
- end
-
- def update_binding_offset_at_path(offset, path)
- # update binding paths for bindings we're iterating on
- self.bindings.each {|binding|
- next unless self.path_within_path?(binding[:path], path)
-
- binding[:path][0] += offset if binding[:path][0]
-
- binding[:props].each { |prop|
- prop[:path][0] += offset if prop[:path][0]
- }
- }
- end
-
- def bind_data_to_scope(data, scope_info, bindings = {})
+ def bind_data_to_scope(data, scope_info, bindings, ctx)
return unless data
+ return unless scope_info
scope = scope_info[:scope]
+ bind_data_to_root(data, scope, bindings, ctx)
- bind_data_to_root(data, scope, bindings)
-
- scope_info[:props].each { |prop_info|
- catch(:unbound) {
+ scope_info[:props].each do |prop_info|
+ catch(:unbound) do
prop = prop_info[:prop]
- if data_has_prop?(data, prop) || Pakyow.app.presenter.binder.has_prop?(prop, scope, bindings)
- value = Pakyow.app.presenter.binder.value_for_prop(prop, scope, data, bindings, context)
- doc = doc_from_path(prop_info[:path])
+ if data_has_prop?(data, prop) || Binder.instance.has_scoped_prop?(scope, prop, bindings)
+ value = Binder.instance.value_for_scoped_prop(scope, prop, data, bindings, ctx)
+ doc = prop_info[:doc]
- if View.form_field?(doc.name)
- bind_to_form_field(doc, scope, prop, value, data)
+ if DocHelpers.form_field?(doc.tagname)
+ bind_to_form_field(doc, scope, prop, value, data, ctx)
end
bind_data_to_doc(doc, value)
else
handle_unbound_data(scope, prop)
end
- }
- }
+ end
+ end
end
- def bind_data_to_root(data, scope, bindings)
- return unless value = Pakyow.app.presenter.binder.value_for_prop(:_root, scope, data, bindings, context)
- value.is_a?(Hash) ? self.bind_attributes_to_doc(value, self.doc) : self.bind_value_to_doc(value, self.doc)
+ def bind_data_to_root(data, scope, bindings, ctx)
+ value = Binder.instance.value_for_scoped_prop(scope, :_root, data, bindings, ctx)
+ return if value.nil?
+
+ value.is_a?(Hash) ? bind_attributes_to_doc(value, doc) : bind_value_to_doc(value, doc)
end
def bind_data_to_doc(doc, data)
- data.is_a?(Hash) ? self.bind_attributes_to_doc(data, doc) : self.bind_value_to_doc(data, doc)
+ data.is_a?(Hash) ? bind_attributes_to_doc(data, doc) : bind_value_to_doc(data, doc)
end
def data_has_prop?(data, prop)
(data.is_a?(Hash) && (data.key?(prop) || data.key?(prop.to_s))) || (!data.is_a?(Hash) && data.class.method_defined?(prop))
end
def bind_value_to_doc(value, doc)
value = String(value)
- tag = doc.name
- return if View.tag_without_value?(tag)
+ tag = doc.tagname
+ return if DocHelpers.tag_without_value?(tag)
- if View.self_closing_tag?(tag)
+ if DocHelpers.self_closing_tag?(tag)
# don't override value if set
- if !doc['value'] || doc['value'].empty?
- doc['value'] = value
+ if !doc.get_attribute(:value) || doc.get_attribute(:value).empty?
+ doc.set_attribute(:value, value)
end
else
- doc.inner_html = value
+ doc.html = value
end
end
- def bind_attributes_to_doc(attrs, doc)
- attrs.each do |attr, v|
- case attr
- when :content
- v = v.call(doc.inner_html) if v.is_a?(Proc)
- bind_value_to_doc(v, doc)
- next
- when :view
- v.call(self)
- next
- end
-
- attr = attr.to_s
- attrs = Attributes.new(doc)
- v = v.call(attrs.send(attr)) if v.is_a?(Proc)
-
- if v.nil?
- doc.remove_attribute(attr)
- else
- attrs.send(:"#{attr}=", v)
- end
- end
- end
-
- def bind_to_form_field(doc, scope, prop, value, bindable)
+ def bind_to_form_field(doc, scope, prop, value, bindable, ctx)
set_form_field_name(doc, scope, prop)
# special binding for checkboxes and radio buttons
- if doc.name == 'input' && (doc[:type] == 'checkbox' || doc[:type] == 'radio')
+ if doc.tagname == 'input' && (doc.get_attribute(:type) == 'checkbox' || doc.get_attribute(:type) == 'radio')
bind_to_checked_field(doc, value)
- # special binding for selects
- elsif doc.name == 'select'
- bind_to_select_field(doc, scope, prop, value, bindable)
+ # special binding for selects
+ elsif doc.tagname == 'select'
+ bind_to_select_field(doc, scope, prop, value, bindable, ctx)
end
end
def bind_to_checked_field(doc, value)
- if value == true || (doc[:value] && doc[:value] == value.to_s)
- doc[:checked] = 'checked'
+ if value == true || (doc.get_attribute(:value) && doc.get_attribute(:value) == value.to_s)
+ doc.set_attribute(:checked, 'checked')
else
- doc.delete('checked')
+ doc.remove_attribute(:checked)
end
# coerce to string since booleans are often used and fail when binding to a view
- value = value.to_s
+ value.to_s
end
- def bind_to_select_field(doc, scope, prop, value, bindable)
- create_select_options(doc, scope, prop, value, bindable)
+ def bind_to_select_field(doc, scope, prop, value, bindable, ctx)
+ create_select_options(doc, scope, prop, value, bindable, ctx)
select_option_with_value(doc, value)
end
def set_form_field_name(doc, scope, prop)
- return if doc['name'] && !doc['name'].empty? # don't overwrite the name if already defined
- doc['name'] = "#{scope}[#{prop}]"
+ return if doc.get_attribute(:name) && !doc.get_attribute(:name).empty? # don't overwrite the name if already defined
+ doc.set_attribute(:name, "#{scope}[#{prop}]")
end
- def create_select_options(doc, scope, prop, value, bindable)
- return unless options = Pakyow.app.presenter.binder.options_for_prop(prop, scope, bindable, context)
+ def create_select_options(doc, scope, prop, value, bindable, ctx)
+ options = Binder.instance.options_for_scoped_prop(scope, prop, bindable, ctx)
+ return if options.nil?
- option_nodes = Nokogiri::HTML::DocumentFragment.parse ""
+ option_nodes = Nokogiri::HTML::DocumentFragment.parse('')
Nokogiri::HTML::Builder.with(option_nodes) do |h|
until options.length == 0
catch :optgroup do
o = options.first
# an array containing value/content
if o.is_a?(Array)
- h.option o[1], :value => o[0]
+ h.option o[1], value: o[0]
options.shift
# likely an object (e.g. string); start a group
else
- h.optgroup(:label => o) {
+ h.optgroup(label: o) {
options.shift
options[0..-1].each_with_index { |o2,i2|
# starting a new group
- throw :optgroup if !o2.is_a?(Array)
+ throw :optgroup unless o2.is_a?(Array)
- h.option o2[1], :value => o2[0]
+ h.option o2[1], value: o2[0]
options.shift
}
}
end
end
end
end
# remove existing options
- doc.children.remove
+ doc.clear
# add generated options
- doc.add_child(option_nodes)
+ doc.append(option_nodes.to_html)
end
def select_option_with_value(doc, value)
- return unless o = doc.css('option[value="' + value.to_s + '"]').first
- o[:selected] = 'selected'
+ option = doc.option(value: value)
+ return if option.nil?
+
+ option.set_attribute(:selected, 'selected')
end
def handle_unbound_data(scope, prop)
- Pakyow.logger.warn("Unbound data for #{scope}[#{prop}]")
+ Pakyow.logger.warn("Unbound data for #{scope}[#{prop}]") if Pakyow.logger
throw :unbound
+ end
+
+ def bind_attributes_to_doc(attrs, doc)
+ attrs.each do |attr, v|
+ case attr
+ when :content
+ v = v.call(doc.inner_html) if v.is_a?(Proc)
+ bind_value_to_doc(v, doc)
+ next
+ when :view
+ v.call(self)
+ next
+ else
+ attr = attr.to_s
+ attrs = Attributes.new(doc)
+ v = v.call(attrs.send(attr)) if v.is_a?(Proc)
+
+ if v.nil?
+ doc.remove_attribute(attr)
+ else
+ attrs.send(:"#{attr}=", v)
+ end
+ end
+ end
end
end
end
end