cadenza 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,4 +7,12 @@ require 'cadenza/cli'
7
7
  options = Cadenza::Cli::Options.parse!
8
8
  path = ARGV[0]
9
9
 
10
- STDOUT.puts Cadenza::Cli.run!(path, options)
10
+ begin
11
+ STDOUT.puts Cadenza::Cli.run!(path, options)
12
+ rescue Cadenza::TemplateNotFoundError => e
13
+ STDERR.puts "Couldn't find template - #{e.message}"
14
+ exit 66
15
+ rescue Cadenza::Error => e
16
+ STDERR.puts "#{e.backtrace}"
17
+ exit 1
18
+ end
@@ -1,13 +1,28 @@
1
1
  require 'cadenza/error'
2
+
2
3
  require 'cadenza/token'
4
+
3
5
  require 'cadenza/lexer'
6
+
4
7
  require 'cadenza/racc_parser'
5
8
  require 'cadenza/parser'
9
+
10
+ require 'cadenza/context/stack'
11
+ require 'cadenza/context/filters'
12
+ require 'cadenza/context/blocks'
13
+ require 'cadenza/context/functional_variables'
14
+ require 'cadenza/context/loaders'
6
15
  require 'cadenza/context'
16
+
7
17
  require 'cadenza/context_object'
18
+
8
19
  require 'cadenza/base_renderer'
9
20
  require 'cadenza/text_renderer'
21
+ require 'cadenza/block_hierarchy'
22
+ require 'cadenza/source_renderer'
23
+
10
24
  require 'cadenza/filesystem_loader'
25
+
11
26
  require 'cadenza/version'
12
27
 
13
28
  require 'stringio'
@@ -6,10 +6,6 @@ module Cadenza
6
6
  # @return [IO] the io object that is being written to
7
7
  attr_reader :output
8
8
 
9
- # @deprecated temporary hack, will be removed later
10
- # @return [DocumentNode] the node which is at the root of the AST
11
- attr_reader :document
12
-
13
9
  # creates a new renderer and assigns the given output io object to it
14
10
  # @param [IO] output_io the IO object which will be written to
15
11
  def initialize(output_io)
@@ -28,10 +24,6 @@ module Cadenza
28
24
  # {BlockNode}. The blocks given should be rendered instead
29
25
  # of blocks of the same name in the given document.
30
26
  def render(node, context, blocks={})
31
- #TODO: memoizing this is a terrible smell, add a "parent" hierarchy so
32
- # we can always find the root node from any node in the AST
33
- @document ||= node
34
-
35
27
  node_type = node.class.name.split("::").last
36
28
 
37
29
  node_name = underscore(node_type).gsub!(/_node$/, '')
@@ -0,0 +1,42 @@
1
+
2
+ module Cadenza
3
+ # This class is used to help implement the "super" magic variable which is
4
+ # available when rendering blocks using the {TextRenderer}. It is essentially
5
+ # a glorified hash table with some special rules for merging.
6
+ #
7
+ # Please treat this class as private to Cadenza, it is not meant to be used
8
+ # outside of this gem.
9
+ class BlockHierarchy
10
+
11
+ # creates a new {BlockHierarchy} with the initial block hash
12
+ # @param [Hash] data the initial data to merge into the names hash
13
+ def initialize(data=nil)
14
+ @names = Hash.new
15
+
16
+ merge(data) if data
17
+ end
18
+
19
+ # @return [Array] the inheritance chain for the given block name
20
+ def fetch(block_name)
21
+ @names[block_name.to_s] || []
22
+ end
23
+
24
+ alias :[] :fetch
25
+
26
+ # appends the given block to the inheritance chain of it's name
27
+ # @param [BlockNode] block
28
+ def push(block)
29
+ @names[block.name.to_s] ||= []
30
+ @names[block.name.to_s] << block
31
+ end
32
+
33
+ alias :<< :push
34
+
35
+ # merges the given hash of blocks (name -> block) onto the end of each
36
+ # inheritance chain
37
+ # @param [Hash] hash
38
+ def merge(hash)
39
+ hash.each {|k,v| self << v }
40
+ end
41
+ end
42
+ end
@@ -16,8 +16,8 @@ module Cadenza
16
16
  [Dir.pwd]
