lib.text

  1from typing import TypeAlias, get_args, Literal
  2import drawBot
  3import string
  4
  5from lib import fonts, layout
  6
  7MIXED_CHARS: str = "".join(
  8    [u + l for u, l in zip(string.ascii_uppercase, string.ascii_lowercase)]
  9)
 10"""Interleaved uppercase and lowercase characters, i.e. `AaBbCc...Zz`"""
 11
 12
 13def wrap(txt: str) -> str:
 14    """
 15    Wrap text in zero-width spaces for eager line breaking.
 16
 17    Args:
 18        txt: The text to wrap.
 19    """
 20    return "\u200b".join(txt)
 21
 22
 23TabAlignment: TypeAlias = fonts.TextAlign | str
 24UnitTab: TypeAlias = tuple[layout.UnitSource, TabAlignment]
 25AbsoluteTab: TypeAlias = tuple[float, TabAlignment]
 26
 27
 28def makeTabsJustified(
 29    container: tuple | int | float,
 30    n: int = 1,
 31    mode: Literal["apply", "kwargs"] = "apply",
 32    alignment: fonts.TextAlign = "center",
 33    gutter: layout.UnitSource = None,
 34) -> list[AbsoluteTab]:
 35    """
 36    Create proportional tab stops across a container width.
 37
 38    Args:
 39        container: Accepts `coords` tuple or `width` value.
 40        n: Number of tabs.
 41        mode: `apply` to set instantly via `drawBot.tabs()`, `kwargs` for FormattedString().
 42        alignment: Alignment for intermediate tabs. Last tab is always `right`.
 43        gutter: Optional gap between tab columns, mirroring `lib.layout.makeGrid` semantics.
 44            Accepts any `UnitSource` value (`"20pt"`, `"10mm"`, etc.).
 45
 46    Returns:
 47        List of absolute tab tuples `(position, alignment)`.
 48    """
 49    width = layout.toWidth(container)
 50    gutterPt = layout.parseUnit(gutter) if gutter is not None else 0
 51    advanceW = (width - gutterPt * (n - 1)) / n
 52    result: list[AbsoluteTab] = []
 53
 54    for tab in range(1, n + 1):
 55        isTabLast = tab == n
 56        tabAlign = alignment if not isTabLast else "right"
 57        # Non-last tabs land on the left edge of the next column (includes trailing gutter).
 58        # Last tab lands on the right edge of the final column (no trailing gutter).
 59        gutterAccum = gutterPt * (tab - 1) if isTabLast else gutterPt * tab
 60        result.append((tab * advanceW + gutterAccum, tabAlign))
 61
 62    if mode == "apply":
 63        drawBot.tabs(*result)
 64
 65    return result
 66
 67
 68def makeTabs(
 69    container: tuple | int | float,
 70    tabs: list[UnitTab],
 71    mode: Literal["apply", "kwargs"] = "apply",
 72) -> list[AbsoluteTab] | None:
 73    """
 74    Convert mixed tab positions to absolute positions.
 75
 76    Args:
 77        container: Container coordinates or width.
 78        tabs: List of `(position, alignment)` tuples.
 79            `position` is parsed via `layout.parseUnit(..., implicitUnit="auto")`.
 80            This allows values like `0.1`, `"20pt"`, `"60mm"`, `"10%"`, or bare numerics.
 81        mode: `apply` to set instantly via drawBot.tabs(), `kwargs` for FormattedString().
 82
 83    Returns:
 84        - If `mode` is `apply`, calls `drawBot.tabs()` with the resulting absolute positions and returns the list.
 85        - If `mode` is `kwargs`, returns a list of `(absolutePosition, alignment)` tuples for use in FormattedString() kwargs.
 86        - If `tabs` is empty or None, returns None to allow FormattedString() to use default tab stops.
 87
 88    Notes:
 89        - Uses width-only semantics (same as `makeTabsJustified`), so container x-offset is ignored.
 90        - Values in `[0, 1]` and `%` strings are interpreted as relative tab positions.
 91        - Other inputs are absolute positions parsed by `layout.parseUnit(..., implicitUnit="auto")`.
 92    """
 93    width = layout.toWidth(container)
 94
 95    allowedAlignments = get_args(fonts.TextAlign)
 96    result: list[AbsoluteTab] = []
 97
 98    if not tabs:
 99        return None
