pakyow-presenter 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ = 0.6.0 / 2011-08-20
2
+
3
+ * Initial gem release of 0.6.0 codebase
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Bryan Powell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File without changes
@@ -0,0 +1,13 @@
1
+ libdir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
+
4
+ # Gems
5
+ require 'nokogiri'
6
+
7
+ # Base
8
+ require 'presenter/base'
9
+ include Presenter
10
+
11
+ require 'presenter/presenter'
12
+ require 'presenter/configuration/base'
13
+ require 'presenter/helpers'
@@ -0,0 +1,11 @@
1
+ module Pakyow
2
+ module Presenter
3
+ autoload :PresenterBase, 'core/presenter_base'
4
+ autoload :ViewLookupStore, 'presenter/view_lookup_store'
5
+ autoload :View, 'presenter/view'
6
+ autoload :LazyView, 'presenter/lazy_view'
7
+ autoload :Binder, 'presenter/binder'
8
+ autoload :Views, 'presenter/views'
9
+ autoload :ViewContext, 'presenter/view_context'
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class Binder
4
+ include Pakyow::GeneralHelpers
5
+
6
+ class << self
7
+ attr_accessor :options
8
+
9
+ def binder_for(klass)
10
+ View.binders = {} unless View.binders
11
+ View.binders[klass.to_s.to_sym] = self
12
+ end
13
+
14
+ def options_for(*args)
15
+ self.options = {} unless self.options
16
+ self.options[args[0]] = args[1]
17
+ end
18
+ end
19
+
20
+ attr_accessor :bindable, :object
21
+
22
+ def initialize(bindable, object)
23
+ self.bindable = bindable
24
+ self.object = object
25
+ end
26
+
27
+ def fetch_options_for(attribute)
28
+ if self.class.options
29
+ if options = self.class.options[attribute]
30
+ unless options.is_a?(Array) || options.is_a?(Hash)
31
+ options = self.send(options)
32
+ end
33
+
34
+ return options
35
+ end
36
+ end
37
+ end
38
+
39
+ def action
40
+ unless routes = Pakyow.app.restful_routes[bindable.class.name.to_sym]
41
+ Log.warn "Attempting to bind object to #{bindable.class.name.downcase}[action] but could not find restful routes for #{bindable.class.name}."
42
+ return {}
43
+ end
44
+
45
+ if id = bindable.id
46
+ self.object.add_child('<input type="hidden" name="_method" value="put">')
47
+
48
+
49
+ action = routes[:update].gsub(':id', id.to_s)
50
+ method = "post"
51
+ else
52
+ action = routes[:create]
53
+ method = "post"
54
+ end
55
+
56
+ return { :action => action, :method => method }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ module Pakyow
2
+ module Configuration
3
+ autoload :Presenter, 'presenter/configuration/presenter'
4
+
5
+ class Base
6
+ # Fetches the server configuration
7
+ def self.presenter
8
+ Configuration::Presenter
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ module Pakyow
2
+ module Configuration
3
+ class Presenter
4
+ class << self
5
+ attr_accessor :view_caching, :javascripts, :stylesheets, :view_dir, :default_view
6
+
7
+ # Location of javascripts
8
+ def javascripts
9
+ @javascripts || '/javascripts'
10
+ end
11
+
12
+ # Location of stylesheets
13
+ def stylesheets
14
+ @stylesheets || '/stylesheets'
15
+ end
16
+
17
+ def view_dir
18
+ @view_dir || "#{Configuration::Base.app.root}/app/views"
19
+ end
20
+
21
+ def view_caching
22
+ @view_caching || false
23
+ end
24
+
25
+ def default_view
26
+ @default_view || "pakyow.html"
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ module Pakyow
2
+ module Helpers
3
+ def presenter
4
+ Pakyow.app.presenter.current_context = self
5
+ Pakyow.app.presenter
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,42 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class LazyView < View
4
+
5
+ def to_html(*args)
6
+ Pakyow.app.presenter.ensure_root_view_built
7
+ super
8
+ end
9
+
10
+ def add_content_to_container(*args)
11
+ Pakyow.app.presenter.ensure_root_view_built
12
+ super
13
+ end
14
+
15
+ def find(*args)
16
+ Pakyow.app.presenter.ensure_root_view_built
17
+ super
18
+ end
19
+
20
+ def repeat_for(*args, &block)
21
+ Pakyow.app.presenter.ensure_root_view_built
22
+ super
23
+ end
24
+
25
+ def reset_container(*args)
26
+ Pakyow.app.presenter.ensure_root_view_built
27
+ super
28
+ end
29
+
30
+ def title=(*args)
31
+ Pakyow.app.presenter.ensure_root_view_built
32
+ super
33
+ end
34
+
35
+ def bind(*args)
36
+ Pakyow.app.presenter.ensure_root_view_built
37
+ super
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,180 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class Presenter < PresenterBase
4
+ attr_accessor :current_context
5
+
6
+ #
7
+ # Methods that are called by core. This is the interface that core expects a Presenter to have
8
+ #
9
+
10
+ def reload!
11
+ load_views
12
+ end
13
+
14
+ def present_for_request(request)
15
+ @presented = false
16
+ @root_path = nil
17
+ @root_view_is_built = false
18
+ @root_view = nil
19
+ @view_path = nil
20
+ @container_name = nil
21
+ @request = request
22
+ end
23
+
24
+ def presented?
25
+ @presented
26
+ end
27
+
28
+ def content
29
+ return unless view
30
+ request_container = @request.params[:_container]
31
+ return view.to_html(request_container) if request_container
32
+ view.to_html(@container_name)
33
+ end
34
+
35
+ #
36
+ # Methods that a controller can call to get and modify the root view.
37
+ # Some are meant to be called directly and some make up a dsl for dom modification
38
+ #
39
+
40
+ # Call these directly
41
+ #
42
+
43
+ def view_for_path(abstract_path, is_root_view=false, klass=View)
44
+ real_path = @view_lookup_store.real_path(abstract_path)
45
+ klass.new(real_path, is_root_view)
46
+ end
47
+
48
+ def view_for_class(view_class, path_override=nil)
49
+ return view_for_path(path_override, view_class.default_is_root_view, view_class) if path_override
50
+ view_for_path(view_class.default_view_path, view_class.default_is_root_view, view_class)
51
+ end
52
+
53
+ def view
54
+ ensure_root_view_built
55
+ @root_view
56
+ end
57
+
58
+ def set_view(view)
59
+ @root_view = View.new(view)
60
+ @root_view_is_built = true
61
+ @presented = true
62
+ @view_path = nil
63
+ @root_path = nil
64
+ end
65
+
66
+ def limit_to_container(id)
67
+ @container_name = id
68
+ end
69
+
70
+ def use_view_path(path)
71
+ @view_path = path
72
+ @root_view_is_built = false
73
+ end
74
+
75
+ def view_path
76
+ @view_path
77
+ end
78
+
79
+ def use_root_view_file(abstract_view_file)
80
+ real_path = @view_lookup_store.real_path(abstract_view_file)
81
+ @root_path = real_path
82
+ @root_view_is_built = false
83
+ end
84
+
85
+ def use_root_view_at_view_path(abstract_view_dir)
86
+ @root_path = @view_lookup_store.view_info(abstract_view_dir)[:root_view]
87
+ @root_view_is_built = false
88
+ end
89
+
90
+ # This is for creating views from within a controller using the route based lookup mechanism
91
+ def view_for_view_path(v_p, name, deep = false)
92
+ v = nil
93
+ view_info = @view_lookup_store.view_info(v_p)
94
+ vpath = view_info[:views][name] if view_info
95
+ v = View.new(vpath) if vpath
96
+ if v && deep
97
+ populate_view(v, view_info[:views])
98
+ end
99
+ v
100
+ end
101
+
102
+ def populate_view_for_view_path(view, v_p)
103
+ return view unless view_info = @view_lookup_store.view_info(v_p)
104
+ views = view_info[:views]
105
+ populate_view(view, views)
106
+ view
107
+ end
108
+
109
+ # Call as part of View DSL for DOM manipulation
110
+ #
111
+
112
+ def with_container(container, &block)
113
+ ViewContext.new(self.view.find("##{container}").first).instance_eval(&block)
114
+ end
115
+
116
+ #
117
+ # Used by LazyView
118
+ #
119
+
120
+ def ensure_root_view_built
121
+ build_root_view unless @root_view_is_built
122
+ end
123
+
124
+ #
125
+ protected
126
+ #
127
+
128
+ def build_root_view
129
+ @root_view_is_built = true
130
+
131
+ if @view_path
132
+ v_p = @view_path
133
+ elsif @request.restful
134
+ v_p = restful_view_path(@request.restful)
135
+ elsif @request.route_spec && @request.route_spec.index(':')
136
+ v_p = StringUtils.remove_route_vars(@request.route_spec)
137
+ else
138
+ v_p = @request.env['PATH_INFO']
139
+ end
140
+ return unless v_p
141
+ return unless view_info = @view_lookup_store.view_info(v_p)
142
+
143
+ @presented = true
144
+ @root_path ||= view_info[:root_view]
145
+ @root_view = LazyView.new(@root_path, true)
146
+ views = view_info[:views]
147
+ populate_view(self.view, views)
148
+ end
149
+
150
+ def restful_view_path(restful_info)
151
+ if restful_info[:restful_action] == :show
152
+ "#{StringUtils.remove_route_vars(@request.route_spec)}/show"
153
+ else
154
+ StringUtils.remove_route_vars(@request.route_spec)
155
+ end
156
+ end
157
+
158
+ def load_views
159
+ @view_lookup_store = ViewLookupStore.new("#{Configuration::Presenter.view_dir}")
160
+ end
161
+
162
+ # populates the top_view using view_store data by recursively building
163
+ # and substituting in child views named in the structure
164
+ def populate_view(top_view, views)
165
+ containers = top_view.elements_with_ids
166
+ containers.each {|e|
167
+ name = e.attr("id")
168
+ path = views[name]
169
+ if path
170
+ v = populate_view(View.new(path), views)
171
+ top_view.reset_container(name)
172
+ top_view.add_content_to_container(v, name)
173
+ end
174
+ }
175
+ top_view
176
+ end
177
+
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,408 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class View
4
+ class << self
5
+ attr_accessor :binders, :cache, :default_view_path, :default_is_root_view
6
+
7
+ def view_path(dvp, dirv=false)
8
+ self.default_view_path = dvp
9
+ self.default_is_root_view = dirv
10
+ end
11
+ end
12
+
13
+ attr_accessor :doc
14
+
15
+ def initialize(arg=nil, is_root_view=false)
16
+ arg = self.class.default_view_path if arg.nil? && self.class.default_view_path
17
+ is_root_view = self.class.default_is_root_view if arg.nil? && self.class.default_is_root_view
18
+
19
+ if arg.is_a?(Nokogiri::XML::Element)
20
+ @doc = arg
21
+ elsif arg.is_a?(Pakyow::Presenter::Views)
22
+ @doc = arg.first.doc.dup
23
+ elsif arg.is_a?(Pakyow::Presenter::View)
24
+ @doc = arg.doc.dup
25
+ elsif arg.is_a?(String)
26
+ if arg[0, 1] == '/'
27
+ view_path = "#{Configuration::Presenter.view_dir}#{arg}"
28
+ else
29
+ view_path = "#{Configuration::Presenter.view_dir}/#{arg}"
30
+ end
31
+ # Only load one time if view caching is enabled
32
+ self.class.cache ||= {}
33
+
34
+ if !self.class.cache.has_key?(view_path) || !Configuration::Base.presenter.view_caching
35
+ if is_root_view then
36
+ self.class.cache[view_path] = Nokogiri::HTML::Document.parse(File.read(view_path))
37
+ else
38
+ self.class.cache[view_path] = Nokogiri::HTML.fragment(File.read(view_path))
39
+ end
40
+ end
41
+
42
+ @doc = self.class.cache[view_path].dup
43
+ else
44
+ raise ArgumentError, "No View for you! Come back, one year."
45
+ end
46
+ end
47
+
48
+ def add_content_to_container(content, container)
49
+ # TODO This .css call works but the equivalent .xpath call doesn't
50
+ # Need to investigate why since the .css call is internally turned into a .xpath call
51
+ if @doc && o = @doc.css("##{container}").first
52
+ content = content.doc unless content.class == String || content.class == Nokogiri::HTML::DocumentFragment || content.class == Nokogiri::XML::Element
53
+ o.add_child(content)
54
+ end
55
+ end
56
+
57
+ def add_resource(*args)
58
+ type, resource, options = args
59
+ options ||= {}
60
+
61
+ content = case type
62
+ when :js then '<script src="' + Pakyow::Configuration::Presenter.javascripts + '/' + resource.to_s + '.js"></script>'
63
+ when :css then '<link href="' + Pakyow::Configuration::Presenter.stylesheets + '/' + resource.to_s + '.css" rel="stylesheet" media="' + (options[:media] || 'screen, projection') + '" type="text/css">'
64
+ end
65
+
66
+ if self.doc.fragment? || self.doc.element?
67
+ self.doc.add_previous_sibling(content)
68
+ else
69
+ self.doc.xpath("//head/*[1]").before(content)
70
+ end
71
+ end
72
+
73
+ def remove_resource(*args)
74
+ type, resource, options = args
75
+ options ||= {}
76
+
77
+ case type
78
+ when :js then self.doc.css("script[src='#{Pakyow::Configuration::Presenter.javascripts}/#{resource}.js']").remove
79
+ when :css then self.doc.css("link[href='#{Pakyow::Configuration::Presenter.stylesheets}/#{resource}.css']").remove
80
+ end
81
+ end
82
+
83
+ def find(element)
84
+ group = Views.new
85
+ @doc.css(element).each {|e| group << View.new(e)}
86
+
87
+ return group
88
+ end
89
+
90
+ def in_context(&block)
91
+ ViewContext.new(self).instance_eval(&block)
92
+ end
93
+
94
+ def bind(object, type = nil)
95
+ type = type || StringUtils.underscore(object.class.name)
96
+
97
+ # This works: .//*
98
+ # Not this: .//*[@itemprop or @name]
99
+ # WTF!
100
+ #
101
+ @doc.xpath('.//*').each do |o|
102
+ if attribute = o.get_attribute('itemprop')
103
+ selector = attribute
104
+ elsif name = o.get_attribute('name')
105
+ selector = name
106
+ else
107
+ next
108
+ end
109
+
110
+ next unless attribute
111
+
112
+ if selector.include?('[')
113
+ type_len = type.length
114
+ object_type = selector[0,type_len]
115
+ attribute = selector[type_len + 1, attribute.length - type_len - 2]
116
+ else
117
+ object_type = nil
118
+ attribute = selector
119
+ end
120
+
121
+ next if !object_type.nil? && object_type != type
122
+
123
+ binding = {
124
+ :element => o,
125
+ :attribute => attribute.to_sym,
126
+ :selector => selector
127
+ }
128
+
129
+ bind_object_to_binding(object, binding, object_type.nil?)
130
+ end
131
+ end
132
+
133
+ def repeat_for(objects, &block)
134
+ if o = @doc
135
+ objects.each do |object|
136
+ view = View.new(self)
137
+ view.bind(object)
138
+ ViewContext.new(view).instance_exec(object, &block) if block_given?
139
+
140
+ o.add_previous_sibling(view.doc)
141
+ end
142
+
143
+ o.remove
144
+ end
145
+ end
146
+
147
+ def reset_container(container)
148
+ if @doc && o = @doc.css("*[id='#{container}']").first
149
+ o.inner_html = ''
150
+ end
151
+ end
152
+
153
+ def title=(title)
154
+ if @doc
155
+ if o = @doc.css('title').first
156
+ o.inner_html = title
157
+ else
158
+ if o = @doc.css('head').first
159
+ o.add_child(Nokogiri::HTML::fragment("<title>#{title}</title>"))
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def title
166
+ o = @doc.css('title').first
167
+ o.inner_html if o
168
+ end
169
+
170
+ def to_html(container = nil)
171
+ if container
172
+ if o = @doc.css('#' + container.to_s).first
173
+ o.inner_html
174
+ else
175
+ ''
176
+ end
177
+ else
178
+ @doc.to_html
179
+ end
180
+ end
181
+
182
+ alias :to_s :to_html
183
+
184
+ # Allows multiple attributes to be set at once.
185
+ # root_view.find(selector).attributes(:class => my_class, :style => my_style)
186
+ #
187
+ def attributes(*args)
188
+ if args.empty?
189
+ @previous_method = :attributes
190
+ return self
191
+ else
192
+ args[0].each_pair { |name, value|
193
+ @previous_method = :attributes
194
+ self.send(name.to_sym, value)
195
+ }
196
+ end
197
+ end
198
+
199
+ def remove
200
+ self.doc.remove
201
+ end
202
+
203
+ alias :delete :remove
204
+
205
+ def add_class(val)
206
+ self.doc['class'] = "#{self.doc['class']} #{val}".strip
207
+ end
208
+
209
+ def remove_class(val)
210
+ self.doc['class'] = self.doc['class'].gsub(val.to_s, '').strip if self.doc['class']
211
+ end
212
+
213
+ def has_class(val)
214
+ self.doc['class'].include? val
215
+ end
216
+
217
+ def clear
218
+ self.doc.inner_html = ''
219
+ end
220
+
221
+ def text
222
+ self.doc.inner_text
223
+ end
224
+
225
+ def content
226
+ self.doc.inner_html
227
+ end
228
+
229
+ alias :html :content
230
+
231
+ def content=(content)
232
+ return unless content
233
+ self.doc.inner_html = content.to_s
234
+ end
235
+
236
+ alias :html= :content=
237
+
238
+ def append(content)
239
+ self.doc.inner_html += content.to_s
240
+ end
241
+
242
+ alias :render :append
243
+
244
+ def +(value)
245
+ if @previous_method
246
+ append_value(val)
247
+ else
248
+ super
249
+ end
250
+ end
251
+
252
+ def <<(value)
253
+ if @previous_method
254
+ append_value(val)
255
+ else
256
+ super
257
+ end
258
+ end
259
+
260
+ def method_missing(method, *args)
261
+ return unless @previous_method == :attributes
262
+ @previous_method = nil
263
+
264
+ if method.to_s.include?('=')
265
+ attribute = method.to_s.gsub('=', '')
266
+ value = args[0]
267
+
268
+ self.doc[attribute] = value
269
+ else
270
+ return self.doc[method.to_s]
271
+ end
272
+ end
273
+
274
+ def class(*args)
275
+ if @previous_method == :attributes
276
+ method_missing(:class, *args)
277
+ else
278
+ super
279
+ end
280
+ end
281
+
282
+ def id
283
+ if @previous_method == :attributes
284
+ method_missing(:id)
285
+ else
286
+ super
287
+ end
288
+ end
289
+
290
+ def elements_with_ids
291
+ elements = []
292
+ @doc.traverse {|e|
293
+ if e.has_attribute?("id")
294
+ elements << e
295
+ end
296
+ }
297
+ elements
298
+ end
299
+
300
+ protected
301
+
302
+ def append_value(value_to_append)
303
+ case @previous_method
304
+ when :content
305
+ append(value_to_append)
306
+ end
307
+
308
+ @previous_method = nil
309
+ end
310
+
311
+ def bind_object_to_binding(object, binding, wild = false)
312
+ binder = nil
313
+
314
+ # fetch value
315
+ if object.is_a? Hash
316
+ value = object[binding[:attribute]]
317
+ else
318
+ if View.binders
319
+ b = View.binders[object.class.to_s.to_sym] and binder = b.new(object, binding[:element])
320
+ end
321
+
322
+ if binder && binder.class.method_defined?(binding[:attribute])
323
+ value = binder.send(binding[:attribute])
324
+ else
325
+ if wild && !object.class.method_defined?(binding[:attribute])
326
+ return
327
+ elsif Configuration::Base.app.dev_mode == true && !object.class.method_defined?(binding[:attribute])
328
+ Log.warn("Attempting to bind object to #{binding[:html_tag]}#{binding[:selector].gsub('*', '').gsub('\'', '')} but #{object.class.name}##{binding[:attribute]} is not defined.")
329
+ return
330
+ else
331
+ value = object.send(binding[:attribute])
332
+ end
333
+ end
334
+ end
335
+
336
+ if value.is_a? Hash
337
+ value.each do |k, v|
338
+ if k == :content
339
+ bind_value_to_binding(v, binding, binder)
340
+ else
341
+ binding[:element][k.to_s] = v.to_s
342
+ end
343
+ end
344
+ else
345
+ bind_value_to_binding(value, binding, binder)
346
+ end
347
+ end
348
+
349
+ def bind_value_to_binding(value, binding, binder)
350
+ if !self.self_closing_tag?(binding[:element].name)
351
+ if binding[:element].name == 'select'
352
+ if binder
353
+ if options = binder.fetch_options_for(binding[:attribute])
354
+ html = ''
355
+ is_group = false
356
+
357
+ options.each do |opt|
358
+ if opt.is_a?(Array)
359
+ if opt.first.is_a?(Array)
360
+ opt.each do |opt2|
361
+ html << '<option value="' + opt2[0].to_s + '">' + opt2[1].to_s + '</option>'
362
+ end
363
+ else
364
+ html << '<option value="' + opt[0].to_s + '">' + opt[1].to_s + '</option>'
365
+ end
366
+ else
367
+ html << "</optgroup>" if is_group
368
+ html << '<optgroup label="' + opt.to_s + '">'
369
+ is_group = true
370
+ end
371
+ end
372
+
373
+ html << "</optgroup>" if is_group
374
+
375
+ binding[:element].inner_html = html
376
+ end
377
+ end
378
+
379
+ if opt = binding[:element].css('option[value="' + value.to_s + '"]').first
380
+ opt['selected'] = 'selected'
381
+ end
382
+ else
383
+ binding[:element].inner_html = value.to_s
384
+ end
385
+ elsif binding[:element].name == 'input' && binding[:element][:type] == 'checkbox'
386
+ if value == true || binding[:element].attributes['value'].value == value.to_s
387
+ binding[:element]['checked'] = 'checked'
388
+ else
389
+ binding[:element].delete('checked')
390
+ end
391
+ elsif binding[:element].name == 'input' && binding[:element][:type] == 'radio'
392
+ if binding[:element].attributes['value'].value == value.to_s
393
+ binding[:element]['checked'] = 'checked'
394
+ else
395
+ binding[:element].delete('checked')
396
+ end
397
+ else
398
+ binding[:element]['value'] = value.to_s
399
+ end
400
+ end
401
+
402
+ def self_closing_tag?(tag)
403
+ %w[area base basefont br hr input img link meta].include? tag
404
+ end
405
+
406
+ end
407
+ end
408
+ end
@@ -0,0 +1,20 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class ViewContext
4
+ include Helpers
5
+
6
+ def initialize(context)
7
+ @context = context
8
+ self
9
+ end
10
+
11
+ def context
12
+ @context
13
+ end
14
+
15
+ def method_missing(method, *args)
16
+ Pakyow.app.presenter.current_context.send(method, *args)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,213 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class ViewLookupStore
4
+
5
+ # @view_store is a hash structured as:
6
+ # {
7
+ # :view_dirs => {
8
+ # "/route" => {
9
+ # :root_view => "path/to/root/view",
10
+ # :views => {
11
+ # "view1_name" => "path/to/view1",
12
+ # "view2_name" => "path/to/view2"
13
+ # }
14
+ # },
15
+ # "/route/sub" => {
16
+ # :root_view => "path/to/root/view",
17
+ # :views => {
18
+ # "view1_name" => "path/to/view1",
19
+ # "view2_name" => "path/to/view2"
20
+ # }
21
+ # }
22
+ # },
23
+ # :abstract_paths => {
24
+ # "/abstract/path/file.html" => "/abstract.root1/path/file.html",
25
+ # "/some/other/path" => "/some/other.root1/path.root2"
26
+ # }
27
+ # }
28
+ # This takes into account that a view directory may have a .root suffix.
29
+ # This not only determines the root view for that route (and sub-routes) but that
30
+ # the route doesn't include the suffix but the path to a view does
31
+
32
+ attr_reader(:view_dir)
33
+
34
+ def initialize(view_dir)
35
+ @view_store = {:view_dirs => {}, :abstract_paths => {}}
36
+ return unless File.exist?(view_dir)
37
+
38
+ @view_dir = view_dir
39
+
40
+ # wack the ./ at the beginning if it's there
41
+ view_dir = view_dir.sub(/^\.\//,'')
42
+
43
+ # making this a variable in case we change whether we store relative or absolute paths to views
44
+ absolute_path_prefix = view_dir # set to '' to store absolute paths
45
+
46
+ default_views = {} # view_basename => path_to_view.html
47
+ if File.exist?(view_dir) then
48
+ default_root_view_file_path = "#{absolute_path_prefix}/#{Configuration::Presenter.default_view}"
49
+ # See if the top level index directory overrides the default root view
50
+ index_dirs = Dir.entries(view_dir).partition{|e| File.directory?("#{absolute_path_prefix}/#{e}") && e.start_with?('index.')}[0]
51
+ if index_dirs.length == 1
52
+ default_root_view_file_path = "#{absolute_path_prefix}/#{StringUtils.split_at_last_dot(index_dirs[0])[1]}.html"
53
+ end
54
+ # The logic depends on this traversing top down
55
+ DirUtils.walk_dir(view_dir) { |vpath|
56
+ if File.directory?(vpath)
57
+ parent,route = pakyow_path_to_route_and_parent(vpath, view_dir, :dir)
58
+ # root_view is same as parent unless this route overrides it
59
+ # views are a copy of parent views
60
+ route_root_path = @view_store[:view_dirs][parent] ? @view_store[:view_dirs][parent][:root_view] : default_root_view_file_path
61
+ route_views = @view_store[:view_dirs][parent] ? deep_hash_clone(@view_store[:view_dirs][parent][:views]) : deep_hash_clone(default_views)
62
+ # see if this route overrides root_view
63
+ route_part, root_part = StringUtils.split_at_last_dot(vpath)
64
+ if root_part && root_part.include?('/')
65
+ route_part, root_part = vpath, nil
66
+ end
67
+ if root_part
68
+ if File.exist?("#{vpath}/#{root_part}.html")
69
+ route_root_path = "#{vpath}/#{root_part}.html".sub(absolute_path_prefix, '')
70
+ elsif route_views[root_part]
71
+ route_root_path = route_views[root_part]
72
+ else
73
+ if Configuration::Base.app.dev_mode == true
74
+ Log.warn("Root view #{root_part} referenced in #{vpath.sub(absolute_path_prefix, '')} was not found.")
75
+ else
76
+ Log.error("Root view #{root_part} referenced in #{vpath.sub(absolute_path_prefix, '')} was not found.")
77
+ raise "Root view #{root_part} referenced in #{vpath.sub(absolute_path_prefix, '')} was not found."
78
+ end
79
+ end
80
+ end
81
+ @view_store[:view_dirs][route] =
82
+ {
83
+ :root_view => route_root_path,
84
+ :views => route_views
85
+ }
86
+ # set the abstract path for this dir
87
+ if route == '/'
88
+ r_p = '/'
89
+ else
90
+ r_p = vpath.sub(absolute_path_prefix, '')
91
+ end
92
+ @view_store[:abstract_paths][route] = r_p
93
+ # duplicate real path under routes permuted with leading/trailing slash
94
+ permute_route(route).each { |r| @view_store[:abstract_paths][r] = r_p } unless route == '/'
95
+ else
96
+ # files here are direct overrides of the route's views
97
+ parent,route = pakyow_path_to_route_and_parent(vpath, view_dir, :file)
98
+ view_key = File.basename(vpath,".*")
99
+ unless @view_store[:view_dirs][route]
100
+ @view_store[:view_dirs][route] = deep_hash_clone(@view_store[:view_dirs][parent])
101
+ end
102
+ @view_store[:view_dirs][route][:views][view_key] = vpath.sub(absolute_path_prefix, '')
103
+ # see if view overrides the root view
104
+ if File.basename(@view_store[:view_dirs][route][:root_view],".*") == view_key
105
+ @view_store[:view_dirs][route][:root_view] = vpath.sub(absolute_path_prefix, '')
106
+ end
107
+ # set the abstract path for this file
108
+ # duplicating real path under route without the leading slash
109
+ r_p = vpath.sub(absolute_path_prefix, '')
110
+ if route == '/'
111
+ @view_store[:abstract_paths]["/#{File.basename(vpath)}"] = r_p
112
+ @view_store[:abstract_paths][File.basename(vpath)] = r_p
113
+ else
114
+ route_with_leading_slash = "#{route}/#{File.basename(vpath)}"
115
+ route_without_leading_slash = route_with_leading_slash.sub('/','')
116
+ @view_store[:abstract_paths][route_with_leading_slash] = r_p
117
+ @view_store[:abstract_paths][route_without_leading_slash] = r_p
118
+ end
119
+ end
120
+ }
121
+ end
122
+
123
+ # adjust @view_store '.../index' entries to override the parent
124
+ @view_store[:view_dirs].each_pair {|route,info|
125
+ next unless File.basename(route) == "index"
126
+ parent = File.dirname(route)
127
+ @view_store[:view_dirs][parent] = info
128
+ }
129
+ # adjust @view_store entries to have a '.../index' counterpart where missing
130
+ index_counterparts = {}
131
+ @view_store[:view_dirs].each_pair {|route,info|
132
+ next if File.basename(route) == "index" || @view_store[:view_dirs]["#{route}/index"]
133
+ if route == "/"
134
+ index_counterparts["/index"] = info
135
+ else
136
+ index_counterparts["#{route}/index"] = info
137
+ end
138
+ }
139
+ index_counterparts.each_pair { |route,info|
140
+ @view_store[:view_dirs][route] = info
141
+ }
142
+ # Duplicate the info for each combination of route with and without a leading and ending slash
143
+ # All current keys have a leading slash and no trailing slash
144
+ slash_permutations = {}
145
+ @view_store[:view_dirs].each_pair {|route0,info|
146
+ unless route0 == '/' then
147
+ route1, route2, route3 = permute_route(route0)
148
+ slash_permutations[route1] = info
149
+ slash_permutations[route2] = info
150
+ slash_permutations[route3] = info
151
+ end
152
+ }
153
+ slash_permutations.each_pair { |route,info|
154
+ @view_store[:view_dirs][route] = info
155
+ }
156
+ end
157
+
158
+ def view_info(route = nil)
159
+ if route
160
+ return @view_store[:view_dirs][route]
161
+ else
162
+ return @view_store[:view_dirs]
163
+ end
164
+ end
165
+
166
+ def real_path(abstract_path)
167
+ @view_store[:abstract_paths][abstract_path]
168
+ end
169
+
170
+ private
171
+
172
+ # path can be of the form prefix_path/this/route.root1/overrides/some.root2/root
173
+ # returns the path without the .root_view parts
174
+ def pakyow_path_to_route_and_parent(path, path_prefix, file_or_dir)
175
+ return "","/" if path == path_prefix
176
+ route_path = path.sub("#{path_prefix}/", "")
177
+ unless route_path.include?("/")
178
+ return "","/" if :file == file_or_dir
179
+ return "/","/#{StringUtils.split_at_last_dot(route_path)[0]}"
180
+ end
181
+ route = ""
182
+ parent = ""
183
+ segments = route_path.split('/')
184
+ segments.each_with_index {|s,i|
185
+ next if (i >= segments.length-1 && :file == file_or_dir)
186
+ route_part = StringUtils.split_at_last_dot(s)[0]
187
+ route << "/#{route_part}"
188
+ next if (i >= segments.length-2 && :file == file_or_dir) || (i >= segments.length-1 && :dir == file_or_dir)
189
+ parent << "/#{route_part}"
190
+ }
191
+ parent = "/" if parent == ""
192
+ return parent,route
193
+ end
194
+
195
+ # Gonna just use Marshal for now.
196
+ # Can change later if needed since we only need to work
197
+ # on hashes of symbols, strings and hashes.
198
+ def deep_hash_clone(h)
199
+ Marshal.load(Marshal.dump(h))
200
+ end
201
+
202
+ # Takes a route with a leading slash and no trailing slash (/route) and
203
+ # returns the three other permutaions (/route/, route/, and route).
204
+ def permute_route(route0)
205
+ route3 = route0.sub('/','')
206
+ route2 = "#{route3}/"
207
+ route1 = "#{route0}/"
208
+ return route1,route2,route3
209
+ end
210
+
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,115 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class Views
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @views = []
8
+ end
9
+
10
+ def each
11
+ @views.each { |v| yield(v) }
12
+ end
13
+
14
+ def in_context(&block)
15
+ ViewContext.new(self).instance_eval(&block)
16
+ end
17
+
18
+ def attributes(*args)
19
+ self.each {|e| e.attributes(*args)}
20
+ return self
21
+ end
22
+
23
+ def remove
24
+ self.each {|e| e.remove}
25
+ end
26
+
27
+ alias :delete :remove
28
+
29
+ def add_class(val)
30
+ self.each {|e| e.add_class(val)}
31
+ end
32
+
33
+ def remove_class(val)
34
+ self.each {|e| e.remove_class(val)}
35
+ end
36
+
37
+ def clear
38
+ self.each {|e| e.clear}
39
+ end
40
+
41
+ def text
42
+ self.map { |v| v.text }
43
+ end
44
+
45
+ def content
46
+ self.map { |v| v.content }
47
+ end
48
+
49
+ alias :html :content
50
+
51
+ def content=(content)
52
+ self.each {|e| e.content = content}
53
+ end
54
+
55
+ alias :html= :content=
56
+
57
+ def to_html
58
+ self.map { |v| v.to_html }.join('')
59
+ end
60
+
61
+ alias :to_s :to_html
62
+
63
+ def append(content)
64
+ self.each {|e| e.append(content)}
65
+ end
66
+
67
+ alias :render :append
68
+
69
+ def +(val)
70
+ self.each {|e| e + val}
71
+ end
72
+
73
+ def <<(val)
74
+ if val.is_a? View
75
+ @views << val
76
+ else
77
+ self.each {|e| e << val}
78
+ end
79
+ end
80
+
81
+ def method_missing(method, *args)
82
+ if method.to_s.include?('=')
83
+ self.each {|e| e.send(method, *args)}
84
+ else
85
+ self.map {|e| e.send(method, *args)}
86
+ end
87
+ end
88
+
89
+ def class(*args)
90
+ method_missing(:class, *args)
91
+ end
92
+
93
+ def id
94
+ method_missing(:id)
95
+ end
96
+
97
+ def repeat_for(objects, &block)
98
+ # Repeat for first match
99
+ self.first.repeat_for(objects, &block)
100
+
101
+ # Remove other matches
102
+ self.each {|e| e.doc.remove}
103
+ end
104
+
105
+ def bind(object)
106
+ self.each {|e| e.bind(object)}
107
+ end
108
+
109
+ def find(element)
110
+ views = Views.new
111
+ self.each {|e| e.find(element, &block).each { |v| views << v }}
112
+ end
113
+ end
114
+ end
115
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pakyow-presenter
3
+ version: !ruby/object:Gem::Version
4
+ hash: 5
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 6
9
+ - 1
10
+ version: 0.6.1
11
+ platform: ruby
12
+ authors:
13
+ - Bryan Powell
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-20 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: pakyow-core
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - "="
27
+ - !ruby/object:Gem::Version
28
+ hash: 5
29
+ segments:
30
+ - 0
31
+ - 6
32
+ - 1
33
+ version: 0.6.1
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: nokogiri
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 7
45
+ segments:
46
+ - 1
47
+ - 4
48
+ version: "1.4"
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ description: pakyow-presenter
52
+ email: bryan@metabahn.com
53
+ executables: []
54
+
55
+ extensions: []
56
+
57
+ extra_rdoc_files: []
58
+
59
+ files:
60
+ - pakyow-presenter/CHANGES
61
+ - pakyow-presenter/README
62
+ - pakyow-presenter/MIT-LICENSE
63
+ - pakyow-presenter/lib/pakyow-presenter.rb
64
+ - pakyow-presenter/lib/presenter/base.rb
65
+ - pakyow-presenter/lib/presenter/binder.rb
66
+ - pakyow-presenter/lib/presenter/configuration/base.rb
67
+ - pakyow-presenter/lib/presenter/configuration/presenter.rb
68
+ - pakyow-presenter/lib/presenter/helpers.rb
69
+ - pakyow-presenter/lib/presenter/lazy_view.rb
70
+ - pakyow-presenter/lib/presenter/presenter.rb
71
+ - pakyow-presenter/lib/presenter/view.rb
72
+ - pakyow-presenter/lib/presenter/view_context.rb
73
+ - pakyow-presenter/lib/presenter/view_lookup_store.rb
74
+ - pakyow-presenter/lib/presenter/views.rb
75
+ homepage: http://pakyow.com
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options: []
80
+
81
+ require_paths:
82
+ - pakyow-presenter/lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 57
89
+ segments:
90
+ - 1
91
+ - 8
92
+ - 7
93
+ version: 1.8.7
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 3
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ requirements: []
104
+
105
+ rubyforge_project: pakyow-presenter
106
+ rubygems_version: 1.8.8
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: pakyow-presenter
110
+ test_files: []
111
+