Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wcwidth/textwrap.py: 10%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

258 statements  

1""" 

2Sequence-aware text wrapping functions. 

3 

4This module provides functions for wrapping text that may contain terminal escape sequences, with 

5proper handling of Unicode grapheme clusters and character display widths. 

6""" 

7 

8from __future__ import annotations 

9 

10# std imports 

11import secrets 

12import textwrap 

13 

14from typing import TYPE_CHECKING, Optional 

15 

16# local 

17from ._width import width as wcwidth_width 

18from .grapheme import iter_graphemes 

19from .hyperlink import HyperlinkParams 

20from .sgr_state import propagate_sgr as _propagate_sgr 

21from .escape_sequences import ZERO_WIDTH_PATTERN, iter_sequences 

22 

23if TYPE_CHECKING: # pragma: no cover 

24 from typing import Any, Literal 

25 

26 

27class SequenceTextWrapper(textwrap.TextWrapper): 

28 """ 

29 Sequence-aware text wrapper extending :class:`textwrap.TextWrapper`. 

30 

31 This wrapper properly handles terminal escape sequences and Unicode grapheme clusters when 

32 calculating text width for wrapping. 

33 

34 This implementation is based on the SequenceTextWrapper from the 'blessed' library, with 

35 contributions from Avram Lubkin and grayjk. 

36 

37 The key difference from the blessed implementation is the addition of grapheme cluster support 

38 via :func:`~.iter_graphemes`, providing width calculation for ZWJ emoji sequences, VS-16 emojis 

39 and variations, regional indicator flags, and combining characters. 

40 

41 OSC 8 hyperlinks are handled specially: when a hyperlink must span multiple lines, each line 

42 receives complete open/close sequences with a shared ``id`` parameter, ensuring terminals 

43 treat the fragments as a single hyperlink for hover underlining. If the original hyperlink 

44 already has an ``id`` parameter, it is preserved; otherwise, one is generated. 

45 """ 

46 

47 def __init__(self, width: int = 70, *, 

48 control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', 

49 tabsize: int = 8, 

50 ambiguous_width: int = 1, 

51 term_program: bool | str = False, 

52 **kwargs: Any) -> None: 

53 """ 

54 Initialize the wrapper. 

55 

56 :param width: Maximum line width in display cells. 

57 :param control_codes: How to handle control sequences (see :func:`~.width`). 

58 :param tabsize: Tab stop width for tab expansion. 

59 :param ambiguous_width: Width to use for East Asian Ambiguous (A) characters. 

60 :param term_program: Terminal software identifier for table correction. 

61 ``False`` (default) disables override lookup. ``True`` reads the 

62 ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. 

63 Accepts a canonical terminal name matching :func:`list_term_programs`, 

64 such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. 

65 

66 .. versionadded:: 0.8.0 

67 :param kwargs: Additional arguments passed to :class:`textwrap.TextWrapper`. 

68 """ 

69 super().__init__(width=width, **kwargs) 

70 self.control_codes = control_codes 

71 self.tabsize = tabsize 

72 self.ambiguous_width = ambiguous_width 

73 self.term_program = term_program 

74 

75 @staticmethod 

76 def _next_hyperlink_id() -> str: 

77 """Generate unique hyperlink id as 8-character hex string.""" 

78 return secrets.token_hex(4) 

79 

80 def _width(self, text: str) -> int: 

81 """Measure text width accounting for sequences.""" 

82 return wcwidth_width(text, control_codes=self.control_codes, tabsize=self.tabsize, 

83 ambiguous_width=self.ambiguous_width, 

84 term_program=self.term_program) 

85 

86 def _strip_sequences(self, text: str) -> str: 

87 """Strip all terminal sequences from text.""" 

88 result = [] 

89 for segment, is_seq in iter_sequences(text): 

90 if not is_seq: 

91 result.append(segment) 

92 return ''.join(result) 

93 

94 def _extract_sequences(self, text: str) -> str: 

95 """Extract only terminal sequences from text.""" 

96 result = [] 

