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
coordstuple orwidthvalue. - n: Number of tabs.
- mode:
applyto set instantly viadrawBot.tabs(),kwargsfor FormattedString(). - alignment: Alignment for intermediate tabs. Last tab is always
right. - gutter: Optional gap between tab columns, mirroring
lib.layout.makeGridsemantics. Accepts anyUnitSourcevalue ("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.positionis parsed vialayout.parseUnit(..., implicitUnit="auto"). This allows values like0.1,"20pt","60mm","10%", or bare numerics. - mode:
applyto set instantly via drawBot.tabs(),kwargsfor FormattedString().
Returns:
- If
modeisapply, callsdrawBot.tabs()with the resulting absolute positions and returns the list.- If
modeiskwargs, returns a list of(absolutePosition, alignment)tuples for use in FormattedString() kwargs.- If
tabsis 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").