cadenza 0.7.0.rc1 → 0.7.0

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