feather 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/LICENSE.txt +20 -0
- data/README.md +52 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/lib/feather.rb +20 -0
- data/lib/feather/support.rb +73 -0
- data/lib/feather/template.rb +295 -0
- data/notes/example.ft +30 -0
- data/test/helper.rb +20 -0
- data/test/test_feather.rb +18 -0
- data/test/test_feather_support.rb +47 -0
- data/test/test_feather_template.rb +246 -0
- metadata +76 -0
data/.document
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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 <em>blue</em>"
|
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 <b>goose</b> 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&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
|
+
|
data/Rakefile
ADDED
@@ -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
|
data/lib/feather.rb
ADDED
@@ -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
|
data/notes/example.ft
ADDED
@@ -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.
|
data/test/helper.rb
ADDED
@@ -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 '<strong>', 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 '<example><text>', 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><strong></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><strong></p>', template.render('<strong>')
|
215
|
+
|
216
|
+
serialized_template = YAML.dump(template)
|
217
|
+
|
218
|
+
deserialized_template = YAML.load(serialized_template)
|
219
|
+
|
220
|
+
assert_equal '<p><strong></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><strong></p>', template.render('<strong>')
|
227
|
+
|
228
|
+
serialized_template = Marshal.dump(template)
|
229
|
+
|
230
|
+
deserialized_template = Marshal.load(serialized_template)
|
231
|
+
|
232
|
+
assert_equal '<p><strong></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: []
|