cadenza 0.7.0.rc1

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.
@@ -0,0 +1,57 @@
1
+ require 'cadenza/token'
2
+ require 'cadenza/lexer'
3
+ require 'cadenza/parser'
4
+ require 'cadenza/context'
5
+ require 'cadenza/base_renderer'
6
+ require 'cadenza/text_renderer'
7
+ require 'cadenza/filesystem_loader'
8
+ require 'cadenza/version'
9
+
10
+ require 'stringio'
11
+
12
+ # require all nodes
13
+ Dir[File.join File.dirname(__FILE__), 'cadenza', 'nodes', '*.rb'].each {|f| require f }
14
+
15
+ module Cadenza
16
+ BaseContext = Context.new
17
+
18
+ def self.render(template_text, scope=nil)
19
+ template = Parser.new.parse(template_text)
20
+
21
+ context = BaseContext.clone
22
+
23
+ context.push(scope) if scope
24
+
25
+ output = StringIO.new
26
+
27
+ TextRenderer.new(output).render(template, context)
28
+
29
+ output.string
30
+ end
31
+
32
+ def self.render_template(template_name, scope=nil)
33
+ context = BaseContext.clone
34
+
35
+ context.push(scope) if scope
36
+
37
+ template = context.load_template(template_name)
38
+
39
+ output = StringIO.new
40
+
41
+ TextRenderer.new(output).render(template, context)
42
+
43
+ output.string
44
+ end
45
+ end
46
+
47
+ Dir[File.join File.dirname(__FILE__), 'cadenza', 'filters', '*.rb'].each do |filename|
48
+ Cadenza::BaseContext.instance_eval File.read(filename)
49
+ end
50
+
51
+ Dir[File.join File.dirname(__FILE__), 'cadenza', 'functional_variables', '*.rb'].each do |filename|
52
+ Cadenza::BaseContext.instance_eval File.read(filename)
53
+ end
54
+
55
+ Dir[File.join File.dirname(__FILE__), 'cadenza', 'blocks', '*.rb'].each do |filename|
56
+ Cadenza::BaseContext.instance_eval File.read(filename)
57
+ end
@@ -0,0 +1,24 @@
1
+ module Cadenza
2
+ class BaseRenderer
3
+ attr_reader :output, :document
4
+
5
+ def initialize(output_io)
6
+ @output = output_io
7
+ end
8
+
9
+ def render(node, context, blocks={})
10
+ @document ||= node
11
+
12
+ node_type = node.class.name.split("::").last
13
+
14
+ node_name = underscore(node_type).gsub!(/_node$/, '')
15
+
16
+ send("render_#{node_name}", node, context, blocks)
17
+ end
18
+
19
+ # very stripped down form of ActiveSupport's underscore method
20
+ def underscore(word)
21
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2').downcase!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+
2
+ define_block :filter do |context, nodes, parameters|
3
+ filter = parameters.first.identifier
4
+
5
+ nodes.inject("") do |output, child|
6
+ node_text = Cadenza::TextRenderer.render(child, context)
7
+ output << context.evaluate_filter(filter, node_text)
8
+ end
9
+ end
@@ -0,0 +1,192 @@
1
+
2
+ module Cadenza
3
+ class TemplateNotFoundError < StandardError
4
+ end
5
+
6
+ class FilterNotDefinedError < StandardError
7
+ end
8
+
9
+ class FunctionalVariableNotDefinedError < StandardError
10
+ end
11
+
12
+ class BlockNotDefinedError < StandardError
13
+ end
14
+
15
+ class Context
16
+ attr_accessor :stack, :filters, :functional_variables, :blocks, :loaders
17
+ attr_accessor :whiny_template_loading
18
+
19
+ def initialize(initial_scope={})
20
+ @stack = []
21
+ @filters = {}
22
+ @functional_variables = {}
23
+ @blocks = {}
24
+ @loaders = []
25
+ @whiny_template_loading = false
26
+
27
+ push initial_scope
28
+ end
29
+
30
+ # creates a new instance of the context with the stack, loaders, filters,
31
+ # functional variables and blocks shallow copied.
32
+ def clone
33
+ copy = super
34
+ copy.stack = stack.dup
35
+ copy.loaders = loaders.dup
36
+ copy.filters = filters.dup
37
+ copy.functional_variables = functional_variables.dup
38
+ copy.blocks = blocks.dup
39
+
40
+ copy
41
+ end
42
+
43
+ def lookup(identifier)
44
+ @stack.reverse_each do |scope|
45
+ value = lookup_identifier(scope, identifier)
46
+
47
+ return value unless value.nil?
48
+ end
49
+
50
+ nil
51
+ end
52
+
53
+ def assign(identifier, value)
54
+ @stack.last[identifier.to_sym] = value
55
+ end
56
+
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)
61
+ def push(scope)
62
+ @stack.push(scope)
63
+ end
64
+
65
+ def pop
66
+ @stack.pop
67
+ end
68
+
69
+ def define_filter(name, &block)
70
+ @filters[name.to_sym] = block
71
+ end
72
+
73
+ def evaluate_filter(name, params=[])
74
+ filter = @filters[name.to_sym]
75
+ raise FilterNotDefinedError.new("undefined filter '#{name}'") if filter.nil?
76
+ filter.call(*params)
77
+ end
78
+
79
+ def define_functional_variable(name, &block)
80
+ @functional_variables[name.to_sym] = block
81
+ end
82
+
83
+ def evaluate_functional_variable(name, params=[])
84
+ var = @functional_variables[name.to_sym]
85
+ raise FunctionalVariableNotDefinedError.new("undefined functional variable '#{name}'") if var.nil?
86
+ var.call([self] + params)
87
+ end
88
+
89
+ def define_block(name, &block)
90
+ @blocks[name.to_sym] = block
91
+ end
92
+
93
+ def evaluate_block(name, nodes, parameters)
94
+ block = @blocks[name.to_sym]
95
+ raise BlockNotDefinedError.new("undefined block '#{name}") if block.nil?
96
+ block.call(self, nodes, parameters)
97
+ end
98
+
99
+ def add_loader(loader)
100
+ if loader.is_a?(String)
101
+ @loaders.push FilesystemLoader.new(loader)
102
+ else
103
+ @loaders.push loader
104
+ end
105
+ end
106
+
107
+ def clear_loaders
108
+ @loaders.reject! { true }
109
+ end
110
+
111
+ def load_source(template_name)
112
+ source = nil
113
+
114
+ @loaders.each do |loader|
115
+ source = loader.load_source(template_name)
116
+ break if source
117
+ end
118
+
119
+ if source.nil? and whiny_template_loading
120
+ raise TemplateNotFoundError.new(template_name)
121
+ else
122
+ return source
123
+ end
124
+ end
125
+
126
+ def load_source!(template_name)
127
+ load_source(template_name) || raise(TemplateNotFoundError.new(template_name))
128
+ end
129
+
130
+ def load_template(template_name)
131
+ template = nil
132
+
133
+ @loaders.each do |loader|
134
+ template = loader.load_template(template_name)
135
+ break if template
136
+ end
137
+
138
+ if template.nil? and whiny_template_loading
139
+ raise TemplateNotFoundError.new(template_name)
140
+ else
141
+ return template
142
+ end
143
+ end
144
+
145
+ def load_template!(template_name)
146
+ load_template(template_name) || raise(TemplateNotFoundError.new(template_name))
147
+ end
148
+
149
+ private
150
+ def lookup_identifier(scope, identifier)
151
+ if identifier.index('.')
152
+ lookup_path(scope, identifier.split("."))
153
+ else
154
+ lookup_on_scope(scope, identifier)
155
+ end
156
+ end
157
+
158
+ def lookup_path(scope, path)
159
+ loop do
160
+ component = path.shift
161
+
162
+ scope = lookup_on_scope(scope, component)
163
+
164
+ return scope if path.empty?
165
+ end
166
+ end
167
+
168
+ def lookup_on_scope(scope, identifier)
169
+ sym_identifier = identifier.to_sym
170
+
171
+ # allow looking up array indexes with dot notation, example: alphabet.0 => "a"
172
+ if scope.respond_to?(:[]) and scope.is_a?(Array) and identifier =~ /\d+/
173
+ return scope[identifier.to_i]
174
+ end
175
+
176
+ # otherwise if it's a hash look up the string or symbolized key
177
+ if scope.respond_to?(:[]) and scope.is_a?(Hash) and (scope.has_key?(identifier) || scope.has_key?(sym_identifier))
178
+ return scope[identifier] || scope[sym_identifier]
179
+ end
180
+
181
+ # if the identifier is a callable method then call that
182
+ return scope.send(sym_identifier) if scope.respond_to?(sym_identifier)
183
+
184
+ # if a functional variable is defined matching the identifier name then return that
185
+ return @functional_variables[sym_identifier] if @functional_variables.has_key?(sym_identifier)
186
+
187
+ nil
188
+ end
189
+
190
+ end
191
+
192
+ end
@@ -0,0 +1,28 @@
1
+
2
+ module Cadenza
3
+ class FilesystemLoader
4
+ attr_accessor :path
5
+
6
+ def initialize(path)
7
+ @path = path
8
+ end
9
+
10
+ def load_source(template)
11
+ filename = File.join(path, template)
12
+
13
+ return unless File.file?(filename)
14
+
15
+ File.read filename
16
+ end
17
+
18
+ def load_template(template)
19
+ source = load_source(template)
20
+
21
+ if source
22
+ return Cadenza::Parser.new.parse(source)
23
+ else
24
+ return nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,97 @@
1
+ require 'cgi'
2
+
3
+ # adds slashes to \, ', and " characters in the given string
4
+ define_filter :addslashes do |string|
5
+ word = string.dup
6
+ word.gsub!(/\\/, "\\\\\\\\")
7
+ word.gsub!(/'/, "\\\\'")
8
+ word.gsub!(/"/, "\\\"")
9
+ word
10
+ end
11
+
12
+ # capitalizes the first letter of the string
13
+ define_filter :capitalize, &:capitalize
14
+
15
+ # centers the string in a fixed width field with the given padding
16
+ define_filter :center do |string, length, *args|
17
+ padding = args.first || ' ' # Ruby 1.8.x compatibility
18
+ string.center(length, padding)
19
+ end
20
+
21
+ # removes all instances of the given string from the string
22
+ define_filter :cut do |string, value|
23
+ string.gsub(value, '')
24
+ end
25
+
26
+ # formats the given date object with the given format string
27
+ define_filter :date do |date, *args|
28
+ format = args.first || '%F' # Ruby 1.8.x compatibility
29
+ date.strftime(format)
30
+ end
31
+
32
+ # returns the given value if the input is falsy or is empty
33
+ define_filter :default do |input, default|
34
+ if input.respond_to?(:empty?) and input.empty?
35
+ default
36
+ else
37
+ input || default
38
+ end
39
+ end
40
+
41
+ # escapes the HTML content of the value
42
+ define_filter :escape do |input|
43
+ CGI::escapeHTML(input)
44
+ end
45
+
46
+ # returns the first item of an iterable
47
+ define_filter :first do |input|
48
+ if input.respond_to?(:[])
49
+ RUBY_VERSION =~ /^1.8/ && input.is_a?(String) ? input[0].chr : input[0]
50
+ else
51
+ input.first
52
+ end
53
+ end
54
+
55
+ # returns the last item of an iterable
56
+ define_filter :last do |input|
57
+ if input.respond_to?(:[])
58
+ RUBY_VERSION =~ /^1.8/ && input.is_a?(String) ? input[-1].chr : input[-1]
59
+ else
60
+ input.last
61
+ end
62
+ end
63
+
64
+ # glues together elements of the input with the glue string
65
+ define_filter :join do |input, glue|
66
+ input.join(glue)
67
+ end
68
+
69
+ # returns the length of the input
70
+ define_filter :length, &:length
71
+
72
+ # returns the string left justified with the given padding character
73
+ define_filter :ljust do |input, length, *args|
74
+ padding = args.first || ' ' # Ruby 1.8.x compatibility
75
+ input.ljust(length, padding)
76
+ end
77
+
78
+ # returns the string right justified with the given padding character
79
+ define_filter :rjust do |input, length, *args|
80
+ padding = args.first || ' ' # Ruby 1.8.x compatibility
81
+ input.rjust(length, padding)
82
+ end
83
+
84
+ # returns the string downcased
85
+ define_filter :lower, &:downcase
86
+
87
+ # returns the string upcased
88
+ define_filter :upper, &:upcase
89
+
90
+ # returns the given words wrapped to fit inside the given column width. Wrapping
91
+ # is done on word boundaries so that no word cutting is done.
92
+ # source: http://www.java2s.com/Code/Ruby/String/WordwrappingLinesofText.htm
93
+ define_filter :wordwrap do |input, length, *args|
94
+ linefeed = args.first || "\n" # Ruby 1.8.x compatibility
95
+ input.gsub(/(.{1,#{length}})(\s+|\Z)/, "\\1#{linefeed}")
96
+ end
97
+
@@ -0,0 +1,14 @@
1
+
2
+ define_functional_variable :load do |context, template|
3
+ context.load_source(template)
4
+ end
5
+
6
+ define_functional_variable :render do |context, template|
7
+ template = context.load_template(template)
8
+
9
+ if template
10
+ Cadenza::TextRenderer.render(template, context)
11
+ else
12
+ ""
13
+ end
14
+ end
@@ -0,0 +1,191 @@
1
+ require 'strscan'
2
+
3
+ module Cadenza
4
+
5
+ class Lexer
6
+ def initialize
7
+ @line = 0
8
+ @column = 0
9
+ end
10
+
11
+ def source=(source)
12
+ @scanner = ::StringScanner.new(source || "")
13
+
14
+ @line = 1
15
+ @column = 1
16
+
17
+ @context = :body
18
+ end
19
+
20
+ def position
21
+ [@line, @column]
22
+ end
23
+
24
+ def next_token
25
+ if @scanner.eos?
26
+ [false, false]
27
+ else
28
+ send("scan_#{@context}")
29
+ end
30
+ end
31
+
32
+ def remaining_tokens
33
+ result = []
34
+
35
+ loop do
36
+ result.push next_token
37
+ break if result.last == [false, false]
38
+ end
39
+
40
+ result
41
+ end
42
+
43
+ private
44
+
45
+ #
46
+ # Updates the line and column counters based on the given text.
47
+ #
48
+ def update_counter(text)
49
+ number_of_newlines = text.count("\n")
50
+
51
+ if number_of_newlines > 0
52
+ @line += text.count("\n")
53
+ @column = text.length - text.rindex("\n")
54
+ else
55
+ @column += text.length
56
+ end
57
+ end
58
+
59
+ #
60
+ # Creates and returns a token with the line and column number from the end of
61
+ # the previous token. Afterwards updates the counter based on the contents
62
+ # of the text. The value of the token is determined by the text given and
63
+ # the type of the token.
64
+ #
65
+ def token(type, text)
66
+ value = case type
67
+ when :INTEGER then text.to_i
68
+ when :REAL then text.to_f
69
+ when :STRING then text[1..-2]
70
+ else text
71
+ end
72
+
73
+ token = Token.new(value, text, @line, @column)
74
+
75
+ update_counter(token.source)
76
+
77
+ [type, token]
78
+ end
79
+
80
+ #
81
+ # Scans the next characters based on the body context (the context the lexer
82
+ # should initially be in), which will attempt to match the opening tokens
83
+ # for statements. Failing that it will parse a text block token.
84
+ #
85
+ def scan_body
86
+ case
87
+ when text = @scanner.scan(/\{\{/)
88
+ @context = :statement
89
+ token(:VAR_OPEN, text)
90
+
91
+ when text = @scanner.scan(/\{%/)
92
+ @context = :statement
93
+ token(:STMT_OPEN, text)
94
+
95
+ when text = @scanner.scan(/\{#/)
96
+ # scan until the end of the comment bracket, ignore the text for all
97
+ # purposes except for advancing the counters appropriately
98
+ comment = @scanner.scan_until(/#\}/)
99
+
100
+ # increment the counters based on the content of the counter
101
+ update_counter(text + comment)
102
+
103
+ # scan in the body context again, since we're not actually returning a
104
+ # token from the comment. Don't scan if we're at the end of the body,
105
+ # just return a terminator token.
106
+ if @scanner.eos?
107
+ [false, false]
108
+ else
109
+ scan_body
110
+ end
111
+
112
+ else
113
+ # scan ahead until we find a variable opening tag or a block opening tag
114
+ text = @scanner.scan_until(/\{[\{%#]/)
115
+
116
+ # if there was no instance of an opening block then just take what remains
117
+ # in the scanner otherwise return the pointer to before the block
118
+ if text
119
+ text = text[0..-3]
120
+ @scanner.pos -= 2
121
+ else
122
+ text = @scanner.rest
123
+ @scanner.terminate
124
+ end
125
+
126
+ token(:TEXT_BLOCK, text)
127
+ end
128
+ end
129
+
130
+ #
131
+ # Scans the next characters based on the statement context, which will ignore
132
+ # whitespace and look for tokens you would expect to find inside any kind
133
+ # of statement.
134
+ #
135
+ def scan_statement
136
+ # eat any whitespace at the start of the string
137
+ whitespace = @scanner.scan_until(/\S/)
138
+
139
+ if whitespace
140
+ @scanner.pos -= 1
141
+ update_counter(whitespace[0..-2])
142
+ end
143
+
144
+ # look for matches
145
+ case
146
+ when text = @scanner.scan(/\}\}/)
147
+ @context = :body
148
+ token(:VAR_CLOSE, text)
149
+
150
+ when text = @scanner.scan(/%\}/)
151
+ @context = :body
152
+ token(:STMT_CLOSE, text)
153
+
154
+ when text = @scanner.scan(/[=]=/) # i've added the square brackets because syntax highlighters dont like /=
155
+ token(:OP_EQ, text)
156
+
157
+ when text = @scanner.scan(/!=/)
158
+ token(:OP_NEQ, text)
159
+
160
+ when text = @scanner.scan(/>=/)
161
+ token(:OP_GEQ, text)
162
+
163
+ when text = @scanner.scan(/<=/)
164
+ token(:OP_LEQ, text)
165
+
166
+ when text = @scanner.scan(/(if|unless|else|endif|endunless|for|in|endfor|block|endblock|extends|end|and|or|not)[\W]/)
167
+ keyword = text[0..-2]
168
+ @scanner.pos -= 1
169
+
170
+ token(keyword.upcase.to_sym, keyword)
171
+
172
+ when text = @scanner.scan(/[+\-]?[0-9]+\.[0-9]+/)
173
+ token(:REAL, text)
174
+
175
+ when text = @scanner.scan(/[+\-]?[1-9][0-9]*|0/)
176
+ token(:INTEGER, text)
177
+
178
+ when text = @scanner.scan(/['][^']*[']|["][^"]*["]/)
179
+ token(:STRING, text)
180
+
181
+ when text = @scanner.scan(/[A-Za-z_][A-Za-z0-9_\.]*/)
182
+ token(:IDENTIFIER, text)
183
+
184
+ else
185
+ next_character = @scanner.getch
186
+ token(next_character, next_character)
187
+ end
188
+ end
189
+ end
190
+
191
+ end