Project

General

Profile

replace.parser.rb

An alternative variant of the yapser parser. - Randolph Herber, 08/18/2011 02:24 PM

 
1
%w{tokenizer reference entity yaml json}.each{
2
  |library| require library}
3

    
4
class UnexpectedBehaviorException < Exception
5
end
6

    
7
module Yapser
8

    
9
  class Nil
10
    def representation; nil; end
11
    def self.pretty; "nil"; end
12
    def pretty; self.class.pretty; end
13
  end
14
  class True
15
    def representation; true; end
16
    def self.pretty; "true"; end
17
    def pretty; self.class.pretty; end
18
  end
19
  class False
20
    def representation; false; end
21
    def self.pretty; "false"; end
22
    def pretty; self.class.pretty; end
23
  end
24

    
25
  class Parser
26

    
27
    # hash of top level configuration items of various types
28
    # the keys are the simple names associated with the values
29
    # this is the parser's internal format
30
    attr_reader :response
31

    
32
    # The lookup of @here value references occurs during the building
33
    # of the presentation format and only uses elements of the existing
34
    # @response.
35

    
36
    # array of errors detected, if not empty, then the response is not valid
37
    # errors are Values to carry line and ordinal data
38
    attr_reader :errors
39

    
40
    # See Tokenizer for tokens recognized by the tokenizer
41

    
42
    # For the purposes for supporting the needs of the yappur service,
43
    # the offical grammar further below is modified as follows:
44

    
45
    # The parser goal is a Goal.
46

    
47
    # The top level rule is:
48

    
49
    # Goal             = EntityList
50
    #                  = LeftBrace NamedValueList RightBrace
51
    #                  = LeftBracket ValueList RightBracket
52
    #                  = Complex
53
    #                  = String
54
    #                  = Numeric
55
    #                  = Nil          # in effect, not a value
56
    #                  = True         # boolean true
57
    #                  = False        # boolean false
58
    
59
    # The purpose of this grammar modification is the maintenance
60
    # of singular values.  These singular values may not be under
61
    # prolog control.  Therefore the presence of the prolog control
62
    # or a name enters the EntityList Rule
63

    
64
    # Note that all entities are named or targeted.
65
    # The entities are placed into a hash (associative array, if you prefer),
66
    # Named values are added to the top level hash.
67
    # Targeted values are applied if applicable as they are encountered
68
    # All selectors except the last must exist in the target value.
69
    # The last selector may cause an array to be extended or new key be
70
    # added to a hash.
71

    
72
    # The parser goal is an EntityList
73
    # 
74
    # EntityList       = Entity
75
    #                  = Entity Comma EntityList
76
    #                  = Include Entity Comma EntityList
77
    #                  = Exclude Entity Comma EntityList
78
    #                  = empty
79
    # Entity           = Target Is Value
80
    #                        If the target is a simple name,
81
    #                        then it adds or replaces that key
82
    #                        in the hash (associative) table; otherwise,
83
    #                        it applies an appropriate modification
84
    #                        to the hash (associative) table being built.
85
    # Value            = LeftBrace NamedValueList RightBrace
86
    #                  = LeftBracket ValueList RightBracket
87
    #                  = Complex
88
    #                  = String
89
    #                  = Numeric
90
    #                  = Reference
91
    #                  = Nil          # in effect, not a value
92
    #                  = True         # boolean true
93
    #                  = False        # boolean false
94
    # Numeric          = Ordinal
95
    #                  = Integer      # a.k.a. signed ordinal
96
    #                  = Number       # decimal point optional sign and
97
    #                                 # optional optionally signed
98
    #                                 # exponent field
99
    # Complex          = LeftParen Numeric Comma Numeric RightParen
100
    # Target           = Name SelectionList
101
    # Reference        = DB In Name Version SelectionList
102
    #                  = Here In Name SelectionList
103
    # VersionValue     = LeftParen Ordinal RightParen
104
    #                  = LeftParen Name RightParen
105
    #                                 # Name may only be 'last'
106
    #                  = empty
107
    # ValueList        = empty
108
    #                  = Value Comma ValueList
109
    # NamedValueList   = empty
110
    #                  = Name Is Value Comma NamedValueList
111
    # Selection        = LeftBracket Ordinal RightBracket
112
    #                  = Dot Name