17
17
  end
18
18
 
19
- load_paths.each do |path|
20
- Cadenza::BaseContext.add_load_path path
19
+ load_paths.each do |load_path|
20
+ Cadenza::BaseContext.add_load_path load_path
21
21
  end
22
22
 
23
23
  Cadenza::BaseContext.whiny_template_loading = true
@@ -1,42 +1,50 @@
1
1
 
2
2
  module Cadenza
3
- class TemplateNotFoundError < Cadenza::Error
4
- end
5
-
6
- class FilterNotDefinedError < Cadenza::Error
7
- end
8
-
9
- class FunctionalVariableNotDefinedError < Cadenza::Error
10
- end
11
-
12
- class BlockNotDefinedError < Cadenza::Error
13
- end
14
-
15
3
  # The {Context} class is an essential class in Cadenza that contains all the
16
4
  # data necessary to render a template to it's output. The context holds all
17
- # defined variable names (see {#stack}), {#filters}, {#functional_variables},
18
- # generic {#blocks}, {#loaders} and configuration data as well as all the
19
- # methods you should need to define and evaluate those.
5
+ # defined variable names (see {#stack}), filters (see {#filters}),
6
+ # functional variables (see {#functional_variables}), generic blocks
7
+ # (see {#blocks}), loaders (see {#loaders}) and configuration data as well
8
+ # as all the methods you should need to define and evaluate those.
20
9
  class Context
21
- # @return [Array] the variable stack
22
- attr_accessor :stack
10
+ include Cadenza::Context::Stack
11
+ include Cadenza::Context::Filters
12
+ include Cadenza::Context::Blocks
13
+ include Cadenza::Context::FunctionalVariables
14
+ include Cadenza::Context::Loaders
23
15
 
24
- # @return [Hash] the filter names mapped to their implementing procs
25
- attr_accessor :filters
26
-
27
- # @return [Hash] the functional variable names mapped to their implementing procs
28
- attr_accessor :functional_variables
16
+ # looks up the given identifier name on the given ruby object using
17
+ # Cadenza's internal logic for doing so.
18
+ #
19
+ # {Array} objects allow identifiers in the form of numbers to retrieve
20
+ # the index specified. Example: alphabet.0 returns "a"
21
+ #
22
+ # {Hash} objects allow identifiers to be fetched as keys of that hash.
23
+ #
24
+ # Any object which is a subclass of {ContextObject} will have a value
25
+ # looked up according to the logic defined in {ContextObject#invoke_context_method}
26
+ #
27
+ # @param [Symbol|String] identifier the name of the value to look up on this object
28
+ # @param [Object] object the object to look up the value on
29
+ # @return [Object] the result of the lookup
30
+ def self.lookup_on_object(identifier, object)
31
+ sym_identifier = identifier.to_sym
29
32
 
30
- # @return [Hash] the block names mapped to their implementing procs
31
- attr_accessor :blocks
33
+ # allow looking up array indexes with dot notation, example: alphabet.0 => "a"
34
+ if object.respond_to?(:[]) && object.is_a?(Array) && identifier =~ /\A\d+\z/
35
+ return object[identifier.to_i]
36
+ end
32
37
 
33
- # @return [Array] the list of loaders
34
- attr_accessor :loaders
38
+ # otherwise if it's a hash look up the string or symbolized key
39
+ if object.respond_to?(:[]) && object.is_a?(Hash) && (object.has_key?(identifier) || object.has_key?(sym_identifier))
40
+ return object[identifier] || object[sym_identifier]
41
+ end
35
42
 
