pakyow-presenter 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|