]> git.wincent.com - walrat.git/blob - lib/walrat/parser_state.rb
Initial import (extraction from Walrus repo, commit 0c9d44c)
[walrat.git] / lib / walrat / parser_state.rb
1 # Copyright 2007-2010 Wincent Colaiuta. All rights reserved.
2 # Redistribution and use in source and binary forms, with or without
3 # modification, are permitted provided that the following conditions are met:
4 #
5 # 1. Redistributions of source code must retain the above copyright notice,
6 #    this list of conditions and the following disclaimer.
7 # 2. Redistributions in binary form must reproduce the above copyright notice,
8 #    this list of conditions and the following disclaimer in the documentation
9 #    and/or other materials provided with the distribution.
10 #
11 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
12 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
13 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
14 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
15 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
16 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
17 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
18 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
19 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
20 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
21 # POSSIBILITY OF SUCH DAMAGE.
22
23 require 'walrat'
24
25 module Walrat
26   # Simple class for maintaining state during a parse operation.
27   class ParserState
28     attr_reader :options
29
30     # Returns the remainder (the unparsed portion) of the string. Will return
31     # an empty string if already at the end of the string.
32     attr_reader :remainder
33
34     # Raises an ArgumentError if string is nil.
35     def initialize string, options = {}
36       raise ArgumentError, 'nil string' if string.nil?
37       self.base_string        = string
38       @results                = ArrayResult.new # for accumulating results
39       @remainder              = @base_string.clone
40       @scanned                = ''
41       @options                = options.clone
42
43       # start wherever we last finished (doesn't seem to behave different to
44       # the alternative)
45       @options[:line_start]   = (@options[:line_end] or @options[:line_start] or 0)
46       @options[:column_start] = (@options[:column_end] or @options[:column_start] or 0)
47       #@options[:line_start]   = 0 if @options[:line_start].nil?
48       #@options[:column_start] = 0 if @options[:column_start].nil?
49
50       # before parsing begins, end point is equal to start point
51       @options[:line_end]     = @options[:line_start]
52       @options[:column_end]   = @options[:column_start]
53       @original_line_start    = @options[:line_start]
54       @original_column_start  = @options[:column_start]
55     end
56
57     # The parsed method is used to inform the receiver of a successful parsing
58     # event.
59     #
60     # Note that substring need not actually be a String but it must respond to
61     # the following messages:
62     #   - "line_end" and "column_end" so that the end position of the receiver
63     #     can be updated
64     # As a convenience returns the remainder.
65     # Raises an ArgumentError if substring is nil.
66     def parsed substring
67       raise ArgumentError if substring.nil?
68       update_and_return_remainder_for_string substring, true
69     end
70
71     # The skipped method is used to inform the receiver of a successful parsing
72     # event where the parsed substring should be consumed but not included in
73     # the accumulated results.
74     # The substring should respond to "line_end" and "column_end".
75     # In all other respects this method behaves exactly like the parsed method.
76     def skipped substring
77       raise ArgumentError if substring.nil?
78       update_and_return_remainder_for_string substring
79     end
80
81     # The auto_skipped method is used to inform the receiver of a successful
82     # parsing event where the parsed substring should be consumed but not
83     # included in the accumulated results and furthermore the parse event
84     # should not affect the overall bounds of the parse result. In reality this
85     # means that the method is only ever called upon the successful use of a
86     # automatic intertoken "skipping" parslet. By definition this method should
87     # only be called for intertoken skipping otherwise incorrect results will
88     # be produced.
89     def auto_skipped substring
90       raise ArgumentError if substring.nil?
91       a, b, c, d = @options[:line_start], @options[:column_start],
92         @options[:line_end], @options[:column_end] # save
93       remainder = update_and_return_remainder_for_string(substring)
94       @options[:line_start], @options[:column_start],
95         @options[:line_end], @options[:column_end] = a, b, c, d # restore
96       remainder
97     end
98
99     # Returns the results accumulated so far.
100     # Returns an empty array if no results have been accumulated.
101     # Returns a single object if only one result has been accumulated.
102     # Returns an array of objects if multiple results have been accumulated.
103     def results
104       updated_start       = [@original_line_start, @original_column_start]
105       updated_end         = [@options[:line_end], @options[:column_end]]
106       updated_source_text = @scanned.clone
107
108       if @results.length == 1
109         # here we ask the single result to exhibit container-like properties
110         # use the "outer" variants so as to not overwrite any data internal to
111         # the result itself
112         # this can happen where a lone result is surrounded only by skipped
113         # elements
114         # the result has to convey data about its own limits, plus those of the
115         # context just around it
116         results = @results[0]
117         results.outer_start       = updated_start if results.start != updated_start
118         results.outer_end         = updated_end if results.end != updated_end
119         results.outer_source_text = updated_source_text if results.source_text != updated_source_text
120
121         # the above trick fixes some of the location tracking issues but opens
122         # up another can of worms
123         # uncomment this line to see
124         #return results
125
126         # need some way of handling unwrapped results (raw results, not AST
127         # nodes) as well
128         results.start             = updated_start
129         results.end               = updated_end
130         results.source_text       = updated_source_text
131       else
132         results = @results
133         results.start             = updated_start
134         results.end               = updated_end
135         results.source_text       = updated_source_text
136       end
137       results
138     end
139
140     # Returns the number of results accumulated so far.
141     def length
142       @results.length
143     end
144
145     # TODO: possibly implement "undo/rollback" and "reset" methods
146     # if I implement "undo" will probbaly do it as a stack
147     # will have the option of implementing "redo" as well but I'd only do that if I could think of a use for it
148
149   private
150
151     def update_and_return_remainder_for_string input, store = false
152       previous_line_end   = @options[:line_end]   # remember old end point
153       previous_column_end = @options[:column_end] # remember old end point
154
155       # special case handling for literal String objects
156       if input.instance_of? String
157         input = StringResult.new(input)
158         input.start = [previous_line_end, previous_column_end]
159         if (line_count  = input.scan(/\r\n|\r|\n/).length) != 0       # count number of newlines in receiver
160           column_end    = input.jlength - input.jrindex(/\r|\n/) - 1  # calculate characters on last line
161         else                                                          # no newlines in match
162           column_end    = input.jlength + previous_column_end
163         end
164         input.end   = [previous_line_end + line_count, column_end]
165       end
166
167       @results << input if store
168
169       if input.line_end > previous_line_end       # end line has advanced
170         @options[:line_end]   = input.line_end
171         @options[:column_end] = 0
172       end
173
174       if input.column_end > @options[:column_end] # end column has advanced
175         @options[:column_end] = input.column_end
176       end
177
178       @options[:line_start]   = @options[:line_end]   # new start point is old end point
179       @options[:column_start] = @options[:column_end] # new start point is old end point
180
181       # calculate remainder
182       line_delta              = @options[:line_end] - previous_line_end
183       if line_delta > 0                                                     # have consumed newline(s)
184         line_delta.times do                                                 # remove them from remainder
185           newline_location    = @remainder.jindex_plus_length /\r\n|\r|\n/  # find the location of the next newline
186           @scanned            << @remainder[0...newline_location]           # record scanned text
187           @remainder          = @remainder[newline_location..-1]            # strip everything up to and including the newline
188         end
189         @scanned              << @remainder[0...@options[:column_end]]
190         @remainder            = @remainder[@options[:column_end]..-1] # delete up to the current column
191       else                                                            # no newlines consumed
192         column_delta          = @options[:column_end] - previous_column_end
193         if column_delta > 0                                           # there was movement within currentline
194           @scanned            << @remainder[0...column_delta]
195           @remainder          = @remainder[column_delta..-1]          # delete up to the current column
196         end
197       end
198       @remainder
199     end
200
201     def base_string=(string)
202       @base_string = (string.clone rescue string)
203     end
204   end # class ParserState
205 end # module Walrat