36
- # @return [Boolean] true if a {TemplateNotFoundError} should still be
37
- # raised if not calling the bang form of {#load_source}
38
- # or {#load_template}
39
- attr_accessor :whiny_template_loading
43
+ # if the identifier is a callable method then call that
44
+ return object.send(:invoke_context_method, identifier) if object.is_a?(Cadenza::ContextObject)
45
+
46
+ nil
47
+ end
40
48
 
41
49
  # creates a new context object with an empty stack, filter list, functional
42
50
  # variable list, block list, loaders list and default configuration options.
@@ -45,13 +53,6 @@ module Cadenza
45
53
  #
46
54
  # @param [Hash] initial_scope the initial scope for the context
47
55
  def initialize(initial_scope={})
48
- @stack = []
49
- @filters = {}
50
- @functional_variables = {}
51
- @blocks = {}
52
- @loaders = []
53
- @whiny_template_loading = false
54
-
55
56
  push initial_scope
56
57
  end
57
58
 
@@ -61,283 +62,13 @@ module Cadenza
61
62
  # @return [Context] the cloned context
62
63
  def clone
63
64
  copy = super
64
- copy.stack = stack.dup
65
- copy.loaders = loaders.dup
66
- copy.filters = filters.dup
67
- copy.functional_variables = functional_variables.dup
68
- copy.blocks = blocks.dup
69
-
70
- copy
71
- end
72
-
73
- # retrieves the value of the given identifier by inspecting the variable
74
- # stack from top to bottom. Identifiers with dots in them are separated
75
- # on that dot character and looked up as a path. If no value could be
76
- # found then nil is returned.
77
- #
78
- # @return [Object] the object matching the identifier or nil if not found
79
- def lookup(identifier)
80
- @stack.reverse_each do |scope|
81
- value = lookup_identifier(scope, identifier)
82
-
83
- return value unless value.nil?
84
- end
65
+ copy.instance_variable_set("@stack", stack.dup)
66
+ copy.instance_variable_set("@loaders", loaders.dup)
67
+ copy.instance_variable_set("@filters", filters.dup)
68
+ copy.instance_variable_set("@functional_variables", functional_variables.dup)
69
+ copy.instance_variable_set("@blocks", blocks.dup)
85
70
 
