monowind 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/wrap.d.ts CHANGED
@@ -1,27 +1,77 @@
1
1
  /**
2
- * Greedy word-wrap for monospace text.
2
+ * Greedy word-wrap for monospace text on the cell grid.
3
3
  *
4
- * Words are runs of non-whitespace; whitespace runs collapse to single spaces
5
- * between fitting words. Browsers also treat a hyphen inside a word as a
6
- * break opportunity (break after `-`, no hyphen added) — except before a
7
- * digit, per UAX #14 (`2026-08` doesn't break) — so words are further split
8
- * into breakable segments. A segment longer than `width` breaks at cell
9
- * boundaries. `\n` in the input is a HARD line break the wrap restarts on
10
- * a new line (the source of these is `<br>` elements, converted to `\n` by
11
- * the tree builder). A blank hard line still occupies one row.
4
+ * Text is a string plus optional per-character `advances` (cells each
5
+ * character occupies 1 by default, `1 + tracking` for letter-spaced text;
6
+ * see specs/cell-model.md). Words are runs of non-whitespace; whitespace
7
+ * runs collapse to single spaces between fitting words. Browsers also treat
8
+ * a hyphen inside a word as a break opportunity (break after `-`, no hyphen
9
+ * added) except before a digit, per UAX #14 (`2026-08` doesn't break)
10
+ * so words are further split into breakable segments. A segment wider than
11
+ * `width` breaks at cell boundaries. `\n` in the input is a HARD line break
12
+ * — the wrap restarts on a new line (the source of these is `<br>`
13
+ * elements, converted to `\n` by the tree builder). A blank hard line still
14
+ * occupies one row.
12
15
  *
13
16
  * Matches how a browser wraps `white-space: normal; overflow-wrap: anywhere`
14
17
  * text in a fixed-width monospace container — we set that in styles.css so
15
18
  * the two agree.
16
19
  */
17
- export declare function wrapLines(text: string, width: number): string[];
20
+ /** A wrapped line as an index range into the text (`end` exclusive). */
21
+ export interface LineSpan {
22
+ start: number;
23
+ end: number;
24
+ }
25
+ /**
26
+ * Wrap options: per-character `advances` for tracked text (cells each
27
+ * character occupies, `1 + tracking` of its innermost element), and the
28
+ * leaf's own `tracking` — the trailing gap it absorbs at line ends (see
29
+ * `lineAdvance`). Defaults: plain 1-cell characters, no tracking.
30
+ */
31
+ export interface WrapOptions {
32
+ advances?: number[] | undefined;
33
+ tracking?: number;
34
+ }
35
+ export declare function wrapLines(text: string, width: number, options?: WrapOptions): string[];
18
36
  /** Number of rows `text` occupies at `width` (see wrapLines). */
19
- export declare function wrapLineCount(text: string, width: number): number;
37
+ export declare function wrapLineCount(text: string, width: number, options?: WrapOptions): number;
38
+ /** Split at hard `\n` breaks only (the `white-space: nowrap` line model). */
39
+ export declare function hardLineSpans(text: string): LineSpan[];
40
+ export declare function wrapLineSpans(text: string, width: number, options?: WrapOptions): LineSpan[];
41
+ /** Cells spanned by `text[start, end)`, every character's gap included. */
42
+ export declare function advanceOf(start: number, end: number, advances?: number[]): number;
43
+ /**
44
+ * Cells `text[start, end)` occupies AS A LINE (specs/cell-model.md): up to
45
+ * `tracking` (the leaf's own tracking) cells of the last character's gap
46
+ * are trailing and don't count — the leaf's box reserves that room. A
47
+ * tracked inline element's larger gap stays counted: browsers keep it at a
48
+ * line end, and the engine doesn't cancel it (uniform across engines).
49
+ */
50
+ export declare function lineAdvance(start: number, end: number, advances?: number[], tracking?: number): number;
51
+ /** Widest unbreakable unit (breakable segment) in the text — the
52
+ * min-content width of a wrapping leaf. */
53
+ export declare function longestSegmentAdvance(text: string, options?: WrapOptions): number;
20
54
  /**
21
55
  * Split a word at its internal break opportunities: after each hyphen run,
22
56
  * unless the next character is a digit (UAX #14: no break between a hyphen
23
57
  * and a following number). `"mx-auto"` → `["mx-", "auto"]`;
24
- * `"2026-08"` → `["2026-08"]`. Also the unit of min-content width.
58
+ * `"2026-08"` → `["2026-08"]`.
25
59
  */
