pakyow-presenter 0.8rc1 → 0.8.rc4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,126 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class Page
4
+ MATTER_MATCHER = /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
5
+
6
+ class << self
7
+ def load(path)
8
+ format = StringUtils.split_at_last_dot(path)[-1]
9
+ name = File.basename(path, '.*').to_sym
10
+ contents = FileTest.file?(path) ? File.read(path) : nil
11
+
12
+ return Page.new(name, contents, path, format)
13
+ end
14
+ end
15
+
16
+ attr_reader :contents, :path
17
+
18
+ def initialize(name, contents, path, format = :html)
19
+ @name, @contents, @path, @format = name, contents, path, format
20
+
21
+ @info = {}
22
+ @content = {}
23
+
24
+ unless @contents.nil?
25
+ parse_info
26
+ parse_content
27
+ end
28
+
29
+ partials
30
+ end
31
+
32
+ def initialize_copy(original_page)
33
+ super
34
+
35
+ # copy content
36
+ @content = {}
37
+ original_page.content.each_pair do |k, v|
38
+ @content[k] = v.dup
39
+ end
40
+ end
41
+
42
+ def build(partial_map)
43
+ partials.each do |container, partial_list|
44
+ partial_list.each do |partial_name|
45
+ regex = /<!--\s*@include\s*#{partial_name}\s*-->/
46
+ @content[container].gsub!(regex, partial_map[partial_name].to_s)
47
+
48
+ partial_map.delete(partial_name)
49
+ end
50
+ end
51
+
52
+ # we have more partials
53
+ if partial_map.count > 0
54
+ # initiate another build if content contains partials
55
+ build(partial_map) if partials(true).count > 0
56
+ end
57
+
58
+ return self
59
+ end
60
+
61
+ def content(container = nil)
62
+ return container.nil? ? @content : @content[container.to_sym]
63
+ end
64
+
65
+ def template
66
+ @info[:template] || :pakyow
67
+ end
68
+
69
+ def ==(page)
70
+ @contents == page.contents
71
+ end
72
+
73
+ def partials(refind = false)
74
+ @partials = (!@partials || refind) ? find_partials : @partials
75
+ end
76
+
77
+ private
78
+
79
+ def parse_info
80
+ info = parse_front_matter(@contents)
81
+ info = {} if !info || !info.is_a?(Hash)
82
+
83
+ @info = HashUtils.symbolize(info)
84
+ end
85
+
86
+ def parse_content
87
+ # remove yaml front matter
88
+ @contents.gsub!(/---(.|\n)*---/, '')
89
+
90
+ # process contents
91
+ @contents = Presenter.process(@contents, @format)
92
+
93
+ # find content in named containers
94
+ within_regex = /<!--\s*@within\s*([a-zA-Z0-9\-_]*)\s*-->(.*?)<!--\s*\/within\s*-->/m
95
+
96
+ @contents.scan(within_regex) do |m|
97
+ container = m[0].to_sym
98
+ @content[container] = m[1]
99
+ end
100
+
101
+ # find default content
102
+ @content[:default] = @contents.gsub(within_regex, '')
103
+ end
104
+
105
+ # returns an array of hashes, each with the partial name and doc
106
+ def find_partials
107
+ partials = {}
108
+
109
+ partial_regex = /<!--\s*@include\s*([a-zA-Z0-9\-_]*)\s*-->/
110
+ @content.each do |name, content|
111
+ content.scan(partial_regex) do |m|
112
+ partials[name] ||= []
113
+ partials[name] << m[0].to_sym
114
+ end
115
+ end
116
+
117
+ return partials
118
+ end
119
+
120
+ def parse_front_matter(contents)
121
+ matter = YAML.load(contents.match(MATTER_MATCHER).to_s)
122
+ return matter
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,24 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class Partial
4
+ include DocHelpers
5
+
6
+ class << self
7
+ def load(path)
8
+ format = StringUtils.split_at_last_dot(path)[-1]
9
+ contents = File.read(path)
10
+ name = File.basename(path, '.*')[1..-1].to_sym
11
+
12
+ return self.new(name, contents, format)
13
+ end
14
+ end
15
+
16
+ def initialize(name, contents, format = :html)
17
+ @name = name
18
+
19
+ processed = Presenter.process(contents, format)
20
+ @doc = Nokogiri::HTML.fragment(processed)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,255 +1,188 @@
1
1
  module Pakyow
2
2
  module Presenter
3
- class Presenter < PresenterBase
3
+ class Presenter
4
4
  class << self
