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