cadenza 0.7.0.rc1 → 0.7.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.
@@ -1,5 +1,7 @@
1
+ require 'cadenza/error'
1
2
  require 'cadenza/token'
2
3
  require 'cadenza/lexer'
4
+ require 'cadenza/racc_parser'
3
5
  require 'cadenza/parser'
4
6
  require 'cadenza/context'
5
7
  require 'cadenza/base_renderer'
@@ -15,6 +17,13 @@ Dir[File.join File.dirname(__FILE__), 'cadenza', 'nodes', '*.rb'].each {|f| requ
15
17
  module Cadenza
16
18
  BaseContext = Context.new
17
19
 
20
+ # this utility method sets up the standard Cadenza lexer/parser/renderer
21
+ # stack and renders the given template text with the given variable scope
22
+ # using the {BaseContext}. the result of rendering is returned as a string.
23
+ #
24
+ # @param [String] template_text the content of the template to parse/render
25
+ # @param [Hash] scope any variables to define as a new scope for {BaseContext}
26
+ # in this template.
18
27
  def self.render(template_text, scope=nil)
19
28
  template = Parser.new.parse(template_text)
20
29
 
@@ -29,6 +38,12 @@ module Cadenza
29
38
  output.string
30
39
  end
31
40
 
41
+ # similar to {#render} except the given template name will be loaded using
42
+ # {BaseContext}s predefined list of loaders.
43
+ #
44
+ # @param [String] template_name the name of the template to load then parse and render
45
+ # @param [Hash] scope any variables to define as a new scope for {BaseContext}
46
+ # in this template.
32
47
  def self.render_template(template_name, scope=nil)
33
48
  context = BaseContext.clone
34
49
 
@@ -1,12 +1,35 @@
1
1
  module Cadenza
2
+ # BaseRenderer is a class you can use to more easily and cleanly implement
3
+ # your own rendering class. To use this then subclass {BaseRenderer} and
4
+ # implement the appropriate render_xyz methods (see {#render} for details).
2
5
  class BaseRenderer
3
- attr_reader :output, :document
6
+ # @return [IO] the io object that is being written to
7
+ attr_reader :output
4
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
+ # creates a new renderer and assigns the given output io object to it
14
+ # @param [IO] output_io the IO object which will be written to
5
15
  def initialize(output_io)
6
16
  @output = output_io
7
17
  end
8
18
 
19
+ # renders the given document node to the output stream given in the
20
+ # constructor. this method will call the render_xyz method for the node
21
+ # given, where xyz is the demodulized underscored version of the node's
22
+ # class name. for example: given a Cadenza::DocumentNode this method will
23
+ # call render_document_node
24
+ #
25
+ # @param [Node] node the node to render
26
+ # @param [Context] context the context to render with
27
+ # @param [Hash] blocks a mapping of the block names to the matching
28
+ # {BlockNode}. The blocks given should be rendered instead
29
+ # of blocks of the same name in the given document.
9
30
  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
10
33
  @document ||= node
11
34
 
12
35
  node_type = node.class.name.split("::").last
@@ -16,6 +39,8 @@ module Cadenza
16
39
  send("render_#{node_name}", node, context, blocks)
17
40
  end
18
41
 
42
+ private
43
+
19
44
  # very stripped down form of ActiveSupport's underscore method
20
45
  def underscore(word)
21
46
  word.gsub!(/([a-z\d])([A-Z])/,'\1_\2').downcase!
@@ -1,21 +1,49 @@
1
1
 
2
2
  module Cadenza
3
- class TemplateNotFoundError < StandardError
3
+ class TemplateNotFoundError < Cadenza::Error
4
4
  end
5
5
 
6
- class FilterNotDefinedError < StandardError
6
+ class FilterNotDefinedError < Cadenza::Error
7
7
  end
8
8
 
9
- class FunctionalVariableNotDefinedError < StandardError
9
+ class FunctionalVariableNotDefinedError < Cadenza::Error
10
10
  end
11
11
 
12
- class BlockNotDefinedError < StandardError
12
+ class BlockNotDefinedError < Cadenza::Error
13
13
  end
14
14
 
15
+ # The {Context} class is an essential class in Cadenza that contains all the
16
+ # 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.
15
20
  class Context
16
- attr_accessor :stack, :filters, :functional_variables, :blocks, :loaders
21
+ # @return [Array] the variable stack
22
+ attr_accessor :stack
23
+
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
29
+
30
+ # @return [Hash] the block names mapped to their implementing procs
31
+ attr_accessor :blocks
32
+
33
+ # @return [Array] the list of loaders
34
+ attr_accessor :loaders
35
+
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}
17
39
  attr_accessor :whiny_template_loading