26
60
  export declare function breakableSegments(word: string): string[];
61
+ /** U+FFFC marks an embedded atomic inline box (see LayoutNode.inlineBox):
62
+ * unbreakable itself, but with break opportunities on BOTH sides, like
63
+ * browsers give replaced elements. */
64
+ export declare const OBJECT_REPLACEMENT = "\uFFFC";
65
+ /** U+2060 (word joiner) marks ONE CELL of inline-element horizontal
66
+ * padding in a run (specs/cell-model.md): pure blank space glued to its
67
+ * neighbors — not collapsible white space, no break opportunity — so it
68
+ * travels with the padded element's edge across wraps exactly like the
69
+ * browser's `box-decoration-break: slice` padding. Multi-cell padding is
70
+ * several 1-cell markers, keeping every gap/advance invariant intact.
71
+ * (Escape form on purpose: the character is invisible.) */
72
+ export declare const INLINE_PAD = "\u2060";
73
+ /** Visit each object-replacement marker in a run, pairing its character
74
+ * index with its ordinal (= index into the leaf's box list, which is in
75
+ * run order). */
76
+ export declare function eachObjectMarker(text: ArrayLike<string>, visit: (charIndex: number, boxIndex: number) => void): void;
27
77
  //# sourceMappingURL=wrap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wrap.d.ts","sourceRoot":"","sources":["../src/wrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAG/D;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAcxD"}
