pakyow-presenter 0.8.0 → 0.9.0

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.
@@ -81,7 +81,7 @@ module Pakyow
81
81
  @context = context
82
82
 
83
83
  if @context.request.has_route_vars?
84
- @path = Utils::String.remove_route_vars(@context.request.route_path)
84
+ @path = String.remove_route_vars(@context.request.route_path)
85
85
  else
86
86
  @path = @context.request.path
87
87
  end
@@ -97,21 +97,15 @@ module Pakyow
97
97
 
98
98
  def content
99
99
  to_present = view
100
- view.is_a?(ViewComposer) ? view.composed.to_html : view.to_html
100
+ to_present.is_a?(ViewComposer) ? to_present.composed.to_html : to_present.to_html
101
101
  end
102
102
 
103
103
  def view
104
- view = @composer || @view
105
- raise MissingView if view.nil?
106
-
107
- view.context = @context
108
-
109
- return view
104
+ @composer || @view || raise(MissingView)
110
105
  end
111
106
 
112
107
  def view=(view)
113
108
  @view = view
114
- @view.context = @context
115
109
 
116
110
  # setting a view means we no longer use/need the composer
117
111
  @composer = nil
@@ -174,16 +168,23 @@ module Pakyow
174
168
  end
175
169
 
176
170
  def setup_for_path(path, explicit = false)
177
- @composer = store.composer(path)
178
- @path = path
179
- rescue MissingView => e # catches no view path error
171
+ @view_stores.each do |name, store|
172
+ begin
173
+ @composer = store.composer(path)
174
+ @path = path
175
+ return
176
+ rescue MissingView
177
+ end
178
+ end
179
+
180
+ e = MissingView.new("No view at path '#{path}'")
180
181
  explicit ? raise(e) : Pakyow.logger.debug(e.message)
181
182
  end
182
183
 
183
184
  def load_views
184
185
  @view_stores = {}
185
186
 