18
40
 
41
+ # creates a new context object with an empty stack, filter list, functional
42
+ # variable list, block list, loaders list and default configuration options.
43
+ #
44
+ # When created you can push an optional scope onto as the initial stack
45
+ #
46
+ # @param [Hash] initial_scope the initial scope for the context
19
47
  def initialize(initial_scope={})
20
48
  @stack = []
21
49
  @filters = {}
@@ -29,6 +57,8 @@ module Cadenza
29
57
 
30
58
  # creates a new instance of the context with the stack, loaders, filters,
31
59
  # functional variables and blocks shallow copied.
60
+ #
61
+ # @return [Context] the cloned context
32
62
  def clone
33
63
  copy = super
34
64
  copy.stack = stack.dup
@@ -40,6 +70,12 @@ module Cadenza
40
70
  copy
41
71
  end
42
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
43
79
  def lookup(identifier)
44
80
  @stack.reverse_each do |scope|
45
81
  value = lookup_identifier(scope, identifier)
@@ -50,64 +86,143 @@ module Cadenza
50
86
  nil
51
87
  end
52
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
53
94
  def assign(identifier, value)
54
95
  @stack.last[identifier.to_sym] = value
55
96
  end
56
97
 
57
- # TODO: symbolizing strings is slow so consider symbolizing here to improve
58
- # the speed of the lookup method (its more important than push)
59
- # TODO: since you can assign with the #assign method then make the scope
60
- # variable optional (assigns an empty hash)
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
61
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)
62
109
  @stack.push(scope)
110
+
111
+ nil
63
112
  end
64
113
 
114
+ # removes the highest scope from the variable stack
115
+ # @return [Hash] the removed scope
65
116
  def pop
66
117
  @stack.pop
67
118
  end
68
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
69
126
  def define_filter(name, &block)
70
127
  @filters[name.to_sym] = block
128
+ nil
71
129
  end
72
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
73
139
  def evaluate_filter(name, params=[])
74
140
  filter = @filters[name.to_sym]
75
141
  raise FilterNotDefinedError.new("undefined filter '#{name}'") if filter.nil?
76
142
  filter.call(*params)
77
143
  end
78
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
79
151
  def define_functional_variable(name, &block)
80
152
  @functional_variables[name.to_sym] = block
153
+ nil
81
154
  end
82
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
83
164
  def evaluate_functional_variable(name, params=[])
84
165
  var = @functional_variables[name.to_sym]
85
166
  raise FunctionalVariableNotDefinedError.new("undefined functional variable '#{name}'") if var.nil?
86
167
  var.call([self] + params)
87
168
  end
88
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
89
178
  def define_block(name, &block)
90
179
  @blocks[name.to_sym] = block
180
+ nil
91
181
  end
92
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
93
192
  def evaluate_block(name, nodes, parameters)
94
193
  block = @blocks[name.to_sym]
95
194
  raise BlockNotDefinedError.new("undefined block '#{name}") if block.nil?
96
195
  block.call(self, nodes, parameters)
97
196
  end
98
197
 
198
+ # adds the given loader to the end of the loader list. If the argument
199
+ # passed is a string then a {FilesystemLoader} will be constructed with
200
+ # the string given as a path for it.
201
+ #
202
+ # @param [Loader,String] loader the loader to add
203
+ # @return nil
99
204
  def add_loader(loader)
100
205
  if loader.is_a?(String)
101
206
  @loaders.push FilesystemLoader.new(loader)
102
207
  else
103
208
  @loaders.push loader
104
209
  end
210
+ nil
105
211
  end
106
212
 
213
+ # removes all loaders from the context
214
+ # @return nil
107
215
  def clear_loaders
