cadenza 0.7.2 → 0.8.0

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.
@@ -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