97 for segment, is_seq in iter_sequences(text): 

98 if is_seq: 

99 result.append(segment) 

100 return ''.join(result) 

101 

102 def _split(self, text: str) -> list[str]: # pylint: disable=too-many-locals 

103 r""" 

104 Sequence-aware variant of :meth:`textwrap.TextWrapper._split`. 

105 

106 This method ensures that terminal escape sequences don't interfere with the text splitting 

107 logic, particularly for hyphen-based word breaking. It builds a position mapping from 

108 stripped text to original text, calls the parent's _split on stripped text, then maps chunks 

109 back. 

110 

111 OSC hyperlink sequences are treated as word boundaries:: 

112 

113 >>> wrap('foo \x1b]8;;https://example.com\x07link\x1b]8;;\x07 bar', 6) 

114 ['foo', '\x1b]8;;https://example.com\x07link\x1b]8;;\x07', 'bar'] 

115 

116 Both BEL (``\x07``) and ST (``\x1b\\``) terminators are supported. 

117 """ 

118 # pylint: disable=too-many-locals,too-many-branches 

119 # Build a mapping from stripped text positions to original text positions. 

120 # 

121 # Track where each character ENDS so that sequences between characters 

122 # attach to the following text, keeping them when whitespace is dropped. 

123 # 

124 # char_end[i] = position in original text right after the i-th stripped char 

125 char_end: list[int] = [] 

126 stripped_text = '' 

127 original_pos = 0 

128 prev_was_hyperlink_close = False 

129 

130 for segment, is_seq in iter_sequences(text): 

131 if not is_seq: 

132 # Conditionally insert space after hyperlink close to force word boundary 

133 if prev_was_hyperlink_close and segment and not segment[0].isspace(): 

134 stripped_text += ' ' 

135 char_end.append(original_pos) 

136 for char in segment: 

137 original_pos += 1 

138 char_end.append(original_pos) 

139 stripped_text += char 

140 prev_was_hyperlink_close = False 

141 else: 

142 is_hyperlink_close = segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07')) 

143 

144 # Conditionally insert space before OSC sequences to artificially create word 

145 # boundary, but *not* before hyperlink close sequences, to ensure hyperlink is 

146 # terminated on the same line. 

147 if (segment.startswith('\x1b]') and stripped_text and not 

148 stripped_text[-1].isspace()): 

149 if not is_hyperlink_close: 

150 stripped_text += ' ' 

151 char_end.append(original_pos) 

152 

153 # Escape sequences advance position but don't add to stripped text 

154 original_pos += len(segment) 

155 prev_was_hyperlink_close = is_hyperlink_close 

156 

157 # Add sentinel for final position 

158 char_end.append(original_pos) 

159 

160 # Use parent's _split on the stripped text 

161 # pylint: disable-next=protected-access 

162 stripped_chunks = textwrap.TextWrapper._split(self, stripped_text) 

163 

164 # Handle text that contains only sequences (no visible characters). 

165 # Return the sequences as a single chunk to preserve them. 

166 if not stripped_chunks and text: 

167 return [text] 

168 

169 # Map the chunks back to the original text with sequences 

170 result: list[str] = [] 

171 stripped_pos = 0 

172 num_chunks = len(stripped_chunks) 

173 

174 for idx, chunk in enumerate(stripped_chunks): 

175 chunk_len = len(chunk) 

176 

177 # Start is where previous character ended (or 0 for first chunk) 

178 start_orig = 0 if stripped_pos == 0 else char_end[stripped_pos - 1] 

179 

180 # End is where next character starts. For last chunk, use sentinel 

181 # to include any trailing sequences. 

182 if idx == num_chunks - 1: 

183 end_orig = char_end[-1] # sentinel includes trailing sequences 

184 else: 

185 end_orig = char_end[stripped_pos + chunk_len - 1] 

186 

187 # Extract the corresponding portion from the original text 

188 # Skip empty chunks (from virtual spaces inserted at OSC boundaries) 

189 if start_orig != end_orig: 