5
- attr_accessor :proc
6
- end
5
+ def process(contents, format)
6
+ format = format.to_sym
7
7
 
8
- attr_accessor :current_context, :parser_store, :view_store
8
+ unless app = Pakyow.app
9
+ return contents
10
+ end
9
11
 
10
- def initialize
11
- reset_state
12
- reset_bindings
13
- end
12
+ unless presenter = app.presenter
13
+ return contents
14
+ end
14
15
 
15
- def scope(name, set = :default, &block)
16
- @bindings[set] ||= {}
16
+ unless processor = presenter.processor_store[format]
17
+ Pakyow.logger.warn("No processor defined for extension #{format}") unless format == :html
18
+ return contents
19
+ end
17
20
 
18
- bs = Bindings.for(block)
19
- @bindings[set][name] = bs
20
- bs
21
+ return processor.call(contents)
22
+ end
21
23
  end
22
24
 
23
- def bindings(scope)
24
- #TODO think about merging on launch instead
25
- @bindings.inject(Bindings.new) { |bs, b| bs.merge(b[1][scope]) }
26
- end
25
+ Pakyow::App.before(:init) {
26
+ @presenter = Presenter.new
27
+ }
27
28
 
28
- def reset_bindings(set = :default)
29
- @bindings ||= {}
30
- @bindings[set] = {}
31
- end
29
+ Pakyow::App.after(:match) {
30
+ @presenter = Pakyow.app.presenter.dup
31
+ @presenter.prepare_for_request(@request)
32
+ }
32
33
 
33
- def current_view_lookup_store
34
- @view_stores[self.view_store]
35
- end
34
+ Pakyow::App.after(:route) {
35
+ if @presenter.presented?
36
+ @found = true
37
+ @response.body = [@presenter.content]
38
+ else
39
+ @found = false unless found?
40
+ end
41
+ }
36
42
 
37
- #
38
- # Methods that are called by core. This is the interface that core expects a Presenter to have
39
- #
43
+ Pakyow::App.after(:load) {
44
+ @presenter.load
45
+ }
40
46
 
41
- def load
42
- load_views
47
+ Pakyow::App.after(:error) {
48
+ unless config.app.errors_in_browser
49
+ @response.body = [@presenter.content] if @presenter.presented?
50
+ end
51
+ }
52
+
53
+ attr_accessor :processor_store, :binder, :path, :template, :page
43
54
 
44
- self.reset_bindings
45
- self.instance_eval(&Presenter.proc) if Presenter.proc
55
+ def initialize
56
+ setup
46
57
  end
47
58
 
48
- def prepare_for_request(request)
49
- reset_state()
50
- @request = request
59
+ def setup
60
+ @view, @template, @page = nil
61
+ @constructed = false
62
+ self.store = :default
63
+ end
51
64
 
52
- if @request && @request.route_path && !@request.route_path.is_a?(Regexp) && @request.route_path.index(':')
53
- @view_path = StringUtils.remove_route_vars(@request.route_path)
65
+ def store(name = nil)
66
+ if name
67
+ @view_stores[name]
54
68
  else
55
- @view_path = @request && @request.working_path
69
+ @view_stores[@store]
56
70
  end
57
- @root_path = self.current_view_lookup_store.root_path(@view_path)
58
- end
59
-
60
- def reset
61
- @request = nil
62
- reset_state
63
- end
64
-
65
- def presented?
66
- #TODO the right thing to do?
67
- self.ensure_root_view_built
68
-
69
- @presented
70
- end
71
-
72
- def content
73
- return unless view
74
- request_container = @request.params[:_container] if @request
75
- return view.to_html(request_container) if request_container
76
- view.to_html(@container_name)
77
71
  end
78
72
 
79
- #
80
- # Methods that a controller can call to get and modify the root view.
81
- # Some are meant to be called directly and some make up a dsl for dom modification
82
- #
83
-
84
- # Call these directly
85
- #
73
+ def store=(name)
74
+ @store = name
86
75
 
87
- def view_for_path(abstract_path, is_root_view=false, klass=View)
88
- real_path = self.current_view_lookup_store.real_path(abstract_path)
89
- klass.new(real_path, is_root_view)
76
+ return unless @path
77
+ setup_for_path
90
78
  end
91
79
 
92
- def view_for_class(view_class, path_override=nil)
93
- return view_for_path(path_override, view_class.default_is_root_view, view_class) if path_override
94
- view_for_path(view_class.default_view_path, view_class.default_is_root_view, view_class)
80
+ def load
81
+ load_processors
82
+ load_views
83
+ load_bindings
95
84
  end
