rind 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/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
|