113
    # SelectionList    = empty
114
    #                  = Selection SelectionList
115

    
116
    # Targets which are not simple names, i.e., contain /[.\[\]]/, will
117
    # be applied as parsing progresses.  If the Target is not applicable when
118
    # encountered, then an error will be generated.
119

    
120
    # Internal references local source will be resolved as they are discovered.
121

    
122
    def initialize(text,resolve_references=true)
123
      @tokenizer = Tokenizer.new(text,nil)
124
      currentPosition = @tokenizer.position
125
      @entity = true
126
      @response = {}
127
      @retain = {}
128
      @errors = []
129
      retention = true
130
      while token = @tokenizer.get
131
        post_error(
132
          "#{token.line}/#{token.ordinal}: illegal character '#{token.text}'"
133
        ) if token.kind == :error
134
      end
135
      if @errors.empty?
136
        @tokenizer.position = currentPosition 
137
        firstToken = @tokenizer.peek
138
        if firstToken and \
139
           not ([:name, :include, :exclude].include?(firstToken.kind))
140
          @entity = false
141
          @response = getValue
142
        else
143
          first = true
144
          while firstToken = @tokenizer.peek
145
            @tokenizer.setMaxPosition
146
            currentPosition = @tokenizer.position
147
            if first
148
              first = false
149
            else
150
              unless expect(:comma)
151
                post_error(
152
                  "#{firstToken.line}/#{firstToken.ordinal}: "+
153
                  "expected a comma, found #{firstToken.text.inspect}.")
154
                break
155
              end
156
            end
157
            if item = getEntity
158
              original = item.name
159
              replacement = item.value
160
              # @response is the hash table to which the item name applies
