ruty 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ # = Ruty Loaders
2
+ #
3
+ # Author:: Armin Ronacher
4
+ #
5
+ # Copyright (c) 2006 by Armin Ronacher
6
+ #
7
+ # You can redistribute it and/or modify it under the terms of the BSD license.
8
+
9
+ module Ruty
10
+
11
+ # base class for all loaders
12
+ class Loader
13
+ attr_reader :options
14
+
15
+ def initialize options=nil
16
+ if not options.nil? and not options.is_a?(Hash)
17
+ raise ArgumentError, 'loader options must be a hash or omitted'
18
+ end
19
+ @options = options or {}
20
+ end
21
+
22
+ # load a template, probably from cache
23
+ # Per default this calls the load_local method with the
24
+ # same paramters
25
+ def load_cached name, parent=nil
26
+ load_local(name, parent)
27
+ end
28
+
29
+ # load a template by always reparsing it. That for example
30
+ # is used by the inheritance and inclusion system because
31
+ # the tags modify the nodelist returned by a loader.
32
+ # If parent is given it must be used to resolve relative
33
+ # paths
34
+ def load_local name, parent=nil
35
+ raise NotImplementedError
36
+ end
37
+
38
+ # method for loading templates in an application. Returns a
39
+ # template instance with the loaded name.
40
+ def get_template name
41
+ Template.new(load_cached(name))
42
+ end
43
+
44
+ end
45
+
46
+ module Loaders
47
+ end
48
+
49
+ # load known builtin loaders
50
+ require 'ruty/loaders/filesystem'
51
+
52
+ end
@@ -0,0 +1,75 @@
1
+ # = Ruty Filesystem Loader
2
+ #
3
+ # Author:: Armin Ronacher
4
+ #
5
+ # Copyright (c) 2006 by Armin Ronacher
6
+ #
7
+ # You can redistribute it and/or modify it under the terms of the BSD license.
8
+
9
+ class Ruty::Loaders::Filesystem < Ruty::Loader
10
+
11
+ # the filesystem loader takes the following arguments:
12
+ #
13
+ # :dirname (required)
14
+ # the path to the folder that includes the templates
15
+ #
16
+ # :suffix (options)
17
+ # the suffix for all templates. For example '.tmpl'.
18
+ # Defaults to an empty string.
19
+ def initialize options=nil
20
+ super(options)
21
+ if not @options.include?(:dirname)
22
+ raise ArgumenError, 'dirname required as argument for filesystem loader'
23
+ end
24
+ @dir = @options[:dirname]
25
+ @suffix = @options[:suffix] || ''
26
+ end
27
+
28
+ def load_local name, parent=nil, path=nil
29
+ path = path || path_for?(name, parent)
30
+ f = File.new(path, 'r')
31
+ begin
32
+ parser = Ruty::Parser.new(f.read, self, name)
33
+ ensure
34
+ f.close
35
+ end
36
+ parser.parse
37
+ end
38
+
39
+ def path_for? name, parent=nil
40
+ # escape name, don't allow access to path parts with a
41
+ # leading dot
42
+ parts = name.split(File::SEPARATOR).select { |p| p and p[0] != ?. }
43
+ path = File.join(@dir, (parent) ?
44
+ path = File.join(File.dirname(parent), parts.join(File::SEPARATOR)) :
45
+ path = parts.join(File::SEPARATOR)
46
+ )
47
+ raise Ruty::TemplateNotFound, name if not File.exist?(path)
48
+ path
49
+ end
50
+
51
+ end
52
+
53
+ # like the normal filesystem loader but uses memcaching
54
+ class Ruty::Loaders::MemcachingFilesystem < Ruty::Loaders::Filesystem
55
+
56
+ # the memcaching filesystem loader takes the
57
+ # same arguments as the normal filesystem loader
58
+ # and additionally a key called :amount that indicates
59
+ # the maximum amount of cached templates. The amount
60
+ # defaults to 20.
61
+ def initialze options=nil
62
+ super(options)
63
+ @amount = @options[:amount] || 20
64
+ @cache = {}
65
+ end
66
+
67
+ def load_cached name, parent=nil
68
+ path = path_for?(name, parent)
69
+ return @cache[path] if @cache.include?(path)
70
+ nodelist = super(name, parent, path)
71
+ @cache.clear if @cache.size >= @amount
72
+ @cache[path] = nodelist
73
+ end
74
+
75
+ end
@@ -0,0 +1,283 @@
1
+ # = Ruty Parser
2
+ #
3
+ # Author:: Armin Ronacher
4
+ #
5
+ # Copyright (c) 2006 by Armin Ronacher
6
+ #
7
+ # You can redistribute it and/or modify it under the terms of the BSD license.
8
+
9
+ require 'strscan'
10
+
11
+
12
+ module Ruty
13
+
14
+ # the parser class. parses a given template into a nodelist
15
+ class Parser
16
+
17
+ TAG_REGEX = /
18
+ (.*?)(?:
19
+ #{Regexp.escape(Constants::BLOCK_START)} (.*?)
20
+ #{Regexp.escape(Constants::BLOCK_END)} |
21
+ #{Regexp.escape(Constants::VAR_START)} (.*?)
22
+ #{Regexp.escape(Constants::VAR_END)} |
23
+ #{Regexp.escape(Constants::COMMENT_START)} (.*?)
24
+ #{Regexp.escape(Constants::COMMENT_END)}
25
+ )
26
+ /xim
27
+
28
+ STRING_ESCAPE_REGEX = /\\([\\nt'"])/m
29
+
30
+ # create a new parser for a given sourcecode and an optional
31
+ # template loader. If a template loader is given the template
32
+ # inheritance and include system will work. Otherwise those
33
+ # tags create an error.
34
+ # If name is given it will be used by the loaders to resolve
35
+ # relative paths on inclutions/inheritance
36
+ def initialize source, loader=nil, name=nil
37
+ @source = source
38
+ @loader = loader
39
+ @name = name
40
+ end
41
+
42
+ # parse the template and return a nodelist representing
43
+ # the parsed template.
44
+ def parse
45
+ controller = ParserController::new(tokenize, @loader, @name)
46
+ controller.parse_all
47
+ end
48
+
49
+ # tokenize the sourcecode and return an array of tokens
50
+ def tokenize
51
+ result = Datastructure::TokenStream.new
52
+ @source.scan(TAG_REGEX).each do |match|
53
+ result << [:text, match[0]] if match[0] and not match[0].empty?
54
+ if data = match[1]
55
+ result << [:block, data.strip]
56
+ elsif data = match[2]
57
+ result << [:var, data.strip]
58
+ elsif data = match[3]
59
+ result << [:comment, data.strip]
60
+ end
61
+ end
62
+ rest = @source[$~.end(0)..-1]
63
+ result << [:text, rest] if not rest.empty?
64
+ result.close
65
+ end
66
+
67
+ # helper function for parsing arguments
68
+ # the return value will be a list of lists. names are returned
69
+ # as symbols, strings as strings, numbers as floats or integers,
70
+ # filters are returned as arrays:
71
+ #
72
+ # for item in seq
73
+ #
74
+ # results in:
75
+ #
76
+ # [:for, :item, :in, :seq]
77
+ #
78
+ # This:
79
+ #
80
+ # user.username|lower|replace '\'', '"'
81
+ #
82
+ # results in:
83
+ #
84
+ # [:"user.username", [:lower], [:replace, "'", "\""]]
85
+ def self.parse_arguments arguments
86
+ lexer = Parser::ArgumentLexer.new(arguments)
87
+ result = cur_buffer = []
88
+ filter_buffer = []
89
+
90
+ lexer.lex do |token, value|
91
+ if token == :filter_start
92
+ cur_buffer = filter_buffer.clear
93
+ elsif token == :filter_end
94
+ result << filter_buffer.dup if not filter_buffer.empty?
95
+ cur_buffer = result
96
+ elsif token == :name
97
+ cur_buffer << value.to_sym
98
+ elsif token == :number
99
+ cur_buffer << (value.include?('.') ? value.to_f : value.to_i)
100
+ elsif token == :string
101
+ cur_buffer << value[1...-1].gsub(STRING_ESCAPE_REGEX) {
102
+ $1.tr!(%q{\\\\nt"'}, %q{\\\\\n\t"'})
103
+ }
104
+ end
105
+ end
106
+
107
+ result
108
+ end
109
+
110
+ # class for parsing arguments. used by the parse_arguments
111
+ # function. It's usualy a better idea to not use this class
112
+ # yourself because it's pretty low level.
113
+ class ArgumentLexer
114
+
115
+ # lexer constants
116
+ WHITESPACE_RE = /\s+/m
117
+ NAME_RE = /[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*/
118
+ PIPE_RE = /\|/
119
+ FILTER_END_RE = /;/
120
+ SEPARATOR_RE = /,/
121
+ STRING_RE = /
122
+ (?:
123
+ "([^"\\]*(?:\\.[^"\\]*)*)"
124
+ |
125
+ '([^'\\]*(?:\\.[^'\\]*)*)'
126
+ )
127
+ /xm
128
+ NUMBER_RE = /\d+(\.\d*)?/
129
+
130
+ # create a new ArgumentLexer for the given string
131
+ def initialize string
132
+ @string = string
133
+ end
134
+
135
+ # lex the string. For each return token this function calls
136
+ # the given block with the token type and value.
137
+ def lex &block
138
+ s = StringScanner.new(@string)
139
+ state = :initial
140
+
141
+ while not s.eos?
142
+ # supress whitespace. no matter in which state we are.
143
+ next if s.scan(WHITESPACE_RE)
144
+
145
+ # normal data, no filters.
146
+ if state == :initial
147
+ if match = s.scan(NAME_RE)
148
+ block.call(:name, match)
149
+ elsif s.scan(PIPE_RE)
150
+ state = :filter
151
+ block.call(:filter_start, nil)
152
+ elsif s.scan(SEPARATOR_RE)
153
+ block.call(:separator, nil)
154
+ elsif match = s.scan(STRING_RE)
155
+ block.call(:string, match)
156
+ elsif match = s.scan(NUMBER_RE)
157
+ block.call(:number, match)
158
+ else
159
+ # nothing matched and we are not at the end of
160
+ # the string, raise an error
161
+ raise TemplateSyntaxError, 'unexpected character ' \
162
+ "'#{s.getch}' in block tag"
163
+ end
164
+
165
+ # filter syntax
166
+ elsif state == :filter
167
+ # if a second pipe occours we start a new filter
168
+ if s.scan(PIPE_RE)
169
+ block.call(:filter_end, nil)
170
+ block.call(:filter_start, nil)
171
+ # filter ends with ; -- handle that
172
+ elsif s.scan(FILTER_END_RE)
173
+ block.call(:filter_end, nil)
174
+ state = :initial
175
+ elsif s.scan(SEPARATOR_RE)
176
+ block.call(:separator, nil)
177
+ # variables, strings and numbers
178
+ elsif match = s.scan(NAME_RE)
179
+ block.call(:name, match)
180
+ elsif match = s.scan(STRING_RE)
181
+ block.call(:string, match)
182
+ elsif match = s.scan(NUMBER_RE)
183
+ block.call(:number, match)
184
+ else
185
+ # nothing matched and we are not at the end of
186
+ # the string, raise an error
187
+ raise TemplateSyntaxError, 'unexpected character ' \
188
+ "'#{s.getch}' in filter def"
189
+ end
190
+ end
191
+ end
192
+ block.call(:filter_end, nil) if state == :filter
193
+ end
194
+ end
195
+ end
196
+
197
+ # parser controller. does the actual parsing, used by
198
+ # the tags to control it.
199
+ class ParserController
200
+
201
+ attr_reader :tokenstream, :storage, :first
202
+
203
+ def initialize stream, loader, name
204
+ @loader = loader
205
+ @name = name
206
+ @tokenstream = stream
207
+ @first = true
208
+ @storage = {}
209
+ end
210
+
211
+ # fail with an template syntax error exception.
212
+ def fail msg
213
+ raise TemplateSyntaxError, msg
214
+ end
215
+
216
+ # alias for the method with the same name on the
217
+ # parser class
218
+ def parse_arguments arguments
219
+ Parser::parse_arguments(arguments)
220
+ end
221
+
222
+ # use the loader to load a subtemplate
223
+ def load_local name
224
+ raise TemplateRuntimeError, 'no loader defined' if not @loader
225
+ @loader.load_local(name, @name)
226
+ end
227
+
228
+ # parse everything until the block returns true
229
+ def parse_until &block
230
+ result = Datastructure::NodeStream.new(self)
231
+ while not @tokenstream.eos?
232
+ token, value = @tokenstream.next
233
+
234
+ # text tokens are returned just if the arn't empty
235
+ if token == :text
236
+ @first = false if @first and not value.strip.empty?
237
+ result << Datastructure::TextNode.new(value) \
238
+ if not value.empty?
239
+
240
+ # variables leave the parser just if they have just
241
+ # one name and some optional filters on it.
242
+ elsif token == :var
243
+ @first = false
244
+ names = []
245
+ filters = []
246
+ Parser::parse_arguments(value).each do |arg|
247
+ if arg.is_a?(Array)
248
+ filters << arg
249
+ else
250
+ names << arg
251
+ end
252
+ end
253
+
254
+ fail('Invalid syntax for variable node') if names.size != 1
255
+ result << Datastructure::VariableNode.new(names[0], filters)
256
+
257
+ # blocks are a bit more complicated. first they can act as
258
+ # needle tokens for other blocks, on the other hand blocks
259
+ # can have their own subprogram
260
+ elsif token == :block
261
+ p = value.split(/\s+/, 2)
262
+ name = p[0].to_sym
263
+ args = p[1] || ''
264
+ if block.call(name, args)
265
+ @first = false
266
+ return result.to_nodelist
267
+ end
268
+
269
+ tag = Tags[name]
270
+ fail("Unknown tag #{name.inspect}") if tag.nil?
271
+ result << tag.new(self, args)
272
+ @first = false
273
+ end
274
+ end
275
+ result.to_nodelist
276
+ end
277
+
278
+ # parse everything and return the nodelist for it
279
+ def parse_all
280
+ parse_until { false }
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,52 @@
1
+ # = Ruty Builtin Tags
2
+ #
3
+ # Author:: Armin Ronacher
4
+ #
5
+ # Copyright (c) 2006 by Armin Ronacher
6
+ #
7
+ # You can redistribute it and/or modify it under the terms of the BSD license.
8
+
9
+ module Ruty
10
+
11
+ # base class for all ruty tags
12
+ class Tag < Datastructure::Node
13
+ end
14
+
15
+ # builtin tags
16
+ module Tags
17
+ @tags = {}
18
+
19
+ class << self
20
+ # function for quickly looking up tags by name
21
+ def [] name
22
+ @tags[name]
23
+ end
24
+
25
+ # array of all known tags by name
26
+ def all
27
+ @tags.keys
28
+ end
29
+
30
+ # function used for registering a new tag.
31
+ def register tag, name
32
+ @tags[name] = tag
33
+ end
34
+
35
+ # function used to unregister a function
36
+ def unregister name
37
+ @tags.delete(name)
38
+ end
39
+ end
40
+ end
41
+
42
+ # load known builtin tags
43
+ require 'ruty/tags/forloop'
44
+ require 'ruty/tags/conditional'
45
+ require 'ruty/tags/looptools'
46
+ require 'ruty/tags/inheritance'
47
+ require 'ruty/tags/inclusion'
48
+ require 'ruty/tags/debug'
49
+ require 'ruty/tags/filter'
50
+ require 'ruty/tags/capture'
51
+
52
+ end