ruty 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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(/&/, '&amp;')\
121
+ .gsub(/>/, '&gt;')\
122
+ .gsub(/</, '&lt;')
123
+ value.gsub!(/"/, '&quot;') 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