1
+ {"version":3,"file":"wrap.d.ts","sourceRoot":"","sources":["../src/wrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,wEAAwE;AACxE,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,EAAE,CAE1F;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CAE5F;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAUtD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,QAAQ,EAAE,CAahG;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAKjF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,QAAQ,SAAI,GAAG,MAAM,CAGjG;AAMD;2CAC2C;AAC3C,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CASrF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD;AAED;;sCAEsC;AACtC,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAE3C;;;;;;2DAM2D;AAC3D,eAAO,MAAM,UAAU,WAAW,CAAC;AAEnC;;iBAEiB;AACjB,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,EACvB,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GACnD,IAAI,CAON"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monowind",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Text-based user interfaces (TUI) on the web, from ordinary HTML and Tailwind utility classes",
5
5
  "keywords": [
6
6
  "ascii",
@@ -31,11 +31,12 @@
31
31
  "types": "./dist/index.d.ts",
32
32
  "default": "./dist/index.js"
33
33
  },
34
- "./styles.css": "./src/styles.css"
34
+ "./styles.css": "./src/styles.css",
35
+ "./package.json": "./package.json"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@tailwindcss/browser": "^4.3.3",
38
- "happy-dom": "^20.11.6",
39
+ "happy-dom": "^20.11.13",
39
40
  "typescript": "^7.0.2",
40
41
  "vite": "^8.2.2",
41
42
  "vitest": "^4.1.11"
package/src/styles.css CHANGED
@@ -55,24 +55,52 @@ mono-wind:not([data-mw-ready]) {
55
55
  visibility: hidden !important;
56
56
  }
57
57
 
58
- /* Lock typography on descendants so the grid metrics stay consistent —
58
+ /* Lock the font on descendants so the grid metrics stay consistent —
59
59
  * `!important` because user classes (`.text-2xl` etc.) would otherwise beat
60
60
  * these element-selector rules on specificity. */
61
61
  mono-wind * {
62
62
  font-family: inherit !important;
63
63
  font-size: inherit !important;
64
- line-height: inherit !important;
65
- letter-spacing: inherit !important;
66
64
  vertical-align: baseline !important;
67
65
  }
68
66
 
69
- /* Match the engine's greedy word-wrap: break at spaces, and break long
70
- * words at cell boundaries when they overflow. Gated on `measuring` (unlike
71
- * the font lock above) so the style reader sees the authored `white-space`
72
- * that's how `nowrap` is detected. */
67
+ /* Grid-template read assist (specs/grid.md), no-Typed-OM engines only
68
+ * (Firefox pre-157): getComputedStyle on a live grid container returns
69
+ * the USED `grid-template-*` track list (expanded px) the authored
70
+ * fr/repeat/minmax structure is gone. The style reader sets this
71
+ * attribute for the duration of the read; blockifying the element makes
72
+ * getComputedStyle return the COMPUTED value, structure intact.
73
+ * `!important` so inline `style="display: grid"` can't defeat the read. */
74
+ mono-wind[measuring] [data-mw-degrid] {
75
+ display: block !important;
76
+ }
77
+
78
+ /* Grid-quantized typography and the wrap model (specs/cell-model.md). Gated
79
+ * on `measuring` (unlike the font lock above) so the style reader sees the
80
+ * AUTHORED white-space / letter-spacing / line-height — that's how nowrap,
81
+ * tracking, and leading are detected without echoing our own output.
82
+ *
83
+ * letter-spacing: the root's own (`--mw-rls`, part of the cell) plus
84
+ * `--mw-ls` extra cells per character, which the engine writes on every
85
+ * element it visits; an element without the var makes this declaration
86
+ * invalid at computed-value time, so the property falls back to inheriting
87
+ * the parent's grid-exact value. line-height is inherited from the
88
+ * laid-out ancestor (which sets the row multiple below) — `leading-*` on
89
+ * inline elements is ignored by design. */
73
90
  mono-wind:not([measuring]) * {
74
91
  white-space: normal !important;
75
92
  overflow-wrap: anywhere !important;
93
+ letter-spacing: calc(var(--mw-rls, 0px) + var(--mw-ls) * var(--mw-cw, 1px)) !important;
94
+ line-height: inherit !important;
95
+ /* Inline-element horizontal padding, quantized: the engine writes
96
+ * `--mw-ipl`/`--mw-ipr` (whole cells) on inline elements whose authored
97
+ * padding it reserved in the run; everything else gets 0, so raw
98
+ * off-grid padding never renders. Laid-out elements and atomic inline
99
+ * boxes override this with their own engine-owned padding rule (higher
100
+ * specificity). Vertical inline padding never moves layout, per CSS,
101
+ * and passes through untouched. */
102
+ padding-left: calc(var(--mw-ipl, 0) * var(--mw-cw, 1px)) !important;
103
+ padding-right: calc(var(--mw-ipr, 0) * var(--mw-cw, 1px)) !important;
76
104
  }
77
105
 
78
106
  /* Author-requested `white-space: nowrap` (or `pre`) — the engine sized this
@@ -82,6 +110,16 @@ mono-wind:not([measuring]) [data-mw-nowrap] {
82
110
  white-space: nowrap !important;
83
111
  }
84
112
 
113
+ /* `white-space: pre` leaves (specs/cell-model.md): the tree builder kept
114
+ * the source's spaces and newlines in the run, so the browser must render
115
+ * them too — on the leaf AND its inline descendants (the lock above would
116
+ * collapse text inside nested spans). Later than the nowrap rule, so it
117
+ * wins on the leaf itself at equal specificity. */
118
+ mono-wind:not([measuring]) [data-mw-pre],
119
+ mono-wind:not([measuring]) [data-mw-pre] * {
120
+ white-space: pre !important;
121
+ }
122
+
85
123
  /* Truncation (`truncate` = nowrap + clip + text-overflow: ellipsis): use
86
124
  * `hidden` rather than the normalized `clip` for this combo — some engines
87
125
  * only apply `text-overflow` to scroll containers, which `clip` boxes are
@@ -113,10 +151,52 @@ mono-wind:not([measuring]) [data-mw-nowrap][data-mw-clip] {
113
151
  * position:absolute (out of flow), so nothing consumes them anyway. */
114
152
  mono-wind:not([measuring]) [data-mw-laid-out] {
115
153
  position: absolute !important;
116
- box-sizing: border-box !important;
117
154
  left: calc(var(--mw-x, 0) * var(--mw-cw, 1px)) !important;
118
155
  top: calc(var(--mw-y, 0) * var(--mw-ch, 1px)) !important;
119
- width: calc(var(--mw-w, 0) * var(--mw-cw, 1px)) !important;
156
+ /* Neutralize the browser's residual grid machinery (specs/grid.md).
157
+ * grid-area: per CSS §10.1 an ABSOLUTE child of a grid container with
158
+ * placement properties uses its grid area as containing block — our
159
+ * left/top would resolve against that area and double-offset the
160
+ * element (the engine already placed it). The template and content/item
161
+ * alignment resets make a laid-out container's own grid/flex inert for
162
+ * the one box the browser still lays out: the anonymous item holding a
163
+ * leaf's direct text (a template would trap it in track 1; the
164
+ * browser's place-* offsets are FRACTIONAL, off the cell grid — the
165
+ * engine applies the quantized offsets through its owned padding
166
+ * instead, specs/cell-model.md). */
167
+ grid-area: auto !important;
168
+ grid-template-columns: none !important;
169
+ grid-template-rows: none !important;
170
+ place-content: start !important;
171
+ place-items: stretch !important;
172
+ /* `leading-*` on the grid: `--mw-lh` rows per wrapped line. CSS puts half
173
+ * the extra leading ABOVE the first line; `--mw-lhs` (−(rows − 1)/2)
174
+ * shifts the box back so every line starts on its own row. */
175
+ translate: 0 calc(var(--mw-lhs, 0) * var(--mw-ch, 1px)) !important;
176
+ }
177
+
178
+ /* Shared cell geometry for engine-sized boxes — both engine-POSITIONED
179
+ * elements and in-flow atomic inline boxes (below). */
180
+ mono-wind:not([measuring]) [data-mw-laid-out],
181
+ mono-wind:not([measuring]) [data-mw-inline-box] {
182
+ box-sizing: border-box !important;
183
+ /* Trailing-gap allowance: a tracked box's lines end with a phantom
184
+ * `--mw-ls`-cell gap the engine doesn't count. Give the content box that
185
+ * room by carving it out of the engine-owned right padding/border cells;
186
+ * only when those are fewer than the gap does the element box itself
187
+ * widen (invisibly — the decorated border stays where the engine put it).
188
+ * The extra 1/32px is one layout unit of headroom: engines store lengths
189
+ * on a fixed grid by flooring (1/64px in Chromium and WebKit, 1/60px in
190
+ * Firefox), so `cells × cw` can land one unit BELOW the shaped advance of
191
+ * a line that fits exactly, and the browser would wrap it. Adding at
192
+ * least one unit before the floor guarantees the stored width is ≥ the
193
+ * exact product; 1/32px covers every engine's unit and is far below a
194
+ * cell (and below a device pixel), so it changes neither cell math nor
195
+ * paint. */
196
+ width: calc(
197
+ (var(--mw-w, 0) + max(0, var(--mw-ls, 0) - var(--mw-pr, 0) - var(--mw-br, 0))) *
198
+ var(--mw-cw, 1px) + 0.03125px
199
+ ) !important;
120
200
  height: calc(var(--mw-h, 0) * var(--mw-ch, 1px)) !important;
121
201
  min-width: 0 !important;
122
202
  min-height: 0 !important;
@@ -124,10 +204,26 @@ mono-wind:not([measuring]) [data-mw-laid-out] {
124
204
  max-height: none !important;
125
205
  border-width: 0 !important;
126
206
  padding-top: calc((var(--mw-pt, 0) + var(--mw-bt, 0)) * var(--mw-ch, 1px)) !important;
127
- padding-right: calc((var(--mw-pr, 0) + var(--mw-br, 0)) * var(--mw-cw, 1px)) !important;
207
+ padding-right: calc(
208
+ max(0, var(--mw-pr, 0) + var(--mw-br, 0) - var(--mw-ls, 0)) * var(--mw-cw, 1px)
209
+ ) !important;
128
210
  padding-bottom: calc((var(--mw-pb, 0) + var(--mw-bb, 0)) * var(--mw-ch, 1px)) !important;
129
211
  padding-left: calc((var(--mw-pl, 0) + var(--mw-bl, 0)) * var(--mw-cw, 1px)) !important;
130
212
  margin: 0 !important;
213
+ line-height: calc(var(--mw-lh, 1) * var(--mw-ch, 1px)) !important;
214
+ }
215
+
216
+ /* Atomic inline boxes (`inline-flex`/`inline-block`/`inline-grid` riding a
217
+ * text run, specs/cell-model.md): the box stays IN FLOW — the browser's
218
+ * own line layout places it, and both models agree because the engine
219
+ * sizes it to exactly the cells its U+FFFC marker occupies in the wrap.
220
+ * `position: relative` makes it the containing block for its laid-out
221
+ * children; `vertical-align: top` pins the box's top to its line's first
222
+ * row (the global baseline lock would hang an abspos-only box below the
223
+ * line) — a box taller than one row grows its line, per CSS. */
224
+ mono-wind:not([measuring]) [data-mw-inline-box] {
225
+ position: relative !important;
226
+ vertical-align: top !important;
131
227
  }
132
228
 
133
229
  /* Author-requested `overflow: hidden` or `overflow: clip` — both keep
@@ -137,6 +233,37 @@ mono-wind [data-mw-clip] {
137
233
  overflow: clip !important;
138
234
  }
139
235
 
236
+ /* Inline (relative/sticky) elements with authored relative insets
237
+ * (specs/positioning.md): the engine writes the cell-rounded offsets into
238
+ * `--mw-it/ir/ib/il` and this rule applies them on the grid. A side
239
+ * without its var is invalid at computed-value time and falls back to
240
+ * `auto` — so authored subsets work and the author's raw off-grid values
241
+ * never apply. Gated on `measuring` so the style reader sees the authored
242
+ * values (no echo loop). Also pins `position: relative` — sticky behaves
243
+ * as relative until the scrolling milestone. */
244
+ mono-wind:not([measuring]) [data-mw-inline-inset] {
245
+ position: relative !important;
246
+ top: calc(var(--mw-it) * var(--mw-ch, 1px)) !important;
247
+ right: calc(var(--mw-ir) * var(--mw-cw, 1px)) !important;
248
+ bottom: calc(var(--mw-ib) * var(--mw-ch, 1px)) !important;
249
+ left: calc(var(--mw-il) * var(--mw-cw, 1px)) !important;
250
+ }
251
+
252
+ /* A container whose direct text was dropped (mixed text + block children,
253
+ * cell-model deviation): the browser would paint that text unpositioned
254
+ * over the laid-out children, so hide it. `visibility` is inherited AND
255
+ * resettable, so the container's laid-out children come back — only in
256
+ * this warned state, which is why the reset may also override an authored
257
+ * `invisible` utility there. */
258
+ mono-wind:not([measuring])[data-mw-dropped-text],
259
+ mono-wind:not([measuring]) [data-mw-dropped-text] {
260
+ visibility: hidden !important;
261
+ }
262
+ mono-wind:not([measuring])[data-mw-dropped-text] > [data-mw-laid-out],
263
+ mono-wind:not([measuring]) [data-mw-dropped-text] > [data-mw-laid-out] {
264
+ visibility: visible !important;
265
+ }
266
+
140
267
  /* Force off-grid text alignments back to `start`. `text-left`/`right`/`start`/
141
268
  * `end` all land on whole cells (offset = (W - N) × cell width); `center` and
142
269
  * `justify` produce fractional per-line offsets we can't snap to the grid. */