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 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
@@ -0,0 +1,9 @@
1
+ module Equality
2
+ def ==(item)
3
+ self.to_s == item.to_s
4
+ end
5
+
6
+ def eql?(item)
7
+ item.instance_of?(self.class) && self == item
8
+ end
9
+ 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