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.
- data/lib/base.rb +175 -0
- data/lib/persia.rb +439 -0
- data/lib/xml.rb +358 -0
- data/test/persia_test.rb +217 -0
- data/test/xml_test.rb +69 -0
- metadata +48 -0
data/lib/base.rb
ADDED
@@ -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
|
data/lib/persia.rb
ADDED
@@ -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> </div>' gives: '<div>&nbsp;</div>'
|
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
|
data/lib/xml.rb
ADDED
@@ -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
|
data/test/persia_test.rb
ADDED
@@ -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
|
data/test/xml_test.rb
ADDED
@@ -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
|
+
|