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
@@ -0,0 +1,18 @@
|
|
1
|
+
module Pakyow
|
2
|
+
module Presenter
|
3
|
+
class StringDocRenderer
|
4
|
+
def self.render(structure)
|
5
|
+
structure.flatten.reject(&:empty?).map { |s|
|
6
|
+
s.is_a?(Hash) ? attrify(s) : s
|
7
|
+
}.join
|
8
|
+
end
|
9
|
+
|
10
|
+
IGNORED_ATTRS = %i[container partial]
|
11
|
+
def self.attrify(attrs)
|
12
|
+
attrs.delete_if { |a| a.nil? || IGNORED_ATTRS.include?(a) }.map { |attr|
|
13
|
+
attr[0].to_s + '="' + attr[1].to_s + '"'
|
14
|
+
}.join(' ')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,24 +1,21 @@
|
|
1
1
|
module Pakyow
|
2
2
|
module Presenter
|
3
3
|
class Template < View
|
4
|
-
include DocHelpers
|
5
|
-
include TitleHelpers
|
6
|
-
|
7
4
|
attr_accessor :name, :doc
|
8
5
|
|
9
6
|
class << self
|
10
7
|
def load(path)
|
11
|
-
format =
|
8
|
+
format = String.split_at_last_dot(path)[-1]
|
12
9
|
contents = File.read(path)
|
13
10
|
name = File.basename(path, '.*').to_sym
|
14
11
|
|
15
|
-
|
12
|
+
self.new(name, contents, format: format)
|
16
13
|
end
|
17
14
|
end
|
18
15
|
|
19
|
-
def initialize(name, contents = '', format
|
16
|
+
def initialize(name, contents = '', format: :html)
|
20
17
|
@name = name
|
21
|
-
super(contents, format)
|
18
|
+
super(contents, format: format)
|
22
19
|
end
|
23
20
|
|
24
21
|
def initialize_copy(original_template)
|
@@ -26,49 +23,28 @@ module Pakyow
|
|
26
23
|
|
27
24
|
# copy doc
|
28
25
|
@doc = original_template.doc.dup
|
29
|
-
@context = original_template.context
|
30
|
-
@composer = original_template.composer
|
31
26
|
end
|
32
27
|
|
33
28
|
def container(name = :default)
|
34
|
-
container
|
35
|
-
return view_from_path(container[:path])
|
36
|
-
end
|
37
|
-
|
38
|
-
def containers(refind = false)
|
39
|
-
@containers = (!@containers || refind) ? find_containers : @containers
|
29
|
+
View.from_doc(@doc.container(name.to_sym))
|
40
30
|
end
|
41
31
|
|
42
32
|
def build(page)
|
43
|
-
|
44
|
-
containers.each do |container|
|
33
|
+
@doc.containers.each do |container|
|
45
34
|
name = container[0]
|
46
35
|
|
47
36
|
begin
|
48
|
-
container
|
37
|
+
container[1][:doc].replace(page.content(name))
|
49
38
|
rescue MissingContainer
|
50
|
-
|
39
|
+
# This hasn't proven to be useful in dev (or prd for that matter)
|
40
|
+
# so decided to remove it. It'll save us from filling console / log
|
41
|
+
# with information that will most likely just be ignored.
|
42
|
+
#
|
43
|
+
# Pakyow.logger.info "No content for '#{name}' in page '#{page.path}'"
|
51
44
|
end
|
52
45
|
end
|
53
46
|
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
# returns an array of hashes, each with the container name and doc
|
60
|
-
def find_containers
|
61
|
-
containers = {}
|
62
|
-
|
63
|
-
@doc.traverse {|e|
|
64
|
-
next unless e.is_a?(Nokogiri::XML::Comment)
|
65
|
-
next unless match = e.text.strip.match(/@container( ([a-zA-Z0-9\-_]*))*/)
|
66
|
-
name = match[2] || :default
|
67
|
-
|
68
|
-
containers[name.to_sym] = { doc: e, path: path_to(e) }
|
69
|
-
}
|
70
|
-
|
71
|
-
return containers
|
47
|
+
View.from_doc(doc)
|
72
48
|
end
|
73
49
|
end
|
74
50
|
end
|
@@ -1,40 +1,28 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
1
3
|
module Pakyow
|
2
4
|
module Presenter
|
3
5
|
class View
|
4
|
-
|
5
|
-
include TitleHelpers
|
6
|
-
|
7
|
-
PARTIAL_REGEX = /<!--\s*@include\s*([a-zA-Z0-9\-_]*)\s*-->/
|
8
|
-
|
9
|
-
class << self
|
10
|
-
attr_accessor :binders
|
6
|
+
extend Forwardable
|
11
7
|
|
12
|
-
|
13
|
-
%w[area base basefont br hr input img link meta].include? tag
|
14
|
-
end
|
15
|
-
|
16
|
-
def form_field?(tag)
|
17
|
-
%w[input select textarea button].include? tag
|
18
|
-
end
|
8
|
+
def_delegators :@doc, :title=, :title, :remove, :clear, :text, :html
|
19
9
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
attr_accessor :doc, :scoped_as, :scopes, :related_views, :context, :composer
|
26
|
-
attr_writer :bindings
|
27
|
-
|
28
|
-
def initialize(contents = '', format = :html)
|
29
|
-
@related_views = []
|
10
|
+
# The object responsible for parsing, manipulating, and rendering
|
11
|
+
# the underlying HTML document for the view.
|
12
|
+
#
|
13
|
+
attr_reader :doc
|
30
14
|
|
31
|
-
|
15
|
+
# The scope, if any, that the view belongs to.
|
16
|
+
#
|
17
|
+
attr_accessor :scoped_as
|
32
18
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
19
|
+
# Creates a view, running `contents` through any registered view processors for `format`.
|
20
|
+
#
|
21
|
+
# @param contents [String] the contents of the view
|
22
|
+
# @param format [Symbol] the format of contents
|
23
|
+
#
|
24
|
+
def initialize(contents = '', format: :html)
|
25
|
+
@doc = Config.presenter.view_doc_class.new(Presenter.process(contents, format))
|
38
26
|
end
|
39
27
|
|
40
28
|
def initialize_copy(original_view)
|
@@ -42,179 +30,94 @@ module Pakyow
|
|
42
30
|
|
43
31
|
@doc = original_view.doc.dup
|
44
32
|
@scoped_as = original_view.scoped_as
|
45
|
-
@context = @context
|
46
|
-
@composer = @composer
|
47
33
|
end
|
48
34
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
def self.load(path)
|
56
|
-
format = Utils::String.split_at_last_dot(path)[-1]
|
57
|
-
contents = File.read(path)
|
58
|
-
|
59
|
-
return self.new(contents, format)
|
35
|
+
# Creates a new view with a soft copy of doc.
|
36
|
+
#
|
37
|
+
def soft_copy
|
38
|
+
copy = View.from_doc(@doc.soft_copy)
|
39
|
+
copy.scoped_as = scoped_as
|
40
|
+
copy
|
60
41
|
end
|
61
42
|
|
62
|
-
#
|
63
|
-
# root_view.find(selector).attributes(:class => my_class, :style => my_style)
|
43
|
+
# Creates a view from a doc.
|
64
44
|
#
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
45
|
+
# @see StringDoc
|
46
|
+
# @see NokogiriDoc
|
47
|
+
#
|
48
|
+
def self.from_doc(doc)
|
49
|
+
view = new
|
50
|
+
view.instance_variable_set(:@doc, doc)
|
51
|
+
view
|
73
52
|
end
|
74
53
|
|
75
|
-
|
76
|
-
|
77
|
-
def
|
78
|
-
|
79
|
-
# best we can do is to remove the children
|
80
|
-
doc.children.remove
|
81
|
-
else
|
82
|
-
doc.remove
|
83
|
-
end
|
84
|
-
|
85
|
-
invalidate!
|
54
|
+
# Creates a view from a file.
|
55
|
+
#
|
56
|
+
def self.load(path)
|
57
|
+
new(File.read(path), format: File.format(path))
|
86
58
|
end
|
87
59
|
|
88
|
-
|
89
|
-
|
90
|
-
def clear
|
91
|
-
return if self.doc.blank?
|
92
|
-
self.doc.inner_html = ''
|
93
|
-
self.invalidate!
|
60
|
+
def ==(other)
|
61
|
+
self.class == other.class && @doc == other.doc
|
94
62
|
end
|
95
63
|
|
96
|
-
|
97
|
-
|
64
|
+
# Allows multiple attributes to be set at once.
|
65
|
+
#
|
66
|
+
# view.attrs(class: '...', style: '...')
|
67
|
+
#
|
68
|
+
def attrs(attrs = {})
|
69
|
+
return Attributes.new(@doc) if attrs.empty?
|
70
|
+
bind_attributes_to_doc(attrs, @doc)
|
98
71
|
end
|
99
72
|
|
100
73
|
def text=(text)
|
101
74
|
text = text.call(self.text) if text.is_a?(Proc)
|
102
|
-
|
103
|
-
self.invalidate!
|
104
|
-
end
|
105
|
-
|
106
|
-
def html
|
107
|
-
self.doc.inner_html
|
75
|
+
@doc.text = text
|
108
76
|
end
|
109
77
|
|
110
78
|
def html=(html)
|
111
79
|
html = html.call(self.html) if html.is_a?(Proc)
|
112
|
-
|
113
|
-
self.invalidate!
|
80
|
+
@doc.html = html
|
114
81
|
end
|
115
82
|
|
116
83
|
def append(view)
|
117
|
-
doc
|
118
|
-
num = doc.children.count
|
119
|
-
path = self.path_to(doc)
|
120
|
-
|
121
|
-
self.doc.add_child(view.doc)
|
122
|
-
|
123
|
-
self.update_binding_offset_at_path(num, path)
|
124
|
-
self.invalidate!
|
84
|
+
@doc.append(view.doc)
|
125
85
|
end
|
126
86
|
|
127
87
|
def prepend(view)
|
128
|
-
doc
|
129
|
-
num = doc.children.count
|
130
|
-
path = self.path_to(doc)
|
131
|
-
|
132
|
-
if first_child = self.doc.children.first
|
133
|
-
first_child.add_previous_sibling(doc)
|
134
|
-
else
|
135
|
-
self.doc = doc
|
136
|
-
end
|
137
|
-
|
138
|
-
self.update_binding_offset_at_path(num, path)
|
139
|
-
self.invalidate!
|
88
|
+
@doc.prepend(view.doc)
|
140
89
|
end
|
141
90
|
|
91
|
+
#TODO allow strings?
|
142
92
|
def after(view)
|
143
|
-
doc
|
144
|
-
num = doc.children.count
|
145
|
-
path = self.path_to(doc)
|
146
|
-
|
147
|
-
self.doc.after(view.doc)
|
148
|
-
|
149
|
-
self.update_binding_offset_at_path(num, path)
|
150
|
-
self.invalidate!
|
93
|
+
@doc.after(view.doc)
|
151
94
|
end
|
152
95
|
|
153
96
|
def before(view)
|
154
|
-
doc
|
155
|
-
num = doc.children.count
|
156
|
-
path = self.path_to(doc)
|
157
|
-
|
158
|
-
self.doc.before(view.doc)
|
159
|
-
|
160
|
-
self.update_binding_offset_at_path(num, path)
|
161
|
-
self.invalidate!
|
97
|
+
@doc.before(view.doc)
|
162
98
|
end
|
163
99
|
|
164
100
|
def replace(view)
|
165
|
-
|
166
|
-
|
167
|
-
if doc.parent.nil?
|
168
|
-
doc.children.remove
|
169
|
-
doc.inner_html = view
|
170
|
-
else
|
171
|
-
doc.replace(view)
|
172
|
-
end
|
173
|
-
|
174
|
-
invalidate!
|
101
|
+
replacement = view.is_a?(View) ? view.doc : view
|
102
|
+
@doc.replace(replacement)
|
175
103
|
end
|
176
104
|
|
177
105
|
def scope(name)
|
178
106
|
name = name.to_sym
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
v = self.view_from_path(s[:path])
|
185
|
-
|
186
|
-
v.bindings = self.update_binding_paths_from_path([s].concat(s[:nested_bindings]), s[:path])
|
187
|
-
v.scoped_as = s[:scope]
|
188
|
-
v.context = @context
|
189
|
-
v.composer = @composer
|
190
|
-
|
191
|
-
views << v
|
192
|
-
}
|
193
|
-
|
194
|
-
views
|
107
|
+
@doc.scope(name).inject(ViewCollection.new) do |coll, scope|
|
108
|
+
view = View.from_doc(scope[:doc])
|
109
|
+
view.scoped_as = name
|
110
|
+
coll << view
|
111
|
+
end
|
195
112
|
end
|
196
113
|
|
197
114
|
def prop(name)
|
198
115
|
name = name.to_sym
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
if binding = self.bindings.select{|binding| binding[:scope] == self.scoped_as}[0]
|
205
|
-
binding[:props].each {|prop|
|
206
|
-
if prop[:prop] == name
|
207
|
-
v = self.view_from_path(prop[:path])
|
208
|
-
|
209
|
-
v.scoped_as = self.scoped_as
|
210
|
-
v.context = @context
|
211
|
-
v.composer = @composer
|
212
|
-
views << v
|
213
|
-
end
|
214
|
-
}
|
116
|
+
@doc.prop(scoped_as, name).inject(ViewCollection.new) do |coll, prop|
|
117
|
+
view = View.from_doc(prop[:doc])
|
118
|
+
view.scoped_as = scoped_as
|
119
|
+
coll << view
|
215
120
|
end
|
216
|
-
|
217
|
-
views
|
218
121
|
end
|
219
122
|
|
220
123
|
# call-seq:
|
@@ -224,7 +127,7 @@ module Pakyow
|
|
224
127
|
#
|
225
128
|
def with(&block)
|
226
129
|
if block.arity == 0
|
227
|
-
|
130
|
+
instance_exec(&block)
|
228
131
|
else
|
229
132
|
yield(self)
|
230
133
|
end
|
@@ -242,13 +145,11 @@ module Pakyow
|
|
242
145
|
# (this is basically Bret's `map` function)
|
243
146
|
#
|
244
147
|
def for(data, &block)
|
245
|
-
|
246
|
-
data = [data] if (!data.is_a?(Enumerable) || data.is_a?(Hash))
|
247
|
-
|
148
|
+
datum = Array.ensure(data).first
|
248
149
|
if block.arity == 1
|
249
|
-
|
150
|
+
instance_exec(datum, &block)
|
250
151
|
else
|
251
|
-
block.call(self,
|
152
|
+
block.call(self, datum)
|
252
153
|
end
|
253
154
|
end
|
254
155
|
|
@@ -258,7 +159,7 @@ module Pakyow
|
|
258
159
|
# Yields a view, its matching dataum, and the index. See #for.
|
259
160
|
#
|
260
161
|
def for_with_index(data, &block)
|
261
|
-
|
162
|
+
self.for(data) do |ctx, datum|
|
262
163
|
if block.arity == 2
|
263
164
|
ctx.instance_exec(datum, 0, &block)
|
264
165
|
else
|
@@ -275,27 +176,29 @@ module Pakyow
|
|
275
176
|
# of self, where n = data.length.
|
276
177
|
#
|
277
178
|
def match(data)
|
278
|
-
data =
|
279
|
-
|
179
|
+
data = Array.ensure(data)
|
180
|
+
coll = ViewCollection.new
|
280
181
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
182
|
+
# an empty set always means an empty view
|
183
|
+
if data.empty?
|
184
|
+
remove
|
185
|
+
else
|
186
|
+
# dup for later
|
187
|
+
original_view = dup if data.length > 1
|
287
188
|
|
288
|
-
|
289
|
-
|
290
|
-
v.scoped_as = self.scoped_as
|
291
|
-
v.context = @context
|
292
|
-
v.composer = @composer
|
189
|
+
# the original view match the first datum
|
190
|
+
coll << self
|
293
191
|
|
294
|
-
views
|
295
|
-
|
192
|
+
# create views for the other datums
|
193
|
+
data[1..-1].inject(coll) { |coll|
|
194
|
+
duped_view = original_view.dup
|
195
|
+
after(duped_view)
|
196
|
+
coll << duped_view
|
197
|
+
}
|
198
|
+
end
|
296
199
|
|
297
|
-
|
298
|
-
|
200
|
+
# return the new collection
|
201
|
+
coll
|
299
202
|
end
|
300
203
|
|
301
204
|
# call-seq:
|
@@ -304,7 +207,7 @@ module Pakyow
|
|
304
207
|
# Matches self with data and yields a view/datum pair.
|
305
208
|
#
|
306
209
|
def repeat(data, &block)
|
307
|
-
|
210
|
+
match(data).for(data, &block)
|
308
211
|
end
|
309
212
|
|
310
213
|
# call-seq:
|
@@ -313,29 +216,23 @@ module Pakyow
|
|
313
216
|
# Matches self with data and yields a view/datum pair with index.
|
314
217
|
#
|
315
218
|
def repeat_with_index(data, &block)
|
316
|
-
|
219
|
+
match(data).for_with_index(data, &block)
|
317
220
|
end
|
318
221
|
|
319
222
|
# call-seq:
|
320
223
|
# bind(data)
|
321
224
|
#
|
322
|
-
# Binds
|
225
|
+
# Binds a single datum across existing scopes.
|
323
226
|
#
|
324
|
-
def bind(data, bindings
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
scope_info = self.bindings.first
|
329
|
-
|
330
|
-
self.bind_data_to_scope(data[0], scope_info, bindings)
|
331
|
-
invalidate!(true)
|
332
|
-
|
227
|
+
def bind(data, bindings: {}, context: nil, &block)
|
228
|
+
datum = Array.ensure(data).first
|
229
|
+
bind_data_to_scope(datum, doc.scopes.first, bindings, context)
|
333
230
|
return if block.nil?
|
334
231
|
|
335
232
|
if block.arity == 1
|
336
|
-
|
233
|
+
instance_exec(datum, &block)
|
337
234
|
else
|
338
|
-
block.call(self,
|
235
|
+
block.call(self, datum)
|
339
236
|
end
|
340
237
|
end
|
341
238
|
|
@@ -344,8 +241,8 @@ module Pakyow
|
|
344
241
|
#
|
345
242
|
# Binds data across existing scopes, yielding a view/datum pair with index.
|
346
243
|
#
|
347
|
-
def bind_with_index(
|
348
|
-
|
244
|
+
def bind_with_index(*a, **k, &block)
|
245
|
+
bind(*a, **k) do |ctx, datum|
|
349
246
|
if block.arity == 2
|
350
247
|
ctx.instance_exec(datum, 0, &block)
|
351
248
|
else
|
@@ -359,207 +256,75 @@ module Pakyow
|
|
359
256
|
#
|
360
257
|
# Matches self to data then binds data to the view.
|
361
258
|
#
|
362
|
-
def apply(data, bindings
|
363
|
-
|
364
|
-
end
|
365
|
-
|
366
|
-
def bindings(refind = false)
|
367
|
-
@bindings = (!@bindings || refind) ? self.find_bindings : @bindings
|
259
|
+
def apply(data, bindings: {}, context: nil, &block)
|
260
|
+
match(data).bind(data, bindings: bindings, context: context, &block)
|
368
261
|
end
|
369
262
|
|
370
263
|
def includes(partial_map)
|
264
|
+
partials = @doc.partials
|
371
265
|
partial_map = partial_map.dup
|
372
266
|
|
373
267
|
# mixin all the partials
|
374
|
-
partials.each do |
|
375
|
-
partial
|
376
|
-
|
377
|
-
|
378
|
-
# now delete them from the map
|
379
|
-
partials.each do |partial|
|
380
|
-
partial_map.delete(partial[0])
|
268
|
+
partials.each do |partial_info|
|
269
|
+
partial = partial_map[partial_info[0]]
|
270
|
+
next if partial.nil?
|
271
|
+
partial_info[1].replace(partial.doc.dup)
|
381
272
|
end
|
382
273
|
|
383
|
-
#
|
384
|
-
|
385
|
-
# initiate another build if content contains partials
|
386
|
-
includes(partial_map) if partials(true).count > 0
|
387
|
-
end
|
388
|
-
|
389
|
-
return self
|
390
|
-
end
|
274
|
+
# refind the partials
|
275
|
+
partials = @doc.partials
|
391
276
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
@related_views.each {|v|
|
397
|
-
v.invalidate!(composer_only)
|
398
|
-
}
|
399
|
-
end
|
277
|
+
# if mixed in partials included partials, we want to run includes again with a new map
|
278
|
+
if partials.count > 0 && (partial_map.keys - partials.keys).count < partial_map.keys.count
|
279
|
+
includes(partial_map)
|
280
|
+
end
|
400
281
|
|
401
|
-
|
402
|
-
|
403
|
-
def partials(refind = false)
|
404
|
-
@partials = (!@partials || refind) ? find_partials : @partials
|
405
|
-
end
|
406
|
-
|
407
|
-
def partials_in(content)
|
408
|
-
partials = []
|
409
|
-
|
410
|
-
content.scan(PARTIAL_REGEX) do |m|
|
411
|
-
partials << m[0].to_sym
|
412
|
-
end
|
413
|
-
|
414
|
-
return partials
|
415
|
-
end
|
416
|
-
|
417
|
-
def find_partials
|
418
|
-
partials = []
|
419
|
-
|
420
|
-
@doc.traverse { |e|
|
421
|
-
next unless e.is_a?(Nokogiri::XML::Comment)
|
422
|
-
next unless match = e.to_html.strip.match(PARTIAL_REGEX)
|
423
|
-
|
424
|
-
name = match[1]
|
425
|
-
partials << [name.to_sym, e]
|
426
|
-
}
|
427
|
-
|
428
|
-
return partials
|
429
|
-
end
|
430
|
-
|
431
|
-
# populates the root_view using view_store data by recursively building
|
432
|
-
# and substituting in child views named in the structure
|
433
|
-
def populate_view(root_view, view_store, view_info)
|
434
|
-
root_view.containers.each {|e|
|
435
|
-
next unless path = view_info[e[:name]]
|
436
|
-
|
437
|
-
v = self.populate_view(View.new(path, view_store), view_store, view_info)
|
438
|
-
v.context = @context
|
439
|
-
v.composer = @composer
|
440
|
-
self.reset_container(e[:doc])
|
441
|
-
self.add_content_to_container(v, e[:doc])
|
442
|
-
}
|
443
|
-
root_view
|
444
|
-
end
|
445
|
-
|
446
|
-
|
447
|
-
# returns an array of hashes that describe each scope
|
448
|
-
def find_bindings(doc = @doc, ignore_root = false)
|
449
|
-
bindings = []
|
450
|
-
breadth_first(doc) {|o|
|
451
|
-
next if o == doc && ignore_root
|
452
|
-
next if !scope = o[Config::Presenter.scope_attribute]
|
453
|
-
|
454
|
-
bindings << {
|
455
|
-
:scope => scope.to_sym,
|
456
|
-
:path => path_to(o),
|
457
|
-
:props => find_props(o)
|
458
|
-
}
|
459
|
-
|
460
|
-
if o == doc
|
461
|
-
# this is the root node, which we need as the first hash in the
|
462
|
-
# list of bindings, but we don't want to nest other scopes inside
|
463
|
-
# of it in this case
|
464
|
-
bindings.last[:nested_bindings] = []
|
465
|
-
else
|
466
|
-
bindings.last[:nested_bindings] = find_bindings(o, true)
|
467
|
-
# reject so children aren't traversed
|
468
|
-
throw :reject
|
469
|
-
end
|
470
|
-
}
|
471
|
-
|
472
|
-
# find unscoped props
|
473
|
-
unless doc[Config::Presenter.scope_attribute]
|
474
|
-
bindings.unshift({
|
475
|
-
:scope => nil,
|
476
|
-
:path => [],
|
477
|
-
:props => find_props(doc),
|
478
|
-
:nested_bindings => []
|
479
|
-
})
|
480
|
-
end
|
481
|
-
|
482
|
-
return bindings
|
483
|
-
end
|
484
|
-
|
485
|
-
def find_props(o)
|
486
|
-
props = []
|
487
|
-
breadth_first(o) {|so|
|
488
|
-
# don't go into deeper scopes
|
489
|
-
throw :reject if so != o && so[Config::Presenter.scope_attribute]
|
490
|
-
|
491
|
-
next unless prop = so[Config::Presenter.prop_attribute]
|
492
|
-
props << {:prop => prop.to_sym, :path => path_to(so)}
|
493
|
-
}
|
494
|
-
|
495
|
-
return props
|
496
|
-
end
|
497
|
-
|
498
|
-
# returns a new binding set that takes into account the starting point of `path`
|
499
|
-
def update_binding_paths_from_path(bindings, path)
|
500
|
-
return bindings.collect { |binding|
|
501
|
-
dup_binding = binding.dup
|
502
|
-
dup_binding[:path] = dup_binding[:path][path.length..-1] || []
|
503
|
-
|
504
|
-
dup_binding[:props] = dup_binding[:props].collect {|prop|
|
505
|
-
dup_prop = prop.dup
|
506
|
-
dup_prop[:path] = dup_prop[:path][path.length..-1]
|
507
|
-
dup_prop
|
508
|
-
}
|
509
|
-
|
510
|
-
dup_binding[:nested_bindings] = update_binding_paths_from_path(dup_binding[:nested_bindings], path)
|
511
|
-
|
512
|
-
dup_binding
|
513
|
-
}
|
282
|
+
self
|
514
283
|
end
|
515
284
|
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
285
|
+
def to_html
|
286
|
+
@doc.to_html
|
287
|
+
end
|
288
|
+
alias :to_s :to_html
|
520
289
|
|
521
|
-
|
290
|
+
private
|
522
291
|
|
523
|
-
|
524
|
-
prop[:path][0] += offset if prop[:path][0]
|
525
|
-
}
|
526
|
-
}
|
527
|
-
end
|
528
|
-
|
529
|
-
def bind_data_to_scope(data, scope_info, bindings = {})
|
292
|
+
def bind_data_to_scope(data, scope_info, bindings, ctx)
|
530
293
|
return unless data
|
294
|
+
return unless scope_info
|
531
295
|
|
532
296
|
scope = scope_info[:scope]
|
297
|
+
bind_data_to_root(data, scope, bindings, ctx)
|
533
298
|
|
534
|
-
|
535
|
-
|
536
|
-
scope_info[:props].each { |prop_info|
|
537
|
-
catch(:unbound) {
|
299
|
+
scope_info[:props].each do |prop_info|
|
300
|
+
catch(:unbound) do
|
538
301
|
prop = prop_info[:prop]
|
539
302
|
|
540
|
-
if data_has_prop?(data, prop) ||
|
541
|
-
value =
|
542
|
-
doc =
|
303
|
+
if data_has_prop?(data, prop) || Binder.instance.has_scoped_prop?(scope, prop, bindings)
|
304
|
+
value = Binder.instance.value_for_scoped_prop(scope, prop, data, bindings, ctx)
|
305
|
+
doc = prop_info[:doc]
|
543
306
|
|
544
|
-
if
|
545
|
-
bind_to_form_field(doc, scope, prop, value, data)
|
307
|
+
if DocHelpers.form_field?(doc.tagname)
|
308
|
+
bind_to_form_field(doc, scope, prop, value, data, ctx)
|
546
309
|
end
|
547
310
|
|
548
311
|
bind_data_to_doc(doc, value)
|
549
312
|
else
|
550
313
|
handle_unbound_data(scope, prop)
|
551
314
|
end
|
552
|
-
|
553
|
-
|
315
|
+
end
|
316
|
+
end
|
554
317
|
end
|
555
318
|
|
556
|
-
def bind_data_to_root(data, scope, bindings)
|
557
|
-
|
558
|
-
|
319
|
+
def bind_data_to_root(data, scope, bindings, ctx)
|
320
|
+
value = Binder.instance.value_for_scoped_prop(scope, :_root, data, bindings, ctx)
|
321
|
+
return if value.nil?
|
322
|
+
|
323
|
+
value.is_a?(Hash) ? bind_attributes_to_doc(value, doc) : bind_value_to_doc(value, doc)
|
559
324
|
end
|
560
325
|
|
561
326
|
def bind_data_to_doc(doc, data)
|
562
|
-
data.is_a?(Hash) ?
|
327
|
+
data.is_a?(Hash) ? bind_attributes_to_doc(data, doc) : bind_value_to_doc(data, doc)
|
563
328
|
end
|
564
329
|
|
565
330
|
def data_has_prop?(data, prop)
|
@@ -569,80 +334,57 @@ module Pakyow
|
|
569
334
|
def bind_value_to_doc(value, doc)
|
570
335
|
value = String(value)
|
571
336
|
|
572
|
-
tag = doc.
|
573
|
-
return if
|
337
|
+
tag = doc.tagname
|
338
|
+
return if DocHelpers.tag_without_value?(tag)
|
574
339
|
|
575
|
-
if
|
340
|
+
if DocHelpers.self_closing_tag?(tag)
|
576
341
|
# don't override value if set
|
577
|
-
if !doc
|
578
|
-
doc
|
342
|
+
if !doc.get_attribute(:value) || doc.get_attribute(:value).empty?
|
343
|
+
doc.set_attribute(:value, value)
|
579
344
|
end
|
580
345
|
else
|
581
|
-
doc.
|
582
|
-
end
|
583
|
-
end
|
584
|
-
|
585
|
-
def bind_attributes_to_doc(attrs, doc)
|
586
|
-
attrs.each do |attr, v|
|
587
|
-
case attr
|
588
|
-
when :content
|
589
|
-
v = v.call(doc.inner_html) if v.is_a?(Proc)
|
590
|
-
bind_value_to_doc(v, doc)
|
591
|
-
next
|
592
|
-
when :view
|
593
|
-
v.call(self)
|
594
|
-
next
|
595
|
-
end
|
596
|
-
|
597
|
-
attr = attr.to_s
|
598
|
-
attrs = Attributes.new(doc)
|
599
|
-
v = v.call(attrs.send(attr)) if v.is_a?(Proc)
|
600
|
-
|
601
|
-
if v.nil?
|
602
|
-
doc.remove_attribute(attr)
|
603
|
-
else
|
604
|
-
attrs.send(:"#{attr}=", v)
|
605
|
-
end
|
346
|
+
doc.html = value
|
606
347
|
end
|
607
348
|
end
|
608
349
|
|
609
|
-
def bind_to_form_field(doc, scope, prop, value, bindable)
|
350
|
+
def bind_to_form_field(doc, scope, prop, value, bindable, ctx)
|
610
351
|
set_form_field_name(doc, scope, prop)
|
611
352
|
|
612
353
|
# special binding for checkboxes and radio buttons
|
613
|
-
if doc.
|
354
|
+
if doc.tagname == 'input' && (doc.get_attribute(:type) == 'checkbox' || doc.get_attribute(:type) == 'radio')
|
614
355
|
bind_to_checked_field(doc, value)
|
615
|
-
|
616
|
-
elsif doc.
|
617
|
-
bind_to_select_field(doc, scope, prop, value, bindable)
|
356
|
+
# special binding for selects
|
357
|
+
elsif doc.tagname == 'select'
|
358
|
+
bind_to_select_field(doc, scope, prop, value, bindable, ctx)
|
618
359
|
end
|
619
360
|
end
|
620
361
|
|
621
362
|
def bind_to_checked_field(doc, value)
|
622
|
-
if value == true || (doc
|
623
|
-
doc
|
363
|
+
if value == true || (doc.get_attribute(:value) && doc.get_attribute(:value) == value.to_s)
|
364
|
+
doc.set_attribute(:checked, 'checked')
|
624
365
|
else
|
625
|
-
doc.
|
366
|
+
doc.remove_attribute(:checked)
|
626
367
|
end
|
627
368
|
|
628
369
|
# coerce to string since booleans are often used and fail when binding to a view
|
629
|
-
value
|
370
|
+
value.to_s
|
630
371
|
end
|
631
372
|
|
632
|
-
def bind_to_select_field(doc, scope, prop, value, bindable)
|
633
|
-
create_select_options(doc, scope, prop, value, bindable)
|
373
|
+
def bind_to_select_field(doc, scope, prop, value, bindable, ctx)
|
374
|
+
create_select_options(doc, scope, prop, value, bindable, ctx)
|
634
375
|
select_option_with_value(doc, value)
|
635
376
|
end
|
636
377
|
|
637
378
|
def set_form_field_name(doc, scope, prop)
|
638
|
-
return if doc
|
639
|
-
doc
|
379
|
+
return if doc.get_attribute(:name) && !doc.get_attribute(:name).empty? # don't overwrite the name if already defined
|
380
|
+
doc.set_attribute(:name, "#{scope}[#{prop}]")
|
640
381
|
end
|
641
382
|
|
642
|
-
def create_select_options(doc, scope, prop, value, bindable)
|
643
|
-
|
383
|
+
def create_select_options(doc, scope, prop, value, bindable, ctx)
|
384
|
+
options = Binder.instance.options_for_scoped_prop(scope, prop, bindable, ctx)
|
385
|
+
return if options.nil?
|
644
386
|
|
645
|
-
option_nodes = Nokogiri::HTML::DocumentFragment.parse
|
387
|
+
option_nodes = Nokogiri::HTML::DocumentFragment.parse('')
|
646
388
|
Nokogiri::HTML::Builder.with(option_nodes) do |h|
|
647
389
|
until options.length == 0
|
648
390
|
catch :optgroup do
|
@@ -650,18 +392,18 @@ module Pakyow
|
|
650
392
|
|
651
393
|
# an array containing value/content
|
652
394
|
if o.is_a?(Array)
|
653
|
-
h.option o[1], :
|
395
|
+
h.option o[1], value: o[0]
|
654
396
|
options.shift
|
655
397
|
# likely an object (e.g. string); start a group
|
656
398
|
else
|
657
|
-
h.optgroup(:
|
399
|
+
h.optgroup(label: o) {
|
658
400
|
options.shift
|
659
401
|
|
660
402
|
options[0..-1].each_with_index { |o2,i2|
|
661
403
|
# starting a new group
|
662
|
-
throw :optgroup
|
404
|
+
throw :optgroup unless o2.is_a?(Array)
|
663
405
|
|
664
|
-
h.option o2[1], :
|
406
|
+
h.option o2[1], value: o2[0]
|
665
407
|
options.shift
|
666
408
|
}
|
667
409
|
}
|
@@ -671,21 +413,47 @@ module Pakyow
|
|
671
413
|
end
|
672
414
|
|
673
415
|
# remove existing options
|
674
|
-
doc.
|
416
|
+
doc.clear
|
675
417
|
|
676
418
|
# add generated options
|
677
|
-
doc.
|
419
|
+
doc.append(option_nodes.to_html)
|
678
420
|
end
|
679
421
|
|
680
422
|
def select_option_with_value(doc, value)
|
681
|
-
|
682
|
-
|
423
|
+
option = doc.option(value: value)
|
424
|
+
return if option.nil?
|
425
|
+
|
426
|
+
option.set_attribute(:selected, 'selected')
|
683
427
|
end
|
684
428
|
|
685
429
|
def handle_unbound_data(scope, prop)
|
686
|
-
Pakyow.logger.warn("Unbound data for #{scope}[#{prop}]")
|
430
|
+
Pakyow.logger.warn("Unbound data for #{scope}[#{prop}]") if Pakyow.logger
|
687
431
|
throw :unbound
|
688
432
|
end
|
433
|
+
|
434
|
+
def bind_attributes_to_doc(attrs, doc)
|
435
|
+
attrs.each do |attr, v|
|
436
|
+
case attr
|
437
|
+
when :content
|
438
|
+
v = v.call(doc.inner_html) if v.is_a?(Proc)
|
439
|
+
bind_value_to_doc(v, doc)
|
440
|
+
next
|
441
|
+
when :view
|
442
|
+
v.call(self)
|
443
|
+
next
|
444
|
+
else
|
445
|
+
attr = attr.to_s
|
446
|
+
attrs = Attributes.new(doc)
|
447
|
+
v = v.call(attrs.send(attr)) if v.is_a?(Proc)
|
448
|
+
|
449
|
+
if v.nil?
|
450
|
+
doc.remove_attribute(attr)
|
451
|
+
else
|
452
|
+
attrs.send(:"#{attr}=", v)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
689
457
|
end
|
690
458
|
end
|
691
459
|
end
|