diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b231fa..0e4415e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,16 @@ rescue Paco::ParseError => e end ``` +- `Paco::Combinators.index` method. ([@skryukov]) + +Call `Paco::Combinators.index` to get `Paco::Index` representing the current offset into the parse without consuming the input. +`Paco::Index` has a 0-based character offset attribute `:pos` and 1-based `:line` and `:column` attributes. + +```ruby +index.parse("Paco") #=> # +``` + + ### Changed - `Paco::Combinators.seq_map` merged into `Paco::Combinators.seq`. ([@skryukov]) diff --git a/bin/console b/bin/console index f7d0145..2900b30 100755 --- a/bin/console +++ b/bin/console @@ -4,12 +4,7 @@ require "bundler/setup" require "paco" -# You can add fixtures and/or initialization code here to make experimenting -# with your gem easier. You can also use a different console, if you like. - -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start +include Paco require "irb" IRB.start(__FILE__) diff --git a/docs/paco.md b/docs/paco.md index 49c4c21..abb1e87 100644 --- a/docs/paco.md +++ b/docs/paco.md @@ -234,6 +234,19 @@ example = lazy { failed("always fails") } example.parse("Paco") #=> Paco::ParseError ``` +### Paco::Combinators.index + +Returns parser that returns `Paco::Index` representing the current offset into the parse without consuming the input. +`Paco::Index` has a 0-based character offset attribute `:pos` and 1-based `:line` and `:column` attributes. + +```ruby +include Paco + +example = seq(one_of("123\n ").many.join, index, remainder) + +example.parse("1\n2\n3\n\n Paco") #=> ["1\n2\n3\n\n ", #, "Paco"] +``` + ## Paco::Combinators: Text related methods ### Paco::Combinators.string(matcher) diff --git a/lib/paco/combinators.rb b/lib/paco/combinators.rb index 2bc8da5..6703478 100644 --- a/lib/paco/combinators.rb +++ b/lib/paco/combinators.rb @@ -153,6 +153,13 @@ def optional(parser) alt(parser, succeed(nil)) end + # Returns parser that returns `Paco::Index` representing + # the current offset into the parse without consuming the input. + # @return [Paco::Parser] + def index + Parser.new { |ctx| ctx.index } + end + # Helper used for memoization def memoize(&block) Memoizer.memoize(block.source_location, &block) diff --git a/lib/paco/context.rb b/lib/paco/context.rb index 86c58b2..59f429e 100644 --- a/lib/paco/context.rb +++ b/lib/paco/context.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "paco/callstack" +require "paco/index" module Paco class Context @@ -25,15 +26,10 @@ def eof? pos >= input.length end + # @param [Integer] from + # @return [Paco::Index] def index(from = nil) - from ||= pos - lines = input[0..from].lines - - { - line: lines.length, - column: lines[-1]&.length || 0, - pos: from - } + Index.calculate(input: input, pos: from || pos) end # @param [Paco::Parser] parser diff --git a/lib/paco/index.rb b/lib/paco/index.rb new file mode 100644 index 0000000..93555dd --- /dev/null +++ b/lib/paco/index.rb @@ -0,0 +1,15 @@ +module Paco + Index = Struct.new(:pos, :line, :column) do + # @param [String] input + # @param [Integer] pos + def self.calculate(input:, pos:) + raise ArgumentError, "`pos` must be a non-negative integer" if pos < 0 + raise ArgumentError, "`pos` is grater then input length" if pos > input.length + + lines = input[0..pos].lines + line = lines.empty? ? 1 : lines.length + column = lines.last&.length || 1 + new(pos, line, column) + end + end +end diff --git a/lib/paco/parse_error.rb b/lib/paco/parse_error.rb index c0f42c4..8f12baa 100644 --- a/lib/paco/parse_error.rb +++ b/lib/paco/parse_error.rb @@ -20,7 +20,7 @@ def message index = ctx.index(pos) <<~MSG \nParsing error - line #{index[:line]}, column #{index[:column]}: + line #{index.line}, column #{index.column}: unexpected #{ctx.eof? ? "end of file" : ctx.input[pos].inspect} expecting #{expected} MSG diff --git a/spec/paco/combinators_spec.rb b/spec/paco/combinators_spec.rb index 07a242e..3c4f4bc 100644 --- a/spec/paco/combinators_spec.rb +++ b/spec/paco/combinators_spec.rb @@ -187,4 +187,10 @@ expect { example.parse("Paco") }.to raise_error(Paco::ParseError) end end + + describe "#index" do + it "returns index" do + expect(index.parse("")).to eq(Paco::Index.new(0, 1, 1)) + end + end end diff --git a/spec/paco/index_spec.rb b/spec/paco/index_spec.rb new file mode 100644 index 0000000..0a07f63 --- /dev/null +++ b/spec/paco/index_spec.rb @@ -0,0 +1,26 @@ +require "spec_helper" + +RSpec.describe Paco::Index do + subject(:index) { described_class } + + let(:input) { "1\n2\n\n3\nPaco" } + + it "returns index", :aggregate_failures do + expect(index.calculate(input: input, pos: 0)).to eq(Paco::Index.new(0, 1, 1)) + expect(index.calculate(input: input, pos: 1)).to eq(Paco::Index.new(1, 1, 2)) + expect(index.calculate(input: input, pos: 2)).to eq(Paco::Index.new(2, 2, 1)) + expect(index.calculate(input: input, pos: 10)).to eq(Paco::Index.new(10, 5, 4)) + end + + it "returns start position when empty string" do + expect(index.calculate(input: "", pos: 0)).to eq(Paco::Index.new(0, 1, 1)) + end + + it "raises an error when pos < 0" do + expect { index.calculate(input: input, pos: -1) }.to raise_error(ArgumentError) + end + + it "raises an error when pos > input length" do + expect { index.calculate(input: input, pos: 1000) }.to raise_error(ArgumentError) + end +end