From b7023a5ac656c450180679a958f858748bbb0633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E6=A5=9A=E7=AC=91?= Date: Tue, 22 Sep 2026 22:55:52 +0800 Subject: [PATCH] Avoid trailing post-replacement delimiters in modern hard cuts --- slugify/slugify.py | 7 ++++++- tests/test_release.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/slugify/slugify.py b/slugify/slugify.py index 0194378..ebe424a 100644 --- a/slugify/slugify.py +++ b/slugify/slugify.py @@ -92,13 +92,18 @@ def _modern_truncate(text: str, max_length: int, word_boundary: bool, separator: # A hard cut may shorten a word but must not emit a partial/trailing delimiter. parts: list[str] = [] length = 0 + previous_index = 0 for index, word in enumerate(tokens): - delimiter = separator if index else '' + if not word: + continue + # Keep repeated delimiters with the next nonempty word until it fits. + delimiter = separator * (index - previous_index) remaining = max_length - length - len(delimiter) if remaining <= 0: break parts.append(delimiter + word[:remaining]) length += len(delimiter) + min(len(word), remaining) + previous_index = index if len(word) > remaining: break return ''.join(parts) diff --git a/tests/test_release.py b/tests/test_release.py index 85ea4e9..83c11ec 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -53,6 +53,32 @@ def test_modern_fitting_post_replacements_preserve_delimiters(self): separator=separator, max_length=limit, word_boundary=boundary, save_order=order), expected) + def test_modern_hard_cut_does_not_end_in_post_replacement_delimiters(self): + cases = ( + ('one--two', '-', 4, 'one'), + ('one--two', '-', 5, 'one'), + ('one--two', '-', 6, 'one--t'), + ('one--two', '::', 6, 'one'), + ('one--two', '::', 7, 'one'), + ('one--two', '::', 8, 'one::::t'), + ('--one', '-', 2, ''), + ('--one', '-', 3, '--o'), + ('---', '-', 2, ''), + ('x--two', 'x', 3, 'x'), + ('one--two', '', 4, 'onet'), + ) + for replacement, separator, limit, expected in cases: + with self.subTest(replacement=replacement, separator=separator, limit=limit): + self.assertEqual(slugify('value', replacements=[('value', replacement)], + replacement_stage='post', allow_unicode=True, + separator=separator, max_length=limit), expected) + + def test_modern_word_boundary_hard_cut_keeps_delimiters_with_the_next_word(self): + for order in (False, True): + self.assertEqual(slugify('value', replacements=[('value', '--longword')], + replacement_stage='post', allow_unicode=True, + max_length=2, word_boundary=True, save_order=order), '') + def test_cli_preserves_legacy_default_shape(self): expected = dict(text='', entities=True, decimal=True, hexadecimal=True, max_length=0, word_boundary=False, save_order=False, separator='-',