From e9bc2b94fe9cd68153f8b5039ef1016c2bf36e97 Mon Sep 17 00:00:00 2001 From: Aleksei Lebedev <1329824+LastDragon-ru@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:57:19 +0400 Subject: [PATCH 1/2] `\LastDragon_ru\LaraASP\Documentator\Editor\Editor::getText()` replace by `\LastDragon_ru\LaraASP\Documentator\Editor\Editor::extract()`. --- packages/documentator/src/Editor/Editor.php | 35 ++--- .../documentator/src/Editor/EditorTest.php | 75 ++++++----- .../documentator/src/Editor/Mutators/Base.php | 48 +++++++ .../src/Editor/Mutators/BaseTest.php | 118 +++++++++++++++++ .../src/Editor/Mutators/Extractor.php | 105 +++++++++++++++ .../src/Editor/Mutators/ExtractorTest.php | 121 ++++++++++++++++++ .../documentator/src/Markdown/Document.php | 4 +- .../src/Markdown/Mutations/Document/Move.php | 4 +- .../Markdown/Mutations/Footnote/Prefix.php | 2 +- .../Markdown/Mutations/Heading/Renumber.php | 2 +- .../src/Processor/Tasks/Preprocess/Task.php | 2 +- 11 files changed, 453 insertions(+), 63 deletions(-) create mode 100644 packages/documentator/src/Editor/Mutators/Base.php create mode 100644 packages/documentator/src/Editor/Mutators/BaseTest.php create mode 100644 packages/documentator/src/Editor/Mutators/Extractor.php create mode 100644 packages/documentator/src/Editor/Mutators/ExtractorTest.php diff --git a/packages/documentator/src/Editor/Editor.php b/packages/documentator/src/Editor/Editor.php index e411f052d..a4cf2d1ed 100644 --- a/packages/documentator/src/Editor/Editor.php +++ b/packages/documentator/src/Editor/Editor.php @@ -2,6 +2,7 @@ namespace LastDragon_ru\LaraASP\Documentator\Editor; +use LastDragon_ru\LaraASP\Documentator\Editor\Mutators\Extractor; use LastDragon_ru\LaraASP\Documentator\Utils\Text; use Override; use Stringable; @@ -25,7 +26,8 @@ /** * @var list */ - protected array $lines; + protected array $lines; + protected Extractor $extractor; /** * @param list|string $content @@ -35,7 +37,8 @@ final public function __construct( protected int $startLine = 0, protected string $endOfLine = "\n", ) { - $this->lines = is_string($content) ? Text::getLines($content) : $content; + $this->lines = is_string($content) ? Text::getLines($content) : $content; + $this->extractor = new Extractor(); } #[Override] @@ -44,29 +47,15 @@ public function __toString(): string { } /** - * @param iterable $location + * @param iterable> $locations + * + * @return new */ - public function getText(iterable $location): ?string { - // Select - $selected = null; - - foreach ($location as $coordinate) { - $number = $coordinate->line - $this->startLine; + public function extract(iterable $locations): static { + $extracted = ($this->extractor)($this->lines, $locations, $this->startLine); + $editor = new static($extracted, $this->startLine, $this->endOfLine); - if (isset($this->lines[$number])) { - $selected[] = mb_substr($this->lines[$number], $coordinate->offset, $coordinate->length); - } else { - $selected = null; - break; - } - } - - if ($selected === null) { - return null; - } - - // Return - return implode($this->endOfLine, $selected); + return $editor; } /** diff --git a/packages/documentator/src/Editor/EditorTest.php b/packages/documentator/src/Editor/EditorTest.php index df5af6a41..1b6cf4433 100644 --- a/packages/documentator/src/Editor/EditorTest.php +++ b/packages/documentator/src/Editor/EditorTest.php @@ -84,6 +84,48 @@ public function getLines(): array { self::assertSame($expected, $actual->getLines()); } + public function testExtract(): void { + $startLine = 2; + $locations = [ + new Location(0 + $startLine, 0 + $startLine, 2, 5), + new Location(0 + $startLine, 0 + $startLine, 14, 5), + new Location(0 + $startLine, 0 + $startLine, 26, 5), + new Location(1 + $startLine, 2 + $startLine, 17, null), + new Location(1 + $startLine, 2 + $startLine, 17, null), // same line -> should be ignored + new Location(4 + $startLine, 4 + $startLine, 2, 7), + new Location(4 + $startLine, 4 + $startLine, 9, 7), + new Location(4 + $startLine, 4 + $startLine, 16, 5), + new Location(5 + $startLine, 5 + $startLine, 16, 5), // no line -> should be ignored + ]; + $lines = [ + '11111 11111 11111 11111 11111 11111', + '22222 22222 22222 22222 22222 22222', + '33333 33333 33333 33333 33333 33333', + '44444 44444 44444 44444 44444 44444', + '55555 55555 55555 55555 55555 55555', + ]; + $expected = [ + '111 1 111 1 111 1', + ' 22222 22222 22222', + '33333 33333 33333 33333 33333 33333', + '555 55555 55555 555', + ]; + $editor = new readonly class($lines, $startLine) extends Editor { + /** + * @return list + */ + public function getLines(): array { + return $this->lines; + } + }; + + $actual = $editor->extract($locations); + + self::assertNotSame($editor, $actual); + self::assertEquals($lines, $editor->getLines()); + self::assertSame($expected, $actual->getLines()); + } + public function testPrepare(): void { $editor = new readonly class(['L1', 'L2']) extends Editor { /** @@ -177,37 +219,4 @@ public function expand(array $changes): array { self::assertEquals($expected, $editor->expand($changes)); } - - public function testGetText(): void { - $editor = new Editor( - [ - 0 => 'a b c d', - 1 => 'e f g h', - 2 => 'i j k l', - 3 => 'm n o p', - 4 => '', - 5 => 'q r s t', - 6 => 'u v w x', - ], - 1, - ); - - self::assertNull($editor->getText(new Location(25, 25, 0))); - self::assertSame('f g', $editor->getText(new Location(2, 2, 2, 3))); - self::assertSame( - <<<'TEXT' - k l - m n o p - - q r s - TEXT, - $editor->getText(new Location(3, 6, 4, 5)), - ); - self::assertSame( - <<<'TEXT' - f g - TEXT, - $editor->getText([new Coordinate(2, 2, 3)]), - ); - } } diff --git a/packages/documentator/src/Editor/Mutators/Base.php b/packages/documentator/src/Editor/Mutators/Base.php new file mode 100644 index 000000000..e3ee96413 --- /dev/null +++ b/packages/documentator/src/Editor/Mutators/Base.php @@ -0,0 +1,48 @@ +line <=> $b->line; + $result = $result === 0 + ? $a->offset <=> $b->offset + : $result; + $result = $result === 0 + ? ($a->length ?? PHP_INT_MAX) <=> ($b->length ?? PHP_INT_MAX) + : $result; + + return $result; + } + + /** + * @phpstan-assert-if-true int $key + * + * @param array> $coordinates + */ + protected function isOverlapped(array $coordinates, Coordinate $coordinate, ?int &$key = null): bool { + $key = null; + + foreach ($coordinates[$coordinate->line] ?? [] as $k => $c) { + $aStart = $c->offset; + $aEnd = $aStart + ($c->length ?? PHP_INT_MAX) - 1; + $bStart = $coordinate->offset; + $bEnd = $bStart + ($coordinate->length ?? PHP_INT_MAX) - 1; + + if (!($bEnd < $aStart || $bStart > $aEnd)) { + $key = $k; + break; + } + } + + return $key !== null; + } +} diff --git a/packages/documentator/src/Editor/Mutators/BaseTest.php b/packages/documentator/src/Editor/Mutators/BaseTest.php new file mode 100644 index 000000000..e6d17fcd2 --- /dev/null +++ b/packages/documentator/src/Editor/Mutators/BaseTest.php @@ -0,0 +1,118 @@ + + // ========================================================================= + public function testCompare(): void { + $base = new readonly class() extends Base { + #[Override] + public function compare(Coordinate $a, Coordinate $b): int { + return parent::compare($a, $b); + } + }; + + self::assertSame(-1, $base->compare(new Coordinate(1, 0, 1), new Coordinate(2, 0, 1))); + self::assertSame(1, $base->compare(new Coordinate(2, 0, 1), new Coordinate(1, 0, 1))); + self::assertSame(0, $base->compare(new Coordinate(1, 0, 1), new Coordinate(1, 0, 1))); + self::assertSame(1, $base->compare(new Coordinate(1, 10, 1), new Coordinate(1, 0, 1))); + self::assertSame(-1, $base->compare(new Coordinate(1, 0, 1), new Coordinate(1, 0, 2))); + } + + /** + * @param array{bool, ?int} $expected + * @param array> $lines + */ + #[DataProvider('dataProviderIsOverlapped')] + public function testIsOverlapped(array $expected, array $lines, Coordinate $coordinate): void { + $base = new readonly class() extends Base { + /** + * @inheritDoc + */ + #[Override] + public function isOverlapped( + array $coordinates, + Coordinate $coordinate, + ?int &$key = null, + ): bool { + return parent::isOverlapped( + $coordinates, + $coordinate, + $key, + ); + } + }; + + self::assertSame($expected[0], $base->isOverlapped($lines, $coordinate, $key)); + self::assertSame($expected[1], $key); + } + // + + // + // ========================================================================= + /** + * @return array>, Coordinate}> + */ + public static function dataProviderIsOverlapped(): array { + return [ + 'nope' => [ + [false, null], + [ + 10 => [ + new Coordinate(10, 10, null), + ], + ], + new Coordinate(11, 10, null), + ], + 'nope (same line)' => [ + [false, null], + [ + 10 => [ + new Coordinate(10, 10, 10), + ], + ], + new Coordinate(10, 20, 10), + ], + 'overlapped' => [ + [true, 0], + [ + 10 => [ + new Coordinate(10, 10, null), + ], + ], + new Coordinate(10, 15, 10), + ], + 'overlapped (reverse)' => [ + [true, 0], + [ + 10 => [ + new Coordinate(10, 15, 10), + ], + ], + new Coordinate(10, 10, null), + ], + 'overlapped (one character)' => [ + [true, 1], + [ + 10 => [ + new Coordinate(10, 0, 5), + new Coordinate(10, 10, 10), + ], + ], + new Coordinate(10, 19, 10), + ], + ]; + } + // +} diff --git a/packages/documentator/src/Editor/Mutators/Extractor.php b/packages/documentator/src/Editor/Mutators/Extractor.php new file mode 100644 index 000000000..6b1c9c98b --- /dev/null +++ b/packages/documentator/src/Editor/Mutators/Extractor.php @@ -0,0 +1,105 @@ + $lines + * @param iterable> $locations + * + * @return list + */ + public function __invoke(array $lines, iterable $locations, int $startLine = 0): array { + $prepared = $this->unpack($locations); + $prepared = $this->prepare($prepared); + $result = []; + + foreach ($prepared as $index => $coordinates) { + // Line exist? + $number = $index - $startLine; + + if (!isset($lines[$number])) { + continue; + } + + // Extract + $line = []; + + foreach ($coordinates as $coordinate) { + $line[] = mb_substr($lines[$number], $coordinate->offset, $coordinate->length); + } + + $result[] = implode(' ', $line); + } + + return $result; + } + + /** + * @param iterable> $locations + * + * @return list + */ + protected function unpack(iterable $locations): array { + $prepared = []; + + foreach ($locations as $location) { + foreach ($location as $coordinate) { + $prepared[] = $coordinate; + } + } + + usort($prepared, $this->compare(...)); + + return $prepared; + } + + /** + * @param list $coordinates + * + * @return array> + */ + protected function prepare(array $coordinates): array { + $lines = []; + + foreach ($coordinates as $coordinate) { + if ($this->isOverlapped($lines, $coordinate, $key)) { + // Coordinates are overlapped -> merge + $overlapped = $lines[$coordinate->line][$key] ?? $coordinate; + $lines[$coordinate->line][$key] = $this->merge($overlapped, $coordinate); + } elseif (isset($lines[$coordinate->line])) { + // Coordinates may touch each other -> merge if yes. + $key = array_key_last($lines[$coordinate->line]); + $previous = $lines[$coordinate->line][$key]; + + if ($previous->length !== null && $previous->offset + $previous->length === $coordinate->offset) { + $lines[$coordinate->line][$key] = $this->merge($previous, $coordinate); + } else { + $lines[$coordinate->line][] = $coordinate; + } + } else { + // Just add + $lines[$coordinate->line][] = $coordinate; + } + } + + return $lines; + } + + private function merge(Coordinate $a, Coordinate $b): Coordinate { + return new Coordinate( + $a->line, + $a->offset, + $b->length !== null && $a->length !== null + ? ($a->length + $b->length) - (($a->offset + $a->length) - $b->offset) + : null, + ); + } +} diff --git a/packages/documentator/src/Editor/Mutators/ExtractorTest.php b/packages/documentator/src/Editor/Mutators/ExtractorTest.php new file mode 100644 index 000000000..2d4e5092f --- /dev/null +++ b/packages/documentator/src/Editor/Mutators/ExtractorTest.php @@ -0,0 +1,121 @@ + should be ignored + new Location(4 + $startLine, 4 + $startLine, 2, 7), + new Location(4 + $startLine, 4 + $startLine, 9, 7), + new Location(4 + $startLine, 4 + $startLine, 16, 5), + new Location(5 + $startLine, 5 + $startLine, 16, 5), // no line -> should be ignored + ]; + $lines = [ + '11111 11111 11111 11111 11111 11111', + '22222 22222 22222 22222 22222 22222', + '33333 33333 33333 33333 33333 33333', + '44444 44444 44444 44444 44444 44444', + '55555 55555 55555 55555 55555 55555', + ]; + $expected = [ + '111 1 111 1 111 1', + ' 22222 22222 22222', + '33333 33333 33333 33333 33333 33333', + '555 55555 55555 555', + ]; + + self::assertEquals($expected, $extractor($lines, $locations, $startLine)); + } + + public function testUnpack(): void { + $extractor = new readonly class() extends Extractor { + /** + * @inheritDoc + */ + #[Override] + public function unpack(iterable $locations): array { + return parent::unpack($locations); + } + }; + $locations = [ + new Location(12, 15, 5, 10), + new Location(10, 10, 15, 10), + new Location(10, 10, 10, null), + ]; + $expected = [ + new Coordinate(10, 10, null, 0), + new Coordinate(10, 15, 10, 0), + new Coordinate(12, 5, null, 0), + new Coordinate(13, 0, null, 0), + new Coordinate(14, 0, null, 0), + new Coordinate(15, 0, 10, 0), + ]; + + self::assertEquals($expected, $extractor->unpack($locations)); + } + + public function testPrepare(): void { + $extractor = new readonly class() extends Extractor { + /** + * @inheritDoc + */ + #[Override] + public function prepare(array $coordinates): array { + return parent::prepare($coordinates); + } + }; + $coordinates = [ + new Coordinate(10, 10, null, 123), + new Coordinate(10, 15, 10, 123), + new Coordinate(12, 0, 4, 123), + new Coordinate(12, 5, null, 123), + new Coordinate(14, 0, null, 123), + new Coordinate(15, 0, 10, 123), + new Coordinate(15, 9, 15, 123), + new Coordinate(16, 10, 10, 123), + new Coordinate(16, 19, 15, 123), + new Coordinate(17, 10, 10, 123), + new Coordinate(17, 20, 15, 123), + ]; + $expected = [ + 10 => [ + new Coordinate(10, 10, null), + ], + 12 => [ + new Coordinate(12, 0, 4, 123), + new Coordinate(12, 5, null, 123), + ], + 14 => [ + new Coordinate(14, 0, null, 123), + ], + 15 => [ + new Coordinate(15, 0, 24), + ], + 16 => [ + new Coordinate(16, 10, 24), + ], + 17 => [ + new Coordinate(17, 10, 25), + ], + ]; + + self::assertEquals($expected, $extractor->prepare($coordinates)); + } +} diff --git a/packages/documentator/src/Markdown/Document.php b/packages/documentator/src/Markdown/Document.php index 487f2be6f..1be37034d 100644 --- a/packages/documentator/src/Markdown/Document.php +++ b/packages/documentator/src/Markdown/Document.php @@ -97,8 +97,8 @@ public function getBody(): ?string { /** * @param iterable $location */ - public function getText(iterable $location): ?string { - return $this->getEditor()->getText($location); + public function getText(iterable $location): string { + return (string) $this->getEditor()->extract([$location]); } public function mutate(Mutation ...$mutations): self { diff --git a/packages/documentator/src/Markdown/Mutations/Document/Move.php b/packages/documentator/src/Markdown/Mutations/Document/Move.php index 5058eb334..99e1a1722 100644 --- a/packages/documentator/src/Markdown/Mutations/Document/Move.php +++ b/packages/documentator/src/Markdown/Mutations/Document/Move.php @@ -76,7 +76,7 @@ public function __invoke(Document $document): iterable { if ($node instanceof Link || $node instanceof Image) { $offset = Offset::get($node); $location = $location->withOffset($offset); - $origin = mb_trim((string) $document->getText($location)); + $origin = mb_trim($document->getText($location)); $titleValue = (string) $node->getTitle(); $titleWrapper = mb_substr(mb_rtrim(mb_substr($origin, 0, -1)), -1, 1); $title = Utils::getLinkTitle($node, $titleValue, $titleWrapper); @@ -85,7 +85,7 @@ public function __invoke(Document $document): iterable { $target = Utils::getLinkTarget($node, $targetValue, $targetWrap); $text = $title !== '' ? "({$target} {$title})" : "({$target})"; } elseif ($node instanceof ReferenceNode) { - $origin = mb_trim((string) $document->getText($location)); + $origin = mb_trim($document->getText($location)); $label = $node->getLabel(); $titleValue = $node->getTitle(); $titleWrapper = mb_substr($origin, -1, 1); diff --git a/packages/documentator/src/Markdown/Mutations/Footnote/Prefix.php b/packages/documentator/src/Markdown/Mutations/Footnote/Prefix.php index 7114a217f..39ef64102 100644 --- a/packages/documentator/src/Markdown/Mutations/Footnote/Prefix.php +++ b/packages/documentator/src/Markdown/Mutations/Footnote/Prefix.php @@ -55,7 +55,7 @@ private function getLabel(Document $document, Footnote|FootnoteRef $footnote): s if ($footnote instanceof FootnoteRef) { $location = LocationData::get($footnote); - $label = mb_substr($document->getText($location) ?? '', 2, -1); + $label = mb_substr($document->getText($location), 2, -1); } return $label; diff --git a/packages/documentator/src/Markdown/Mutations/Heading/Renumber.php b/packages/documentator/src/Markdown/Mutations/Heading/Renumber.php index 9f14f6ed9..8341452b1 100644 --- a/packages/documentator/src/Markdown/Mutations/Heading/Renumber.php +++ b/packages/documentator/src/Markdown/Mutations/Heading/Renumber.php @@ -74,7 +74,7 @@ private function getHeadings(Document $document, int &$highest): array { $location = LocationData::get($node); $line = $document->getText($location); - if ($line === null || !str_starts_with(mb_trim($line), '#')) { + if (!str_starts_with(mb_trim($line), '#')) { continue; } diff --git a/packages/documentator/src/Processor/Tasks/Preprocess/Task.php b/packages/documentator/src/Processor/Tasks/Preprocess/Task.php index 000f23ffc..a926b2991 100644 --- a/packages/documentator/src/Processor/Tasks/Preprocess/Task.php +++ b/packages/documentator/src/Processor/Tasks/Preprocess/Task.php @@ -173,7 +173,7 @@ public function __invoke(File $file): Generator { $location = Location::get($next); } else { $location = Location::get($node); - $instruction = mb_trim((string) $document->getText($location)); + $instruction = mb_trim($document->getText($location)); $text = "{$instruction}\n{$text}"; } From 488645f9d7f95a32b22205fe64aa98ce1cf5c90d Mon Sep 17 00:00:00 2001 From: Aleksei Lebedev <1329824+LastDragon-ru@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:27:33 +0400 Subject: [PATCH 2/2] Methods to mutate `\LastDragon_ru\LaraASP\Documentator\Editor\Editor` moved into new `\LastDragon_ru\LaraASP\Documentator\Editor\Mutators\Mutator` class. --- packages/documentator/src/Editor/Editor.php | 190 +----------------- .../documentator/src/Editor/EditorTest.php | 99 --------- .../src/Editor/Mutators/Mutator.php | 174 ++++++++++++++++ .../src/Editor/Mutators/MutatorTest.php | 172 ++++++++++++++++ 4 files changed, 353 insertions(+), 282 deletions(-) create mode 100644 packages/documentator/src/Editor/Mutators/Mutator.php create mode 100644 packages/documentator/src/Editor/Mutators/MutatorTest.php diff --git a/packages/documentator/src/Editor/Editor.php b/packages/documentator/src/Editor/Editor.php index a4cf2d1ed..b7b2fa99d 100644 --- a/packages/documentator/src/Editor/Editor.php +++ b/packages/documentator/src/Editor/Editor.php @@ -3,30 +3,20 @@ namespace LastDragon_ru\LaraASP\Documentator\Editor; use LastDragon_ru\LaraASP\Documentator\Editor\Mutators\Extractor; +use LastDragon_ru\LaraASP\Documentator\Editor\Mutators\Mutator; use LastDragon_ru\LaraASP\Documentator\Utils\Text; use Override; use Stringable; -use function array_merge; -use function array_push; -use function array_reverse; -use function array_slice; -use function array_splice; -use function array_values; -use function count; use function implode; use function is_string; -use function mb_rtrim; -use function mb_substr; -use function usort; - -use const PHP_INT_MAX; readonly class Editor implements Stringable { /** * @var list */ protected array $lines; + protected Mutator $mutator; protected Extractor $extractor; /** @@ -38,6 +28,7 @@ final public function __construct( protected string $endOfLine = "\n", ) { $this->lines = is_string($content) ? Text::getLines($content) : $content; + $this->mutator = new Mutator(); $this->extractor = new Extractor(); } @@ -59,181 +50,14 @@ public function extract(iterable $locations): static { } /** - * @param iterable, ?string}> $changes + * @param iterable, ?string}> $changes * * @return new */ public function mutate(iterable $changes): static { - // Modify - $lines = $this->lines; - $changes = $this->prepare($changes); - $changes = $this->removeOverlaps($changes); - $changes = $this->expand($changes); - - foreach ($changes as [$coordinate, $text]) { - // Append? - if ($coordinate->line === PHP_INT_MAX) { - array_push($lines, ...$text); - continue; - } - - // Change - $number = $coordinate->line - $this->startLine; - $line = $lines[$number] ?? ''; - $count = count($text); - $prefix = mb_substr($line, 0, $coordinate->offset); - $suffix = $coordinate->length !== null - ? mb_substr($line, $coordinate->offset + $coordinate->length) - : ''; - $padding = mb_substr($line, 0, $coordinate->padding); - - if ($count > 1) { - $insert = []; - - for ($t = 0; $t < $count; $t++) { - $insert[] = match (true) { - $t === 0 => mb_rtrim($prefix.$text[$t]), - $t === $count - 1 => mb_rtrim($padding.$text[$t].$suffix), - default => mb_rtrim($padding.$text[$t]), - }; - } - - array_splice($lines, $number, 1, $insert); - } elseif ($count === 1) { - $lines[$number] = mb_rtrim($prefix.$text[0].$suffix); - } elseif (($prefix !== '' && $prefix !== $padding) || $suffix !== '') { - $lines[$number] = mb_rtrim($prefix.$suffix); - } else { - unset($lines[$number]); - } - } - - // Return - return new static(array_values($lines), $this->startLine, $this->endOfLine); - } - - /** - * @param iterable, ?string}> $changes - * - * @return list, ?string}> - */ - protected function prepare(iterable $changes): array { - $prepared = []; - - foreach ($changes as [$location, $text]) { - $coordinates = []; - - foreach ($location as $coordinate) { - $coordinates[] = $coordinate; - } - - if ($coordinates !== []) { - $prepared[] = [$coordinates, $text]; - } - } - - return array_reverse($prepared); - } - - /** - * @param array, ?string}> $changes - * - * @return list}> - */ - protected function expand(array $changes): array { - $expanded = []; - $append = []; - $sort = static function (Coordinate $a, Coordinate $b): int { - $result = $a->line <=> $b->line; - $result = $result === 0 - ? $a->offset <=> $b->offset - : $result; - - return $result; - }; - - foreach ($changes as [$coordinates, $text]) { - $text = match (true) { - $text === null => [], - $text === '' => [''], - default => Text::getLines($text), - }; + $mutated = ($this->mutator)($this->lines, $changes, $this->startLine); + $editor = new static($mutated, $this->startLine, $this->endOfLine); - usort($coordinates, $sort); - - for ($i = 0, $c = count($coordinates); $i < $c; $i++) { - $line = $i === $c - 1 ? array_slice($text, $i) : (array) ($text[$i] ?? null); - - if ($coordinates[$i]->line === PHP_INT_MAX) { - $append[] = [$coordinates[$i], $line]; - } else { - $expanded[] = [$coordinates[$i], $line]; - } - } - } - - usort($expanded, static fn ($a, $b) => -$sort($a[0], $b[0])); - - return array_merge($expanded, array_reverse($append)); - } - - /** - * @param list, ?string}> $changes - * - * @return array, ?string}> - */ - protected function removeOverlaps(array $changes): array { - $used = []; - - foreach ($changes as $key => [$coordinates]) { - $lines = []; - - foreach ($coordinates as $coordinate) { - $lines[$coordinate->line][] = $coordinate; - - if ($this->isOverlapped($used, $coordinate)) { - $lines = []; - break; - } - } - - if ($lines !== []) { - foreach ($lines as $line => $coords) { - $used[$line] = array_merge($used[$line] ?? [], $coords); - } - } else { - unset($changes[$key]); - } - } - - // Return - return $changes; - } - - /** - * @param array> $coordinates - */ - private function isOverlapped(array $coordinates, Coordinate $coordinate): bool { - // Append? - if ($coordinate->line === PHP_INT_MAX) { - return false; - } - - // Check - $overlapped = false; - - foreach ($coordinates[$coordinate->line] ?? [] as $c) { - $aStart = $c->offset; - $aEnd = $aStart + ($c->length ?? PHP_INT_MAX) - 1; - $bStart = $coordinate->offset; - $bEnd = $bStart + ($coordinate->length ?? PHP_INT_MAX) - 1; - $overlapped = !($bEnd < $aStart || $bStart > $aEnd); - - if ($overlapped) { - break; - } - } - - return $overlapped; + return $editor; } } diff --git a/packages/documentator/src/Editor/EditorTest.php b/packages/documentator/src/Editor/EditorTest.php index 1b6cf4433..0922c1fa3 100644 --- a/packages/documentator/src/Editor/EditorTest.php +++ b/packages/documentator/src/Editor/EditorTest.php @@ -2,15 +2,10 @@ namespace LastDragon_ru\LaraASP\Documentator\Editor; -use LastDragon_ru\LaraASP\Documentator\Editor\Locations\Append; use LastDragon_ru\LaraASP\Documentator\Editor\Locations\Location; use LastDragon_ru\LaraASP\Documentator\Testing\Package\TestCase; -use Override; use PHPUnit\Framework\Attributes\CoversClass; -use function array_values; -use function iterator_to_array; - use const PHP_INT_MAX; /** @@ -125,98 +120,4 @@ public function getLines(): array { self::assertEquals($lines, $editor->getLines()); self::assertSame($expected, $actual->getLines()); } - - public function testPrepare(): void { - $editor = new readonly class(['L1', 'L2']) extends Editor { - /** - * @inheritDoc - */ - #[Override] - public function prepare(iterable $changes): array { - return parent::prepare($changes); - } - }; - $changes = [ - [new Location(10, 10, 15, 10), 'a'], - [new Location(10, 10, 10, null), 'b'], - [new Location(12, 15, 5, 10), 'c'], - ]; - $expected = [ - [iterator_to_array(new Location(12, 15, 5, 10)), 'c'], - [iterator_to_array(new Location(10, 10, 10, null)), 'b'], - [iterator_to_array(new Location(10, 10, 15, 10)), 'a'], - ]; - - self::assertEquals($expected, $editor->prepare($changes)); - } - - public function testRemoveOverlaps(): void { - $editor = new readonly class([]) extends Editor { - /** - * @inheritDoc - */ - #[Override] - public function removeOverlaps(array $changes): array { - return parent::removeOverlaps($changes); - } - }; - $changes = [ - 0 => [array_values(iterator_to_array(new Location(18, 18, 5, 10))), 'a'], - 1 => [array_values(iterator_to_array(new Location(17, 17, 11, 10))), 'b'], - 2 => [array_values(iterator_to_array(new Location(17, 17, 5, 10))), 'c'], - 3 => [array_values(iterator_to_array(new Location(14, 15, 5, 10))), 'd'], - 4 => [array_values(iterator_to_array(new Location(12, 15, 5, 10))), 'e'], - 5 => [array_values(iterator_to_array(new Location(10, 10, 10, null))), 'f'], - 6 => [array_values(iterator_to_array(new Location(10, 10, 15, 10))), 'g'], - 7 => [array_values(iterator_to_array(new Location(9, 9, 39, 11))), 'h'], - 8 => [array_values(iterator_to_array(new Location(9, 9, 50, null))), 'i'], - 9 => [array_values(iterator_to_array(new Location(9, 9, 40, 10))), 'j'], - 10 => [array_values(iterator_to_array(new Location(PHP_INT_MAX, PHP_INT_MAX))), 'k'], - 11 => [array_values(iterator_to_array(new Append())), 'l'], - ]; - $expected = [ - 0 => [iterator_to_array(new Location(18, 18, 5, 10)), 'a'], - 1 => [iterator_to_array(new Location(17, 17, 11, 10)), 'b'], - 3 => [iterator_to_array(new Location(14, 15, 5, 10)), 'd'], - 5 => [iterator_to_array(new Location(10, 10, 10, null)), 'f'], - 7 => [iterator_to_array(new Location(9, 9, 39, 11)), 'h'], - 8 => [iterator_to_array(new Location(9, 9, 50, null)), 'i'], - 10 => [iterator_to_array(new Location(PHP_INT_MAX, PHP_INT_MAX)), 'k'], - 11 => [iterator_to_array(new Append()), 'l'], - ]; - - self::assertEquals($expected, $editor->removeOverlaps($changes)); - } - - public function testExpand(): void { - $editor = new readonly class([]) extends Editor { - /** - * @inheritDoc - */ - #[Override] - public function expand(array $changes): array { - return parent::expand($changes); - } - }; - $changes = [ - [array_values(iterator_to_array(new Location(PHP_INT_MAX, PHP_INT_MAX))), "new line aa\nnew line ab"], - [array_values(iterator_to_array(new Location(6, 6, 5, 10, 2))), "text aa\ntext ab"], - [array_values(iterator_to_array(new Location(4, 5, 5, 5, 1))), "text ba\ntext bb"], - [array_values(iterator_to_array(new Location(2, 3, 5, null))), 'text c'], - [array_values(iterator_to_array(new Location(1, 1, 5, 10))), "text da\ntext db\ntext dc"], - [array_values(iterator_to_array(new Append())), "new line ba\nnew line bb"], - ]; - $expected = [ - [new Coordinate(6, 7, 10, 2), ['text aa', 'text ab']], - [new Coordinate(5, 1, 5, 1), ['text bb']], - [new Coordinate(4, 6, null, 1), ['text ba']], - [new Coordinate(3, 0, null, 0), []], - [new Coordinate(2, 5, null, 0), ['text c']], - [new Coordinate(1, 5, 10, 0), ['text da', 'text db', 'text dc']], - [new Coordinate(PHP_INT_MAX, 0, null, 0), ['new line ba', 'new line bb']], - [new Coordinate(PHP_INT_MAX, 0, null, 0), ['new line aa', 'new line ab']], - ]; - - self::assertEquals($expected, $editor->expand($changes)); - } } diff --git a/packages/documentator/src/Editor/Mutators/Mutator.php b/packages/documentator/src/Editor/Mutators/Mutator.php new file mode 100644 index 000000000..ccc71cd6c --- /dev/null +++ b/packages/documentator/src/Editor/Mutators/Mutator.php @@ -0,0 +1,174 @@ + $lines + * @param iterable, ?string}> $changes + * + * @return list + */ + public function __invoke(array $lines, iterable $changes, int $startLine = 0): array { + // Modify + $changes = $this->unpack($changes); + $changes = $this->cleanup($changes); + $changes = $this->prepare($changes); + + foreach ($changes as [$coordinate, $text]) { + // Append? + if ($coordinate->line === PHP_INT_MAX) { + array_push($lines, ...$text); + continue; + } + + // Change + $number = $coordinate->line - $startLine; + $line = $lines[$number] ?? ''; + $count = count($text); + $prefix = mb_substr($line, 0, $coordinate->offset); + $suffix = $coordinate->length !== null + ? mb_substr($line, $coordinate->offset + $coordinate->length) + : ''; + $padding = mb_substr($line, 0, $coordinate->padding); + + if ($count > 1) { + $insert = []; + + for ($t = 0; $t < $count; $t++) { + $insert[] = match (true) { + $t === 0 => mb_rtrim($prefix.$text[$t]), + $t === $count - 1 => mb_rtrim($padding.$text[$t].$suffix), + default => mb_rtrim($padding.$text[$t]), + }; + } + + array_splice($lines, $number, 1, $insert); + } elseif ($count === 1) { + $lines[$number] = mb_rtrim($prefix.$text[0].$suffix); + } elseif (($prefix !== '' && $prefix !== $padding) || $suffix !== '') { + $lines[$number] = mb_rtrim($prefix.$suffix); + } else { + unset($lines[$number]); + } + } + + return array_values($lines); + } + + /** + * @param iterable, ?string}> $changes + * + * @return list, ?string}> + */ + protected function unpack(iterable $changes): array { + $prepared = []; + + foreach ($changes as [$location, $text]) { + $coordinates = []; + + foreach ($location as $coordinate) { + $coordinates[] = $coordinate; + } + + if ($coordinates !== []) { + $prepared[] = [$coordinates, $text]; + } + } + + return array_reverse($prepared); + } + + /** + * @param array, ?string}> $changes + * + * @return list}> + */ + protected function prepare(array $changes): array { + $expanded = []; + $append = []; + + foreach ($changes as [$coordinates, $text]) { + $text = match (true) { + $text === null => [], + $text === '' => [''], + default => Text::getLines($text), + }; + + usort($coordinates, $this->compare(...)); + + for ($i = 0, $c = count($coordinates); $i < $c; $i++) { + $line = $i === $c - 1 ? array_slice($text, $i) : (array) ($text[$i] ?? null); + + if ($coordinates[$i]->line === PHP_INT_MAX) { + $append[] = [$coordinates[$i], $line]; + } else { + $expanded[] = [$coordinates[$i], $line]; + } + } + } + + usort($expanded, fn ($a, $b) => -$this->compare($a[0], $b[0])); + + return array_merge($expanded, array_reverse($append)); + } + + /** + * @param list, ?string}> $changes + * + * @return array, ?string}> + */ + protected function cleanup(array $changes): array { + $used = []; + + foreach ($changes as $key => [$coordinates]) { + $lines = []; + + foreach ($coordinates as $coordinate) { + $lines[$coordinate->line][] = $coordinate; + + if ($this->isOverlapped($used, $coordinate)) { + $lines = []; + break; + } + } + + if ($lines !== []) { + foreach ($lines as $line => $coords) { + $used[$line] = array_merge($used[$line] ?? [], $coords); + } + } else { + unset($changes[$key]); + } + } + + // Return + return $changes; + } + + /** + * @inheritDoc + */ + #[Override] + protected function isOverlapped(array $coordinates, Coordinate $coordinate, ?int &$key = null): bool { + return $coordinate->line !== PHP_INT_MAX + && parent::isOverlapped($coordinates, $coordinate, $key); + } +} diff --git a/packages/documentator/src/Editor/Mutators/MutatorTest.php b/packages/documentator/src/Editor/Mutators/MutatorTest.php new file mode 100644 index 000000000..a5a044d6c --- /dev/null +++ b/packages/documentator/src/Editor/Mutators/MutatorTest.php @@ -0,0 +1,172 @@ + 'a b c d', + 1 => 'e f g h', + 2 => 'i j k l', + 3 => 'm n o p', + 4 => '', + 5 => 'q r s t', + 6 => 'u v w x', + 7 => '', + 8 => 'y z', + 9 => '', + 10 => '> a b c d', + 11 => '> e f g h', + 12 => '>', + 13 => '> i j k l', + 14 => '>', + 15 => '>', + ]; + $changes = [ + [new Location(1, 1, 2, 3), "123\n345\n567"], + [new Location(2, 4, 4, 4), '123'], + [new Location(6, 8, 4, 4), "123\n345"], + [new Location(11, 12, 4, 3, 2), "123\n345\n567"], + [new Location(12, 12, 5, 2, 2), null], + [new Location(14, 16, 4, 3, 2), '123'], + [new Location(PHP_INT_MAX, PHP_INT_MAX), "added line a\n"], + [new Location(PHP_INT_MAX, PHP_INT_MAX), "added line b\n"], + ]; + $mutator = new Mutator(); + $actual = ($mutator)($lines, $changes, $start); + $expected = [ + 'a 123', + '345', + '567 d', + 'e f 123', + 'o p', + '', + 'q r 123', + '345', + 'y z', + '', + '> a b 123', + '> 345', + '> 567 g', + '>', + '> i j 123', + 'added line a', + '', + 'added line b', + '', + ]; + + self::assertEquals($expected, $actual); + } + + public function testUnpack(): void { + $mutator = new readonly class() extends Mutator { + /** + * @inheritDoc + */ + #[Override] + public function unpack(iterable $changes): array { + return parent::unpack($changes); + } + }; + $changes = [ + [new Location(10, 10, 15, 10), 'a'], + [new Location(10, 10, 10, null), 'b'], + [new Location(12, 15, 5, 10), 'c'], + ]; + $expected = [ + [iterator_to_array(new Location(12, 15, 5, 10)), 'c'], + [iterator_to_array(new Location(10, 10, 10, null)), 'b'], + [iterator_to_array(new Location(10, 10, 15, 10)), 'a'], + ]; + + self::assertEquals($expected, $mutator->unpack($changes)); + } + + public function testCleanup(): void { + $mutator = new readonly class() extends Mutator { + /** + * @inheritDoc + */ + #[Override] + public function cleanup(array $changes): array { + return parent::cleanup($changes); + } + }; + $changes = [ + 0 => [array_values(iterator_to_array(new Location(18, 18, 5, 10))), 'a'], + 1 => [array_values(iterator_to_array(new Location(17, 17, 11, 10))), 'b'], + 2 => [array_values(iterator_to_array(new Location(17, 17, 5, 10))), 'c'], + 3 => [array_values(iterator_to_array(new Location(14, 15, 5, 10))), 'd'], + 4 => [array_values(iterator_to_array(new Location(12, 15, 5, 10))), 'e'], + 5 => [array_values(iterator_to_array(new Location(10, 10, 10, null))), 'f'], + 6 => [array_values(iterator_to_array(new Location(10, 10, 15, 10))), 'g'], + 7 => [array_values(iterator_to_array(new Location(9, 9, 39, 11))), 'h'], + 8 => [array_values(iterator_to_array(new Location(9, 9, 50, null))), 'i'], + 9 => [array_values(iterator_to_array(new Location(9, 9, 40, 10))), 'j'], + 10 => [array_values(iterator_to_array(new Location(PHP_INT_MAX, PHP_INT_MAX))), 'k'], + 11 => [array_values(iterator_to_array(new Append())), 'l'], + ]; + $expected = [ + 0 => [iterator_to_array(new Location(18, 18, 5, 10)), 'a'], + 1 => [iterator_to_array(new Location(17, 17, 11, 10)), 'b'], + 3 => [iterator_to_array(new Location(14, 15, 5, 10)), 'd'], + 5 => [iterator_to_array(new Location(10, 10, 10, null)), 'f'], + 7 => [iterator_to_array(new Location(9, 9, 39, 11)), 'h'], + 8 => [iterator_to_array(new Location(9, 9, 50, null)), 'i'], + 10 => [iterator_to_array(new Location(PHP_INT_MAX, PHP_INT_MAX)), 'k'], + 11 => [iterator_to_array(new Append()), 'l'], + ]; + + self::assertEquals($expected, $mutator->cleanup($changes)); + } + + public function testPrepare(): void { + $mutator = new readonly class() extends Mutator { + /** + * @inheritDoc + */ + #[Override] + public function prepare(array $changes): array { + return parent::prepare($changes); + } + }; + $changes = [ + [array_values(iterator_to_array(new Location(PHP_INT_MAX, PHP_INT_MAX))), "new line aa\nnew line ab"], + [array_values(iterator_to_array(new Location(6, 6, 5, 10, 2))), "text aa\ntext ab"], + [array_values(iterator_to_array(new Location(4, 5, 5, 5, 1))), "text ba\ntext bb"], + [array_values(iterator_to_array(new Location(2, 3, 5, null))), 'text c'], + [array_values(iterator_to_array(new Location(1, 1, 5, 10))), "text da\ntext db\ntext dc"], + [array_values(iterator_to_array(new Append())), "new line ba\nnew line bb"], + ]; + $expected = [ + [new Coordinate(6, 7, 10, 2), ['text aa', 'text ab']], + [new Coordinate(5, 1, 5, 1), ['text bb']], + [new Coordinate(4, 6, null, 1), ['text ba']], + [new Coordinate(3, 0, null, 0), []], + [new Coordinate(2, 5, null, 0), ['text c']], + [new Coordinate(1, 5, 10, 0), ['text da', 'text db', 'text dc']], + [new Coordinate(PHP_INT_MAX, 0, null, 0), ['new line ba', 'new line bb']], + [new Coordinate(PHP_INT_MAX, 0, null, 0), ['new line aa', 'new line ab']], + ]; + + self::assertEquals($expected, $mutator->prepare($changes)); + } +}