feather 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Scott Tadman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,52 @@
1
+ # feather
2
+
3
+ A simple text template system for generating output for a variety of uses
4
+ including plain-text, HTML, and JavaScript.
5
+
6
+ ## Examples
7
+
8
+ The straight-forward usage is substitutions:
9
+
10
+ template = Feather::Template.new("This {{noun}} is {{adjective}}")
11
+
12
+ template.render(:noun => 'shoe', :adjective => 'red')
13
+ # => "This shoe is red"
14
+
15
+ If required, the content can be HTML-escaped automatically:
16
+
17
+ template = Feather::Template.new(
18
+ "This {{noun}} is {{adjective}}",
19
+ :escape => :html
20
+ )
21
+
22
+ template.render(:noun => 'goose', :adjective => '<em>blue</em>')
23
+ # => "This goose is &lt;em&gt;blue&lt;/em&gt;"
24
+
25
+ This can also be engaged on a case-by-case basis:
26
+
27
+ template = Feather::Template.new("This {{&noun}} is {{adjective}}")
28
+
29
+ template.render(:noun => '<b>goose</b>', :adjective => '<em>blue</em>')
30
+ # => "This &lt;b&gt;goose&lt;/b&gt; is <em>blue</em>"
31
+
32
+ Also available is URI encoding for links:
33
+
34
+ template = Feather::Template.new(
35
+ "<a href='/home?user_id={{%user_id}}'>{{&label}}</a>"
36
+ )
37
+
38
+ template.render(:user_id => 'joe&2', :label => 'Joe&2')
39
+ # => "<a href='/home?user_id=joe%262'>Joe&amp;2</a>"
40
+
41
+ A sample template is located in:
42
+
43
+ notes/example.ft
44
+
45
+ A number of other usage cases are described in test/test_feather_template.rb
46
+ as a reference.
47
+
48
+ ## Copyright
49
+
50
+ Copyright (c) 2011-2012 Scott Tadman, The Working Group Inc.
51
+ See LICENSE.txt for further details.
52
+
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+
3
+ require 'rake'
4
+ require 'jeweler'
5
+
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "feather"
8
+ gem.homepage = "http://github.com/twg/feather"
9
+ gem.license = "MIT"
10
+ gem.summary = %Q{Light-weight text tempating system}
11
+ gem.description = %Q{A simple light-weight text templating system}
12
+ gem.email = "github@tadman.ca"
13
+ gem.authors = [ "Scott Tadman" ]
14
+ end
15
+
16
+ Jeweler::RubygemsDotOrgTasks.new
17
+
18
+ require 'rake/testtask'
19
+
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/test_*.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1,20 @@
1
+ module Feather
2
+ # == Submodules ===========================================================
3
+
4
+ autoload(:Support, 'feather/support')
5
+ autoload(:Template, 'feather/template')
6
+
7
+ def self.version
8
+ @version ||= File.readlines(
9
+ File.expand_path('../VERSION', File.dirname(__FILE__))
10
+ ).first.chomp
11
+ end
12
+
13
+ def self.new(*args)
14
+ template = Feather::Template.new(*args)
15
+
16
+ yield(template) if (block_given?)
17
+
18
+ template
19
+ end
20
+ end
@@ -0,0 +1,73 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+
4
+ module Feather::Support
5
+ def uri_escape(object)
6
+ URI.escape(object.to_s, /[^a-z0-9\-\.]/i)
7
+ end
8
+
9
+ def html_escape(object)
10
+ CGI.escapeHTML(object.to_s)
11
+ end
12
+
13
+ def js_escape(object)
14
+ object.inspect
15
+ end
16
+
17
+ def css_escape(object)
18
+ [ object ].flatten.join(' ')
19
+ end
20
+
21
+ def iterate(object)
22
+ if (object.respond_to?(:each))
23
+ object.each do |i|
24
+ yield(i)
25
+ end
26
+ elsif (object)
27
+ yield(object)
28
+ end
29
+ end
30
+
31
+ def cast_as_vars(object, stack)
32
+ if (object.is_a?(Hash))
33
+ stack.each do |parent|
34
+ if (parent.is_a?(Hash))
35
+ object = parent.merge(object)
36
+ end
37
+ end
38
+
39
+ object
40
+ else
41
+ object.respond_to?(:each) ? object : [ object ]
42
+ end
43
+ end
44
+
45
+ def variable_stack(variables, force_as_array = true)
46
+ case (variables)
47
+ when Hash
48
+ remapped = Hash[
49
+ variables.collect do |k, v|
50
+ [ k ? k.to_sym : k, variable_stack(v, false) ]
51
+ end
52
+ ]
53
+
54
+ if (default = variables.default)
55
+ remapped.default = default
56
+ end
57
+
58
+ if (default_proc = variables.default_proc)
59
+ remapped.default_proc = default_proc
60
+ end
61
+
62
+ remapped
63
+ when Array
64
+ variables.collect do |v|
65
+ variable_stack(v, false)
66
+ end
67
+ else
68
+ force_as_array ? [ variables ] : variables
69
+ end
70
+ end
71
+
72
+ extend self
73
+ end
@@ -0,0 +1,295 @@
1
+ class Feather::Template
2
+ # == Constants ============================================================
3
+
4
+ TOKEN_REGEXP = /((?:[^\{]|\{[^\{]|\{\{\{)+)|\{\{\s*([\&\%\$\.\:\*\/\=]|\?\!?)?([^\}]*)\}\}/.freeze
5
+ TOKEN_TRIGGER = /\{\{/.freeze
6
+
7
+ TO_YAML_PROPERTIES = %w[ @content @escape_method ].freeze
8
+
9
+ # == Utility Classes ======================================================
10
+
11
+ class TemplateHash < Hash; end
12
+
13
+ class VariableTracker < Hash
14
+ def initialize
15
+ super do |h, k|
16
+ h[k] = h.length
17
+ end
18
+ end
19
+ end
20
+
21
+ # == Exceptions ===========================================================
22
+
23
+ class ParseError < Exception ; end
24
+ class ArgumentError < Exception; end
25
+ class MissingVariable < Exception ; end
26
+ class RecursionError < Exception; end
27
+
28
+ # == Class Methods ========================================================
29
+
30
+ # == Instance Methods =====================================================
31
+
32
+ def initialize(content, options = nil)
33
+ if (options)
34
+ if (source = options[:escape])
35
+ case (source.to_sym)
36
+ when :html, :html_escape
37
+ @escape_method = :html_escape
38
+ when :text, nil
39
+ # Default, ignored
40
+ else
41
+ raise ArgumentError, "Unknown escape source #{source}"
42
+ end
43
+ end
44
+ end
45
+
46
+ @content =
47
+ case (content)
48
+ when IO
49
+ content.read
50
+ else
51
+ content.to_s
52
+ end
53
+
54
+ yield(self) if (block_given?)
55
+ end
56
+
57
+ def to_proc
58
+ @_proc ||= begin
59
+ source = ''
60
+
61
+ self.compile(:source => source, :escape_method => @escape_method)
62
+
63
+ eval(source)
64
+ end
65
+ end
66
+
67
+ def render(variables = nil, templates = nil, parents = nil)
68
+ variables = Feather::Support.variable_stack(variables, true)
69
+
70
+ if (templates)
71
+ # Unless the template options have already been processed, mapping
72
+ # will need to be performed.
73
+ unless (templates.is_a?(TemplateHash))
74
+ _templates = templates
75
+ templates = TemplateHash.new do |h, k|
76
+ v = _templates[k]
77
+
78
+ h[k] =
79
+ case (v)
80
+ when Feather::Template, Proc, Array
81
+ v
82
+ when TOKEN_TRIGGER
83
+ self.class.new(v, :escape => @escape_method)
84
+ else
85
+ v.to_s
86
+ end
87
+ end
88
+ end
89
+ else
90
+ templates = TemplateHash.new
91
+ end
92
+
93
+ if (parents)
94
+ case (parents)
95
+ when Array
96
+ _parents = parents.dup
97
+ _parent = _parents.shift
98
+
99
+ unless (_parent.is_a?(Feather::Template))
100
+ _parent = self.class.new(_parent, :escape => @escape_method)
101
+ end
102
+
103
+ _parent.render(
104
+ variables,
105
+ templates.merge(
106
+ nil => self.to_proc.call(variables, templates)
107
+ ),
108
+ _parents.empty? ? nil : _parents
109
+ )
110
+ when Feather::Template, Proc
111
+ parents.render(
112
+ variables,
113
+ templates.merge(
114
+ nil => self.to_proc.call(variables, templates)
115
+ )
116
+ )
117
+ when String
118
+ _parent = parents
119
+
120
+ unless (_parent.is_a?(Feather::Template))
121
+ _parent = self.class.new(_parent, :escape => @escape_method)
122
+ end
123
+
124
+ _parent.render(
125
+ variables,
126
+ templates.merge(
127
+ nil => self.to_proc.call(variables, templates)
128
+ )
129
+ )
130
+ else
131
+ raise ArgumentError, "Invalid options passed in to parents"
132
+ end
133
+ else
134
+ self.to_proc.call(variables, templates)
135
+ end
136
+ end
137
+ alias_method :call, :render
138
+
139
+ def compile(options)
140
+ escape_method = options[:escape_method]
141
+ sections = options[:sections]
142
+ templates = options[:templates]
143
+ variables = options[:variables]
144
+ source = options[:source]
145
+
146
+ stack = [ [ :base, nil, VariableTracker.new ] ]
147
+ stack_variables = nil
148
+
149
+ @content.scan(TOKEN_REGEXP).each do |text, tag_type, tag|
150
+ if (text)
151
+ text = text.sub(/\{(\{\{+)/, '\1').sub(/\}(\}\}+)/, '\1')
152
+
153
+ source and source << "r<<#{text.inspect};"
154
+ else
155
+ tag = tag.strip
156
+ tag = tag.empty? ? nil : tag.to_sym
157
+
158
+ case (tag_type)
159
+ when '&'
160
+ # HTML escaped
161
+ index = stack[-1][2][tag.inspect]
162
+
163
+ source and source << "v&&r<<h.html_escape(v[#{tag.inspect}].to_s);"
164
+
165
+ variables and variables[tag] = true
166
+
167
+ when '%'
168
+ # URI escaped
169
+ index = stack[-1][2][tag.inspect]
170
+
171
+ source and source << "v&&r<<h.uri_escape(v.is_a?(Array)?v[#{index}]:v[#{tag.inspect}]);"
172
+
173
+ variables and variables[tag] = true
174
+ when '$'
175
+ # JavaScript escaped
176
+ index = stack[-1][2][tag.inspect]
177
+
178
+ source and source << "v&&r<<h.js_escape(v.is_a?(Array)?v[#{index}]:v[#{tag.inspect}]);"
179
+
180
+ variables and variables[tag] = true
181
+ when '.'
182
+ # CSS escaped
183
+ index = stack[-1][2][tag.inspect]
184
+
185
+ source and source << "v&&r<<h.css_escape(v.is_a?(Array)?v[#{index}]:v[#{tag.inspect}]);"
186
+
187
+ variables and variables[tag] = true
188
+ when ':'
189
+ # Defines start of a :section
190
+ index = stack[-1][2][tag.inspect]
191
+
192
+ stack_variables ||= 's=[];'
193
+ stack << [ :section, tag, VariableTracker.new ]
194
+
195
+ source and source << "if(v);s<<v;v=v.is_a?(Array)?v[#{index}]:(v.is_a?(Hash)&&v[#{tag.inspect}]);"
196
+ source and source << "h.iterate(v){|v|;v=h.cast_as_vars(v, s);"
197
+
198
+ sections and sections[tag] = true
199
+ when '?', '?!'
200
+ # Defines start of a ?conditional
201
+
202
+ # The stack will inherit the variable assignment locations from the
203
+ # existing stack layer.
204
+ stack << [ :conditional, tag, stack[-1][2] ]
205
+ source and source << "#{tag_type=='?' ? 'if' : 'unless'}(v&&v.is_a?(Hash)&&v[#{tag.inspect}]);"
206
+
207
+ variables and variables[tag] = true
208
+ when '*'
209
+ source and source << "_t=t&&t[#{tag.inspect}];r<<(_t.respond_to?(:call)?_t.call(v,t):_t.to_s);"
210
+
211
+ templates and templates[tag] = true
212
+ when '/'
213
+ # Closes out a section or conditional
214
+ closed = stack.pop
215
+
216
+ case (closed[0])
217
+ when :section
218
+ if (tag and tag != closed[1])
219
+ raise ParseError, "Template contains unexpected {{#{tag}}}, expected {{#{closed[1]}}}"
220
+ end
221
+
222
+ source and source << "};v=s.pop;end;"
223
+ when :conditional
224
+ source and source << "end;"
225
+ when :base
226
+ raise ParseError, "Unexpected {{#{tag}}}, too many tags closed"
227
+ end
228
+ when '='
229
+ # Literal insertion
230
+ index = stack[-1][2][tag.inspect]
231
+
232
+ source and source << "v&&r<<(v.is_a?(Array)?v[#{index}]:v[#{tag.inspect}]).to_s;"
233
+
234
+ variables and variables[tag] = true
235
+ else
236
+ # Contextual insertion
237
+ index = stack[-1][2][tag.inspect]
238
+
239
+ subst = "v.is_a?(Array)?v[#{stack[-1][2][tag.inspect]}]:v[#{tag.inspect}]"
240
+
241
+ if (escape_method)
242
+ source and source << "v&&r<<h.#{escape_method}(#{subst}.to_s);"
243
+ else
244
+ source and source << "v&&r<<(#{subst}).to_s;"
245
+ end
246
+
247
+ variables and variables[tag] = true
248
+ end
249
+ end
250
+ end
251
+
252
+ unless (stack.length == 1)
253
+ case (stack[1][0])
254
+ when :section
255
+ raise ParseError, "Unclosed {{:#{stack[1][1]}}} in template"
256
+ when :conditional
257
+ raise ParseError, "Unclosed {{?#{stack[1][1]}}} in template"
258
+ else
259
+ raise ParseError, "Unclosed {{#{stack[1][1]}}} in template"
260
+ end
261
+ end
262
+
263
+ if (source)
264
+ source.replace("begin;c=false;h=Feather::Support;lambda{|v,t|raise RecursionError if(c);c=true;#{stack_variables}r='';#{source}c=false;r};end")
265
+ end
266
+
267
+ true
268
+ end
269
+
270
+ def to_yaml_properties
271
+ TO_YAML_PROPERTIES
272
+ end
273
+
274
+ def psych_to_yaml(dump)
275
+ # Avoid serializing the generated proc by moving it to a temporary
276
+ # variable for the duration of this operation.
277
+ _proc, @_proc = @_proc, nil
278
+
279
+ super(dump)
280
+
281
+ @_proc = _proc
282
+
283
+ dump
284
+ end
285
+
286
+ def marshal_dump
287
+ [ @content, { :escape => @escape_method } ]
288
+ end
289
+
290
+ def marshal_load(dump)
291
+ @content, options = dump
292
+
293
+ @escape_method = options[:escape]
294
+ end
295
+ end
@@ -0,0 +1,30 @@
1
+ This is an example document with simple {{placeholder}} values included.
2
+
3
+ Each of these {{variable}} substitutions can be either {{simple}},
4
+ {{=literal}}, {{%url_escaped}}, {{.css_escaped}}, {{$json_escaped}} or
5
+ {{&html_escaped}} depending on preference.
6
+
7
+ It's even possible to place other {{*templates}} by name using the standard
8
+ markup, or include or default one using {{*}} instead of {{default}}.
9
+
10
+ Sometimes it's practical to define a block which can be re-used. These are
11
+ called sections:
12
+
13
+ {{:section}}
14
+ {{title}}
15
+
16
+ {{:bullet_point}}
17
+ * {{point}}
18
+ {{/}}
19
+ {{/:section}}
20
+
21
+ {{?conditional}}
22
+ It's also possible to render sections conditionally by testing if a value
23
+ is present and true.
24
+ {{/}}
25
+
26
+ * For avoiding interpolation, {{{three}}} or more braces can be used. The
27
+ outer most ones are always stripped, the rest left alone.
28
+
29
+ * Entire sections can be left unprocessed by declaring {{#raw}} which will
30
+ be terminated upon the first instance of {{#/}} if found.
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+
3
+ require 'test/unit'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+
8
+ require 'feather'
9
+
10
+ class Test::Unit::TestCase
11
+ def assert_exception(exception_class, message = nil)
12
+ begin
13
+ yield
14
+ rescue exception_class
15
+ # Expected
16
+ else
17
+ flunk message || "Did not raise #{exception_class}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ require_relative './helper'
2
+
3
+ class TestFeather < Test::Unit::TestCase
4
+ def test_module_loaded
5
+ assert Feather
6
+
7
+ assert Feather.version
8
+ assert Feather.version.match(/^\d/)
9
+ assert !Feather.version.match(/\n/)
10
+ end
11
+
12
+ def test_empty_template
13
+ template = Feather.new('')
14
+
15
+ assert template
16
+ assert_equal '', template.render
17
+ end
18
+ end
@@ -0,0 +1,47 @@
1
+ require_relative './helper'
2
+
3
+ require 'yaml'
4
+
5
+ class TestFeatherSupport < Test::Unit::TestCase
6
+ def test_variable_stack
7
+ test = { :test => [ { :a => 'a', :b => 'b' }, { :c => 'c' } ] }
8
+
9
+ variables = Feather::Support.variable_stack(test)
10
+
11
+ assert_equal test, variables
12
+
13
+ assert_equal 'a', variables[:test][0][:a]
14
+ assert_equal 'b', variables[:test][0][:b]
15
+ assert_equal 'c', variables[:test][1][:c]
16
+
17
+ test = { 'test' => [ { 'a' => :a, 'b' => :b }, { 'c' => :c } ] }
18
+
19
+ variables = Feather::Support.variable_stack(test)
20
+
21
+ assert_equal :a, variables[:test][0][:a]
22
+ assert_equal :b, variables[:test][0][:b]
23
+ assert_equal :c, variables[:test][1][:c]
24
+
25
+ assert_equal 'test', Feather::Support.variable_stack('test', false)
26
+ assert_equal [ 'test' ], Feather::Support.variable_stack('test')
27
+ assert_equal [ 'test' ], Feather::Support.variable_stack([ 'test' ], false)
28
+ assert_equal [ 'test' ], Feather::Support.variable_stack([ 'test' ])
29
+
30
+ variables = Feather::Support.variable_stack(:head => [ { :tag => 'meta' }, { :tag => 'link' } ])
31
+
32
+ assert_equal 'meta', variables[:head][0][:tag]
33
+ assert_equal 'link', variables[:head][1][:tag]
34
+
35
+ test = { 'top' => { 'layer' => 'top', 't' => 'top', 'middle' => { 'layer' => 'middle', 'm' => 'middle', 'bottom' => { 'layer' => 'bottom', 'b' => 'bottom' } } } }
36
+
37
+ variables = Feather::Support.variable_stack(test)
38
+
39
+ assert_equal 'top', variables[:top][:t]
40
+ assert_equal 'middle', variables[:top][:middle][:m]
41
+ assert_equal 'bottom', variables[:top][:middle][:bottom][:b]
42
+
43
+ assert_equal 'top', variables[:top][:layer]
44
+ assert_equal 'middle', variables[:top][:middle][:layer]
45
+ assert_equal 'bottom', variables[:top][:middle][:bottom][:layer]
46
+ end
47
+ end
@@ -0,0 +1,246 @@
1
+ require_relative './helper'
2
+
3
+ require 'yaml'
4
+
5
+ class TestFeatherTemplate < Test::Unit::TestCase
6
+ def test_empty_template
7
+ template = Feather::Template.new('')
8
+
9
+ assert_equal '', template.render
10
+ end
11
+
12
+ def test_simple_templates
13
+ template = Feather::Template.new('example')
14
+
15
+ assert_equal 'example', template.render
16
+
17
+ template = Feather::Template.new('{{{example}}}')
18
+
19
+ assert_equal '{{example}}', template.render
20
+
21
+ template = Feather::Template.new('example {{example}} text')
22
+
23
+ assert_equal 'example something text', template.render(:example => 'something')
24
+
25
+ template = Feather::Template.new('example {{ example }} text')
26
+
27
+ assert_equal 'example something text', template.render(:example => 'something')
28
+ end
29
+
30
+ def test_boolean_templates
31
+ template = Feather::Template.new('{{?boolean}}true {{/}}false')
32
+
33
+ assert_equal 'false', template.render
34
+ assert_equal 'true false', template.render(:boolean => true)
35
+ assert_equal 'false', template.render(:boolean => false)
36
+
37
+ template = Feather::Template.new('{{?boolean}}true{{/}}{{?!boolean}}false{{/}}')
38
+
39
+ assert_equal 'false', template.render
40
+ assert_equal 'true', template.render(:boolean => true)
41
+ assert_equal 'false', template.render(:boolean => false)
42
+
43
+ template = Feather::Template.new('{{?boolean}}{{true}}{{/boolean}}{{?!boolean}}{{false}}{{/boolean}}')
44
+
45
+ assert_equal '', template.render
46
+ assert_equal 'TRUE', template.render(:boolean => true, :true => 'TRUE')
47
+ assert_equal 'FALSE', template.render(:boolean => false, :false => 'FALSE')
48
+
49
+ template = Feather::Template.new('{{?boolean}}{{/boolean}}{{?!boolean}}{{/boolean}}')
50
+
51
+ assert_equal '', template.render
52
+ assert_equal '', template.render(:boolean => true)
53
+ assert_equal '', template.render(:boolean => false)
54
+ end
55
+
56
+ def test_sectioned_templates
57
+ template = Feather::Template.new('<head>{{:head}}<{{tag}}>{{/}}</head>')
58
+
59
+ assert_equal '<head><meta></head>', template.render(:head => 'meta')
60
+ assert_equal '<head><meta></head>', template.render('head' => 'meta')
61
+ assert_equal '<head><meta><link></head>', template.render(:head => %w[ meta link ])
62
+ assert_equal '<head><meta><link></head>', template.render('head' => [ :meta, :link ])
63
+ assert_equal '<head><meta><link></head>', template.render(:head => [ { :tag => 'meta' }, { :tag => 'link' } ])
64
+ assert_equal '<head><meta><link></head>', template.render('head' => [ { 'tag' => :meta }, { 'tag' => :link } ])
65
+ assert_equal '<head></head>', template.render
66
+ assert_equal '<head></head>', template.render([ ])
67
+ assert_equal '<head></head>', template.render({ })
68
+ assert_equal '<head><></head>', template.render('')
69
+ assert_equal '<head><></head>', template.render(:head => '')
70
+ assert_equal '<head></head>', template.render(:head => nil)
71
+ assert_equal '<head></head>', template.render(:head => [ ])
72
+
73
+ template = Feather::Template.new('<div>{{:link}}<a href="{{href}}" alt="{{alt}}">{{/}}</div>')
74
+
75
+ assert_equal '<div><a href="meta" alt=""></div>', template.render(:link => 'meta')
76
+ assert_equal '<div><a href="meta" alt="link"></div>', template.render(:link => [ %w[ meta link ] ])
77
+ assert_equal '<div><a href="/h" alt=""><a href="" alt="alt"><a href="/" alt="top"></div>', template.render(:link => [ { :href => '/h' }, { :alt => 'alt' }, { :href => '/', :alt => 'top' } ])
78
+ assert_equal '<div></div>', template.render
79
+ assert_equal '<div></div>', template.render(:link => nil)
80
+ assert_equal '<div></div>', template.render(:link => [ ])
81
+ end
82
+
83
+ def test_template_with_context
84
+ template = Feather::Template.new('{{example}}', :escape => :html)
85
+
86
+ assert_equal '&lt;strong&gt;', template.render('<strong>')
87
+
88
+ template = Feather::Template.new('{{=example}}', :escape => :html)
89
+
90
+ assert_equal '<strong>', template.render('<strong>')
91
+ end
92
+
93
+ def test_recursive_templates
94
+ template = Feather::Template.new('{{*example}}', :escape => :html)
95
+
96
+ assert_equal 'child', template.render(nil, { :example => '{{*parent}}', :parent => 'child' }.freeze)
97
+ end
98
+
99
+ def test_dynamic_variables
100
+ template = Feather::Template.new('{{example}}{{text}}', :escape => :html)
101
+
102
+ generator = Hash.new do |h, k|
103
+ h[k] = "<#{k}>"
104
+ end
105
+
106
+ assert_equal '&lt;example&gt;&lt;text&gt;', template.render(generator)
107
+ end
108
+
109
+ def test_dynamic_templates
110
+ template = Feather::Template.new('<{{*example}}>', :escape => :html)
111
+
112
+ generator = Hash.new do |h, k|
113
+ h[k] = k.to_s.upcase
114
+ end
115
+
116
+ assert_equal '<EXAMPLE>', template.render(nil, generator)
117
+ end
118
+
119
+ def test_missing_templates
120
+ template = Feather::Template.new('{{*example}}', :escape => :html)
121
+
122
+ assert_equal '', template.render(nil, { })
123
+ end
124
+
125
+ def test_recursive_circular_templates
126
+ template = Feather::Template.new('{{*reference}}', :escape => :html)
127
+
128
+ assert_exception Feather::Template::RecursionError do
129
+ template.render(nil, { :reference => '{{*backreference}}', :backreference => '{{*reference}}' }.freeze)
130
+ end
131
+ end
132
+
133
+ def test_parent_templates
134
+ parent_template = Feather::Template.new('{{a}}[{{*}}]{{b}}'.freeze)
135
+ child_template = Feather::Template.new('{{c}}{{*}}'.freeze)
136
+ final_template = Feather::Template.new('{{a}}'.freeze)
137
+
138
+ variables = { :a => 'A', :b => 'B', :c => 'C' }
139
+
140
+ assert_equal 'A', final_template.render(variables)
141
+ assert_equal 'CA', final_template.render(variables, nil, child_template)
142
+ assert_equal 'A[CA]B', final_template.render(variables, nil, [ child_template, parent_template ].freeze)
143
+ end
144
+
145
+ def test_inline_parent_templates
146
+ template = Feather::Template.new('{{a}}')
147
+
148
+ variables = { :a => 'A', :b => 'B', :c => 'C' }
149
+
150
+ assert_equal 'A', template.render(variables)
151
+ assert_equal 'CA', template.render(variables, nil, '{{c}}{{*}}'.freeze)
152
+ assert_equal 'A[CA]B', template.render(variables, nil, %w[ {{c}}{{*}} {{a}}[{{*}}]{{b}} ].freeze)
153
+ end
154
+
155
+ def test_extract_variables
156
+ template = Feather::Template.new('{{a}}{{?b}}{{=c}}{{/b}}{{&d}}{{$e}}{{.f}}{{%g}}{{:h}}{{i}}{{/h}}')
157
+
158
+ variables = { }
159
+ sections = { }
160
+ templates = { }
161
+
162
+ template.compile(
163
+ :variables => variables,
164
+ :sections => sections,
165
+ :templates => templates
166
+ )
167
+
168
+ assert_equal [ :a, :b, :c, :d, :e, :f, :g, :i ], variables.keys.sort_by(&:to_s)
169
+ assert_equal [ :h ], sections.keys.sort_by(&:to_s)
170
+ assert_equal [ ], templates.keys.sort_by(&:to_s)
171
+ end
172
+
173
+ def test_chain_extract_variables
174
+ template = Feather::Template.new('{{a}}{{?b}}{{=c}}{{/b}}{{&d}}{{$e}}{{.f}}{{%g}}{{:h}}{{i}}{{/h}}')
175
+
176
+ variables = { :x => true }
177
+ sections = { :y => true }
178
+ templates = { :z => true }
179
+
180
+ template.compile(
181
+ :variables => variables,
182
+ :sections => sections,
183
+ :templates => templates
184
+ )
185
+
186
+ assert_equal [ :a, :b, :c, :d, :e, :f, :g, :i, :x ], variables.keys.sort_by(&:to_s)
187
+ assert_equal [ :h, :y ], sections.keys.sort_by(&:to_s)
188
+ assert_equal [ :z ], templates.keys.sort_by(&:to_s)
189
+ end
190
+
191
+ def test_variable_tracker
192
+ tracker = Feather::Template::VariableTracker.new
193
+
194
+ assert_equal true, tracker.empty?
195
+ assert_equal 0, tracker[:a]
196
+ assert_equal 1, tracker[:b]
197
+ assert_equal 2, tracker[:c]
198
+ assert_equal 0, tracker[:a]
199
+ assert_equal 2, tracker[:c]
200
+ assert_equal 3, tracker[:z]
201
+ end
202
+
203
+ def test_clone
204
+ template = Feather::Template.new('<p>{{example}}</p>', :escape => :html)
205
+
206
+ cloned = template.clone
207
+
208
+ assert_equal '<p>&lt;strong&gt;</p>', cloned.render('<strong>')
209
+ end
210
+
211
+ def test_serialization_with_yaml
212
+ template = Feather::Template.new('<p>{{example}}</p>', :escape => :html)
213
+
214
+ assert_equal '<p>&lt;strong&gt;</p>', template.render('<strong>')
215
+
216
+ serialized_template = YAML.dump(template)
217
+
218
+ deserialized_template = YAML.load(serialized_template)
219
+
220
+ assert_equal '<p>&lt;strong&gt;</p>', deserialized_template.render('<strong>')
221
+ end
222
+
223
+ def test_serialization_with_marshal
224
+ template = Feather::Template.new('<p>{{example}}</p>', :escape => :html)
225
+
226
+ assert_equal '<p>&lt;strong&gt;</p>', template.render('<strong>')
227
+
228
+ serialized_template = Marshal.dump(template)
229
+
230
+ deserialized_template = Marshal.load(serialized_template)
231
+
232
+ assert_equal '<p>&lt;strong&gt;</p>', deserialized_template.render('<strong>')
233
+ end
234
+
235
+ def test_with_broken_input
236
+ template = Feather::Template.new('{{}}')
237
+
238
+ assert_equal '', template.render
239
+ assert_equal '', template.render(nil)
240
+ assert_equal '', template.render(nil => nil)
241
+
242
+ template = Feather::Template.new('{{test}}')
243
+
244
+ assert_equal '', template.render(nil => nil)
245
+ end
246
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: feather
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Scott Tadman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: jeweler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: A simple light-weight text templating system
31
+ email: github@tadman.ca
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files:
35
+ - LICENSE.txt
36
+ - README.md
37
+ files:
38
+ - .document
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - VERSION
43
+ - lib/feather.rb
44
+ - lib/feather/support.rb
45
+ - lib/feather/template.rb
46
+ - notes/example.ft
47
+ - test/helper.rb
48
+ - test/test_feather.rb
49
+ - test/test_feather_support.rb
50
+ - test/test_feather_template.rb
51
+ homepage: http://github.com/twg/feather
52
+ licenses:
53
+ - MIT
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.24
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Light-weight text tempating system
76
+ test_files: []