186
- Config::Presenter.view_stores.each_pair {|name, path|
187
+ Pakyow::Config.presenter.view_stores.each_pair {|name, path|
187
188
  @view_stores[name] = ViewStore.new(path, name)
188
189
  }
189
190
  end
@@ -0,0 +1,287 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class StringDoc
4
+ attr_reader :structure
5
+
6
+ TITLE_REGEX = /<title>(.*?)<\/title>/m
7
+
8
+ def initialize(html)
9
+ @structure = StringDocParser.new(html).structure
10
+ end
11
+
12
+ def self.from_structure(structure, node: nil)
13
+ instance = allocate
14
+ instance.instance_variable_set(:@structure, structure)
15
+ instance.instance_variable_set(:@node, node)
16
+ return instance
17
+ end
18
+
19
+ def self.ensure(object)
20
+ return object if object.is_a?(StringDoc)
21
+ StringDoc.new(object)
22
+ end
23
+
24
+ def initialize_copy(original_doc)
25
+ super
26
+
27
+ original_structure = original_doc.instance_variable_get(:@structure)
28
+ @structure = Utils::Dup.deep(original_structure) if original_structure
29
+
30
+ original_node = original_doc.instance_variable_get(:@node)
31
+ if original_node
32
+ node_index = original_structure.index(original_node)
33
+ @node = @structure[node_index]
34
+ end
35
+ end
36
+
37
+ # Creates a StringDoc instance with the same structure, but a duped node.
38
+ #
39
+ def soft_copy
40
+ StringDoc.from_structure(@structure, node: @node ? Utils::Dup.deep(@node) : nil)
41
+ end
42
+
43
+ def title
44
+ title_search do |n, match|
45
+ return match[1]
46
+ end
47
+ end
48
+
49
+ def title=(title)
50
+ title_search do |n, match|
51
+ n.gsub!(TITLE_REGEX, "<title>#{title}</title>")
52
+ end
53
+ end
54
+
55
+ def set_attribute(name, value)
56
+ return if attributes.nil?
57
+ attributes[name.to_sym] = value
58
+ end
59
+ alias :update_attribute :set_attribute
60
+
61
+ def get_attribute(name)
62
+ attributes[name.to_sym]
63
+ end
64
+
65
+ def remove_attribute(name)
66
+ attributes.delete(name.to_sym)
67
+ end
68
+
69
+ def remove
70
+ @structure.delete_if { |n| n.equal?(node) }
71
+ end
72
+
73
+ def clear
74
+ children.clear
75
+ end
76
+
77
+ def text
78
+ html.gsub(/<[^>]*>/, '')
79
+ end
80
+
81
+ def text=(text)
82
+ clear
83
+ children << [text, {}, []]
84
+ end
85
+
86
+ def html
87
+ StringDocRenderer.render(children)
88
+ end
89
+
90
+ def html=(html)
91
+ clear
92
+ children << [html, {}, []]
93
+ end
94
+
95
+ def append(doc)
96
+ doc = StringDoc.ensure(doc)
97
+
98
+ if doc.node?
99
+ children.push(doc.node)
100
+ else
101
+ children.concat(doc.structure)
102
+ end
103
+ end
104
+
105
+ def prepend(doc)
106
+ doc = StringDoc.ensure(doc)
107
+
108
+ if doc.node?
109
+ children.unshift(doc.node)
110
+ else
111
+ children.unshift(*doc.structure)
112
+ end
113
+ end
114
+
115
+ def after(doc)
116
+ doc = StringDoc.ensure(doc)
117
+
118
+ if doc.node?
119
+ @structure.push(doc.node)
120
+ else
121
+ @structure.concat(doc.structure)
122
+ end
123
+ end
124
+
125
+ def before(doc)
126
+ doc = StringDoc.ensure(doc)
127
+
128
+ if doc.node?
129
+ @structure.unshift(doc.node)
130
+ else
131
+ @structure.unshift(*doc.structure)
132
+ end
133
+ end
134
+
135
+ def replace(doc)
136
+ doc = StringDoc.ensure(doc)
137
+ index = @structure.index(node) || 0
138
+
139
+ if doc.node?
140
+ @structure.insert(index + 1, node)
141
+ else
142
+ @structure.insert(index + 1, *doc.structure)
143
+ end
144
+
145
+ @structure.delete_at(index)
146
+ end
147
+
148
+ def scope(scope_name)
149
+ scopes.select { |b| b[:scope] == scope_name }
150
+ end
151
+
152
+ def prop(scope_name, prop_name)
153
+ return [] unless scope = scopes.select { |s| s[:scope] == scope_name }[0]
154
+ scope[:props].select { |p| p[:prop] == prop_name }
155
+ end
156
+
157
+ def container(name)
158
+ containers.fetch(name, {})[:doc]
159
+ end
160
+
161
+ def containers
162
+ find_containers(@node ? [@node] : @structure)
163
+ end
164
+
165
+ def partials
166
+ find_partials(@node ? [@node] : @structure)
167
+ end
168
+
169
+ def scopes
170
+ find_scopes(@node ? [@node] : @structure)
171
+ end
172
+
173
+ def to_html
174
+ StringDocRenderer.render(@node ? [@node] : @structure)
175
+ end
176
+ alias :to_s :to_html
177
+
178
+ def ==(o)
179
+ #TODO do this without rendering?
180
+ # (at least in the case of comparing StringDoc to StringDoc)
181
+ to_s == o.to_s
182
+ end
183
+
184
+ def node
185
+ return @structure if @structure.empty?
186
+ return @node || @structure[0]
187
+ end
188
+
189
+ def node?
190
+ !@node.nil?
191
+ end
192
+
193
+ def tagname
194
+ node[0].gsub(/[^a-zA-Z]/, '')
195
+ end
196
+
197
+ def option(value: nil)
198
+ StringDoc.from_structure(node[2][0][2].select { |option|
199
+ option[1][:value] == value.to_s
200
+ })
201
+ end
202
+
203
+ private
204
+
205
+ def title_search
206
+ @structure.flatten.each do |n|
207
+ next unless n.is_a?(String)
208
+ if match = n.match(TITLE_REGEX)
209
+ yield n, match
210
+ end
211
+ end
212
+ end
213
+
214
+ # Returns the structure representing the attributes for the node
215
+ #
216
+ def attributes
217
+ node[1]
218
+ end
219
+
220
+ def children
221
+ node[2][0][2]
222
+ end
223
+
224
+ def find_containers(structure, primary_structure = @structure, containers = {})
225
+ return {} if structure.empty?
226
+ structure.inject(containers) { |s, e|
227
+ if e[1].has_key?(:container)
228
+ s[e[1][:container]] = { doc: StringDoc.from_structure(primary_structure, node: e) }
229
+ end
230
+ find_containers(e[2], e[2], s)
231
+ s
232
+ } || {}
233
+ end
234
+
235
+ def find_partials(structure, primary_structure = @structure, partials = {})
236
+ structure.inject(partials) { |s, e|
237
+ if e[1].has_key?(:partial)
238
+ s[e[1][:partial]] = StringDoc.from_structure(primary_structure, node: e)
239
+ end
240
+ find_partials(e[2], e[2], s)
241
+ s
242
+ } || {}
243
+ end
244
+
245
+ def find_scopes(structure, primary_structure = @structure, scopes = [])
246
+ ret_scopes = structure.inject(scopes) { |s, e|
247
+ if e[1].has_key?(:'data-scope')
248
+ s << {
249
+ doc: StringDoc.from_structure(primary_structure, node: e),
250
+ scope: e[1][:'data-scope'].to_sym,
251
+ props: find_node_props(e).concat(find_props(e[2])),
252
+ nested: find_scopes(e[2]),
253
+ }
254
+ end
255
+ # only find scopes if `e` is the root node or we're not decending into a nested scope
256
+ find_scopes(e[2], e[2], s) if e == node || !e[1].has_key?(:'data-scope')
257
+ s
258
+ } || []
259
+
260
+ ret_scopes
261
+ end
262
+
263
+ def find_props(structure, primary_structure = @structure, props = [])
264
+ structure.each do |e|
265
+ find_node_props(e, primary_structure, props)
266
+ end
267
+
268
+ props || []
269
+ end
270
+
271
+ def find_node_props(node, primary_structure = @structure, props = [])
272
+ if node[1].has_key?(:'data-prop')
273
+ props << {
274
+ doc: StringDoc.from_structure(primary_structure, node: node),
275
+ prop: node[1][:'data-prop'].to_sym,
276
+ }
277
+ end
278
+
279
+ unless node[1].has_key?(:'data-scope')
280
+ find_props(node[2], node[2], props)
281
+ end
282
+
283
+ props
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,124 @@
1
+ module Pakyow
2
+ module Presenter
3
+ class StringDocParser
4
+ PARTIAL_REGEX = /<!--\s*@include\s*([a-zA-Z0-9\-_]*)\s*-->/
5
+ CONTAINER_REGEX = /@container( ([a-zA-Z0-9\-_]*))*/
6
+
7
+ def initialize(html)
8
+ @html = html
9
+ structure
10
+ end
11
+
12
+ def structure
13
+ @structure ||= parse(doc_from_string(@html))
14
+ end
15
+
16
+ private
17
+
18
+ # Parses HTML and returns a nested structure representing the document.
19
+ #
20
+ def parse(doc)
21
+ structure = []
22
+
23
+ if doc.is_a?(Nokogiri::HTML::Document)
24
+ structure << ['<!DOCTYPE html>', {}, []]
25
+ end
26
+
27
+ breadth_first(doc) do |node, queue|
28
+ if node == doc
29
+ queue.concat(node.children)
30
+ next
31
+ end
32
+
33
+ children = node.children.reject {|n| n.is_a?(Nokogiri::XML::Text)}
34
+ attributes = node.attributes
35
+ if children.empty? && !significant?(node)
36
+ structure << [node.to_html, {}, []]
37
+ else
38
+ if significant?(node)
39
+ if scope?(node) || prop?(node) || option?(node)
40
+ attr_structure = attributes.inject({}) do |attrs, attr|
41
+ attrs[attr[1].name.to_sym] = attr[1].value
42
+ attrs
43
+ end
44
+
45
+ closing = [['>', {}, parse(node)]]
46
+ closing << ["</#{node.name}>", {}, []] unless self_closing?(node.name)
47
+ structure << ["<#{node.name} ", attr_structure, closing]
48
+ elsif container?(node)
49
+ match = node.text.strip.match(CONTAINER_REGEX)
50
+ name = (match[2] || :default).to_sym
51
+ structure << [node.to_html, { container: name }, []]
52
+ elsif partial?(node)
53
+ next unless match = node.to_html.strip.match(PARTIAL_REGEX)
54
+ name = match[1].to_sym
55
+ structure << [node.to_html, { partial: name }, []]
56
+ end
57
+ else
58
+ attr_s = attributes.inject('') { |s, a| s << " #{a[1].name}=\"#{a[1].value}\""; s }
59
+ closing = [['>', {}, parse(node)]]
60
+ closing << ['</' + node.name + '>', {}, []] unless self_closing?(node.name)
61
+ structure << ['<' + node.name + attr_s, {}, closing]
62
+ end
63
+ end
64
+ end
65
+
66
+ return structure
67
+ end
68
+
69
+ def significant?(node)
70
+ scope?(node) || prop?(node) || container?(node) || partial?(node) || option?(node)
71
+ end
72
+
73
+ def scope?(node)
74
+ return false unless node['data-scope']
75
+ return true
76
+ end
77
+
78
+ def prop?(node)
79
+ return false unless node['data-prop']
80
+ return true
81
+ end
82
+
83
+ def container?(node)
84
+ return false unless node.is_a?(Nokogiri::XML::Comment)
85
+ return false unless node.text.strip.match(CONTAINER_REGEX)
86
+ return true
87
+ end
88
+
89
+ def partial?(node)
90
+ return false unless node.is_a?(Nokogiri::XML::Comment)
91
+ return false unless node.to_html.strip.match(PARTIAL_REGEX)
92
+ return true
93
+ end
94
+
95
+ def option?(node)
96
+ node.name == 'option'
97
+ end
98
+
99
+ def breadth_first(doc)
100
+ queue = [doc]
101
+ until queue.empty?
102
+ catch(:reject) do
103
+ node = queue.shift
104
+ yield node, queue
105
+ end
106
+ end
107
+ end
108
+
109
+ def doc_from_string(string)
110
+ if string.match(/<html.*>/)
111
+ Nokogiri::HTML::Document.parse(string)
112
+ else
113
+ Nokogiri::HTML.fragment(string)
114
+ end
115
+ end
116
+
117
+ SELF_CLOSING = %w[area base basefont br hr input img link meta]
118
+ def self_closing?(tag)
119
+ SELF_CLOSING.include? tag
120
+ end
121
+
122
+ end
123
+ end
124
+ end