Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions spec/Lexer/CursorSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,21 @@ public function it_returns_the_remaining_text_after_advancing_over_multibyte_cha

$this->isEndOfText()->shouldReturn(true);
}

public function it_can_start_at_a_given_line_and_column(): void
{
$this->beConstructedWith("ab\ncd", 10, 5);

$this->getLine()->shouldReturn(10);
$this->getColumn()->shouldReturn(5);

$this->advance(1);

$this->getColumn()->shouldReturn(6);

$this->advance(3);

$this->getLine()->shouldReturn(11);
$this->getColumn()->shouldReturn(2);
}
}
33 changes: 33 additions & 0 deletions spec/Lexer/LexerSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use chrisjenkinson\StructuredDocumentParser\State\StateInterface;
use chrisjenkinson\StructuredDocumentParser\Token\TokenPosition;
use chrisjenkinson\StructuredDocumentParser\Token\TokenStream;
use PhpSpec\Exception\Example\FailureException;
use PhpSpec\ObjectBehavior;

class LexerSpec extends ObjectBehavior
Expand Down Expand Up @@ -163,6 +164,38 @@ public function it_gives_each_token_its_position(): void
$tokens->consumeToken()->getPosition()->shouldBeLike(new TokenPosition(2, 1));
}

public function it_can_start_from_a_given_position(): void
{
$state = new InitialState();
$state->registerMatcher(new LexerSpecRegexMatcher('Line', '/(?<all>[^\n]*\n|[^\n]+)/A'));

$this->beConstructedWith($state);

$tokens = $this->tokenise("ab\ncd", new TokenPosition(10, 5));

$tokens->consumeToken()->getPosition()->shouldBeLike(new TokenPosition(10, 5));
$tokens->consumeToken()->getPosition()->shouldBeLike(new TokenPosition(11, 1));
}

public function it_reports_errors_relative_to_the_given_position(): void
{
$state = new InitialState();

$this->beConstructedWith($state);

try {
$this->getWrappedObject()->tokenise('ab', new TokenPosition(10, 5));
} catch (NoTokenFoundException $exception) {
if (10 !== $exception->getPosition()->getLine() || 5 !== $exception->getPosition()->getColumn()) {
throw new FailureException('Expected the error at line 10, column 5.');
}

return;
}

throw new FailureException('Expected a NoTokenFoundException.');
}

public function it_switches_state_on_a_zero_length_lookahead_match(): void
{
$origState = new InitialState();
Expand Down
14 changes: 8 additions & 6 deletions src/Lexer/Cursor.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ class Cursor

private readonly int $textLength;

private int $line = 1;

private int $column = 1;

private int $byteOffset = 0;

public function __construct(private readonly string $text)
{
/**
* $line and $column give the position of the start of $text, for text taken from a larger document.
*/
public function __construct(
private readonly string $text,
private int $line = 1,
private int $column = 1,
) {
$this->textLength = mb_strlen($text);
}

Expand Down
13 changes: 9 additions & 4 deletions src/Lexer/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace chrisjenkinson\StructuredDocumentParser\Lexer;

use chrisjenkinson\StructuredDocumentParser\State\StateInterface;
use chrisjenkinson\StructuredDocumentParser\Token\TokenPosition;
use chrisjenkinson\StructuredDocumentParser\Token\TokenStream;

class Lexer
Expand All @@ -21,12 +22,16 @@ public function __construct(private readonly StateInterface $initialState)
$this->state = $initialState;
}

public function tokenise(string $text): TokenStream
/**
* Pass $start when $text comes from a larger document, such as a paragraph lexed
* again for inline syntax, so token positions refer to that document.
*/
public function tokenise(string $text, ?TokenPosition $start = null): TokenStream
{
$this->reset();

try {
return $this->tokeniseFromInitialState($text);
return $this->tokeniseFromInitialState($text, $start ?? new TokenPosition(1, 1));
} finally {
$this->reset();
}
Expand Down Expand Up @@ -73,10 +78,10 @@ public function popState(): void
array_pop($this->previousStates);
}

private function tokeniseFromInitialState(string $text): TokenStream
private function tokeniseFromInitialState(string $text, TokenPosition $start): TokenStream
{
$tokens = new TokenStream();
$cursor = new Cursor($text);
$cursor = new Cursor($text, $start->getLine(), $start->getColumn());

$statesAtPosition = [];

Expand Down
Loading