Styling¶

Click Extra ships its own Style class as a drop-in replacement for cloup.Style (which itself wraps click.style). The runtime contract (calling the instance to apply the style, equality, hashing) is identical to cloup’s; everything below is purely additive ergonomics.

The class lands automatically when you do from click_extra import Style: __init__.py re-exports it after from cloup import * so the click-extra version takes precedence.

hello

Hex-string color shorthand¶

Style.fg and Style.bg accept "#rrggbb" or "#rgb" shorthand strings alongside Click’s named colors and RGB tuples. The shorthand is converted to an RGB tuple on construction:

sample
sample
sample
sample

REPL-friendly __repr__ and __str__¶

The compact __repr__ hides None and falsy attributes and renders RGB tuples as #rrggbb. The __str__ returns the styled word "sample" so print(style) and debugger inspectors visualize what the style does, not just its fields:

Style(fg=#f1fa8c, bold, italic)
sample

Composition operator a | b¶

The | operator merges two styles. The right operand wins on conflicts; None fields on either side don’t override the other side’s set fields. The reflected __ror__ makes mixing with cloup.Style (or any compatible subclass) symmetric:

Style(fg='magenta', bold, italic)
Style(fg='cyan', bold, italic)

cascade(base): fill gaps from a base style¶

Where | lets the right operand always win, cascade keeps the instance’s set values and only fills its None fields from base. Useful for theme-inheritance patterns where a derived style should keep its overrides and inherit the rest:

Style(fg='magenta', bold, italic)

TOML/JSON round-trip with to_dict / from_dict¶

Style.to_dict() emits a plain mapping containing only the set fields, with RGB tuples flattened to #rrggbb strings so the output round-trips through TOML/JSON/YAML untouched. Style.from_dict() is the symmetric loader and rejects unknown keys with TypeError so typos surface immediately:

{'fg': '#f1fa8c', 'bold': True}
{"fg": "#f1fa8c", "bold": true}
True
TypeError('Unknown Style field(s): colour')

to_css(): CSS declaration list¶

Renders the style as a semicolon-separated CSS declaration list, suitable for inline style="..." attributes on HTML spans. Used by AnsiHtmlFormatter and the theme palette swatches:

color: #f1fa8c; font-weight: bold; font-style: italic
color: #ff6d67; text-decoration: underline line-through
color: cyan; opacity: 0.6

The mapping is:

Attribute

CSS declaration

fg

color: <color>

bg

background-color: <color>

bold

font-weight: bold

italic

font-style: italic

underline

text-decoration: underline

overline

text-decoration: overline

strikethrough

text-decoration: line-through

dim

opacity: 0.6

reverse

filter: invert(1)

underline, overline and strikethrough collapse into a single text-decoration declaration when more than one is set.

from_ansi(): parse ANSI SGR escapes¶

Given one or more consecutive ANSI SGR escapes (the \x1b[...m sequences Click emits), rebuild a Style instance. Supports the standard 8/16-color codes (30–37, 40–47, 90–97, 100–107), the 38;5;n / 48;5;n 256-color extension, and the 38;2;r;g;b / 48;2;r;g;b 24-bit extension. Reset codes (the full 0 reset, its parameter-less \x1b[m form included, and selective resets like 22, 39 or 49) are ignored, so parsing the full output of a style call recovers that style. Multiple back-to-back escapes (as Click emits when combining colors with attributes) are merged into a single Style:

Style(fg='red', bold)
Style(fg='red', bold)
Style(fg=#f1fa8c)
Style(fg=226)

from_ansi is the inverse of calling the style: parsing the output of Style(fg="red", bold=True)("text") recovers the same style.

split_ansi() and render_ansi(): tokenize ANSI streams¶

While from_ansi() parses bare escapes, split_ansi() tokenizes a whole string mixing text and escapes. It is a stateful SGR stream parser: each escape updates the current style (full and selective resets honored), and every maximal run of text sharing the same style is yielded as a (Style, text) tuple. Non-SGR escapes (cursor movements, OSC hyperlink wrappers) carry no style information and are removed from the yielded text:

Style(fg='blue') -> 'Monday'
Style() -> ' was '
Style(fg='yellow', bold) -> 'sunny'

render_ansi(text, emitter) builds on it to rewrite a string: unstyled runs pass through verbatim, and each styled run is replaced by whatever markup the emitter callable produces. Styled runs are split at newlines, so no markup wrapper ever crosses a line boundary:

a [styled] word

open_ansi(): splice a fragment into styled text¶

Styling a fragment closes it with a reset, and that reset also closes whatever styling surrounded the fragment. open_ansi(text) returns the escapes text still leaves in effect, so the surrounding styling can be reopened right after the splice:

'\x1b[33mRain on Monday, sun on \x1b[34mTuesday\x1b[0m\x1b[33m.\x1b[0m'

A full reset empties the accumulator, so a string that closes its own styling reopens nothing:

Help screens are painted in several passes, and each one runs over what the previous ones already styled. open_ansi() is what lets an option name quoted in a deprecation message take its own color without stripping the message’s color from everything after it.

ANSI markup converters¶

Four ready-made converters translate ANSI styling to markup languages with native styling support. They power the table styles translation, and are just as useful standalone, to export any styled CLI output:

  • ansi_to_html() produces self-contained, inline-CSS <span> tags (also valid in markups accepting embedded HTML, like MediaWiki).

  • ansi_to_jira() produces {color:…} macros and Jira wiki emphasis markers.

  • ansi_to_latex() produces xcolor-based color macros and core LaTeX text macros.

  • ansi_to_textile() produces %{…} spans carrying the style as inline CSS.

<span style="color: blue; font-weight: bold">Summer</span>
{color:blue}*Summer*{color}
\textcolor{blue}{\textbf{Summer}}
%{color: blue; font-weight: bold}Summer%

Each converter maps ANSI attributes to their closest equivalent in the target markup, and silently drops those the target cannot express (like blink in CSS, backgrounds in Jira markup, or dim in LaTeX). Colors named after the 8 base ANSI colors pass through as color keywords; bright variants, 256-color indices and 24-bit values resolve to hex.

contrast_ratio(other): WCAG accessibility check¶

Returns the WCAG 2.x contrast ratio between this style’s foreground and another style’s foreground. Result is in [1, 21]: 1 = identical colors (no contrast), 21 = maximum contrast (black on white). WCAG AA requires 4.5+ for normal text and 3.0+ for large text; AAA wants 7.0+ and 4.5+ respectively.

19.26
1.47
4.08

Click Extra uses this internally to gate the WCAG legibility floor and AA Large compliance for branded themes.

Equality and hash¶

A Style compares equal to the cloup.Style carrying the same fields, and hashes the same, so the two classes mix in sets and dict keys:

True

Shared dataclass round-trip helpers¶

The serialization machinery Style.to_dict / from_dict relies on two small module-level helpers that codify “walk dataclass fields, skip default-valued fields, raise on unknown keys”. They’re public so other dataclass-shaped values in click-extra (notably HelpTheme) can reuse them, and so downstream code with similar patterns can build on the same primitives.

fields_to_dict(instance, *, encode=…, keep=…)¶

Walks every field via dataclasses.fields, skips the ones still holding their default, applies an optional keep(field, value) -> bool filter to the rest (default keeps them all), and passes the surviving values through an encode(field, value) -> encoded_value callback (default identity):

{'r': 255, 'g': 128, 'name': 'orange'}
{'r': 'ff', 'g': '80', 'name': 'orange'}

dict_to_fields(cls, data, *, decode=…)¶

The symmetric loader. Validates every key in data against cls’s dataclass fields and raises TypeError listing every unknown key, so callers can build a constructor call without an extra pre-validation pass:

Color(r=255, g=0, b=0, name='orange')
TypeError('Unknown Color field(s): red')

click_extra.styling API¶

        classDiagram
  Style <|-- Style
    

Drop-in replacement for cloup.Style with extra features.

The module name mirrors cloup.styling, the upstream module that hosts the original Style class. Click Extra’s Style is a subclass that keeps cloup’s runtime contract (calling the instance to apply the style, equality, hashing) intact and adds:

  • An equality and a hash shared with cloup.Style, so a Style equals the cloup.Style carrying the same fields.

  • A compact, single-line __repr__ that hides None and falsy attributes and renders RGB tuples as #rrggbb hex.

  • Hex-string color shorthand: Style(fg="#f1fa8c") works alongside Style(fg=(241, 250, 140)).

  • A __str__ that returns the styled word "sample" so REPL prints and debuggers visualize what the style does, not just its fields.

  • A composition operator a | b that merges two styles, with the right operand winning on conflicts.

  • A Style.cascade() method that fills the style’s None fields from a base style without overriding any value already set.

  • Style.to_dict() / Style.from_dict() for round-tripping styles through TOML/JSON/YAML.

  • Style.to_css() for emitting CSS-equivalent declarations: useful for HTML renderings of help screens.

  • Style.from_ansi() for parsing an ANSI SGR escape sequence back into a Style instance.

  • Style.contrast_ratio() returning the WCAG contrast ratio between two foreground colors. Useful for theme designers checking accessibility.

  • split_ansi() and render_ansi() for tokenizing a string mixing text and ANSI escapes into styled runs, then re-rendering those runs through a markup emitter. Wrapping them to a width lives in wrap_ansi(), with the rest of the terminal-grid measures.

  • The ansi_to_html(), ansi_to_jira(), ansi_to_latex() and ansi_to_textile() converters, translating ANSI styling to markup languages with native styling support.

The escape grammar, the SGR decoder and the color conversions all of this reads through live in click_extra.ansi, with strip_ansi() and open_ansi().

click_extra.styling.fields_to_dict(instance, *, encode=<function <lambda>>, keep=<function <lambda>>)[source]

Serialize a dataclass instance to a dict of its non-default fields.

Skips every field still holding its declared default, then applies keep to decide which of the remaining fields are written, and passes their values through encode.

Parameters:
  • instance (Any) – the dataclass to serialize.

  • encode (Callable[[Any, Any], Any]) – callable (field, value) -> encoded_value applied to every kept value. Use to convert RGB tuples to #rrggbb strings, nested dataclasses to dicts, etc. Defaults to the identity.

  • keep (Callable[[Any, Any], bool]) – callable (field, value) -> bool deciding whether a non-default field is emitted. Defaults to keeping all of them.

Return type:

dict[str, Any]

click_extra.styling.dict_to_fields(cls, data, *, decode=<function <lambda>>)[source]

Validate data’s keys against cls’s dataclass fields and decode them.

Returns a kwargs dict ready to splat into cls(**kwargs).

Parameters:
  • cls (type) – the dataclass type whose fields define the legal keys.

  • data (dict[str, Any]) – mapping from field name to a serialized value.

  • decode (Callable[[Any, Any], Any]) – callable (field, raw) -> decoded_value invoked for every recognized key. Default returns the raw value unchanged.

Raises:

TypeError – listing every key that names no field of cls, so callers can build a constructor call without validating first.

Return type:

dict[str, Any]

click_extra.styling.supports_truecolor()[source]

Whether the terminal is assumed to render 24-bit (truecolor) ANSI.

Drives Style.__call__’s choice between emitting a 24-bit 38;2;r;g;b sequence and quantizing it to the nearest 38;5;n 256-color index, so a branded theme’s RGB colors degrade gracefully on a terminal that cannot display them.

The policy is optimistic: assume truecolor unless the environment positively says otherwise. Precedence, highest first:

  1. COLORTERM of truecolor / 24bit (see _TRUECOLOR_COLORTERMS) keeps 24-bit.

  2. Any other non-empty COLORTERM quantizes: an explicit lower advertisement.

  3. A TERM ending in -16color quantizes: an unambiguous sub-256 terminal. *-256color is deliberately not treated as a downgrade, since truecolor terminals routinely report it while advertising their 24-bit support through COLORTERM instead. Honoring it would strip truecolor from the very terminals this optimistic default protects.

  4. Otherwise keeps 24-bit.

A dumb / unknown TERM never reaches this decision for CLI output: it has already disabled color upstream through resolve_color_env().

Return type:

bool

class click_extra.styling.Style(fg=None, bg=None, bold=None, dim=None, underline=None, overline=None, italic=None, blink=None, reverse=None, strikethrough=None, text_transform=None)[source]

Bases: Style

cloup.Style with extra ergonomics.

See the module docstring for the full list of additions. The runtime contract (calling the instance to apply the style, equality, hashing) is otherwise identical to cloup.Style.

fg: str | tuple[int, int, int] | int | None = None

Foreground color: named ANSI string, #rrggbb hex, RGB tuple, or palette index.

bg: str | tuple[int, int, int] | int | None = None

Background color: named ANSI string, #rrggbb hex, RGB tuple, or palette index.

cascade(base)[source]

Return a copy with None fields filled in from base.

The instance’s own non-None values always win: cascade only fills gaps. Useful for theme inheritance: derived.cascade(parent) keeps derived’s overrides and inherits the rest from parent.

Return type:

Style

to_dict()[source]

Serialize to a plain dict with only the set fields.

RGB tuples are emitted as #rrggbb strings so the result round-trips through TOML/JSON/YAML untouched. Pair with from_dict() to rebuild a Style.

Return type:

dict[str, Any]

classmethod from_dict(data)[source]

Build a Style from the plain dict produced by to_dict().

Validates that every key in data names a known Style field and raises TypeError otherwise. Pair with to_dict() to round-trip through TOML/JSON/YAML.

Return type:

Style

to_css()[source]

Render the style as a semicolon-separated CSS declaration list.

Style(fg="#f1fa8c", bold=True).to_css() returns "color: #f1fa8c; font-weight: bold". Suitable for inline style="..." attributes on HTML spans.

Return type:

str

classmethod from_ansi(escape)[source]

Parse one or more consecutive ANSI SGR escapes into a Style.

Supports the standard 8/16-color codes (30–37, 40–47, 90–97, 100–107), the 38;5;n / 48;5;n 256-color extension, and the 38;2;r;g;b / 48;2;r;g;b 24-bit extension. Reset codes (the full 0 reset, its parameter-less \x1b[m form included, and selective resets like 22, 39 or 49) are ignored, so parsing the full output of a style call (trailing reset included) recovers that style. Multiple back-to-back escapes (as click emits when combining colors with attributes: \x1b[31m\x1b[1m) are merged into a single Style.

To tokenize a string mixing text and escapes, with resets honored, see split_ansi().

Return type:

Style

contrast_ratio(other)[source]

Return the WCAG 2.x contrast ratio between this fg and other’s fg.

Result is in [1, 21]: 1 = identical colors (no contrast), 21 = maximum contrast (black on white). WCAG AA requires 4.5+ for normal text, 3.0+ for large text; AAA wants 7.0+ and 4.5+ respectively.

Return type:

float

click_extra.styling.split_ansi(text)[source]

Split text into (style, text) runs at ANSI SGR escape boundaries.

A stateful SGR stream parser: each escape updates the current style state (with full and selective resets honored, unlike Style.from_ansi()), and every maximal run of text sharing the same state is yielded with its Style. Unstyled text is yielded with an empty Style. Consecutive runs with equal styles are merged and empty runs are dropped, so wrapping each yielded run produces minimal markup.

Every other escape carries no style information and is removed from the yielded text by strip_ansi(), so a terminal hyperlink keeps its display text and loses its URI.

Return type:

Iterator[tuple[Style, str]]

click_extra.styling.render_ansi(text, emitter)[source]

Rebuild text, replacing each ANSI-styled run by emitter’s markup.

Unstyled runs pass through verbatim, so any markup surrounding the styled runs (table borders, tags produced by another renderer) is preserved byte for byte. Styled runs are handed to emitter one line at a time: runs are split on newlines so no markup wrapper ever crosses a line boundary, which keeps line-oriented markup (LaTeX rows, wiki tables) well-formed even when a style spans multiple lines.

Return type:

str

click_extra.styling.ansi_to_html(text)[source]

Translate ANSI styling in text to inline-styled HTML <span> tags.

\x1b[34mSummer\x1b[0m becomes <span style="color: blue">Summer</span>. The spans are self-contained (no stylesheet needed) and also valid in markups accepting inline HTML, like MediaWiki.

Return type:

str

click_extra.styling.ansi_to_jira(text)[source]

Translate ANSI styling in text to Jira wiki markup.

\x1b[34;1mSummer\x1b[0m becomes {color:blue}*Summer*{color}.

Return type:

str

click_extra.styling.ansi_to_latex(text)[source]

Translate ANSI styling in text to LaTeX macros.

\x1b[34;1mSummer\x1b[0m becomes \textcolor{blue}{\textbf{Summer}}. The colored macros require \usepackage{xcolor} in the document preamble.

Return type:

str

click_extra.styling.ansi_to_textile(text)[source]

Translate ANSI styling in text to Textile spans.

\x1b[34;1mSummer\x1b[0m becomes %{color: blue; font-weight: bold}Summer%.

Return type:

str

click_extra.ansi API¶

Read and write ANSI escape sequences, and convert terminal colors.

A terminal styles its text with escape sequences mixed into the text. Every module of this package that reads or writes such text uses the grammar and the color arithmetic of this module:

Style, the stream parser split_ansi() and the markup converters are built on this module, in click_extra.styling.

click_extra.ansi.RESET = '\x1b[0m'

Escape closing every style a line left open, written before any padding.

click_extra.ansi.ANSI_16_RGB: tuple[tuple[int, int, int], ...] = ((0, 0, 0), (239, 41, 41), (138, 226, 52), (252, 233, 79), (52, 101, 164), (197, 9, 197), (52, 226, 226), (245, 245, 245), (103, 103, 103), (255, 109, 103), (95, 249, 103), (254, 251, 103), (104, 113, 255), (255, 118, 255), (95, 253, 255), (254, 255, 255))

Canonical sRGB values for the 16 ANSI colors.

The single source of truth wherever a named ANSI color, or one of the first 16 slots of the 256-color palette, must materialize as a concrete RGB value: to_css() (bright_* names are not valid CSS keywords), contrast_ratio() (luminance needs RGB), palette_to_rgb() (indices 0-15), and the palettes rendering terminal sessions in the documentation (click_extra.pygments._NAMED_COLORS and _PALETTE_256).

Values are the ones the documentation always rendered: Tango-flavored normal colors and iTerm2’s default brights, picked for legibility on both light and dark backgrounds.

Note

The 8 base named colors keep bypassing this table in markup output: _color_to_css() emits them as plain CSS keywords (and the Jira and LaTeX emitters as macro/xcolor names), delegating the exact shade to the renderer. This table only answers when a concrete value is unavoidable.

click_extra.ansi.ANSI_NAMES: tuple[str, ...] = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')

Names of the 8 base ANSI colors, in palette order.

A bright variant takes the bright_ prefix and the slot 8 places further on.

click_extra.ansi.ANSI_INDEX: dict[str, int] = {'black': 0, 'blue': 4, 'cyan': 6, 'green': 2, 'magenta': 5, 'red': 1, 'white': 7, 'yellow': 3}

Palette index of each base ANSI color name, the O(1) mirror of ANSI_NAMES for the name-to-slot lookups color resolution performs.

click_extra.ansi.ANSI_SGR_RE: Pattern[str] = re.compile('\\x1b\\[(?P<sgr_params>[0-9;]*)m')

Match a single ANSI SGR escape: \x1b[...m.

The parameter substring may be empty: \x1b[m is a valid, parameter-less full reset. It is captured as sgr_params: the lexer of click_extra.pygments composes this pattern into its own tokenizer and reads the parameters by that name.

click_extra.ansi.SGR_ATTRIBUTES: dict[int, str] = {1: 'bold', 2: 'dim', 3: 'italic', 4: 'underline', 5: 'blink', 7: 'reverse', 9: 'strikethrough', 53: 'overline'}

The SGR code switching each boolean style attribute on.

click_extra.ansi.is_rgb(color)[source]

Whether color is a 24-bit (r, g, b) triplet.

The one shape of color this module computes on: a named ANSI color, a palette index or a hex string is resolved to it first.

Return type:

TypeGuard[tuple[int, int, int]]

click_extra.ansi.hex_to_rgb(value)[source]

Parse a hex color (#rrggbb or shorthand #rgb) to an RGB tuple.

Raises:

ValueError – when value holds anything but 3 or 6 hex digits after its #. int alone would take a sign, an underscore or a non-ASCII digit, and turn #-1-1-1 into negative channels.

Return type:

tuple[int, int, int]

click_extra.ansi.rgb_to_hex(rgb)[source]

Format an (r, g, b) tuple as a #rrggbb hex string.

The inverse of hex_to_rgb(). The single source of truth for the RGB-to-hex rendering shared by __repr__, to_css, to_dict, and the documentation palette.

Return type:

str

click_extra.ansi.palette_to_rgb(idx)[source]

Convert a 256-color palette index to its sRGB tuple.

Indices 0-15 are the terminal’s system colors: terminals render them with their theme’s palette, so they resolve through the canonical ANSI_16_RGB table. Indices 16-231 (the 6x6x6 color cube) and 232-255 (the grayscale ramp) are fixed by the xterm 256-color layout.

Return type:

tuple[int, int, int]

click_extra.ansi.nearest_256(r, g, b)[source]

Map a 24-bit RGB triplet to the nearest index in the 256-color palette.

The inverse of palette_to_rgb(). Compares the Euclidean distance in RGB space against both the 6x6x6 color cube (indices 16-231) and the grayscale ramp (indices 232-255), returning whichever is closer.

Used by Style.__call__ to downsample branded-theme colors when the terminal lacks truecolor (see supports_truecolor()), and by click_extra.pygments and click_extra.cli for the same 24-bit-to-8-bit quantization.

See also

Previous implementation of full-color to 8-bit quantization.

Return type:

int

click_extra.ansi.resolve_rgb(color)[source]

Convert a color value to an (r, g, b) tuple.

Accepts RGB tuples, hex strings, named ANSI colors ("red", "bright_blue") and palette indices.

Raises:

ValueError – for any other value.

Return type:

tuple[int, int, int]

click_extra.ansi.CSI_PREFIX_PATTERN = '\\x1b\\[[0-?]*[ -/]*'

A CSI sequence short of its final byte, in ECMA-48 syntax.

ESC [, then the parameter bytes and the intermediate bytes. A stream read in chunks can end on one, the rest of the sequence still to arrive.

click_extra.ansi.CSI_PATTERN = '\\x1b\\[[0-?]*[ -/]*[@-~]'

One CSI sequence, SGR styles included.

CSI_PREFIX_PATTERN, then a final byte, like the m closing an SGR escape or the ~ closing ESC [ 200 ~.

click_extra.ansi.ANSI_ESCAPE_PATTERN = '(?:\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)?|\\x1b\\[[0-?]*[ -/]*[@-~]|\\x1b[ -/]*[0-Z\\\\^-~])'

One ANSI escape sequence of any kind, SGR styles included, in ECMA-48 syntax.

click_extra.ansi.strip_ansi(text)[source]

Remove every ANSI escape sequence from text, leaving its plain text.

Overloads:

Styles, cursor movements, screen clearing and OSC commands all go. A terminal hyperlink keeps its display text and loses its URI, which lived in the markers around it. Bytes come back as bytes.

click_extra.ansi.sgr_params(params)[source]

Decode an SGR parameter substring into a sequence of integer codes.

Per ECMA-48, an empty parameter defaults to 0: \x1b[m is a full reset, and \x1b[;31m carries a reset followed by a red foreground.

Return type:

tuple[int, ...]

click_extra.ansi.apply_sgr_codes(codes, state, *, resets=True)[source]

Apply SGR parameter codes to the mutable style state, in place.

The single source of truth for SGR semantics, shared by split_ansi() (with the default resets=True) and from_ansi() (with resets=False).

Style-setting codes add entries to state, keyed by Style field names. When resets is true, the full 0 reset clears the state, and selective resets drop their attribute: 22 (bold and dim), 23 (italic), 24 (underline), 25 (blink), 27 (reverse), 29 (strikethrough), 39 (foreground), 49 (background) and 55 (overline). When resets is false all of them are ignored, letting from_ansi() merge escapes without wiping already-parsed codes.

Unknown codes are silently skipped, like terminals do.

Return type:

None

click_extra.ansi.open_ansi(text, opened='')[source]

Return the ANSI SGR escapes still in effect at the end of text.

Escapes accumulate in the order they were read. A full 0 reset (its parameter-less \x1b[m form included) empties the accumulator, so the result re-opens what text left dangling and nothing else. Escapes come back verbatim rather than folded into a Style, which keeps a code this module does not model.

Use it to splice a styled fragment into styled text: the fragment closes with a reset of its own, which would otherwise strip the surrounding styling from everything after it. See highlight().

Parameters:
  • text (str) – the string to read the escapes from.

  • opened (str) – escapes already in effect before text, as returned by an earlier call. Feeding a string one chunk at a time then costs one pass over it, where re-reading the whole prefix per chunk costs one per chunk. A chunk must not start or end in the middle of an escape: the two halves match nothing, and the escape is lost.

Return type:

str