108
216
  @loaders.reject! { true }
217
+ nil
109
218
  end
110
219
 
220
+ # loads and returns the given template but does not parse it
221
+ #
222
+ # @raise [TemplateNotFoundError] if {#whiny_template_loading} is enabled and
223
+ # the template could not be loaded.
224
+ # @param [String] template_name the name of the template to load
225
+ # @return [String] the template text or nil if the template could not be loaded
111
226
  def load_source(template_name)
112
227
  source = nil
113
228
 
@@ -123,10 +238,22 @@ module Cadenza
123
238
  end
124
239
  end
125
240
 
241
+ # loads and returns the given template but does not parse it
242
+ #
243
+ # @raise [TemplateNotFoundError] if the template could not be loaded
244
+ # @param [String] template_name the name of the template to load
245
+ # @return [String] the template text
126
246
  def load_source!(template_name)
127
247
  load_source(template_name) || raise(TemplateNotFoundError.new(template_name))
128
248
  end
129
249
 
250
+ # loads, parses and returns the given template
251
+ #
252
+ # @raise [TemplateNotFoundError] if {#whiny_template_loading} is enabled and
253
+ # the template could not be loaded.
254
+ # @param [String] template_name the name of the template to load
255
+ # @return [DocumentNode] the root of the parsed document or nil if the
256
+ # template could not be loaded.
130
257
  def load_template(template_name)
131
258
  template = nil
132
259
 
@@ -142,6 +269,11 @@ module Cadenza
142
269
  end
143
270
  end
144
271
 
272
+ # loads, parses and returns the given template
273
+ #
274
+ # @raise [TemplateNotFoundError] if the template could not be loaded
275
+ # @param [String] template_name the name of the template ot load
276
+ # @return [DocumentNode] the root of the parsed document
145
277
  def load_template!(template_name)
146
278
  load_template(template_name) || raise(TemplateNotFoundError.new(template_name))
147
279
  end
@@ -0,0 +1,13 @@
1
+ module Cadenza
2
+ # The {Error} class is the base class of all types of errors Cadenza will
3
+ # raise, this should make exception handling much simpler for you.
4
+ #
5
+ # Example:
6
+ # begin
7
+ # Cadenza.parse("some {{ invalid template")
8
+ # rescue Cadenza::Error => e
9
+ # puts "oh noes!"
10
+ # end
11
+ class Error < StandardError
12
+ end
13
+ end
@@ -1,12 +1,35 @@
1
1
 
2
2
  module Cadenza
3
+ # The {FilesystemLoader} is a very simple loader object which takes a given
4
+ # "root" directory and loads templates using the filesystem. Relative file
5
+ # paths from this directory should be used for template names.
6
+ #
7
+ # This implemenation makes no attempt to be secure so upwards relative file
8
+ # paths could be used to load sensitive files into the output template.
9
+ #
10
+ # ```django
11
+ # {# assuming you add /home/someuser as a loaded path #}
12
+ # {{ load '../../etc/passwd' }}
13
+ # ```
14
+ #
15
+ # If you allow loading to be used for insecure user content then consider
16
+ # using a more secure loader class such as {ZipLoader} or writing a simple
17
+ # loader for your database connection.
3
18
  class FilesystemLoader
19
+ # @return [String] the path on the filesystem to load relative to
4
20
  attr_accessor :path
5
21
 
22
+ # creates a new {FilesystemLoader} with the given filesystem directory
23
+ # to load templates relative to.
24
+ # @param [String] path see {#path}
6
25
  def initialize(path)
7
26
  @path = path
8
27
  end
9
28
 
29
+ # loads and returns the given template's content or nil if the file was
30
+ # not a file object (such as a directory).
31
+ # @param [String] template the name of the template to load
32
+ # @return [String] the content of the template
10
33
  def load_source(template)
11
34
  filename = File.join(path, template)
12
35
 
@@ -15,6 +38,10 @@ module Cadenza
15
38
  File.read filename
16
39
  end
17
40
 
41
+ # loads and parses the given template name using {Parser}. If the template
42
+ # could not be loaded then nil is returned.
43
+ # @param [String] template the name of the template to load
44
+ # @return [DocumentNode] the root node of the parsed AST
18
45
  def load_template(template)
