Stencil 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+