190 result.append(text[start_orig:end_orig]) 

191 stripped_pos += chunk_len 

192 

193 return result 

194 

195 def _wrap_chunks(self, chunks: list[str]) -> list[str]: # pylint: disable=too-many-branches 

196 """ 

197 Wrap chunks into lines using sequence-aware width. 

198 

199 Override TextWrapper._wrap_chunks to measure with _width. Follows stdlib's algorithm: 

200 greedily fill lines, handle long words. Also handle OSC hyperlink processing. When 

201 hyperlinks span multiple lines, each line gets complete open/close sequences with matching 

202 id parameters for hover underlining continuity per OSC 8 spec. 

203 """ 

204 # pylint: disable=too-many-branches,too-many-statements,too-complex,too-many-locals 

205 # pylint: disable=too-many-nested-blocks 

206 # The hyperlink code pushes the complexity rating of this method. It stays in one method 

207 # because of the shared local state. 

208 if self.width <= 0: 

209 raise ValueError('invalid width %r (must be > 0)' % self.width) 

210 if not chunks: 

211 return [] 

212 

213 if self.max_lines is not None: 

214 if self.max_lines > 1: 

215 indent = self.subsequent_indent 

216 else: 

217 indent = self.initial_indent 

218 if (self._width(indent) 

219 + self._width(self.placeholder.lstrip()) 

220 > self.width): 

221 raise ValueError("placeholder too large for max width") 

222 

223 lines: list[str] = [] 

224 is_first_line = True 

225 

226 hyperlink_state: Optional[HyperlinkParams] = None 

227 # Track the id we're using for the current hyperlink continuation 

228 current_hyperlink_id: Optional[str] = None 

229 

230 # Arrange in reverse order so items can be efficiently popped 

231 chunks = list(reversed(chunks)) 

232 

233 while chunks: 

234 current_line: list[str] = [] 

235 current_width = 0 

236 

237 # Get the indent and available width for current line 

238 indent = self.initial_indent if is_first_line else self.subsequent_indent 

239 line_width = self.width - self._width(indent) 

240 

241 # If continuing a hyperlink from previous line, prepend open sequence 

242 if hyperlink_state is not None: 

243 open_seq = HyperlinkParams( 

244 url=hyperlink_state.url, 

245 params=hyperlink_state.params, 

246 terminator=hyperlink_state.terminator, 

247 ).make_open() 

248 chunks[-1] = open_seq + chunks[-1] 

249 

250 # Drop leading whitespace (except at very start) 

251 # When dropping, transfer any sequences to the next chunk. 

252 # Only drop when actual whitespace text is present. 

253 stripped = self._strip_sequences(chunks[-1]) 

254 if self.drop_whitespace and lines and stripped and not stripped.strip(): 

255 sequences = self._extract_sequences(chunks[-1]) 

256 del chunks[-1] 

257 if sequences and chunks: 

258 chunks[-1] = sequences + chunks[-1] 

259 

260 # Greedily add chunks that fit 

261 while chunks: 

262 chunk = chunks[-1] 

263 chunk_width = self._width(chunk) 

264 

265 if current_width + chunk_width <= line_width: 

266 current_line.append(chunks.pop()) 

267 current_width += chunk_width 

268 else: 

269 break 

270 

271 # Handle chunk that's too long for any line 

272 if chunks and self._width(chunks[-1]) > line_width: 

273 self._handle_long_word( 

274 chunks, current_line, current_width, line_width 

275 ) 

276 current_width = self._width(''.join(current_line)) 

277 # Remove any empty chunks left by _handle_long_word 

278 while chunks and not chunks[-1]: 

279 del chunks[-1] 

280 

281 # Drop trailing whitespace 

282 # When dropping, transfer any sequences to the previous chunk. 

283 # Only drop when actual whitespace text is present. 

284 stripped_last = self._strip_sequences(current_line[-1]) if current_line else '' 

285 if (self.drop_whitespace and current_line and 

286 stripped_last and not stripped_last.strip()): 

