| #!/usr/bin/ruby |
| # encoding: utf-8 |
| |
| =begin LICENSE |
| |
| [The "BSD licence"] |
| Copyright (c) 2009-2010 Kyle Yetter |
| All rights reserved. |
| |
| Redistribution and use in source and binary forms, with or without |
| modification, are permitted provided that the following conditions |
| are met: |
| |
| 1. Redistributions of source code must retain the above copyright |
| notice, this list of conditions and the following disclaimer. |
| 2. Redistributions in binary form must reproduce the above copyright |
| notice, this list of conditions and the following disclaimer in the |
| documentation and/or other materials provided with the distribution. |
| 3. The name of the author may not be used to endorse or promote products |
| derived from this software without specific prior written permission. |
| |
| THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
| IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
| IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
| INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| =end |
| |
| module ANTLR3 |
| |
| =begin rdoc ANTLR3::TokenRewriteStream |
| |
| TokenRewriteStream is a specialized form of CommonTokenStream that provides simple stream editing functionality. By creating <i>rewrite programs</i>, new text output can be created based upon the tokens in the stream. The basic token stream itself is preserved, and text output is rendered on demand using the #to_s method. |
| |
| =end |
| |
| class TokenRewriteStream < CommonTokenStream |
| |
| unless defined?( RewriteOperation ) |
| RewriteOperation = Struct.new( :stream, :location, :text ) |
| end |
| |
| =begin rdoc ANTLR3::TokenRewriteStream::RewriteOperation |
| |
| RewiteOperation objects represent some particular editing command that should |
| be executed by a token rewrite stream at some time in future when the stream is |
| rendering a rewritten stream. |
| |
| To perform token stream rewrites safely and efficiently, the rewrites are |
| executed lazily (that is, only when the rewritten text is explicitly requested). |
| Rewrite streams implement lazy rewriting by storing the parameters of |
| edit-inducing methods like +delete+ and +insert+ as RewriteOperation objects in |
| a rewrite program list. |
| |
| The three subclasses of RewriteOperation, InsertBefore, Delete, and Replace, |
| define specific implementations of stream edits. |
| |
| =end |
| |
| class RewriteOperation |
| extend ClassMacros |
| @operation_name = '' |
| |
| class << self |
| ## |
| # the printable name of operations represented by the class -- used for inspection |
| attr_reader :operation_name |
| end |
| |
| ## |
| # :method: execute( buffer ) |
| # run the rewrite operation represented by this object and append the output to +buffer+ |
| abstract :execute |
| |
| ## |
| # return the name of this operation as set by its class |
| def name |
| self.class.operation_name |
| end |
| |
| ## |
| # return a compact, readable representation of this operation |
| def inspect |
| return "(%s @ %p : %p)" % [ name, location, text ] |
| end |
| end |
| |
| |
| =begin rdoc ANTLR3::TokenRewriteStream::InsertBefore |
| |
| Represents rewrite operation: |
| |
| add string <tt>op.text</tt> to the rewrite output immediately before adding the |
| text content of the token at index <tt>op.index</tt> |
| |
| =end |
| |
| class InsertBefore < RewriteOperation |
| @operation_name = 'insert-before'.freeze |
| |
| alias index location |
| alias index= location= |
| |
| def execute( buffer ) |
| buffer << text.to_s |
| token = stream[ location ] |
| buffer << token.text.to_s if token |
| return location + 1 |
| end |
| end |
| |
| =begin rdoc ANTLR3::TokenRewriteStream::Replace |
| |
| Represents rewrite operation: |
| |
| add text <tt>op.text</tt> to the rewrite buffer in lieu of the text of tokens |
| indexed within the range <tt>op.index .. op.last_index</tt> |
| |
| =end |
| |
| class Replace < RewriteOperation |
| |
| @operation_name = 'replace'.freeze |
| |
| def initialize( stream, location, text ) |
| super( stream, nil, text ) |
| self.location = location |
| end |
| |
| def location=( val ) |
| case val |
| when Range then super( val ) |
| else |
| val = val.to_i |
| super( val..val ) |
| end |
| end |
| |
| def execute( buffer ) |
| buffer << text.to_s unless text.nil? |
| return( location.end + 1 ) |
| end |
| |
| def index |
| location.first |
| end |
| |
| end |
| |
| =begin rdoc ANTLR3::TokenRewriteStream::Delete |
| |
| Represents rewrite operation: |
| |
| skip over the tokens indexed within the range <tt>op.index .. op.last_index</tt> |
| and do not add any text to the rewrite buffer |
| |
| =end |
| |
| class Delete < Replace |
| @operation_name = 'delete'.freeze |
| |
| def initialize( stream, location ) |
| super( stream, location, nil ) |
| end |
| end |
| |
| class RewriteProgram |
| def initialize( stream, name = nil ) |
| @stream = stream |
| @name = name |
| @operations = [] |
| end |
| |
| def replace( *range_arguments ) |
| range, text = cast_range( range_arguments, 1 ) |
| |
| op = Replace.new( @stream, range, text ) |
| @operations << op |
| return op |
| end |
| |
| def insert_before( index, text ) |
| index = index.to_i |
| index < 0 and index += @stream.length |
| op = InsertBefore.new( @stream, index, text ) |
| @operations << op |
| return op |
| end |
| |
| def insert_after( index, text ) |
| index = index.to_i |
| index < 0 and index += @stream.length |
| op = InsertBefore.new( @stream, index + 1, text ) |
| @operations << op |
| return op |
| end |
| |
| def delete( *range_arguments ) |
| range, = cast_range( range_arguments ) |
| op = Delete.new( @stream, range ) |
| @operations << op |
| return op |
| end |
| |
| def reduce |
| operations = @operations.reverse |
| reduced = [] |
| |
| until operations.empty? |
| operation = operations.shift |
| location = operation.location |
| |
| case operation |
| when Replace |
| operations.delete_if do |prior_operation| |
| prior_location = prior_operation.location |
| |
| case prior_operation |
| when InsertBefore |
| location.include?( prior_location ) |
| when Replace |
| if location.covers?( prior_location ) |
| true |
| elsif location.overlaps?( prior_location ) |
| conflict!( operation, prior_operation ) |
| end |
| end |
| end |
| when InsertBefore |
| operations.delete_if do |prior_operation| |
| prior_location = prior_operation.location |
| |
| case prior_operation |
| when InsertBefore |
| if prior_location == location |
| operation.text += prior_operation.text |
| true |
| end |
| when Replace |
| if location == prior_location.first |
| prior_operation.text = operation.text << prior_operation.text.to_s |
| operation = nil |
| break( false ) |
| elsif prior_location.include?( location ) |
| conflict!( operation, prior_operation ) |
| end |
| end |
| end |
| end |
| |
| reduced.unshift( operation ) if operation |
| end |
| |
| @operations.replace( reduced ) |
| |
| @operations.inject( {} ) do |map, operation| |
| other_operaiton = map[ operation.index ] and |
| ANTLR3.bug!( Util.tidy( <<-END ) % [ self.class, operation, other_operaiton ] ) |
| | %s#reduce! should have left only one operation per index, |
| | but %p conflicts with %p |
| END |
| map[ operation.index ] = operation |
| map |
| end |
| end |
| |
| def execute( *range_arguments ) |
| if range_arguments.empty? |
| range = 0 ... @stream.length |
| else |
| range, = cast_range( range_arguments ) |
| end |
| |
| output = '' |
| |
| tokens = @stream.tokens |
| |
| operations = reduce |
| |
| cursor = range.first |
| while range.include?( cursor ) |
| if operation = operations.delete( cursor ) |
| cursor = operation.execute( output ) |
| else |
| token = tokens[ cursor ] |
| output << token.text if token |
| cursor += 1 |
| end |
| end |
| if operation = operations.delete( cursor ) and |
| operation.is_a?( InsertBefore ) |
| # catch edge 'insert-after' operations |
| operation.execute( output ) |
| end |
| |
| return output |
| end |
| |
| def clear |
| @operations.clear |
| end |
| |
| def undo( number_of_operations = 1 ) |
| @operations.pop( number_of_operations ) |
| end |
| |
| def conflict!( current, previous ) |
| message = 'operation %p overlaps with previous operation %p' % [ current, previous ] |
| raise( RangeError, message, caller ) |
| end |
| |
| def cast_range( args, extra = 0 ) |
| single, pair = extra + 1, extra + 2 |
| case check_arguments( args, single, pair ) |
| when single |
| loc = args.shift |
| |
| if loc.is_a?( Range ) |
| first, last = loc.first.to_i, loc.last.to_i |
| loc.exclude_end? and last -= 1 |
| return cast_range( args.unshift( first, last ), extra ) |
| else |
| loc = loc.to_i |
| return cast_range( args.unshift( loc, loc ), extra ) |
| end |
| when pair |
| first, last = args.shift( 2 ).map! { |arg| arg.to_i } |
| if first < 0 and last < 0 |
| first += @stream.length |
| last += @stream.length |
| else |
| last < 0 and last += @stream.length |
| first = first.at_least( 0 ) |
| end |
| return( args.unshift( first .. last ) ) |
| end |
| end |
| |
| def check_arguments( args, min, max ) |
| n = args.length |
| if n < min |
| raise ArgumentError, |
| "wrong number of arguments (#{ args.length } for #{ min })", |
| caller |
| elsif n > max |
| raise ArgumentError, |
| "wrong number of arguments (#{ args.length } for #{ max })", |
| caller |
| else return n |
| end |
| end |
| |
| private :conflict!, :cast_range, :check_arguments |
| end |
| |
| attr_reader :programs |
| |
| def initialize( token_source, options = {} ) |
| super( token_source, options ) |
| |
| @programs = Hash.new do |programs, name| |
| if name.is_a?( String ) |
| programs[ name ] = RewriteProgram.new( self, name ) |
| else programs[ name.to_s ] |
| end |
| end |
| |
| @last_rewrite_token_indexes = {} |
| end |
| |
| def rewrite( program_name = 'default', range = nil ) |
| program = @programs[ program_name ] |
| if block_given? |
| yield( program ) |
| program.execute( range ) |
| else program |
| end |
| end |
| |
| def program( name = 'default' ) |
| return @programs[ name ] |
| end |
| |
| def delete_program( name = 'default' ) |
| @programs.delete( name ) |
| end |
| |
| def original_string( start = 0, finish = size - 1 ) |
| @position == -1 and fill_buffer |
| |
| return( self[ start..finish ].map { |t| t.text }.join( '' ) ) |
| end |
| |
| def insert_before( *args ) |
| @programs[ 'default' ].insert_before( *args ) |
| end |
| |
| def insert_after( *args ) |
| @programs[ 'default' ].insert_after( *args ) |
| end |
| |
| def replace( *args ) |
| @programs[ 'default' ].replace( *args ) |
| end |
| |
| def delete( *args ) |
| @programs[ 'default' ].delete( *args ) |
| end |
| |
| def render( *arguments ) |
| case arguments.first |
| when String, Symbol then name = arguments.shift.to_s |
| else name = 'default' |
| end |
| @programs[ name ].execute( *arguments ) |
| end |
| end |
| end |