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.
- checksums.yaml +4 -4
- data/pakyow-presenter/CHANGES +11 -1
- data/pakyow-presenter/lib/presenter/attributes.rb +18 -15
- data/pakyow-presenter/lib/presenter/base.rb +8 -2
- data/pakyow-presenter/lib/presenter/binder.rb +53 -43
- data/pakyow-presenter/lib/presenter/binder_set.rb +18 -27
- data/pakyow-presenter/lib/presenter/binding_eval.rb +23 -0
- data/pakyow-presenter/lib/presenter/config/presenter.rb +36 -61
- data/pakyow-presenter/lib/presenter/doc_helpers.rb +6 -66
- data/pakyow-presenter/lib/presenter/helpers.rb +26 -3
- data/pakyow-presenter/lib/presenter/nokogiri_doc.rb +321 -0
- data/pakyow-presenter/lib/presenter/page.rb +5 -14
- data/pakyow-presenter/lib/presenter/presenter.rb +14 -13
- data/pakyow-presenter/lib/presenter/string_doc.rb +287 -0
- data/pakyow-presenter/lib/presenter/string_doc_parser.rb +124 -0
- data/pakyow-presenter/lib/presenter/string_doc_renderer.rb +18 -0
- data/pakyow-presenter/lib/presenter/template.rb +13 -37
- data/pakyow-presenter/lib/presenter/view.rb +192 -424
- data/pakyow-presenter/lib/presenter/view_collection.rb +70 -112
- data/pakyow-presenter/lib/presenter/view_composer.rb +2 -47
- data/pakyow-presenter/lib/presenter/view_context.rb +89 -0
- data/pakyow-presenter/lib/presenter/view_store.rb +12 -12
- metadata +30 -10
- data/pakyow-presenter/lib/presenter/title_helpers.rb +0 -21
@@ -81,7 +81,7 @@ module Pakyow
|
|
81
81
|
@context = context
|
82
82
|
|
83
83
|
if @context.request.has_route_vars?
|
84
|
-
@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
|
-
|
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
|
-
|
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
|
-
@
|
178
|
-
|
179
|
-
|
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
|
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
|