Stencil 0.1
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/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
|
+
|