feather 0.3.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.
@@ -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: []