100
101    for position, alignment in tabs:
102        isRelativeNumeric = (
103            isinstance(position, (int, float)) and 0 <= float(position) <= 1
104        )
105        isRelativePercent = isinstance(position, str) and position.endswith("%")
106        isRelativeAuto = layout.isAutoRelative(position, "auto")
107
108        if isRelativeNumeric:
109            absolutePosition = float(width) * float(position)
110        else:
111            parsedPosition = layout.parseUnit(position, implicitUnit="auto")
112
113            if parsedPosition is None:
114                raise TypeError("Tab position cannot be None.")
115
116            absolutePosition = (
117                float(width) * float(parsedPosition)
118                if (isRelativePercent or isRelativeAuto)
119                else float(parsedPosition)
120            )
121
122        isKeywordAlignment = alignment in allowedAlignments
123        isCharacterAlignment = isinstance(alignment, str) and len(alignment) == 1
124
125        if not (isKeywordAlignment or isCharacterAlignment):
126            raise ValueError(
127                f"Invalid tab alignment `{alignment}`. Expected one of {allowedAlignments} or a single character."
128            )
129
130        result.append((absolutePosition, alignment))
131
132    if mode == "apply":
133        drawBot.tabs(*result)
134
135    return result
MIXED_CHARS: str = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

Interleaved uppercase and lowercase characters, i.e. AaBbCc...Zz

def wrap(txt: str) -> str:
14def wrap(txt: str) -> str:
15    """
16    Wrap text in zero-width spaces for eager line breaking.
17
18    Args:
19        txt: The text to wrap.
20    """
21    return "\u200b".join(txt)

Wrap text in zero-width spaces for eager line breaking.

Arguments:
  • txt: The text to wrap.
TabAlignment: TypeAlias = Union[Literal['left', 'center', 'right'], str]
UnitTab: TypeAlias = tuple[int | float | str | None, typing.Union[typing.Literal['left', 'center', 'right'], str]]
AbsoluteTab: TypeAlias = tuple[float, typing.Union[typing.Literal['left', 'center', 'right'], str]]
def makeTabsJustified( container: tuple | int | float, n: int = 1, mode: Literal['apply', 'kwargs'] = 'apply', alignment: Literal['left', 'center', 'right'] = 'center', gutter: int | float | str | None = None) -> list[tuple[float, typing.Union[typing.Literal['left', 'center', 'right'], str]]]:
29def makeTabsJustified(
30    container: tuple | int | float,
31    n: int = 1,
32    mode: Literal["apply", "kwargs"] = "apply",
33    alignment: fonts.TextAlign = "center",
34    gutter: layout.UnitSource = None,
35) -> list[AbsoluteTab]:
36    """
37    Create proportional tab stops across a container width.
38
39    Args:
40        container: Accepts `coords` tuple or `width` value.
41        n: Number of tabs.
42        mode: `apply` to set instantly via `drawBot.tabs()`, `kwargs` for FormattedString().
43        alignment: Alignment for intermediate tabs. Last tab is always `right`.
44        gutter: Optional gap between tab columns, mirroring `lib.layout.makeGrid` semantics.
45            Accepts any `UnitSource` value (`"20pt"`, `"10mm"`, etc.).
46
47    Returns:
48        List of absolute tab tuples `(position, alignment)`.
49    """
50    width = layout.toWidth(container)
51    gutterPt = layout.parseUnit(gutter) if gutter is not None else 0
52    advanceW = (width - gutterPt * (n - 1)) / n
53    result: list[AbsoluteTab] = []
54
55    for tab in range(1, n + 1):
56        isTabLast = tab == n
57        tabAlign = alignment if not isTabLast else "right"
58        # Non-last tabs land on the left edge of the next column (includes trailing gutter).
59        # Last tab lands on the right edge of the final column (no trailing gutter).
60        gutterAccum = gutterPt * (tab - 1) if isTabLast else gutterPt * tab
61        result.append((tab * advanceW + gutterAccum, tabAlign))
62
63    if mode == "apply":
64        drawBot.tabs(*result)
65
66    return result

Create proportional tab stops across a container width.

Arguments:
  • container: Accepts coords tuple or width value.
  • n: Number of tabs.
  • mode: apply to set instantly via drawBot.tabs(), kwargs for FormattedString().
  • alignment: Alignment for intermediate tabs. Last tab is always right.
  • gutter: Optional gap between tab columns, mirroring lib.layout.makeGrid semantics. Accepts any UnitSource value ("20pt", "10mm", etc.).
