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,571 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'strscan'
4
+ require 'forwardable'
5
+ require 'pluginfactory'
6
+
7
+ require 'arrow/object'
8
+ require 'arrow/mixins'
9
+ require 'arrow/exceptions'
10
+ require 'arrow/template'
11
+ require 'arrow/path'
12
+
13
+ # The Arrow::Template::Parser class, a derivative of
14
+ # Arrow::Object. This is the default parser class for the default Arrow
15
+ # templating system.
16
+ #
17
+ # == Authors
18
+ #
19
+ # * Michael Granger <ged@FaerieMUD.org>
20
+ #
21
+ # Please see the file LICENSE in the top-level directory for licensing details.
22
+ #
23
+ class Arrow::Template::Parser < Arrow::Object
24
+ include Arrow::HashUtilities
25
+
26
+ ### Regexp constants for parsing
27
+ module Patterns
28
+
29
+ # The Regexp that is used to match directive tag openings. Must be at
30
+ # least 2 characters wide.
31
+ TAGOPEN = %r{([<|\[])\?}
32
+
33
+ # The Hash that maps tag-closings to tag-middle patterns.
34
+ TAGMIDDLE = Hash.new {|hsh, key|
35
+ # Extract the first and second characters from the tag-closing
36
+ # pattern to build the tag-middle pattern.
37
+ src = key.is_a?( Regexp ) ? key.source : key.to_s
38
+ mobj = /(\\.|.)(\\.|.)/.match( src ) or
39
+ raise Arrow::ParseError, "couldn't extract first and second "\
40
+ "char from closing tag '%s'" % key
41
+ char1, char2 = mobj[1,2]
42
+ hsh[key] = Regexp.new( /((?:[^#{char1}]|#{char1}(?!#{char2}))+)/ )
43
+ }
44
+
45
+ # The Hash that maps tag openings to matching tag closings with flipped
46
+ # braces.
47
+ TAGCLOSE = Hash.new {|hsh, key|
48
+ compliment = key.reverse.tr('<{([', '>})]')
49
+ hsh[key] = Regexp.new( Regexp.quote(compliment) )
50
+ }
51
+
52
+ # Paren-group map
53
+ CAPTURE = Hash.new {|hsh, key|
54
+ Regexp.new( '(' + key.to_s + ')' )
55
+ }
56
+ ALTERNATION = Hash.new {|hsh, *keys|
57
+ Regexp.new( '(?:' + keys.join('|') + ')' )
58
+ }
59
+
60
+
61
+ # Constant patterns for parsing
62
+ DOT = /\./
63
+ COMMA = /,/
64
+ WHITESPACE = /\s*/
65
+ EQUALS = /=/
66
+
67
+ INFIX = /\.|::|(?>\[)/
68
+ IDENTIFIER = /[a-z]\w*/i
69
+ LBRACKET = /[\[(]/
70
+ RBRACKET = Hash.new {|hsh, key|
71
+ compliment = key.tr( '<{([', '>})]' )
72
+ hsh[key] = Regexp.new( Regexp.quote(compliment) )
73
+ }
74
+
75
+ NUMBER = /[-+]?\d+(?:\.\d+)?(?:e-?\d+)?/
76
+
77
+ # :FIXME: I would do this with something like
78
+ # /(["'/])((?:[^\1]|\\\.)*)\1/, but Ruby apparently doesn't grok
79
+ # backreferences inside character classes:
80
+ # (ruby 1.8.1 (2003-10-25) [i686-linux])
81
+ # irb(main):001:0> /(["'])((?:[^\1]|\\\.)*)\1/.match( \
82
+ # %{foo "and \"bar\" and baz" and some "other stuff"} )[0]
83
+ # ==> "\"and \"bar\" and baz\" and some \"other stuff\""
84
+ TICKQSTRING = /'((?:[^']|\\')*)'/
85
+ DBLQSTRING = /"((?:[^"]|\\")*)"/
86
+ SLASHQSTRING = %r{/((?:[^/]|\\/)*)/}
87
+ QUOTEDSTRING = ALTERNATION[[ TICKQSTRING, DBLQSTRING, SLASHQSTRING ]]
88
+
89
+ SYMBOL = /:[@$]?[a-z]\w+/ | /:/ +
90
+ ALTERNATION[[ DBLQSTRING, TICKQSTRING ]]
91
+ VARIABLE = /(?:\$|@@?)?_?/ + IDENTIFIER
92
+
93
+ REGEXP = %r{/((?:[^/]|\\.)+)/}
94
+ REBINDOP = /\s*(?:=~|matches)(?=\s)/
95
+
96
+ PATHNAME = %r{((?:[-\w.,:+@#$\%\(\)/]|\\ |\?(?!>))+)}
97
+
98
+ ARGUMENT = /[*&]?/ + IDENTIFIER
99
+ ARGDEFAULT = EQUALS + WHITESPACE +
100
+ ALTERNATION[[ IDENTIFIER, NUMBER, QUOTEDSTRING, SYMBOL, VARIABLE ]]
101
+
102
+ end
103
+ include Patterns
104
+
105
+
106
+ ### Parse state object class. Instance of this class represent a
107
+ ### parser's progress through a given template body.
108
+ class State < Arrow::Object
109
+ include Patterns
110
+ extend Forwardable
111
+
112
+ ### Create and return a new parse state for the specified
113
+ ### +text+. The +initialData+ is used to propagate directive
114
+ ### bookkeeping state through recursive parses.
115
+ def initialize( text, template, initialData={} )
116
+ @scanner = StringScanner.new( text )
117
+ @template = template
118
+ @tag_open = nil
119
+ @tag_middle = nil
120
+ @tag_close = nil
121
+ @current_branch = []
122
+ @current_branch_node = nil
123
+ @current_node = nil
124
+ @data = initialData
125
+
126
+ #self.log.debug "From %s: Created a parse state for %p (%p). Data is: %p" %
127
+ # [ caller(1).first, text, template, initialData ]
128
+ end
129
+
130
+
131
+ ######
132
+ public
133
+ ######
134
+
135
+ # Delegate the index operators to the @data hash
136
+ def_delegators :@data, :[], :[]=
137
+
138
+ # The template that corresponds to the parse state
139
+ attr_reader :template
140
+
141
+ # The StringScanner object which is scanning the text being parsed.
142
+ attr_reader :scanner
143
+
144
+ # The current branch of the parse state. Branches are added and
145
+ # removed for re-entrances into the parse loop.
146
+ attr_reader :current_branch
147
+
148
+ # The pointer into the syntax tree for the node which is the base of
149
+ # the current branch.
150
+ attr_reader :current_branch_node
151
+
152
+ # The pointer into the syntax tree for the current node
153
+ attr_reader :current_node
154
+
155
+ # The string that contains the opening string of the current tag, if
156
+ # any.
157
+ attr_reader :tag_open
158
+
159
+ # The pattern that will match the middle of the current tag during
160
+ # the parse of a directive.
161
+ attr_accessor :tag_middle
162
+
163
+ # The pattern that will match the current tag-closing characters during
164
+ # the parse of a directive.
165
+ attr_accessor :tag_close
166
+
167
+ # A miscellaneous data hash to allow directives to keep their own
168
+ # state for a parse.
169
+ attr_reader :data
170
+
171
+
172
+ ### Return the line number of the line on which the parse's pointer
173
+ ### current rests.
174
+ def line
175
+ return @scanner.string[ 0, @scanner.pos ].count( $/ ) + 1
176
+ end
177
+
178
+
179
+ ### Set the middle and closing tag patterns from the given matched
180
+ ### opening string.
181
+ def set_tag_patterns( opening )
182
+ @tag_open = opening
183
+ @tag_close = TAGCLOSE[ opening ]
184
+ @tag_middle = TAGMIDDLE[ @tag_close ]
185
+ end
186
+
187
+
188
+ ### Add the given +nodes+ to the state's syntax tree.
189
+ def add_nodes( *nodes )
190
+ @current_branch.push( *nodes )
191
+ @current_node = @current_branch.last
192
+ return self
193
+ end
194
+ alias_method :<<, :add_nodes
195
+
196
+
197
+ ### Add a branch belonging to the specified +node+ to the parse
198
+ ### state for the duration of the supplied block, removing and
199
+ ### returning it when the block returns.
200
+ def branch( node )
201
+ raise LocalJumpError, "no block given" unless
202
+ block_given?
203
+
204
+ # Save and set the current branch node
205
+ entryNode = @current_branch_node
206
+ @current_branch_node = node
207
+
208
+ # Push a new branch and make the current branch point to it
209
+ # while saving the one we entered with so it can be restored
210
+ # later.
211
+ entryBranch = @current_branch
212
+ entryBranch.push( @current_branch = [] )
213
+
214
+ yield( self )
215
+
216
+ # Restore the current branch and branch node to what they were
217
+ # before
218
+ @current_branch = entryBranch
219
+ @current_branch_node = entryNode
220
+
221
+ return @current_branch.pop
222
+ end
223
+ end # class State
224
+
225
+
226
+ # Default configuration hash
227
+ Defaults = {
228
+ :strict_end_tags => false,
229
+ :ignore_unknown_PIs => true,
230
+ }
231
+
232
+
233
+ #############################################################
234
+ ### I N S T A N C E M E T H O D S
235
+ #############################################################
236
+
237
+ ### Create a new parser using the specified +config+. The +config+ can
238
+ ### contain one or more of the following keys:
239
+ ###
240
+ ### [<b>:strict_end_tags</b>]
241
+ ### Raise an error if the optional name associated with an <?end?> tag
242
+ ### doesn't match the directive it closes. Defaults to +false+.
243
+ ### [<b>:ignore_unknown_PIs</b>]
244
+ ### When this is set to +true+, any processing instructions found in a
245
+ ### template that don't parse will be kept as-is in the output. If
246
+ ### this is +false+, unrecognized PIs will raise an error at parse
247
+ ### time. Defaults to +true+.
248
+ def initialize( config={} )
249
+ @config = Defaults.merge( config, &HashMergeFunction )
250
+ end
251
+
252
+
253
+ ### Initialize a duplicate of the +original+ parser.
254
+ def initialize_copy( original )
255
+ super
256
+ @config = original.config.dup
257
+ end
258
+
259
+
260
+ ######
261
+ public
262
+ ######
263
+
264
+ # The configuration object which contains the parser's config.
265
+ attr_reader :config
266
+
267
+
268
+ ### Parse and return a template syntax tree from the given +string+.
269
+ def parse( string, template, initialData={} )
270
+
271
+ # Create a new parse state and build the parse tree with it.
272
+ begin
273
+ state = State.new( string, template, initialData )
274
+ syntax_tree = self.scan_for_nodes( state )
275
+
276
+ rescue Arrow::TemplateError => err
277
+ Kernel.raise( err ) unless defined? state
278
+ state.scanner.unscan if state.scanner.matched? #<- segfaults
279
+
280
+ # Get the linecount and chunk of erroring content
281
+ errorContent = get_parse_context( state.scanner )
282
+
283
+ msg = err.message.split( /:/ ).uniq.join( ':' ) +
284
+ %{ at line %d of %s: %s...} %
285
+ [ state.line, template._file, errorContent ]
286
+ Kernel.raise( err.class, msg )
287
+ end
288
+
289
+ return syntax_tree
290
+ end
291
+
292
+
293
+ ### Use the specified +state+ (a StringScanner object) to scan for
294
+ ### directive and plain-text nodes. The +context+ argument, if set,
295
+ ### indicates a recursive call for the directive named. The +node+ will
296
+ ### be used as the branch node in the parse state.
297
+ def scan_for_nodes( state, context=nil, node=nil )
298
+ return state.branch( node ) do
299
+ scanner = state.scanner
300
+
301
+ # Scan until the scanner reaches the end of its string. Early exits
302
+ # 'break' of this loop.
303
+ catch( :endscan ) {
304
+ until scanner.eos?
305
+ startpos = scanner.pos
306
+ #self.log.debug %{Scanning from %d:%p} %
307
+ # [ scanner.pos, scanner.rest[0,20] + '..' ]
308
+
309
+ # Scan for the next directive. When the scanner reaches
310
+ # the end of the parsed string, just append any plain
311
+ # text that's left and stop scanning.
312
+ if scanner.skip_until( TAGOPEN )
313
+
314
+ # Add the literal String node leading up to the tag
315
+ # as a text node :FIXME: Have to do it this way
316
+ # because StringScanner#pre_match does weird crap if
317
+ # skip_until skips only one or no character/s.
318
+ if ( scanner.pos - startpos > scanner.matched.length )
319
+ offset = scanner.pos - scanner.matched.length - 1
320
+ state << Arrow::Template::TextNode.
321
+ new( scanner.string[startpos..offset] )
322
+ #self.log.debug "Added text node %p" %
323
+ # scanner.string[startpos..offset]
324
+ end
325
+
326
+ # Now scan the directive that was found
327
+ state << self.scan_directive( state, context )
328
+ else
329
+ state << Arrow::Template::TextNode.new( scanner.rest )
330
+ scanner.terminate
331
+ end
332
+ end
333
+ }
334
+ end
335
+ end
336
+
337
+
338
+ ### Given the specified parse +state+ which is pointing past the opening
339
+ ### of a directive tag, parse the directive.
340
+ def scan_directive( state, context=nil )
341
+ scanner = state.scanner
342
+
343
+ # Set the patterns in the parse state to compliment the
344
+ # opening tag.
345
+ state.set_tag_patterns( scanner.matched )
346
+ tag_begin = state.line
347
+
348
+ # Scan for the directive name; if no valid name can be
349
+ # found, handle an unknown PI/directive.
350
+ scanner.skip( WHITESPACE )
351
+ unless (( tag = scanner.scan( IDENTIFIER ) ))
352
+ #self.log.debug "No identifier at '%s...'" % scanner.rest[0,20]
353
+
354
+ # If the tag_open is <?, then this is a PI that we don't
355
+ # grok. The reaction to this is configurable, so decide what to
356
+ # do.
357
+ if state.tag_open == '<?'
358
+ return handle_unknown_pi( state )
359
+
360
+ # ...otherwise, it's just a malformed non-PI tag, which
361
+ # is always an error.
362
+ else
363
+ raise Arrow::ParseError, "malformed directive name"
364
+ end
365
+ end
366
+
367
+ # If it's anything but an 'end' tag, create a directive object.
368
+ unless tag == 'end'
369
+ begin
370
+ node = Arrow::Template::Directive.create( tag, self, state )
371
+ rescue ::FactoryError => err
372
+ return self.handle_unknown_pi( state, tag )
373
+ end
374
+
375
+ # If it's an 'end',
376
+ else
377
+ #self.log.debug "Found end tag."
378
+
379
+ # If this scan is occuring in a recursive parse, make sure the
380
+ # 'end' is closing the correct thing and break out of the node
381
+ # search. Note that the trailing '?>' is left in the scanner to
382
+ # match at the end of the loop that opened this recursion.
383
+ if context
384
+ scanner.skip( WHITESPACE )
385
+ closed_tag = scanner.scan( IDENTIFIER )
386
+ #self.log.debug "End found for #{closed_tag}"
387
+
388
+ # If strict end tags is turned on, check to be sure we
389
+ # got the correct 'end'.
390
+ if @config[:strict_end_tags]
391
+ raise Arrow::ParseError,
392
+ "missing or malformed closing tag name" if
393
+ closed_tag.nil?
394
+ raise Arrow::ParseError,
395
+ "mismatched closing tag name '#{closed_tag}'" unless
396
+ closed_tag.downcase == context.downcase
397
+ end
398
+
399
+ # Jump out of the loop in #scan_for_nodes...
400
+ throw :endscan
401
+ else
402
+ raise Arrow::ParseError, "dangling end"
403
+ end
404
+ end
405
+
406
+ # Skip to the end of the tag
407
+ self.scan_for_tag_ending( state ) or
408
+ raise Arrow::ParseError,
409
+ "malformed tag starting at line %d: no closing tag "\
410
+ "delimiters %p found" % [ tag_begin, state.tag_close ]
411
+
412
+ return node
413
+ end
414
+
415
+
416
+ ### Given the specified +state+ (an Arrow::Template::Parser::State
417
+ ### object), scan for and return an indentifier. If +skip_whitespace+ is
418
+ ### +true+, any leading whitespace characters will be skipped. Returns
419
+ ### +nil+ if no identifier is found.
420
+ def scan_for_identifier( state, skip_whitespace=true )
421
+ #self.log.debug "Scanning for identifier at %p" %
422
+ # state.scanner.rest[0,20]
423
+
424
+ state.scanner.skip( WHITESPACE ) if skip_whitespace
425
+ rval = state.scanner.scan( IDENTIFIER ) or return nil
426
+
427
+ #self.log.debug "Found identifier %p" % rval
428
+ return rval
429
+ end
430
+
431
+
432
+ ### Given the specified +state+ (an Arrow::Template::Parser::State
433
+ ### object), scan for and return a quoted string, including the
434
+ ### quotes. If +skip_whitespace+ is +true+, any leading whitespace
435
+ ### characters will be skipped. Returns +nil+ if no quoted string is
436
+ ### found.
437
+ def scan_for_quoted_string( state, skip_whitespace=true )
438
+ #self.log.debug "Scanning for quoted string at %p" %
439
+ # state.scanner.rest[0,20]
440
+
441
+ state.scanner.skip( WHITESPACE ) if skip_whitespace
442
+
443
+ rval = state.scanner.scan( QUOTEDSTRING ) or return nil
444
+
445
+ #self.log.debug "Found quoted string %p" % rval
446
+ return rval
447
+ end
448
+
449
+
450
+ ### Given the specified +state+ (an Arrow::Template::Parser::State
451
+ ### object), scan for and return a methodchain. Returns +nil+ if no
452
+ ### methodchain is found.
453
+ def scan_for_methodchain( state )
454
+ scanner = state.scanner
455
+ #self.log.debug "Scanning for methodchain at %p" %
456
+ # scanner.rest[0,20]
457
+
458
+ rval = scanner.scan( INFIX ) || ''
459
+ rval << (scanner.scan( WHITESPACE ) || '')
460
+ rval << (scanner.scan( state.tag_middle ) || '')
461
+
462
+ #self.log.debug "Found methodchain %p" % rval
463
+ return rval
464
+ end
465
+
466
+
467
+ ### Given the specified +state+ (an Arrow::Template::Parser::State
468
+ ### object), scan for and return the current tag ending. Returns +nil+
469
+ ### if no tag ending is found.
470
+ def scan_for_tag_ending( state, skip_whitespace=true )
471
+ scanner = state.scanner
472
+ #self.log.debug "Scanning for tag ending at %p" %
473
+ # scanner.rest[0,20]
474
+
475
+ scanner.skip( WHITESPACE ) if skip_whitespace
476
+ rval = scanner.scan( state.tag_close ) or
477
+ return nil
478
+
479
+ #self.log.debug "Found tag ending %p" % rval
480
+ return rval
481
+ end
482
+
483
+
484
+ ### Given the specified +state+ (an Arrow::Template::Parser::State
485
+ ### object), scan for and return two Arrays of identifiers. The first is
486
+ ### the list of parsed arguments as they appeared in the source, and the
487
+ ### second is the same list with all non-word characters removed. Given
488
+ ### an arglist like:
489
+ ### foo, bar=baz, *bim, &boozle
490
+ ### the returned arrays will contain:
491
+ ### ["foo", "bar=baz", "*bim", "&boozle"]
492
+ ### and
493
+ ### ["foo", "bar", "bim", "boozle"]
494
+ ### respectively.
495
+ def scan_for_arglist( state, skip_whitespace=true )
496
+ scanner = state.scanner
497
+ #self.log.debug "Scanning for arglist at %p" %
498
+ # scanner.rest[0,20]
499
+
500
+ args = []
501
+ pureargs = []
502
+ scanner.skip( WHITESPACE ) if skip_whitespace
503
+ while (( rval = scanner.scan(ARGUMENT) ))
504
+ args << rval
505
+ pureargs << rval.gsub( /\W+/, '' )
506
+ scanner.skip( WHITESPACE )
507
+ if (( rval = scanner.scan(ARGDEFAULT) ))
508
+ args.last << rval
509
+ end
510
+ break unless scanner.skip( WHITESPACE + COMMA + WHITESPACE )
511
+ end
512
+
513
+ return nil if args.empty?
514
+
515
+ #self.log.debug "Found args: %p, pureargs: %p" %
516
+ # [ args, pureargs ]
517
+ return args, pureargs
518
+ end
519
+
520
+
521
+ ### Given the specified +state+ (an Arrow::Template::Parser::State
522
+ ### object), scan for and return a valid-looking file pathname.
523
+ def scan_for_pathname( state, skip_whitespace=true )
524
+ scanner = state.scanner
525
+ #self.log.debug "Scanning for file path at %p" %
526
+ # scanner.rest[0,20]
527
+
528
+ scanner.skip( WHITESPACE ) if skip_whitespace
529
+ rval = scanner.scan( PATHNAME ) or
530
+ return nil
531
+
532
+ #self.log.debug "Found path: %p" % rval
533
+ return rval
534
+ end
535
+
536
+
537
+
538
+ #########
539
+ protected
540
+ #########
541
+
542
+ ### Handle an unknown ProcessingInstruction.
543
+ def handle_unknown_pi( state, tag="" )
544
+
545
+ # If the configuration doesn't say to ignore unknown PIs or it's an
546
+ # [?alternate-synax?] directive, raise an error.
547
+ if state.tag_open == '[?' || !@config[:ignore_unknown_PIs]
548
+ raise Arrow::ParseError, "unknown directive"
549
+ end
550
+
551
+ remainder = state.scanner.scan( %r{(?:[^?]|\?(?!>))*\?>} ) or
552
+ raise Arrow::ParseError, "failed to skip unknown PI"
553
+
554
+ pi = state.tag_open + tag + remainder
555
+ self.log.info( "Ignoring unknown PI (to = #{state.tag_open.inspect}) '#{pi}'" )
556
+ return Arrow::Template::TextNode.new( pi )
557
+ end
558
+
559
+
560
+ ### Return a string showing the given +scanner+'s context in the string
561
+ ### being parsed.
562
+ def get_parse_context( scanner )
563
+ str = scanner.string
564
+
565
+ pre = str[ scanner.pos < 40 ? 0 : scanner.pos - 40, 39 ]
566
+ post = scanner.rest[ 0, 40 ]
567
+
568
+ return "#{pre}[*** ERROR ***]#{post}"
569
+ end
570
+
571
+ end # class Arrow::Template::Parser