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:
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.
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.
26 # Simple class for maintaining state during a parse operation.
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
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
41 @options = options.clone
43 # start wherever we last finished (doesn't seem to behave different to
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?
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]
57 # The parsed method is used to inform the receiver of a successful parsing
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
64 # As a convenience returns the remainder.
65 # Raises an ArgumentError if substring is nil.
67 raise ArgumentError if substring.nil?
68 update_and_return_remainder_for_string substring, true
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.
77 raise ArgumentError if substring.nil?
78 update_and_return_remainder_for_string substring
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
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
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.
104 updated_start = [@original_line_start, @original_column_start]
105 updated_end = [@options[:line_end], @options[:column_end]]
106 updated_source_text = @scanned.clone
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
112 # this can happen where a lone result is surrounded only by skipped
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
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
126 # need some way of handling unwrapped results (raw results, not AST
128 results.start = updated_start
129 results.end = updated_end
130 results.source_text = updated_source_text
133 results.start = updated_start
134 results.end = updated_end
135 results.source_text = updated_source_text
140 # Returns the number of results accumulated so far.
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
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
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
164 input.end = [previous_line_end + line_count, column_end]
167 @results << input if store
169 if input.line_end > previous_line_end # end line has advanced
170 @options[:line_end] = input.line_end
171 @options[:column_end] = 0
174 if input.column_end > @options[:column_end] # end column has advanced
175 @options[:column_end] = input.column_end
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
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
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
201 def base_string=(string)
202 @base_string = (string.clone rescue string)
204 end # class ParserState