ruty 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/context.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# = Ruty Context Class
|
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
|
+
|
10
|
+
module Ruty
|
11
|
+
|
12
|
+
# represents the internal namespace used by ruty
|
13
|
+
# It basically works like a hash just that it has
|
14
|
+
# multiple layers which can be pushed and popped.
|
15
|
+
# That feature is used by the template engine in
|
16
|
+
# loops, blocks and other block elements which set
|
17
|
+
# variables.
|
18
|
+
class Context
|
19
|
+
include Enumerable
|
20
|
+
|
21
|
+
# create a new context instance. initial can be
|
22
|
+
# a hash which represents the initial root stack
|
23
|
+
# which cannot be popped.
|
24
|
+
def initialize initial=nil
|
25
|
+
@stack = [initial || {}]
|
26
|
+
end
|
27
|
+
|
28
|
+
# push a new empty or given hash to the stack.
|
29
|
+
def push hash=nil
|
30
|
+
@stack << (hash or {})
|
31
|
+
end
|
32
|
+
|
33
|
+
# pop the outermost hash from the stack and return
|
34
|
+
# it. The root hash is never popped, in that case
|
35
|
+
# the method returns nil.
|
36
|
+
def pop
|
37
|
+
@stack.pop if @stack.size > 1
|
38
|
+
end
|
39
|
+
|
40
|
+
# manipulate the outermost hash.
|
41
|
+
def []= name, value
|
42
|
+
@stack[-1][name] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
# start a recursive lookup for name.
|
46
|
+
def [] name
|
47
|
+
@stack.each do |hash|
|
48
|
+
val = hash[name]
|
49
|
+
return val if not val.nil?
|
50
|
+
end
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# checks if the key exists in one of the hashes.
|
55
|
+
def has_key? key
|
56
|
+
not send(:[], key).nil?
|
57
|
+
end
|
58
|
+
|
59
|
+
# call a block for each item in the context. if an item
|
60
|
+
# exists in two layers, only the item from the higher
|
61
|
+
# layer is yielded.
|
62
|
+
def each &block
|
63
|
+
found = {}
|
64
|
+
@stack.reverse_each do |hash|
|
65
|
+
hash.each do |key, value|
|
66
|
+
next if found.include?(key)
|
67
|
+
found[key] = hash
|
68
|
+
block.call(key, value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# overrides pretty print so that the output for the debug
|
74
|
+
# tag looks nicer
|
75
|
+
def pretty_print q
|
76
|
+
t = {}
|
77
|
+
each do |key, value|
|
78
|
+
t[key] = value
|
79
|
+
end
|
80
|
+
q.pp_hash(t)
|
81
|
+
end
|
82
|
+
|
83
|
+
# method that resolves dotted names. Internal it first
|
84
|
+
# tries to access hash keys, later array indices and if
|
85
|
+
# this also does not work it looks for a ruty_safe?
|
86
|
+
# function on the object, calls it with the current part
|
87
|
+
# of the dotted name, and if it returns true it calls it
|
88
|
+
# without arguments and uses the output as new object for
|
89
|
+
# the next part.
|
90
|
+
#
|
91
|
+
# {{ foo.bar.blah.42 }}
|
92
|
+
#
|
93
|
+
# could for example resolve this:
|
94
|
+
#
|
95
|
+
# {{ foo['bar']['blah'][42] }}
|
96
|
+
#
|
97
|
+
# call this method only with symbols, numbers and strings
|
98
|
+
# are meant to be catched somewhere first.
|
99
|
+
def resolve path
|
100
|
+
# start a recursive lookup#
|
101
|
+
current = self
|
102
|
+
path.to_s.split(/\./).each do |part|
|
103
|
+
part_sym = part.to_sym
|
104
|
+
# try hash like objects (with has_key? and [])
|
105
|
+
if current.respond_to?(:has_key?) and tmp = current[part_sym]
|
106
|
+
current = tmp
|
107
|
+
# try hash like objects with integers and array. If this
|
108
|
+
# fails we don't try any longer because method names which
|
109
|
+
# start with numbers are illegal.
|
110
|
+
elsif part =~ /^-?\d+$/
|
111
|
+
if current.respond_to?(:fetch) or current.respond_to?(:has_key?) \
|
112
|
+
and tmp = current[part.to_i]
|
113
|
+
current = tmp
|
114
|
+
else
|
115
|
+
return nil
|
116
|
+
end
|
117
|
+
# try method calls on objects with ruty_safe? methods
|
118
|
+
elsif current.respond_to?(:ruty_safe?) and
|
119
|
+
current.ruty_safe?(part_sym)
|
120
|
+
current = current.send(part_sym)
|
121
|
+
# fail with nil in all other cases.
|
122
|
+
else
|
123
|
+
return nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
current
|
128
|
+
end
|
129
|
+
|
130
|
+
# apply filters on a value
|
131
|
+
def apply_filters value, filters
|
132
|
+
filters.each do |filter|
|
133
|
+
name, args = filter[0], filter[1..-1]
|
134
|
+
filter = Filters[name]
|
135
|
+
raise TemplateRuntimeError, "filter '#{name}' missing" if filter.nil?
|
136
|
+
args.map! do |arg|
|
137
|
+
if arg.kind_of?(Symbol)
|
138
|
+
resolve(arg)
|
139
|
+
else
|
140
|
+
arg
|
141
|
+
end
|
142
|
+
end
|
143
|
+
value = filter.call(self, value, *args)
|
144
|
+
end
|
145
|
+
value
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# = Ruty Data Structure
|
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 'stringio'
|
10
|
+
|
11
|
+
module Ruty::Datastructure
|
12
|
+
|
13
|
+
# baseclass for all nodes
|
14
|
+
class Node
|
15
|
+
|
16
|
+
# render the block and call the block for each return value
|
17
|
+
def render_node context, stream
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# node list class. can store multiple nodes
|
22
|
+
class NodeList < Node
|
23
|
+
include Enumerable
|
24
|
+
attr_reader :parser
|
25
|
+
|
26
|
+
def initialize initial=nil, parser=nil
|
27
|
+
@nodes = initial || []
|
28
|
+
@parser = parser
|
29
|
+
end
|
30
|
+
|
31
|
+
def << node
|
32
|
+
@nodes << node
|
33
|
+
end
|
34
|
+
|
35
|
+
def each &block
|
36
|
+
@nodes.each(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def render_node context, stream
|
40
|
+
@nodes.each do |node|
|
41
|
+
node.render_node(context, stream)
|
42
|
+
end
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# a node that stores text data
|
48
|
+
class TextNode < Node
|
49
|
+
|
50
|
+
def initialize text
|
51
|
+
@text = text
|
52
|
+
end
|
53
|
+
|
54
|
+
def render_node context, stream
|
55
|
+
stream << @text
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# a node that stores a variable plus filters
|
61
|
+
class VariableNode < Node
|
62
|
+
|
63
|
+
def initialize name, filters
|
64
|
+
@name = name
|
65
|
+
@filters = filters
|
66
|
+
end
|
67
|
+
|
68
|
+
def render_node context, stream
|
69
|
+
value = context.apply_filters(context.resolve(@name), @filters).to_s
|
70
|
+
stream << value if not value.empty?
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# stream class. Some kind of write only array which just
|
76
|
+
# accepts nodes and can be converted into a nodelist afterwards.
|
77
|
+
class NodeStream
|
78
|
+
def initialize parser
|
79
|
+
@parser = parser
|
80
|
+
@stream = []
|
81
|
+
@nodelist = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# add a new node to the stream
|
85
|
+
def << node
|
86
|
+
raise RuntimeError, 'cannot write to closed stream' if @nodelist
|
87
|
+
@stream << node
|
88
|
+
end
|
89
|
+
|
90
|
+
# convert the streamed data into a nodelist. This
|
91
|
+
# automatically closes the stream, you can't write
|
92
|
+
# to it later.
|
93
|
+
def to_nodelist
|
94
|
+
@nodelist = NodeList.new(@stream, @parser) if not @nodelist
|
95
|
+
@nodelist
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# stream for the tokenize function
|
100
|
+
class TokenStream
|
101
|
+
|
102
|
+
def initialize
|
103
|
+
@stream = []
|
104
|
+
@closed = false
|
105
|
+
@pushed = []
|
106
|
+
end
|
107
|
+
|
108
|
+
def next
|
109
|
+
if not @pushed.empty?
|
110
|
+
@pushed.pop
|
111
|
+
else
|
112
|
+
@stream.pop
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def eos?
|
117
|
+
@stream.empty?
|
118
|
+
end
|
119
|
+
|
120
|
+
# push a token to a closed or nonclosed stream.
|
121
|
+
# pushed tokens are always processed first in
|
122
|
+
# reverse order
|
123
|
+
def push token
|
124
|
+
@pushed << token
|
125
|
+
end
|
126
|
+
|
127
|
+
# add one token to a non closed stream
|
128
|
+
# once the stream is closed you can still add tokens
|
129
|
+
# to the stream but by pushing back which you can do
|
130
|
+
# by calling push.
|
131
|
+
def << token
|
132
|
+
raise RuntimeError, 'cannot write to closed stream' if @closed
|
133
|
+
@stream << token
|
134
|
+
end
|
135
|
+
|
136
|
+
# close the stream and return self
|
137
|
+
def close
|
138
|
+
raise RuntimeError, 'cannot close closed token stream' if @closed
|
139
|
+
@closed = true
|
140
|
+
@stream.reverse!
|
141
|
+
self
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# special class that is used by some ruty tags to
|
146
|
+
# provide data for the context that requires calculation
|
147
|
+
# or rendering and is optional (for example block.super)
|
148
|
+
class Deferred
|
149
|
+
|
150
|
+
def initialize callables=nil
|
151
|
+
@callables = callables || {}
|
152
|
+
end
|
153
|
+
|
154
|
+
def add_deferred name, &block
|
155
|
+
@callables[name] = block
|
156
|
+
end
|
157
|
+
|
158
|
+
def ruty_safe? name
|
159
|
+
@callables.include?(name)
|
160
|
+
end
|
161
|
+
|
162
|
+
def method_missing name
|
163
|
+
@callables[name].call if @callables.include?(name)
|
164
|
+
end
|
165
|
+
|
166
|
+
# override the pretty print callback function so that we
|
167
|
+
# get values instead of just a lot of proc inspect outputs.
|
168
|
+
def pretty_print q
|
169
|
+
unknown = (Class.new{
|
170
|
+
define_method(:inspect) { '?' }
|
171
|
+
}).new
|
172
|
+
t = {}
|
173
|
+
@callables.each do |name, callable|
|
174
|
+
t[name] = callable.call rescue unknown
|
175
|
+
end
|
176
|
+
q.pp_hash(t)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
data/lib/ruty/filters.rb
ADDED
@@ -0,0 +1,188 @@
|
|
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
|
+
require 'uri'
|
10
|
+
|
11
|
+
module Ruty
|
12
|
+
|
13
|
+
# default class for all filter collections
|
14
|
+
class FilterCollection
|
15
|
+
|
16
|
+
# iterate over all filters, used by Filters to
|
17
|
+
# register them.
|
18
|
+
def self.each_filter &block
|
19
|
+
instance = self.new
|
20
|
+
instance_methods(false).each do |method_name|
|
21
|
+
name = method_name.to_sym
|
22
|
+
filter = instance.method(name).to_proc
|
23
|
+
block.call(name, filter)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# builtin filter collection
|
29
|
+
class StandardFilters < FilterCollection
|
30
|
+
|
31
|
+
# convert a string to lowercase
|
32
|
+
def lower context, value
|
33
|
+
value.to_s.downcase
|
34
|
+
end
|
35
|
+
|
36
|
+
# convert a string to uppercase
|
37
|
+
def upper context, value
|
38
|
+
value.to_s.upcase
|
39
|
+
end
|
40
|
+
|
41
|
+
# capitalize a string
|
42
|
+
def capitalize context, value
|
43
|
+
value.to_s.capitalize
|
44
|
+
end
|
45
|
+
|
46
|
+
# truncate a string down to n characters
|
47
|
+
def truncate context, value, n=80, ellipsis='...'
|
48
|
+
if value
|
49
|
+
if (value = value.to_s).length > n
|
50
|
+
value[0...n] + ellipsis
|
51
|
+
else
|
52
|
+
value
|
53
|
+
end
|
54
|
+
else
|
55
|
+
''
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# join an array with a string between the array elements
|
60
|
+
def join context, value, char=''
|
61
|
+
if value.respond_to?(:join)
|
62
|
+
value.join(char)
|
63
|
+
else
|
64
|
+
value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# replace a substring with another
|
69
|
+
# if the replacement string isn't given it replaces it with
|
70
|
+
# an empty value.
|
71
|
+
def replace context, value, search, repl=''
|
72
|
+
value.to_s.gsub(search.to_s, repl.to_s)
|
73
|
+
end
|
74
|
+
|
75
|
+
# return a sorted version of an array or object that
|
76
|
+
# supports sorting. If it does not this function returns
|
77
|
+
# the value unchanged.
|
78
|
+
def sort context, value
|
79
|
+
if value.respond_to?(:sort)
|
80
|
+
value.sort
|
81
|
+
else
|
82
|
+
value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# reverse an item that supports reversing. else return
|
87
|
+
# the item unchanged
|
88
|
+
def reverse context, value
|
89
|
+
if value.respond_to?(:reverse)
|
90
|
+
value.reverse
|
91
|
+
else
|
92
|
+
value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# get the first item of an array
|
97
|
+
def first context, value
|
98
|
+
if value.respond_to?(:first)
|
99
|
+
value.first
|
100
|
+
elsif value.respond_to?(:[])
|
101
|
+
value[0] || value
|
102
|
+
else
|
103
|
+
value
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# get the last item of an array
|
108
|
+
def last context, value
|
109
|
+
if value.respond_to?(:last)
|
110
|
+
value.last
|
111
|
+
elsif value.respond_to?(:[])
|
112
|
+
value[-1] || value
|
113
|
+
else
|
114
|
+
value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# xml escape a string
|
119
|
+
def escape context, value, attribute=false
|
120
|
+
value = value.to_s.gsub(/&/, '&')\
|
121
|
+
.gsub(/>/, '>')\
|
122
|
+
.gsub(/</, '<')
|
123
|
+
value.gsub!(/"/, '"') if attribute
|
124
|
+
value
|
125
|
+
end
|
126
|
+
|
127
|
+
# urlencode an string
|
128
|
+
def urlencode context, value
|
129
|
+
URI.escape(value.to_s)
|
130
|
+
end
|
131
|
+
|
132
|
+
# return the length of an object
|
133
|
+
def length context, value
|
134
|
+
if value.respond_to?(:size)
|
135
|
+
value.size
|
136
|
+
elsif value.respond_to?(:length)
|
137
|
+
value.length
|
138
|
+
elsif value.respond_to?(:count)
|
139
|
+
value.count
|
140
|
+
else
|
141
|
+
0
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
# module used to lookup filters
|
148
|
+
module Filters
|
149
|
+
@collections = {}
|
150
|
+
@filters = {}
|
151
|
+
|
152
|
+
class << self
|
153
|
+
# return filter `name`
|
154
|
+
def [] name
|
155
|
+
@filters[name]
|
156
|
+
end
|
157
|
+
|
158
|
+
# register a new filter collection
|
159
|
+
def register_collection collection
|
160
|
+
return if @collections.include? collection
|
161
|
+
collection.each_filter {|name, filter|
|
162
|
+
@filters[name] = filter
|
163
|
+
}
|
164
|
+
@collections[collection] = true
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
|
168
|
+
# add just one filter using a code block:
|
169
|
+
# Ruty::Filters::add('swapcase') { |context, value|
|
170
|
+
# value.to_s.swap_case
|
171
|
+
# }
|
172
|
+
def add name, &block
|
173
|
+
raise AttributeError, 'block required' if !block
|
174
|
+
@filters[name] = block
|
175
|
+
end
|
176
|
+
|
177
|
+
# return an array of symbols containing the names
|
178
|
+
# of all registered filters.
|
179
|
+
def all
|
180
|
+
return @filters.keys
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# bootstrap filters module and register builtin filters
|
186
|
+
Filters.register_collection(StandardFilters)
|
187
|
+
|
188
|
+
end
|