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.
- data/lib/ruty.rb +450 -0
- data/lib/ruty/constants.rb +23 -0
- data/lib/ruty/context.rb +148 -0
- data/lib/ruty/datastructure.rb +180 -0
- data/lib/ruty/filters.rb +188 -0
- data/lib/ruty/loaders.rb +52 -0
- data/lib/ruty/loaders/filesystem.rb +75 -0
- data/lib/ruty/parser.rb +283 -0
- data/lib/ruty/tags.rb +52 -0
- data/lib/ruty/tags/capture.rb +28 -0
- data/lib/ruty/tags/conditional.rb +59 -0
- data/lib/ruty/tags/debug.rb +28 -0
- data/lib/ruty/tags/filter.rb +27 -0
- data/lib/ruty/tags/forloop.rb +83 -0
- data/lib/ruty/tags/inclusion.rb +31 -0
- data/lib/ruty/tags/inheritance.rb +80 -0
- data/lib/ruty/tags/looptools.rb +85 -0
- metadata +65 -0
data/lib/ruty/loaders.rb
ADDED
@@ -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
|
data/lib/ruty/parser.rb
ADDED
@@ -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
|
data/lib/ruty/tags.rb
ADDED
@@ -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
|