From 37bdc535cb1bb9cbc8898eae3c4e9b9ce447d638 Mon Sep 17 00:00:00 2001 From: Martijn van der Lee Date: Mon, 3 Aug 2026 08:47:06 +0200 Subject: [PATCH 1/8] Add preserve-original patch workflow --- .../apply-preserve-original-text.yml | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 .github/workflows/apply-preserve-original-text.yml diff --git a/.github/workflows/apply-preserve-original-text.yml b/.github/workflows/apply-preserve-original-text.yml new file mode 100644 index 0000000..a51b95a --- /dev/null +++ b/.github/workflows/apply-preserve-original-text.yml @@ -0,0 +1,174 @@ +name: Apply preserve original text fix + +on: + push: + branches: + - agent/preserve-original-text + pull_request: + branches: + - agent/modernize-test-matrix + +permissions: + contents: write + +jobs: + patch: + if: github.event_name == 'push' || github.head_ref == 'agent/preserve-original-text' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: agent/preserve-original-text + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring + coverage: none + - name: Apply source and test changes + shell: python + run: | + from pathlib import Path + + source = Path('src/Sentence.php') + text = source.read_text() + + def replace_once(old, new): + global text + assert text.count(old) == 1, old + text = text.replace(old, new, 1) + + replace_once( + """ const SPLIT_TRIM = 0x1; +""", + """ const SPLIT_TRIM = 0x1; + + /** + * Preserve the original Unicode characters in returned sentences. + */ + const SPLIT_PRESERVE = 0x2; +""" + ) + + replace_once( + """ /** + * Return the sentences detected in the provided text. +""", + """ /** + * Project normalized sentence boundaries back onto the original text. + * + * Quote normalization is character-for-character, so offsets remain stable. + * If another normalization changes the character count, retain the normalized + * output rather than risk returning incorrect slices. + * + * @param string[] $sentences + * @param string $normalized + * @param string $original + * + * @return string[] + */ + private static function preserveOriginalText($sentences, $normalized, $original) + { + if (mb_strlen($normalized) !== mb_strlen($original)) { + return $sentences; + } + + $result = []; + $offset = 0; + + foreach ($sentences as $sentence) { + $position = mb_strpos($normalized, $sentence, $offset); + if ($position === false) { + return $sentences; + } + + $length = mb_strlen($sentence); + $result[] = mb_substr($original, $position, $length); + $offset = $position + $length; + } + + return $result; + } + + /** + * Return the sentences detected in the provided text. +""" + ) + + replace_once( + """ // clean funny quotes + $text = Multibyte::cleanUnicode($text); +""", + """ $originalText = $text; + + // clean funny quotes for boundary detection + $text = Multibyte::cleanUnicode($text); +""" + ) + + replace_once( + """ // Post process + if ($flags & self::SPLIT_TRIM) { +""", + """ // Post process + if ($flags & self::SPLIT_PRESERVE) { + $sentences = self::preserveOriginalText($sentences, $text, $originalText); + } + + if ($flags & self::SPLIT_TRIM) { +""" + ) + + source.write_text(text) + + tests = Path('tests/SentenceTest.php') + test_text = tests.read_text() + marker = """ /** + * @covers ::split + */ + public function testSplitWord() +""" + addition = """ /** + * @covers ::split + */ + public function testPreserveOriginalUnicode() + { + $text = 'Super Tortas is outstanding! I haven’t had as many tortas as I would like.'; + + $this->assertSame( + ['Super Tortas is outstanding!', ' I haven’t had as many tortas as I would like.'], + $this->object->split($text, Sentence::SPLIT_PRESERVE) + ); + $this->assertSame( + ['Super Tortas is outstanding!', 'I haven’t had as many tortas as I would like.'], + $this->object->split($text, Sentence::SPLIT_PRESERVE | Sentence::SPLIT_TRIM) + ); + } + +""" + marker + assert test_text.count(marker) == 1 + tests.write_text(test_text.replace(marker, addition, 1)) + + readme = Path('README.md') + readme_text = readme.read_text() + old = """-\t**`Sentence::SPLIT_TRIM`**: Trim whitespace off the left and right sides of +\teach returned sentence. +""" + new = old + """-\t**`Sentence::SPLIT_PRESERVE`**: Preserve original Unicode quote and +\tapostrophe characters in returned sentences instead of returning their normalized +\tASCII equivalents. It can be combined with `SPLIT_TRIM` using `|`. +""" + assert readme_text.count(old) == 1 + readme.write_text(readme_text.replace(old, new, 1)) + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + - name: Run tests + run: composer test + - name: Remove patch workflow + run: rm .github/workflows/apply-preserve-original-text.yml + - name: Commit patch + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add src/Sentence.php tests/SentenceTest.php README.md .github/workflows/apply-preserve-original-text.yml + git commit -m "Preserve original Unicode text" + git push From 9c75d7765dd83d8153eec602b45ec968ef0e6160 Mon Sep 17 00:00:00 2001 From: Martijn van der Lee Date: Mon, 3 Aug 2026 08:55:52 +0200 Subject: [PATCH 2/8] Run preserve patch from master PR --- .github/workflows/apply-preserve-original-text.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/apply-preserve-original-text.yml b/.github/workflows/apply-preserve-original-text.yml index a51b95a..698bd66 100644 --- a/.github/workflows/apply-preserve-original-text.yml +++ b/.github/workflows/apply-preserve-original-text.yml @@ -1,24 +1,22 @@ name: Apply preserve original text fix on: - push: - branches: - - agent/preserve-original-text pull_request: branches: - - agent/modernize-test-matrix + - master permissions: contents: write jobs: patch: - if: github.event_name == 'push' || github.head_ref == 'agent/preserve-original-text' + if: github.actor != 'github-actions[bot]' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: agent/preserve-original-text + fetch-depth: 0 - uses: shivammathur/setup-php@v2 with: php-version: '8.4' @@ -171,4 +169,4 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add src/Sentence.php tests/SentenceTest.php README.md .github/workflows/apply-preserve-original-text.yml git commit -m "Preserve original Unicode text" - git push + git push origin HEAD:agent/preserve-original-text From 2d5064d3a5cb903af607bda3bc62e75fe7dc885d Mon Sep 17 00:00:00 2001 From: Martijn van der Lee Date: Mon, 3 Aug 2026 08:58:44 +0200 Subject: [PATCH 3/8] Add preserve-original patch script --- tools/apply-preserve-original-text.py | 138 ++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tools/apply-preserve-original-text.py diff --git a/tools/apply-preserve-original-text.py b/tools/apply-preserve-original-text.py new file mode 100644 index 0000000..68d8a8e --- /dev/null +++ b/tools/apply-preserve-original-text.py @@ -0,0 +1,138 @@ +from pathlib import Path + +source = Path('src/Sentence.php') +text = source.read_text(encoding='utf-8') + + +def replace_once(old, new): + global text + if text.count(old) != 1: + raise SystemExit('Expected source anchor exactly once: ' + old[:80]) + text = text.replace(old, new, 1) + + +replace_once( + " const SPLIT_TRIM = 0x1;\n", + """ const SPLIT_TRIM = 0x1; + + /** + * Preserve the original Unicode characters in returned sentences. + */ + const SPLIT_PRESERVE = 0x2; +""" +) + +replace_once( + """ /** + * Return the sentences detected in the provided text. +""", + """ /** + * Project normalized sentence boundaries back onto the original text. + * + * Quote normalization is character-for-character, so offsets remain stable. + * If another normalization changes the character count, retain the normalized + * output rather than risk returning incorrect slices. + * + * @param string[] $sentences + * @param string $normalized + * @param string $original + * + * @return string[] + */ + private static function preserveOriginalText($sentences, $normalized, $original) + { + if (mb_strlen($normalized) !== mb_strlen($original)) { + return $sentences; + } + + $result = []; + $offset = 0; + + foreach ($sentences as $sentence) { + $position = mb_strpos($normalized, $sentence, $offset); + if ($position === false) { + return $sentences; + } + + $length = mb_strlen($sentence); + $result[] = mb_substr($original, $position, $length); + $offset = $position + $length; + } + + return $result; + } + + /** + * Return the sentences detected in the provided text. +""" +) + +replace_once( + """ // clean funny quotes + $text = Multibyte::cleanUnicode($text); +""", + """ $originalText = $text; + + // clean funny quotes for boundary detection + $text = Multibyte::cleanUnicode($text); +""" +) + +replace_once( + """ // Post process + if ($flags & self::SPLIT_TRIM) { +""", + """ // Post process + if ($flags & self::SPLIT_PRESERVE) { + $sentences = self::preserveOriginalText($sentences, $text, $originalText); + } + + if ($flags & self::SPLIT_TRIM) { +""" +) +source.write_text(text, encoding='utf-8') + +tests = Path('tests/SentenceTest.php') +test_text = tests.read_text(encoding='utf-8') +marker = """ /** + * @covers ::split + */ + public function testSplitWord() +""" +addition = """ /** + * @covers ::split + */ + public function testPreserveOriginalUnicode() + { + $text = 'Super Tortas is outstanding! I haven’t had as many tortas as I would like.'; + + $this->assertSame( + ['Super Tortas is outstanding!', ' I haven’t had as many tortas as I would like.'], + $this->object->split($text, Sentence::SPLIT_PRESERVE) + ); + $this->assertSame( + ['Super Tortas is outstanding!', 'I haven’t had as many tortas as I would like.'], + $this->object->split($text, Sentence::SPLIT_PRESERVE | Sentence::SPLIT_TRIM) + ); + } + +""" + marker +if test_text.count(marker) != 1: + raise SystemExit('Expected test insertion marker exactly once') +tests.write_text(test_text.replace(marker, addition, 1), encoding='utf-8') + +readme = Path('README.md') +readme_text = readme.read_text(encoding='utf-8') +old = """-\t**`Sentence::SPLIT_TRIM`**: Trim whitespace off the left and right sides of +\teach returned sentence. +""" +new = old + """-\t**`Sentence::SPLIT_PRESERVE`**: Preserve original Unicode quote and +\tapostrophe characters in returned sentences instead of returning their normalized +\tASCII equivalents. It can be combined with `SPLIT_TRIM` using `|`. +""" +if readme_text.count(old) != 1: + raise SystemExit('Expected README flag description exactly once') +readme.write_text(readme_text.replace(old, new, 1), encoding='utf-8') + +Path('.github/workflows/apply-preserve-original-text.yml').unlink() +Path('tools/apply-preserve-original-text.py').unlink() From 9065bd237191dc94d02ca3e3e27601c41903c0a8 Mon Sep 17 00:00:00 2001 From: Martijn van der Lee Date: Mon, 3 Aug 2026 08:59:03 +0200 Subject: [PATCH 4/8] Use minimal preserve-original patch workflow --- .../apply-preserve-original-text.yml | 144 +----------------- 1 file changed, 7 insertions(+), 137 deletions(-) diff --git a/.github/workflows/apply-preserve-original-text.yml b/.github/workflows/apply-preserve-original-text.yml index 698bd66..eeb5759 100644 --- a/.github/workflows/apply-preserve-original-text.yml +++ b/.github/workflows/apply-preserve-original-text.yml @@ -17,156 +17,26 @@ jobs: with: ref: agent/preserve-original-text fetch-depth: 0 + - uses: shivammathur/setup-php@v2 with: php-version: '8.4' extensions: mbstring coverage: none - - name: Apply source and test changes - shell: python - run: | - from pathlib import Path - - source = Path('src/Sentence.php') - text = source.read_text() - - def replace_once(old, new): - global text - assert text.count(old) == 1, old - text = text.replace(old, new, 1) - - replace_once( - """ const SPLIT_TRIM = 0x1; -""", - """ const SPLIT_TRIM = 0x1; - - /** - * Preserve the original Unicode characters in returned sentences. - */ - const SPLIT_PRESERVE = 0x2; -""" - ) - - replace_once( - """ /** - * Return the sentences detected in the provided text. -""", - """ /** - * Project normalized sentence boundaries back onto the original text. - * - * Quote normalization is character-for-character, so offsets remain stable. - * If another normalization changes the character count, retain the normalized - * output rather than risk returning incorrect slices. - * - * @param string[] $sentences - * @param string $normalized - * @param string $original - * - * @return string[] - */ - private static function preserveOriginalText($sentences, $normalized, $original) - { - if (mb_strlen($normalized) !== mb_strlen($original)) { - return $sentences; - } - - $result = []; - $offset = 0; - - foreach ($sentences as $sentence) { - $position = mb_strpos($normalized, $sentence, $offset); - if ($position === false) { - return $sentences; - } - - $length = mb_strlen($sentence); - $result[] = mb_substr($original, $position, $length); - $offset = $position + $length; - } - - return $result; - } - /** - * Return the sentences detected in the provided text. -""" - ) + - name: Apply exact source patch + run: python3 tools/apply-preserve-original-text.py - replace_once( - """ // clean funny quotes - $text = Multibyte::cleanUnicode($text); -""", - """ $originalText = $text; - - // clean funny quotes for boundary detection - $text = Multibyte::cleanUnicode($text); -""" - ) - - replace_once( - """ // Post process - if ($flags & self::SPLIT_TRIM) { -""", - """ // Post process - if ($flags & self::SPLIT_PRESERVE) { - $sentences = self::preserveOriginalText($sentences, $text, $originalText); - } - - if ($flags & self::SPLIT_TRIM) { -""" - ) - - source.write_text(text) - - tests = Path('tests/SentenceTest.php') - test_text = tests.read_text() - marker = """ /** - * @covers ::split - */ - public function testSplitWord() -""" - addition = """ /** - * @covers ::split - */ - public function testPreserveOriginalUnicode() - { - $text = 'Super Tortas is outstanding! I haven’t had as many tortas as I would like.'; - - $this->assertSame( - ['Super Tortas is outstanding!', ' I haven’t had as many tortas as I would like.'], - $this->object->split($text, Sentence::SPLIT_PRESERVE) - ); - $this->assertSame( - ['Super Tortas is outstanding!', 'I haven’t had as many tortas as I would like.'], - $this->object->split($text, Sentence::SPLIT_PRESERVE | Sentence::SPLIT_TRIM) - ); - } - -""" + marker - assert test_text.count(marker) == 1 - tests.write_text(test_text.replace(marker, addition, 1)) - - readme = Path('README.md') - readme_text = readme.read_text() - old = """-\t**`Sentence::SPLIT_TRIM`**: Trim whitespace off the left and right sides of -\teach returned sentence. -""" - new = old + """-\t**`Sentence::SPLIT_PRESERVE`**: Preserve original Unicode quote and -\tapostrophe characters in returned sentences instead of returning their normalized -\tASCII equivalents. It can be combined with `SPLIT_TRIM` using `|`. -""" - assert readme_text.count(old) == 1 - readme.write_text(readme_text.replace(old, new, 1)) - name: Install dependencies run: composer install --no-interaction --prefer-dist + - name: Run tests run: composer test - - name: Remove patch workflow - run: rm .github/workflows/apply-preserve-original-text.yml - - name: Commit patch + + - name: Commit patched source run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add src/Sentence.php tests/SentenceTest.php README.md .github/workflows/apply-preserve-original-text.yml + git add src/Sentence.php tests/SentenceTest.php README.md .github/workflows/apply-preserve-original-text.yml tools/apply-preserve-original-text.py git commit -m "Preserve original Unicode text" git push origin HEAD:agent/preserve-original-text From b6f8fdc4cf48052ebc12628ea580135bc7e2d5de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 06:59:20 +0000 Subject: [PATCH 5/8] Preserve original Unicode text --- .../apply-preserve-original-text.yml | 42 ------ README.md | 3 + src/Sentence.php | 49 ++++++- tests/SentenceTest.php | 17 +++ tools/apply-preserve-original-text.py | 138 ------------------ 5 files changed, 68 insertions(+), 181 deletions(-) delete mode 100644 .github/workflows/apply-preserve-original-text.yml delete mode 100644 tools/apply-preserve-original-text.py diff --git a/.github/workflows/apply-preserve-original-text.yml b/.github/workflows/apply-preserve-original-text.yml deleted file mode 100644 index eeb5759..0000000 --- a/.github/workflows/apply-preserve-original-text.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Apply preserve original text fix - -on: - pull_request: - branches: - - master - -permissions: - contents: write - -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: agent/preserve-original-text - fetch-depth: 0 - - - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - extensions: mbstring - coverage: none - - - name: Apply exact source patch - run: python3 tools/apply-preserve-original-text.py - - - name: Install dependencies - run: composer install --no-interaction --prefer-dist - - - name: Run tests - run: composer test - - - name: Commit patched source - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add src/Sentence.php tests/SentenceTest.php README.md .github/workflows/apply-preserve-original-text.yml tools/apply-preserve-original-text.py - git commit -m "Preserve original Unicode text" - git push origin HEAD:agent/preserve-original-text diff --git a/README.md b/README.md index 8e563c4..cd4e057 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ Splits the text into sentences. - **`Sentence::SPLIT_TRIM`**: Trim whitespace off the left and right sides of each returned sentence. +- **`Sentence::SPLIT_PRESERVE`**: Preserve original Unicode quote and + apostrophe characters in returned sentences instead of returning their normalized + ASCII equivalents. It can be combined with `SPLIT_TRIM` using `|`. ## Documentation You can find documentation generated from the source code by ApiGen here: [ApiGen documentation](doc/) diff --git a/src/Sentence.php b/src/Sentence.php index 0ac1f4f..8a9e18c 100644 --- a/src/Sentence.php +++ b/src/Sentence.php @@ -22,6 +22,11 @@ class Sentence */ const SPLIT_TRIM = 0x1; + /** + * Preserve the original Unicode characters in returned sentences. + */ + const SPLIT_PRESERVE = 0x2; + /** * List of characters used to terminate sentences. * @@ -396,6 +401,42 @@ private function sentenceMerge($shorts) return $sentences; } + /** + * Project normalized sentence boundaries back onto the original text. + * + * Quote normalization is character-for-character, so offsets remain stable. + * If another normalization changes the character count, retain the normalized + * output rather than risk returning incorrect slices. + * + * @param string[] $sentences + * @param string $normalized + * @param string $original + * + * @return string[] + */ + private static function preserveOriginalText($sentences, $normalized, $original) + { + if (mb_strlen($normalized) !== mb_strlen($original)) { + return $sentences; + } + + $result = []; + $offset = 0; + + foreach ($sentences as $sentence) { + $position = mb_strpos($normalized, $sentence, $offset); + if ($position === false) { + return $sentences; + } + + $length = mb_strlen($sentence); + $result[] = mb_substr($original, $position, $length); + $offset = $position + $length; + } + + return $result; + } + /** * Return the sentences detected in the provided text. * Set the Sentence::SPLIT_TRIM flag to trim whitespace. @@ -420,7 +461,9 @@ public function split($text, $flags = 0, $pipeline = []) ]; } - // clean funny quotes + $originalText = $text; + + // clean funny quotes for boundary detection $text = Multibyte::cleanUnicode($text); // Split @@ -435,6 +478,10 @@ public function split($text, $flags = 0, $pipeline = []) } // Post process + if ($flags & self::SPLIT_PRESERVE) { + $sentences = self::preserveOriginalText($sentences, $text, $originalText); + } + if ($flags & self::SPLIT_TRIM) { return self::trimSentences($sentences); } diff --git a/tests/SentenceTest.php b/tests/SentenceTest.php index 0bbc3b7..d5c9196 100644 --- a/tests/SentenceTest.php +++ b/tests/SentenceTest.php @@ -148,6 +148,23 @@ public function testCleanupUnicode() $this->assertSame(['Fix "these" quotes'], $this->object->split("Fix \xC2\xABthese\xC2\xAB quotes")); } + /** + * @covers ::split + */ + public function testPreserveOriginalUnicode() + { + $text = 'Super Tortas is outstanding! I haven’t had as many tortas as I would like.'; + + $this->assertSame( + ['Super Tortas is outstanding!', ' I haven’t had as many tortas as I would like.'], + $this->object->split($text, Sentence::SPLIT_PRESERVE) + ); + $this->assertSame( + ['Super Tortas is outstanding!', 'I haven’t had as many tortas as I would like.'], + $this->object->split($text, Sentence::SPLIT_PRESERVE | Sentence::SPLIT_TRIM) + ); + } + /** * @covers ::split */ diff --git a/tools/apply-preserve-original-text.py b/tools/apply-preserve-original-text.py deleted file mode 100644 index 68d8a8e..0000000 --- a/tools/apply-preserve-original-text.py +++ /dev/null @@ -1,138 +0,0 @@ -from pathlib import Path - -source = Path('src/Sentence.php') -text = source.read_text(encoding='utf-8') - - -def replace_once(old, new): - global text - if text.count(old) != 1: - raise SystemExit('Expected source anchor exactly once: ' + old[:80]) - text = text.replace(old, new, 1) - - -replace_once( - " const SPLIT_TRIM = 0x1;\n", - """ const SPLIT_TRIM = 0x1; - - /** - * Preserve the original Unicode characters in returned sentences. - */ - const SPLIT_PRESERVE = 0x2; -""" -) - -replace_once( - """ /** - * Return the sentences detected in the provided text. -""", - """ /** - * Project normalized sentence boundaries back onto the original text. - * - * Quote normalization is character-for-character, so offsets remain stable. - * If another normalization changes the character count, retain the normalized - * output rather than risk returning incorrect slices. - * - * @param string[] $sentences - * @param string $normalized - * @param string $original - * - * @return string[] - */ - private static function preserveOriginalText($sentences, $normalized, $original) - { - if (mb_strlen($normalized) !== mb_strlen($original)) { - return $sentences; - } - - $result = []; - $offset = 0; - - foreach ($sentences as $sentence) { - $position = mb_strpos($normalized, $sentence, $offset); - if ($position === false) { - return $sentences; - } - - $length = mb_strlen($sentence); - $result[] = mb_substr($original, $position, $length); - $offset = $position + $length; - } - - return $result; - } - - /** - * Return the sentences detected in the provided text. -""" -) - -replace_once( - """ // clean funny quotes - $text = Multibyte::cleanUnicode($text); -""", - """ $originalText = $text; - - // clean funny quotes for boundary detection - $text = Multibyte::cleanUnicode($text); -""" -) - -replace_once( - """ // Post process - if ($flags & self::SPLIT_TRIM) { -""", - """ // Post process - if ($flags & self::SPLIT_PRESERVE) { - $sentences = self::preserveOriginalText($sentences, $text, $originalText); - } - - if ($flags & self::SPLIT_TRIM) { -""" -) -source.write_text(text, encoding='utf-8') - -tests = Path('tests/SentenceTest.php') -test_text = tests.read_text(encoding='utf-8') -marker = """ /** - * @covers ::split - */ - public function testSplitWord() -""" -addition = """ /** - * @covers ::split - */ - public function testPreserveOriginalUnicode() - { - $text = 'Super Tortas is outstanding! I haven’t had as many tortas as I would like.'; - - $this->assertSame( - ['Super Tortas is outstanding!', ' I haven’t had as many tortas as I would like.'], - $this->object->split($text, Sentence::SPLIT_PRESERVE) - ); - $this->assertSame( - ['Super Tortas is outstanding!', 'I haven’t had as many tortas as I would like.'], - $this->object->split($text, Sentence::SPLIT_PRESERVE | Sentence::SPLIT_TRIM) - ); - } - -""" + marker -if test_text.count(marker) != 1: - raise SystemExit('Expected test insertion marker exactly once') -tests.write_text(test_text.replace(marker, addition, 1), encoding='utf-8') - -readme = Path('README.md') -readme_text = readme.read_text(encoding='utf-8') -old = """-\t**`Sentence::SPLIT_TRIM`**: Trim whitespace off the left and right sides of -\teach returned sentence. -""" -new = old + """-\t**`Sentence::SPLIT_PRESERVE`**: Preserve original Unicode quote and -\tapostrophe characters in returned sentences instead of returning their normalized -\tASCII equivalents. It can be combined with `SPLIT_TRIM` using `|`. -""" -if readme_text.count(old) != 1: - raise SystemExit('Expected README flag description exactly once') -readme.write_text(readme_text.replace(old, new, 1), encoding='utf-8') - -Path('.github/workflows/apply-preserve-original-text.yml').unlink() -Path('tools/apply-preserve-original-text.py').unlink() From 63accdfda5bbcb8e8386f9a45ea0d9e1c1c472ed Mon Sep 17 00:00:00 2001 From: Martijn van der Lee Date: Mon, 3 Aug 2026 09:10:50 +0200 Subject: [PATCH 6/8] Add explicit UTF-8 offset patch --- tools/fix-preserve-utf8-offsets.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tools/fix-preserve-utf8-offsets.py diff --git a/tools/fix-preserve-utf8-offsets.py b/tools/fix-preserve-utf8-offsets.py new file mode 100644 index 0000000..33725b9 --- /dev/null +++ b/tools/fix-preserve-utf8-offsets.py @@ -0,0 +1,17 @@ +from pathlib import Path + +source = Path('src/Sentence.php') +text = source.read_text(encoding='utf-8') +replacements = { + "mb_strlen($normalized) !== mb_strlen($original)": "mb_strlen($normalized, 'UTF-8') !== mb_strlen($original, 'UTF-8')", + "mb_strpos($normalized, $sentence, $offset)": "mb_strpos($normalized, $sentence, $offset, 'UTF-8')", + "mb_strlen($sentence);": "mb_strlen($sentence, 'UTF-8');", + "mb_substr($original, $position, $length);": "mb_substr($original, $position, $length, 'UTF-8');", +} +for old, new in replacements.items(): + if text.count(old) != 1: + raise SystemExit('Expected offset expression exactly once: ' + old) + text = text.replace(old, new, 1) +source.write_text(text, encoding='utf-8') +Path('.github/workflows/apply-preserve-utf8-offsets.yml').unlink() +Path('tools/fix-preserve-utf8-offsets.py').unlink() From 2b6b096d097c9741c0643998556f2d75e3e7c53b Mon Sep 17 00:00:00 2001 From: Martijn van der Lee Date: Mon, 3 Aug 2026 09:11:05 +0200 Subject: [PATCH 7/8] Validate preservation on PHP 5.4 --- .../workflows/apply-preserve-utf8-offsets.yml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/apply-preserve-utf8-offsets.yml diff --git a/.github/workflows/apply-preserve-utf8-offsets.yml b/.github/workflows/apply-preserve-utf8-offsets.yml new file mode 100644 index 0000000..8499fcb --- /dev/null +++ b/.github/workflows/apply-preserve-utf8-offsets.yml @@ -0,0 +1,43 @@ +name: Apply explicit UTF-8 offsets + +on: + pull_request: + branches: + - master + +permissions: + contents: write + +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: agent/preserve-original-text + fetch-depth: 0 + + - uses: shivammathur/setup-php@v2 + with: + php-version: '5.4' + extensions: mbstring, ctype + tools: composer:2.2 + coverage: none + + - name: Apply UTF-8 offset patch + run: python3 tools/fix-preserve-utf8-offsets.py + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run tests + run: composer test + + - name: Commit patch + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add src/Sentence.php .github/workflows/apply-preserve-utf8-offsets.yml tools/fix-preserve-utf8-offsets.py + git commit -m "Use explicit UTF-8 sentence offsets" + git push origin HEAD:agent/preserve-original-text From 75f460949972ce5a470c78e79769a48814bfc584 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 07:12:18 +0000 Subject: [PATCH 8/8] Use explicit UTF-8 sentence offsets --- .../workflows/apply-preserve-utf8-offsets.yml | 43 ------------------- src/Sentence.php | 8 ++-- tools/fix-preserve-utf8-offsets.py | 17 -------- 3 files changed, 4 insertions(+), 64 deletions(-) delete mode 100644 .github/workflows/apply-preserve-utf8-offsets.yml delete mode 100644 tools/fix-preserve-utf8-offsets.py diff --git a/.github/workflows/apply-preserve-utf8-offsets.yml b/.github/workflows/apply-preserve-utf8-offsets.yml deleted file mode 100644 index 8499fcb..0000000 --- a/.github/workflows/apply-preserve-utf8-offsets.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Apply explicit UTF-8 offsets - -on: - pull_request: - branches: - - master - -permissions: - contents: write - -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: agent/preserve-original-text - fetch-depth: 0 - - - uses: shivammathur/setup-php@v2 - with: - php-version: '5.4' - extensions: mbstring, ctype - tools: composer:2.2 - coverage: none - - - name: Apply UTF-8 offset patch - run: python3 tools/fix-preserve-utf8-offsets.py - - - name: Install dependencies - run: composer install --no-interaction --prefer-dist - - - name: Run tests - run: composer test - - - name: Commit patch - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add src/Sentence.php .github/workflows/apply-preserve-utf8-offsets.yml tools/fix-preserve-utf8-offsets.py - git commit -m "Use explicit UTF-8 sentence offsets" - git push origin HEAD:agent/preserve-original-text diff --git a/src/Sentence.php b/src/Sentence.php index 8a9e18c..0821fce 100644 --- a/src/Sentence.php +++ b/src/Sentence.php @@ -416,7 +416,7 @@ private function sentenceMerge($shorts) */ private static function preserveOriginalText($sentences, $normalized, $original) { - if (mb_strlen($normalized) !== mb_strlen($original)) { + if (mb_strlen($normalized, 'UTF-8') !== mb_strlen($original, 'UTF-8')) { return $sentences; } @@ -424,13 +424,13 @@ private static function preserveOriginalText($sentences, $normalized, $original) $offset = 0; foreach ($sentences as $sentence) { - $position = mb_strpos($normalized, $sentence, $offset); + $position = mb_strpos($normalized, $sentence, $offset, 'UTF-8'); if ($position === false) { return $sentences; } - $length = mb_strlen($sentence); - $result[] = mb_substr($original, $position, $length); + $length = mb_strlen($sentence, 'UTF-8'); + $result[] = mb_substr($original, $position, $length, 'UTF-8'); $offset = $position + $length; } diff --git a/tools/fix-preserve-utf8-offsets.py b/tools/fix-preserve-utf8-offsets.py deleted file mode 100644 index 33725b9..0000000 --- a/tools/fix-preserve-utf8-offsets.py +++ /dev/null @@ -1,17 +0,0 @@ -from pathlib import Path - -source = Path('src/Sentence.php') -text = source.read_text(encoding='utf-8') -replacements = { - "mb_strlen($normalized) !== mb_strlen($original)": "mb_strlen($normalized, 'UTF-8') !== mb_strlen($original, 'UTF-8')", - "mb_strpos($normalized, $sentence, $offset)": "mb_strpos($normalized, $sentence, $offset, 'UTF-8')", - "mb_strlen($sentence);": "mb_strlen($sentence, 'UTF-8');", - "mb_substr($original, $position, $length);": "mb_substr($original, $position, $length, 'UTF-8');", -} -for old, new in replacements.items(): - if text.count(old) != 1: - raise SystemExit('Expected offset expression exactly once: ' + old) - text = text.replace(old, new, 1) -source.write_text(text, encoding='utf-8') -Path('.github/workflows/apply-preserve-utf8-offsets.yml').unlink() -Path('tools/fix-preserve-utf8-offsets.py').unlink()