From 454b6e7bdd2b1a83625b723d3a5ba1a28e08f66a Mon Sep 17 00:00:00 2001 From: John Koster Date: Fri, 4 Sep 2026 10:50:40 -0500 Subject: [PATCH] Fix Antlers line and column numbers after multibyte text or a leading newline --- .../Language/Parser/DocumentParser.php | 23 +++++-------- tests/Antlers/Parser/BasicNodeTest.php | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/View/Antlers/Language/Parser/DocumentParser.php b/src/View/Antlers/Language/Parser/DocumentParser.php index ee5f47b2adc..f6d0bd154ac 100644 --- a/src/View/Antlers/Language/Parser/DocumentParser.php +++ b/src/View/Antlers/Language/Parser/DocumentParser.php @@ -328,24 +328,19 @@ private function processInputText($input) $this->inputLen = mb_strlen($this->content); // The document content was normalized, so we can search for "\n". - preg_match_all('/\n/', $this->content, $documentNewLines, PREG_OFFSET_CAPTURE); - $newLineCountLen = count($documentNewLines[0]); - $currentLine = $this->seedStartLine; - $lastOffset = null; - for ($i = 0; $i < $newLineCountLen; $i++) { - $thisNewLine = $documentNewLines[0][$i]; - $thisIndex = $thisNewLine[1]; - $indexChar = $thisIndex; - - if ($lastOffset != null) { - $indexChar = $thisIndex - $lastOffset; - } else { - $indexChar = $indexChar + 1; + $lastOffset = -1; + $thisIndex = -1; + + foreach (explode("\n", $this->content) as $line) { + $thisIndex += mb_strlen($line) + 1; + + if ($thisIndex >= $this->inputLen) { + break; } $this->documentOffsets[$thisIndex] = [ - self::K_CHAR => $indexChar, + self::K_CHAR => $thisIndex - $lastOffset, self::K_LINE => $currentLine, ]; diff --git a/tests/Antlers/Parser/BasicNodeTest.php b/tests/Antlers/Parser/BasicNodeTest.php index 8e89e89fa83..74c3bcf53d7 100644 --- a/tests/Antlers/Parser/BasicNodeTest.php +++ b/tests/Antlers/Parser/BasicNodeTest.php @@ -31,6 +31,39 @@ public function test_it_returns_nodes() $this->assertInstanceOf(LiteralNode::class, $nodes[3]); } + public function test_multibyte_literals_keep_character_based_region_boundaries_and_lines() + { + $nodes = $this->parseNodes("caf\u{00E9}\n\u{65E5}\u{672C} {{ first }}\n\u{1F389} {{ second }} tail"); + + $this->assertSame("caf\u{00E9}\n\u{65E5}\u{672C} ", $nodes[0]->content); + $this->assertSame(' first ', $nodes[1]->content); + $this->assertSame("\n\u{1F389} ", $nodes[2]->content); + $this->assertSame(' second ', $nodes[3]->content); + $this->assertSame(' tail', $nodes[4]->content); + + $this->assertSame(8, $nodes[1]->startPosition->offset); + $this->assertSame(2, $nodes[1]->startPosition->line); + $this->assertSame(4, $nodes[1]->startPosition->char); + $this->assertSame(22, $nodes[3]->startPosition->offset); + $this->assertSame(3, $nodes[3]->startPosition->line); + $this->assertSame(3, $nodes[3]->startPosition->char); + } + + public function test_columns_are_correct_after_a_document_initial_newline() + { + foreach (["\n", "\r", "\r\n"] as $newline) { + $nodes = $this->parseNodes($newline."\u{65E5}\u{672C}{{ first }}".$newline.'ab{{ second }}'); + + $this->assertSame(3, $nodes[1]->startPosition->offset); + $this->assertSame(2, $nodes[1]->startPosition->line); + $this->assertSame(3, $nodes[1]->startPosition->char); + + $this->assertSame(17, $nodes[3]->startPosition->offset); + $this->assertSame(3, $nodes[3]->startPosition->line); + $this->assertSame(3, $nodes[3]->startPosition->char); + } + } + public function test_it_doesnt_trim_off_content_start() { $nodes = $this->parseNodes('{{ meta_title ?? title ?? "No Title Set" }}');