287 sequences = self._extract_sequences(current_line[-1]) 

288 current_width -= self._width(current_line[-1]) 

289 del current_line[-1] 

290 if sequences and current_line: 

291 current_line[-1] = current_line[-1] + sequences 

292 

293 if current_line: 

294 # Check whether this is a normal append or max_lines 

295 # truncation. Matches stdlib textwrap precedence: 

296 # normal if max_lines not set, not yet reached, or no 

297 # remaining visible content that would need truncation. 

298 no_more_content = ( 

299 not chunks or 

300 self.drop_whitespace and 

301 len(chunks) == 1 and 

302 not self._strip_sequences(chunks[0]).strip() 

303 ) 

304 if (self.max_lines is None or 

305 len(lines) + 1 < self.max_lines or 

306 no_more_content 

307 and current_width <= line_width): 

308 line_content = ''.join(current_line) 

309 

310 # Track hyperlink state through this line's content 

311 new_state = self._track_hyperlink_state(line_content, hyperlink_state) 

312 

313 # If we end inside a hyperlink, append close sequence 

314 if new_state is not None: 

315 # Ensure we have an id for continuation 

316 if current_hyperlink_id is None: 

317 if 'id=' in new_state.params: 

318 current_hyperlink_id = new_state.params 

319 elif new_state.params: 

320 # Prepend id to existing params. Per OSC 8 spec, params can have 

321 # multiple key=value pairs separated by ':'. 

322 current_hyperlink_id = ( 

323 f'id={self._next_hyperlink_id()}:{new_state.params}') 

324 else: 

325 current_hyperlink_id = f'id={self._next_hyperlink_id()}' 

326 line_content += HyperlinkParams( 

327 terminator=new_state.terminator, url='').make_close() 

328 

329 # Also need to inject the id into the opening 

330 # sequence if it didn't have one 

331 if 'id=' not in new_state.params: 

332 # Find and replace the original open sequence with one that has id 

333 old_open = HyperlinkParams( 

334 url=new_state.url, 

335 params=new_state.params, 

336 terminator=new_state.terminator, 

337 ).make_open() 

338 new_open = HyperlinkParams( 

339 url=new_state.url, 

340 params=current_hyperlink_id, 

341 terminator=new_state.terminator, 

342 ).make_open() 

343 line_content = line_content.replace(old_open, new_open, 1) 

344 

345 # Update state for next line, using computed id 

346 hyperlink_state = HyperlinkParams( 

347 new_state.url, current_hyperlink_id, new_state.terminator) 

348 else: 

349 hyperlink_state = None 

350 current_hyperlink_id = None # Reset id when hyperlink closes 

351 

352 # Strip trailing whitespace when drop_whitespace is enabled 

353 # (matches CPython #140627 fix behavior) 

354 if self.drop_whitespace: 

355 line_content = line_content.rstrip() 

356 if not line_content: 

357 continue 

358 lines.append(indent + line_content) 

359 is_first_line = False 

360 else: 

361 # max_lines reached with remaining content. 

362 # pop chunks until placeholder fits, then break. 

363 placeholder_w = self._width(self.placeholder) 

364 while current_line: 

365 last_text = self._strip_sequences(current_line[-1]) 

366 if (last_text.strip() 

367 and current_width + placeholder_w <= line_width): 

368 line_content = ''.join(current_line) 

369 new_state = self._track_hyperlink_state( 

370 line_content, hyperlink_state) 

371 if new_state is not None: 

372 line_content += HyperlinkParams( 

373 terminator=new_state.terminator, url='').make_close() 

374 lines.append(indent + line_content + self.placeholder) 

375 break 

376 current_width -= self._width(current_line[-1]) 

377 del current_line[-1] 

378 else: 

379 if lines: 

380 prev_line = self._rstrip_visible(lines[-1]) 

381 if (self._width(prev_line) + placeholder_w 

382 <= self.width): 

383 lines[-1] = prev_line + self.placeholder 

384 break 

385 lines.append(indent + self.placeholder.lstrip()) 

