persia 0.1.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.
@@ -0,0 +1,175 @@
1
+ require 'persia'
2
+
3
+ module ActionController #:nodoc:
4
+ class Base
5
+ include ActionView::Helpers::TextHelper
6
+
7
+ # original version of method in actionpack 1.12.5
8
+ # def render_file(template_path, status = nil, use_full_path = false, locals = {}) #:nodoc:
9
+ # add_variables_to_assigns
10
+ # assert_existence_of_template_file(template_path) if use_full_path
11
+ # logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
12
+ # render_text(@template.render_file(template_path, use_full_path, locals), status)
13
+ # end
14
+
15
+ def render_file(template_path, status = nil, use_full_path = false, locals = {}) #:nodoc:
16
+ add_variables_to_assigns
17
+ r = check_mdml and return r
18
+ assert_existence_of_template_file(template_path) if use_full_path
19
+ logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
20
+ render_text(@template.render_file(template_path, use_full_path, locals), status)
21
+ end
22
+
23
+ # original version of method in actionpack 1.12.5
24
+ # def render_action(action_name, status = nil, with_layout = true) #:nodoc:
25
+ # template = default_template_name(action_name.to_s)
26
+ # if with_layout && !template_exempt_from_layout?(template)
27
+ # render_with_layout(template, status)
28
+ # else
29
+ # render_without_layout(template, status)
30
+ # end
31
+ # end
32
+
33
+ def render_action(action_name, status = nil, with_layout = true) #:nodoc:
34
+ r = check_mdml(action_name) and return r
35
+ template = default_template_name(action_name.to_s)
36
+ if with_layout && !template_exempt_from_layout?(template)
37
+ render_with_layout(template, status)
38
+ else
39
+ render_without_layout(template, status)
40
+ end
41
+ end
42
+
43
+ def check_mdml(action = nil)
44
+ begin
45
+ handle_mdml(view_class, action)
46
+ rescue Exception => e
47
+ logger.error(e)
48
+ render_text "<h1>Exception</h1> <pre>#{ERB::Util.h(e.to_s)}\n#{e.backtrace.join("\n")}</pre>"
49
+ end
50
+ end
51
+
52
+ def view_class
53
+ # set to false if missing
54
+ return nil if @view_class == false
55
+ require_dependency "views/#{self.class.controller_name}/view"
56
+ view_classname = "#{self.class.controller_class_name.to_s[0...-10]}View"
57
+ if Module.constants.include?(view_classname)
58
+ @view_class = eval(view_classname)
59
+ else
60
+ @view_class = false
61
+ end
62
+ end
63
+
64
+ def handle_mdml(view_class, action = nil)
65
+ action ||= self.action_name
66
+ vc = self.view_class
67
+ return false unless vc and vc.public_method_defined?(action)
68
+ view = vc.new
69
+ view.controller = self
70
+ add_variables_to_assigns
71
+ view.load_assigns(@assigns)
72
+ html = Persia::Element.new 'html'
73
+ cursor = Persia::Cursor.create_element(html, view)
74
+ view.send(action, cursor)
75
+ render_text cursor.to_s
76
+ end
77
+ end
78
+ end
79
+
80
+ module ActionView #:nodoc:
81
+ class PersiaView
82
+ include Persia
83
+ include ActionView::Helpers::UrlHelper
84
+ include ActionView::Helpers::TagHelper
85
+ include ActionView::Helpers::AssetTagHelper
86
+ include ActionView::Helpers::ActiveRecordHelper
87
+
88
+ # resources used by view - :logical_name => "file_name"
89
+ class << self
90
+ attr_accessor :view_resources
91
+ end
92
+
93
+ attr_accessor :request, :controller
94
+
95
+ # filename (relative to project root) => id_hash
96
+ # only used in PersiaView itself
97
+ @resources = {}
98
+
99
+ def PersiaView.inherited(subclass)
100
+ pattern = "app/views/#{subclass.view_name}/*.mdml"
101
+ glob = Dir.glob(pattern)
102
+ r = glob.map{|x|[x[(pattern.size-6)...-5].to_sym, x]}.flatten
103
+ h = (subclass.view_resources = Hash[*r]).values
104
+ puts "Found #{h.size} mdml files in view dir."
105
+ h.each do |fn|
106
+ puts "Loading resources for file #{fn} in class #{subclass.name}"
107
+ load_resource fn
108
+ end
109
+ subclass.update_id_hash h
110
+ end
111
+
112
+ # load and parse resource
113
+ def PersiaView.load_resource(fn)
114
+ rexml = File.open(fn, 'r') do |f|
115
+ begin
116
+ REXML::Document.new f
117
+ rescue REXML::ParseException => e
118
+ puts "Error parsing file [#{fn}]:", e.to_s
119
+ return
120
+ end
121
+ end
122
+ r = @resources[fn] = {}
123
+ Element.new(rexml, r)
124
+ puts "Loaded #{r.size} resources"
125
+ end
126
+
127
+ # return resources id_hash for filename
128
+ def PersiaView.resource_for(filename)
129
+ @resources[filename]
130
+ end
131
+
132
+ # define resources outside view dir: :logical_name => "file_name"
133
+ def self.define_resources(hash)
134
+ @view_resources.merge! hash
135
+ hash.each_value {|fn| PersiaView.load_resource fn }
136
+ update_id_hash(hash.values)
137
+ end
138
+
139
+ def self.update_id_hash(filenames)
140
+ # merged hashes of used resources
141
+ # only used in subclasses of PersiaView
142
+ @id_hash ||= {}
143
+ filenames.each do |fn|
144
+ r = PersiaView.resource_for(fn)
145
+ @id_hash.merge! r
146
+ end
147
+ end
148
+
149
+ def self.resource_by_id(id, logical_resource = nil)
150
+ hash = if logical_resource
151
+ fn = @view_resources[logical_resource.to_sym]
152
+ r = PersiaView.resource_for(fn)
153
+ r.empty? and raise "No resources found in file #{fn}.\nPlease set xmlns attribute on root and mark resources with id tags."
154
+ r
155
+ else
156
+ @id_hash
157
+ end
158
+ hash.empty? and raise "No resources found at all.\nPlease set xmlns attribute on root and mark resources with id tags for all mdml files in your view directory."
159
+ hash[id.to_sym] or raise "No resource with id[#{id}] found in resources #{hash.keys.inspect}"
160
+ end
161
+
162
+ def self.id_hash
163
+ @id_hash
164
+ end
165
+
166
+ # returns the name of class, minus 'View' in under_score notation
167
+ def self.view_name
168
+ self.name.split('::')[-1][0...-4].underscore
169
+ end
170
+
171
+ def load_assigns(assigns)
172
+ assigns.each {|k,v| instance_variable_set("@#{k}",v) }
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,439 @@
1
+ require 'xml'
2
+
3
+ module Persia
4
+ # Creates a macro which stores commands, and which can be
5
+ # played back later on a Cursor.
6
+ def macro
7
+ Macro.new
8
+ end
9
+
10
+ # A cursor points to a position within a DOM tree. Some methods
11
+ # in the cursor class return a new cursor pointing at a new position,
12
+ # other modify the DOM tree at the current position.
13
+ # When the cursor points at a list of elements, it will be interpreted as
14
+ # pointing to the first element of that list if the command would otherwise
15
+ # be nonsensical:
16
+ # cursor.tr => points to list of tr elements that are children of current element.
17
+ # cursor.tr.td => points to list of td elements that are children of first tr element of current element.
18
+ # cursor.tr[1].td => points to list of td elements that are children of second tr element of current element.
19
+ # cursor.id![:title] => find element within current element with id = :title
20
+ class Cursor
21
+ attr_reader :view
22
+
23
+ protected
24
+
25
+ def initialize(el, v, state = nil) #:nodoc:
26
+ raise "Not allowed to call Cursor.new with nil element" unless @current_element = el
27
+ raise "Not allowed to call Cursor.new with empty array" if Array === el and el.empty?
28
+ raise "Not allowed to call Cursor.new with element object of type '#{el.class.name}'" unless [String,Array,Element].include?(el.class)
29
+ @current_element = el.clone if el.frozen?
30
+ @view, @state = v, state
31
+ end
32
+
33
+ public
34
+
35
+ # Creates new cursor with given element and optionally a view
36
+ # Some method need the view for the instance variables, the
37
+ # resources or the url_for method of the view.
38
+ def Cursor.create_element(el, view = nil)
39
+ Cursor.new el, view
40
+ end
41
+
42
+ # returns current element
43
+ def current_element
44
+ return @current_element unless array?
45
+ @current_element.empty? and raise "Empty array" or @current_element.first
46
+ end
47
+
48
+ # is current element a list of elements
49
+ def array?
50
+ @current_element.kind_of? Array
51
+ end
52
+
53
+ # is current element a single element
54
+ def element?
55
+ !@state or array?
56
+ end
57
+
58
+ # is current node a textnode
59
+ def text?
60
+ @state == :text
61
+ end
62
+
63
+ # is current node an attribute
64
+ def attribute?
65
+ @state.kind_of? Symbol
66
+ end
67
+
68
+ # Does the cursor point to the root of the document
69
+ def root?
70
+ !current_element.parent
71
+ end
72
+
73
+ # finds all input fields of type text, hidden, radio, checkbox, all textarea's and select fields.
74
+ # argument for fill_with is domain object name d
75
+ # domain object o is instance_variable_get "@#{d.to_s}"
76
+ # indexes these fields by name
77
+ # creates macro object with method_missing implemented
78
+ # instance_evals block on macro object
79
+ # for every call to method m in block:
80
+ # finds field in index
81
+ def fill_with!(d, &blk)
82
+ o = @view.instance_variable_get "@#{d.to_s}"
83
+ raise "No domain object with name '#{d}' found" unless o
84
+ index = create_name_index
85
+ mod = Module.new
86
+ cursor = self
87
+ mod.send :define_method, :method_missing do |m, *args|
88
+ f = index[m.to_sym]
89
+ raise "#{m} not found in form fields #{index.keys.inspect}" unless f
90
+ f = [f] unless Array === f
91
+ f.each {|x| raise 'nil? WTF' unless x; x.fill_field! d, o, m, *args }
92
+ end
93
+ macro = Object.new
94
+ macro.extend mod
95
+ macro.instance_eval &blk
96
+ end
97
+
98
+ # d = domain object name
99
+ # o = domain object
100
+ # p = property
101
+ # for all fields, set id to #{d}_#{m} and name to #{d}[#{m}]
102
+ # for hidden, text, set value to o.send m
103
+ # for textarea, set textnode to o.send m
104
+ # for checkbox, value="1", add hidden same name value="0", if o.send "#{m}?" set checked="checked"
105
+ # for radio, if o.send "#{m}" == value set checked="checked"
106
+ # for select, if no argument, use options in mdml - find selected option, set selected="selected", unselect rest
107
+ # for select, if argument, delete all options and generate them with given hash, and set selected.
108
+ def fill_field!(d, o, p, *args)
109
+ raise "Domain object #{d} doesn't respond to #{p}" unless o.respond_to? p
110
+ self[:id] <= "#{d}_#{p}"
111
+ self[:name] <= "#{d}[#{p}]"
112
+ current_element.name == 'textarea' and return self[] <= o.send(p)
113
+
114
+ return handle_select(o, p, args) if current_element.name == 'select'
115
+
116
+ type = self[:type].to_s or return
117
+ type = type.to_sym
118
+ [:hidden, :text].include?(type) and return self[:value] <= o.send(p)
119
+ if type == :radio
120
+ self[:checked] <= ((o.send(p).to_s == self[:value].to_s) and 'checked')
121
+ elsif type == :checkbox
122
+ handle_checkbox(d, o, p)
123
+ else
124
+ raise "Unknown type #{type}"
125
+ end
126
+ end
127
+
128
+ def handle_checkbox(d, o, p)
129
+ self[:value] <= '1'
130
+ self[:checked] <= (o.send("#{p}?") and 'checked')
131
+ hidden = Element.new 'input'
132
+ hidden.change_attributes do |att|
133
+ att[:type] = 'hidden'
134
+ att[:name] = "#{d}[#{p}]"
135
+ att[:value] = '0'
136
+ end
137
+ self.current_element.insert_after_self hidden
138
+ end
139
+
140
+ def handle_select(o, p, args)
141
+ puts args.inspect
142
+ mk_node = lambda do |key, label|
143
+ node = Element.new 'option'
144
+ node.change_attributes {|at| at[:value] = key.to_s }
145
+ node.insert_child_last label.to_s
146
+ node
147
+ end
148
+ unless args.empty?
149
+ current_element.delete_all_children
150
+ if args.size > 1
151
+ args[0].each do |e|
152
+ current_element.insert_child_last mk_node[e.send(args[1]), e.send(args[2])]
153
+ end
154
+ else
155
+ args[0].each do |e|
156
+ current_element.insert_child_last mk_node[e.first, e.last]
157
+ end
158
+ end
159
+ end
160
+ # set selected
161
+ selected_key = o.send(p).to_s
162
+ puts "selected_key: #{selected_key.inspect}"
163
+ current_element.children.each do |c|
164
+ if c.attributes[:value] == selected_key
165
+ c.change_attributes {|at| at[:selected] = 'selected' }
166
+ end
167
+ end
168
+ end
169
+
170
+ def create_name_index
171
+ index = {}
172
+ current_element.each_descendant_element do |el|
173
+ puts "processing element #{el.name}"
174
+ type = el.attributes[:type]
175
+ if %w(select textarea).include?(el.name) or el.name == 'input' && %w(hidden text radio checkbox).include?(type)
176
+ name = el.attributes[:name].to_sym
177
+ raise "name attribute missing in element #{el.to_s}" unless name
178
+ if type == 'radio'
179
+ (index[name] ||= []) << clone!(el)
180
+ else
181
+ raise "Two elements with name #{name}" if index[name]
182
+ index[name] = clone!(el)
183
+ end
184
+ end
185
+ end
186
+ index
187
+ end
188
+
189
+ # cursor[] references text node of current element
190
+ # cursor[2] references 3rd element of current element list
191
+ # cursor[:onclick] references attribute onclick of current element
192
+ # cursor['#onclick'] references child with id 'onclick' (identical to cursor.id![:onclick])
193
+ def [](index = nil)
194
+ raise "hey, this is not an element" if @state
195
+ if !index
196
+ clone! @current_element, :text
197
+ elsif index.kind_of?(String)
198
+ raise "You called ['#{index}'] on this cursor. Did you mean ['##{index}'] or [:#{index}]?" unless index[0] == ?# or index.size > 1
199
+ id![index[1..-1]]
200
+ elsif index.kind_of? Numeric
201
+ raise "hey, this is not an array" unless array?
202
+ raise unless @current_element = @current_element[index]
203
+ self
204
+ elsif index.kind_of? Array
205
+ raise "no array allowed here"
206
+ else
207
+ clone! @current_element, index.to_sym
208
+ end
209
+ end
210
+
211
+ # insert textnode before current element
212
+ # e.g. cursor << 'Homepage'
213
+ def <<(text)
214
+ raise "hey, this is not an element" if @state
215
+ raise "not implemented"
216
+ end
217
+
218
+ # insert textnode after current element
219
+ # e.g. cursor >> 'Homepage'
220
+ def >>(text)
221
+ raise "hey, this is not an element" if @state
222
+ raise "not implemented"
223
+ end
224
+
225
+ # if cursor references a textnode, appends string to textnode
226
+ # e.g. cursor[] < 'world'
227
+ # if cursor references an attribute, appends string to attribute
228
+ # e.g. cursor[:href] < '?id=foo'
229
+ def <(text)
230
+ raise "hey, this is an element" unless @state
231
+ if text?
232
+ current_element.insert_child_first(current_element.delete_child(0) + text.to_s)
233
+ else
234
+ current_element.change_attributes {|attr| attr[@state] += text.to_s}
235
+ end
236
+ end
237
+
238
+ # if cursor references text, replaces textnode with new ESCAPED textnode.
239
+ # e.g. cursor[] =~ '<div>&nbsp;</div>' gives: '&lt;div&gt;&amp;nbsp;&lt;/div&gt;'
240
+ def =~(text)
241
+ raise "The =~ works only on text elements" unless text?
242
+ raise "You can't set text on an element unless it is childless or has one child that is a textnode" if current_element.children[0].kind_of? Element
243
+ current_element.delete_child(0) unless current_element.children.empty?
244
+ current_element.insert_child_first(ERB::Util.h(text.to_s))
245
+ end
246
+
247
+ # if cursor references element, replaces element (+children) with textnode
248
+ # cursor <= 'Next page'
249
+ # if cursor references text, replaces textnode with new textnode.
250
+ # e.g. cursor[] <= 'Updated!'
251
+ # if cursor references attribute, replaces attribute value with new value
252
+ # e.g. cursor[:class] <= 'hidden'
253
+ # deletes the attributes when called with nil or false
254
+ def <=(text)
255
+ if !@state
256
+ current_element.insert_after_self text.to_s
257
+ current_element.delete_self
258
+ elsif text?
259
+ raise "You can't set text on an element with element children" if current_element.children[0].kind_of? Element
260
+ current_element.delete_child 0
261
+ current_element.insert_child_first text.to_s
262
+ else
263
+ current_element.change_attributes do |attr|
264
+ text ? attr[@state] = text.to_s : attr.delete(@state)
265
+ end
266
+ end
267
+ end
268
+
269
+ # Calculates new value by calling 'call' on param cmd with current value.
270
+ # if cursor references text, replaces textnode with new value.
271
+ # e.g. cursor[] <=> lambda &:upcase
272
+ # if cursor references attribute, replaces attribute value with new value
273
+ # e.g. cursor[:class] <=> lambda {|x| x.reverse }
274
+ # (Note that operators do not take blocks)
275
+ def <=>(cmd)
276
+ raise "hey, this is an element" unless @state
277
+ self <= cmd.call(to_s).to_s
278
+ end
279
+
280
+ # renders what cursors refers to as a string
281
+ def to_s
282
+ if text?
283
+ return '' if !current_element.children or current_element.children.empty?
284
+ return current_element.children[0] if current_element.children.size == 1
285
+ raise "More than one node where single text node expected."
286
+ end
287
+ return current_element.attributes[@state] if attribute?
288
+ current_element.to_s
289
+ end
290
+
291
+ def method_missing(meth, *args, &block) #:nodoc:
292
+ return super if meth.to_s =~ /^should/
293
+ raise "hey, this is not an element" if @state
294
+ el = current_element.elements.select{|c| c.name == meth.to_s }
295
+ raise "#{current_element.name} has no #{meth} elements" if el.empty?
296
+ clone! el
297
+ end
298
+
299
+ # returns new cursor pointing to element with current id
300
+ # searches only in descendants of current element
301
+ def id!
302
+ lambda do |n|
303
+ el = find_by_id!(n.to_sym)
304
+ raise "Missing element with id #{n} in children of element #{current_element.name}\n#{current_element.to_s}" unless el
305
+ clone! el
306
+ end
307
+ end
308
+
309
+ # Calls each proc in list on each element in current nodelist in
310
+ # the equivalent position
311
+ # e.g. list[0].call node
312
+ # +proclist+ A list of procs
313
+ def /(proclist)
314
+ raise "Current element moet array zijn" unless array?
315
+ proclist.zip(@current_element).each do|tuple|
316
+ raise "@current_element[x] is nil, #{@current_element}" unless tuple.last
317
+ tuple.first.call clone!(tuple.last)
318
+ end
319
+ end
320
+
321
+ # replaces current single element by resource with given id
322
+ # for each pair k,v in replacements param, replaces element
323
+ # with id == k with resource with id v.
324
+ def replace!(id, replacements = {})
325
+ raise "hey, this is not a single element" unless element?
326
+ el = view.class.resource_by_id id
327
+ c = el.clone @current_element.parent
328
+ if root?
329
+ @current_element = c
330
+ else
331
+ @current_element.insert_after_self c
332
+ @current_element.delete_self
333
+ end
334
+ replacements.each {|k,v| self.id![k].replace! v }
335
+ c
336
+ end
337
+
338
+ # Replaces current element by list of elements
339
+ def swap!(list)
340
+ raise "trying to swap nil" unless list
341
+ raise "you cannot call swap on a text node" if text?
342
+ raise "you cannot call swap on an attribute" if attribute?
343
+ raise "you cannot call swap on root; use replace" if root?
344
+ list.each {|i| current_element.insert_before_self i}
345
+ current_element.delete_self
346
+ @current_element = list.empty? ? @current_element.parent : list[0]
347
+ end
348
+
349
+ def unwrap!
350
+ parent = current_element.parent
351
+ current_element.children.each do |child|
352
+ parent.insert_child_after_node current_element, child
353
+ end
354
+ parent.delete_child current_element
355
+ current_element = parent
356
+ end
357
+
358
+ # el ^ options is an alias for el['href'] <= url_for(options)
359
+ # el ^ [condition, options] does the same, unless the condition
360
+ # is false, in which case it will remove the anchor tag.
361
+ def ^(options)
362
+ if options.kind_of? Array
363
+ return unwrap! unless options[0]
364
+ options = options[1]
365
+ end
366
+ if current_element.name == 'a'
367
+ self[:href] <= view.url_for(options)
368
+ elsif current_element.name == 'form'
369
+ self[:action] <= view.url_for(options)
370
+ else
371
+ raise "Don't know how to set url on element [#{current_element.name}]"
372
+ end
373
+ end
374
+
375
+ # hash id => options
376
+ # for each entry, calls id![id] ^ options
377
+ def set_urls!(hash)
378
+ hash.each {|myid,options| self.id![myid] ^ options}
379
+ end
380
+
381
+ # call block on each cursor with persia:class == name
382
+ def each_cursor_with_class!(name)
383
+ current_element.each_descendant_element do |el|
384
+ yield clone!(el) if el.has_class?(name)
385
+ end
386
+ end
387
+
388
+ # Makes a clone of the element for each list item and calls
389
+ # block on each clone.
390
+ def times!(list)
391
+ list.map do |obj|
392
+ e = current_element.clone
393
+ yield clone!(e), obj
394
+ e
395
+ end
396
+ end
397
+
398
+ # Makes a clone of the element for each list item and calls
399
+ # block on each clone. Replaces the current elements with the list.
400
+ def times_and_swap!(list, &cmd)
401
+ raise "trying to times_and_swap nil" unless list
402
+ raise "you cannot call times_and_swap on a text node" if text?
403
+ raise "you cannot call times_and_swap on an attribute" if attribute?
404
+ raise "you cannot call times_and_swap on root; use replace" if root?
405
+ swap! times!(list, &cmd)
406
+ end
407
+
408
+ private
409
+
410
+ def find_by_id!(n)
411
+ if array?
412
+ @current_element.detect {|x| x.detect {|y| y.has_id?(n) } }
413
+ else
414
+ @current_element.detect {|x| x.has_id?(n) }
415
+ end
416
+ end
417
+
418
+ def clone!(d, state = nil)
419
+ Cursor.new(d, view, state)
420
+ end
421
+ end
422
+
423
+ class Macro #:nodoc:
424
+ def initialize
425
+ @cmds = []
426
+ end
427
+
428
+ def method_missing(meth, *args, &block)
429
+ @cmds << [meth, args, block]
430
+ self
431
+ end
432
+
433
+ def call(target)
434
+ @cmds.each do |cmd|
435
+ target = target.send(cmd[0], *cmd[1], &cmd[2])
436
+ end
437
+ end
438
+ end
439
+ end
@@ -0,0 +1,358 @@
1
+ module Persia
2
+ XMLNS = 'http://persia.rubyforge.org/specs'
3
+ class Element
4
+ SLOW_RENDER = false
5
+
6
+ attr_accessor :source, :name
7
+ attr_reader :children, :outer, :inner, :parent, :persia_id, :persia_class
8
+
9
+ # create new element, creating from rexml document if document, or
10
+ # with name if string.
11
+ def initialize(r = nil, id_hash = nil)
12
+ @children = []
13
+ if r and r.kind_of?(String)
14
+ @name = r.to_s
15
+ elsif r and r.kind_of?(REXML::Document)
16
+ populate_document(r, id_hash)
17
+ end
18
+ end
19
+
20
+ def populate_document(r, id_hash)
21
+ populate_from_rexml(r.root, nil, id_hash)
22
+ # cache roots
23
+ if id_hash
24
+ id_hash.values.select(&:root?).each(&:cache)
25
+ id_hash.freeze
26
+ end
27
+ each_descendant_element(&:freeze)
28
+ end
29
+
30
+ def freeze
31
+ @attributes.freeze
32
+ super
33
+ end
34
+
35
+ # populate from rexml element with given parent
36
+ def populate_from_rexml(e, parent = nil, id_hash = nil)
37
+ @name = e.name
38
+ @parent = parent
39
+ # skip children if element contains skip attribute
40
+ if load_attr(e.attributes, id_hash)
41
+ e.children.each do |c|
42
+ @children << Element.new.populate_from_rexml(c, self, id_hash) if c.kind_of? REXML::Element
43
+ if c.kind_of?(REXML::Text) && (s = c.to_s)
44
+ s.strip!
45
+ @children << s unless s.empty?
46
+ end
47
+ end
48
+ end
49
+
50
+ self
51
+ end
52
+
53
+ # populate from native element with given parent
54
+ def populate_from_element(el, parent)
55
+ raise TypeError, "Element is frozen" if frozen?
56
+ @name = el.name
57
+ @parent = parent
58
+ @persia_id = el.persia_id
59
+ @persia_class = el.persia_class
60
+ load_attr(el.attributes)
61
+ @children = el.copy_children(self)
62
+ self
63
+ end
64
+
65
+ # return copy of children
66
+ def copy_children(parent)
67
+ @children.map do |c|
68
+ c.kind_of?(String) ? c.dup : Element.new.populate_from_element(c, parent)
69
+ end if @children
70
+ end
71
+
72
+ # returns first element for which block returns true, going depth first
73
+ def detect(&cmd)
74
+ if cmd.call self
75
+ return self
76
+ end
77
+ elements.each {|x| r = x.detect(&cmd) and return r }
78
+ nil
79
+ end
80
+
81
+ # calls each descendant element (no textnodes) including current one
82
+ def each_descendant_element(&cmd)
83
+ cmd.call self
84
+ @children.each do |c|
85
+ if c.kind_of? Element
86
+ c.each_descendant_element &cmd
87
+ end
88
+ end
89
+ end
90
+
91
+ # has this element a persia id of name?
92
+ def has_id?(name)
93
+ name.to_sym == @persia_id
94
+ end
95
+
96
+ # has this element a persia class of name?
97
+ def has_class?(name)
98
+ name.to_sym == @persia_class
99
+ end
100
+
101
+ # returns attribute hash
102
+ def attributes
103
+ @attributes || {}
104
+ end
105
+
106
+ # returns root element
107
+ def root
108
+ @parent ? @parent.root : self
109
+ end
110
+
111
+ # clones current element, giving it parent as parent
112
+ def clone(parent = nil)
113
+ Element.populate_from_element self, parent
114
+ end
115
+
116
+ # returns all children that are elements
117
+ def elements
118
+ @children.select {|c| c.kind_of?(Element) }
119
+ end
120
+
121
+ # raise Exception unless current element is equal to obj
122
+ def assert_equal(obj)
123
+ unless obj.kind_of? Element
124
+ raise "element (#{name}) != #{obj.to_s}[#{obj.class}]"
125
+ end
126
+ obj.assert_attr_eql? @attributes
127
+ unless name == obj.name
128
+ raise "element (#{name}) != element(#{obj.name})"
129
+ end
130
+ @children.each_with_index do |e,i|
131
+ e2 = obj.children[i]
132
+ if e.kind_of?(Element)
133
+ e.assert_equal e2
134
+ elsif e != e2
135
+ raise "'#{e}'[#{e.class}] and '#{e2}'[#{e2.class}] not equal"
136
+ end
137
+ end
138
+ end
139
+
140
+ # raise Exception unless attributes of element are equal to attrib param
141
+ def assert_attr_eql?(attrib)
142
+ unless @attributes == attrib
143
+ raise "Attributes #{attrib.inspect} and #{@attributes.inspect} not equal\nself:#{self.to_s}"
144
+ end
145
+ end
146
+
147
+ # METHODS: modify document
148
+
149
+ # insert node before self (as sibling)
150
+ def insert_before_self(node)
151
+ parent.insert_child index_self, node
152
+ end
153
+
154
+ # insert node after self (as sibling)
155
+ def insert_after_self(node)
156
+ parent.insert_child index_self + 1, node
157
+ end
158
+
159
+ # insert node as first child
160
+ def insert_child_first(node)
161
+ insert_child 0, node
162
+ end
163
+
164
+ # insert node as last child
165
+ def insert_child_last(node)
166
+ insert_child -1, node
167
+ end
168
+
169
+ def insert_child_before_node(node, newnode)
170
+ insert_child index(node), newnode
171
+ end
172
+
173
+ def insert_child_after_node(node, newnode)
174
+ insert_child 1 + index(node), newnode
175
+ end
176
+
177
+ def insert_child(index, node)
178
+ raise TypeError, "Element is frozen" if frozen?
179
+ clear_inner
180
+ (@children ||= []).insert index, node
181
+ attempt_concat(index - 1)
182
+ attempt_concat(index)
183
+ end
184
+
185
+ def delete_self
186
+ raise TypeError, "Element is frozen" if frozen?
187
+ @parent.delete_child(self) if @parent
188
+ end
189
+
190
+ def delete_child(index)
191
+ raise TypeError, "Element is frozen" if frozen?
192
+ clear_inner
193
+ index = index(index) unless Numeric === index
194
+ @children.delete_at index
195
+ attempt_concat(index - 1)
196
+ end
197
+
198
+ def attempt_concat(first)
199
+ second = first + 1
200
+ return if second == 0
201
+ return unless second < @children.size
202
+ if String === @children[first] and String === @children[second]
203
+ @children[first] << @children[second]
204
+ @children.delete_at second
205
+ end
206
+ end
207
+
208
+ def change_attributes
209
+ raise TypeError, "Element is frozen" if frozen?
210
+ @attributes ||= {}
211
+ yield @attributes
212
+ clear_outer
213
+ end
214
+
215
+ def change_type(newtype)
216
+ raise TypeError, "Element is frozen" if frozen?
217
+ @name = newtype
218
+ clear_outer
219
+ end
220
+
221
+ def index(node)
222
+ index = -1
223
+ @children.detect {|c| index += 1; node.equal? c }
224
+ index
225
+ end
226
+
227
+ def delete_all_children
228
+ @children = []
229
+ end
230
+
231
+ def index_self
232
+ @parent.index self if @parent
233
+ end
234
+
235
+ # end modify
236
+
237
+ def render(array)
238
+ if SLOW_RENDER or not @inner
239
+ render_tags(array) { render_children(array) }
240
+ elsif @outer
241
+ array << root.source[@outer] if @outer
242
+ else
243
+ render_tags(array) { array << root.source[@inner] }
244
+ end
245
+ end
246
+
247
+ def render_tags(array)
248
+ array << el_open
249
+ yield
250
+ array << el_close
251
+ end
252
+
253
+ def render_children(array)
254
+ @children.each do |c|
255
+ c.kind_of?(String) ? array << c : c.render(array)
256
+ end
257
+ end
258
+
259
+ def clear_inner
260
+ @inner = nil
261
+ @parent.clear_inner if @parent
262
+ end
263
+
264
+ def clear_outer
265
+ @outer = nil
266
+ @parent.clear_inner if @parent
267
+ end
268
+
269
+ def root?
270
+ !@parent
271
+ end
272
+
273
+ def cache
274
+ array = []
275
+ dump array
276
+ cursor = 0
277
+ array.each do |fr|
278
+ if fr.kind_of? Array
279
+ fr[1].touch(cursor, fr[0].size)
280
+ cursor += fr[0].size
281
+ else
282
+ cursor += fr.size
283
+ end
284
+ end
285
+ root.source = array.map {|x,*y| x }.join
286
+ end
287
+
288
+ def to_s
289
+ render([]).join "\n"
290
+ end
291
+
292
+ def dump(array)
293
+ array << [el_open, self]
294
+ @children.each do |c|
295
+ if c.kind_of? String
296
+ array << c
297
+ else
298
+ c.dump array
299
+ end
300
+ end
301
+ array << [el_close, self]
302
+ end
303
+
304
+ def touch(first, size)
305
+ if @first
306
+ @outer = @first ... first + size
307
+ @inner = @last ... first
308
+ @first,@last = nil, nil
309
+ else
310
+ @first,@last = first, first + size
311
+ end
312
+ end
313
+
314
+ def el_open
315
+ "<#{@name}#{attributes.map{|k,v|%Q! #{k}="#{v}"!}.join}>"
316
+ end
317
+
318
+ def el_close
319
+ "</#{@name}>"
320
+ end
321
+
322
+ private
323
+
324
+ def self.populate_from_element(el, parent)
325
+ (e = Element.new).populate_from_element el, parent
326
+ end
327
+
328
+ # returns nil if persia:skip attribute found. this means - ignore children
329
+ def load_attr(attrib, id_hash = nil)
330
+ raise TypeError, "Element is frozen" if frozen?
331
+ @attributes = {}
332
+ if attrib
333
+ attrib.each {|k,v| @attributes[k.to_sym] = v }
334
+ end
335
+ return true unless REXML::Attributes === attrib
336
+ l = lambda do |el, cmd|
337
+ if a = attrib.get_attribute_ns(XMLNS, el)
338
+ puts "Found marker #{a.inspect}"
339
+ @attributes.delete a.fully_expanded_name.to_sym
340
+ v = a.value.to_sym
341
+ cmd[v]
342
+ end
343
+ true
344
+ end
345
+ l['id', lambda do |v|
346
+ id_hash[v] = self if id_hash
347
+ @persia_id = v
348
+ end]
349
+ l['class', lambda do |v|
350
+ @persia_class = v
351
+ end]
352
+ l['skip', lambda do |v|
353
+ id_hash[v] = self if id_hash
354
+ @persia_id = v
355
+ end]
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,217 @@
1
+ @@lib_path = File.join(File.dirname(__FILE__), "..", "lib")
2
+ $:.unshift @@lib_path
3
+
4
+ require 'rexml/document'
5
+ require 'persia'
6
+ require 'test/unit'
7
+
8
+ def mockit(name, &blk)
9
+ o = Object.new
10
+ o.class.instance_variable_set('@blk', blk)
11
+ eval <<-"end_of_def"
12
+ class << o.class
13
+ def #{name}(a)
14
+ @blk.call a
15
+ end
16
+ end
17
+ end_of_def
18
+ o
19
+ end
20
+
21
+ class Symbol
22
+ def to_proc; Proc.new {|obj,*args| obj.send(self, *args) }; end
23
+ end
24
+
25
+ class PersiaTest < Test::Unit::TestCase
26
+ include REXML
27
+ include Persia
28
+
29
+ def test_cursor_set_text
30
+ eval_xml '<div>1</div>', '<div>2</div>' do |cursor|
31
+ cursor[] <= '2'
32
+ end
33
+ end
34
+
35
+ def test_cursor_set_text_alt_notation
36
+ eval_xml '<div>1</div>', '<div>2</div>' do |cursor|
37
+ cursor[] <= '2'
38
+ end
39
+ end
40
+
41
+ def test_cursor_append_text
42
+ eval_xml '<div>1</div>', '<div>12</div>' do |cursor|
43
+ cursor[] < '2'
44
+ end
45
+ end
46
+
47
+ def test_cursor_change_text
48
+ eval_xml '<div>3</div>', '<div>27</div>' do |cursor|
49
+ cursor[] <=> lambda {|x| x.to_i ** 3}
50
+ end
51
+ end
52
+
53
+ def test_cursor_set_attribute
54
+ eval_xml '<div id="1"/>', '<div id="2"/>' do |cursor|
55
+ cursor[:id] <= '2'
56
+ end
57
+ end
58
+
59
+ def test_cursor_append_attribute
60
+ eval_xml '<div id="1"/>', '<div id="12"/>' do |cursor|
61
+ cursor[:id] < '2'
62
+ end
63
+ end
64
+
65
+ def test_cursor_change_attribute
66
+ eval_xml '<div id="3"/>', '<div id="27"/>' do |cursor|
67
+ cursor[:id] <=> lambda {|x| x.to_i ** 3}
68
+ end
69
+ end
70
+
71
+ def test_hole
72
+ i1,i2 = {}, {}
73
+ e1 = to_element '<a persia:id="document"><b><c persia:skip="hole">LAYOUT.MDML BABY</c></b></a>', i1, {}
74
+ e2 = to_element '<X><Y persia:id="head"><Z persia:id="title">LAYOUT.MDML BABY</Z></Y></X>', i2, {}
75
+ id_hash = i1.merge i2
76
+ html = Element.new
77
+ html.name = 'html'
78
+ view = mockit('resource_by_id') {|x| id_hash[x.to_sym] }
79
+ cursor = Cursor.create_element(html, view)
80
+ cursor.replace! 'document'
81
+ cursor.id![:hole].replace! 'head'
82
+ cursor.id![:title]
83
+ end
84
+
85
+ def test_cursor
86
+ cursor = load_doc "set_text"
87
+ body = cursor.id!['body']
88
+ assert_equal "body", body.doc.name
89
+ assert_equal "div", body.div.doc.name
90
+ table = body.div.table
91
+ assert_equal "table", table.doc.name
92
+ assert_equal "tr", table.tr.doc.name
93
+ assert_equal Cursor, table.tr[2].class
94
+ assert table.tr
95
+ assert table.tr.array?
96
+ assert_equal "even", table.tr[2]['class'].to_s
97
+ tr = table.tr[2]
98
+ assert_equal "Ruby Wizard", tr.td[1][].to_s
99
+ end
100
+
101
+ def test_cursor2
102
+ org = '<ul persia:id="ps"><li persia:id="lilo">bla</li></ul>'
103
+ expected = '<ul><li>bar</li></ul>'
104
+ eval_xml org, expected do |cursor|
105
+ assert cursor.doc.has_id?(:ps)
106
+ cursor.id!['lilo'][] <= 'bar'
107
+ end
108
+ end
109
+
110
+ def test_find_id
111
+ org = '<html xmlns:persia="http://persia.finalist.com/" ><body><p style="color: blue" persia:id="flash">hello</p></body></html>'
112
+ eval_xml org, org do |cursor|
113
+ assert !cursor.doc.frozen?
114
+ assert_equal :flash, cursor.body.p[0].doc.persia_id
115
+ p = cursor.doc.detect {|x| x.has_id?(:flash) }
116
+ assert_equal 'p', p.name
117
+ cursor.body.id![:flash]
118
+ assert_equal 'p', cursor.id!['flash'].doc.name
119
+ end
120
+ end
121
+
122
+ def test_multiply
123
+ xml_equal? "multiply" do |cursor|
124
+ assert !cursor.doc.frozen?
125
+ cursor.id!['flash'].times_and_swap!(1..3) { |e, x| e[] <= x }
126
+ end
127
+ end
128
+
129
+ def test_times
130
+ org = '<div><p persia:id="ps">bla</p></div>'
131
+ expected = '<div><p>bla</p></div>'
132
+ eval_xml org, expected do |cursor|
133
+ cursor.id!['ps'].times!(1..3) { |e,i| e[] <= i }
134
+ end
135
+ end
136
+
137
+ def test_times_and_swap
138
+ org = '<div><p persia:id="ps">bla</p></div>'
139
+ expected = '<div><p>1</p><p>2</p><p>3</p></div>'
140
+ eval_xml org, expected do |cursor|
141
+ cursor.id!['ps'].times_and_swap!(1..3) { |e,i| e[] <= i }
142
+ end
143
+ end
144
+
145
+ def test_replace
146
+ org = '<div><p persia:id="ps">bla</p></div>'
147
+ expected = '<p>bla</p>'
148
+ eval_xml org, expected do |cursor|
149
+ assert cursor.p.doc.has_id?(:ps)
150
+ cursor.replace! 'ps'
151
+ end
152
+ end
153
+
154
+ def test_slice
155
+ org = '<ul persia:id="ps"><li class="active">lisp</li><li class="active">2</li><li class="active">3</li></ul>'
156
+ expected = '<ul><li class="active">pascal</li><li class="hidden">2</li><li id="ruby" class="active">3</li></ul>'
157
+ eval_xml org, expected do |cursor|
158
+ cursor.id!['ps'].li / [
159
+ lambda {|e| e[] <= 'pascal'},
160
+ lambda {|e| e['class'] <= 'hidden'},
161
+ lambda {|e| e['id'] <= 'ruby'}
162
+ ]
163
+ end
164
+ end
165
+
166
+ def test_slice2
167
+ org = '<ul persia:id="ps"><li class="active">lisp</li><li class="active">2</li><li class="active">3</li></ul>'
168
+ expected = '<ul><li class="active">pascal</li><li class="hidden">2</li><li id="ruby" class="active">3</li></ul>'
169
+ eval_xml org, expected do |cursor|
170
+ cursor.id!['ps'].li / [
171
+ macro[] <= 'pascal',
172
+ macro[:class] <= 'hidden',
173
+ macro[:id] <= 'ruby'
174
+ ]
175
+ end
176
+ end
177
+
178
+ def test_href
179
+ eval_xml '<ul persia:id="ps"><li><a href="">bla</a></li></ul>', '<ul><li>bla</li></ul>' do |cursor|
180
+ cursor.id!['ps'].li.a ^ [false, {:action => 'list'}]
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def load_doc(fname)
187
+ doc = Document.new(File.new("test/data/#{fname}.mdml"))
188
+ el = Element.new(doc, id_hash = {})
189
+ Cursor.create_element(el)
190
+ end
191
+
192
+ def xml_equal?(fname)
193
+ expected = File.new("test/data/#{fname}.xml")
194
+ cursor = load_doc(fname)
195
+ yield cursor
196
+ assert_xml_equal(expected, cursor.doc)
197
+ end
198
+
199
+ def eval_xml(orig, expected)
200
+ el = Element.new(Document.new(orig), id_hash = {}, {})
201
+ view = mockit('resource_by_id') {|x| id_hash[x.to_sym] }
202
+ yield cursor = Cursor.create_element(el, view)
203
+ assert_xml_equal(expected, cursor.doc)
204
+ end
205
+
206
+ def to_element(obj, i = nil, c = nil)
207
+ obj = REXML::Document.new(obj) if obj.kind_of? String
208
+ obj = REXML::Document.new(obj) if obj.kind_of? File
209
+ obj = Persia::Element.new(obj, i, c) if obj.kind_of? REXML::Document
210
+ obj
211
+ end
212
+
213
+ def assert_xml_equal(expected, doc)
214
+ a,b = to_element(expected), to_element(doc)
215
+ a.assert_equal b
216
+ end
217
+ end
@@ -0,0 +1,69 @@
1
+ @@lib_path = File.join(File.dirname(__FILE__), "..", "lib")
2
+ $:.unshift @@lib_path
3
+
4
+ require 'rexml/document'
5
+ require 'persia'
6
+ require 'test/unit'
7
+
8
+ class XmlTest < Test::Unit::TestCase
9
+ include REXML
10
+ include Persia
11
+
12
+ def test_insert_before_self
13
+ eval_xml '<div><b>1</b></div>', '<div>X<b>1</b></div>' do |e|
14
+ e.elements[0].insert_before_self "X"
15
+ end
16
+ end
17
+
18
+ def test_insert_after_self
19
+ eval_xml '<div><b>1</b></div>', '<div><b>1</b>X</div>' do |e|
20
+ e.elements[0].insert_after_self "X"
21
+ end
22
+ end
23
+
24
+ def test_insert_child_first
25
+ eval_xml '<div><b>1</b></div>', '<div>X<b>1</b></div>' do |e|
26
+ e.insert_child_first "X"
27
+ end
28
+ end
29
+
30
+ def test_insert_child_last
31
+ eval_xml '<div><b>1</b></div>', '<div><b>1</b>X</div>' do |e|
32
+ e.insert_child_last "X"
33
+ end
34
+ end
35
+
36
+ def test_id_hash
37
+ i,c = {}, {}
38
+ e = to_element '<div><p persia:id="ps">bla</p></div>', i, c
39
+ assert i.include?(:ps), e.inspect
40
+ end
41
+
42
+ def test_skip
43
+ i,c = {}, {}
44
+ e = to_element '<div persia:skip="foo"><p persia:id="invisible">bla</p></div>', i, c
45
+ assert i.keys.include?(:foo)
46
+ assert !i.keys.include?(:invisible)
47
+ end
48
+
49
+ private
50
+
51
+ def eval_xml(orig, expected)
52
+ doc = REXML::Document.new(orig)
53
+ el = Persia::Element.new(doc).clone
54
+ yield el
55
+ assert_xml_equal(expected, el)
56
+ end
57
+
58
+ def to_element(obj, i = nil, c = nil)
59
+ obj = REXML::Document.new(obj) if obj.kind_of? String
60
+ obj = REXML::Document.new(obj) if obj.kind_of? File
61
+ obj = Persia::Element.new(obj, i, c) if obj.kind_of? REXML::Document
62
+ obj
63
+ end
64
+
65
+ def assert_xml_equal(expected, doc)
66
+ a,b = to_element(expected), to_element(doc)
67
+ a.assert_equal b
68
+ end
69
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: persia
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2006-09-15 00:00:00 +02:00
8
+ summary: Alternative view layer for Rails based on separation of HTML and logic.
9
+ require_paths:
10
+ - lib
11
+ email: michiel@finalist.com
12
+ homepage: http://persia.rubyforge.org/
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: base
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Michiel de Mare
30
+ files:
31
+ - lib/base.rb
32
+ - lib/persia.rb
33
+ - lib/xml.rb
34
+ test_files:
35
+ - test/persia_test.rb
36
+ - test/xml_test.rb
37
+ rdoc_options: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ executables: []
42
+
43
+ extensions: []
44
+
45
+ requirements: []
46
+
47
+ dependencies: []
48
+