arrow 1.0.7

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.
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
+