386 break 

387 

388 return lines 

389 

390 def _track_hyperlink_state( 

391 self, text: str, 

392 state: Optional[HyperlinkParams]) -> Optional[HyperlinkParams]: 

393 """ 

394 Track hyperlink state through text. 

395 

396 :param text: Text to scan for hyperlink sequences. 

397 :param state: Current state or None if outside hyperlink. 

398 :returns: Updated state after processing text. 

399 """ 

400 for segment, is_seq in iter_sequences(text): 

401 if is_seq: 

402 parsed_link = HyperlinkParams.parse(segment) 

403 if parsed_link is not None and parsed_link.url: # has URL = open 

404 state = parsed_link 

405 elif segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07')): # close 

406 state = None 

407 return state 

408 

409 def _handle_long_word(self, reversed_chunks: list[str], 

410 cur_line: list[str], cur_len: int, 

411 width: int) -> None: 

412 """ 

413 Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`. 

414 

415 This method ensures that word boundaries are not broken mid-sequence, and respects grapheme 

416 cluster boundaries when breaking long words. 

417 """ 

418 if width < 1: 

419 space_left = 1 

420 else: 

421 space_left = width - cur_len 

422 

423 chunk = reversed_chunks[-1] 

424 

425 if self.break_long_words: 

426 break_at_hyphen = False 

427 hyphen_end = 0 

428 # End of the prefix that fits within space_left by display width. 

429 prefix_end = self._find_break_position(chunk, space_left) 

430 

431 # Handle break_on_hyphens: find last hyphen in the portion that fits. 

432 if self.break_on_hyphens: 

433 stripped = self._strip_sequences(chunk[:prefix_end]) 

434 hyphen_pos = stripped.rfind('-') 

435 if hyphen_pos > 0 and any(c != '-' for c in stripped[:hyphen_pos]): 

436 # Map back to original position including sequences 

437 hyphen_end = self._map_stripped_pos_to_original(chunk, hyphen_pos + 1) 

438 break_at_hyphen = True 

439 

440 # Break at grapheme boundaries to avoid splitting multi-codepoint characters 

441 if break_at_hyphen: 

442 actual_end = hyphen_end 

443 else: 

444 actual_end = prefix_end 

445 # Include first visible unit when break would take only leading sequences. 

446 if not cur_line and ( 

447 actual_end == 0 

448 or (actual_end < len(chunk) 

449 and self._width(chunk[:actual_end]) == 0)): 

450 actual_end = self._find_first_visible_break(chunk) 

451 cur_line.append(chunk[:actual_end]) 

452 reversed_chunks[-1] = chunk[actual_end:] 

453 

454 elif not cur_line: 

455 cur_line.append(reversed_chunks.pop()) 

456 

457 def _map_stripped_pos_to_original(self, text: str, stripped_pos: int) -> int: 

458 """Map a position in stripped text back to original text position.""" 

459 stripped_idx = 0 

460 original_idx = 0 

461 

462 for segment, is_seq in iter_sequences(text): 

463 if is_seq: 

464 original_idx += len(segment) 

465 elif stripped_idx + len(segment) > stripped_pos: 

466 # Position is within this segment 

467 return original_idx + (stripped_pos - stripped_idx) 

468 else: 

469 stripped_idx += len(segment) 

470 original_idx += len(segment) 

471 

472 # Caller guarantees stripped_pos < total stripped chars, so we always 

473 # return from within the loop. This line satisfies the type checker. 

474 return original_idx # pragma: no cover 

475 

476 def _find_break_position(self, text: str, max_width: int) -> int: 

477 """Find string index in text that fits within max_width cells.""" 

478 idx = 0 

479 width_so_far = 0 

480 

481 while idx < len(text): 

482 char = text[idx] 

483 

484 # Skip escape sequences (they don't add width) 

485 if char == '\x1b': 

486 match = ZERO_WIDTH_PATTERN.match(text, idx) 

487 if match: 

488 idx = match.end() 

489 continue 

490 

491 # Get grapheme (use start= to avoid slice allocation) 

