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.
- data/bin/cadenza +9 -1
- data/lib/cadenza.rb +15 -0
- data/lib/cadenza/base_renderer.rb +0 -8
- data/lib/cadenza/block_hierarchy.rb +42 -0
- data/lib/cadenza/cli.rb +2 -2
- data/lib/cadenza/context.rb +44 -313
- data/lib/cadenza/context/blocks.rb +60 -0
- data/lib/cadenza/context/filters.rb +60 -0
- data/lib/cadenza/context/functional_variables.rb +58 -0
- data/lib/cadenza/context/loaders.rb +116 -0
- data/lib/cadenza/context/stack.rb +85 -0
- data/lib/cadenza/context_object.rb +92 -2
- data/lib/cadenza/filters/standard_filters.rb +82 -23
- data/lib/cadenza/lexer.rb +0 -3
- data/lib/cadenza/nodes/filter_node.rb +1 -2
- data/lib/cadenza/nodes/filtered_value_node.rb +48 -0
- data/lib/cadenza/nodes/variable_node.rb +17 -4
- data/lib/cadenza/racc_parser.rb +329 -339
- data/lib/cadenza/source_renderer.rb +225 -0
- data/lib/cadenza/text_renderer.rb +25 -10
- data/lib/cadenza/version.rb +2 -2
- metadata +10 -3
- data/lib/cadenza/nodes/inject_node.rb +0 -57
data/bin/cadenza
CHANGED
@@ -7,4 +7,12 @@ require 'cadenza/cli'
|
|
7
7
|
options = Cadenza::Cli::Options.parse!
|
8
8
|
path = ARGV[0]
|
9
9
|
|
10
|
-
|
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
|
data/lib/cadenza.rb
CHANGED
@@ -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
|
data/lib/cadenza/cli.rb
CHANGED
data/lib/cadenza/context.rb
CHANGED
@@ -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},
|
18
|
-
#
|
19
|
-
#
|
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
|
-
|
22
|
-
|
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
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
#
|
28
|
-
|
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
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
65
|
-
copy.loaders
|
66
|
-
copy.filters
|
67
|
-
copy.functional_variables
|
68
|
-
copy.blocks
|
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
|
-
|
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
|