19
46
  source = load_source(template)
20
47
 
@@ -1,13 +1,22 @@
1
1
  require 'strscan'
2
2
 
3
3
  module Cadenza
4
-
4
+ # The {Lexer} class accepts in input {IO} object which it will parse simple
5
+ # {Token}s from for use in a {Parser} class.
5
6
  class Lexer
7
+ #TODO: look at using the CodeRay scanner instead, it supports reading from
8
+ # an IO object (unlike StringScanner which must have a string in memory)
9
+ # http://coderay.rubychan.de/doc/classes/CodeRay/Scanners/Scanner.html
10
+
11
+ # constructs a new parser and sets it to the position (0, 0)
6
12
  def initialize
7
13
  @line = 0
8
14
  @column = 0
9
15
  end
10
16
 
17
+ # assigns a new string to retrieve tokens and resets the line and column
18
+ # counters to (1, 1)
19
+ # @param [String] source the string from which to parse tokens
11
20
  def source=(source)
12
21
  @scanner = ::StringScanner.new(source || "")
13
22
 
@@ -17,10 +26,47 @@ module Cadenza
17
26
  @context = :body
18
27
  end
19
28
 
29
+ # gives the current line and column counter as a two element array
30
+ # @return [Array] the line and column
20
31
  def position
21
32
  [@line, @column]
22
33
  end
23
34
 
35
+ # Gets the next token and returns it. Tokens are two element arrays where
36
+ # the first element is one of the following symbols and the second is an
37
+ # instance of {Cadenza::Token} containing the value of the token.
38
+ #
39
+ # valid tokens:
40
+ # - :VAR_OPEN - for opening an inject tag ex. "{{"
41
+ # - :VAR_CLOSE - for closing an inject tag ex. "}}"
42
+ # - :STMT_OPEN - for opening a control tag ex. "{%"
43
+ # - :STMT_CLOSE - for closing a control tag ex. "%}"
44
+ # - :TEXT_BLOCK - for a block of raw text
45
+ # - :OP_EQ - for an equivalence symbol ex. "=="
46
+ # - :OP_NEQ - for a nonequivalence symbol ex. "!="
47
+ # - :OP_GEQ - for a greater than or equal to symbol ex. ">="
48
+ # - :OP_LEQ - for a less than or equal to symbol ex. "<="
49
+ # - :REAL - for a number with a decimal value ex. "123.45"
50
+ # - :INTEGER - for a number without a decimal value ex. "12345"
51
+ # - :STRING - for a string literal, either from single quotes or double quotes ex. "'foo'"
52
+ # - :IDENTIFIER - for a variable name ex. "foo"
53
+ # - :IF - for the 'if' keyword
54
+ # - :UNLESS - for the 'unless' keyword
55
+ # - :ELSE - for the 'else' keyword
56
+ # - :ENDIF - for the 'endif' keyword
57
+ # - :ENDUNLESS - for the 'endunless' keyword
58
+ # - :FOR - for the 'for' keyword
59
+ # - :IN - for the 'in' keyword
60
+ # - :ENDFOR - for the 'endfor' keyword
61
+ # - :BLOCK - for the 'block' keyword
62
+ # - :ENDBLOCK - for the 'endblock' keyword
63
+ # - :EXTENDS - for the 'extends' keyword
64
+ # - :END - for the 'end' keyword
65
+ # - :AND - for the 'and' keyword
66
+ # - :OR - for the 'or' keyword
67
+ # - :NOT - for the 'not' keyword
68
+ #
69
+ # if no tokens are left the return value will be [false, false]
24
70
  def next_token
25
71
  if @scanner.eos?
26
72
  [false, false]
@@ -29,6 +75,11 @@ module Cadenza
29
75
  end
30
76
  end
31
77
 
78
+ # returns an array of all remaining tokens left to be parsed. See {#next_token}
79
+ # for details regarding the definition of a token. The array will always end in
80
+ # [false, false].
81
+ #
82
+ # @return [Array] a list of all remaining tokens
32
83
  def remaining_tokens
33
84
  result = []
34
85