command-set 0.8.0
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.
- data/doc/README +2 -0
- data/doc/Specifications +219 -0
- data/doc/argumentDSL +36 -0
- data/lib/command-set/arguments.rb +547 -0
- data/lib/command-set/batch-interpreter.rb +0 -0
- data/lib/command-set/command-set.rb +282 -0
- data/lib/command-set/command.rb +456 -0
- data/lib/command-set/dsl.rb +526 -0
- data/lib/command-set/interpreter.rb +196 -0
- data/lib/command-set/og.rb +615 -0
- data/lib/command-set/quick-interpreter.rb +91 -0
- data/lib/command-set/result-list.rb +300 -0
- data/lib/command-set/results.rb +754 -0
- data/lib/command-set/standard-commands.rb +243 -0
- data/lib/command-set/subject.rb +91 -0
- data/lib/command-set/text-interpreter.rb +171 -0
- data/lib/command-set.rb +3 -0
- metadata +70 -0
@@ -0,0 +1,754 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'command-set/result-list'
|
3
|
+
|
4
|
+
module Kernel
|
5
|
+
def puts(*args)
|
6
|
+
$stdout.puts(*args)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
module Command
|
12
|
+
class << self
|
13
|
+
#Call anywhere to be sure that $stdout is replaced by an OutputStandin that
|
14
|
+
#delegates to the original STDOUT IO. This by itself won't change output behavior.
|
15
|
+
#Requiring 'command-set/command-set' does this for you. Multiple calls are safe though.
|
16
|
+
def wrap_stdout
|
17
|
+
return if $stdout.respond_to?(:add_dispatcher)
|
18
|
+
$stdout = OutputStandin.new($stdout)
|
19
|
+
end
|
20
|
+
|
21
|
+
#If you need the actual IO for /dev/stdout, you can call this to get it. Useful inside of
|
22
|
+
#Results::Formatter subclasses, for instance, so that they can actually send messages out to
|
23
|
+
#the user.
|
24
|
+
def raw_stdout
|
25
|
+
if $stdout.respond_to?(:__getobj__)
|
26
|
+
$stdout.__getobj__
|
27
|
+
else
|
28
|
+
$stdout
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#See Command::wrap_stdout
|
33
|
+
def wrap_stderr
|
34
|
+
return if $stdout.respond_to?(:add_dispatcher)
|
35
|
+
$stderr = OutputStandin.new($stderr)
|
36
|
+
end
|
37
|
+
|
38
|
+
#See Command::raw_stdout
|
39
|
+
def raw_stderr
|
40
|
+
if $stderr.respond_to?(:__getobj__)
|
41
|
+
$stderr.__getobj__
|
42
|
+
else
|
43
|
+
$stderr
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#Wraps an IO using DelegateClass. Dispatches all calls to the IO, until a
|
49
|
+
#Collector is registered, at which point, methods that the Collector
|
50
|
+
#handles will get sent to it.
|
51
|
+
class OutputStandin < IO
|
52
|
+
def initialize(io)
|
53
|
+
@_dc_obj = io
|
54
|
+
@dispatch_stack = nil
|
55
|
+
unless io.fileno.nil?
|
56
|
+
super(io.fileno,"w")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def method_missing(m, *args) # :nodoc:
|
61
|
+
unless @_dc_obj.respond_to?(m)
|
62
|
+
super(m, *args)
|
63
|
+
end
|
64
|
+
@_dc_obj.__send__(m, *args)
|
65
|
+
end
|
66
|
+
|
67
|
+
def respond_to?(m) # :nodoc:
|
68
|
+
return true if super
|
69
|
+
return @_dc_obj.respond_to?(m)
|
70
|
+
end
|
71
|
+
|
72
|
+
def __getobj__ # :nodoc:
|
73
|
+
@_dc_obj
|
74
|
+
end
|
75
|
+
|
76
|
+
def __setobj__(obj) # :nodoc:
|
77
|
+
raise ArgumentError, "cannot delegate to self" if self.equal?(obj)
|
78
|
+
@_dc_obj = obj
|
79
|
+
end
|
80
|
+
|
81
|
+
def clone # :nodoc:
|
82
|
+
super
|
83
|
+
__setobj__(__getobj__.clone)
|
84
|
+
end
|
85
|
+
|
86
|
+
def dup # :nodoc:
|
87
|
+
super
|
88
|
+
__setobj__(__getobj__.dup)
|
89
|
+
end
|
90
|
+
|
91
|
+
#This looks gnarly, but DelegateClass takes out methods defined by
|
92
|
+
#Kernel -- Which usually makes sense, but IO has a bunch of methods that
|
93
|
+
#Kernel defines to basically delegate to an IO.... I have a headache
|
94
|
+
#now.
|
95
|
+
methods = IO.public_instance_methods(false)
|
96
|
+
methods -= self.public_instance_methods(false)
|
97
|
+
methods |= ['class']
|
98
|
+
methods.each do |method|
|
99
|
+
begin
|
100
|
+
module_eval <<-EOS
|
101
|
+
def #{method}(*args, &block)
|
102
|
+
begin
|
103
|
+
@_dc_obj.__send__(:#{method}, *args, &block)
|
104
|
+
rescue
|
105
|
+
$@[0,2] = nil
|
106
|
+
raise
|
107
|
+
end
|
108
|
+
end
|
109
|
+
EOS
|
110
|
+
rescue SyntaxError
|
111
|
+
raise NameError, "invalid identifier %s" % method, caller(3)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def thread_stack_index
|
116
|
+
"standin_dispatch_stack_#{self.object_id}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def relevant_collector
|
120
|
+
Thread.current[thread_stack_index] || @dispatch_stack
|
121
|
+
end
|
122
|
+
|
123
|
+
def dispatched_method(method, *args)# :nodoc:
|
124
|
+
collector = relevant_collector
|
125
|
+
if not collector.nil? and collector.respond_to?(method)
|
126
|
+
return collector.__send__(method, *args)
|
127
|
+
end
|
128
|
+
return __getobj__.__send__(method, *args)
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_thread_local_dispatcher(collector)
|
132
|
+
Thread.current[thread_stack_index]=collector
|
133
|
+
define_dispatch_methods(collector)
|
134
|
+
end
|
135
|
+
alias set_thread_collector add_thread_local_dispatcher
|
136
|
+
|
137
|
+
#Puts the dispatcher in place to handle normal IO methods.
|
138
|
+
def add_dispatcher(collector)
|
139
|
+
@dispatch_stack = collector
|
140
|
+
define_dispatch_methods(collector)
|
141
|
+
end
|
142
|
+
alias set_default_collector add_dispatcher
|
143
|
+
|
144
|
+
def define_dispatch_methods(dispatcher)# :nodoc:
|
145
|
+
dispatcher.dispatches.each do |dispatch|
|
146
|
+
(class << self; self; end).module_eval <<-EOS
|
147
|
+
def #{dispatch}(*args)
|
148
|
+
dispatched_method(:#{dispatch.to_s}, *args)
|
149
|
+
end
|
150
|
+
EOS
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
#Unregisters the dispatcher.
|
155
|
+
def remove_dispatcher(dispatcher)
|
156
|
+
@dispatch_stack = nil if @dispatch_stack == dispatcher
|
157
|
+
end
|
158
|
+
alias remove_collector remove_dispatcher
|
159
|
+
|
160
|
+
def remove_thread_local_dispatcher(dispatcher)
|
161
|
+
if Thread.current[thread_stack_index] == dispatcher
|
162
|
+
Thread.current[thread_stack_index] = nil
|
163
|
+
end
|
164
|
+
end
|
165
|
+
alias remove_thread_collector remove_thread_local_dispatcher
|
166
|
+
end
|
167
|
+
|
168
|
+
#This is the output management module for CommandSet. With an eye towards
|
169
|
+
#being a general purpose UI library, and motivated by the need to manage
|
170
|
+
#pretty serious output management, the Results module provides a
|
171
|
+
#reasonably sophisticated output train that runs like this:
|
172
|
+
#
|
173
|
+
#0. An OutputStandin intercepts normal output and feeds it to ...
|
174
|
+
#0. A Collector aggregates output from OutputStandins and explicit #item
|
175
|
+
# and #list calls and feeds to to ...
|
176
|
+
#0. A Presenter handles the stream of output from Collector objects and
|
177
|
+
# emits +saw+ and +closed+ events to one or more ...
|
178
|
+
#0. Formatter objects, which interpret those events into user-readable
|
179
|
+
# output.
|
180
|
+
module Results
|
181
|
+
#Collects the events spawned by dispatchers and sends them to the presenter.
|
182
|
+
#Responsible for maintaining it's own place within the larger tree, but doesn't
|
183
|
+
#understand that other Collectors could be running at the same time - that's the
|
184
|
+
#Presenter's job.
|
185
|
+
class Collector
|
186
|
+
def initialize(presenter)
|
187
|
+
@presenter = presenter
|
188
|
+
@nesting = []
|
189
|
+
end
|
190
|
+
|
191
|
+
def initialize_copy(original)
|
192
|
+
@presenter = original.instance_variable_get("@presenter")
|
193
|
+
@nesting = original.instance_variable_get("@nesting").dup
|
194
|
+
end
|
195
|
+
|
196
|
+
def items(*objs)
|
197
|
+
if Hash === objs.last
|
198
|
+
options = objs.pop
|
199
|
+
else
|
200
|
+
options = {}
|
201
|
+
end
|
202
|
+
objs.each do |obj|
|
203
|
+
item(obj, options)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def item( obj, options={} )
|
208
|
+
@presenter.item(@nesting.dup + [obj], options)
|
209
|
+
end
|
210
|
+
|
211
|
+
def begin_list( name, options={} )
|
212
|
+
@nesting.push(name)
|
213
|
+
@presenter.begin_list(@nesting.dup, options)
|
214
|
+
if block_given?
|
215
|
+
yield
|
216
|
+
end_list
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def open_list(name, options={})
|
221
|
+
@nesting.push(name)
|
222
|
+
unless @presenter.list_open?(@nesting)
|
223
|
+
@presenter.begin_list(@nesting.dup, options)
|
224
|
+
end
|
225
|
+
if block_given?
|
226
|
+
yield
|
227
|
+
end_list
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def end_list
|
232
|
+
@presenter.end_list(@nesting.dup)
|
233
|
+
@nesting.pop
|
234
|
+
end
|
235
|
+
|
236
|
+
@dispatches = {}
|
237
|
+
|
238
|
+
def self.inherited(sub)
|
239
|
+
sub.instance_variable_set("@dispatches", @dispatches.dup)
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.dispatches
|
243
|
+
@dispatches.keys
|
244
|
+
end
|
245
|
+
|
246
|
+
def dispatches
|
247
|
+
self.class.dispatches
|
248
|
+
end
|
249
|
+
|
250
|
+
#Use to register an IO +method+ to handle. The block will be passed a Collector and the
|
251
|
+
#arguments passed to +method+.
|
252
|
+
def self.dispatch(method, &block)
|
253
|
+
@dispatches[method] = true
|
254
|
+
define_method(method, &block)
|
255
|
+
end
|
256
|
+
|
257
|
+
dispatch :puts do |*args|
|
258
|
+
args.each do |arg|
|
259
|
+
item arg
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
dispatch :write do |*args|
|
264
|
+
args.each do |arg|
|
265
|
+
item arg
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
dispatch :p do |*args|
|
270
|
+
args.each do |arg|
|
271
|
+
item(arg, :string => :inspect, :timing => :immediate)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
#Gets item and list events from Collectors, and emits two kinds of
|
277
|
+
#events to Formatters:
|
278
|
+
#[+saw+ events] occur in chronological order, with no guarantee regarding timing.
|
279
|
+
#[+closed+ events] occur in tree order.
|
280
|
+
#
|
281
|
+
#In general, +saw+ events are good for immediate feedback to the user,
|
282
|
+
#not so good in terms of making sense of things. They're generated as
|
283
|
+
#soon as the relevant output element enters the system.
|
284
|
+
#
|
285
|
+
#On the other hand, +closed+ events will be generated in the natural
|
286
|
+
#order you'd expect the output to appear in. Most Formatter subclasses use
|
287
|
+
#+closed+ events.
|
288
|
+
#
|
289
|
+
#A list which has not received a "list_end" event from upstream will
|
290
|
+
#block lists later in tree order until it closes. A Formatter that
|
291
|
+
#listens only to +closed+ events can present them to the user in a way
|
292
|
+
#that should be reasonable, although output might be halting for any
|
293
|
+
#process that takes noticeable time.
|
294
|
+
class Presenter
|
295
|
+
class Exception < ::Exception; end
|
296
|
+
|
297
|
+
def initialize
|
298
|
+
@results = List.new("")
|
299
|
+
@leading_edge = @results
|
300
|
+
@formatters = []
|
301
|
+
end
|
302
|
+
|
303
|
+
def create_collector
|
304
|
+
return Collector.new(self)
|
305
|
+
end
|
306
|
+
|
307
|
+
def item( item_path, options={} )
|
308
|
+
item = item_path.pop
|
309
|
+
home = get_collection(item_path)
|
310
|
+
item = home.add item
|
311
|
+
item.options = home.options.merge(options)
|
312
|
+
item.depth = item_path.length
|
313
|
+
notify(:saw, item)
|
314
|
+
advance_leading_edge
|
315
|
+
end
|
316
|
+
|
317
|
+
def leading_edge?(list)
|
318
|
+
return list == @leading_edge
|
319
|
+
end
|
320
|
+
|
321
|
+
def register_formatter(formatter)
|
322
|
+
@formatters << formatter
|
323
|
+
|
324
|
+
formatter.notify(:start, nil)
|
325
|
+
end
|
326
|
+
|
327
|
+
def begin_list( list_path, options={} )
|
328
|
+
list = list_path.pop
|
329
|
+
home = get_collection(list_path)
|
330
|
+
list = List.new(list)
|
331
|
+
list.options = home.options.merge(options)
|
332
|
+
list.depth = list_path.length
|
333
|
+
notify(:saw_begin, list)
|
334
|
+
home.add(list)
|
335
|
+
advance_leading_edge
|
336
|
+
end
|
337
|
+
|
338
|
+
def list_open?(list_path)
|
339
|
+
begin
|
340
|
+
get_collection(list_path)
|
341
|
+
return true
|
342
|
+
rescue Exception
|
343
|
+
return false
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def end_list( list_path )
|
348
|
+
list = get_collection( list_path )
|
349
|
+
notify(:saw_end, list)
|
350
|
+
list.close
|
351
|
+
advance_leading_edge
|
352
|
+
end
|
353
|
+
|
354
|
+
def done
|
355
|
+
@results.close
|
356
|
+
advance_leading_edge
|
357
|
+
notify(:done, nil)
|
358
|
+
end
|
359
|
+
|
360
|
+
#Returns the current list of results. A particularly advanced Formatter might treat +saw_*+ events
|
361
|
+
#like notifications, and then use the List#filter functionality to discover the specifics about the
|
362
|
+
#item or list just closed.
|
363
|
+
def output
|
364
|
+
@results
|
365
|
+
end
|
366
|
+
|
367
|
+
protected
|
368
|
+
def advance_leading_edge
|
369
|
+
iter = ListIterator.new(@leading_edge.tree_order_next)
|
370
|
+
iter.each do |forward|
|
371
|
+
case forward
|
372
|
+
when ListEnd
|
373
|
+
break if forward.end_of.open?
|
374
|
+
break if forward.end_of.name.empty?
|
375
|
+
notify(:leave, forward.end_of)
|
376
|
+
when List
|
377
|
+
notify(:arrive, forward)
|
378
|
+
when ListItem
|
379
|
+
notify(:arrive, forward)
|
380
|
+
end
|
381
|
+
|
382
|
+
@leading_edge = forward
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def notify(msg, item)
|
387
|
+
@formatters.each do |f|
|
388
|
+
f.notify(msg, item)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def get_collection(path)
|
393
|
+
thumb = @results.values
|
394
|
+
list = @results
|
395
|
+
path.each do |step|
|
396
|
+
list = thumb.find{|member| List === member && member.open? && member.name == step}
|
397
|
+
if list.nil?
|
398
|
+
raise Exception, "#{step} in #{path.inspect} missing from #{thumb}!"
|
399
|
+
end
|
400
|
+
thumb = list.values
|
401
|
+
end
|
402
|
+
return list
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
#The end of the Results train. Formatter objects are supposed to output to the user events that they
|
407
|
+
#receive from their presenters. To simplify this process, a number of common IO functions are delegated
|
408
|
+
#to an IO object - usually Command::raw_stdout.
|
409
|
+
#
|
410
|
+
#This class in particular is pretty quiet - probably not helpful for everyday use.
|
411
|
+
#Of course, for some purposes, singleton methods might be very useful
|
412
|
+
class Formatter
|
413
|
+
extend Forwardable
|
414
|
+
|
415
|
+
class FormatAdvisor
|
416
|
+
def initialize(formatter)
|
417
|
+
@advisee = formatter
|
418
|
+
end
|
419
|
+
|
420
|
+
def list(&block)
|
421
|
+
@advisee.advice[:list] << proc(&block)
|
422
|
+
end
|
423
|
+
|
424
|
+
def item(&block)
|
425
|
+
@advisee.advice[:item] << proc(&block)
|
426
|
+
end
|
427
|
+
|
428
|
+
def output(&block)
|
429
|
+
@advisee.advice[:output] << proc(&block)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def notify(msg, item)
|
434
|
+
if msg == :start
|
435
|
+
start
|
436
|
+
return
|
437
|
+
end
|
438
|
+
if msg == :done
|
439
|
+
finish
|
440
|
+
return
|
441
|
+
end
|
442
|
+
|
443
|
+
apply_advice(item)
|
444
|
+
|
445
|
+
if List === item
|
446
|
+
case msg
|
447
|
+
when :saw_begin
|
448
|
+
saw_begin_list(item)
|
449
|
+
when :saw_end
|
450
|
+
saw_end_list(item)
|
451
|
+
when :arrive
|
452
|
+
closed_begin_list(item)
|
453
|
+
when :leave
|
454
|
+
closed_end_list(item)
|
455
|
+
end
|
456
|
+
else
|
457
|
+
case msg
|
458
|
+
when :arrive
|
459
|
+
closed_item(item)
|
460
|
+
when :saw
|
461
|
+
saw_item(item)
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
def initialize(io)
|
467
|
+
@out_to = io
|
468
|
+
@advisor = FormatAdvisor.new(self)
|
469
|
+
@advice = {:list => [], :item => [], :output => []}
|
470
|
+
end
|
471
|
+
|
472
|
+
def apply_advice(item)
|
473
|
+
type = List === item ? :list : :item
|
474
|
+
|
475
|
+
item.options[:format_advice] =
|
476
|
+
@advice[type].inject(default_advice(type)) do |advice, advisor|
|
477
|
+
result = advisor[item]
|
478
|
+
break if result == :DONE
|
479
|
+
advice.merge!(result) if Hash === result
|
480
|
+
advice
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
attr_reader :advice
|
485
|
+
|
486
|
+
def receive_advice(&block)
|
487
|
+
@advisor.instance_eval(&block)
|
488
|
+
end
|
489
|
+
|
490
|
+
def default_advice(type)
|
491
|
+
{}
|
492
|
+
end
|
493
|
+
|
494
|
+
def_delegators :@out_to, :p, :puts, :print, :printf, :putc, :write, :write_nonblock, :flush
|
495
|
+
|
496
|
+
def self.inherited(sub)
|
497
|
+
sub.extend Forwardable
|
498
|
+
sub.class_eval do
|
499
|
+
def_delegators :@out_to, :p, :puts, :print, :printf, :putc, :write, :write_nonblock, :flush
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
#Presenter callback: output is beginning
|
504
|
+
def start; end
|
505
|
+
|
506
|
+
#Presenter callback: a list has just started
|
507
|
+
def saw_begin_list(list); end
|
508
|
+
|
509
|
+
#Presenter callback: an item has just been added
|
510
|
+
def saw_item(item); end
|
511
|
+
|
512
|
+
#Presenter callback: a list has just ended
|
513
|
+
def saw_end_list(list); end
|
514
|
+
|
515
|
+
#Presenter callback: a list opened, tree order
|
516
|
+
def closed_begin_list(list); end
|
517
|
+
|
518
|
+
#Presenter callback: an item added, tree order
|
519
|
+
def closed_item(item); end
|
520
|
+
|
521
|
+
#Presenter callback: an list closed, tree order
|
522
|
+
def closed_end_list(list); end
|
523
|
+
|
524
|
+
#Presenter callback: output is done
|
525
|
+
def finish; end
|
526
|
+
end
|
527
|
+
|
528
|
+
#The simplest useful Formatter: it outputs the value of every item in tree order. Think of
|
529
|
+
#it as what would happen if you just let puts and p go directly to the screen, without the
|
530
|
+
#annoying consequences of threading, etc.
|
531
|
+
class TextFormatter < Formatter
|
532
|
+
def closed_item(value)
|
533
|
+
puts value
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
class StrategyFormatter < Formatter
|
538
|
+
class FormatStrategy
|
539
|
+
extend Forwardable
|
540
|
+
|
541
|
+
def initialize(name, formatter)
|
542
|
+
@name = name
|
543
|
+
@formatter = formatter
|
544
|
+
setup
|
545
|
+
end
|
546
|
+
|
547
|
+
def setup; end
|
548
|
+
|
549
|
+
def_delegators :@formatter, :p, :puts, :print, :printf, :putc, :write, :write_nonblock, :flush
|
550
|
+
|
551
|
+
attr_reader :name
|
552
|
+
|
553
|
+
def switch_to(name)
|
554
|
+
@formatter.push_strategy(name)
|
555
|
+
end
|
556
|
+
|
557
|
+
def finish
|
558
|
+
@formatter.pop_strategy(self.name)
|
559
|
+
end
|
560
|
+
|
561
|
+
#Presenter callback: a list has just started
|
562
|
+
def saw_begin_list(list); end
|
563
|
+
|
564
|
+
#Presenter callback: an item has just been added
|
565
|
+
def saw_item(item); end
|
566
|
+
|
567
|
+
#Presenter callback: a list has just ended
|
568
|
+
def saw_end_list(list); end
|
569
|
+
|
570
|
+
#Presenter callback: a list opened, tree order
|
571
|
+
def closed_begin_list(list);
|
572
|
+
unless list.options[:strategy_start] == self or list.options[:format_advice].nil?
|
573
|
+
switch_to(list.options[:format_advice][:type])
|
574
|
+
if (next_strat = @formatter.current_strategy) != self
|
575
|
+
list.options[:strategy_start] = next_strat
|
576
|
+
next_strat.closed_begin_list(list)
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
#Presenter callback: an item added, tree order
|
582
|
+
def closed_item(item); end
|
583
|
+
|
584
|
+
#Presenter callback: an list closed, tree order
|
585
|
+
def closed_end_list(list);
|
586
|
+
if list.options[:strategy_start] == self
|
587
|
+
finish
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
@strategies = {:default => FormatStrategy}
|
593
|
+
|
594
|
+
class << self
|
595
|
+
def strategy(name, base_klass = FormatStrategy, &def_block)
|
596
|
+
@strategies[name.to_sym] = Class.new(base_klass, &def_block)
|
597
|
+
end
|
598
|
+
|
599
|
+
def inherited(sub)
|
600
|
+
self.instance_variables.each do |var|
|
601
|
+
value = self.instance_variable_get(var)
|
602
|
+
if value.nil?
|
603
|
+
sub.instance_variable_set(var, nil)
|
604
|
+
else
|
605
|
+
sub.instance_variable_set(var, value.dup)
|
606
|
+
end
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
def strategy_set(formatter)
|
611
|
+
set = {}
|
612
|
+
@strategies.each_pair do |name, klass|
|
613
|
+
set[name] = klass.new(name, formatter)
|
614
|
+
end
|
615
|
+
return set
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
def initialize(io)
|
620
|
+
super(io)
|
621
|
+
@strategies = self.class.strategy_set(self)
|
622
|
+
@strategy_stack = [@strategies[:default]]
|
623
|
+
end
|
624
|
+
|
625
|
+
def_delegators :current_strategy, :saw_begin_list, :saw_item, :saw_end_list,
|
626
|
+
:closed_begin_list, :closed_item, :closed_end_list
|
627
|
+
|
628
|
+
def current_strategy
|
629
|
+
@strategy_stack.last
|
630
|
+
end
|
631
|
+
|
632
|
+
def push_strategy(name)
|
633
|
+
if @strategies.has_key?(name)
|
634
|
+
@strategy_stack.push(@strategies[name])
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
def pop_strategy(name)
|
639
|
+
if current_strategy.name == name
|
640
|
+
@strategy_stack.pop
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
strategy :default do
|
645
|
+
def closed_item(value)
|
646
|
+
puts value
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
strategy :progress do
|
651
|
+
def closed_begin_list(list)
|
652
|
+
print list.to_s.ljust(50)
|
653
|
+
end
|
654
|
+
|
655
|
+
def closed_item(item)
|
656
|
+
print "."
|
657
|
+
flush
|
658
|
+
end
|
659
|
+
|
660
|
+
def finish
|
661
|
+
super
|
662
|
+
puts
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
strategy :indent do
|
667
|
+
def setup
|
668
|
+
@indent_level = 0
|
669
|
+
end
|
670
|
+
|
671
|
+
def indent
|
672
|
+
return " " * [0, @indent_level].max
|
673
|
+
end
|
674
|
+
|
675
|
+
def closed_begin_list(list)
|
676
|
+
puts indent + list.to_s
|
677
|
+
@indent_level += 1
|
678
|
+
super
|
679
|
+
end
|
680
|
+
|
681
|
+
def closed_item(item)
|
682
|
+
item.to_s.split(/\s*\n\s*/).each do |line|
|
683
|
+
puts indent + line
|
684
|
+
end
|
685
|
+
super
|
686
|
+
end
|
687
|
+
|
688
|
+
def closed_end_list(list)
|
689
|
+
@indent_level -= 1
|
690
|
+
super
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
strategy :invisible do
|
695
|
+
def closed_item(value)
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
strategy :skip do
|
700
|
+
def closed_begin_list(list)
|
701
|
+
finish
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
strategy :chatty do
|
706
|
+
def saw_begin_list(list); $stderr.print "B"; end
|
707
|
+
def saw_item(list); $stderr.print "."; end
|
708
|
+
def saw_end_list(list); $stderr.print "E"; end
|
709
|
+
def closed_begin_list(list);
|
710
|
+
clean_options = list.options.dup
|
711
|
+
clean_options.delete(:strategy_start)
|
712
|
+
puts "> #{list.to_s} (depth=#{list.depth} #{clean_options.inspect})"
|
713
|
+
end
|
714
|
+
def closed_item(list); puts " " + list.to_s; end
|
715
|
+
def closed_end_list(list); puts "< " + list.to_s; end
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
#A trivial and obvious Formatter: produces well-formed XML fragments based on events. It even
|
720
|
+
#indents. Might be handy for more complicated output processing, since you could feed the document
|
721
|
+
#to a XSLT processor.
|
722
|
+
class XMLFormatter < Formatter
|
723
|
+
def initialize(io, indent=" ", newline="\n")
|
724
|
+
super(io)
|
725
|
+
@indent = indent
|
726
|
+
@newline = newline
|
727
|
+
@indent_level=0
|
728
|
+
end
|
729
|
+
|
730
|
+
def line(string)
|
731
|
+
print "#{@indent * @indent_level}#{string}#@newline"
|
732
|
+
end
|
733
|
+
|
734
|
+
def closed_begin_list(name)
|
735
|
+
line "<#{name}>"
|
736
|
+
@indent_level += 1
|
737
|
+
end
|
738
|
+
|
739
|
+
def closed_item(value)
|
740
|
+
line "<item value=\"#{value}\" />"
|
741
|
+
end
|
742
|
+
|
743
|
+
def closed_end_list(name)
|
744
|
+
@indent_level -= 1
|
745
|
+
if @indent_level < 0
|
746
|
+
@indent_level = 0
|
747
|
+
return
|
748
|
+
end
|
749
|
+
line "</#{name}>"
|
750
|
+
end
|
751
|
+
end
|
752
|
+
end
|
753
|
+
end
|
754
|
+
|