rind 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.rdoc +111 -0
- data/lib/rind.rb +15 -0
- data/lib/rind/document.rb +81 -0
- data/lib/rind/equality.rb +9 -0
- data/lib/rind/html.rb +41 -0
- data/lib/rind/manipulate.rb +31 -0
- data/lib/rind/nodes.rb +234 -0
- data/lib/rind/parser.rb +141 -0
- data/lib/rind/traverse.rb +86 -0
- data/lib/rind/xml.rb +28 -0
- data/lib/rind/xpath.rb +144 -0
- data/test/all_test.rb +14 -0
- data/test/cdata_test.rb +9 -0
- data/test/children_test.rb +50 -0
- data/test/comment_test.rb +9 -0
- data/test/document_test.rb +23 -0
- data/test/element_test.rb +49 -0
- data/test/equality_test.rb +19 -0
- data/test/files/document_test.html +8 -0
- data/test/files/traverse_test.html +13 -0
- data/test/html_test.rb +16 -0
- data/test/manipulate_test.rb +23 -0
- data/test/nodes_test.rb +16 -0
- data/test/parser_test.rb +7 -0
- data/test/traverse_test.rb +57 -0
- data/test/xml_test.rb +14 -0
- data/test/xpath_test.rb +27 -0
- metadata +109 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2010 Aaron Lasseigne
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
= Rind
|
2
|
+
|
3
|
+
Rind is a templating engine that turns HTML (and XML) into node trees
|
4
|
+
and allows you to create custom tags or reuse someone else's genius.
|
5
|
+
Rind gives web devs tags to work with and provides the same thing to
|
6
|
+
app devs as an object. This project is just getting started so watch
|
7
|
+
out for sharp corners and unfinished rooms. Enough of that, let's talk
|
8
|
+
about what's done.
|
9
|
+
|
10
|
+
== Come say "Hello".
|
11
|
+
|
12
|
+
# example.rb
|
13
|
+
require 'rind'
|
14
|
+
|
15
|
+
# Create a new document and load your template.
|
16
|
+
doc = Rind::Document.new('index.html')
|
17
|
+
|
18
|
+
# Xpath search for the the title and add some text.
|
19
|
+
doc.sf('/html/head/title').children.push('Hello World')
|
20
|
+
|
21
|
+
# Send it out the door.
|
22
|
+
puts doc.render!
|
23
|
+
|
24
|
+
== Create your own!
|
25
|
+
|
26
|
+
One of the great things about Rind is that you can create your own HTML
|
27
|
+
elements and bundle them in modules. Imagine making a module that performs
|
28
|
+
a variety of useful functions on images. For example, it could provide
|
29
|
+
a gallery view that automatically generates thumbnails and paginates. Clicked
|
30
|
+
pics could provide full sized versions of themselves in a lightbox.
|
31
|
+
|
32
|
+
Create your custom module.
|
33
|
+
|
34
|
+
# images.rb
|
35
|
+
require 'rind'
|
36
|
+
|
37
|
+
module Images
|
38
|
+
class Gallery < Rind::Element
|
39
|
+
attr_accessor :path_to_images
|
40
|
+
|
41
|
+
# gallery magic here
|
42
|
+
...
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
App devs treat it like an object.
|
47
|
+
|
48
|
+
# index.cgi
|
49
|
+
require 'rind'
|
50
|
+
require 'images'
|
51
|
+
|
52
|
+
doc = Rind::Document.new('index.html')
|
53
|
+
doc.sf('/html/body/images:gallery').path_to_images = '/home/me/photos'
|
54
|
+
puts doc.render!
|
55
|
+
|
56
|
+
Web devs treat it like a tag.
|
57
|
+
|
58
|
+
# index.html
|
59
|
+
<html>
|
60
|
+
<body>
|
61
|
+
<images:gallery width="400px" per_row="5" max_rows="3"/>
|
62
|
+
</body>
|
63
|
+
</html>
|
64
|
+
|
65
|
+
And just like a regular Ruby module, if you make it available, we can all benefit.
|
66
|
+
|
67
|
+
== Mucking with standard HTML
|
68
|
+
|
69
|
+
Interested in modifying the behavior of a standard HTML element? Let's
|
70
|
+
say that you want all external links to use <tt>rel="nofollow"</tt>.
|
71
|
+
Rather than remembering to do this every time you can build it into a base
|
72
|
+
namespace.
|
73
|
+
|
74
|
+
Create your base module.
|
75
|
+
|
76
|
+
# core.rb
|
77
|
+
require 'rind'
|
78
|
+
|
79
|
+
module Core
|
80
|
+
class A < Rind::Html::A
|
81
|
+
def initialize(options={})
|
82
|
+
super(options)
|
83
|
+
@attributes[:rel] = 'nofollow' if is_external?
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_external?
|
87
|
+
....
|
88
|
+
end
|
89
|
+
private :is_external?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
Pass it to the Document as the <tt>base_namespace</tt>.
|
94
|
+
|
95
|
+
# my_file.cgi
|
96
|
+
require 'rind'
|
97
|
+
require 'core'
|
98
|
+
|
99
|
+
doc = Document.new('index.html', :base_namespace => 'core')
|
100
|
+
puts doc.render!
|
101
|
+
|
102
|
+
The links in <tt>index.html</tt> will now have the <tt>rel</tt> attribute
|
103
|
+
automatically added.
|
104
|
+
<a href="http://github.com" rel="nofollow">GitHub</a>
|
105
|
+
|
106
|
+
== The Future?
|
107
|
+
This is an early release. An alpha of sorts. The interface may change before it's
|
108
|
+
all over. Rind needs to help out modules with style sheets, JavaScript libraries,
|
109
|
+
images, querying system/user info, etc. Virtually no time has been spent optimizing
|
110
|
+
the code. More test cases need to be written. I still have a bunch of stuff in my
|
111
|
+
office that needs filing. You get the idea.
|
data/lib/rind.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# :title: Rind
|
2
|
+
# :main: lib/rind.rb
|
3
|
+
# :include: README.rdoc
|
4
|
+
# = License
|
5
|
+
# :include: LICENSE
|
6
|
+
|
7
|
+
require 'rind/equality'
|
8
|
+
require 'rind/traverse'
|
9
|
+
require 'rind/manipulate'
|
10
|
+
require 'rind/xpath'
|
11
|
+
require 'rind/nodes'
|
12
|
+
require 'rind/html'
|
13
|
+
require 'rind/xml'
|
14
|
+
require 'rind/parser'
|
15
|
+
require 'rind/document'
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Rind
|
2
|
+
class Document
|
3
|
+
include Equality
|
4
|
+
|
5
|
+
attr_reader :base_namespace, :dom, :template, :type
|
6
|
+
|
7
|
+
# === Parameter
|
8
|
+
# _template_ = file system path to the template
|
9
|
+
#
|
10
|
+
# === Options
|
11
|
+
# * _base_namespace_ = namespace
|
12
|
+
#
|
13
|
+
# Allows you to provide a namespace to check before
|
14
|
+
# falling back to the default Rind namespace.
|
15
|
+
# * _require_ = array of namespaces
|
16
|
+
#
|
17
|
+
# All namespaces that should be rendered must be
|
18
|
+
# listed.
|
19
|
+
# * _type_ = "xml" or "html"
|
20
|
+
#
|
21
|
+
# This can be automatically detect based on the
|
22
|
+
# file extension. Unknown extensions will default
|
23
|
+
# to "xml".
|
24
|
+
# === Example
|
25
|
+
# Rind::Document.new( "template.tpl", {
|
26
|
+
# :type => "html",
|
27
|
+
# :base_namespace => "core",
|
28
|
+
# :require => ["forms","photos"]
|
29
|
+
# })
|
30
|
+
def initialize(template, options = {})
|
31
|
+
@template = template
|
32
|
+
raise 'No such template.' if not File.file? @template
|
33
|
+
|
34
|
+
if options[:type]
|
35
|
+
@type = options[:type]
|
36
|
+
else
|
37
|
+
@type = case File.extname(@template)
|
38
|
+
when '.html', '.htm'
|
39
|
+
'html'
|
40
|
+
else
|
41
|
+
'xml'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if options[:base_namespace]
|
46
|
+
@base_namespace = options[:base_namespace]
|
47
|
+
else
|
48
|
+
@base_namespace = ['rind', @type].join(':')
|
49
|
+
end
|
50
|
+
|
51
|
+
@dom = Rind.parse(@template, @type, @base_namespace, options[:require])
|
52
|
+
end
|
53
|
+
|
54
|
+
# Renders the Document in place. This will recursively call
|
55
|
+
# <tt>render!</tt> on all the Document contents.
|
56
|
+
def render!
|
57
|
+
@dom.collect{|node| node.respond_to?(:render!) ? node.render! : node}.join('')
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return the root node of the Document.
|
61
|
+
def root
|
62
|
+
@dom.each do |node|
|
63
|
+
return node if not node.is_a? Rind::DocType
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
@dom.to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
# Xpath search of the root node that returns a list of matching nodes.
|
72
|
+
def s(path)
|
73
|
+
root.s(path)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Xpath search returning only the first matching node in the list.
|
77
|
+
def sf(path)
|
78
|
+
root.sf(path)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/rind/html.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rind
|
2
|
+
# Rind::Html will dynamically create any standard (or not) HTML element.
|
3
|
+
module Html
|
4
|
+
@@self_closing = ['br','hr','img','input','meta','link']
|
5
|
+
|
6
|
+
def self.const_missing(full_class_name, options={}) # :nodoc:
|
7
|
+
klass = Class.new(Element) do
|
8
|
+
# <b>Parent:</b> Element
|
9
|
+
# === Example
|
10
|
+
# Rind::Html::A.new(
|
11
|
+
# :attributes => {:href => "http://github.com"},
|
12
|
+
# :children => "GitHub"
|
13
|
+
# )
|
14
|
+
def initialize(options={})
|
15
|
+
super(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def expanded_name # :nodoc:
|
19
|
+
if @namespace_name.nil? or @namespace_name == '' or @namespace_name =~ /^(?:rind:)?html/
|
20
|
+
@local_name
|
21
|
+
else
|
22
|
+
[@namespace_name, @local_name].join(':')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s # :nodoc:
|
27
|
+
attrs = @attributes.collect{|k,v| "#{k}=\"#{v}\""}.join(' ')
|
28
|
+
attrs = " #{attrs}" if not attrs.empty?
|
29
|
+
|
30
|
+
if @@self_closing.include? @local_name
|
31
|
+
"<#{self.expanded_name}#{attrs} />"
|
32
|
+
else
|
33
|
+
"<#{self.expanded_name}#{attrs}>#{@children}</#{self.expanded_name}>"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
const_set full_class_name, klass
|
38
|
+
klass
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Note: These functions are not available for the root node in a tree.
|
2
|
+
module Manipulate
|
3
|
+
# Calls {Rind::Children::insert}[link:classes/Rind/Children.html#insert]
|
4
|
+
# to add nodes after <tt>self</tt>.
|
5
|
+
# === Example
|
6
|
+
# nodes = ['a', 'b', 'c']
|
7
|
+
# nodes[0].insert_after('d', 'e') => ['a', 'd', 'e', 'b', 'c']
|
8
|
+
def insert_after(*nodes)
|
9
|
+
children = self.parent.children
|
10
|
+
children.insert(children.index(self)+1, *nodes)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Calls {Rind::Children::insert}[link:classes/Rind/Children.html#insert]
|
14
|
+
# to add nodes before <tt>self</tt>.
|
15
|
+
# === Example
|
16
|
+
# nodes = ['a', 'b', 'c']
|
17
|
+
# nodes[2].insert_after('d', 'e') => ['a', 'b', 'd', 'e', 'c']
|
18
|
+
def insert_before(*nodes)
|
19
|
+
children = self.parent.children
|
20
|
+
children.insert(children.index(self), *nodes)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Calls {Rind::Children::delete}[link:classes/Rind/Children.html#delete]
|
24
|
+
# on <tt>self</tt>.
|
25
|
+
# === Example
|
26
|
+
# nodes = ['a', 'b', 'c']
|
27
|
+
# nodes[1].delete => 'b'
|
28
|
+
def remove
|
29
|
+
self.parent.children.delete(self)
|
30
|
+
end
|
31
|
+
end
|
data/lib/rind/nodes.rb
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
module Rind
|
2
|
+
class Nodes < Array
|
3
|
+
# Return only the nodes that match the Xpath provided.
|
4
|
+
def filter(path)
|
5
|
+
# if the path doesn't have an axis then default to "self"
|
6
|
+
if path !~ /^([.\/]|(.+?::))/
|
7
|
+
path = "self::#{path}"
|
8
|
+
end
|
9
|
+
Nodes.new(self.find_all{|node| node.s(path)})
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Cdata
|
14
|
+
include Equality
|
15
|
+
include Manipulate
|
16
|
+
include Traverse
|
17
|
+
include Xpath
|
18
|
+
|
19
|
+
# Create a CDATA with <tt>content</tt> holding
|
20
|
+
# the character data to contain.
|
21
|
+
def initialize(content)
|
22
|
+
@content = content
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"<![CDATA[#{@content}]]>"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Comment
|
31
|
+
include Equality
|
32
|
+
include Manipulate
|
33
|
+
include Traverse
|
34
|
+
include Xpath
|
35
|
+
|
36
|
+
# Create a comment with <tt>content</tt> holding
|
37
|
+
# the character data of the comment.
|
38
|
+
def initialize(content)
|
39
|
+
@content = content
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
"<!--#{@content}-->"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class DocType
|
48
|
+
# Create a Document Type Declaration with
|
49
|
+
# +content+ holding the DTD identifiers.
|
50
|
+
def initialize(content)
|
51
|
+
@content = content
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
"<!DOCTYPE#{@content}>"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class ProcessingInstruction
|
60
|
+
# Create a processing instruction with
|
61
|
+
# +content+ holding the character data.
|
62
|
+
def initialize(content)
|
63
|
+
@content = content
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
"<?#{@content}>"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Element
|
72
|
+
include Equality
|
73
|
+
include Manipulate
|
74
|
+
include Traverse
|
75
|
+
include Xpath
|
76
|
+
|
77
|
+
attr_reader :children, :local_name, :namespace_name
|
78
|
+
alias :name :local_name
|
79
|
+
alias :namespace :namespace_name
|
80
|
+
|
81
|
+
# === Options
|
82
|
+
# * _attributes_ = hash or string
|
83
|
+
# * _children_ = array of nodes
|
84
|
+
# === Examples
|
85
|
+
# Rind::Element.new(
|
86
|
+
# :attributes => { :id => "first", :class => "second" },
|
87
|
+
# :children => [Rind::Element.new(), Rind::Element.new()]
|
88
|
+
# )
|
89
|
+
# Rind::Element.new(
|
90
|
+
# :attributes => 'id="first" class="second"',
|
91
|
+
# :children => "Hello World!"
|
92
|
+
# )
|
93
|
+
def initialize(options={})
|
94
|
+
self.class.to_s =~ /^(?:([\w:]+)::)?(\w+)$/
|
95
|
+
@namespace_name, @local_name = $1, $2.downcase
|
96
|
+
@namespace_name.downcase!.gsub!(/::/, ':') if not @namespace_name.nil?
|
97
|
+
|
98
|
+
@namespace_name = options[:namespace_name] if options[:namespace_name]
|
99
|
+
|
100
|
+
@attributes = Hash.new
|
101
|
+
if options[:attributes].is_a? Hash
|
102
|
+
options[:attributes].each do |k,v|
|
103
|
+
@attributes[k.to_s] = v
|
104
|
+
end
|
105
|
+
elsif options[:attributes].is_a? String
|
106
|
+
options[:attributes].split(/\s+/).each do |attribute|
|
107
|
+
name, value = attribute.split(/=/)
|
108
|
+
@attributes[name] = value.scan(/\"(.*?)\"/).to_s
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
@children = Children.new(self, *options[:children])
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get attributes or an attribute values.
|
116
|
+
# === Examples
|
117
|
+
# e = Rind::Element.new(:attributes => {:id => "id_1", :class => "class_1"})
|
118
|
+
# e[] => {"id"=>"id_1", "class"=>"class_1"}
|
119
|
+
# e[:id] => "id_1"
|
120
|
+
# e['class'] => "class_1"
|
121
|
+
def [](key = nil)
|
122
|
+
key.nil? ? @attributes : @attributes[key.to_s]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Set the value of an attribute.
|
126
|
+
# === Examples
|
127
|
+
# e = Rind::Element.new(:attributes => {:id => "id_1", :class => "class_1"})
|
128
|
+
# e['id'] => "id_2"
|
129
|
+
# e[:class] => "class_2"
|
130
|
+
def []=(key, value)
|
131
|
+
@attributes[key.to_s] = value
|
132
|
+
end
|
133
|
+
|
134
|
+
# Get the full name of the Element.
|
135
|
+
# === Examples
|
136
|
+
# b = Rind::Html::Br.new()
|
137
|
+
# b.expanded_name => 'br'
|
138
|
+
# cn = Custom::Node.new()
|
139
|
+
# cn.expanded_name => 'custom:node'
|
140
|
+
def expanded_name
|
141
|
+
if @namespace_name == 'rind'
|
142
|
+
@local_name
|
143
|
+
else
|
144
|
+
[@namespace_name, @local_name].join(':')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Renders the node in place. This will recursively call
|
149
|
+
# <tt>render!</tt> on all child nodes.
|
150
|
+
def render!
|
151
|
+
@children.render! if not @children.empty?
|
152
|
+
self
|
153
|
+
end
|
154
|
+
|
155
|
+
def to_s
|
156
|
+
attrs = @attributes.collect{|k,v| "#{k}=\"#{v}\""}.join(' ')
|
157
|
+
attrs = " #{attrs}" if not attrs.empty?
|
158
|
+
|
159
|
+
if @children.empty?
|
160
|
+
"<#{self.expanded_name}#{attrs} />"
|
161
|
+
else
|
162
|
+
"<#{self.expanded_name}#{attrs}>#{@children}</#{self.expanded_name}>"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# All of the Array functions have been modified to work with Children.
|
168
|
+
# Functions like <tt>pop</tt> that remove a node and return it will
|
169
|
+
# remove the association to the parent node. Functions like "push"
|
170
|
+
# will automatically associate the nodes to the parent.
|
171
|
+
class Children < Nodes
|
172
|
+
include Enumerable
|
173
|
+
include Equality
|
174
|
+
|
175
|
+
def initialize(parent, *nodes)
|
176
|
+
super(nodes)
|
177
|
+
@parent = parent
|
178
|
+
fix_children!
|
179
|
+
end
|
180
|
+
|
181
|
+
def fix_children!
|
182
|
+
compact!
|
183
|
+
collect! do |node|
|
184
|
+
node = Rind::Text.new(node) if node.is_a?(String)
|
185
|
+
node.parent = @parent
|
186
|
+
node
|
187
|
+
end
|
188
|
+
end
|
189
|
+
private :fix_children!
|
190
|
+
|
191
|
+
def self.call_and_fix_children(*functions)
|
192
|
+
functions.each do |f|
|
193
|
+
define_method(f) do
|
194
|
+
value = super
|
195
|
+
fix_children!
|
196
|
+
value
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
private_class_method :call_and_fix_children
|
201
|
+
call_and_fix_children :fill, :insert, :push, :replace, :unshift
|
202
|
+
|
203
|
+
def self.pass_and_clear_parent(*functions)
|
204
|
+
functions.each do |f|
|
205
|
+
define_method(f) do
|
206
|
+
node = super
|
207
|
+
node.parent = nil if not node.nil?
|
208
|
+
node
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
private_class_method :pass_and_clear_parent
|
213
|
+
pass_and_clear_parent :delete_at, :pop, :shift
|
214
|
+
|
215
|
+
def delete(child, &block) # :nodoc:
|
216
|
+
child = Rind::Text.new(child) if child.is_a?(String)
|
217
|
+
node = super(child, &block)
|
218
|
+
node.parent = nil if node.respond_to? :parent
|
219
|
+
node
|
220
|
+
end
|
221
|
+
|
222
|
+
# Replace the child nodes with their rendered values.
|
223
|
+
def render!
|
224
|
+
nodes = collect{|child| child.respond_to?(:render!) ? child.render! : child}.flatten
|
225
|
+
replace(nodes)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class Text < String
|
230
|
+
include Manipulate
|
231
|
+
include Traverse
|
232
|
+
include Xpath
|
233
|
+
end
|
234
|
+
end
|