492 grapheme = next(iter_graphemes(text, start=idx)) 

493 

494 grapheme_width = self._width(grapheme) 

495 if width_so_far + grapheme_width > max_width: 

496 return idx # Found break point 

497 

498 width_so_far += grapheme_width 

499 idx += len(grapheme) 

500 

501 # Caller guarantees chunk_width > max_width, so a grapheme always 

502 # exceeds and we return from within the loop. Type checker requires this. 

503 return idx # pragma: no cover 

504 

505 def _find_first_visible_break(self, text: str) -> int: 

506 """End of leading escape sequences plus the first grapheme.""" 

507 idx = 0 

508 while idx < len(text) and text[idx] == '\x1b': 

509 match = ZERO_WIDTH_PATTERN.match(text, idx) 

510 if match is None: 

511 break 

512 idx = match.end() 

513 return idx + len(next(iter_graphemes(text, start=idx))) 

514 

515 def _rstrip_visible(self, text: str) -> str: 

516 """Strip trailing visible whitespace, preserving trailing sequences.""" 

517 segments = list(iter_sequences(text)) 

518 last_vis = -1 

519 for i, (segment, is_seq) in enumerate(segments): 

520 if not is_seq and segment.rstrip(): 

521 last_vis = i 

522 if last_vis == -1: 

523 return '' 

524 result = [] 

525 for i, (segment, is_seq) in enumerate(segments): 

526 if i < last_vis: 

527 result.append(segment) 

528 elif i == last_vis: 

529 result.append(segment.rstrip()) 

530 elif is_seq: 

531 result.append(segment) 

532 return ''.join(result) 

533 

534 

535def wrap(text: str, width: int = 70, *, 

536 control_codes: Literal['parse', 'strict', 'ignore'] = 'parse', 

537 tabsize: int = 8, 

538 expand_tabs: bool = True, 

539 replace_whitespace: bool = True, 

540 ambiguous_width: int = 1, 

541 term_program: bool | str = False, 

542 initial_indent: str = '', 

543 subsequent_indent: str = '', 

544 fix_sentence_endings: bool = False, 

545 break_long_words: bool = True, 

546 break_on_hyphens: bool = True, 

547 drop_whitespace: bool = True, 

548 max_lines: Optional[int] = None, 

549 placeholder: str = ' [...]', 

550 propagate_sgr: bool = True) -> list[str]: 