86
- nil
87
- end
88
-
89
- # assigns the given value to the given identifier at the highest current
90
- # scope of the stack.
91
- #
92
- # @param [String] identifier the name of the variable to store
93
- # @param [Object] value the value to assign to the given name
94
- def assign(identifier, value)
95
- @stack.last[identifier.to_sym] = value
96
- end
97
-
98
- # creates a new scope on the variable stack and assigns the given hash
99
- # to it.
100
- #
101
- # @param [Hash] scope the mapping of names to values for the new scope
102
- # @return nil
103
- def push(scope)
104
- # TODO: symbolizing strings is slow so consider symbolizing here to improve
105
- # the speed of the lookup method (its more important than push)
106
-
107
- # TODO: since you can assign with the #assign method then make the scope
108
- # variable optional (assigns an empty hash)
109
- @stack.push(scope)
110
-
111
- nil
112
- end
113
-
114
- # removes the highest scope from the variable stack
115
- # @return [Hash] the removed scope
116
- def pop
117
- @stack.pop
118
- end
119
-
120
- # defines a filter proc with the given name
121
- #
122
- # @param [Symbol] name the name for the template to use for this filter
123
- # @yield [String, *args] the block will receive the input string and a
124
- # variable number of arguments passed to the filter.
125
- # @return nil
126
- def define_filter(name, &block)
127
- @filters[name.to_sym] = block
128
- nil
129
- end
130
-
131
- # calls the defined filter proc with the given parameters and returns the
132
- # result.
133
- #
134
- # @raise [FilterNotDefinedError] if the named filter doesn't exist
135
- # @param [Symbol] name the name of the filter to evaluate
136
- # @param [Array] params a list of parameters to pass to the filter
137
- # block when calling it.
138
- # @return [String] the result of evaluating the filter
139
- def evaluate_filter(name, params=[])
140
- filter = @filters[name.to_sym]
141
- raise FilterNotDefinedError.new("undefined filter '#{name}'") if filter.nil?
142
- filter.call(*params)
143
- end
144
-
145
- # defines a functional variable proc with the given name
146
- #
147
- # @param [Symbol] name the name for the template to use for this variable
148
- # @yield [Context, *args] the block will receive the context object and a
149
- # variable number of arguments passed to the variable.
150
- # @return nil
151
- def define_functional_variable(name, &block)
152
- @functional_variables[name.to_sym] = block
153
- nil
154
- end
155
-
156
- # calls the defined functional variable proc with the given parameters and
157
- # returns the result.
158
- #
159
- # @raise [FunctionalVariableNotDefinedError] if the named variable doesn't exist
160
- # @param [Symbol] name the name of the functional variable to evaluate
161
- # @param [Array] params a list of parameters to pass to the variable
162
- # block when calling it
163
- # @return [Object] the result of evaluating the functional variable
164
- def evaluate_functional_variable(name, params=[])
165
- var = @functional_variables[name.to_sym]
166
- raise FunctionalVariableNotDefinedError.new("undefined functional variable '#{name}'") if var.nil?
167
- var.call([self] + params)
168
- end
169
-
170
- # defines a generic block proc with the given name
171
- #
172
- # @param [Symbol] name the name for the template to use for this block
173
- # @yield [Context, Array, *args] the block will receive the context object,
174
- # a list of Node objects (it's children), and
175
- # a variable number of aarguments passed to
176
- # the block.
177
- # @return nil
178
- def define_block(name, &block)
179
- @blocks[name.to_sym] = block
180
- nil
181
- end
182
-
183
- # calls the defined generic block proc with the given name and children
184
- # nodes.
185
- #
186
- # @raise [BlockNotDefinedError] if the named block does not exist
187
- # @param [Symbol] name the name of the block to evaluate
188
- # @param [Array] nodes the child nodes of the block
189
- # @param [Array, []] params a list of parameters to pass to the block
190
- # when calling it.
191
- # @return [String] the result of evaluating the block
192
- def evaluate_block(name, nodes, parameters)
193
- block = @blocks[name.to_sym]
194
- raise BlockNotDefinedError.new("undefined block '#{name}") if block.nil?
195
- block.call(self, nodes, parameters)
196
- end
197
-
198
- # constructs a {FilesystemLoader} with the string given as its path and
199
- # adds the loader to the end of the loader list.
200
- #
201
- # @param [String] path to use for loader
202
- # @return [Loader] the loader that was created
203
- def add_load_path(path)
204
- loader = FilesystemLoader.new(path)
205
- add_loader(loader)
206
- loader
207
- end
208
-
209
- # adds the given loader to the end of the loader list.
210
- #
211
- # @param [Loader] loader the loader to add
212
- # @return nil
213
- def add_loader(loader)
214
- @loaders.push loader
215
- nil
216
- end
217
-
218
- # removes all loaders from the context
219
- # @return nil
220
- def clear_loaders
221
- @loaders.reject! { true }
222
- nil
223
- end
224
-
225
- # loads and returns the given template but does not parse it
226
- #
227
- # @raise [TemplateNotFoundError] if {#whiny_template_loading} is enabled and
228
- # the template could not be loaded.
229
- # @param [String] template_name the name of the template to load
230
- # @return [String] the template text or nil if the template could not be loaded
231
- def load_source(template_name)
232
- source = nil
233
-
234
- @loaders.each do |loader|
235
- source = loader.load_source(template_name)
236
- break if source
237
- end
238
-
239
- if source.nil? and whiny_template_loading
240
- raise TemplateNotFoundError.new(template_name)
241
- else
242
- return source
243
- end
244
- end
245
-
246
- # loads and returns the given template but does not parse it
247
- #
248
- # @raise [TemplateNotFoundError] if the template could not be loaded
249
- # @param [String] template_name the name of the template to load
250
- # @return [String] the template text
251
- def load_source!(template_name)
252
- load_source(template_name) || raise(TemplateNotFoundError.new(template_name))
253
- end
254
-
255
- # loads, parses and returns the given template
256
- #
257
- # @raise [TemplateNotFoundError] if {#whiny_template_loading} is enabled and
258
- # the template could not be loaded.
259
- # @param [String] template_name the name of the template to load
260
- # @return [DocumentNode] the root of the parsed document or nil if the
261
- # template could not be loaded.
262
- def load_template(template_name)
263
- template = nil
264
-
265
- @loaders.each do |loader|
266
- template = loader.load_template(template_name)
267
- break if template
268
- end
269
-
270
- if template.nil? and whiny_template_loading
271
- raise TemplateNotFoundError.new(template_name)
272
- else
273
- return template
274
- end
275
- end
276
-
277
- # loads, parses and returns the given template
278
- #
279
- # @raise [TemplateNotFoundError] if the template could not be loaded
280
- # @param [String] template_name the name of the template ot load
281
- # @return [DocumentNode] the root of the parsed document
282
- def load_template!(template_name)
283
- load_template(template_name) || raise(TemplateNotFoundError.new(template_name))
284
- end
285
-
286
- private
287
- def lookup_identifier(scope, identifier)
288
- if identifier.index('.')
289
- lookup_path(scope, identifier.split("."))
290
- else
291
- lookup_on_scope(scope, identifier)
292
- end
293
- end
294
-
295
- def lookup_path(scope, path)
296
- loop do
297
- component = path.shift
298
-
299
- scope = lookup_on_scope(scope, component)
300
-
301
- return scope if path.empty?
302
- end
303
- end
304
-
305
- def lookup_on_scope(scope, identifier)
306
- sym_identifier = identifier.to_sym
307
-
308
- # allow looking up array indexes with dot notation, example: alphabet.0 => "a"
309
- #TODO: the /\d+/ regex doesn't have the \A\z terminators, could that allow expected calling? example: alpabet.a0
310
- if scope.respond_to?(:[]) and scope.is_a?(Array) and identifier =~ /\d+/
311
- return scope[identifier.to_i]
312
- end
313
-
314
- # otherwise if it's a hash look up the string or symbolized key
315
- if scope.respond_to?(:[]) and scope.is_a?(Hash) and (scope.has_key?(identifier) || scope.has_key?(sym_identifier))
316
- return scope[identifier] || scope[sym_identifier]
317
- end
318
-
319
- #TODO: security vulnerability below, we use #send on an object which can
320
- # allow us to call private or protected methods on an object.
321
- #
322
- # We could use #public_send but it is only available in Ruby 1.9.x so
323
- # we would have to drop 1.8.7 support.
324
- #
325
- # Alternatively we could forbid binding regular objects to contexts
326
- # entirely and require that any bound object is a subclass of Cadenza::Drop
327
- # Knowing this we could take public methods defined on the object
328
- # and subtract public methods defined on the drop to get a list of
329
- # safely callable methods. And it will work in 1.8.7
330
- #
331
- # This won't happen until Cadenza 0.8.0 however as it will possibly break
332
- # backwards compatibility.
333
-
334
- # if the identifier is a callable method then call that
335
- return scope.send(sym_identifier) if scope.respond_to?(sym_identifier)
336
-
337
- # if a functional variable is defined matching the identifier name then return that
338
- return @functional_variables[sym_identifier] if @functional_variables.has_key?(sym_identifier)
339
-
340
- nil
71
+ copy
341
72
  end
342
73
 
343
74
  end