rind 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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