cadenza 0.7.0.rc1

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