ruty 0.0.1

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,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