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