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 ADDED
@@ -0,0 +1,2 @@
1
+ == This is a Really Cool Ruby Thing
2
+ === ( That deserves better documentation than this. )
@@ -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
@@ -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
+