magic-xml 0.2013.04.14

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 (5) hide show
  1. checksums.yaml +7 -0
  2. data/lib/magic_xml.rb +1379 -0
  3. data/test.xml +1 -0
  4. data/tests.rb +836 -0
  5. metadata +56 -0
data/lib/magic_xml.rb ADDED
@@ -0,0 +1,1379 @@
1
+ # Needed for parsing
2
+ require 'rexml/parsers/baseparser'
3
+ # Needed for fetching XMLs from the Internet
4
+ require 'uri'
5
+ require 'net/http'
6
+
7
+ # FIXME: Make comment formatting RDoc-friendly. It's not always so now.
8
+
9
+ # In Ruby 2 Symbol will be a subclass of String, and
10
+ # this won't be needed any more. Before then...
11
+ class Symbol
12
+ include Comparable
13
+ def <=>(other)
14
+ raise ArgumentError.new("comparison of #{self.class} with #{other.class} failed") unless other.is_a? Symbol
15
+ to_s <=> other.to_s
16
+ end
17
+
18
+ alias_method :eqeqeq_before_magic_xml, :===
19
+ def ===(*args, &blk)
20
+ if args.size >= 1 and args[0].is_a? XML
21
+ self == args[0].name
22
+ else
23
+ eqeqeq_before_magic_xml(*args, &blk)
24
+ end
25
+ end
26
+ end
27
+
28
+ class Hash
29
+ alias_method :eqeqeq_before_magic_xml, :===
30
+ def ===(*args, &blk)
31
+ if args.size >= 1 and args[0].is_a? XML
32
+ all?{|k,v| v === args[0][k]}
33
+ else
34
+ eqeqeq_before_magic_xml(*args, &blk)
35
+ end
36
+ end
37
+ end
38
+
39
+ class String
40
+ # Escape string for output as XML text (< > &)
41
+ def xml_escape
42
+ replacements = {"<" => "&lt;", ">" => "&gt;", "&" => "&amp;" }
43
+ gsub(/([<>&])/) { replacements[$1] }
44
+ end
45
+ # Escape characters for output as XML attribute values (< > & ' ")
46
+ def xml_attr_escape
47
+ replacements = {"<" => "&lt;", ">" => "&gt;", "&" => "&amp;", "\"" => "&quot;", "'" => "&apos;"}
48
+ gsub(/([<>&\'\"])/) { replacements[$1] }
49
+ end
50
+ # Unescape entities
51
+ # Supports:
52
+ # * Full set of HTML-compatible named entities
53
+ # * Decimal entities &#1234;
54
+ # * Hex entities &#xA0b1;
55
+ def xml_unescape(extra_entities=nil)
56
+ @@xhtml_entity_replacements ||= {
57
+ 'nbsp' => 160,
58
+ 'iexcl' => 161,
59
+ 'cent' => 162,
60
+ 'pound' => 163,
61
+ 'curren' => 164,
62
+ 'yen' => 165,
63
+ 'brvbar' => 166,
64
+ 'sect' => 167,
65
+ 'uml' => 168,
66
+ 'copy' => 169,
67
+ 'ordf' => 170,
68
+ 'laquo' => 171,
69
+ 'not' => 172,
70
+ 'shy' => 173,
71
+ 'reg' => 174,
72
+ 'macr' => 175,
73
+ 'deg' => 176,
74
+ 'plusmn' => 177,
75
+ 'sup2' => 178,
76
+ 'sup3' => 179,
77
+ 'acute' => 180,
78
+ 'micro' => 181,
79
+ 'para' => 182,
80
+ 'middot' => 183,
81
+ 'cedil' => 184,
82
+ 'sup1' => 185,
83
+ 'ordm' => 186,
84
+ 'raquo' => 187,
85
+ 'frac14' => 188,
86
+ 'frac12' => 189,
87
+ 'frac34' => 190,
88
+ 'iquest' => 191,
89
+ 'Agrave' => 192,
90
+ 'Aacute' => 193,
91
+ 'Acirc' => 194,
92
+ 'Atilde' => 195,
93
+ 'Auml' => 196,
94
+ 'Aring' => 197,
95
+ 'AElig' => 198,
96
+ 'Ccedil' => 199,
97
+ 'Egrave' => 200,
98
+ 'Eacute' => 201,
99
+ 'Ecirc' => 202,
100
+ 'Euml' => 203,
101
+ 'Igrave' => 204,
102
+ 'Iacute' => 205,
103
+ 'Icirc' => 206,
104
+ 'Iuml' => 207,
105
+ 'ETH' => 208,
106
+ 'Ntilde' => 209,
107
+ 'Ograve' => 210,
108
+ 'Oacute' => 211,
109
+ 'Ocirc' => 212,
110
+ 'Otilde' => 213,
111
+ 'Ouml' => 214,
112
+ 'times' => 215,
113
+ 'Oslash' => 216,
114
+ 'Ugrave' => 217,
115
+ 'Uacute' => 218,
116
+ 'Ucirc' => 219,
117
+ 'Uuml' => 220,
118
+ 'Yacute' => 221,
119
+ 'THORN' => 222,
120
+ 'szlig' => 223,
121
+ 'agrave' => 224,
122
+ 'aacute' => 225,
123
+ 'acirc' => 226,
124
+ 'atilde' => 227,
125
+ 'auml' => 228,
126
+ 'aring' => 229,
127
+ 'aelig' => 230,
128
+ 'ccedil' => 231,
129
+ 'egrave' => 232,
130
+ 'eacute' => 233,
131
+ 'ecirc' => 234,
132
+ 'euml' => 235,
133
+ 'igrave' => 236,
134
+ 'iacute' => 237,
135
+ 'icirc' => 238,
136
+ 'iuml' => 239,
137
+ 'eth' => 240,
138
+ 'ntilde' => 241,
139
+ 'ograve' => 242,
140
+ 'oacute' => 243,
141
+ 'ocirc' => 244,
142
+ 'otilde' => 245,
143
+ 'ouml' => 246,
144
+ 'divide' => 247,
145
+ 'oslash' => 248,
146
+ 'ugrave' => 249,
147
+ 'uacute' => 250,
148
+ 'ucirc' => 251,
149
+ 'uuml' => 252,
150
+ 'yacute' => 253,
151
+ 'thorn' => 254,
152
+ 'yuml' => 255,
153
+ 'quot' => 34,
154
+ 'apos' => 39, # Wasn't present in the HTML entities set, but is defined in XML standard
155
+ 'amp' => 38,
156
+ 'lt' => 60,
157
+ 'gt' => 62,
158
+ 'OElig' => 338,
159
+ 'oelig' => 339,
160
+ 'Scaron' => 352,
161
+ 'scaron' => 353,
162
+ 'Yuml' => 376,
163
+ 'circ' => 710,
164
+ 'tilde' => 732,
165
+ 'ensp' => 8194,
166
+ 'emsp' => 8195,
167
+ 'thinsp' => 8201,
168
+ 'zwnj' => 8204,
169
+ 'zwj' => 8205,
170
+ 'lrm' => 8206,
171
+ 'rlm' => 8207,
172
+ 'ndash' => 8211,
173
+ 'mdash' => 8212,
174
+ 'lsquo' => 8216,
175
+ 'rsquo' => 8217,
176
+ 'sbquo' => 8218,
177
+ 'ldquo' => 8220,
178
+ 'rdquo' => 8221,
179
+ 'bdquo' => 8222,
180
+ 'dagger' => 8224,
181
+ 'Dagger' => 8225,
182
+ 'permil' => 8240,
183
+ 'lsaquo' => 8249,
184
+ 'rsaquo' => 8250,
185
+ 'euro' => 8364,
186
+ 'fnof' => 402,
187
+ 'Alpha' => 913,
188
+ 'Beta' => 914,
189
+ 'Gamma' => 915,
190
+ 'Delta' => 916,
191
+ 'Epsilon' => 917,
192
+ 'Zeta' => 918,
193
+ 'Eta' => 919,
194
+ 'Theta' => 920,
195
+ 'Iota' => 921,
196
+ 'Kappa' => 922,
197
+ 'Lambda' => 923,
198
+ 'Mu' => 924,
199
+ 'Nu' => 925,
200
+ 'Xi' => 926,
201
+ 'Omicron' => 927,
202
+ 'Pi' => 928,
203
+ 'Rho' => 929,
204
+ 'Sigma' => 931,
205
+ 'Tau' => 932,
206
+ 'Upsilon' => 933,
207
+ 'Phi' => 934,
208
+ 'Chi' => 935,
209
+ 'Psi' => 936,
210
+ 'Omega' => 937,
211
+ 'alpha' => 945,
212
+ 'beta' => 946,
213
+ 'gamma' => 947,
214
+ 'delta' => 948,
215
+ 'epsilon' => 949,
216
+ 'zeta' => 950,
217
+ 'eta' => 951,
218
+ 'theta' => 952,
219
+ 'iota' => 953,
220
+ 'kappa' => 954,
221
+ 'lambda' => 955,
222
+ 'mu' => 956,
223
+ 'nu' => 957,
224
+ 'xi' => 958,
225
+ 'omicron' => 959,
226
+ 'pi' => 960,
227
+ 'rho' => 961,
228
+ 'sigmaf' => 962,
229
+ 'sigma' => 963,
230
+ 'tau' => 964,
231
+ 'upsilon' => 965,
232
+ 'phi' => 966,
233
+ 'chi' => 967,
234
+ 'psi' => 968,
235
+ 'omega' => 969,
236
+ 'thetasym' => 977,
237
+ 'upsih' => 978,
238
+ 'piv' => 982,
239
+ 'bull' => 8226,
240
+ 'hellip' => 8230,
241
+ 'prime' => 8242,
242
+ 'Prime' => 8243,
243
+ 'oline' => 8254,
244
+ 'frasl' => 8260,
245
+ 'weierp' => 8472,
246
+ 'image' => 8465,
247
+ 'real' => 8476,
248
+ 'trade' => 8482,
249
+ 'alefsym' => 8501,
250
+ 'larr' => 8592,
251
+ 'uarr' => 8593,
252
+ 'rarr' => 8594,
253
+ 'darr' => 8595,
254
+ 'harr' => 8596,
255
+ 'crarr' => 8629,
256
+ 'lArr' => 8656,
257
+ 'uArr' => 8657,
258
+ 'rArr' => 8658,
259
+ 'dArr' => 8659,
260
+ 'hArr' => 8660,
261
+ 'forall' => 8704,
262
+ 'part' => 8706,
263
+ 'exist' => 8707,
264
+ 'empty' => 8709,
265
+ 'nabla' => 8711,
266
+ 'isin' => 8712,
267
+ 'notin' => 8713,
268
+ 'ni' => 8715,
269
+ 'prod' => 8719,
270
+ 'sum' => 8721,
271
+ 'minus' => 8722,
272
+ 'lowast' => 8727,
273
+ 'radic' => 8730,
274
+ 'prop' => 8733,
275
+ 'infin' => 8734,
276
+ 'ang' => 8736,
277
+ 'and' => 8743,
278
+ 'or' => 8744,
279
+ 'cap' => 8745,
280
+ 'cup' => 8746,
281
+ 'int' => 8747,
282
+ 'there4' => 8756,
283
+ 'sim' => 8764,
284
+ 'cong' => 8773,
285
+ 'asymp' => 8776,
286
+ 'ne' => 8800,
287
+ 'equiv' => 8801,
288
+ 'le' => 8804,
289
+ 'ge' => 8805,
290
+ 'sub' => 8834,
291
+ 'sup' => 8835,
292
+ 'nsub' => 8836,
293
+ 'sube' => 8838,
294
+ 'supe' => 8839,
295
+ 'oplus' => 8853,
296
+ 'otimes' => 8855,
297
+ 'perp' => 8869,
298
+ 'sdot' => 8901,
299
+ 'lceil' => 8968,
300
+ 'rceil' => 8969,
301
+ 'lfloor' => 8970,
302
+ 'rfloor' => 8971,
303
+ 'lang' => 9001,
304
+ 'rang' => 9002,
305
+ 'loz' => 9674,
306
+ 'spades' => 9824,
307
+ 'clubs' => 9827,
308
+ 'hearts' => 9829,
309
+ 'diams' => 9830,
310
+ }
311
+ gsub(/&(?:([a-zA-Z]+)|#([0-9]+)|#x([a-fA-F0-9]+));/) {
312
+ if $1 then
313
+ v = @@xhtml_entity_replacements[$1]
314
+ # Nonstandard entity
315
+ unless v
316
+ if extra_entities.is_a? Proc
317
+ v = extra_entities.call($1)
318
+ # Well, we expect a Hash here, but any container will do.
319
+ # As long as it's not a nil.
320
+ elsif extra_entities
321
+ v = extra_entities[$1]
322
+ end
323
+ end
324
+ raise "Unknown escape #{$1}" unless v
325
+ elsif $2
326
+ v = $2.to_i
327
+ else
328
+ v = $3.hex
329
+ end
330
+ # v can be a String or an Integer
331
+ if v.is_a? String then v else [v].pack('U') end
332
+ }
333
+ end
334
+ def xml_parse
335
+ XML.parse(self)
336
+ end
337
+ end
338
+
339
+ class File
340
+ def xml_parse
341
+ XML.parse(self)
342
+ end
343
+ end
344
+
345
+ class Array
346
+ # children of any element
347
+ def children(*args, &blk)
348
+ res = []
349
+ each{|c|
350
+ res += c.children(*args, &blk) if c.is_a? XML
351
+ }
352
+ res
353
+ end
354
+ # descendants of any element
355
+ def descendants(*args, &blk)
356
+ res = []
357
+ each{|c|
358
+ res += c.descendants(*args, &blk) if c.is_a? XML
359
+ }
360
+ res
361
+ end
362
+ end
363
+
364
+ # Methods of Enumerable.
365
+ # It is not easy to design good methods, because XML
366
+ # is not really "a container", it just acts as one sometimes.
367
+ # Generally:
368
+ # * Methods that return nil should work
369
+ # * Methods that return an element should work
370
+ # * Methods that return a container should return XML container, not Array
371
+ # * Conversion methods should convert
372
+ #
373
+ # FIXME: Many methods use .dup, but do we want a shallow or a deep copy ?
374
+ class XML
375
+ include Enumerable
376
+ # Default any? is ok
377
+ # Default all? is ok
378
+
379
+ # Iterate over children, possibly with a selector
380
+ def each(*selector, &blk)
381
+ children(*selector, &blk)
382
+ self
383
+ end
384
+
385
+ # Sort XML children of XML element.
386
+ def sort_by(*args, &blk)
387
+ self.dup{ @contents = @contents.select{|c| c.is_a? XML}.sort_by(*args, &blk) }
388
+ end
389
+
390
+ # Sort children of XML element.
391
+ def children_sort_by(*args, &blk)
392
+ self.dup{ @contents = @contents.sort_by(*args, &blk) }
393
+ end
394
+
395
+ # Sort children of XML element.
396
+ #
397
+ # Using sort is highly wrong, as XML (and XML-extras) is not even Comparable.
398
+ # Use sort_by instead.
399
+ #
400
+ # Unless you define your own XML#<=> operator, or do something equally weird.
401
+ def sort(*args, &blk)
402
+ self.dup{ @contents = @contents.sort(*args, &blk) }
403
+ end
404
+
405
+ #collect/map
406
+ #detect/find
407
+ #each_cons
408
+ #each_slice
409
+ #each_with_index
410
+ #to_a
411
+ #entries
412
+ #enum_cons
413
+ #enum_slice
414
+ #enum
415
+ # grep
416
+ # include?/member?
417
+ # inject
418
+ # max/min
419
+ # max_by/min_by - Ruby 1.9
420
+ # partition
421
+ # reject
422
+ # sort
423
+ # sort_by
424
+ # to_set
425
+ # zip
426
+ # And Enumerable::Enumerator-generating methods
427
+ end
428
+
429
+ # Class methods
430
+ class XML
431
+ # XML.foo! == xml!(:foo)
432
+ # XML.foo == xml(:foo)
433
+ def self.method_missing(meth, *args, &blk)
434
+ if meth.to_s =~ /^(.*)!$/
435
+ xml!($1.to_sym, *args, &blk)
436
+ else
437
+ XML.new(meth, *args, &blk)
438
+ end
439
+ end
440
+
441
+ # Read file and parse
442
+ def self.from_file(file)
443
+ file = File.open(file) if file.is_a? String
444
+ parse(file)
445
+ end
446
+
447
+ # Fetch URL and parse
448
+ # Supported:
449
+ # http://.../
450
+ # https://.../
451
+ # file:foo.xml
452
+ # string:<foo/>
453
+ def self.from_url(url)
454
+ if url =~ /^string:(.*)$/m
455
+ parse($1)
456
+ elsif url =~ /^file:(.*)$/m
457
+ from_file($1)
458
+ elsif url =~ /^http(s?):/
459
+ ssl = ($1 == "s")
460
+ # No, seriously - Ruby needs something better than net/http
461
+ # Something that groks basic auth and queries and redirects automatically:
462
+ # HTTP_LIBRARY.get_content("http://username:passwd/u.r.l/?query")
463
+ # URI parsing must go inside the library, client programs
464
+ # should have nothing to do with it
465
+
466
+ # net/http is really inconvenient to use here
467
+ u = URI.parse(url)
468
+ # You're not seeing this:
469
+ if u.query then
470
+ path = u.path + "?" + u.query
471
+ else
472
+ path = u.path
473
+ end
474
+ req = Net::HTTP::Get.new(path)
475
+ if u.userinfo
476
+ username, passwd = u.userinfo.split(/:/,2)
477
+ req.basic_auth username, passwd
478
+ end
479
+ if ssl
480
+ # NOTE: You need libopenssl-ruby installed
481
+ # if you want to use HTTPS. Ubuntu is broken
482
+ # as it doesn't provide it in the default packages.
483
+ require 'net/https'
484
+ http = Net::HTTP.new(u.host, u.port)
485
+ http.use_ssl = true
486
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
487
+ else
488
+ http = Net::HTTP.new(u.host, u.port)
489
+ end
490
+
491
+ res = http.start {|http| http.request(req) }
492
+ # TODO: Throw a more meaningful exception
493
+ parse(res.body)
494
+ else
495
+ raise "URL protocol #{url} not supported (http, https, file, string are supported)"
496
+ end
497
+ end
498
+
499
+ # Like CDuce load_xml
500
+ # The path can be:
501
+ # * file handler
502
+ # * URL (a string with :)
503
+ # * file name (a string without :)
504
+ def self.load(obj)
505
+ if obj.is_a? String
506
+ if obj.include? ":"
507
+ from_url(obj)
508
+ else
509
+ from_file(obj)
510
+ end
511
+ else
512
+ parse(obj)
513
+ end
514
+ end
515
+
516
+ # Parse XML in mixed stream/tree mode
517
+ # Basically the idea is that every time we get start element,
518
+ # we ask the block what to do about it.
519
+ # If it wants a tree below it, it should call e.tree
520
+ # If a tree was requested, elements below the current one
521
+ # are *not* processed. If it wasn't, they are.
522
+ #
523
+ # For example:
524
+ # <foo><bar/></foo><foo2/>
525
+ # yield <foo> ... </foo>
526
+ # .complete! called
527
+ # process <foo2> next
528
+ #
529
+ # But:
530
+ # <foo><bar/></foo><foo2/>
531
+ # yield <foo> ... </foo>
532
+ # .complete! not called
533
+ # process <bar> next
534
+ #
535
+ # FIXME: yielded values are not reusable for now
536
+ # FIXME: make more object-oriented
537
+ def self.parse_as_twigs(stream)
538
+ parser = REXML::Parsers::BaseParser.new stream
539
+ # We don't really need to keep the stack ;-)
540
+ stack = []
541
+ while true
542
+ event = parser.pull
543
+ case event[0]
544
+ when :start_element
545
+ # Now the evil part evil
546
+ attrs = {}
547
+ event[2].each{|k,v| attrs[k.to_sym] = v.xml_unescape}
548
+ node = XML.new(event[1].to_sym, attrs, *event[3..-1])
549
+
550
+ # I can't say it's superelegant
551
+ class <<node
552
+ attr_accessor :do_complete
553
+ def complete!
554
+ if @do_complete
555
+ @do_complete.call
556
+ @do_complete = nil
557
+ end
558
+ end
559
+ end
560
+ node.do_complete = proc{
561
+ parse_subtree(node, parser)
562
+ }
563
+
564
+ yield(node)
565
+ if node.do_complete
566
+ stack.push node
567
+ node.do_complete = nil # It's too late, complete! shouldn't do anything now
568
+ end
569
+ when :end_element
570
+ stack.pop
571
+ when :end_document
572
+ return
573
+ else
574
+ # FIXME: Do the right thing.
575
+ # For now, ignore *everything* else
576
+ # This is totally incorrect, user might want to
577
+ # see text, comments and stuff like that anyway
578
+ end
579
+ end
580
+ end
581
+
582
+ # Basically it's a copy of self.parse, ugly ...
583
+ def self.parse_subtree(start_node, parser)
584
+ stack = [start_node]
585
+ res = nil
586
+ while true
587
+ event = parser.pull
588
+ case event[0]
589
+ when :start_element
590
+ attrs = {}
591
+ event[2].each{|k,v| attrs[k.to_sym] = v.xml_unescape}
592
+ stack << XML.new(event[1].to_sym, attrs, *event[3..-1])
593
+ if stack.size == 1
594
+ res = stack[0]
595
+ else
596
+ stack[-2] << stack[-1]
597
+ end
598
+ when :end_element
599
+ stack.pop
600
+ return if stack == []
601
+ # Needs unescaping
602
+ when :text
603
+ # Ignore whitespace
604
+ if stack.size == 0
605
+ next if event[1] !~ /\S/
606
+ raise "Non-whitespace text out of document root"
607
+ end
608
+ stack[-1] << event[1].xml_unescape
609
+ # CDATA is already unescaped
610
+ when :cdata
611
+ if stack.size == 0
612
+ raise "CDATA out of the document root"
613
+ end
614
+ stack[-1] << event[1]
615
+ when :end_document
616
+ raise "Parse error: end_document inside a subtree, tags are not balanced"
617
+ when :xmldecl,:start_doctype,:end_doctype,:elementdecl,:processing_instruction
618
+ # Positivery ignore
619
+ when :comment,:externalentity,:entity,:attlistdecl,:notationdecl
620
+ # Ignore ???
621
+ #print "Ignored XML event #{event[0]} when parsing\n"
622
+ else
623
+ # Huh ? What's that ?
624
+ #print "Unknown XML event #{event[0]} when parsing\n"
625
+ end
626
+ end
627
+ res
628
+
629
+ end
630
+
631
+ # Parse XML using REXML. Available options:
632
+ # * :extra_entities => Proc or Hash (default = nil)
633
+ # * :remove_pretty_printing => true/false (default = false)
634
+ # * :comments => true/false (default = false)
635
+ # * :pi => true/false (default = false)
636
+ # * :normalize => true/false (default = false) - normalize
637
+ # * :multiple_roots => true/false (default=false) - document
638
+ # can have any number of roots (instread of one).
639
+ # Return all in an array instead of root/nil.
640
+ # Also include non-elements (String/PI/Comment) in the return set !!!
641
+ #
642
+ # FIXME: :comments/:pi will break everything
643
+ # if there are comments/PIs outside document root.
644
+ # Now PIs are outside the document root more often than not,
645
+ # so we're pretty much screwed here.
646
+ #
647
+ # FIXME: Integrate all kinds of parse, and make them support extra options
648
+ #
649
+ # FIXME: Benchmark normalize!
650
+ #
651
+ # FIXME: Benchmark dup-based Enumerable methods
652
+ #
653
+ # FIXME: Make it possible to include bogus XML_Document superparent,
654
+ # and to make it support out-of-root PIs/Comments
655
+ def self.parse(stream, options={})
656
+ extra_entities = options[:extra_entities]
657
+
658
+ parser = REXML::Parsers::BaseParser.new stream
659
+ stack = [[]]
660
+
661
+ while true
662
+ event = parser.pull
663
+ case event[0]
664
+ when :start_element
665
+ attrs = {}
666
+ event[2].each{|k,v| attrs[k.to_sym] = v.xml_unescape(extra_entities) }
667
+ stack << XML.new(event[1].to_sym, attrs, event[3..-1])
668
+ stack[-2] << stack[-1]
669
+ when :end_element
670
+ stack.pop
671
+ # Needs unescaping
672
+ when :text
673
+ e = event[1].xml_unescape(extra_entities)
674
+ # Either inside root or in multi-root mode
675
+ if stack.size > 1 or options[:multiple_roots]
676
+ stack[-1] << e
677
+ elsif event[1] !~ /\S/
678
+ # Ignore out-of-root whitespace in single-root mode
679
+ else
680
+ raise "Non-whitespace text out of document root (and not in multiroot mode): #{event[1]}"
681
+ end
682
+ # CDATA is already unescaped
683
+ when :cdata
684
+ e = event[1]
685
+ if stack.size > 1 or options[:multiple_roots]
686
+ stack[-1] << e
687
+ else
688
+ raise "CDATA out of the document root"
689
+ end
690
+ when :comment
691
+ next unless options[:comments]
692
+ e = XML_Comment.new(event[1])
693
+ if stack.size > 1 or options[:multiple_roots]
694
+ stack[-1] << e
695
+ else
696
+ # FIXME: Ugly !
697
+ raise "Comments out of the document root"
698
+ end
699
+ when :processing_instruction
700
+ # FIXME: Real PI node
701
+ next unless options[:pi]
702
+ e = XML_PI.new(event[1], event[2])
703
+ if stack.size > 1 or options[:multiple_roots]
704
+ stack[-1] << e
705
+ else
706
+ # FIXME: Ugly !
707
+ raise "Processing instruction out of the document root"
708
+ end
709
+ when :end_document
710
+ break
711
+ when :xmldecl,:start_doctype,:end_doctype,:elementdecl
712
+ # Positivery ignore
713
+ when :externalentity,:entity,:attlistdecl,:notationdecl
714
+ # Ignore ???
715
+ #print "Ignored XML event #{event[0]} when parsing\n"
716
+ else
717
+ # Huh ? What's that ?
718
+ #print "Unknown XML event #{event[0]} when parsing\n"
719
+ end
720
+ end
721
+ roots = stack[0]
722
+
723
+ roots.each{|root| root.remove_pretty_printing!} if options[:remove_pretty_printing]
724
+ # :remove_pretty_printing does :normalize anyway
725
+ roots.each{|root| root.normalize!} if options[:normalize]
726
+ if options[:multiple_roots]
727
+ roots
728
+ else
729
+ roots[0]
730
+ end
731
+ end
732
+
733
+ # Parse a sequence. Equivalent to XML.parse(stream, :multiple_roots => true).
734
+ def self.parse_sequence(stream, options={})
735
+ o = options.dup
736
+ o[:multiple_roots] = true
737
+ parse(stream, o)
738
+ end
739
+
740
+ # Renormalize a string containing XML document
741
+ def self.renormalize(stream)
742
+ parse(stream).to_s
743
+ end
744
+
745
+ # Renormalize a string containing a sequence of XML documents
746
+ # and strings
747
+ # XMLrenormalize_sequence("<hello />, <world></world>!") =>
748
+ # "<hello/>, <world/>!"
749
+ def self.renormalize_sequence(stream)
750
+ parse_sequence(stream).join
751
+ end
752
+ end
753
+
754
+ # Instance methods (other than those of Enumerable)
755
+ class XML
756
+ attr_accessor :name, :attrs, :contents
757
+
758
+ # initialize can be run in many ways
759
+ # * XML.new
760
+ # * XML.new(:tag_symbol)
761
+ # * XML.new(:tag_symbol, {attributes})
762
+ # * XML.new(:tag_symbol, "children", "more", XML.new(...))
763
+ # * XML.new(:tag_symbol, {attributes}, "and", "children")
764
+ # * XML.new(:tag_symbol) { monadic code }
765
+ # * XML.new(:tag_symbol, {attributes}) { monadic code }
766
+ #
767
+ # Or even:
768
+ # * XML.new(:tag_symbol, "children") { and some monadic code }
769
+ # * XML.new(:tag_symbol, {attributes}, "children") { and some monadic code }
770
+ # But typically you won't be mixing these two style
771
+ #
772
+ # Attribute values can will be converted to strings
773
+ def initialize(*args, &blk)
774
+ @name = nil
775
+ @attrs = {}
776
+ @contents = []
777
+ @name = args.shift if args.size != 0
778
+ if args.size != 0 and args[0].is_a? Hash
779
+ args.shift.each{|k,v|
780
+ # Do automatic conversion here
781
+ # This also assures that the hashes are *not* shared
782
+ self[k] = v
783
+ }
784
+ end
785
+ # Expand Arrays passed as arguments
786
+ self << args
787
+ # FIXME: We'd rather not have people say @name = :foo there :-)
788
+ if blk
789
+ instance_eval(&blk)
790
+ end
791
+ end
792
+
793
+ # Convert to a well-formatted XML
794
+ def to_s
795
+ "<#{@name}" + @attrs.sort.map{|k,v| " #{k}='#{v.xml_attr_escape}'"}.join +
796
+ if @contents.size == 0
797
+ "/>"
798
+ else
799
+ ">" + @contents.map{|x| if x.is_a? String then x.xml_escape else x.to_s end}.join + "</#{name}>"
800
+ end
801
+ end
802
+
803
+ # Convert to a well-formatted XML, but without children information.
804
+ # This is a reasonable format for irb and debugging.
805
+ # If you want to see a few levels of children, call inspect(2) and so on
806
+ def inspect(include_children=0)
807
+ "<#{@name}" + @attrs.sort.map{|k,v| " #{k}='#{v.xml_attr_escape}'"}.join +
808
+ if @contents.size == 0
809
+ "/>"
810
+ elsif include_children == 0
811
+ ">...</#{name}>"
812
+ else
813
+ ">" + @contents.map{|x| if x.is_a? String then x.xml_escape else x.inspect(include_children-1) end}.join + "</#{name}>"
814
+ end
815
+ end
816
+
817
+ # Read attributes.
818
+ # Also works with pseudoattributes:
819
+ # img[:@x] == img.child(:x).text # or nil if there isn't any.
820
+ def [](key)
821
+ if key.to_s[0] == ?@
822
+ tag = key.to_s[1..-1].to_sym
823
+ c = child(tag)
824
+ if c
825
+ c.text
826
+ else
827
+ nil
828
+ end
829
+ else
830
+ @attrs[key]
831
+ end
832
+ end
833
+
834
+ # Set attributes.
835
+ # Value is automatically converted to String, so you can say:
836
+ # img[:x] = 200
837
+ # Also works with pseudoattributes:
838
+ # foo[:@bar] = "x"
839
+ def []=(key, value)
840
+ if key.to_s[0] == ?@
841
+ tag = key.to_s[1..-1].to_sym
842
+ c = child(tag)
843
+ if c
844
+ c.contents = [value.to_s]
845
+ else
846
+ self << XML.new(tag, value.to_s)
847
+ end
848
+ else
849
+ @attrs[key] = value.to_s
850
+ end
851
+ end
852
+
853
+ # Add children.
854
+ # Possible uses:
855
+ # * Add single element
856
+ # self << xml(...)
857
+ # self << "foo"
858
+ # Add nothing:
859
+ # self << nil
860
+ # Add multiple elements (also works recursively):
861
+ # self << [a, b, c]
862
+ # self << [a, [b, c], d]
863
+ def <<(cnt)
864
+ if cnt.nil?
865
+ # skip
866
+ elsif cnt.is_a? Array
867
+ cnt.each{|elem| self << elem}
868
+ else
869
+ @contents << cnt
870
+ end
871
+ self
872
+ end
873
+
874
+ # Equality test, works as if XMLs were normalized, so:
875
+ # XML.new(:foo, "Hello, ", "world") == XML.new(:foo, "Hello, world")
876
+ def ==(x)
877
+ return false unless x.is_a? XML
878
+ return false unless name == x.name and attrs == x.attrs
879
+ # Now the hard part, strings can be split in different ways
880
+ # empty string children are possible etc.
881
+ self_i = 0
882
+ othr_i = 0
883
+ while self_i != contents.size or othr_i != x.contents.size
884
+ # Ignore ""s
885
+ if contents[self_i].is_a? String and contents[self_i] == ""
886
+ self_i += 1
887
+ next
888
+ end
889
+ if x.contents[othr_i].is_a? String and x.contents[othr_i] == ""
890
+ othr_i += 1
891
+ next
892
+ end
893
+
894
+ # If one is finished and the other contains non-empty elements,
895
+ # they are not equal
896
+ return false if self_i == contents.size or othr_i == x.contents.size
897
+
898
+ # Are they both Strings ?
899
+ # Strings can be divided in different ways, and calling normalize!
900
+ # here would be rather expensive, so let's use this complicated
901
+ # algorithm
902
+ if contents[self_i].is_a? String and x.contents[othr_i].is_a? String
903
+ a = contents[self_i]
904
+ b = x.contents[othr_i]
905
+ self_i += 1
906
+ othr_i += 1
907
+ while a != "" or b != ""
908
+ if a == b
909
+ a = ""
910
+ b = ""
911
+ elsif a.size > b.size and a[0, b.size] == b
912
+ a = a[b.size..-1]
913
+ if x.contents[othr_i].is_a? String
914
+ b = x.contents[othr_i]
915
+ othr_i += 1
916
+ next
917
+ end
918
+ elsif b.size > a.size and b[0, a.size] == a
919
+ b = b[a.size..-1]
920
+ if contents[self_i].is_a? String
921
+ a = contents[self_i]
922
+ self_i += 1
923
+ next
924
+ end
925
+ else
926
+ return false
927
+ end
928
+ end
929
+ next
930
+ end
931
+
932
+ # OK, so at least one of them is not a String.
933
+ # Hopefully they're either both XMLs or one is an XML and the
934
+ # other is a String. It is also possible that contents contains
935
+ # something illegal, but we aren't catching that,
936
+ # so xml(:foo, Garbage.new) is going to at least equal itself.
937
+ # And we aren't, because xml(:foo, Garbage.new) == xml(:bar, Garbage.new)
938
+ # is going to return an honest false, and incoherent sanity
939
+ # check is worse than no sanity check.
940
+ #
941
+ # Oh yeah, they can be XML_PI or XML_Comment. In such case, this
942
+ # is ok.
943
+ return false unless contents[self_i] == x.contents[othr_i]
944
+ self_i += 1
945
+ othr_i += 1
946
+ end
947
+ return true
948
+ end
949
+
950
+ alias_method :real_method_missing, :method_missing
951
+ # Define all foo!-methods for monadic interface, so you can write:
952
+ #
953
+ def method_missing(meth, *args, &blk)
954
+ if meth.to_s =~ /^(.*)!$/
955
+ self << XML.new($1.to_sym, *args, &blk)
956
+ else
957
+ real_method_missing(meth, *args, &blk)
958
+ end
959
+ end
960
+
961
+ # Make monadic interface more "official"
962
+ # * node.exec! { foo!; bar! }
963
+ # is equivalent to
964
+ # * node << xml(:foo) << xml(:bar)
965
+ def exec!(&blk)
966
+ instance_eval(&blk)
967
+ end
968
+
969
+ # Select a subtree
970
+ # NOTE: Uses object_id of the start/end tags !
971
+ # They have to be the same, not just identical !
972
+ # <foo>0<a>1</a><b/><c/><d>2</d><e/>3</foo>.range(<a>1</a>, <d>2</d>)
973
+ # returns
974
+ # <foo><b/><c/></foo>
975
+ # start and end and their descendants are not included in
976
+ # the result tree.
977
+ # Either start or end can be nil.
978
+ # * If both start and end are nil, return whole tree.
979
+ # * If start is nil, return subtree up to range_end.
980
+ # * If start is not inside the tree, return nil.
981
+ # * If end is nil, return subtree from start
982
+ # * If end is not inside the tree, return subtree from start.
983
+ # * If end is before or below start, or they're the same node, the result is unspecified.
984
+ # * if end comes directly after start, or as first node when start==nil, return path reaching there.
985
+ def range(range_start, range_end, end_reached_cb=nil)
986
+ if range_start == nil
987
+ result = XML.new(name, attrs)
988
+ else
989
+ result = nil
990
+ end
991
+ @contents.each {|c|
992
+ # end reached !
993
+ if range_end and c.object_id == range_end.object_id
994
+ end_reached_cb.call if end_reached_cb
995
+ break
996
+ end
997
+ # start reached !
998
+ if range_start and c.object_id == range_start.object_id
999
+ result = XML.new(name, attrs)
1000
+ next
1001
+ end
1002
+ if result # We already started
1003
+ if c.is_a? XML
1004
+ break_me = false
1005
+ result.add! c.range(nil, range_end, lambda{ break_me = true })
1006
+ if break_me
1007
+ end_reached_cb.call if end_reached_cb
1008
+ break
1009
+ end
1010
+ else # String/XML_PI/XML_Comment
1011
+ result.add! c
1012
+ end
1013
+ else
1014
+ # Strings/XML_PI/XML_Comment obviously cannot start a range
1015
+ if c.is_a? XML
1016
+ break_me = false
1017
+ r = c.range(range_start, range_end, lambda{ break_me = true })
1018
+ if r
1019
+ # start reached !
1020
+ result = XML.new(name, attrs, r)
1021
+ end
1022
+ if break_me
1023
+ # end reached !
1024
+ end_reached_cb.call if end_reached_cb
1025
+ break
1026
+ end
1027
+ end
1028
+ end
1029
+ }
1030
+ return result
1031
+ end
1032
+
1033
+ # XML#subsequence is similar to XML#range, but instead of
1034
+ # trimmed subtree in returns a list of elements
1035
+ # The same elements are included in both cases, but here
1036
+ # we do not include any parents !
1037
+ #
1038
+ # <foo><a/><b/><c/></foo>.range(a,c) => <foo><b/></foo>
1039
+ # <foo><a/><b/><c/></foo>.subsequence(a,c) => <b/>
1040
+ #
1041
+ # <foo><a><a1/></a><b/><c/></foo>.range(a1,c) => <foo><a/><b/></foo> # Does <a/> make sense ?
1042
+ # <foo><a><a1/></a><b/><c/></foo>.subsequence(a1,c) => <b/>
1043
+ #
1044
+ # <foo><a><a1/><a2/></a><b/><c/></foo>.range(a1,c) => <foo><a><a2/></a><b/></foo>
1045
+ # <foo><a><a1/><a2/></a><b/><c/></foo>.subsequence(a1,c) => <a2/><b/>
1046
+ #
1047
+ # And we return [], not nil if nothing matches
1048
+ def subsequence(range_start, range_end, start_seen_cb=nil, end_seen_cb=nil)
1049
+ result = []
1050
+ start_seen = range_start.nil?
1051
+ @contents.each{|c|
1052
+ if range_end and range_end.object_id == c.object_id
1053
+ end_seen_cb.call if end_seen_cb
1054
+ break
1055
+ end
1056
+ if range_start and range_start.object_id == c.object_id
1057
+ start_seen = true
1058
+ start_seen_cb.call if start_seen_cb
1059
+ next
1060
+ end
1061
+ if start_seen
1062
+ if c.is_a? XML
1063
+ break_me = false
1064
+ result += c.subsequence(nil, range_end, nil, lambda{break_me=true})
1065
+ break if break_me
1066
+ else # String/XML_PI/XML_Comment
1067
+ result << c
1068
+ end
1069
+ else
1070
+ # String/XML_PI/XML_Comment cannot start a subsequence
1071
+ if c.is_a? XML
1072
+ break_me = false
1073
+ result += c.subsequence(range_start, range_end, lambda{start_seen=true}, lambda{break_me=true})
1074
+ break if break_me
1075
+ end
1076
+ end
1077
+ }
1078
+ # Include starting tag if it was right from the range_start
1079
+ # Otherwise, return just the raw sequence
1080
+ result = [XML.new(@name, @attrs, result)] if range_start == nil
1081
+ return result
1082
+ end
1083
+
1084
+ # =~ for a few reasonable patterns
1085
+ def =~(pattern)
1086
+ if pattern.is_a? Symbol
1087
+ @name == pattern
1088
+ elsif pattern.is_a? Regexp
1089
+ rv = text =~ pattern
1090
+ else # Hash, Pattern_any, Pattern_all
1091
+ pattern === self
1092
+ end
1093
+ end
1094
+
1095
+ # Get rid of pretty-printing whitespace. Also normalizes the XML.
1096
+ def remove_pretty_printing!(exceptions=nil)
1097
+ normalize!
1098
+ real_remove_pretty_printing!(exceptions)
1099
+ normalize!
1100
+ end
1101
+
1102
+ # normalize! is already recursive, so only one call at top level is needed.
1103
+ # This helper method lets us avoid extra calls to normalize!.
1104
+ def real_remove_pretty_printing!(exceptions=nil)
1105
+ return if exceptions and exceptions.include? @name
1106
+ each{|c|
1107
+ if c.is_a? String
1108
+ c.sub!(/^\s+/, "")
1109
+ c.sub!(/\s+$/, "")
1110
+ c.gsub!(/\s+/, " ")
1111
+ elsif c.is_a? XML_PI or c.is_a? XML_Comment
1112
+ else
1113
+ c.real_remove_pretty_printing!(exceptions)
1114
+ end
1115
+ }
1116
+ end
1117
+
1118
+ protected :real_remove_pretty_printing!
1119
+
1120
+ # Add pretty-printing whitespace. Also normalizes the XML.
1121
+ def add_pretty_printing!
1122
+ normalize!
1123
+ real_add_pretty_printing!
1124
+ normalize!
1125
+ end
1126
+
1127
+ def real_add_pretty_printing!(indent = "")
1128
+ return if @contents.empty?
1129
+ each{|c|
1130
+ if c.is_a? XML
1131
+ c.real_add_pretty_printing!(indent+" ")
1132
+ elsif c.is_a? String
1133
+ c.gsub!(/\n\s*/, "\n#{indent} ")
1134
+ end
1135
+ }
1136
+ @contents = @contents.inject([]){|children, c| children + ["\n#{indent} ", c]}+["\n#{indent}"]
1137
+ end
1138
+
1139
+ protected :real_add_pretty_printing!
1140
+
1141
+ alias_method :raw_dup, :dup
1142
+ # This is not a trivial method - first it does a *deep* copy,
1143
+ # second it takes a block which is instance_eval'ed,
1144
+ # so you can do things like:
1145
+ # * node.dup{ @name = :foo }
1146
+ # * node.dup{ self[:color] = "blue" }
1147
+ def dup(&blk)
1148
+ new_obj = self.raw_dup
1149
+ # Attr values stay shared - ugly
1150
+ new_obj.attrs = new_obj.attrs.dup
1151
+ new_obj.contents = new_obj.contents.map{|c| c.dup}
1152
+
1153
+ new_obj.instance_eval(&blk) if blk
1154
+ return new_obj
1155
+ end
1156
+
1157
+
1158
+ # Add some String children (all attributes get to_s'ed)
1159
+ def text!(*args)
1160
+ args.each{|s| self << s.to_s}
1161
+ end
1162
+ # Add XML child
1163
+ def xml!(*args, &blk)
1164
+ @contents << XML.new(*args, &blk)
1165
+ end
1166
+
1167
+ alias_method :add!, :<<
1168
+
1169
+ # Normalization means joining strings
1170
+ # and getting rid of ""s, recursively
1171
+ def normalize!
1172
+ new_contents = []
1173
+ @contents.each{|c|
1174
+ if c.is_a? String
1175
+ next if c == ""
1176
+ if new_contents[-1].is_a? String
1177
+ new_contents[-1] += c
1178
+ next
1179
+ end
1180
+ else
1181
+ c.normalize!
1182
+ end
1183
+ new_contents.push c
1184
+ }
1185
+ @contents = new_contents
1186
+ end
1187
+
1188
+ # Return text below the node, stripping all XML tags,
1189
+ # "<foo>Hello, <bar>world</bar>!</foo>".xml_parse.text
1190
+ # returns "Hello, world!"
1191
+ def text
1192
+ res = ""
1193
+ @contents.each{|c|
1194
+ if c.is_a? XML
1195
+ res << c.text
1196
+ elsif c.is_a? String
1197
+ res << c
1198
+ end # Ignore XML_PI/XML_Comment
1199
+ }
1200
+ res
1201
+ end
1202
+
1203
+ # Equivalent to node.children(pat, *rest)[0]
1204
+ # Returns nil if there aren't any matching children
1205
+ def child(pat=nil, *rest)
1206
+ children(pat, *rest) {|c|
1207
+ return c
1208
+ }
1209
+ return nil
1210
+ end
1211
+
1212
+ # Equivalent to node.descendants(pat, *rest)[0]
1213
+ # Returns nil if there aren't any matching descendants
1214
+ def descendant(pat=nil, *rest)
1215
+ descendants(pat, *rest) {|c|
1216
+ return c
1217
+ }
1218
+ return nil
1219
+ end
1220
+
1221
+ # XML#children(pattern, more_patterns)
1222
+ # Return all children of a node with tags matching tag.
1223
+ # Also:
1224
+ # * children(:a, :b) == children(:a).children(:b)
1225
+ # * children(:a, :*, :c) == children(:a).descendants(:c)
1226
+ def children(pat=nil, *rest, &blk)
1227
+ return descendants(*rest, &blk) if pat == :*
1228
+ res = []
1229
+ @contents.each{|c|
1230
+ if pat.nil? or pat === c
1231
+ if rest == []
1232
+ res << c
1233
+ yield c if block_given?
1234
+ else
1235
+ res += c.children(*rest, &blk)
1236
+ end
1237
+ end
1238
+ }
1239
+ res
1240
+ end
1241
+
1242
+ # * XML#descendants
1243
+ # * XML#descendants(pattern)
1244
+ # * XML#descendants(pattern, more_patterns)
1245
+ #
1246
+ # Return all descendants of a node matching the pattern.
1247
+ # If pattern==nil, simply return all descendants.
1248
+ # Optionally run a block on each of them if a block was given.
1249
+ # If pattern==nil, also match Strings !
1250
+ def descendants(pat=nil, *rest, &blk)
1251
+ res = []
1252
+ @contents.each{|c|
1253
+ if pat.nil? or pat === c
1254
+ if rest == []
1255
+ res << c
1256
+ yield c if block_given?
1257
+ else
1258
+ res += c.children(*rest, &blk)
1259
+ end
1260
+ end
1261
+ if c.is_a? XML
1262
+ res += c.descendants(pat, *rest, &blk)
1263
+ end
1264
+ }
1265
+ res
1266
+ end
1267
+
1268
+ # Change elements based on pattern
1269
+ def deep_map(pat, &blk)
1270
+ if self =~ pat
1271
+ yield self
1272
+ else
1273
+ r = XML.new(self.name, self.attrs)
1274
+ each{|c|
1275
+ if c.is_a? XML
1276
+ r << c.deep_map(pat, &blk)
1277
+ else
1278
+ r << c
1279
+ end
1280
+ }
1281
+ r
1282
+ end
1283
+ end
1284
+
1285
+ # FIXME: do we want a shallow or a deep copy here ?
1286
+ # Map children, but leave the name/attributes
1287
+ def map(pat=nil)
1288
+ r = XML.new(self.name, self.attrs)
1289
+ each{|c|
1290
+ if !pat || (c.is_a?(XML) && c =~ pat)
1291
+ r << yield(c)
1292
+ else
1293
+ r << c
1294
+ end
1295
+ }
1296
+ r
1297
+ end
1298
+ end
1299
+
1300
+ # FIXME: Is this even sane ?
1301
+ # * What about escaping and all that stuff ?
1302
+ # * Rest of the code assumes that everything is either XML or String
1303
+ class XML_PI
1304
+ def initialize(c, t)
1305
+ @c = c
1306
+ @t = t
1307
+ end
1308
+ def to_s
1309
+ "<?#{@c}#{@t}?>"
1310
+ end
1311
+ end
1312
+
1313
+ # FIXME: Is this even sane ?
1314
+ # * What about escaping and all that stuff ?
1315
+ # * Rest of the code assumes that everything is either XML or String
1316
+ # * There are some limitations on where one can put -s in the comment. Do not overdo.
1317
+ class XML_Comment
1318
+ def initialize(c)
1319
+ @c = c
1320
+ end
1321
+ def to_s
1322
+ "<!--#{@c}-->"
1323
+ end
1324
+ end
1325
+
1326
+ # Syntactic sugar for XML.new
1327
+ def xml(*args, &blk)
1328
+ XML.new(*args, &blk)
1329
+ end
1330
+
1331
+ # xml! in XML { ... } - context adds node to parent
1332
+ # xml! in main context prints the argument (and returns it anyway)
1333
+ def xml!(*args, &blk)
1334
+ node = xml(*args, &blk)
1335
+ print node
1336
+ node
1337
+ end
1338
+
1339
+ # Perl 6 is supposed to have native support for something like that.
1340
+ # Constructor takes multiple patterns. The object matches if they all match.
1341
+ #
1342
+ # Usage:
1343
+ # case foo
1344
+ # when all(:foo, {:color => 'blue'}, /Hello/)
1345
+ # print foo
1346
+ # end
1347
+ class Patterns_all
1348
+ def initialize(*patterns)
1349
+ @patterns = patterns
1350
+ end
1351
+ def ===(obj)
1352
+ @patterns.all?{|p| p === obj}
1353
+ end
1354
+ end
1355
+
1356
+ def all(*patterns)
1357
+ Patterns_all.new(*patterns)
1358
+ end
1359
+
1360
+ # Perl 6 is supposed to have native support for something like that.
1361
+ # Constructor takes multiple patterns. The object matches if they all match.
1362
+ #
1363
+ # Usage:
1364
+ # case foo
1365
+ # when all(:foo, any({:color => 'blue'}, {:color => 'red'}), /Hello/)
1366
+ # print foo
1367
+ # end
1368
+ class Patterns_any
1369
+ def initialize(*patterns)
1370
+ @patterns = patterns
1371
+ end
1372
+ def ===(obj)
1373
+ @patterns.any?{|p| p === obj}
1374
+ end
1375
+ end
1376
+
1377
+ def any(*patterns)
1378
+ Patterns_any.new(*patterns)
1379
+ end