diff --git a/docs/history.rst b/docs/history.rst index b0a0db69..36ae74f7 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -2,7 +2,8 @@ History ======= -0.9.2 *2026-10-02* +0.9.2 *2026-10-04* + * **Bugfix** `wrap()`_ should omit an empty line as output, like `textwrap.wrap()`_ `PR #291`_. * **Bugfix** `clip()`_ when using non-default argument, ``tabsize=0``, `PR #288`_. * **Bugfix** regional-indicator pairs (flags) when corrected for terminals that show lone regional indicators as narrow (xterm, wezterm, VTE, and others) in `wcstwidth()`_ and `width()`_, `PR @@ -316,6 +317,7 @@ https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c:: .. _`PR #281`: https://github.com/jquast/wcwidth/pull/281 .. _`PR #284`: https://github.com/jquast/wcwidth/pull/284 .. _`PR #288`: https://github.com/jquast/wcwidth/pull/288 +.. _`PR #291`: https://github.com/jquast/wcwidth/pull/291 .. _`Issue #101`: https://github.com/jquast/wcwidth/issues/101 .. _`Issue #155`: https://github.com/jquast/wcwidth/issues/155 .. _`Issue #211`: https://github.com/jquast/wcwidth/issues/211 diff --git a/libwcwidth/src/textwrap.c b/libwcwidth/src/textwrap.c index 6f36d107..7cc45bf0 100644 --- a/libwcwidth/src/textwrap.c +++ b/libwcwidth/src/textwrap.c @@ -806,6 +806,16 @@ _wrap_chunks(chunklist_t *chunks, const wcwidth_wrap_opts_t *opts, lines_t *line lc.len = rstrip_len(lc.data, lc.len); if (lc.data != NULL) lc.data[lc.len] = '\0'; + if (lc.len == 0) { + /* + * The line held only whitespace, which dropping has now emptied. Skip + * it and leave `first` unchanged so initial_indent still applies to the + * first line with content, as stdlib textwrap and wcwidth.textwrap do. + */ + sbuf_free(&lc); + cl_free(&cur); + continue; + } } sbuf_t fl; sbuf_init(&fl); diff --git a/libwcwidth/tests/test_textwrap.c b/libwcwidth/tests/test_textwrap.c index d10eed35..b345adeb 100644 --- a/libwcwidth/tests/test_textwrap.c +++ b/libwcwidth/tests/test_textwrap.c @@ -289,6 +289,26 @@ TEST(wrap_max_lines_placeholder) free(out); } +TEST(wrap_drops_leading_whitespace_only_line) +{ + char *out; + size_t len; + wcwidth_wrap_opts_t o = WCWIDTH_WRAP_OPTS_DEFAULT; + const char *exp[] = {"h", "e", "l", "l", "o"}; + const char *exp_ws[] = {" ", "h", "e", "l", "l", "o"}; + + o.width = 1; + ASSERT_EQ(0, wcwidth_wrap_u8(" hello", 6, &o, &out, &len)); + check_lines(out, len, exp, 5); + free(out); + + o.width = 1; + o.drop_whitespace = false; + ASSERT_EQ(0, wcwidth_wrap_u8(" hello", 6, &o, &out, &len)); + check_lines(out, len, exp_ws, 6); + free(out); +} + int main(void) { @@ -301,5 +321,6 @@ main(void) RUN_TEST(wrap_tabsize_zero); RUN_TEST(wrap_lone_continuation_bytes); RUN_TEST(wrap_max_lines_placeholder); + RUN_TEST(wrap_drops_leading_whitespace_only_line); return test_summary(); } diff --git a/tests/test_textwrap.py b/tests/test_textwrap.py index 18cfce56..9836f5a8 100644 --- a/tests/test_textwrap.py +++ b/tests/test_textwrap.py @@ -678,3 +678,10 @@ def test_wrap_unterminated_osc(): def test_wrap_virama_conjunct_known_limitation(text): """Known limitation: wrap() breaks mid-conjunct on Tamil, Kannada and Sinhala text.""" assert [wcwidth.width(line) for line in wrap(text * 4, width=4)] == [3, 4, 3] + + +@pytest.mark.parametrize('text', [' hello', ' hello', ' hello']) +def test_wrap_drops_leading_whitespace_only_lines(text): + """Discard all-whitespace first lines rather than emitting empty lines.""" + assert wrap(text, width=1) == list('hello') + assert wrap(text, width=1, drop_whitespace=False) == list(text) diff --git a/wcwidth/textwrap.py b/wcwidth/textwrap.py index 3d147433..5136d69e 100644 --- a/wcwidth/textwrap.py +++ b/wcwidth/textwrap.py @@ -353,6 +353,8 @@ def _wrap_chunks(self, chunks: list[str]) -> list[str]: # pylint: disable=too-m # (matches CPython #140627 fix behavior) if self.drop_whitespace: line_content = line_content.rstrip() + if not line_content: + continue lines.append(indent + line_content) is_first_line = False else: