Stencil 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/doc/README +2 -0
- data/doc/Specifications +83 -0
- data/lib/stencil/directives.rb +1 -0
- data/lib/stencil/directives/base.rb +335 -0
- data/lib/stencil/directives/term-style.rb +127 -0
- data/lib/stencil/directives/text.rb +62 -0
- data/lib/stencil/dynamic-template.rb +98 -0
- data/lib/stencil/processor.rb +35 -0
- data/lib/stencil/spec/view_matcher.rb +146 -0
- data/lib/stencil/template.rb +269 -0
- data/lib/stencil/view.rb +133 -0
- metadata +70 -0
data/doc/README
ADDED
data/doc/Specifications
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
|
2
|
+
Stencil::Template parsing an invalid template
|
3
|
+
- should indicate the line and column of the error
|
4
|
+
|
5
|
+
Stencil::RenderError
|
6
|
+
- should has a useful string representation
|
7
|
+
|
8
|
+
Stencil::Template with text directives to wrap lines
|
9
|
+
- should transparently pass strings that don't need wrapping
|
10
|
+
- should wrap long strings to width
|
11
|
+
- should not re-wrap hard newlines
|
12
|
+
- should not choke on unwrappable lines
|
13
|
+
|
14
|
+
Stencil::Template with text directives to indent lines
|
15
|
+
- should indent a single line
|
16
|
+
- should indent multiple lines
|
17
|
+
|
18
|
+
Stencil::Template with text directives to insert newlines
|
19
|
+
- should insert a newline
|
20
|
+
|
21
|
+
Stencil::ViewMatcher
|
22
|
+
- should succeed with complex subview
|
23
|
+
- should fail when structure is wrong
|
24
|
+
- should fail when value doesn't match
|
25
|
+
- should fail when matcher doesn't match
|
26
|
+
|
27
|
+
Stencil::Template from a file
|
28
|
+
- should sing and dance
|
29
|
+
|
30
|
+
Stencil::Template with a file accessor and include
|
31
|
+
- should work just fine
|
32
|
+
|
33
|
+
Stencil::Template with simple value reference
|
34
|
+
- should render numbers
|
35
|
+
- should render strings
|
36
|
+
|
37
|
+
Stencil::Template with a deep value reference
|
38
|
+
- should render numbers
|
39
|
+
|
40
|
+
Stencil::Template with a simple expression
|
41
|
+
- should render numbers
|
42
|
+
|
43
|
+
Stencil::Template with a 'with' directive
|
44
|
+
- should render numbers
|
45
|
+
|
46
|
+
Stencil::Template with a 'with' directive and a relative path
|
47
|
+
- should render numbers
|
48
|
+
- should render empty string if target is missing
|
49
|
+
|
50
|
+
Stencil::Template with one list
|
51
|
+
- should render combinations
|
52
|
+
|
53
|
+
Stencil::Template with two lists
|
54
|
+
- should render combinations
|
55
|
+
|
56
|
+
Stencil::Template with a with-guarded list
|
57
|
+
- should render an array
|
58
|
+
|
59
|
+
Stencil::Template with an if block
|
60
|
+
- should render true case
|
61
|
+
- should render elseif case
|
62
|
+
- should render false case
|
63
|
+
- should raise error on missing case
|
64
|
+
|
65
|
+
Stencil::Template nicely printing a deep list
|
66
|
+
- should render list correctly
|
67
|
+
|
68
|
+
Stencil::Template with whitespace absorbtion
|
69
|
+
- should ignore extra whitespace in tags
|
70
|
+
|
71
|
+
Stencil::DynamicTemplate for JSON
|
72
|
+
- should render correctly
|
73
|
+
|
74
|
+
Stencil::View
|
75
|
+
- should render the correct viewset
|
76
|
+
|
77
|
+
Stencil::TermStyleDirective
|
78
|
+
- should change foreground color
|
79
|
+
- should nest stylings
|
80
|
+
|
81
|
+
Finished in 0.054271 seconds
|
82
|
+
|
83
|
+
35 examples, 0 failures
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'stencil/directives/base'
|
@@ -0,0 +1,335 @@
|
|
1
|
+
require 'stencil/processor'
|
2
|
+
module Stencil
|
3
|
+
class Directive
|
4
|
+
class Source
|
5
|
+
def initialize(file, line, column)
|
6
|
+
@file, @line, @column = file, line, column
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :file, :line, :column
|
10
|
+
|
11
|
+
def inspect
|
12
|
+
"#{@file}:#{@line},#{@column}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def exception(based_on=nil)
|
16
|
+
message = case based_on
|
17
|
+
when String
|
18
|
+
based_on
|
19
|
+
when ::Exception
|
20
|
+
[based_on.class, based_on.message, based_on.backtrace.first].join(": ")
|
21
|
+
else
|
22
|
+
"resulting from #{based_on}.inspect"
|
23
|
+
end
|
24
|
+
|
25
|
+
re = RenderError.new(message)
|
26
|
+
re.line = @line
|
27
|
+
re.column = @column
|
28
|
+
re.file = @file
|
29
|
+
re
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
@@directives = {}
|
34
|
+
|
35
|
+
def self.create(location, string)
|
36
|
+
string = string.sub(/^\s*/,"").sub(/\s*$/,"")
|
37
|
+
name, arguments = string.split(/\s+/, 2)
|
38
|
+
klass = @@directives[name]
|
39
|
+
raise "Unrecognized directive: #{name}" if klass.nil?
|
40
|
+
return klass.new(location, arguments)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.register(as)
|
44
|
+
@@directives[as.to_s] = self
|
45
|
+
end
|
46
|
+
|
47
|
+
def interpret(code, data)
|
48
|
+
env = EvaluationHost.new(data)
|
49
|
+
return env.run(code)
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(source, string)
|
53
|
+
@source = source
|
54
|
+
setup_parameters(string)
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_parameters(string)
|
58
|
+
end
|
59
|
+
|
60
|
+
def inspect
|
61
|
+
"[#{self.class.name.split("::").last} #{inspect_args}]"
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect_args
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def parsed(stack)
|
69
|
+
stack.last.add(self)
|
70
|
+
end
|
71
|
+
|
72
|
+
def prerender(state)
|
73
|
+
end
|
74
|
+
|
75
|
+
def checked_render(state)
|
76
|
+
begin
|
77
|
+
render(state)
|
78
|
+
rescue RenderError
|
79
|
+
raise
|
80
|
+
rescue Object => ex
|
81
|
+
raise @source, ex
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def render(data)
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def postrender(state)
|
90
|
+
end
|
91
|
+
|
92
|
+
def pre_end(state)
|
93
|
+
end
|
94
|
+
|
95
|
+
def render_end(data)
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Literal < Directive
|
101
|
+
def setup_parameters(string)
|
102
|
+
@value = string
|
103
|
+
end
|
104
|
+
|
105
|
+
def render(data)
|
106
|
+
@value
|
107
|
+
end
|
108
|
+
|
109
|
+
def inspect_args
|
110
|
+
@value.inspect
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class Apply < Directive
|
115
|
+
register "apply"
|
116
|
+
|
117
|
+
def setup_parameters(string)
|
118
|
+
@type, @path = string.split(" ")
|
119
|
+
end
|
120
|
+
|
121
|
+
def render(state)
|
122
|
+
state.data.push_context(@path)
|
123
|
+
result = DynamicTemplate::formatter(@type).render_raw(state.data)
|
124
|
+
state.data.pop_context
|
125
|
+
return result
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class Include < Directive
|
130
|
+
register "include"
|
131
|
+
|
132
|
+
def setup_parameters(string)
|
133
|
+
@template_path, @data_path = string.split(" ")
|
134
|
+
end
|
135
|
+
|
136
|
+
def render(state)
|
137
|
+
template = state.get_template(@template_path)
|
138
|
+
state.data.with_context(@data_path) do |data|
|
139
|
+
template.render_raw(data)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class Interpolate < Directive
|
145
|
+
register "="
|
146
|
+
|
147
|
+
def setup_parameters(string)
|
148
|
+
@code = string || "@"
|
149
|
+
end
|
150
|
+
|
151
|
+
def render(state)
|
152
|
+
return interpret(@code, state.data)
|
153
|
+
end
|
154
|
+
|
155
|
+
def inspect_args
|
156
|
+
@path
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class Block < Directive
|
161
|
+
def initialize(location, string)
|
162
|
+
super
|
163
|
+
@apply = []
|
164
|
+
end
|
165
|
+
|
166
|
+
def inspect
|
167
|
+
"[#{self.class.name.split("::").last} #{inspect_args} {#{@apply.map{|dr| dr.inspect}.join(" ")}}]"
|
168
|
+
end
|
169
|
+
|
170
|
+
def add(directive)
|
171
|
+
@apply << directive
|
172
|
+
end
|
173
|
+
|
174
|
+
def parsed(stack)
|
175
|
+
super
|
176
|
+
stack << self
|
177
|
+
end
|
178
|
+
|
179
|
+
def render(state)
|
180
|
+
@apply.map do |directive|
|
181
|
+
directive.checked_render(state)
|
182
|
+
end.flatten.compact
|
183
|
+
end
|
184
|
+
|
185
|
+
def ended(stack)
|
186
|
+
stack.pop
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class With < Block
|
191
|
+
register :with
|
192
|
+
|
193
|
+
def setup_parameters(path)
|
194
|
+
@path = path
|
195
|
+
end
|
196
|
+
|
197
|
+
def inspect_args
|
198
|
+
@path
|
199
|
+
end
|
200
|
+
|
201
|
+
def render(state)
|
202
|
+
if state.data.access(@path).nil?
|
203
|
+
return []
|
204
|
+
else
|
205
|
+
state.data.push_context(@path)
|
206
|
+
res = super
|
207
|
+
state.data.pop_context
|
208
|
+
|
209
|
+
return res
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class Each < Block
|
215
|
+
register :each
|
216
|
+
|
217
|
+
|
218
|
+
#XXX what if @list doesn't point to an array?
|
219
|
+
def setup_parameters(string)
|
220
|
+
@path, @named_context = string.split(" ", 2)
|
221
|
+
end
|
222
|
+
|
223
|
+
def intercept(directive)
|
224
|
+
@apply << directive
|
225
|
+
nil
|
226
|
+
end
|
227
|
+
|
228
|
+
def inspect_args
|
229
|
+
"#@path => #@named_context"
|
230
|
+
end
|
231
|
+
|
232
|
+
def render(state)
|
233
|
+
data = state.data
|
234
|
+
list = data.access(@path)
|
235
|
+
data.push_context(@path, @named_context)
|
236
|
+
data.push_context(@path)
|
237
|
+
result = (0...list.length).map do |index|
|
238
|
+
data.push_context("##{index}", @named_context)
|
239
|
+
data.push_context("##{index}")
|
240
|
+
|
241
|
+
res = super
|
242
|
+
|
243
|
+
data.pop_context(@named_context)
|
244
|
+
data.pop_context
|
245
|
+
res
|
246
|
+
end.flatten.compact
|
247
|
+
data.pop_context(@named_context)
|
248
|
+
data.pop_context
|
249
|
+
|
250
|
+
result
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
class If < Block
|
255
|
+
register :if
|
256
|
+
|
257
|
+
def setup_parameters(code)
|
258
|
+
@code = code
|
259
|
+
@truth = nil
|
260
|
+
end
|
261
|
+
attr_reader :truth
|
262
|
+
|
263
|
+
def inspect_args
|
264
|
+
@code
|
265
|
+
end
|
266
|
+
|
267
|
+
def determine_truth(data)
|
268
|
+
@truth = interpret(@code, data)
|
269
|
+
return @truth
|
270
|
+
end
|
271
|
+
|
272
|
+
def render(state)
|
273
|
+
if determine_truth(state.data)
|
274
|
+
return super
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class Else < If
|
280
|
+
register :else
|
281
|
+
register :elif
|
282
|
+
|
283
|
+
def setup_parameters(code)
|
284
|
+
super
|
285
|
+
@previous = nil
|
286
|
+
@a_truth = false
|
287
|
+
end
|
288
|
+
|
289
|
+
def truth
|
290
|
+
@a_truth
|
291
|
+
end
|
292
|
+
|
293
|
+
def determine_truth(data)
|
294
|
+
if @previous.truth
|
295
|
+
@a_truth = true
|
296
|
+
@truth = false
|
297
|
+
else
|
298
|
+
if @code.nil?
|
299
|
+
@truth = true
|
300
|
+
else
|
301
|
+
@truth = interpret(@code, data)
|
302
|
+
end
|
303
|
+
@a_truth = @truth
|
304
|
+
end
|
305
|
+
@truth
|
306
|
+
end
|
307
|
+
|
308
|
+
def parsed(stack)
|
309
|
+
@previous = stack.last
|
310
|
+
unless If === @previous
|
311
|
+
raise @source, "else has to follow if"
|
312
|
+
end
|
313
|
+
@previous.ended(stack)
|
314
|
+
super
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
class End < Directive
|
320
|
+
register :end
|
321
|
+
register "/"
|
322
|
+
|
323
|
+
def setup_parameters(arg)
|
324
|
+
@ended = nil
|
325
|
+
@count = arg.to_i
|
326
|
+
@count = 1 if @count < 1
|
327
|
+
end
|
328
|
+
|
329
|
+
def parsed(stack)
|
330
|
+
@count.times do
|
331
|
+
stack.last.ended(stack)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Stencil
|
2
|
+
module Styles
|
3
|
+
Foreground = {
|
4
|
+
'black' => 30,
|
5
|
+
'red' => 31,
|
6
|
+
'green' => 32,
|
7
|
+
'yellow' => 33,
|
8
|
+
'blue' => 34,
|
9
|
+
'magenta' => 35,
|
10
|
+
'cyan' => 36,
|
11
|
+
'white' => 37,
|
12
|
+
'default' => 39
|
13
|
+
}
|
14
|
+
|
15
|
+
Background = {}
|
16
|
+
|
17
|
+
Foreground.each() do |name, value|
|
18
|
+
Background[name] = value + 10
|
19
|
+
end
|
20
|
+
|
21
|
+
Extra = {
|
22
|
+
'clear' => 0,
|
23
|
+
'bold' => 1,
|
24
|
+
'unbold' => 22,
|
25
|
+
'underline' => 4,
|
26
|
+
'ununderline' => 24,
|
27
|
+
'blink' => 5,
|
28
|
+
'unblink' => 25,
|
29
|
+
'inverse' => 7,
|
30
|
+
'uninverse' => 27
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
class TermStyleDirective < Block
|
35
|
+
def initialize(location, string)
|
36
|
+
@previous_style = nil
|
37
|
+
@style = nil
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def code_for(kind, name)
|
42
|
+
if kind.has_key?(name.to_s)
|
43
|
+
"\e[#{kind[name.to_s]}m"
|
44
|
+
else
|
45
|
+
""
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def escape_codes(options)
|
50
|
+
return "\e[0m" if options.empty?
|
51
|
+
return code_for(Styles::Foreground, options[:foreground]) +
|
52
|
+
code_for(Styles::Background, options[:background]) +
|
53
|
+
code_for(Styles::Extra, options[:extra])
|
54
|
+
end
|
55
|
+
|
56
|
+
def parsed(stack)
|
57
|
+
reverse = Enumerable::Enumerator::new(stack, :reverse_each)
|
58
|
+
@previous_style = reverse.find {|dir| TermStyleDirective === dir}
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
def old_style(state)
|
63
|
+
@old_style ||= @previous_style.style(state) rescue {:foreground => 'default', :background => 'default'}
|
64
|
+
end
|
65
|
+
|
66
|
+
def style(state)
|
67
|
+
@style ||= update_style(old_style(state), state.data)
|
68
|
+
end
|
69
|
+
|
70
|
+
def render(state)
|
71
|
+
[escape_codes(style(state))] + super + [escape_codes(old_style(state))]
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_style(style, state)
|
75
|
+
return style
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class ForegroundColor < TermStyleDirective
|
80
|
+
register :fcolor
|
81
|
+
def setup_parameters(color)
|
82
|
+
@color_text = color
|
83
|
+
end
|
84
|
+
|
85
|
+
def update_style(style, state)
|
86
|
+
return style.merge(:foreground => interpret(@color_text, state))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Bold < TermStyleDirective
|
91
|
+
register :bold
|
92
|
+
|
93
|
+
def render(state)
|
94
|
+
[code_for(Styles::Extra, 'bold')] +
|
95
|
+
super +
|
96
|
+
[code_for(Styles::Extra, 'unbold')]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Underline < TermStyleDirective
|
101
|
+
register :underline
|
102
|
+
|
103
|
+
def render(state)
|
104
|
+
[code_for(Styles::Extra, 'underline')] +
|
105
|
+
super +
|
106
|
+
[code_for(Styles::Extra, 'ununderline')]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Inverse < TermStyleDirective
|
111
|
+
register :inverse
|
112
|
+
|
113
|
+
|
114
|
+
def render(state)
|
115
|
+
[code_for(Styles::Extra, 'inverse')] +
|
116
|
+
super +
|
117
|
+
[code_for(Styles::Extra, 'uninverse')]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class BackgroundColor < ForegroundColor
|
122
|
+
register :bcolor
|
123
|
+
def update_style(style, state)
|
124
|
+
return style.merge(:background => interpret(@color_text, state))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
module Stencil
|
3
|
+
class Newline < Literal
|
4
|
+
register "nl"
|
5
|
+
def setup_parameters(dummy)
|
6
|
+
super("\n")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Wrap < Block
|
11
|
+
register "wrap"
|
12
|
+
def setup_parameters(width)
|
13
|
+
@width_text = width
|
14
|
+
end
|
15
|
+
|
16
|
+
def render(state)
|
17
|
+
env = EvaluationHost.new(state.data)
|
18
|
+
width = env.run(@width_text)
|
19
|
+
res = super.compact.join("")
|
20
|
+
scanner = StringScanner.new(res)
|
21
|
+
scanner.skip(/\s*/m)
|
22
|
+
|
23
|
+
result = []
|
24
|
+
line = scanner.scan(/\S+/m) || ""
|
25
|
+
|
26
|
+
until scanner.eos?
|
27
|
+
white = scanner.scan(/\s*/m)
|
28
|
+
white.gsub(/\n/) do |nl|
|
29
|
+
result << line + "\n"
|
30
|
+
line = ""
|
31
|
+
end
|
32
|
+
word = scanner.scan(/\S+/m)
|
33
|
+
next if word.nil?
|
34
|
+
if word.length > (width - line.length)
|
35
|
+
result << line + "\n"
|
36
|
+
line = word
|
37
|
+
else
|
38
|
+
line << (" " + word)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
result << line
|
43
|
+
|
44
|
+
return result
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Indent < Block
|
49
|
+
register "indent"
|
50
|
+
|
51
|
+
def setup_parameters(spaces)
|
52
|
+
@space_text = spaces
|
53
|
+
end
|
54
|
+
|
55
|
+
def render(state)
|
56
|
+
spaces = " " * interpret(@space_text, state.data)
|
57
|
+
res = super.compact.join("")
|
58
|
+
res = spaces + res.gsub(/\n(?!$)/, "\n" + spaces)
|
59
|
+
return res
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'stencil/template'
|
2
|
+
|
3
|
+
module Stencil
|
4
|
+
class DynamicTemplate < Template
|
5
|
+
@@formatters = Hash.new
|
6
|
+
def self.formatter(kind, &file_opener)
|
7
|
+
file_opener ||= DefaultFileOpener
|
8
|
+
formatter = @@formatters[kind.to_s]
|
9
|
+
if Class === formatter
|
10
|
+
formatter = formatter.new(&file_opener)
|
11
|
+
@@formatters[kind] = formatter
|
12
|
+
end
|
13
|
+
formatter
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.register(kind)
|
17
|
+
@@formatters[kind.to_s] = self
|
18
|
+
define_method(:my_name){kind.to_s}
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(&file_opener)
|
22
|
+
@hash_template = parse(*self.class.hash_template)
|
23
|
+
@array_template = parse(*self.class.array_template)
|
24
|
+
@item_template = parse(*self.class.item_template)
|
25
|
+
@file_opener = file_opener
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse(string, file, line)
|
29
|
+
super(string.gsub(/\[;\s*reapply/, "[;apply #{my_name}"), file, line)
|
30
|
+
end
|
31
|
+
|
32
|
+
def array_render(data)
|
33
|
+
state = State.new(data, &@file_opener)
|
34
|
+
res = render_with(state, @array_template)
|
35
|
+
return res
|
36
|
+
end
|
37
|
+
|
38
|
+
def hash_render(data)
|
39
|
+
pairs = data.access("").map{|k,v| {"key" => k, "value" => v}}
|
40
|
+
state = State.new(data, &@file_opener)
|
41
|
+
state.data.inject_dataset("pair", pairs)
|
42
|
+
render_with(state, @hash_template)
|
43
|
+
end
|
44
|
+
|
45
|
+
def item_render(data)
|
46
|
+
state = State.new(data, &@file_opener)
|
47
|
+
res = render_with(state, @item_template)
|
48
|
+
end
|
49
|
+
|
50
|
+
def render_raw(data)
|
51
|
+
res = nil
|
52
|
+
case data.access("")
|
53
|
+
when Array
|
54
|
+
res = array_render(data)
|
55
|
+
when Hash
|
56
|
+
res = hash_render(data)
|
57
|
+
else
|
58
|
+
res = item_render(data)
|
59
|
+
end
|
60
|
+
return res
|
61
|
+
end
|
62
|
+
|
63
|
+
class << self
|
64
|
+
attr_reader :hash_template, :array_template, :item_template
|
65
|
+
|
66
|
+
def line_and_file
|
67
|
+
m = %r{^([^:]+):(\d+)}.match caller[1]
|
68
|
+
if m.nil?
|
69
|
+
return []
|
70
|
+
else
|
71
|
+
return [m[1], m[2].to_i]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def list(string)
|
76
|
+
@array_template = [string] + line_and_file
|
77
|
+
end
|
78
|
+
|
79
|
+
def hash(string)
|
80
|
+
@hash_template = [string] + line_and_file
|
81
|
+
end
|
82
|
+
|
83
|
+
def item(string)
|
84
|
+
@item_template = [string] + line_and_file
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class JSONTemplate < DynamicTemplate
|
90
|
+
register :json
|
91
|
+
|
92
|
+
hash "{[;nl;][;each pair h;][;indent 2;][;= @h:key;]: [;reapply @h:value;][;if @h+1;], [;/;][;/;][;nl;][;/;]}"
|
93
|
+
|
94
|
+
list "[ [;each @ i;][;reapply @i;][;if @i+1;], [;/;][;/;] ]"
|
95
|
+
|
96
|
+
item "[;=;]"
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
module Stencil
|
3
|
+
class EvaluationHost
|
4
|
+
remove_methods = self.instance_methods(true)
|
5
|
+
remove_methods -= %w{binding extend}
|
6
|
+
old_verbose = $VERBOSE
|
7
|
+
$VERBOSE = nil
|
8
|
+
remove_methods.each do |method|
|
9
|
+
undef_method(method)
|
10
|
+
end
|
11
|
+
$VERBOSE = old_verbose
|
12
|
+
|
13
|
+
remove_constants = constants
|
14
|
+
remove_constants.each do |const|
|
15
|
+
remove_const(const)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.const_missing(name)
|
19
|
+
raise "Access to constants (including classes) is not allowed in template processing"
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(data)
|
23
|
+
@data = data
|
24
|
+
end
|
25
|
+
|
26
|
+
def val(path)
|
27
|
+
@data.access(path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def run(code)
|
31
|
+
code = code.gsub(/@([^\s.]*)/){|m| "val('#{$1}')"}
|
32
|
+
return ::Kernel::eval(code, binding)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Kernel
|
2
|
+
def at_path(*args)
|
3
|
+
Stencil::ViewMatcher::AtPath::Expectation.new(args, self)
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
module Stencil
|
8
|
+
module ViewMatcher
|
9
|
+
def have_subview(view)
|
10
|
+
return SubviewMatcher.new(view)
|
11
|
+
end
|
12
|
+
|
13
|
+
class SubviewMatcher
|
14
|
+
def initialize(subview)
|
15
|
+
@subview = subview
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(view)
|
19
|
+
@view = view
|
20
|
+
create_submatches([], @subview)
|
21
|
+
return true
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_submatches(path, subview)
|
25
|
+
case subview
|
26
|
+
when Hash
|
27
|
+
subview.each_pair do |name, value|
|
28
|
+
create_submatches(path + [name], value)
|
29
|
+
end
|
30
|
+
when Array
|
31
|
+
subview.each_with_index do |value, index|
|
32
|
+
create_submatches(path + [index], value)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
if subview.respond_to?(:matches?)
|
36
|
+
@view.at_path(*path).should(subview)
|
37
|
+
else
|
38
|
+
@view.at_path(*path).should == subview
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module AtPath
|
45
|
+
class PositiveOperatorMatcher < ::Spec::Matchers::PositiveOperatorMatcher
|
46
|
+
def initialize(path, actual)
|
47
|
+
@path = path
|
48
|
+
super(actual)
|
49
|
+
end
|
50
|
+
|
51
|
+
['==', '===', '=~', '>', '>=', '<', '<='].each do |operator|
|
52
|
+
define_method(operator) do |expected|
|
53
|
+
begin
|
54
|
+
super
|
55
|
+
rescue ::Spec::Expectations::ExpectationNotMetError => enme
|
56
|
+
message = "At #{@path.inspect}\n" + enme.message
|
57
|
+
Kernel::raise(Spec::Expectations::ExpectationNotMetError.new(message))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class NegativeOperatorMatcher < ::Spec::Matchers::NegativeOperatorMatcher
|
64
|
+
def initialize(path, actual)
|
65
|
+
@path = path
|
66
|
+
super(actual)
|
67
|
+
end
|
68
|
+
|
69
|
+
['==', '===', '=~', '>', '>=', '<', '<='].each do |operator|
|
70
|
+
define_method(operator) do |expected|
|
71
|
+
begin
|
72
|
+
super
|
73
|
+
rescue ::Spec::Expectations::ExpectationNotMetError => enme
|
74
|
+
message = "At #{@path.inspect}\n" + enme.message
|
75
|
+
Kernel::raise(Spec::Expectations::ExpectationNotMetError.new(message))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Expectation
|
82
|
+
def initialize(path, actual)
|
83
|
+
@path, @actual = path, actual
|
84
|
+
@thumb = @actual
|
85
|
+
@into = []
|
86
|
+
end
|
87
|
+
|
88
|
+
def should(matcher=nil, &block)
|
89
|
+
dereference
|
90
|
+
if matcher.nil?
|
91
|
+
::Spec::Matchers.last_should = :should
|
92
|
+
::Spec::Matchers.last_matcher = matcher
|
93
|
+
return PositiveOperatorMatcher.new(@path, @thumb)
|
94
|
+
end
|
95
|
+
|
96
|
+
begin
|
97
|
+
Spec::Expectations::PositiveExpectationHandler.handle_matcher(@thumb, matcher, &block)
|
98
|
+
rescue ::Spec::Expectations::ExpectationNotMetError => enme
|
99
|
+
message = "At #{@path.inspect}\n" + enme.message
|
100
|
+
Kernel::raise(Spec::Expectations::ExpectationNotMetError.new(message))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def should_not(matcher=nil, &block)
|
105
|
+
dereference
|
106
|
+
|
107
|
+
if matcher.nil?
|
108
|
+
::Spec::Matchers.last_should = :should_not
|
109
|
+
::Spec::Matchers.last_matcher = matcher
|
110
|
+
return NegativeOperatorMatcher.new(@path, @thumb)
|
111
|
+
end
|
112
|
+
|
113
|
+
begin
|
114
|
+
Spec::Expectations::NegativeExpectationHandler.handle_matcher(@thumb, matcher, &block)
|
115
|
+
rescue ::Spec::Expectations::ExpectationNotMetError => enme
|
116
|
+
message = "At #{@path.inspect}\n" + enme.message
|
117
|
+
Kernel::raise(Spec::Expectations::ExpectationNotMetError.new(message))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def dereference
|
122
|
+
@path.each do |segment|
|
123
|
+
@into << segment
|
124
|
+
case @thumb
|
125
|
+
when Array
|
126
|
+
unless Integer === segment
|
127
|
+
deref_fail
|
128
|
+
end
|
129
|
+
when Hash
|
130
|
+
unless @thumb.has_key?(segment)
|
131
|
+
deref_fail
|
132
|
+
end
|
133
|
+
else
|
134
|
+
deref_fail
|
135
|
+
end
|
136
|
+
@thumb = @thumb[segment] rescue deref_fail
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def deref_fail
|
141
|
+
::Spec::Expectations.fail_with("Member: #{@thumb.class.name} at (#{@into[0..-2].inspect}) has no index #{@into.last.inspect}")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,269 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
require 'stencil/processor'
|
3
|
+
require 'stencil/directives'
|
4
|
+
|
5
|
+
module Stencil
|
6
|
+
class Exception < ::Exception; end
|
7
|
+
class RenderError < Exception
|
8
|
+
attr_accessor :line, :column, :file, :based_on
|
9
|
+
|
10
|
+
def message
|
11
|
+
super + " at: #@file:#@line,#@column"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
class ParseError < Exception; end
|
15
|
+
|
16
|
+
|
17
|
+
class Data
|
18
|
+
def initialize(data)
|
19
|
+
@data = data
|
20
|
+
@path = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def relative(string)
|
24
|
+
path = @path.dup
|
25
|
+
s = StringScanner.new(string||"")
|
26
|
+
|
27
|
+
goto = s.scan(/[^:#]*/)
|
28
|
+
g = StringScanner.new(goto)
|
29
|
+
|
30
|
+
while g.rest?
|
31
|
+
go = g.getch
|
32
|
+
|
33
|
+
case go
|
34
|
+
when "^"
|
35
|
+
path.pop
|
36
|
+
when "/"
|
37
|
+
path.clear
|
38
|
+
when "+"
|
39
|
+
number = g.scan(/\d+/).to_i
|
40
|
+
here = path.pop
|
41
|
+
path.push(here + number)
|
42
|
+
when "-"
|
43
|
+
number = g.scan(/\d+/).to_i
|
44
|
+
here = path.pop
|
45
|
+
path.push(here - number)
|
46
|
+
else
|
47
|
+
raise "Unrecognized path offset: #{go}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
while s.rest?
|
52
|
+
delim = s.getch
|
53
|
+
segment = s.scan(/[^:#]*/)
|
54
|
+
case delim
|
55
|
+
when ":"
|
56
|
+
path << segment
|
57
|
+
when "#"
|
58
|
+
path << segment.to_i
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
path
|
63
|
+
end
|
64
|
+
|
65
|
+
def recontext(string)
|
66
|
+
result = Data.new(@data)
|
67
|
+
result.path = relative(string)
|
68
|
+
return result
|
69
|
+
end
|
70
|
+
|
71
|
+
attr_accessor :data, :point, :path
|
72
|
+
|
73
|
+
def access(string)
|
74
|
+
data = @data
|
75
|
+
path = relative(string)
|
76
|
+
path.each do |segment|
|
77
|
+
data = data[segment]
|
78
|
+
end
|
79
|
+
return data
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
class Template
|
85
|
+
class DataContext
|
86
|
+
def initialize(data)
|
87
|
+
@datasets = Hash.new{|h,k| h[k]=[]}
|
88
|
+
@datasets[nil] << Data.new(data)
|
89
|
+
end
|
90
|
+
|
91
|
+
def context_name(path)
|
92
|
+
m = /^@?([a-z]*)/.match(path)
|
93
|
+
which = m[1]
|
94
|
+
which = nil if which.empty?
|
95
|
+
path = m.post_match
|
96
|
+
return which,path
|
97
|
+
end
|
98
|
+
|
99
|
+
def push_context(path, onto=nil)
|
100
|
+
which, path = context_name(path)
|
101
|
+
@datasets[onto] << @datasets[which].last.recontext(path)
|
102
|
+
end
|
103
|
+
|
104
|
+
def pop_context(from=nil)
|
105
|
+
@datasets[from].pop
|
106
|
+
end
|
107
|
+
|
108
|
+
def with_context(path, onto = nil)
|
109
|
+
push_context(path, onto)
|
110
|
+
result = yield(self)
|
111
|
+
pop_context(onto)
|
112
|
+
return result
|
113
|
+
end
|
114
|
+
|
115
|
+
def inject_dataset(name, dataset)
|
116
|
+
@datasets[name] << Data.new(dataset)
|
117
|
+
end
|
118
|
+
|
119
|
+
def access(path)
|
120
|
+
which, path = context_name(path)
|
121
|
+
@datasets[which].last.access(path)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class State
|
126
|
+
def initialize(data, &file_opener)
|
127
|
+
@data = data
|
128
|
+
@file_opener = file_opener
|
129
|
+
end
|
130
|
+
|
131
|
+
def get_template(path)
|
132
|
+
Template::file(path, &@file_opener)
|
133
|
+
end
|
134
|
+
|
135
|
+
attr_accessor :data
|
136
|
+
end
|
137
|
+
|
138
|
+
DefaultFileOpener = proc do |path|
|
139
|
+
File.open(path) do |file|
|
140
|
+
file.read
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
class << self
|
146
|
+
def string(data, file=nil, line=nil, &file_open)
|
147
|
+
m = %r{^([^:]+):(\d+)}.match caller[0]
|
148
|
+
file ||= m[1] unless m.nil?
|
149
|
+
line ||= m[2].to_i unless m.nil?
|
150
|
+
|
151
|
+
file ||= ""
|
152
|
+
line ||= 1
|
153
|
+
template = self.new(data, file, line)
|
154
|
+
template.file_opener = file_open unless file_open.nil?
|
155
|
+
return template
|
156
|
+
end
|
157
|
+
|
158
|
+
def file(path, &file_open)
|
159
|
+
file_open ||= DefaultFileOpener
|
160
|
+
template = self.new(file_open[path], path)
|
161
|
+
template.file_opener = file_open
|
162
|
+
return template
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def initialize(template, file="", line=1, column=1)
|
167
|
+
@template = parse(template, file, line, column)
|
168
|
+
@file_opener = DefaultFileOpener
|
169
|
+
end
|
170
|
+
|
171
|
+
class DocumentScanner
|
172
|
+
def initialize(string, file="", line=1, column=1)
|
173
|
+
@file = file
|
174
|
+
@begin_line, @begin_column = line,column
|
175
|
+
@end_line, @end_column = line,column
|
176
|
+
@scanner = StringScanner.new(string)
|
177
|
+
end
|
178
|
+
|
179
|
+
attr_accessor :file, :line, :col
|
180
|
+
|
181
|
+
def scan(pattern)
|
182
|
+
result = @scanner.scan(pattern)
|
183
|
+
return nil if result.nil?
|
184
|
+
|
185
|
+
lines = result.split("\n")
|
186
|
+
@begin_line = @end_line
|
187
|
+
@begin_column = @end_column
|
188
|
+
if lines.length > 1
|
189
|
+
@end_line += lines.length - 1
|
190
|
+
@end_column = lines.last.length + 1
|
191
|
+
else
|
192
|
+
@end_column += result.length
|
193
|
+
end
|
194
|
+
return result
|
195
|
+
end
|
196
|
+
|
197
|
+
def rest
|
198
|
+
@scanner.rest
|
199
|
+
end
|
200
|
+
|
201
|
+
def rest?
|
202
|
+
@scanner.rest?
|
203
|
+
end
|
204
|
+
|
205
|
+
def location
|
206
|
+
Directive::Source.new(@file, @begin_line, @begin_column)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
attr_writer :file_opener
|
210
|
+
|
211
|
+
BeginDirective = %r{\[;}m
|
212
|
+
EndDirective = %r{;\]}m
|
213
|
+
UntilDirective = %r{.*?(?=#{BeginDirective})}m
|
214
|
+
FinishDirective = %r{.*?(?=#{EndDirective})}m
|
215
|
+
|
216
|
+
|
217
|
+
def parse(template, file="", line=1, column=1)
|
218
|
+
scanner = DocumentScanner.new(template, file, line, column)
|
219
|
+
result = [Block.new(scanner.location, "")]
|
220
|
+
last_newline = 0
|
221
|
+
|
222
|
+
while scanner.rest?
|
223
|
+
literal = scanner.scan(UntilDirective)
|
224
|
+
|
225
|
+
unless literal.nil? or literal.empty?
|
226
|
+
result.last.add(Literal.new(scanner.location, literal))
|
227
|
+
end
|
228
|
+
|
229
|
+
delim = scanner.scan(BeginDirective)
|
230
|
+
|
231
|
+
if delim.nil?
|
232
|
+
result.last.add(Literal.new(scanner.location, scanner.rest))
|
233
|
+
break
|
234
|
+
end
|
235
|
+
|
236
|
+
directive = scanner.scan(FinishDirective)
|
237
|
+
|
238
|
+
directive = Directive.create(scanner.location, directive)
|
239
|
+
directive.parsed(result)
|
240
|
+
|
241
|
+
scanner.scan(EndDirective)
|
242
|
+
end
|
243
|
+
|
244
|
+
raise "Unterminated directives: #{result[1..-1].inspect}" unless result.length == 1
|
245
|
+
result.last
|
246
|
+
end
|
247
|
+
|
248
|
+
def render(data)
|
249
|
+
data_context = DataContext.new(data)
|
250
|
+
render_raw(data_context).compact.join("")
|
251
|
+
end
|
252
|
+
|
253
|
+
def render_raw(data)
|
254
|
+
state = State.new(data, &@file_opener)
|
255
|
+
render_with(state, @template)
|
256
|
+
end
|
257
|
+
|
258
|
+
def render_with(state, template)
|
259
|
+
begin
|
260
|
+
result = template.checked_render(state)
|
261
|
+
return result
|
262
|
+
rescue RenderError
|
263
|
+
raise
|
264
|
+
rescue Object => ex
|
265
|
+
raise RenderError, [ex.class, ex.message].join(" ")
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
data/lib/stencil/view.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
module Stencil
|
2
|
+
module ViewHost
|
3
|
+
class Renderer
|
4
|
+
def initialize(data, capsule)
|
5
|
+
@data = data
|
6
|
+
@capsule = capsule
|
7
|
+
end
|
8
|
+
|
9
|
+
def go
|
10
|
+
instance_eval(&@capsule.block)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :data
|
14
|
+
end
|
15
|
+
|
16
|
+
class EvalRenderer < Renderer
|
17
|
+
def go
|
18
|
+
@data.instance_eval(&@capsule.block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def view(mod = nil, &block)
|
23
|
+
render_class = EvalRenderer
|
24
|
+
unless mod.nil?
|
25
|
+
render_class = Class.new(EvalRenderer) do
|
26
|
+
include(mod)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
sub_view(render_class, &block)
|
30
|
+
end
|
31
|
+
module_function :view
|
32
|
+
|
33
|
+
def sub_view(render_class, &block)
|
34
|
+
template = ViewDefinition.create_template(render_class, &block)
|
35
|
+
return View.new(template)
|
36
|
+
end
|
37
|
+
module_function :sub_view
|
38
|
+
end
|
39
|
+
|
40
|
+
class ViewDefinition
|
41
|
+
def self.create_template(render_class, &block)
|
42
|
+
definer = self.new(render_class)
|
43
|
+
return definer.instance_eval(&block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(render_class)
|
47
|
+
@render_class = render_class
|
48
|
+
end
|
49
|
+
|
50
|
+
def item(&block)
|
51
|
+
Capsule.new(@render_class, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
def list(&block)
|
55
|
+
ListCapsule.new(@render_class, &block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Capsule
|
60
|
+
def initialize(render_class, &block)
|
61
|
+
@render_class = render_class
|
62
|
+
@block = block
|
63
|
+
end
|
64
|
+
attr_reader :block
|
65
|
+
|
66
|
+
def render(data)
|
67
|
+
@render_class.new(data, self).go
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class ListCapsule < Capsule
|
72
|
+
def initialize(render_class, &block)
|
73
|
+
super
|
74
|
+
@mapping = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def map(&block)
|
78
|
+
@mapping = ViewHost.sub_view(@render_class, &block)
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def render(data)
|
83
|
+
list = super
|
84
|
+
rendered = list.map do |item|
|
85
|
+
@mapping.viewset(item)
|
86
|
+
end
|
87
|
+
rendered
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class View
|
92
|
+
def initialize(template)
|
93
|
+
@template = template
|
94
|
+
end
|
95
|
+
|
96
|
+
attr_reader :template
|
97
|
+
|
98
|
+
def render_map(data, hash)
|
99
|
+
rendered = {}
|
100
|
+
hash.each_pair do |key, value|
|
101
|
+
rendered[key.to_s] = render_sub(data, value)
|
102
|
+
end
|
103
|
+
rendered
|
104
|
+
end
|
105
|
+
|
106
|
+
def render_list(data, list)
|
107
|
+
list.map do |item|
|
108
|
+
render_sub(data, item)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def render_item(data, item)
|
113
|
+
item
|
114
|
+
end
|
115
|
+
|
116
|
+
def render_sub(data, template)
|
117
|
+
case template
|
118
|
+
when Hash
|
119
|
+
render_map(data, template)
|
120
|
+
when Array
|
121
|
+
render_list(data, template)
|
122
|
+
when Capsule
|
123
|
+
render_sub(data, template.render(data))
|
124
|
+
else
|
125
|
+
render_item(data, template)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def viewset(data)
|
130
|
+
render_sub(data, @template)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: Stencil
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Judson Lester
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-28 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: " Stencil is a templating library with a number of design goals.\n\n * Limited code in templates. This isn't meant to embed ruby in anything - \n it allows for simple control structures, since that's typically what you need \n in a template, but full access to the Ruby\n interpreter is just a tempatation into sin. (From a separation of concerns \n standpoint.) There's a certain amount of code available in conditionals and \n interpolations, since otherwise they're much harder to do...\n\n * Easy to extend. If you do need something extra from a template, not \n having it in the templating language is frustrating. It's easy to add \n features to stencil, since they're described in as well-designed classes.\n\n * Generic output. Not everything is a website or a mime-encoded email. It's \n nice to be able to spit out generic text from time to time.\n\n * Data sourced from simple datatypes - hashes and array, referenced with data \n paths. Views can be extracted from any object, or built up in code.\n"
|
17
|
+
email: nyarly@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- doc/README
|
24
|
+
- doc/Specifications
|
25
|
+
files:
|
26
|
+
- lib/stencil/spec/view_matcher.rb
|
27
|
+
- lib/stencil/processor.rb
|
28
|
+
- lib/stencil/directives.rb
|
29
|
+
- lib/stencil/template.rb
|
30
|
+
- lib/stencil/dynamic-template.rb
|
31
|
+
- lib/stencil/directives/text.rb
|
32
|
+
- lib/stencil/directives/base.rb
|
33
|
+
- lib/stencil/directives/term-style.rb
|
34
|
+
- lib/stencil/view.rb
|
35
|
+
- doc/README
|
36
|
+
- doc/Specifications
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: http://stencil-templ.rubyforge.org/
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message: Another tidy package brought to you by Judson
|
42
|
+
rdoc_options:
|
43
|
+
- --inline-source
|
44
|
+
- --main
|
45
|
+
- doc/README
|
46
|
+
- --title
|
47
|
+
- Stencil-0.1 RDoc
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
requirements: []
|
63
|
+
|
64
|
+
rubyforge_project: stencil-templ
|
65
|
+
rubygems_version: 1.3.5
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Output agnostic templating engine
|
69
|
+
test_files: []
|
70
|
+
|