pakyow-presenter 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+