551 r""" 

552 Wrap text to fit within given width, returning a list of wrapped lines. 

553 

554 Like :func:`textwrap.wrap`, but measures width in display cells, correctly 

555 handling wide characters, combining marks, and terminal 

556 escape sequences. 

557 

558 :param text: Text to wrap, may contain terminal sequences. 

559 :param width: Maximum line width in display cells. 

560 :param control_codes: How to handle terminal sequences (see :func:`~.width`). 

561 :param tabsize: Tab stop width for tab expansion. 

562 :param expand_tabs: If True (default), tab characters are expanded 

563 to spaces using ``tabsize``. 

564 :param replace_whitespace: If True (default), each whitespace character 

565 is replaced with a single space after tab expansion. When False, 

566 control whitespace like ``\n`` has zero display width (unlike 

567 :func:`textwrap.wrap` which counts ``len()``), so wrap points 

568 may differ from stdlib for non-space whitespace characters. 

569 :param ambiguous_width: Width to use for East Asian Ambiguous (A) 

570 characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts. 

571 :param term_program: Terminal software identifier for table correction. 

572 ``False`` (default) disables override lookup. ``True`` reads the 

573 ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection. 

574 Accepts a canonical terminal name matching :func:`list_term_programs`, 

575 such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``. 

576 

577 .. versionadded:: 0.8.0 

578 :param initial_indent: String prepended to first line. 

579 :param subsequent_indent: String prepended to subsequent lines. 

580 :param fix_sentence_endings: If True, ensure sentences are always 

581 separated by exactly two spaces. 

582 :param break_long_words: If True, break words longer than width. 

583 :param break_on_hyphens: If True, allow breaking at hyphens. 

584 :param drop_whitespace: If True (default), whitespace at the beginning 

585 and end of each line (after wrapping but before indenting) is dropped. 

586 Set to False to preserve whitespace. 

587 :param max_lines: If set, output contains at most this many lines, with 

588 ``placeholder`` appended to the last line if the text was truncated. 

589 :param placeholder: String appended to the last line when text is 

590 truncated by ``max_lines``. Default is ``' [...]'``. 

591 :param propagate_sgr: If True (default), SGR (terminal styling) sequences 

592 are propagated across wrapped lines. Each line ends with a reset 

593 sequence and the next line begins with the active style restored. 

594 :returns: List of wrapped lines without trailing newlines. 

595 

596 SGR (terminal styling) sequences are propagated across wrapped lines 

597 by default. Each line ends with a reset sequence and the next line 

598 begins with the active style restored:: 

599 

600 >>> wrap('\x1b[1;34mHello world\x1b[0m', width=6) 

601 ['\x1b[1;34mHello\x1b[0m', '\x1b[1;34mworld\x1b[0m'] 

602 

603 Set ``propagate_sgr=False`` to disable this behavior. 

604 

605 Like :func:`textwrap.wrap`, newlines in the input text are treated as 

606 whitespace and collapsed. To preserve paragraph breaks, wrap each 

607 paragraph separately:: 

608 

609 >>> text = 'First line.\nSecond line.' 

610 >>> wrap(text, 40) # newline collapsed to space 

611 ['First line. Second line.'] 

612 >>> [line for para in text.split('\n') 

613 ... for line in (wrap(para, 40) if para else [''])] 

614 ['First line.', 'Second line.'] 

615 

616 .. note:: 

617 

618 Segmentation follows UAX #29 grapheme clusters. Tamil, Kannada and Sinhala may break in the 

619 middle of a conjunct and fall short of ``width``. Correct rendering of these languages is 

620 not specified by unicode.org standards, and no terminal emulator or editor handles them 

621 legibly, so this library function makes no attempt at accommodating them. 

622 

623 .. seealso:: 

624 

625 :func:`textwrap.wrap`, :class:`textwrap.TextWrapper` 

626 Standard library text wrapping (character-based). 

627 

628 :class:`.SequenceTextWrapper` 

629 Class interface for advanced wrapping options. 

630 

631 .. versionadded:: 0.3.0 

632 

633 .. versionchanged:: 0.5.0 

634 Added ``propagate_sgr`` parameter (default True). 

635 

636 .. versionchanged:: 0.6.0 

637 Added ``expand_tabs``, ``replace_whitespace``, ``fix_sentence_endings``, 

638 ``drop_whitespace``, ``max_lines``, and ``placeholder`` parameters. 

639 

640 Example:: 

641 

642 >>> from wcwidth import wrap 

643 >>> wrap('hello world', 5) 

644 ['hello', 'world'] 

645 >>> wrap('中文字符', 4) # CJK characters (2 cells each) 

646 ['中文', '字符'] 

647 """ 

648 # pylint: disable=too-many-arguments,too-many-locals 

649 wrapper = SequenceTextWrapper( 

650 width=width, 

651 control_codes=control_codes, 

652 tabsize=tabsize, 

653 expand_tabs=expand_tabs, 

654 replace_whitespace=replace_whitespace, 

655 ambiguous_width=ambiguous_width, 

656 term_program=term_program, 

657 initial_indent=initial_indent, 

658 subsequent_indent=subsequent_indent, 

659 fix_sentence_endings=fix_sentence_endings, 

660 break_long_words=break_long_words, 

661 break_on_hyphens=break_on_hyphens, 

662 drop_whitespace=drop_whitespace, 

663 max_lines=max_lines, 

664 placeholder=placeholder, 

665 ) 

666 lines = wrapper.wrap(text) 

667 

668 if propagate_sgr: 

669 lines = _propagate_sgr(lines) 

670 

671 return lines