Returns:

List of absolute tab tuples (position, alignment).

def makeTabs( container: tuple | int | float, tabs: list[tuple[int | float | str | None, typing.Union[typing.Literal['left', 'center', 'right'], str]]], mode: Literal['apply', 'kwargs'] = 'apply') -> list[tuple[float, typing.Union[typing.Literal['left', 'center', 'right'], str]]] | None:
 69def makeTabs(
 70    container: tuple | int | float,
 71    tabs: list[UnitTab],
 72    mode: Literal["apply", "kwargs"] = "apply",
 73) -> list[AbsoluteTab] | None:
 74    """
 75    Convert mixed tab positions to absolute positions.
 76
 77    Args:
 78        container: Container coordinates or width.
 79        tabs: List of `(position, alignment)` tuples.
 80            `position` is parsed via `layout.parseUnit(..., implicitUnit="auto")`.
 81            This allows values like `0.1`, `"20pt"`, `"60mm"`, `"10%"`, or bare numerics.
 82        mode: `apply` to set instantly via drawBot.tabs(), `kwargs` for FormattedString().
 83
 84    Returns:
 85        - If `mode` is `apply`, calls `drawBot.tabs()` with the resulting absolute positions and returns the list.
 86        - If `mode` is `kwargs`, returns a list of `(absolutePosition, alignment)` tuples for use in FormattedString() kwargs.
 87        - If `tabs` is empty or None, returns None to allow FormattedString() to use default tab stops.
 88
 89    Notes:
 90        - Uses width-only semantics (same as `makeTabsJustified`), so container x-offset is ignored.
 91        - Values in `[0, 1]` and `%` strings are interpreted as relative tab positions.
 92        - Other inputs are absolute positions parsed by `layout.parseUnit(..., implicitUnit="auto")`.
 93    """
 94    width = layout.toWidth(container)
 95
 96    allowedAlignments = get_args(fonts.TextAlign)
 97    result: list[AbsoluteTab] = []
 98
 99    if not tabs:
100        return None
101
102    for position, alignment in tabs:
103        isRelativeNumeric = (
104            isinstance(position, (int, float)) and 0 <= float(position) <= 1
105        )
106        isRelativePercent = isinstance(position, str) and position.endswith("%")
107        isRelativeAuto = layout.isAutoRelative(position, "auto")
108
109        if isRelativeNumeric:
110            absolutePosition = float(width) * float(position)
111        else:
112            parsedPosition = layout.parseUnit(position, implicitUnit="auto")
113
114            if parsedPosition is None:
115                raise TypeError("Tab position cannot be None.")
116
117            absolutePosition = (
118                float(width) * float(parsedPosition)
119                if (isRelativePercent or isRelativeAuto)
120                else float(parsedPosition)
121            )
122
123        isKeywordAlignment = alignment in allowedAlignments
124        isCharacterAlignment = isinstance(alignment, str) and len(alignment) == 1
125
126        if not (isKeywordAlignment or isCharacterAlignment):
127            raise ValueError(
128                f"Invalid tab alignment `{alignment}`. Expected one of {allowedAlignments} or a single character."
129            )
130
131        result.append((absolutePosition, alignment))
132
133    if mode == "apply":
134        drawBot.tabs(*result)
135
136    return result

Convert mixed tab positions to absolute positions.

Arguments:
  • container: Container coordinates or width.
  • tabs: List of (position, alignment) tuples. position is parsed via layout.parseUnit(..., implicitUnit="auto"). This allows values like 0.1, "20pt", "60mm", "10%", or bare numerics.
  • mode: apply to set instantly via drawBot.tabs(), kwargs for FormattedString().
Returns:
  • If mode is apply, calls drawBot.tabs() with the resulting absolute positions and returns the list.
  • If mode is kwargs, returns a list of (absolutePosition, alignment) tuples for use in FormattedString() kwargs.
  • If tabs is empty or None, returns None to allow FormattedString() to use default tab stops.
Notes:
  • Uses width-only semantics (same as makeTabsJustified), so container x-offset is ignored.
  • Values in [0, 1] and % strings are interpreted as relative tab positions.
  • Other inputs are absolute positions parsed by layout.parseUnit(..., implicitUnit="auto").