96
85
 
97
- def view
98
- ensure_root_view_built
99
- @root_view
100
- end
86
+ def prepare_for_request(request)
87
+ @request = request
101
88
 
102
- def view=(v)
103
- # TODO: Why is it important to dup here?
104
- @root_view = View.new(v)
105
- @root_view_is_built = true
106
- @presented = true
89
+ if @request.has_route_vars?
90
+ @path = StringUtils.remove_route_vars(@request.route_path)
91
+ else
92
+ @path = @request.path
93
+ end
107
94
 
108
- # reset paths
109
- @view_path = nil
110
- @root_path = nil
95
+ setup
111
96
  end
112
97
 
113
- def root
114
- @is_compiled = false
115
- @root ||= View.root_at_path(@root_path)
98
+ def presented?
99
+ ensure_construction
100
+ @constructed
116
101
  end
117
102
 
118
- def root=(v)
119
- @is_compiled = false
120
- @root = v
103
+ def content
104
+ return unless view
105
+ view.to_html
121
106
  end
122
107
 
123
- def limit_to_container(id)
124
- @container_name = id
108
+ def view
109
+ ensure_construction
110
+ @view
125
111
  end
126
112
 
127
- def view_path
128
- @view_path
113
+ def partial(name)
114
+ store.partial(@path, name)
129
115
  end
130
116
 
131
- def view_path=(path)
132
- @is_compiled = false
133
- @view_path = path
117
+ def view=(view)
118
+ @view = view
119
+ @constructed = true
134
120
  end
135
121
 
136
- def root_path
137
- @root_path
138
- end
122
+ def template=(template)
123
+ unless template.is_a?(Template)
124
+ # get template by name
125
+ template = store.template(template)
126
+ end
139
127
 
140
- def root_path=(abstract_path)
141
- @is_compiled = false
142
- @root = nil
143
- @root_path = abstract_path
128
+ @template = template
129
+ @constructed = false
144
130
  end
145
131
 
146
- # Call as part of View DSL for DOM manipulation
147
- #
148
-
149
- def with_container(container, &block)
150
- v = self.view.find("##{container}").first
151
- ViewContext.new(v).instance_exec(v, &block)
132
+ def page=(page)
133
+ @page = page
134
+ @constructed = false
152
135
  end
153
136
 
154
- #
155
- # Used by LazyView
156
- #
157
-
158
- def ensure_root_view_built
159
- build_root_view unless @root_view_is_built
137
+ def path=(path)
138
+ @path = path
139
+ setup_for_path(true)
160
140
  end
161
141
 
162
- #
163
- protected
164
- #
142
+ def ensure_construction
143
+ # only construct once
144
+ return if @constructed
165
145
 
166
- def reset_state
167
- @view_store = :default
168
- @presented = false
169
- @root_path = nil
170
- @root_view_is_built = false
171
- @root_view = nil
172
- @view_path = nil
173
- @container_name = nil
174
- end
146
+ # if no template/page was found, we can't construct
147
+ return if @template.nil? || @page.nil?
175
148
 
176
- def build_root_view
177
- @root_view_is_built = true
149
+ # construct
150
+ @view = @template.dup.build(@page)
151
+ @constructed = true
152
+ end
178
153
 
179
- return unless v_p = @view_path
154
+ protected
180
155
 
181
- return unless view_info = self.current_view_lookup_store.view_info(v_p)
182
- @root_path ||= view_info[:root_view]
156
+ def setup_for_path(explicit = false)
157
+ self.template = store.template(@path)
158
+ self.page = store.page(@path)
159
+ self.view = store.view(@path)
183
160
 
184
- if Configuration::Base.presenter.view_caching
185
- r_v = @populated_root_view_cache.get([v_p, @root_path]) {
186
- populate_view(LazyView.new(@root_path, true), view_info[:views])
187
- }
188
- @root_view = r_v.dup
189
- @presented = true
190
- else
191
- @root_view = populate_view(LazyView.new(@root_path, true), view_info[:views])
192
- @presented = true
193
- end
194
- end
195
-
196
- def restful_view_path(restful_info)
197
- if restful_info[:restful_action] == :show
198
- "#{StringUtils.remove_route_vars(@request.route_spec)}/show"
199
- else
200
- StringUtils.remove_route_vars(@request.route_spec)
201
- end
161
+ @constructed = true
162
+ rescue MissingView => e # catches no view path error
163
+ explicit ? raise(e) : Pakyow.logger.debug(e.message)
164
+ @constructed = false
202
165
  end