161
              unless original =~ /[.\[]/
162
                @retain[original] = retention
163
                @response[original] = research(replacement)
164
              else
165
                # to understand why the "goofy" previous_thing, to_be_applied
166
                # and thing, please read carefully The Ruby Language,
167
                # section 3.8.
168
                m = Regexp.last_match
169
                thing = @response
170
                to_be_applied = m.pre_match
171
                unless thing.has_key?(to_be_applied)
172
                  post_error(
173
                    "#{item.line}/#{item.ordinal}: "+
174
                    "#{original}: '#{field}' is not a key at the top level.")
175
                else
176
                  text = original[m.begin(0)..-1]
177
                  selection = 0
178
                  error = false
179
                  while text.size > 0
180
                    previous_thing = thing
181
                    thing = thing[to_be_applied]
182
                    selection += 1
183
                    case text[0]
184
                    when '.'
185
                      if Hash === thing
186
                        pos = text[1..-1] =~ /[.\[]/
187
                        if pos
188
                          field = text[1..pos]
189
                          text = text[(pos+1)..-1]
190
                        else
191
                          field = text[1..-1]
192
                          text = ""
193
                        end
194
                        if (text.size <= 0) || thing.has_key?(field)
195
                          to_be_applied = field
196
                        else
197
                          error = true
198
                          post_error(
199
                            "#{item.line}/#{item.ordinal}: "+
200
                            "{original}: selection #{selection} '#{field}' "+
201
                            "is not a key at that level.")
202
                        end
203
                      else
204
                        error = true
205
                        post_error(
206
                          "#{item.line}/#{item.ordinal}: "+
207
                          "{original}: selection #{selection} is not "+
208
                          "a parameter set. It is a #{thing.class.to_s}.")
209
                        break
210
                      end
211
                    when '['
212
                      if Array === thing
213
                        pos = text =~ /\]/
214
                        if pos
215
                          field = text[1...pos]
216
                          text = text[(pos+1)..-1]
217
                        else
218
                          error = true
219
                          post_error(
220
                            "#{item.line}/#{item.ordinal}: "+
221
                            "{original}: selection #{selection} index "+
222
                            "not terminated by ']'")
223
                          break
224
                        end
225
                        begin
226
                          n = Integer(field)
227
                        rescue
228
                          error = true
229
                          post_error(
230
                            "#{item.line}/#{item.ordinal}: "+
231
                            "{original}: selection #{selection} '#{field} "+
232
                            "is not a ordinal.")
233
                          break
234
                        end
235
                        if text.size == 0 || (n >= 0 && n < thing.size)
236
                          to_be_applied = n
237
                        else
238
                          error = true
239
                          post_error(
240
                            "#{item.line}/#{item.ordinal}: "+
241
                            "{original}: selection #{selection} '#{field} "+
242
                            "is not in the sequence at that level.")
243
                          break
244
                        end
245
                      else
246
                        error = true
247
                        post_error(
248
                          "#{item.line}/#{item.ordinal}: "+
249
                          "{original}: selection #{selection} "+
250
                          "is not a sequence. It is a #{thing.text}.")
251
                        break
252
                      end
253
                    else
254
                      post_error(
255
                        "#{item.line}/#{item.ordinal}: "+
256
                        "{original}: selection at #{text} unparsable")
257
                      error = true
258
                      break
259
                    end
260
                  end
261
                  # see above re "goofy" algorithm
262
                  thing[to_be_applied] = research(replacement) unless error
263
                end
264
              end
265
            else
266
              generate_syntax_error_message(currentPosition,firstToken)
267
              break # Quit on the first syntax error
268
            end
269
          end
270
        end
271
      end
272
    end
273

    
274
    def generate_syntax_error_message(here,firstToken)
275
      text = ""
276
      if (here+4) >= (@tokenizer.maxPosition)
277
        spacer = false
278
        for position in here..@tokenizer.maxPosition
279
          text += " " if spacer
280
          spacer = true
281
          text += @tokenizer[position].text
282
        end
283
      else
284
        spacer = false
285
        for position in here..(here+2)
286
          text += " " if spacer
287
          spacer = true
288
          text += @tokenizer[position].text
289
        end
290
        text += "' ... '"
291
        spacer = false
292
        for position in (@tokenizer.maxPosition-2)..@tokenizer.maxPosition
293
          text += " " if spacer
294
          spacer = true
295
          text += @tokenizer[position].text
296
        end
297
      end
298
      last = @tokenizer[@tokenizer.maxPosition]
299
      errorLocation = "#{firstToken.line}/#{firstToken.ordinal}"
300
      if firstToken.line == last.line
301
        if firstToken.ordinal != last.ordinal
302
          errorLocation += "-#{last.ordinal}"
303
        end
304
      else
305
        errorLocation += "-#{last.line}/#{last.ordinal}"
306
      end
307
      post_error("#{errorLocation}: syntax error near '#{text}'")
308
    end
309

    
310
    def presentation
311
      if @entity
312
        return_value = {}
313
        (@errors.empty? ? research(@response) : {}).each{|key, value|
314
          return_value[key] = value if @retain[key]
315
        }
316
      else
317
        return_value = (@errors.empty? ? research(@response) : {})
318
      end
319
      return_value
320
    end
321

    
322
    def prettyPrint(destination=STDOUT,indent="",unused="",mark="")
323
      Parser.prettyPrint(@response,destination,indent,"",mark,true)
324
    end
325

    
326
    # N.B., this pattern parallels those in the tokenizer for recognizing
327
    # numeric values.  Because Ruby numbers are objects, Ruby has unusual
328
    # rules about decimal points in floating point constants.  The pretty
329
    # print code has to take that into consideration.  All these regular
330
    # expression elements are discussed in the PickAxe book.
331
    @@prettyPattern = %r{
332
      # the definitions
333
      (?<exponent>         [Ee][-+]?\d{1,3}){0}
334
      (?<optionalexponent> \g<exponent>?){0}
335
      (?<floatA>           [-+]?\d+\.\d*\g<optionalexponent>){0}
336
      (?<floatB>           [-+]?\d*\.\d+\g<optionalexponent>){0}
337
      (?<floatC>           [-+]?\d+\g<optionalexponent>){0}
338
      (?<float>            (\g<floatA>|\g<floatB>|\g<floatC>)){0}
339
      (?<complex>          \(\g<float>,\g<float>\)){0}
340

341
      # the pattern
342

343
      \A(?:
344
        (?:nil) |
345
        (?:true) |
346
        (?:false) |
347
        (?:[-+]?infinity) |
348
        (?:\g<float>) |
349
        (?:\g<complex>)
350
      )\Z}x
351
      
352
    # 2010 Sept 13 -- numeric strings will not be range checked
353

    
354
    def self.prettyPrint(
355
        data=nil,destination=STDOUT,indent="",name="",mark="",toplevel=false)
356
      # input (in data) should be in re/presentation format.
357
      newIndent = indent + "  "
358
      name ||= ""
359
      tag = name
360
      case data
361
      when String
362
        destination.write(indent)
363
        if not(tag.empty?) then destination.write(tag+": ") end
364
        if data =~ @@prettyPattern
365
          destination.write(data+mark+"\n")
366
        else
367
          destination.write(
368
            '"'+data.gsub(/[\\"]/,"\\\\\\\&")+'"'+mark+"\n")
369
        end
370
      when NilClass, Nil
371
        destination.write(indent)
372
        if not(tag.empty?) then destination.write(tag+": ") end
373
        destination.write(Nil.pretty+mark+"\n")
374
      when TrueClass, True
375
        destination.write(indent)
376
        if not(tag.empty?) then destination.write(tag+": ") end
377
        destination.write(True.pretty+mark+"\n")
378
      when FalseClass, False
379
        destination.write(indent)
380
        if not(tag.empty?) then destination.write(tag+": ") end
381
        destination.write(False.pretty+mark+"\n")
382
      when Array
383
        destination.write(indent)
384
        if not(tag.empty?) then destination.write(tag+": ") end
385
        stringOut = StringIO.open
386
        stringOut.write("[\n")
387
        index = 0
388
        for item in data
389
          index = index + 1
390
          prettyPrint(
391
            item,stringOut,newIndent,"",((index == data.length)?"":","))
392
        end
393
        stringOut.write(indent+"]"+mark)
394
        text = stripWhitespace(stringOut.string)
395
        text = stringOut.string if text.size > 60
396
        destination.write(text+"\n")
397
      when Hash
398
        newIndent = indent if toplevel
399
        destination.write(indent) unless toplevel
400
        if not(tag.empty?) then destination.write(tag+": ") end
401
        stringOut = StringIO.open
402
        stringOut.write("{\n") unless toplevel
403
        index = 0
404
        keys = data.keys.sort!
405
        for key in keys
406
          index = index + 1
407
          prettyPrint(
408
            data[key],stringOut,newIndent,key,((index == data.length)?"":","))
409
        end
410
        stringOut.write(indent+"}") unless toplevel
411
        stringOut.write(mark)
412
        text1 = stringOut.string
413
        text1.gsub!(/\n+\Z/,"")
414
        text2 = stripWhitespace(text1)
415
        text2 = text1 if text2.size > 60
416
        destination.write(text2)
417
        destination.write("\n")
418
      else
419
        raise UnexpectedBehaviorException,
420
              "#{data.inspect} processed elsewhere!"
421
      end
422
      nil
423
    end
424

    
425
    def getYAML
426
      presentation.to_yaml
427
    end
428

    
429
    def getJSON
430
      presentation.to_json
431
    end
432

    
433
    def getReferences
434
      answer = getReferencesInternal(presentation)
435
      if answer.empty? then nil else answer end
436
    end
437

    
438
    private
439

    
440
    def expect(*listOfSymbols)
441
      currentPosition = @tokenizer.position
442
      result = nil
443
      token = @tokenizer.get
444
      if token
445
        if listOfSymbols.include? token.kind
446
          result = token
447
        else
448
          @tokenizer.position = currentPosition
449
        end
450
      end
451
      result
452
    end
453

    
454
    def getEntity
455
      currentPosition = @tokenizer.position
456
      newEntity = nil
457
      if (mode = expect(:include, :exclude))
458
          @retention = !!(mode.kind == :include)
459
      end
460
      target = getTarget
461
      if target
462
        if isToken = expect(:is)
463
          if value = getValue
464
            newEntity = Entity.new(
465
              target.text,value,
466
              target.line, target.ordinal)
467
          end
468
        end
469
      end
470
      @tokenizer.position = currentPosition unless newEntity
471
      return newEntity
472
    end
473

    
474
    def getValue
475
      currentPosition = @tokenizer.position
476
      token = expect(
477
        :string, :number, :integer, :ordinal, :name, :nil, :true, :false,
478
        :leftbrace, :leftbracket, :leftparen)
479
      result = nil
480
      if token
481
        case token.kind
482
        when :string
483
          result = token.text[1...-1].gsub(/\\(["\\])/,'\1')
484
        when :number, :integer, :ordinal
485
          result = token.text
486
        when :nil
487
          result = Nil.new
488
        when :true
489
          result = True.new
490
        when :false
491
          result = False.new
492
        when :name
493
          @tokenizer.backup
494
          result = getReference
495
        when :leftbrace
496
          @tokenizer.backup
497
          result = getHash
498
        when :leftbracket
499
          @tokenizer.backup
500
          result = getList
501
        when :leftparen
502
          @tokenizer.backup
503
          # getReference and getComplex handle adding the type indicators
504
          result = getReference || getComplex
505
        end
506
      end
507
      @tokenizer.position = currentPosition unless result
508
      result
509
    end
510

    
511
    def getVersion
512
      currentPosition = @tokenizer.position
513
      token1 = expect(:leftparen)
514
      if token1
515
        token2 = expect(:ordinal, :name)
516
        if token2
517
          case token2.kind
518
          when :ordinal
519
            token3 = expect(:rightparen)
520
            if token3
521
              return token2.text
522
            end
523
          when :name
524
            if token2.text != "last"
525
              post_error(
526
                "#{token2.line}/#{token2.ordinal}: "+
527
                "named version must be 'last', not '#{token2.text}'")
528
            else
529
              token3 = expect(:rightparen)
530
              if token3
531
                return "last"
532
              end
533
            end
534
          end
535
        end
536
      end
537
      @tokenizer.position = currentPosition
538
      return nil
539
    end
540

    
541
    def getTarget
542
      # Targets are Name SelectionList
543
      currentPosition = @tokenizer.position
544
      newTarget = nil
545
      token1 = expect(:name)
546
      error = False
547
      names = []
548
      if token1
549
        names.push(token1.text) 
550
        while token2 = expect(:dot, :leftbracket)
551
          case token2.kind
552
          when :dot
553
            token2 = expect(:name)
554
            if token2
555
              names.push(token2.text)
556
            else
557
              error = True
558
              post_error(
559
                "#{token1.line}/#{token1.ordinal}: "+
560
                "in a target, a . must be followed by a name.")
561
              break
562
            end
563
          when :leftbracket
564
            token2 = expect(:ordinal)
565
            if token2
566
              if token3 = expect(:rightbracket)
567
                names.push(token2.text)
568
              else
569
                error = True
570
                post_error(
571
                  "#{token1.line}/#{token1.ordinal}: "+
572
                  "in a target, an ordinal must be followed by a ].")
573
                break
574
              end
575
            else
576
              error = True
577
              post_error(
578
                "#{token1.line}/#{token1.ordinal}: "+
579
                "in a target, a [ must be followed by an ordinal.")
580
              break
581
            end
582
            break
583
          else
584
            break
585
          end
586
        end
587
        if !(error) && names.size > 0
588
          newTarget = Reference.new(
589
            names,nil,nil,token1.line,token1.ordinal)
590
        end
591
      end
592
      @tokenizer.position = currentPosition unless newTarget
593
      newTarget
594
    end
595

    
596
    def getReference
597
      currentPosition = @tokenizer.position
598
      # written this way as source is mandatory
599
      # if source were optional, then write it like getVersion
600
      newReference = nil
601
      names = []
602
      error = false
603
      source = expect(:name)
604
      source = nil unless ["here", "db"].include?(source.text)
605
      if source
606
        token1 = expect(:in)
607
        if token1
608
          token1 = expect(:name)
609
          if token1
610
            names.push(token1.text)
611
            version = (source.text == 'here') ? nil : getVersion
612
            while token2 = expect(:dot, :leftbracket)
613
              case token2.kind
614
              when :dot
615
                token2 = expect(:name)
616
                if token2
617
                  names.push(token2.text)
618
                else
619
                  error = True
620
                  post_error(
621
                    "#{source.line}/#{source.ordinal}: "+
622
                    "in a reference, a . must be followed by a name.")
623
                  break
624
                end
625
              when :leftbracket
626
                token2 = expect(:ordinal)
627
                if token2
628
                  if token3 = expect(:rightbracket)
629
                    names.push(token2.text)
630
                  else
631
                    error = True
632
                    post_error(
633
                      "#{source.line}/#{source.ordinal}: "+
634
                      "in a reference, an ordinal must be followed by a ].")
635
                    break
636
                  end
637
                else
638
                  error = True
639
                  post_error(
640
                    "#{source.line}/#{source.ordinal}: "+
641
                    "in a reference, a [ must be followed by an ordinal.")
642
                  break
643
                end
644
                break
645
              else
646
                break
647
              end
648
            end
649
            if !(error) && names.size > 0
650
              return Reference.new(
651
                names,version,source.text,source.line,source.ordinal)
652
            end
653
          end
654
        end
655
      end
656
      @tokenizer.position = currentPosition
657
      nil
658
    end
659

    
660
    def getList
661
      currentPosition = @tokenizer.position
662
      token1 = expect(:leftbracket)
663
      if token1
664
        token2 = expect(:rightbracket)
665
        return [] if token2
666
        result = []
667
        while true
668
          near = @tokenizer.peek
669
          token2 = getValue
670
          if token2
671
            result.push(token2)
672
          else
673
            if near
674
              post_error("#{near.line}/#{near.ordinal}: no array value")
675
            else
676
              post_error("at end: no array value")
677
            end
678
            break
679
          end
680
          near = @tokenizer.peek
681
          token2 = expect(:comma, :rightbracket)
682
          if token2
683
            case token2.kind
684
            when :comma
685
              next
686
            when :rightbracket
687
              return result
688
            end
689
          else
690
            if near
691
              post_error("#{near.line}/#{near.ordinal}: no array value")
692
            else
693
              post_error("at end: no array value")
694
            end
695
            break
696
          end
697
        end
698
      end
699
      @tokenizer.position = currentPosition
700
      return nil
701
    end
702

    
703
    def getHash
704
      currentPosition = @tokenizer.position
705
      token1 = expect(:leftbrace)
706
      if token1
707
        result = {}
708
        token2 = expect(:rightbrace)
709
        if token2
710
          return result
711
        end
712
        error = false
713
        while true
714
          near = @tokenizer.peek
715
          token2 = expect(:name)
716
          if token2
717
            name = token2.text
718
            if result.include? name
719
              error = true
720
              post_error(
721
                "#{token2.line}/#{token2.ordinal}: "+
722
                "duplicated name '#{token2.text}'")
723
            end
724
          else
725
            error = true
726
            if near
727
              post_error(
728
                "#{near.line}/#{near.ordinal}: no hash key")
729
            else
730
              post_error(
731
                "at end: no hash key")
732
            end
733
            break
734
          end
735
          near = @tokenizer.peek
736
          token2 = expect(:is)
737
          if not token2
738
            error = true
739
            if near
740
              post_error(
741
                "#{near.line}/#{near.ordinal}: no : hash delimiter")
742
            else
743
              post_error(
744
                "at end: no : hash delimiter")
745
            end
746
            break
747
          end
748
          near = @tokenizer.peek
749
          value = getValue
750
          if value
751
            result[name] = value
752
          else
753
            error = true
754
            if near
755
              post_error(
756
                "#{near.line}/#{near.ordinal}: no hash value")
757
            else
758
              post_error(
759
                "at end: no hash value")
760
            end
761
            break
762
          end
763
          near = @tokenizer.peek
764
          token2 = expect(:comma, :rightbrace)
765
          if token2
766
            case token2.kind
767
            when :comma
768
              next
769
            when :rightbrace
770
              return result
771
            end
772
          else
773
            error = true
774
            if near
775
              post_error(
776
                "#{near.line}/#{near.ordinal}: no hash value delimiter")
777
            else
778
              post_error(
779
                "at end: no hash value delimiter")
780
            end
781
            break
782
          end
783
        end
784
      end
785
      @tokenizer.position = currentPosition
786
      return nil
787
    end
788

    
789
    def getComplex
790
      currentPosition = @tokenizer.position
791
      if expect(:leftparen)
792
        real = expect(:number, :integer, :ordinal)
793
        if real
794
          if expect(:comma)
795
            imaginary = expect(:number, :integer, :ordinal)
796
            if imaginary
797
              if expect(:rightparen)
798
                return "("+real.text+","+imaginary.text+")"
799
              end
800
            end
801
          end
802
        end
803
      end
804
      @tokenizer.position = currentPosition
805
      return nil
806
    end
807

    
808
    def getReferencesInternal(data)
809
      result = []
810
      case data
811
      when Hash
812
        data.each_key{|key| result += getReferencesInternal(data[key])}
813
      when Array
814
        data.each{|item| result += getReferencesInternal(item)}
815
      when Reference
816
        result.push(
817
          "#{data.line}/#{data.ordinal}, "+
818
          "#{fmtReference(data)}"+
819
          ((data.cause.nil? || data.cause.empty?) ? '' : ", #{data.cause}."))
820
      end
821
      result
822
    end
823

    
824
    def fmtReference(reference)
825
      if Reference === reference
826
        result = ''
827
        result << reference.source << '::' if reference.source
828
        result << reference.names[0] if reference.names.size > 0
829
        result << ('('+reference.version+')') if reference.version
830
        reference.names[1..-1].each do |field|
831
          if field =~ /\A\d+\Z/
832
            result << ('['+field+']')
833
          else
834
            result << ('.'+field)
835
          end
836
        end
837
      else
838
        result = "not-a-reference"
839
      end
840
      result
841
    end
842

    
843
    # N.B., this pattern parallels those in the tokenizer for recognizing
844
    # numeric values.  Because Ruby numbers are objects, Ruby has unusual
845
    # rules about decimal points in floating point constants.  The pretty
846
    # print code has to take that into consideration.  All these regular
847
    # expression elements are discussed in the PickAxe book.
848
    @@complexPattern = %r{
849
      # the definitions
850
      (?<exponent>         [Ee][-+]?\d{1,3}){0}
851
      (?<optionalexponent> \g<exponent>?){0}
852
      (?<floatA>           [-+]?\d+\.\d*\g<optionalexponent>){0}
853
      (?<floatB>           [-+]?\d*\.\d+\g<optionalexponent>){0}
854
      (?<floatC>           [-+]?\d+\g<optionalexponent>){0}
855
      (?<float>            (\g<floatA>|\g<floatB>|\g<floatC>)){0}
856
      (?<complex>          \(\g<float>,\g<float>\)){0}
857

858
      # the pattern
859

860
      \A(?: (?:\g<complex>))\Z}x
861

    
862
    def research(value)
863
      case value
864
      when String
865
        copy = value.dup
866
        copy.gsub!(/\s+/,'')
867
        if %w{infinity +infinity -infinity}.include?(copy)
868
          return_value = (copy == "infinity") ? "+infinity" : copy
869
        elsif copy =~ @@complexPattern
870
          return_value = copy
871
        else
872
          return_value = value.dup
873
        end
874
      when Reference
875
        # If possible, then research the Reference's value,
876
        # otherwise, return the Reference itself
877
        case value.source
878
        when "here"
879
          # Oddly enough, the current value of @response is
880
          # exactly what we need!
881
          successful = true
882
          target = value.names[0]
883
          if target.nil? || target.empty? || !(@response.has_key?(target))
884
            value.cause = "no such key, #{target.inspect}, in parameter set"
885
            post_error(
886
              "#{value.line}/#{value.ordinal}: "+
887
              "#{value.cause}, #{fmtReference(value)}.")
888
          else
889
            target = @response[target]
890
            if target
891
              subvalue = ""
892
              for name in value.names[1..-1]
893
                if /\A\d+\Z/ === name
894
                  subvalue += "["+name+"]"
895
                else
896
                  subvalue += "."+name
897
                end
898
                case target.class.to_s
899
                when "String"
900
                  value.cause = "can not be subselected (#{subvalue})"
901
                  post_error(
902
                    "#{value.line}/#{value.ordinal}: "+
903
                    "#{value.cause}, #{fmtReference(value)}.")
904
                  successful = false
905
                  break
906
                when "Array"
907
                  if /\A\d+\Z/ === name
908
                    begin
909
                      n = name.to_i
910
                      if n < target.size
911
                        target = target[n]
912
                      else
913
                        value.cause = "invalid index (#{subvalue})"
914
                        post_error(
915
                          "#{value.line}/#{value.ordinal}: "+
916
                          "#{value.cause}, #{fmtReference(value)}.")
917
                        successful = false
918
                        break
919
                      end
920
                    rescue
921
                      value.cause = "invalid index (#{subvalue})"
922
                      post_error(
923
                        "#{value.line}/#{value.ordinal}: "+
924
                        "#{value.cause}, #{fmtReference(value)}.")
925
                      successful = false
926
                    end
927
                  else
928
                    value.cause = "nonnumeric index (#{subvalue})"
929
                    post_error(
930
                      "#{value.line}/#{value.ordinal}: "+
931
                      "#{value.cause}, #{fmtReference(value)}.")
932
                    successful = false
933
                  end
934
                when "Hash"
935
                  if target.has_key? name
936
                    target = target[name]
937
                  else
938
                    value.cause = "nonexistent key (#{subvalue})"
939
                    post_error(
940
                      "#{value.line}/#{value.ordinal}: "+
941
                      "#{value.cause}, #{fmtReference(value)}.")
942
                    successful = false
943
                  end
944
                else
945
                  value.cause = "invalid value (#{subvalue})"
946
                  post_error(
947
                    "#{value.line}/#{value.ordinal}: "+
948
                    "#{value.cause}, #{fmtReference(value)}.")
949
                  successful = false
950
                  break
951
                end
952
                break unless target and successful
953
              end
954
            end
955
          end
956
          return_value = (target and successful) ? target : value.dup
957
        when "db"
958
          return_value = value.dup
959
        else
960
          value.cause = "#{value.source} not supported"
961
          post_error(
962
            "#{value.line}/#{value.ordinal}: "+
963
            "#{value.cause}, #{fmtReference(value)}.")
964
          return_value = value.dup
965
        end
966
      when Array
967
        result = []
968
        value.each {|item| result.push(research(item))}
969
        return_value = result
970
      when Hash
971
        result = {}
972
        value.each_key {|key|
973
          result[key] = research(value[key])}
974
        return_value = result
975
      when Nil, True, False
976
        return_value = value.pretty
977
      when NilClass # occurs when an array is extended
978
        return_value = 'nil'
979
      else
980
        raise UnexpectedBehaviorException, \
981
          "Not a recognized type of value (#{value.class.to_s})"
982
      end
983
      return_value
984
    end
985
    
986
    def post_error(text)
987
      @errors.push(text) unless @errors.index(text)
988
    end
989

    
990
    def self.stripWhitespace(text)
991
      newText = ""
992
      while text && text.size > 0 && text =~ /\s+|"([^"\\]|\\["\\])*"/
993
        md = Regexp.last_match
994
        newText << md.pre_match << ((md[0][0] == '"') ? md[0] : " ")
995
        text = md.post_match
996
      end
997
      newText << text
998
      newText.gsub!(/\s+\Z/,"") # no trailing whitespace kept
999
      newText = "{" + newText[2..-1] if newText[0..1] == '{ '
1000
      newText = newText[0..-3] + '}' if newText[-2..-1] == ' }'
1001
      newText = newText[0..-4] + '},' if newText[-3..-1] == ' },'
1002
      newText = "[" + newText[2..-1] if newText[0..1] == '[ '
1003
      newText = newText[0..-3] + ']' if newText[-2..-1] == ' ]'
1004
      newText = newText[0..-4] + '],' if newText[-3..-1] == ' ],'
1005
      newText
1006
    end
1007

    
1008
    # self.floatCheck is unused.  Floats are no longer range checked.
1009

    
1010
    def self.floatCheck(integer_part=nil,fraction_part=nil,exponent_part=nil)
1011
      save_verbosity = $VERBOSE
1012
      $VERBOSE = nil
1013
      zero = begin
1014
        i_part = integer_part.nil? ? 0 : Integer(integer_part)
1015
        f_part = fraction_part.nil? ? 0 : Integer(fraction_part)
1016
        !!(i_part == 0 && f_part == 0)
1017
      rescue
1018
        false
1019
      end
1020
      if not zero
1021
        data = ""
1022
        data << ((integer_part.nil? || integer_part.empty?) \
1023
                  ? "0" : integer_part)
1024
        data << "."
1025
        data << ((fraction_part.nil? || fraction_part.empty?) \
1026
                  ? "0" : fraction_part)
1027
        if not (exponent_part.nil? || exponent_part.empty?)
1028
          data << exponent_part
1029
        end
1030
        begin
1031
          x = Float(data)
1032
          numeric = x.finite? && x != 0
1033
        rescue
1034
          numeric = false
1035
        else
1036
          $VERBOSE = save_verbosity
1037
        end
1038
      else
1039
        numeric = true
1040
      end
1041
      numeric
1042
    end
1043
  end
1044
end