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