Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ History
* **Bugfix** libwcwidth no longer has undefined behavior on NULL or empty input in the UTF-8 and
alignment functions, `PR #293`_, `PR #294`_.
* **Changed** `HyperlinkParams.parse()`_ now rejects trailing text, `PR #296`_.
* **Bugfix** `wrap()`_ should keep a link's own OSC 8 params across a line break, `PR #299`_.

0.9.2 *2026-10-04*
* **Bugfix** `wrap()`_ should omit an empty line as output, like `textwrap.wrap()`_ `PR #291`_.
Expand Down Expand Up @@ -326,6 +327,7 @@ https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c::
.. _`PR #293`: https://github.com/jquast/wcwidth/pull/293
.. _`PR #294`: https://github.com/jquast/wcwidth/pull/294
.. _`PR #296`: https://github.com/jquast/wcwidth/pull/296
.. _`PR #299`: https://github.com/jquast/wcwidth/pull/299
.. _`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
Expand Down
11 changes: 11 additions & 0 deletions tests/test_textwrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,17 @@ def test_wrap_tabsize_wide_chars(text, w, tabsize, expected):
'\x1b]8;id=00000002;http://example.com\x07gh\x1b]8;;\x07',
],
),
( # a link opened after a preceding link
'\x1b]8;id=first;https://a\x07aa bb\x1b]8;;\x07 '
'\x1b]8;id=second;https://b\x07cc dd ee\x1b]8;;\x07',
5,
[
'\x1b]8;id=first;https://a\x07aa bb\x1b]8;;\x07',
('\x1b]8;id=first;https://a\x07\x1b]8;;\x07' +
'\x1b]8;id=second;https://b\x07cc dd\x1b]8;;\x07'),
'\x1b]8;id=second;https://b\x07ee\x1b]8;;\x07',
],
),
( # long word inside hyperlink forces character-level breaking
f'{OSC_START_ST}abcdefgh{OSC_END_ST}',
3,
Expand Down
30 changes: 13 additions & 17 deletions wcwidth/textwrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,6 @@ def _wrap_chunks(self, chunks: list[str]) -> list[str]: # pylint: disable=too-m
is_first_line = True

hyperlink_state: Optional[HyperlinkParams] = None
# Track the id we're using for the current hyperlink continuation
current_hyperlink_id: Optional[str] = None

# Arrange in reverse order so items can be efficiently popped
chunks = list(reversed(chunks))
Expand Down Expand Up @@ -312,17 +310,16 @@ def _wrap_chunks(self, chunks: list[str]) -> list[str]: # pylint: disable=too-m

# If we end inside a hyperlink, append close sequence
if new_state is not None:
# Ensure we have an id for continuation
if current_hyperlink_id is None:
if 'id=' in new_state.params:
current_hyperlink_id = new_state.params
elif new_state.params:
# Prepend id to existing params. Per OSC 8 spec, params can have
# multiple key=value pairs separated by ':'.
current_hyperlink_id = (
f'id={self._next_hyperlink_id()}:{new_state.params}')
else:
current_hyperlink_id = f'id={self._next_hyperlink_id()}'
# Use the active link's params; wrapper state can leak a prior link's id.
if 'id=' in new_state.params:
continuation_params = new_state.params
elif new_state.params:
# Prepend id to existing params. Per OSC 8 spec, params can have
# multiple key=value pairs separated by ':'.
continuation_params = (
f'id={self._next_hyperlink_id()}:{new_state.params}')
else:
continuation_params = f'id={self._next_hyperlink_id()}'
line_content += HyperlinkParams(
terminator=new_state.terminator, url='').make_close()

Expand All @@ -337,17 +334,16 @@ def _wrap_chunks(self, chunks: list[str]) -> list[str]: # pylint: disable=too-m
).make_open()
new_open = HyperlinkParams(
url=new_state.url,
params=current_hyperlink_id,
params=continuation_params,
terminator=new_state.terminator,
).make_open()
line_content = line_content.replace(old_open, new_open, 1)

# Update state for next line, using computed id
# Update state for next line, using computed params
hyperlink_state = HyperlinkParams(
new_state.url, current_hyperlink_id, new_state.terminator)
new_state.url, continuation_params, new_state.terminator)
else:
hyperlink_state = None
current_hyperlink_id = None # Reset id when hyperlink closes

# Strip trailing whitespace when drop_whitespace is enabled
# (matches CPython #140627 fix behavior)
Expand Down
Loading