arrow 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (198) hide show
  1. data/ChangeLog +1590 -0
  2. data/LICENSE +28 -0
  3. data/README +75 -0
  4. data/Rakefile +366 -0
  5. data/Rakefile.local +63 -0
  6. data/data/arrow/applets/TEMPLATE.rb.tpl +53 -0
  7. data/data/arrow/applets/args.rb +50 -0
  8. data/data/arrow/applets/config.rb +55 -0
  9. data/data/arrow/applets/error.rb +63 -0
  10. data/data/arrow/applets/files.rb +46 -0
  11. data/data/arrow/applets/inspect.rb +46 -0
  12. data/data/arrow/applets/nosuchapplet.rb +31 -0
  13. data/data/arrow/applets/status.rb +92 -0
  14. data/data/arrow/applets/test.rb +133 -0
  15. data/data/arrow/applets/tutorial/counter.rb +96 -0
  16. data/data/arrow/applets/tutorial/dingus.rb +67 -0
  17. data/data/arrow/applets/tutorial/hello.rb +34 -0
  18. data/data/arrow/applets/tutorial/hello2.rb +73 -0
  19. data/data/arrow/applets/tutorial/imgtext.rb +90 -0
  20. data/data/arrow/applets/tutorial/imgtext2.rb +286 -0
  21. data/data/arrow/applets/tutorial/index.rb +36 -0
  22. data/data/arrow/applets/tutorial/logo.rb +98 -0
  23. data/data/arrow/applets/tutorial/memcache.rb +61 -0
  24. data/data/arrow/applets/tutorial/missing.rb +37 -0
  25. data/data/arrow/applets/tutorial/protected.rb +100 -0
  26. data/data/arrow/applets/tutorial/redirector.rb +52 -0
  27. data/data/arrow/applets/tutorial/rndimages.rb +159 -0
  28. data/data/arrow/applets/tutorial/sharenotes.rb +83 -0
  29. data/data/arrow/applets/tutorial/subclassed-hello.rb +32 -0
  30. data/data/arrow/applets/tutorial/superhello.rb +72 -0
  31. data/data/arrow/applets/tutorial/timeclock.rb +78 -0
  32. data/data/arrow/applets/view-applet.rb +123 -0
  33. data/data/arrow/applets/view-template.rb +85 -0
  34. data/data/arrow/applets/wiki.rb +274 -0
  35. data/data/arrow/templates/TEMPLATE.tmpl.tpl +36 -0
  36. data/data/arrow/templates/applet-status.tmpl +153 -0
  37. data/data/arrow/templates/args-display.tmpl +120 -0
  38. data/data/arrow/templates/config/display-table.tmpl +36 -0
  39. data/data/arrow/templates/config/display.tmpl +36 -0
  40. data/data/arrow/templates/counter-deleted.tmpl +33 -0
  41. data/data/arrow/templates/counter.tmpl +59 -0
  42. data/data/arrow/templates/dingus.tmpl +55 -0
  43. data/data/arrow/templates/enumtable.tmpl +8 -0
  44. data/data/arrow/templates/error-display.tmpl +92 -0
  45. data/data/arrow/templates/filemap.tmpl +89 -0
  46. data/data/arrow/templates/hello-world-src.tmpl +34 -0
  47. data/data/arrow/templates/hello-world.tmpl +60 -0
  48. data/data/arrow/templates/imgtext/fontlist.tmpl +46 -0
  49. data/data/arrow/templates/imgtext/form.tmpl +70 -0
  50. data/data/arrow/templates/imgtext/reload-error.tmpl +40 -0
  51. data/data/arrow/templates/imgtext/reload.tmpl +55 -0
  52. data/data/arrow/templates/inspect/display.tmpl +80 -0
  53. data/data/arrow/templates/loginform.tmpl +64 -0
  54. data/data/arrow/templates/logout.tmpl +32 -0
  55. data/data/arrow/templates/memcache/display.tmpl +41 -0
  56. data/data/arrow/templates/navbar.incl +27 -0
  57. data/data/arrow/templates/nosuchapplet.tmpl +32 -0
  58. data/data/arrow/templates/printsource.tmpl +35 -0
  59. data/data/arrow/templates/protected.tmpl +36 -0
  60. data/data/arrow/templates/rndimages.tmpl +38 -0
  61. data/data/arrow/templates/service-response.tmpl +13 -0
  62. data/data/arrow/templates/sharenotes/display.tmpl +38 -0
  63. data/data/arrow/templates/status.tmpl +120 -0
  64. data/data/arrow/templates/templateviewer.tmpl +43 -0
  65. data/data/arrow/templates/test/harness.tmpl +57 -0
  66. data/data/arrow/templates/test/list.tmpl +48 -0
  67. data/data/arrow/templates/test/problem.tmpl +42 -0
  68. data/data/arrow/templates/tutorial/index.tmpl +37 -0
  69. data/data/arrow/templates/tutorial/missingapplet.tmpl +29 -0
  70. data/data/arrow/templates/view-applet-nosuch.tmpl +32 -0
  71. data/data/arrow/templates/view-applet.tmpl +40 -0
  72. data/data/arrow/templates/view-template.tmpl +83 -0
  73. data/data/arrow/templates/wiki/formerror.tmpl +47 -0
  74. data/data/arrow/templates/wiki/markup_help.incl +6 -0
  75. data/data/arrow/templates/wiki/new.tmpl +56 -0
  76. data/data/arrow/templates/wiki/new_system.tmpl +122 -0
  77. data/data/arrow/templates/wiki/sectionlist.tmpl +43 -0
  78. data/data/arrow/templates/wiki/show.tmpl +34 -0
  79. data/docs/manual/layouts/default.page +43 -0
  80. data/docs/manual/lib/api-filter.rb +81 -0
  81. data/docs/manual/lib/editorial-filter.rb +64 -0
  82. data/docs/manual/lib/examples-filter.rb +244 -0
  83. data/docs/manual/lib/links-filter.rb +117 -0
  84. data/lib/apache/fakerequest.rb +448 -0
  85. data/lib/apache/logger.rb +33 -0
  86. data/lib/arrow.rb +51 -0
  87. data/lib/arrow/acceptparam.rb +207 -0
  88. data/lib/arrow/applet.rb +725 -0
  89. data/lib/arrow/appletmixins.rb +218 -0
  90. data/lib/arrow/appletregistry.rb +590 -0
  91. data/lib/arrow/applettestcase.rb +503 -0
  92. data/lib/arrow/broker.rb +255 -0
  93. data/lib/arrow/cache.rb +176 -0
  94. data/lib/arrow/config-loaders/yaml.rb +75 -0
  95. data/lib/arrow/config.rb +615 -0
  96. data/lib/arrow/constants.rb +24 -0
  97. data/lib/arrow/cookie.rb +359 -0
  98. data/lib/arrow/cookieset.rb +108 -0
  99. data/lib/arrow/dispatcher.rb +368 -0
  100. data/lib/arrow/dispatcherloader.rb +50 -0
  101. data/lib/arrow/exceptions.rb +61 -0
  102. data/lib/arrow/fallbackhandler.rb +48 -0
  103. data/lib/arrow/formvalidator.rb +631 -0
  104. data/lib/arrow/htmltokenizer.rb +343 -0
  105. data/lib/arrow/logger.rb +488 -0
  106. data/lib/arrow/logger/apacheoutputter.rb +69 -0
  107. data/lib/arrow/logger/arrayoutputter.rb +63 -0
  108. data/lib/arrow/logger/coloroutputter.rb +111 -0
  109. data/lib/arrow/logger/fileoutputter.rb +96 -0
  110. data/lib/arrow/logger/htmloutputter.rb +54 -0
  111. data/lib/arrow/logger/outputter.rb +123 -0
  112. data/lib/arrow/mixins.rb +425 -0
  113. data/lib/arrow/monkeypatches.rb +94 -0
  114. data/lib/arrow/object.rb +117 -0
  115. data/lib/arrow/path.rb +196 -0
  116. data/lib/arrow/service.rb +447 -0
  117. data/lib/arrow/session.rb +289 -0
  118. data/lib/arrow/session/dbstore.rb +100 -0
  119. data/lib/arrow/session/filelock.rb +160 -0
  120. data/lib/arrow/session/filestore.rb +132 -0
  121. data/lib/arrow/session/id.rb +98 -0
  122. data/lib/arrow/session/lock.rb +253 -0
  123. data/lib/arrow/session/md5id.rb +42 -0
  124. data/lib/arrow/session/nulllock.rb +42 -0
  125. data/lib/arrow/session/posixlock.rb +166 -0
  126. data/lib/arrow/session/sha1id.rb +54 -0
  127. data/lib/arrow/session/store.rb +366 -0
  128. data/lib/arrow/session/usertrackid.rb +52 -0
  129. data/lib/arrow/spechelpers.rb +73 -0
  130. data/lib/arrow/template.rb +713 -0
  131. data/lib/arrow/template/attr.rb +31 -0
  132. data/lib/arrow/template/call.rb +31 -0
  133. data/lib/arrow/template/comment.rb +33 -0
  134. data/lib/arrow/template/container.rb +118 -0
  135. data/lib/arrow/template/else.rb +41 -0
  136. data/lib/arrow/template/elsif.rb +44 -0
  137. data/lib/arrow/template/escape.rb +53 -0
  138. data/lib/arrow/template/export.rb +87 -0
  139. data/lib/arrow/template/for.rb +145 -0
  140. data/lib/arrow/template/if.rb +78 -0
  141. data/lib/arrow/template/import.rb +119 -0
  142. data/lib/arrow/template/include.rb +206 -0
  143. data/lib/arrow/template/iterator.rb +208 -0
  144. data/lib/arrow/template/nodes.rb +734 -0
  145. data/lib/arrow/template/parser.rb +571 -0
  146. data/lib/arrow/template/prettyprint.rb +53 -0
  147. data/lib/arrow/template/render.rb +191 -0
  148. data/lib/arrow/template/selectlist.rb +94 -0
  149. data/lib/arrow/template/set.rb +87 -0
  150. data/lib/arrow/template/timedelta.rb +81 -0
  151. data/lib/arrow/template/unless.rb +78 -0
  152. data/lib/arrow/template/urlencode.rb +51 -0
  153. data/lib/arrow/template/yield.rb +139 -0
  154. data/lib/arrow/templatefactory.rb +125 -0
  155. data/lib/arrow/testcase.rb +567 -0
  156. data/lib/arrow/transaction.rb +608 -0
  157. data/rake/191_compat.rb +26 -0
  158. data/rake/dependencies.rb +76 -0
  159. data/rake/documentation.rb +114 -0
  160. data/rake/helpers.rb +502 -0
  161. data/rake/hg.rb +282 -0
  162. data/rake/manual.rb +787 -0
  163. data/rake/packaging.rb +129 -0
  164. data/rake/publishing.rb +278 -0
  165. data/rake/style.rb +62 -0
  166. data/rake/svn.rb +668 -0
  167. data/rake/testing.rb +187 -0
  168. data/rake/verifytask.rb +64 -0
  169. data/spec/arrow/acceptparam_spec.rb +157 -0
  170. data/spec/arrow/applet_spec.rb +575 -0
  171. data/spec/arrow/appletmixins_spec.rb +409 -0
  172. data/spec/arrow/appletregistry_spec.rb +294 -0
  173. data/spec/arrow/broker_spec.rb +153 -0
  174. data/spec/arrow/config_spec.rb +224 -0
  175. data/spec/arrow/cookieset_spec.rb +164 -0
  176. data/spec/arrow/dispatcher_spec.rb +137 -0
  177. data/spec/arrow/dispatcherloader_spec.rb +65 -0
  178. data/spec/arrow/formvalidator_spec.rb +781 -0
  179. data/spec/arrow/logger_spec.rb +346 -0
  180. data/spec/arrow/mixins_spec.rb +120 -0
  181. data/spec/arrow/service_spec.rb +645 -0
  182. data/spec/arrow/session_spec.rb +121 -0
  183. data/spec/arrow/template/iterator_spec.rb +222 -0
  184. data/spec/arrow/templatefactory_spec.rb +185 -0
  185. data/spec/arrow/transaction_spec.rb +319 -0
  186. data/spec/arrow_spec.rb +37 -0
  187. data/spec/lib/appletmatchers.rb +281 -0
  188. data/spec/lib/constants.rb +77 -0
  189. data/spec/lib/helpers.rb +41 -0
  190. data/spec/lib/matchers.rb +44 -0
  191. data/tests/cookie.tests.rb +310 -0
  192. data/tests/path.tests.rb +157 -0
  193. data/tests/session.tests.rb +111 -0
  194. data/tests/session_id.tests.rb +82 -0
  195. data/tests/session_lock.tests.rb +191 -0
  196. data/tests/session_store.tests.rb +53 -0
  197. data/tests/template.tests.rb +1360 -0
  198. metadata +339 -0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'arrow/exceptions'
4
+ require 'arrow/session/id'
5
+
6
+ # The Arrow::Session::UsertrackId class, a derivative of Arrow::Session::Id.
7
+ # This class creates session id objects which uses Apache's builtin
8
+ # mod_usertrack for the session key.
9
+ #
10
+ # == Authors
11
+ #
12
+ # * Michael Granger <ged@FaerieMUD.org>
13
+ #
14
+ # Please see the file LICENSE in the top-level directory for licensing details.
15
+ #
16
+ class Arrow::Session::UserTrackId < Arrow::Session::Id
17
+
18
+
19
+
20
+ #############################################################
21
+ ### C L A S S M E T H O D S
22
+ #############################################################
23
+
24
+ ### Returns an untainted copy of the specified +idstring+ if it is in
25
+ ### the expected form for this type of id.
26
+ def self::validate( uri, idstring )
27
+ return nil if idstring.nil?
28
+ rval = idstring[/^[\w.]+\.\d+$/] or return nil?
29
+ rval.untaint
30
+ return rval
31
+ end
32
+
33
+
34
+ ### Generate a new id string for the given request
35
+ def self::generate( uri, request )
36
+ if uri.path
37
+ cookieName = uri.path.sub( %r{^/}, '' )
38
+ else
39
+ cookieName = 'Apache'
40
+ end
41
+
42
+ unless request.cookies.key?( cookieName )
43
+ raise SessionError, "No cookie named '%s' was found. Make sure "\
44
+ "mod_usertrack is enabled and configured correctly" %
45
+ cookieName
46
+ end
47
+
48
+ return validate( uri, request.cookies[cookieName].value )
49
+ end
50
+
51
+ end # class Arrow::Session::UsertrackId
52
+
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+
5
+ require 'apache/fakerequest'
6
+
7
+ require 'arrow'
8
+ require 'arrow/applet'
9
+
10
+ #
11
+ # A collection of helper methods and classes for RSpec applet specifications
12
+ #
13
+ # == Synopsis
14
+ #
15
+ # require 'arrow/spechelpers'
16
+ #
17
+ # describe "SomeApplet" do
18
+ # include Arrow::AppletFixtures
19
+ #
20
+ # before( :all ) do
21
+ # @appletclass = load_appletclass( "someapplet" )
22
+ # end
23
+ #
24
+ # before( :each ) do
25
+ # @applet = @appletclass.new( nil, nil, nil )
26
+ # end
27
+ # end
28
+ #
29
+ # == Authors
30
+ #
31
+ # * Michael Granger <mgranger@laika.com>
32
+ #
33
+ # Please see the file LICENSE in the top-level directory for licensing details.
34
+ #
35
+ module Arrow::SpecHelpers
36
+
37
+ TEST_HEADERS = {
38
+ 'Host' => 'www.example.com:80',
39
+ 'User-Agent' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4',
40
+ 'Accept' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
41
+ 'Accept-Language' => 'en-us,en;q=0.5',
42
+ 'Accept-Encoding' => 'gzip,deflate',
43
+ 'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
44
+ 'Keep-Alive' => '300',
45
+ 'Connection' => 'keep-alive',
46
+ 'Referer' => 'https://www.example.com/',
47
+ }
48
+
49
+
50
+ ### Find directories that applets live in (current just searches the CWD for subdirectories
51
+ ### called 'applets')
52
+ def find_applet_directories
53
+ basedir = Pathname.pwd
54
+ return Pathname.glob( basedir + '**' + 'applets' ).
55
+ find_all {|path| path.directory? && path.readable? }
56
+ end
57
+
58
+
59
+ ### Load an appletclass for the specified +name+ and return it.
60
+ def load_appletclass( name )
61
+ dirs = self.find_applet_directories
62
+ appletfiles = dirs.collect {|dir| Pathname.glob(dir + "**/#{name}.rb") }.flatten
63
+
64
+ if appletfiles.empty?
65
+ raise "Couldn't find an applet named '#{name}' in applet path %s" %
66
+ [ dirs.collect {|dir| dir.to_s}.join(':') ]
67
+ end
68
+
69
+ return Arrow::Applet.load( appletfiles.first ).first
70
+ end
71
+
72
+ end # Arrow::SpecHelpers
73
+
@@ -0,0 +1,713 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'forwardable'
4
+
5
+ require 'arrow/mixins'
6
+ require 'arrow/exceptions'
7
+ require 'arrow/object'
8
+ require 'arrow/path'
9
+
10
+
11
+ # The Arrow::Template class, instances of which are used to
12
+ # generate output for Arrow applications.
13
+ #
14
+ # == Synopsis
15
+ #
16
+ # :TODO: Write some useful Arrow::Template examples
17
+ #
18
+ # == Authors
19
+ #
20
+ # * Michael Granger <ged@FaerieMUD.org>
21
+ #
22
+ # Please see the file LICENSE in the top-level directory for licensing details.
23
+ #
24
+ class Arrow::Template < Arrow::Object
25
+ extend Forwardable
26
+ include Arrow::HashUtilities
27
+
28
+ require 'arrow/template/parser'
29
+ require 'arrow/template/nodes'
30
+ require 'arrow/template/iterator'
31
+
32
+
33
+ # Configuration defaults. Valid members are the same as those listed for
34
+ # the +config+ item of the #new method.
35
+ DEFAULTS = {
36
+ :parserClass => Arrow::Template::Parser,
37
+ :elideDirectiveLines => true,
38
+ :debuggingComments => false,
39
+ :commentStart => '<!-- ',
40
+ :commentEnd => ' -->',
41
+ :strictAttributes => false,
42
+ }
43
+ DEFAULTS.freeze
44
+
45
+ # A Hash which specifies the default renderers for different classes of
46
+ # objects.
47
+ DEFAULT_RENDERERS = {
48
+ Arrow::Template => lambda {|subtempl,templ|
49
+ subtempl.render( nil, nil, templ )
50
+ },
51
+ ::Object => :to_s,
52
+ ::Array => lambda {|ary,tmpl|
53
+ tmpl.render_objects(*ary)
54
+ },
55
+ ::Hash => lambda {|hsh,tmpl|
56
+ hsh.collect do |k,v| tmpl.render_objects(k, ": ", v) end
57
+ },
58
+ ::Method => lambda {|meth,tmpl|
59
+ tmpl.render_objects( meth.call )
60
+ },
61
+ ::Exception => lambda {|err,tmpl|
62
+ tmpl.render_comment "%s: %s: %s" % [
63
+ err.class.name,
64
+ err.message,
65
+ err.backtrace ? err.backtrace[0] : "Stupid exception with no backtrace.",
66
+ ]
67
+ },
68
+ }
69
+
70
+
71
+ ### A class for objects which contain the execution space of all the
72
+ ### code in a single rendering of a template.
73
+ class RenderingScope < Arrow::Object
74
+
75
+ ### Create a new RenderingScope object with the specified
76
+ ### definitions +defs+. Each key => value pair in +defs+ will become
77
+ ### singleton accessors on the resulting object.
78
+ def initialize( defs={} )
79
+ @definitions = []
80
+ self.add_definition_set( defs )
81
+ end
82
+
83
+
84
+ ######
85
+ public
86
+ ######
87
+
88
+ # The stack of definition contexts being represented by the scope.
89
+ #attr_reader :definitions
90
+
91
+
92
+ ### Fetch the Binding object for the RenderingScope.
93
+ def get_binding; binding; end
94
+
95
+
96
+ ### Add the specified definitions +defs+ to the object.
97
+ def add_definition_set( defs )
98
+ # self.log.debug "adding definition set: %p" % [ defs ]
99
+ @definitions.push( defs )
100
+
101
+ defs.each do |name,val|
102
+ raise Arrow::ScopeError, "Cannot add a definition with a blank key" if
103
+ name.to_s.empty?
104
+ raise Arrow::ScopeError, "Cannot override @definitions" if
105
+ name == 'definitions'
106
+ @definitions.last[ name ] = val
107
+
108
+ # Add accessor and ivar for the definition if it doesn't
109
+ # already have one.
110
+ unless self.respond_to?( name.to_s.to_sym )
111
+ #self.log.debug "Adding accessor for %s" % name
112
+ (class << self; self; end).instance_eval {
113
+ attr_accessor name.to_s.to_sym
114
+ }
115
+ else
116
+ #self.log.debug "Already have an accessor for '#{name}'"
117
+ end
118
+
119
+ self.instance_variable_set( "@#{name}", defs[name] )
120
+ end
121
+ end
122
+
123
+
124
+ ### Remove the specified definitions +defs+ from the object. Using
125
+ ### a definition so removed after this point will raise an error.
126
+ def remove_definition_set
127
+ #self.log.debug "Removing definition set from stack of %d frames" %
128
+ # @definitions.nitems
129
+ defs = @definitions.pop
130
+ #self.log.debug "Removing defs: %p, %d frames left" %
131
+ # [ defs, @definitions.nitems ]
132
+
133
+ defs.keys.each do |name|
134
+ next if name == 'definitions'
135
+
136
+ # If there was already a definition in effect with the same
137
+ # name in a previous scope, fetch it so we can play with it.
138
+ previousSet = @definitions.reverse.find {|set| set.key?(name)}
139
+ #self.log.debug "Found previousSet %p for %s in scope stack of %d frames" %
140
+ # [ previousSet, name, @definitions.nitems ]
141
+
142
+ # If none of the previous definition sets had a definition
143
+ # with the same name, remove the accessor and the ivar
144
+ unless previousSet
145
+ #self.log.debug "Removing definition '%s' entirely" % name
146
+ (class << self; self; end).module_eval {
147
+ remove_method name.to_s.to_sym
148
+ }
149
+ remove_instance_variable( "@#{name}" )
150
+
151
+ # Otherwise just reset the ivar to the previous value
152
+ else
153
+ #self.log.debug "Restoring previous def for '%s'" % name
154
+ self.instance_variable_set( "@#{name}", previousSet[name] )
155
+ end
156
+ end
157
+
158
+ end
159
+
160
+
161
+ ### Override the given definitions +defs+ for the duration of the
162
+ ### given block. After the block exits, the original definitions
163
+ ### will be restored.
164
+ def override( defs ) # :yields: receiver
165
+ begin
166
+ #self.log.debug "Before adding definitions: %d scope frame/s. Last: %p" %
167
+ # [ @definitions.nitems, @definitions.last.keys ]
168
+ self.add_definition_set( defs )
169
+ #self.log.debug "After adding definitions: %d scope frame/s. Last: %p" %
170
+ # [ @definitions.nitems, @definitions.last.keys ]
171
+ yield( self )
172
+ ensure
173
+ self.remove_definition_set
174
+ end
175
+ end
176
+
177
+ end # class RenderingScope
178
+
179
+
180
+
181
+ #############################################################
182
+ ### C L A S S M E T H O D S
183
+ #############################################################
184
+
185
+ # The Array of directories the template class searches for template
186
+ # names given to #load.
187
+ @load_path = %w{.}
188
+ class << self
189
+ attr_accessor :load_path
190
+ end
191
+
192
+
193
+ ### Load a template from a file.
194
+ def self::load( name, path=[] )
195
+
196
+ # Find the file on either the specified or default path
197
+ path = self.load_path if path.empty?
198
+ Arrow::Logger[self].debug "Searching for template '%s' in %d directories" %
199
+ [ name, path.size ]
200
+ filename = self.find_file( name, path )
201
+ Arrow::Logger[self].debug "Found '%s'" % [ filename ]
202
+
203
+ # Read the template source
204
+ source = File.read( filename )
205
+ source.untaint
206
+
207
+ # Create a new template object, set its path and filename, then tell it
208
+ # to parse the loaded source to define its behaviour. Parse is called
209
+ # after the file and path are set so directives in the template can
210
+ # use them.
211
+ obj = new()
212
+ obj._file = filename
213
+ obj._load_path.replace( path )
214
+ obj.parse( source )
215
+
216
+ return obj
217
+ end
218
+
219
+
220
+ ### Find the specified +file+ in the given +path+ (or the Template
221
+ ### class's #load_path if not specified).
222
+ def self::find_file( file, path=[] )
223
+ raise TemplateError, "Filename #{file} is tainted." if
224
+ file.tainted?
225
+
226
+ filename = nil
227
+ path.collect {|dir| File.expand_path(file, dir).untaint }.each do |fn|
228
+ Arrow::Logger[self].debug "Checking path %p" % [ fn ]
229
+ if File.file?( fn )
230
+ Arrow::Logger[self].debug " found the template file at %p" % [ fn ]
231
+ filename = fn
232
+ break
233
+ end
234
+
235
+ Arrow::Logger[self].debug " %p does not exist or is not a plain file." % [ fn ]
236
+ end
237
+
238
+ raise Arrow::TemplateError,
239
+ "Template '%s' not found. Search path was %p" %
240
+ [ file, path ] unless filename
241
+
242
+ return filename
243
+ end
244
+
245
+
246
+ ### Create an attr_reader method for the specified +sym+, but one which
247
+ ### will look for instance variables with any leading underbars removed.
248
+ def self::attr_underbarred_reader( sym )
249
+ ivarname = '@' + sym.to_s.gsub( /^_+/, '' )
250
+ define_method( sym ) {
251
+ self.instance_variable_get( ivarname )
252
+ }
253
+ end
254
+
255
+
256
+ ### Create an attr_accessor method for the specified +sym+, but one which
257
+ ### will look for instance variables with any leading underbars removed.
258
+ def self::attr_underbarred_accessor( sym )
259
+ ivarname = '@' + sym.to_s.gsub( /^_+/, '' )
260
+ define_method( sym ) {
261
+ self.instance_variable_get( ivarname )
262
+ }
263
+ define_method( "#{sym}=" ) {|arg|
264
+ self.instance_variable_set( ivarname, arg )
265
+ }
266
+ end
267
+
268
+
269
+ #############################################################
270
+ ### I N S T A N C E M E T H O D S
271
+ #############################################################
272
+
273
+ ### Create a new template object with the specified +content+ (a String)
274
+ ### and +config+ hash. The +config+ can contain one or more of the
275
+ ### following keys:
276
+ ###
277
+ ### [<b>:parserClass</b>]
278
+ ### The class object that will be instantiated to parse the template
279
+ ### text into nodes. Defaults to Arrow::Template::Parser.
280
+ ### [<b>:elideDirectiveLines</b>]
281
+ ### If set to a +true+ value, lines of the template which contain only
282
+ ### whitespace and one or more non-rendering directives will be
283
+ ### discarded from the rendered output.
284
+ ### [<b>:debuggingComments</b>]
285
+ ### If set to a +true+ value, nodes which are set up to do so will
286
+ ### insert a comment with debugging information immediately before
287
+ ### their rendered output.
288
+ ### [<b>:commentStart</b>]
289
+ ### The String which will be prepended to all comments rendered in the
290
+ ### output. See #render_comment.
291
+ ### [<b>:commentEnd</b>]
292
+ ### The String which will be appended to all comments rendered in the
293
+ ### output. See #render_comment.
294
+ ### [<b>:strictAttributes</b>]
295
+ ### If set to a +true+ value, method calls which don't match
296
+ ### already-extant attributes will result in NameErrors. This is
297
+ ### +false+ by default, which causes method calls to generate
298
+ ### attributes with the same name.
299
+ def initialize( content=nil, config={} )
300
+ @config = DEFAULTS.merge( config, &HashMergeFunction )
301
+ @renderers = DEFAULT_RENDERERS.dup
302
+ @attributes = {}
303
+ @syntax_tree = []
304
+ @source = content
305
+ @file = nil
306
+ @creation_time = Time.now
307
+ @load_path = self.class.load_path.dup
308
+ @prerender_done = false
309
+ @postrender_done = false
310
+
311
+ @enclosing_templates = []
312
+
313
+ case content
314
+ when String
315
+ self.parse( content )
316
+ when Array
317
+ self.install_syntax_tree( content )
318
+ when NilClass
319
+ # No-op
320
+ else
321
+ raise TemplateError,
322
+ "Can't handle a %s as template content" % content.class.name
323
+ end
324
+ end
325
+
326
+
327
+ ### Initialize a copy of the +original+ template object.
328
+ def initialize_copy( original )
329
+ super
330
+
331
+ @attributes = {}
332
+ tree = original._syntax_tree.collect {|node| node.clone}
333
+ self.install_syntax_tree( tree )
334
+ end
335
+
336
+
337
+ ######
338
+ public
339
+ ######
340
+
341
+ # Square-bracket methods access template attributes
342
+ def_delegators :@attributes, :[], :[]=
343
+
344
+ # The Hash of "attributes" for the template -- data fields which hold
345
+ # values which can be accessed by the template's nodes for rendering.
346
+ attr_underbarred_reader :_attributes
347
+
348
+ # The Array of first-level nodes which make up the AST of the template.
349
+ attr_underbarred_reader :_syntax_tree
350
+
351
+ # The template's configuration
352
+ attr_underbarred_reader :_config
353
+
354
+ # The Hash of rendering Procs, Methods, or Symbols (which specify a
355
+ # method on the rendered object) which are used to render objects in the
356
+ # template's node contents.
357
+ attr_underbarred_reader :_renderers
358
+
359
+ # The source file for the template, if any
360
+ attr_underbarred_accessor :_file
361
+
362
+ # The template source
363
+ attr_underbarred_accessor :_source
364
+
365
+ # The load path used when the template was loaded. This is the path that
366
+ # will be used to load any subordinate resources (eg., includes).
367
+ attr_underbarred_accessor :_load_path
368
+
369
+ # The Time that the template object was created
370
+ attr_underbarred_accessor :_creation_time
371
+
372
+ # The template which contains this one (if any) during a render.
373
+ attr_underbarred_accessor :_enclosing_templates
374
+
375
+
376
+
377
+ ### Return the template that is enclosing the receiver in the current context,
378
+ ### if any.
379
+ def _enclosing_template
380
+ self._enclosing_templates.last
381
+ end
382
+
383
+
384
+ ### Return a human-readable representation of the template object.
385
+ def inspect
386
+ "#<%s:0x%0x %s (%d nodes)>" % [
387
+ self.class.name,
388
+ self.object_id * 2,
389
+ @file ? @file : '(anonymous)',
390
+ @syntax_tree.length,
391
+ ]
392
+ end
393
+
394
+
395
+ ### Return the approximate size of the template, in bytes. Used by
396
+ ### Arrow::Cache for size thresholds.
397
+ def memsize
398
+ @source ? @source.length : 0
399
+ end
400
+
401
+
402
+ ### Parse the given template source (a String) and put the resulting
403
+ ### nodes into the template's syntax_tree.
404
+ def parse( source )
405
+ @source = source
406
+ parserClass = @config[:parserClass]
407
+ tree = parserClass.new( @config ).parse( source, self )
408
+
409
+ #self.log.debug "Parse complete: syntax tree is: %p" % tree
410
+ return self.install_syntax_tree( tree )
411
+ end
412
+
413
+
414
+ ### Install a new syntax tree in the template object, replacing the old one,
415
+ ### if any.
416
+ def install_syntax_tree( tree )
417
+ @syntax_tree = tree
418
+ @syntax_tree.each do |node| node.add_to_template(self) end
419
+ end
420
+
421
+
422
+ ### Install the given +node+ into the template object.
423
+ def install_node( node )
424
+ #self.log.debug "Installing a %s %p" % [node.type, node]
425
+
426
+ if node.respond_to?( :name ) && node.name
427
+ unless @attributes.key?( node.name )
428
+ #self.log.debug "Installing an attribute for a node named %p" % node.name
429
+ @attributes[ node.name ] = nil
430
+ self.add_attribute_accessor( node.name.to_sym )
431
+ self.add_attribute_mutator( node.name.to_sym )
432
+ else
433
+ #self.log.debug "Already have a attribute named %p" % node.name
434
+ end
435
+ end
436
+ end
437
+
438
+
439
+ ### Returns +true+ if the source file from which the template was read
440
+ ### has been modified since the receiver was instantiated. Always
441
+ ### returns +false+ if the template wasn't loaded from a file.
442
+ def changed?
443
+ return false unless @file
444
+
445
+ if File.exists?( @file )
446
+ self.log.debug "Comparing creation time '%s' with file mtime '%s'" %
447
+ [ @creation_time, File.mtime(@file) ]
448
+ rval = File.mtime( @file ) > @creation_time
449
+ end
450
+
451
+ self.log.debug "Template file '%s' has %s" %
452
+ [ @file, rval ? "changed" : "not changed" ]
453
+ return rval
454
+ end
455
+
456
+
457
+ ### Returns +true+ if this template has already been through a pre-render.
458
+ def prerender_done?
459
+ return @prerender_done
460
+ end
461
+
462
+
463
+ ### Prep the template for rendering, calling each of its nodes'
464
+ ### #before_rendering hook.
465
+ def prerender( enclosing_template=nil )
466
+ @enclosing_templates << enclosing_template
467
+
468
+ @syntax_tree.each do |node|
469
+ # self.log.debug " pre-rendering %p" % [node]
470
+ node.before_rendering( self ) if
471
+ node.respond_to?( :before_rendering )
472
+ end
473
+ end
474
+ alias_method :before_rendering, :prerender
475
+
476
+
477
+ ### Render the template to text and return it as a String. If called with an
478
+ ### Array of +nodes+, the template will render them instead of its own
479
+ ### syntax_tree. If given a scope (a Module object), a Binding of its
480
+ ### internal state it will be used as the context of evaluation for the
481
+ ### render. If not specified, a new anonymous Module instance is created for
482
+ ### the render. If a +enclosing_template+ is given, make it available during
483
+ ### rendering for variable-sharing, etc. Returns the results of each nodes'
484
+ ### render joined together with the default string separator (+$,+).
485
+ def render( nodes=nil, scope=nil, enclosing_template=nil )
486
+ rval = []
487
+ nodes ||= self.get_prepped_nodes
488
+ scope ||= self.make_rendering_scope
489
+
490
+ self.prerender( enclosing_template )
491
+
492
+ # Render each node
493
+ nodes.each do |node|
494
+ # self.log.debug " rendering %p" % [ node ]
495
+ begin
496
+ rval << node.render( self, scope )
497
+ rescue ::Exception => err
498
+ rval << err
499
+ end
500
+ end
501
+
502
+ return self.render_objects( *rval )
503
+ ensure
504
+ self.postrender
505
+ end
506
+ alias_method :to_s, :render
507
+
508
+
509
+ ### Returns +true+ if this template has already been through a post-render.
510
+ def postrender_done?
511
+ return @postrender_done
512
+ end
513
+
514
+
515
+ ### Clean up after template rendering, calling each of its nodes'
516
+ ### #after_rendering hook.
517
+ def postrender( enclosing_template=nil )
518
+ @syntax_tree.each do |node|
519
+ # self.log.debug " post-rendering %p" % [node]
520
+ node.after_rendering( self ) if
521
+ node.respond_to?( :after_rendering )
522
+ end
523
+ @enclosing_templates.pop
524
+ end
525
+ alias_method :after_rendering, :postrender
526
+
527
+
528
+ ### Create an anonymous module to act as a scope for any evals that take
529
+ ### place during a single render.
530
+ def make_rendering_scope
531
+ # self.log.debug "Making rendering scope with attributes: %p" % [@attributes]
532
+ scope = RenderingScope.new( @attributes )
533
+ return scope
534
+ end
535
+
536
+
537
+ ### Render the specified objects into text.
538
+ def render_objects( *objs )
539
+ objs.collect do |obj|
540
+ rval = nil
541
+ key = (@renderers.keys & obj.class.ancestors).sort {|a,b| a <=> b}.first
542
+
543
+ begin
544
+ if key
545
+ case @renderers[ key ]
546
+ when Proc, Method
547
+ rval = @renderers[ key ].call( obj, self )
548
+ when Symbol
549
+ methodname = @renderers[ key ]
550
+ rval = obj.send( methodname )
551
+ else
552
+ raise TypeError, "Unknown renderer type '%s' for %p" %
553
+ [ @renderers[key], obj ]
554
+ end
555
+ else
556
+ rval = obj.to_s
557
+ end
558
+ rescue => err
559
+ self.log.error "rendering error while rendering %p (a %s): %s" %
560
+ [obj, obj.class.name, err.message]
561
+ @renderers[ ::Exception ].call( err, self )
562
+ end
563
+ end.join
564
+ end
565
+
566
+
567
+ ### Render the given +message+ as a comment as specified by the template
568
+ ### configuration.
569
+ def render_comment( message )
570
+ comment = "%s%s%s\n" % [
571
+ @config[:commentStart],
572
+ message,
573
+ @config[:commentEnd],
574
+ ]
575
+ #self.log.debug "Rendered comment: %s" % comment
576
+ return comment
577
+ end
578
+
579
+
580
+ ### Call the given +block+, overriding the contents of the template's attributes
581
+ ### and the definitions in the specified +scope+ with those from the pairs in
582
+ ### the given +hash+.
583
+ def with_overridden_attributes( scope, hash )
584
+ oldvals = {}
585
+ begin
586
+ hash.each do |name, value|
587
+ #self.log.debug "Overriding attribute %s with value: %p" %
588
+ # [ name, value ]
589
+ oldvals[name] = @attributes.key?( name ) ? @attributes[ name ] : nil
590
+ @attributes[ name ] = value
591
+ end
592
+ scope.override( hash ) do
593
+ yield( self )
594
+ end
595
+ ensure
596
+ oldvals.each do |name, value|
597
+ #self.log.debug "Restoring old value: %s for attribute %p" %
598
+ # [ name, value ]
599
+ @attributes.delete( name )
600
+ @attributes[ name ] = oldvals[name] if oldvals[name]
601
+ end
602
+ end
603
+ end
604
+
605
+
606
+
607
+ #########
608
+ protected
609
+ #########
610
+
611
+ ### Returns the syntax tree with its nodes prerendered in accordance with
612
+ ### the template's configuration.
613
+ def get_prepped_nodes
614
+ tree = @syntax_tree.dup
615
+
616
+ self.strip_directive_whitespace( tree ) if
617
+ @config[:elideDirectiveLines]
618
+
619
+ return tree
620
+ end
621
+
622
+
623
+ ### Strip whitespace from the tails of textnodes before and the head
624
+ ### of textnodes after lines consisting only of non-rendering directives
625
+ ### in the given template syntax +tree+.
626
+ def strip_directive_whitespace( tree )
627
+ # Make a flat list of all nodes
628
+ nodes = tree.collect {|node| node.to_a}.flatten
629
+
630
+ # Elide non-rendering directive lines. Match node lists like:
631
+ # <TextNode> =~ /\n\s*$/
632
+ # <NonRenderingNode>*
633
+ # <TextNode> =~ /^\n/
634
+ # removing one "\n" from the tail of the leading textnode and the
635
+ # head of the trailing textnode. Trailing textnode can also be a
636
+ # leading textnode for another series.
637
+ nodes.each_with_index do |node,i|
638
+ leadingNode = nodes[i-1]
639
+
640
+ # If both the leading node and the current one match the
641
+ # criteria, look for a trailing node.
642
+ if i.nonzero? && leadingNode.is_a?( TextNode ) &&
643
+ leadingNode =~ /\n\s*$/s
644
+
645
+ # Find the trailing node. Abandon the search on any
646
+ # rendering directive or text node that includes a blank line.
647
+ trailingNode = nodes[i..-1].find do |node|
648
+ break nil if node.rendering?
649
+ node.is_a?( TextNode ) && node =~ /^\n/
650
+ end
651
+
652
+ leadingNode.body.sub!( /\n\s*$/, '' ) if trailingNode
653
+ end
654
+ end
655
+ end
656
+
657
+
658
+ ### Autoload accessor/mutator methods for attributes.
659
+ def method_missing( sym, *args, &block )
660
+ name = sym.to_s.gsub( /=$/, '' )
661
+ super unless @attributes.key?( name ) || !@config[:strictAttributes]
662
+
663
+ #self.log.debug "Autoloading for #{sym}"
664
+
665
+ # Mutator
666
+ if /=$/ =~ sym.to_s
667
+ #self.log.debug "Autoloading mutator %p" % sym
668
+ self.add_attribute_mutator( sym )
669
+ # Accessor
670
+ else
671
+ #self.log.debug "Autoloading accessor %p" % sym
672
+ self.add_attribute_accessor( sym )
673
+ end
674
+
675
+ # Don't use #send to avoid infinite recursion in case method
676
+ # definition has failed for some reason.
677
+ self.method( sym ).call( *args )
678
+ end
679
+
680
+
681
+ ### Add a singleton accessor (getter) method for accessing the attribute
682
+ ### specified by +sym+ to the receiver.
683
+ def add_attribute_accessor( sym )
684
+ name = sym.to_s.sub( /=$/, '' )
685
+
686
+ code = %Q{
687
+ def self::#{name}
688
+ @attributes[#{name.inspect}]
689
+ end
690
+ }
691
+
692
+ # $stderr.puts "Auto-defining accessor for #{name}: #{code}"
693
+ eval( code, nil, "#{name} [Auto-defined]", __LINE__ )
694
+ end
695
+
696
+
697
+ ### Add a singleton mutator (setter) method for accessing the attribute
698
+ ### specified by +sym+ to the receiver.
699
+ def add_attribute_mutator( sym )
700
+ name = sym.to_s.sub( /=$/, '' )
701
+
702
+ code = %Q{
703
+ def self::#{name}=( arg )
704
+ @attributes[ #{name.inspect} ] = arg
705
+ end
706
+ }
707
+
708
+ # $stderr.puts "Auto-defining mutator for #{name}: #{code}"
709
+ eval( code, nil, "#{name}= [Auto-defined]", __LINE__ )
710
+ end
711
+
712
+ end # class Arrow::Template
713
+