203
166
 
204
167
  def load_views
205
168
  @view_stores = {}
206
- Configuration::Presenter.view_stores.each_pair {|name, path|
207
- @view_stores[name] = ViewLookupStore.new(path)
208
- }
209
-
210
- if Configuration::Base.presenter.view_caching then
211
- @populated_root_view_cache = build_root_view_cache(self.current_view_lookup_store.view_info)
212
- end
213
- end
214
169
 
215
- def build_root_view_cache(view_info)
216
- cache = Pakyow::Cache.new
217
- view_info.each{|dir,info|
218
- r_v = LazyView.new(info[:root_view], true)
219
- populate_view(r_v, info[:views])
220
- key = [dir, info[:root_view]]
221
- cache.put(key, r_v)
170
+ Config::Presenter.view_stores.each_pair {|name, path|
171
+ @view_stores[name] = ViewStore.new(path, name)
222
172
  }
223
- cache
224
173
  end
225
174
 
226
- # populates the top_view using view_store data by recursively building
227
- # and substituting in child views named in the structure
228
- def populate_view(top_view, views)
229
- top_view.containers.each {|e|
230
- next unless path = views[e[:name]]
175
+ def load_bindings
176
+ @binder = Binder.instance.reset
231
177
 
232
- v = populate_view(View.new(path), views)
233
- self.reset_container(e[:doc])
234
- self.add_content_to_container(v, e[:doc])
178
+ Pakyow::App.bindings.each_pair {|set_name, block|
179
+ @binder.set(set_name, &block)
235
180
  }
236
- top_view
237
- end
238
-
239
- def parser(format, &block)
240
- @parser_store ||= {}
241
- @parser_store[format.to_sym] = block
242
181
  end
243
182
 
244
- def add_content_to_container(content, container)
245
- content = content.doc unless content.class == String || content.class == Nokogiri::HTML::DocumentFragment || content.class == Nokogiri::XML::Element
246
- container.add_child(content)
183
+ def load_processors
184
+ @processor_store = Pakyow::App.processors
247
185
  end
248
-
249
- def reset_container(container)
250
- container.inner_html = ''
251
- end
252
-
253
186
  end
254
187
  end
255
188
  end
@@ -0,0 +1,79 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class Template
4
+ include DocHelpers
5
+ attr_accessor :name, :doc
6
+
7
+ class << self
8
+ def load(path)
9
+ format = StringUtils.split_at_last_dot(path)[-1]
10
+ contents = File.read(path)
11
+ name = File.basename(path, '.*').to_sym
12
+
13
+ return self.new(name, contents, format)
14
+ end
15
+ end
16
+
17
+ def initialize(name, contents, format = :html)
18
+ @name = name
19
+
20
+ if contents.is_a?(Nokogiri::HTML::Document)
21
+ @doc = contents
22
+ else
23
+ processed = Presenter.process(contents, format)
24
+ @doc = Nokogiri::HTML::Document.parse(processed)
25
+ end
26
+
27
+ containers
28
+ end
29
+
30
+ def initialize_copy(original_template)
31
+ super
32
+
33
+ # copy doc
34
+ @doc = original_template.doc.dup
35
+ end
36
+
37
+ def container(name = :default)
38
+ container = @containers[name.to_sym]
39
+ return view_from_path(container[:path])
40
+ end
41
+
42
+ def containers(refind = false)
43
+ @containers = (!@containers || refind) ? find_containers : @containers
44
+ end
45
+
46
+ def build(page)
47
+ # add content to each container
48
+ containers.each do |container|
49
+ name = container[0]
50
+
51
+ if content = page.content(name)
52
+ container(name).replace(content)
53
+ else
54
+ Pakyow.logger.debug "No content for '#{name}' in page '#{page.path}'"
55
+ end
56
+ end
57
+
58
+ return View.from_doc(doc)
59
+ end
60
+
61
+ private
62
+
63
+ # returns an array of hashes, each with the container name and doc
64
+ def find_containers
65
+ containers = {}
66
+
67
+ @doc.traverse {|e|
68
+ next unless e.is_a?(Nokogiri::XML::Comment)
69
+ next unless match = e.text.strip.match(/@container( ([a-zA-Z0-9\-_]*))*/)
70
+ name = match[2] || :default
71
+
72
+ containers[name.to_sym] = { doc: e, path: path_to(e) }
73
+ }
74
+
75
+ return containers
76
+ end
77
+ end
78
+ end
79
+ end