@mark1russell7/typeset 0.0.0-bootstrap.0 → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
1
  # @mark1russell7/typeset
2
2
 
3
- This version is a placeholder. The release workflow of https://github.com/mark1russell7/typeset publishes the real versions. Use a later version.
3
+ This package measures text and breaks it into lines. Each area has its own subpath.
4
+
5
+ | Subpath | What it has |
6
+ | --- | --- |
7
+ | `@mark1russell7/typeset/breaking` | The paragraph model of boxes, glue and penalties, and the greedy, total fit and balanced line breakers. |
8
+
9
+ ## Line breaking
10
+
11
+ ```ts
12
+ import { greedy, knuthPlass, linesOf, paragraphOf } from "@mark1russell7/typeset/breaking";
13
+
14
+ const widthOf = (text: string): number => 8 * [...text].length;
15
+ const paragraph = paragraphOf("The quick brown fox jumps over the lazy dog", { widthOf });
16
+ const lines = linesOf(paragraph, knuthPlass.breaks(paragraph, 120));
17
+ ```
18
+
19
+ - Each breaker takes any width function. The function gives the width of a run of text in any unit.
20
+ - `greedy` fills each line as a browser does. `knuthPlass` chooses the breaks with the least sum of demerits. `balanced` keeps the greedy line count with more equal lines.
21
+ - The option `hyphenate` gives the offsets of the hyphen breaks of a word.
22
+
23
+ The repository [mark1russell7/typeset](https://github.com/mark1russell7/typeset) has the report of the breakers in `docs/BREAKING.md`.
@@ -0,0 +1,2 @@
1
+ import { C as lineWidth, S as lineStart, T as paragraphOf, _ as Paragraph, a as Breaker, b as WidthOf, c as balancedBreaker, d as knuthPlass, f as totalFit, g as Line, h as Item, i as BalancedOptions, l as breakers, m as Glue, n as raggedness, o as TotalFitOptions, p as Box, r as spread, s as balanced, u as greedy, v as ParagraphOptions, w as linesOf, x as isBreak, y as Penalty } from "./chunks/breaking.js";
2
+ export { type BalancedOptions, type Box, type Breaker, type Glue, type Item, type Line, type Paragraph, type ParagraphOptions, type Penalty, type TotalFitOptions, type WidthOf, balanced, balancedBreaker, breakers, greedy, isBreak, knuthPlass, lineStart, lineWidth, linesOf, paragraphOf, raggedness, spread, totalFit };
@@ -0,0 +1 @@
1
+ export { i as balanced, a as balancedBreaker, o as breakers, s as greedy, u as isBreak, c as knuthPlass, d as lineStart, f as lineWidth, p as linesOf, m as paragraphOf, n as raggedness, r as spread, l as totalFit } from "./chunks/breaking.js";
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The paragraph model of the breakers: a list of boxes, glue and penalties, as Knuth and Plass define it. A builder makes
3
+ * the items of a text. The breakers read only the items, so a new rule of break opportunities is a new builder.
4
+ */
5
+ /** A width function: the width of a run of text, in any unit. The breakers add the widths and compare them. */
6
+ type WidthOf = (text: string) => number;
7
+ /** A box: text that the breaker does not break. */
8
+ interface Box {
9
+ readonly type: "box";
10
+ readonly text: string;
11
+ readonly width: number;
12
+ }
13
+ /** Glue: a space between two boxes. A line can end at glue that comes after a box. The line then does not show the glue. */
14
+ interface Glue {
15
+ readonly type: "glue";
16
+ readonly text: string;
17
+ readonly width: number;
18
+ }
19
+ /**
20
+ * A penalty: a break opportunity with a cost. A line that ends at the penalty shows its text, for example a hyphen. A cost of
21
+ * `Infinity` forbids the break, and a cost of `-Infinity` forces it.
22
+ */
23
+ interface Penalty {
24
+ readonly type: "penalty";
25
+ readonly text: string;
26
+ readonly width: number;
27
+ readonly cost: number;
28
+ /** A flagged penalty is a hyphen. Total fit adds a cost to two flagged breaks in sequence. */
29
+ readonly flagged: boolean;
30
+ }
31
+ /** An item of a paragraph. */
32
+ type Item = Box | Glue | Penalty;
33
+ /** A paragraph: the items in the order of the text. */
34
+ interface Paragraph {
35
+ readonly items: readonly Item[];
36
+ }
37
+ /** The options of `paragraphOf`. */
38
+ interface ParagraphOptions {
39
+ /** The width function of the boxes, the spaces and the hyphens. */
40
+ readonly widthOf: WidthOf;
41
+ /**
42
+ * The hyphenation hook. It gives the offsets in a word where a break with a hyphen is possible. The default gives no
43
+ * offset. The width of each part of a word comes from `widthOf`, so the kerning across an offset is not in the sum.
44
+ */
45
+ readonly hyphenate?: (word: string) => readonly number[];
46
+ /** The text that a line shows at a hyphen break. The default is "-". */
47
+ readonly hyphen?: string;
48
+ /** The cost of a hyphen break. The default is 50, as in TeX. */
49
+ readonly hyphenCost?: number;
50
+ }
51
+ /**
52
+ * This function makes the paragraph of a text with the break opportunities of CSS `white-space: normal`. Each run of white
53
+ * space becomes one glue of the width of a space. Each word between two runs becomes a box, or boxes and hyphen penalties.
54
+ */
55
+ declare function paragraphOf(text: string, options: ParagraphOptions): Paragraph;
56
+ /** A line of a broken paragraph. */
57
+ interface Line {
58
+ readonly text: string;
59
+ /** The width of the line: its boxes, the glue between them and the text of a penalty at its end. */
60
+ readonly width: number;
61
+ /** The index of the first item of the line. */
62
+ readonly start: number;
63
+ /** The index after the last item of the line. A break at glue or at a penalty has the index of that item. */
64
+ readonly end: number;
65
+ }
66
+ /** This function tells if a line can end at the item `index`: glue after a box, or a penalty with a cost below `Infinity`. */
67
+ declare function isBreak(items: readonly Item[], index: number): boolean;
68
+ /** This function gives the index of the first item of a line after a break at `index`. It skips the glue and the penalties. */
69
+ declare function lineStart(items: readonly Item[], index: number): number;
70
+ /** This function gives the width of the line from the item `start` to a break at `end`, with the text of a penalty at `end`. */
71
+ declare function lineWidth(items: readonly Item[], start: number, end: number): number;
72
+ /**
73
+ * This function gives the lines of a paragraph from the indices of its breaks. Each break is the index of glue or of a penalty.
74
+ * The last line ends at the end of the items.
75
+ */
76
+ declare function linesOf(paragraph: Paragraph, breaks: readonly number[]): Line[];
77
+ /**
78
+ * A breaker: a strategy that chooses the breaks of a paragraph for the line width `width`. It gives the indices of the
79
+ * items where the lines end, in order. A breaker reads only the items, so any width function and any builder work with it.
80
+ */
81
+ interface Breaker {
82
+ readonly name: string;
83
+ breaks(paragraph: Paragraph, width: number): number[];
84
+ }
85
+ /**
86
+ * The greedy breaker: first fit, as a browser with `text-wrap: wrap`. Each line takes the items while they fit in the width.
87
+ * A box that is wider than a line stays whole on its own line.
88
+ */
89
+ declare const greedy: Breaker;
90
+ /** The options of the total fit breaker. */
91
+ interface TotalFitOptions {
92
+ /** The cost of each line, as `\linepenalty` of TeX. A larger value gives fewer lines. The default is 10. */
93
+ readonly linePenalty?: number;
94
+ /** The cost of two hyphen breaks in sequence. The default is 3000, as `\doublehyphendemerits` of TeX. */
95
+ readonly flaggedCost?: number;
96
+ }
97
+ /**
98
+ * This function gives the total fit breaker of Knuth and Plass for text with a ragged right edge. It chooses the breaks with
99
+ * the least sum of demerits over all the lines. The demerits of a line are `(linePenalty + 100 r²)²` plus the square of the
100
+ * cost of its penalty. The ratio `r` is the free space of the line divided by the width. The last line has `r = 0`.
101
+ */
102
+ declare function totalFit(options?: TotalFitOptions): Breaker;
103
+ /** The total fit breaker with the default options. */
104
+ declare const knuthPlass: Breaker;
105
+ /** The options of the balanced breaker. */
106
+ interface BalancedOptions {
107
+ /** The steps of the search for the width. The default is 32. */
108
+ readonly steps?: number;
109
+ }
110
+ /**
111
+ * This function gives the balanced breaker, as a browser with `text-wrap: balance`. It keeps the line count of the greedy
112
+ * breaker, and it finds the least width that gives that count. Thus the lines have more equal widths.
113
+ */
114
+ declare function balancedBreaker(options?: BalancedOptions): Breaker;
115
+ /** The balanced breaker with the default options. */
116
+ declare const balanced: Breaker;
117
+ /** The breakers of the package, by name. */
118
+ declare const breakers: Readonly<Record<"greedy" | "total-fit" | "balanced", Breaker>>;
119
+ /**
120
+ * This function gives the raggedness of the lines for the width `width`: the mean of `((width - line) / width)²` over the
121
+ * lines before the last line. It is 0 for one line. `@mark1russell7/layout-contract/text` uses the same measure.
122
+ */
123
+ declare function raggedness(lines: readonly Line[], width: number): number;
124
+ /** This function gives the spread of the lines: the width of the widest line less the width of the narrowest line, the last line included. */
125
+ declare function spread(lines: readonly Line[]): number;
126
+ declare namespace breaking_d_exports {
127
+ export { BalancedOptions, Box, Breaker, Glue, Item, Line, Paragraph, ParagraphOptions, Penalty, TotalFitOptions, WidthOf, balanced, balancedBreaker, breakers, greedy, isBreak, knuthPlass, lineStart, lineWidth, linesOf, paragraphOf, raggedness, spread, totalFit };
128
+ }
129
+ export { lineWidth as C, lineStart as S, paragraphOf as T, Paragraph as _, Breaker as a, WidthOf as b, balancedBreaker as c, knuthPlass as d, totalFit as f, Line as g, Item as h, BalancedOptions as i, breakers as l, Glue as m, raggedness as n, TotalFitOptions as o, Box as p, spread as r, balanced as s, breaking_d_exports as t, greedy as u, ParagraphOptions as v, linesOf as w, isBreak as x, Penalty as y };
130
+ //# sourceMappingURL=breaking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breaking.d.ts","names":[],"sources":["../../src/breaking/paragraph.ts","../../src/breaking/breakers.ts","../../src/breaking/measures.ts","../../src/breaking.ts"],"mappings":";;;;;KAMY,WAAW;;UAGN;WACN;WACA;WACA;;;UAIM;WACN;WACA;WACA;;;;;;UAOM;WACN;WACA;WACA;WACA;;WAEA;;;KAIC,OAAO,MAAM,OAAO;;UAGf;WACN,gBAAgB;;;UAIV;;WAEN,SAAS;;;;;WAKT,aAAa;;WAEb;;WAEA;;;;;;iBASK,YAAY,cAAc,SAAS,mBAAmB;;UAyBrD;WACN;;WAEA;;WAEA;;WAEA;;;iBAIK,QAAQ,gBAAgB,QAAQ;;iBAOhC,UAAU,gBAAgB,QAAQ;;iBAOlC,UAAU,gBAAgB,QAAQ,eAAe;;;;;iBAcjD,QAAQ,WAAW,WAAW,4BAA4B;;;;;UC1HzD;WACN;EACT,OAAO,WAAW,WAAW;;;;;;cAOlB,QAAQ;;UAsCJ;;WAEN;;WAEA;;;;;;;iBAQK,SAAS,UAAS,kBAAuB;;cAU5C,YAAY;;UAsDR;;WAEN;;;;;;iBAOK,gBAAgB,UAAS,kBAAuB;;cAmBnD,UAAU;;cAGV,UAAU,SAAS,4CAA4C;;;;;iBC1J5D,WAAW,gBAAgB,QAAQ;;iBAWnC,OAAO,gBAAgB"}
@@ -0,0 +1,267 @@
1
+ import { t as __exportAll } from "./rolldown-runtime.js";
2
+ // src/breaking/paragraph.ts
3
+ const whiteSpace = /\s+/u;
4
+ /**
5
+ * This function makes the paragraph of a text with the break opportunities of CSS `white-space: normal`. Each run of white
6
+ * space becomes one glue of the width of a space. Each word between two runs becomes a box, or boxes and hyphen penalties.
7
+ */
8
+ function paragraphOf(text, options) {
9
+ const { widthOf } = options;
10
+ const hyphen = options.hyphen ?? "-";
11
+ const hyphenCost = options.hyphenCost ?? 50;
12
+ const space = widthOf(" ");
13
+ const hyphenWidth = options.hyphenate === void 0 ? 0 : widthOf(hyphen);
14
+ const items = [];
15
+ for (const word of text.split(whiteSpace)) {
16
+ if (word === "") continue;
17
+ if (items.length > 0) items.push({
18
+ type: "glue",
19
+ text: " ",
20
+ width: space
21
+ });
22
+ const offsets = options.hyphenate === void 0 ? [] : [...new Set(options.hyphenate(word))].filter((o) => o > 0 && o < word.length).sort((a, b) => a - b);
23
+ let start = 0;
24
+ for (const offset of offsets) {
25
+ const part = word.slice(start, offset);
26
+ items.push({
27
+ type: "box",
28
+ text: part,
29
+ width: widthOf(part)
30
+ });
31
+ items.push({
32
+ type: "penalty",
33
+ text: hyphen,
34
+ width: hyphenWidth,
35
+ cost: hyphenCost,
36
+ flagged: true
37
+ });
38
+ start = offset;
39
+ }
40
+ const last = word.slice(start);
41
+ items.push({
42
+ type: "box",
43
+ text: last,
44
+ width: widthOf(last)
45
+ });
46
+ }
47
+ return { items };
48
+ }
49
+ /** This function tells if a line can end at the item `index`: glue after a box, or a penalty with a cost below `Infinity`. */
50
+ function isBreak(items, index) {
51
+ const item = items[index];
52
+ if (item.type === "glue") return index > 0 && items[index - 1].type === "box";
53
+ return item.type === "penalty" && item.cost < Infinity;
54
+ }
55
+ /** This function gives the index of the first item of a line after a break at `index`. It skips the glue and the penalties. */
56
+ function lineStart(items, index) {
57
+ let i = index + 1;
58
+ while (i < items.length && items[i].type !== "box") i++;
59
+ return i;
60
+ }
61
+ /** This function gives the width of the line from the item `start` to a break at `end`, with the text of a penalty at `end`. */
62
+ function lineWidth(items, start, end) {
63
+ let width = 0;
64
+ for (let i = start; i < end; i++) {
65
+ const item = items[i];
66
+ if (item.type !== "penalty") width += item.width;
67
+ }
68
+ const last = items[end];
69
+ return last?.type === "penalty" ? width + last.width : width;
70
+ }
71
+ /**
72
+ * This function gives the lines of a paragraph from the indices of its breaks. Each break is the index of glue or of a penalty.
73
+ * The last line ends at the end of the items.
74
+ */
75
+ function linesOf(paragraph, breaks) {
76
+ const { items } = paragraph;
77
+ const lines = [];
78
+ let start = 0;
79
+ for (const end of [...breaks, items.length]) {
80
+ let text = "";
81
+ for (let i = start; i < end; i++) {
82
+ const item = items[i];
83
+ if (item.type !== "penalty") text += item.text;
84
+ }
85
+ const last = items[end];
86
+ if (last?.type === "penalty") text += last.text;
87
+ lines.push({
88
+ text,
89
+ width: lineWidth(items, start, end),
90
+ start,
91
+ end
92
+ });
93
+ start = lineStart(items, end);
94
+ }
95
+ return lines;
96
+ }
97
+ // src/breaking/breakers.ts
98
+ /**
99
+ * The greedy breaker: first fit, as a browser with `text-wrap: wrap`. Each line takes the items while they fit in the width.
100
+ * A box that is wider than a line stays whole on its own line.
101
+ */
102
+ const greedy = {
103
+ name: "greedy",
104
+ breaks: (paragraph, width) => greedyBreaks(paragraph, width)
105
+ };
106
+ /**
107
+ * This function gives the breaks of the greedy breaker. After each break, it reads the items again from the start of the new
108
+ * line, so that each break opportunity of the new line counts.
109
+ */
110
+ function greedyBreaks(paragraph, width) {
111
+ const { items } = paragraph;
112
+ const breaks = [];
113
+ let used = 0;
114
+ let candidate = -1;
115
+ const breakAt = (index) => {
116
+ breaks.push(index);
117
+ used = 0;
118
+ candidate = -1;
119
+ return lineStart(items, index) - 1;
120
+ };
121
+ for (let i = 0; i < items.length; i++) {
122
+ const item = items[i];
123
+ if (item.type === "box") {
124
+ used += item.width;
125
+ if (used > width && candidate >= 0) i = breakAt(candidate);
126
+ } else if (item.type === "penalty" && item.cost === -Infinity) i = breakAt(i);
127
+ else if (item.type === "glue") {
128
+ if (isBreak(items, i)) candidate = i;
129
+ used += item.width;
130
+ } else if (isBreak(items, i) && used + item.width <= width) candidate = i;
131
+ }
132
+ return breaks;
133
+ }
134
+ /**
135
+ * This function gives the total fit breaker of Knuth and Plass for text with a ragged right edge. It chooses the breaks with
136
+ * the least sum of demerits over all the lines. The demerits of a line are `(linePenalty + 100 r²)²` plus the square of the
137
+ * cost of its penalty. The ratio `r` is the free space of the line divided by the width. The last line has `r = 0`.
138
+ */
139
+ function totalFit(options = {}) {
140
+ const linePenalty = options.linePenalty ?? 10;
141
+ const flaggedCost = options.flaggedCost ?? 3e3;
142
+ return {
143
+ name: "total-fit",
144
+ breaks: (paragraph, width) => totalFitBreaks(paragraph, width, linePenalty, flaggedCost)
145
+ };
146
+ }
147
+ /** The total fit breaker with the default options. */
148
+ const knuthPlass = totalFit();
149
+ /** This function gives the breaks of the total fit breaker, by dynamic programming over the break opportunities. */
150
+ function totalFitBreaks(paragraph, width, linePenalty, flaggedCost) {
151
+ const { items } = paragraph;
152
+ const nodes = [-1];
153
+ for (let i = 0; i < items.length; i++) if (isBreak(items, i)) nodes.push(i);
154
+ nodes.push(items.length);
155
+ const best = new Array(nodes.length).fill(Infinity);
156
+ const previous = new Array(nodes.length).fill(-1);
157
+ best[0] = 0;
158
+ for (let a = 0; a < nodes.length - 1; a++) {
159
+ if (best[a] === Infinity) continue;
160
+ const start = a === 0 ? 0 : lineStart(items, nodes[a]);
161
+ const flaggedBefore = a > 0 && isFlagged(paragraph, nodes[a]);
162
+ for (let b = a + 1; b < nodes.length; b++) {
163
+ const end = nodes[b];
164
+ const last = b === nodes.length - 1;
165
+ const used = lineWidth(items, start, end);
166
+ const overflow = used > width;
167
+ if (overflow && b > a + 1) {
168
+ if (items[end]?.type === "penalty") continue;
169
+ break;
170
+ }
171
+ const penalty = items[end];
172
+ const cost = penalty?.type === "penalty" ? penalty.cost : 0;
173
+ const ratio = last || overflow ? 0 : (width - used) / width;
174
+ let demerits = (linePenalty + 100 * ratio * ratio) ** 2;
175
+ if (cost > 0) demerits += cost * cost;
176
+ else if (cost > -Infinity && cost < 0) demerits -= cost * cost;
177
+ if (flaggedBefore && isFlagged(paragraph, end)) demerits += flaggedCost;
178
+ const total = best[a] + demerits;
179
+ if (total < best[b]) {
180
+ best[b] = total;
181
+ previous[b] = a;
182
+ }
183
+ if (cost === -Infinity) break;
184
+ }
185
+ }
186
+ const breaks = [];
187
+ for (let b = previous[nodes.length - 1]; b > 0; b = previous[b]) breaks.push(nodes[b]);
188
+ return breaks.reverse();
189
+ }
190
+ /** This function tells if the item `index` is a flagged penalty. */
191
+ function isFlagged(paragraph, index) {
192
+ const item = paragraph.items[index];
193
+ return item?.type === "penalty" && item.flagged;
194
+ }
195
+ /**
196
+ * This function gives the balanced breaker, as a browser with `text-wrap: balance`. It keeps the line count of the greedy
197
+ * breaker, and it finds the least width that gives that count. Thus the lines have more equal widths.
198
+ */
199
+ function balancedBreaker(options = {}) {
200
+ const steps = options.steps ?? 32;
201
+ return {
202
+ name: "balanced",
203
+ breaks: (paragraph, width) => {
204
+ const count = greedyBreaks(paragraph, width).length;
205
+ let low = 0;
206
+ let high = width;
207
+ for (let step = 0; step < steps; step++) {
208
+ const middle = (low + high) / 2;
209
+ if (greedyBreaks(paragraph, middle).length === count) high = middle;
210
+ else low = middle;
211
+ }
212
+ return greedyBreaks(paragraph, high);
213
+ }
214
+ };
215
+ }
216
+ /** The balanced breaker with the default options. */
217
+ const balanced = balancedBreaker();
218
+ /** The breakers of the package, by name. */
219
+ const breakers = {
220
+ greedy,
221
+ "total-fit": knuthPlass,
222
+ balanced
223
+ };
224
+ // src/breaking/measures.ts
225
+ /**
226
+ * This function gives the raggedness of the lines for the width `width`: the mean of `((width - line) / width)²` over the
227
+ * lines before the last line. It is 0 for one line. `@mark1russell7/layout-contract/text` uses the same measure.
228
+ */
229
+ function raggedness(lines, width) {
230
+ if (lines.length < 2) return 0;
231
+ let sum = 0;
232
+ for (let i = 0; i < lines.length - 1; i++) {
233
+ const free = (width - lines[i].width) / width;
234
+ sum += free * free;
235
+ }
236
+ return sum / (lines.length - 1);
237
+ }
238
+ /** This function gives the spread of the lines: the width of the widest line less the width of the narrowest line, the last line included. */
239
+ function spread(lines) {
240
+ if (lines.length === 0) return 0;
241
+ let widest = -Infinity;
242
+ let narrowest = Infinity;
243
+ for (const line of lines) {
244
+ widest = Math.max(widest, line.width);
245
+ narrowest = Math.min(narrowest, line.width);
246
+ }
247
+ return widest - narrowest;
248
+ }
249
+ // src/breaking.ts
250
+ var breaking_exports = /* @__PURE__ */ __exportAll({
251
+ balanced: () => balanced,
252
+ balancedBreaker: () => balancedBreaker,
253
+ breakers: () => breakers,
254
+ greedy: () => greedy,
255
+ isBreak: () => isBreak,
256
+ knuthPlass: () => knuthPlass,
257
+ lineStart: () => lineStart,
258
+ lineWidth: () => lineWidth,
259
+ linesOf: () => linesOf,
260
+ paragraphOf: () => paragraphOf,
261
+ raggedness: () => raggedness,
262
+ spread: () => spread,
263
+ totalFit: () => totalFit
264
+ });
265
+ export { balancedBreaker as a, knuthPlass as c, lineStart as d, lineWidth as f, balanced as i, totalFit as l, paragraphOf as m, raggedness as n, breakers as o, linesOf as p, spread as r, greedy as s, breaking_exports as t, isBreak as u };
266
+
267
+ //# sourceMappingURL=breaking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breaking.js","names":[],"sources":["../../src/breaking/paragraph.ts","../../src/breaking/breakers.ts","../../src/breaking/measures.ts","../../src/breaking.ts"],"sourcesContent":["/**\n * The paragraph model of the breakers: a list of boxes, glue and penalties, as Knuth and Plass define it. A builder makes\n * the items of a text. The breakers read only the items, so a new rule of break opportunities is a new builder.\n */\n\n/** A width function: the width of a run of text, in any unit. The breakers add the widths and compare them. */\nexport type WidthOf = (text: string) => number;\n\n/** A box: text that the breaker does not break. */\nexport interface Box {\n readonly type: \"box\";\n readonly text: string;\n readonly width: number;\n}\n\n/** Glue: a space between two boxes. A line can end at glue that comes after a box. The line then does not show the glue. */\nexport interface Glue {\n readonly type: \"glue\";\n readonly text: string;\n readonly width: number;\n}\n\n/**\n * A penalty: a break opportunity with a cost. A line that ends at the penalty shows its text, for example a hyphen. A cost of\n * `Infinity` forbids the break, and a cost of `-Infinity` forces it.\n */\nexport interface Penalty {\n readonly type: \"penalty\";\n readonly text: string;\n readonly width: number;\n readonly cost: number;\n /** A flagged penalty is a hyphen. Total fit adds a cost to two flagged breaks in sequence. */\n readonly flagged: boolean;\n}\n\n/** An item of a paragraph. */\nexport type Item = Box | Glue | Penalty;\n\n/** A paragraph: the items in the order of the text. */\nexport interface Paragraph {\n readonly items: readonly Item[];\n}\n\n/** The options of `paragraphOf`. */\nexport interface ParagraphOptions {\n /** The width function of the boxes, the spaces and the hyphens. */\n readonly widthOf: WidthOf;\n /**\n * The hyphenation hook. It gives the offsets in a word where a break with a hyphen is possible. The default gives no\n * offset. The width of each part of a word comes from `widthOf`, so the kerning across an offset is not in the sum.\n */\n readonly hyphenate?: (word: string) => readonly number[];\n /** The text that a line shows at a hyphen break. The default is \"-\". */\n readonly hyphen?: string;\n /** The cost of a hyphen break. The default is 50, as in TeX. */\n readonly hyphenCost?: number;\n}\n\nconst whiteSpace = /\\s+/u;\n\n/**\n * This function makes the paragraph of a text with the break opportunities of CSS `white-space: normal`. Each run of white\n * space becomes one glue of the width of a space. Each word between two runs becomes a box, or boxes and hyphen penalties.\n */\nexport function paragraphOf(text: string, options: ParagraphOptions): Paragraph {\n const { widthOf } = options;\n const hyphen = options.hyphen ?? \"-\";\n const hyphenCost = options.hyphenCost ?? 50;\n const space = widthOf(\" \");\n const hyphenWidth = options.hyphenate === undefined ? 0 : widthOf(hyphen);\n const items: Item[] = [];\n for (const word of text.split(whiteSpace)) {\n if (word === \"\") continue;\n if (items.length > 0) items.push({ type: \"glue\", text: \" \", width: space });\n const offsets = options.hyphenate === undefined ? [] : [...new Set(options.hyphenate(word))].filter((o) => o > 0 && o < word.length).sort((a, b) => a - b);\n let start = 0;\n for (const offset of offsets) {\n const part = word.slice(start, offset);\n items.push({ type: \"box\", text: part, width: widthOf(part) });\n items.push({ type: \"penalty\", text: hyphen, width: hyphenWidth, cost: hyphenCost, flagged: true });\n start = offset;\n }\n const last = word.slice(start);\n items.push({ type: \"box\", text: last, width: widthOf(last) });\n }\n return { items };\n}\n\n/** A line of a broken paragraph. */\nexport interface Line {\n readonly text: string;\n /** The width of the line: its boxes, the glue between them and the text of a penalty at its end. */\n readonly width: number;\n /** The index of the first item of the line. */\n readonly start: number;\n /** The index after the last item of the line. A break at glue or at a penalty has the index of that item. */\n readonly end: number;\n}\n\n/** This function tells if a line can end at the item `index`: glue after a box, or a penalty with a cost below `Infinity`. */\nexport function isBreak(items: readonly Item[], index: number): boolean {\n const item = items[index]!;\n if (item.type === \"glue\") return index > 0 && items[index - 1]!.type === \"box\";\n return item.type === \"penalty\" && item.cost < Infinity;\n}\n\n/** This function gives the index of the first item of a line after a break at `index`. It skips the glue and the penalties. */\nexport function lineStart(items: readonly Item[], index: number): number {\n let i = index + 1;\n while (i < items.length && items[i]!.type !== \"box\") i++;\n return i;\n}\n\n/** This function gives the width of the line from the item `start` to a break at `end`, with the text of a penalty at `end`. */\nexport function lineWidth(items: readonly Item[], start: number, end: number): number {\n let width = 0;\n for (let i = start; i < end; i++) {\n const item = items[i]!;\n if (item.type !== \"penalty\") width += item.width;\n }\n const last = items[end];\n return last?.type === \"penalty\" ? width + last.width : width;\n}\n\n/**\n * This function gives the lines of a paragraph from the indices of its breaks. Each break is the index of glue or of a penalty.\n * The last line ends at the end of the items.\n */\nexport function linesOf(paragraph: Paragraph, breaks: readonly number[]): Line[] {\n const { items } = paragraph;\n const lines: Line[] = [];\n let start = 0;\n for (const end of [...breaks, items.length]) {\n let text = \"\";\n for (let i = start; i < end; i++) {\n const item = items[i]!;\n if (item.type !== \"penalty\") text += item.text;\n }\n const last = items[end];\n if (last?.type === \"penalty\") text += last.text;\n lines.push({ text, width: lineWidth(items, start, end), start, end });\n start = lineStart(items, end);\n }\n return lines;\n}\n","import { isBreak, lineStart, lineWidth, type Paragraph } from \"./paragraph.ts\";\n\n/**\n * A breaker: a strategy that chooses the breaks of a paragraph for the line width `width`. It gives the indices of the\n * items where the lines end, in order. A breaker reads only the items, so any width function and any builder work with it.\n */\nexport interface Breaker {\n readonly name: string;\n breaks(paragraph: Paragraph, width: number): number[];\n}\n\n/**\n * The greedy breaker: first fit, as a browser with `text-wrap: wrap`. Each line takes the items while they fit in the width.\n * A box that is wider than a line stays whole on its own line.\n */\nexport const greedy: Breaker = {\n name: \"greedy\",\n breaks: (paragraph, width) => greedyBreaks(paragraph, width),\n};\n\n/**\n * This function gives the breaks of the greedy breaker. After each break, it reads the items again from the start of the new\n * line, so that each break opportunity of the new line counts.\n */\nfunction greedyBreaks(paragraph: Paragraph, width: number): number[] {\n const { items } = paragraph;\n const breaks: number[] = [];\n let used = 0;\n let candidate = -1;\n const breakAt = (index: number): number => {\n breaks.push(index);\n used = 0;\n candidate = -1;\n return lineStart(items, index) - 1;\n };\n for (let i = 0; i < items.length; i++) {\n const item = items[i]!;\n if (item.type === \"box\") {\n used += item.width;\n if (used > width && candidate >= 0) i = breakAt(candidate);\n } else if (item.type === \"penalty\" && item.cost === -Infinity) {\n i = breakAt(i);\n } else if (item.type === \"glue\") {\n if (isBreak(items, i)) candidate = i;\n used += item.width;\n } else if (isBreak(items, i) && used + item.width <= width) {\n candidate = i;\n }\n }\n return breaks;\n}\n\n/** The options of the total fit breaker. */\nexport interface TotalFitOptions {\n /** The cost of each line, as `\\linepenalty` of TeX. A larger value gives fewer lines. The default is 10. */\n readonly linePenalty?: number;\n /** The cost of two hyphen breaks in sequence. The default is 3000, as `\\doublehyphendemerits` of TeX. */\n readonly flaggedCost?: number;\n}\n\n/**\n * This function gives the total fit breaker of Knuth and Plass for text with a ragged right edge. It chooses the breaks with\n * the least sum of demerits over all the lines. The demerits of a line are `(linePenalty + 100 r²)²` plus the square of the\n * cost of its penalty. The ratio `r` is the free space of the line divided by the width. The last line has `r = 0`.\n */\nexport function totalFit(options: TotalFitOptions = {}): Breaker {\n const linePenalty = options.linePenalty ?? 10;\n const flaggedCost = options.flaggedCost ?? 3000;\n return {\n name: \"total-fit\",\n breaks: (paragraph, width) => totalFitBreaks(paragraph, width, linePenalty, flaggedCost),\n };\n}\n\n/** The total fit breaker with the default options. */\nexport const knuthPlass: Breaker = totalFit();\n\n/** This function gives the breaks of the total fit breaker, by dynamic programming over the break opportunities. */\nfunction totalFitBreaks(paragraph: Paragraph, width: number, linePenalty: number, flaggedCost: number): number[] {\n const { items } = paragraph;\n // The nodes: the start of the paragraph (-1), each break opportunity, and the end (items.length)\n const nodes = [-1];\n for (let i = 0; i < items.length; i++) if (isBreak(items, i)) nodes.push(i);\n nodes.push(items.length);\n const best = new Array<number>(nodes.length).fill(Infinity);\n const previous = new Array<number>(nodes.length).fill(-1);\n best[0] = 0;\n for (let a = 0; a < nodes.length - 1; a++) {\n if (best[a] === Infinity) continue;\n const start = a === 0 ? 0 : lineStart(items, nodes[a]!);\n const flaggedBefore = a > 0 && isFlagged(paragraph, nodes[a]!);\n for (let b = a + 1; b < nodes.length; b++) {\n const end = nodes[b]!;\n const last = b === nodes.length - 1;\n const used = lineWidth(items, start, end);\n const overflow = used > width;\n // A line wider than the width is possible only when no break opportunity is inside it\n if (overflow && b > a + 1) {\n if (items[end]?.type === \"penalty\") continue;\n break;\n }\n const penalty = items[end];\n const cost = penalty?.type === \"penalty\" ? penalty.cost : 0;\n const ratio = last || overflow ? 0 : (width - used) / width;\n let demerits = (linePenalty + 100 * ratio * ratio) ** 2;\n if (cost > 0) demerits += cost * cost;\n else if (cost > -Infinity && cost < 0) demerits -= cost * cost;\n if (flaggedBefore && isFlagged(paragraph, end)) demerits += flaggedCost;\n const total = best[a]! + demerits;\n if (total < best[b]!) {\n best[b] = total;\n previous[b] = a;\n }\n // A forced break ends each line that comes to it\n if (cost === -Infinity) break;\n }\n }\n const breaks: number[] = [];\n for (let b = previous[nodes.length - 1]!; b > 0; b = previous[b]!) breaks.push(nodes[b]!);\n return breaks.reverse();\n}\n\n/** This function tells if the item `index` is a flagged penalty. */\nfunction isFlagged(paragraph: Paragraph, index: number): boolean {\n const item = paragraph.items[index];\n return item?.type === \"penalty\" && item.flagged;\n}\n\n/** The options of the balanced breaker. */\nexport interface BalancedOptions {\n /** The steps of the search for the width. The default is 32. */\n readonly steps?: number;\n}\n\n/**\n * This function gives the balanced breaker, as a browser with `text-wrap: balance`. It keeps the line count of the greedy\n * breaker, and it finds the least width that gives that count. Thus the lines have more equal widths.\n */\nexport function balancedBreaker(options: BalancedOptions = {}): Breaker {\n const steps = options.steps ?? 32;\n return {\n name: \"balanced\",\n breaks: (paragraph, width) => {\n const count = greedyBreaks(paragraph, width).length;\n let low = 0;\n let high = width;\n for (let step = 0; step < steps; step++) {\n const middle = (low + high) / 2;\n if (greedyBreaks(paragraph, middle).length === count) high = middle;\n else low = middle;\n }\n return greedyBreaks(paragraph, high);\n },\n };\n}\n\n/** The balanced breaker with the default options. */\nexport const balanced: Breaker = balancedBreaker();\n\n/** The breakers of the package, by name. */\nexport const breakers: Readonly<Record<\"greedy\" | \"total-fit\" | \"balanced\", Breaker>> = { greedy, \"total-fit\": knuthPlass, balanced };\n","import type { Line } from \"./paragraph.ts\";\n\n/**\n * This function gives the raggedness of the lines for the width `width`: the mean of `((width - line) / width)²` over the\n * lines before the last line. It is 0 for one line. `@mark1russell7/layout-contract/text` uses the same measure.\n */\nexport function raggedness(lines: readonly Line[], width: number): number {\n if (lines.length < 2) return 0;\n let sum = 0;\n for (let i = 0; i < lines.length - 1; i++) {\n const free = (width - lines[i]!.width) / width;\n sum += free * free;\n }\n return sum / (lines.length - 1);\n}\n\n/** This function gives the spread of the lines: the width of the widest line less the width of the narrowest line, the last line included. */\nexport function spread(lines: readonly Line[]): number {\n if (lines.length === 0) return 0;\n let widest = -Infinity;\n let narrowest = Infinity;\n for (const line of lines) {\n widest = Math.max(widest, line.width);\n narrowest = Math.min(narrowest, line.width);\n }\n return widest - narrowest;\n}\n","export { balanced, balancedBreaker, breakers, greedy, knuthPlass, totalFit, type BalancedOptions, type Breaker, type TotalFitOptions } from \"./breaking/breakers.ts\";\nexport { raggedness, spread } from \"./breaking/measures.ts\";\nexport { isBreak, lineStart, lineWidth, linesOf, paragraphOf, type Box, type Glue, type Item, type Line, type Paragraph, type ParagraphOptions, type Penalty, type WidthOf } from \"./breaking/paragraph.ts\";\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ // \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ export { __exportAll as t };
@@ -0,0 +1,2 @@
1
+ import { t as breaking_d_exports } from "./chunks/breaking.js";
2
+ export { breaking_d_exports as breaking };
package/build/index.js ADDED
@@ -0,0 +1 @@
1
+ export { t as breaking } from "./chunks/breaking.js";
package/package.json CHANGED
@@ -1,13 +1,66 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@mark1russell7/typeset",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "A placeholder that lets npm trust the release workflow of mark1russell7/typeset. Use a later version.",
4
+ "version": "0.1.0",
5
+ "description": "Text measurement and line breaking. The subpath ./breaking has the greedy, total fit and balanced line breakers, for any width function.",
6
+ "keywords": [
7
+ "typeset",
8
+ "line-breaking",
9
+ "knuth-plass",
10
+ "text-layout",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/mark1russell7/typeset#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/mark1russell7/typeset/issues"
16
+ },
5
17
  "repository": {
6
18
  "type": "git",
7
- "url": "git+https://github.com/mark1russell7/typeset.git"
19
+ "url": "git+https://github.com/mark1russell7/typeset.git",
20
+ "directory": "packages/typeset"
8
21
  },
9
22
  "license": "MIT",
23
+ "author": "Mark Russell (https://github.com/mark1russell7)",
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "main": "./build/index.js",
27
+ "types": "./build/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./build/index.d.ts",
31
+ "default": "./build/index.js"
32
+ },
33
+ "./breaking": {
34
+ "types": "./build/breaking.d.ts",
35
+ "default": "./build/breaking.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "files": [
40
+ "build",
41
+ "src",
42
+ "!src/**/*.test.ts",
43
+ "!src/**/report.ts"
44
+ ],
45
+ "engines": {
46
+ "node": ">=22.12"
47
+ },
10
48
  "publishConfig": {
11
49
  "access": "public"
50
+ },
51
+ "devDependencies": {
52
+ "@mark1russell7/cue": "^1.0.2",
53
+ "@mark1russell7/layout-contract": "^0.2.21",
54
+ "@types/node": "^26.6.5",
55
+ "@typeset/corpus": "0.1.0",
56
+ "tsdown": "^0.23.1",
57
+ "typescript": "^7.0.2",
58
+ "vitest": "^5.0.3"
59
+ },
60
+ "scripts": {
61
+ "build": "tsdown",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest",
64
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
12
65
  }
13
- }
66
+ }
@@ -0,0 +1,161 @@
1
+ import { isBreak, lineStart, lineWidth, type Paragraph } from "./paragraph.ts";
2
+
3
+ /**
4
+ * A breaker: a strategy that chooses the breaks of a paragraph for the line width `width`. It gives the indices of the
5
+ * items where the lines end, in order. A breaker reads only the items, so any width function and any builder work with it.
6
+ */
7
+ export interface Breaker {
8
+ readonly name: string;
9
+ breaks(paragraph: Paragraph, width: number): number[];
10
+ }
11
+
12
+ /**
13
+ * The greedy breaker: first fit, as a browser with `text-wrap: wrap`. Each line takes the items while they fit in the width.
14
+ * A box that is wider than a line stays whole on its own line.
15
+ */
16
+ export const greedy: Breaker = {
17
+ name: "greedy",
18
+ breaks: (paragraph, width) => greedyBreaks(paragraph, width),
19
+ };
20
+
21
+ /**
22
+ * This function gives the breaks of the greedy breaker. After each break, it reads the items again from the start of the new
23
+ * line, so that each break opportunity of the new line counts.
24
+ */
25
+ function greedyBreaks(paragraph: Paragraph, width: number): number[] {
26
+ const { items } = paragraph;
27
+ const breaks: number[] = [];
28
+ let used = 0;
29
+ let candidate = -1;
30
+ const breakAt = (index: number): number => {
31
+ breaks.push(index);
32
+ used = 0;
33
+ candidate = -1;
34
+ return lineStart(items, index) - 1;
35
+ };
36
+ for (let i = 0; i < items.length; i++) {
37
+ const item = items[i]!;
38
+ if (item.type === "box") {
39
+ used += item.width;
40
+ if (used > width && candidate >= 0) i = breakAt(candidate);
41
+ } else if (item.type === "penalty" && item.cost === -Infinity) {
42
+ i = breakAt(i);
43
+ } else if (item.type === "glue") {
44
+ if (isBreak(items, i)) candidate = i;
45
+ used += item.width;
46
+ } else if (isBreak(items, i) && used + item.width <= width) {
47
+ candidate = i;
48
+ }
49
+ }
50
+ return breaks;
51
+ }
52
+
53
+ /** The options of the total fit breaker. */
54
+ export interface TotalFitOptions {
55
+ /** The cost of each line, as `\linepenalty` of TeX. A larger value gives fewer lines. The default is 10. */
56
+ readonly linePenalty?: number;
57
+ /** The cost of two hyphen breaks in sequence. The default is 3000, as `\doublehyphendemerits` of TeX. */
58
+ readonly flaggedCost?: number;
59
+ }
60
+
61
+ /**
62
+ * This function gives the total fit breaker of Knuth and Plass for text with a ragged right edge. It chooses the breaks with
63
+ * the least sum of demerits over all the lines. The demerits of a line are `(linePenalty + 100 r²)²` plus the square of the
64
+ * cost of its penalty. The ratio `r` is the free space of the line divided by the width. The last line has `r = 0`.
65
+ */
66
+ export function totalFit(options: TotalFitOptions = {}): Breaker {
67
+ const linePenalty = options.linePenalty ?? 10;
68
+ const flaggedCost = options.flaggedCost ?? 3000;
69
+ return {
70
+ name: "total-fit",
71
+ breaks: (paragraph, width) => totalFitBreaks(paragraph, width, linePenalty, flaggedCost),
72
+ };
73
+ }
74
+
75
+ /** The total fit breaker with the default options. */
76
+ export const knuthPlass: Breaker = totalFit();
77
+
78
+ /** This function gives the breaks of the total fit breaker, by dynamic programming over the break opportunities. */
79
+ function totalFitBreaks(paragraph: Paragraph, width: number, linePenalty: number, flaggedCost: number): number[] {
80
+ const { items } = paragraph;
81
+ // The nodes: the start of the paragraph (-1), each break opportunity, and the end (items.length)
82
+ const nodes = [-1];
83
+ for (let i = 0; i < items.length; i++) if (isBreak(items, i)) nodes.push(i);
84
+ nodes.push(items.length);
85
+ const best = new Array<number>(nodes.length).fill(Infinity);
86
+ const previous = new Array<number>(nodes.length).fill(-1);
87
+ best[0] = 0;
88
+ for (let a = 0; a < nodes.length - 1; a++) {
89
+ if (best[a] === Infinity) continue;
90
+ const start = a === 0 ? 0 : lineStart(items, nodes[a]!);
91
+ const flaggedBefore = a > 0 && isFlagged(paragraph, nodes[a]!);
92
+ for (let b = a + 1; b < nodes.length; b++) {
93
+ const end = nodes[b]!;
94
+ const last = b === nodes.length - 1;
95
+ const used = lineWidth(items, start, end);
96
+ const overflow = used > width;
97
+ // A line wider than the width is possible only when no break opportunity is inside it
98
+ if (overflow && b > a + 1) {
99
+ if (items[end]?.type === "penalty") continue;
100
+ break;
101
+ }
102
+ const penalty = items[end];
103
+ const cost = penalty?.type === "penalty" ? penalty.cost : 0;
104
+ const ratio = last || overflow ? 0 : (width - used) / width;
105
+ let demerits = (linePenalty + 100 * ratio * ratio) ** 2;
106
+ if (cost > 0) demerits += cost * cost;
107
+ else if (cost > -Infinity && cost < 0) demerits -= cost * cost;
108
+ if (flaggedBefore && isFlagged(paragraph, end)) demerits += flaggedCost;
109
+ const total = best[a]! + demerits;
110
+ if (total < best[b]!) {
111
+ best[b] = total;
112
+ previous[b] = a;
113
+ }
114
+ // A forced break ends each line that comes to it
115
+ if (cost === -Infinity) break;
116
+ }
117
+ }
118
+ const breaks: number[] = [];
119
+ for (let b = previous[nodes.length - 1]!; b > 0; b = previous[b]!) breaks.push(nodes[b]!);
120
+ return breaks.reverse();
121
+ }
122
+
123
+ /** This function tells if the item `index` is a flagged penalty. */
124
+ function isFlagged(paragraph: Paragraph, index: number): boolean {
125
+ const item = paragraph.items[index];
126
+ return item?.type === "penalty" && item.flagged;
127
+ }
128
+
129
+ /** The options of the balanced breaker. */
130
+ export interface BalancedOptions {
131
+ /** The steps of the search for the width. The default is 32. */
132
+ readonly steps?: number;
133
+ }
134
+
135
+ /**
136
+ * This function gives the balanced breaker, as a browser with `text-wrap: balance`. It keeps the line count of the greedy
137
+ * breaker, and it finds the least width that gives that count. Thus the lines have more equal widths.
138
+ */
139
+ export function balancedBreaker(options: BalancedOptions = {}): Breaker {
140
+ const steps = options.steps ?? 32;
141
+ return {
142
+ name: "balanced",
143
+ breaks: (paragraph, width) => {
144
+ const count = greedyBreaks(paragraph, width).length;
145
+ let low = 0;
146
+ let high = width;
147
+ for (let step = 0; step < steps; step++) {
148
+ const middle = (low + high) / 2;
149
+ if (greedyBreaks(paragraph, middle).length === count) high = middle;
150
+ else low = middle;
151
+ }
152
+ return greedyBreaks(paragraph, high);
153
+ },
154
+ };
155
+ }
156
+
157
+ /** The balanced breaker with the default options. */
158
+ export const balanced: Breaker = balancedBreaker();
159
+
160
+ /** The breakers of the package, by name. */
161
+ export const breakers: Readonly<Record<"greedy" | "total-fit" | "balanced", Breaker>> = { greedy, "total-fit": knuthPlass, balanced };
@@ -0,0 +1,27 @@
1
+ import type { Line } from "./paragraph.ts";
2
+
3
+ /**
4
+ * This function gives the raggedness of the lines for the width `width`: the mean of `((width - line) / width)²` over the
5
+ * lines before the last line. It is 0 for one line. `@mark1russell7/layout-contract/text` uses the same measure.
6
+ */
7
+ export function raggedness(lines: readonly Line[], width: number): number {
8
+ if (lines.length < 2) return 0;
9
+ let sum = 0;
10
+ for (let i = 0; i < lines.length - 1; i++) {
11
+ const free = (width - lines[i]!.width) / width;
12
+ sum += free * free;
13
+ }
14
+ return sum / (lines.length - 1);
15
+ }
16
+
17
+ /** This function gives the spread of the lines: the width of the widest line less the width of the narrowest line, the last line included. */
18
+ export function spread(lines: readonly Line[]): number {
19
+ if (lines.length === 0) return 0;
20
+ let widest = -Infinity;
21
+ let narrowest = Infinity;
22
+ for (const line of lines) {
23
+ widest = Math.max(widest, line.width);
24
+ narrowest = Math.min(narrowest, line.width);
25
+ }
26
+ return widest - narrowest;
27
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The paragraph model of the breakers: a list of boxes, glue and penalties, as Knuth and Plass define it. A builder makes
3
+ * the items of a text. The breakers read only the items, so a new rule of break opportunities is a new builder.
4
+ */
5
+
6
+ /** A width function: the width of a run of text, in any unit. The breakers add the widths and compare them. */
7
+ export type WidthOf = (text: string) => number;
8
+
9
+ /** A box: text that the breaker does not break. */
10
+ export interface Box {
11
+ readonly type: "box";
12
+ readonly text: string;
13
+ readonly width: number;
14
+ }
15
+
16
+ /** Glue: a space between two boxes. A line can end at glue that comes after a box. The line then does not show the glue. */
17
+ export interface Glue {
18
+ readonly type: "glue";
19
+ readonly text: string;
20
+ readonly width: number;
21
+ }
22
+
23
+ /**
24
+ * A penalty: a break opportunity with a cost. A line that ends at the penalty shows its text, for example a hyphen. A cost of
25
+ * `Infinity` forbids the break, and a cost of `-Infinity` forces it.
26
+ */
27
+ export interface Penalty {
28
+ readonly type: "penalty";
29
+ readonly text: string;
30
+ readonly width: number;
31
+ readonly cost: number;
32
+ /** A flagged penalty is a hyphen. Total fit adds a cost to two flagged breaks in sequence. */
33
+ readonly flagged: boolean;
34
+ }
35
+
36
+ /** An item of a paragraph. */
37
+ export type Item = Box | Glue | Penalty;
38
+
39
+ /** A paragraph: the items in the order of the text. */
40
+ export interface Paragraph {
41
+ readonly items: readonly Item[];
42
+ }
43
+
44
+ /** The options of `paragraphOf`. */
45
+ export interface ParagraphOptions {
46
+ /** The width function of the boxes, the spaces and the hyphens. */
47
+ readonly widthOf: WidthOf;
48
+ /**
49
+ * The hyphenation hook. It gives the offsets in a word where a break with a hyphen is possible. The default gives no
50
+ * offset. The width of each part of a word comes from `widthOf`, so the kerning across an offset is not in the sum.
51
+ */
52
+ readonly hyphenate?: (word: string) => readonly number[];
53
+ /** The text that a line shows at a hyphen break. The default is "-". */
54
+ readonly hyphen?: string;
55
+ /** The cost of a hyphen break. The default is 50, as in TeX. */
56
+ readonly hyphenCost?: number;
57
+ }
58
+
59
+ const whiteSpace = /\s+/u;
60
+
61
+ /**
62
+ * This function makes the paragraph of a text with the break opportunities of CSS `white-space: normal`. Each run of white
63
+ * space becomes one glue of the width of a space. Each word between two runs becomes a box, or boxes and hyphen penalties.
64
+ */
65
+ export function paragraphOf(text: string, options: ParagraphOptions): Paragraph {
66
+ const { widthOf } = options;
67
+ const hyphen = options.hyphen ?? "-";
68
+ const hyphenCost = options.hyphenCost ?? 50;
69
+ const space = widthOf(" ");
70
+ const hyphenWidth = options.hyphenate === undefined ? 0 : widthOf(hyphen);
71
+ const items: Item[] = [];
72
+ for (const word of text.split(whiteSpace)) {
73
+ if (word === "") continue;
74
+ if (items.length > 0) items.push({ type: "glue", text: " ", width: space });
75
+ const offsets = options.hyphenate === undefined ? [] : [...new Set(options.hyphenate(word))].filter((o) => o > 0 && o < word.length).sort((a, b) => a - b);
76
+ let start = 0;
77
+ for (const offset of offsets) {
78
+ const part = word.slice(start, offset);
79
+ items.push({ type: "box", text: part, width: widthOf(part) });
80
+ items.push({ type: "penalty", text: hyphen, width: hyphenWidth, cost: hyphenCost, flagged: true });
81
+ start = offset;
82
+ }
83
+ const last = word.slice(start);
84
+ items.push({ type: "box", text: last, width: widthOf(last) });
85
+ }
86
+ return { items };
87
+ }
88
+
89
+ /** A line of a broken paragraph. */
90
+ export interface Line {
91
+ readonly text: string;
92
+ /** The width of the line: its boxes, the glue between them and the text of a penalty at its end. */
93
+ readonly width: number;
94
+ /** The index of the first item of the line. */
95
+ readonly start: number;
96
+ /** The index after the last item of the line. A break at glue or at a penalty has the index of that item. */
97
+ readonly end: number;
98
+ }
99
+
100
+ /** This function tells if a line can end at the item `index`: glue after a box, or a penalty with a cost below `Infinity`. */
101
+ export function isBreak(items: readonly Item[], index: number): boolean {
102
+ const item = items[index]!;
103
+ if (item.type === "glue") return index > 0 && items[index - 1]!.type === "box";
104
+ return item.type === "penalty" && item.cost < Infinity;
105
+ }
106
+
107
+ /** This function gives the index of the first item of a line after a break at `index`. It skips the glue and the penalties. */
108
+ export function lineStart(items: readonly Item[], index: number): number {
109
+ let i = index + 1;
110
+ while (i < items.length && items[i]!.type !== "box") i++;
111
+ return i;
112
+ }
113
+
114
+ /** This function gives the width of the line from the item `start` to a break at `end`, with the text of a penalty at `end`. */
115
+ export function lineWidth(items: readonly Item[], start: number, end: number): number {
116
+ let width = 0;
117
+ for (let i = start; i < end; i++) {
118
+ const item = items[i]!;
119
+ if (item.type !== "penalty") width += item.width;
120
+ }
121
+ const last = items[end];
122
+ return last?.type === "penalty" ? width + last.width : width;
123
+ }
124
+
125
+ /**
126
+ * This function gives the lines of a paragraph from the indices of its breaks. Each break is the index of glue or of a penalty.
127
+ * The last line ends at the end of the items.
128
+ */
129
+ export function linesOf(paragraph: Paragraph, breaks: readonly number[]): Line[] {
130
+ const { items } = paragraph;
131
+ const lines: Line[] = [];
132
+ let start = 0;
133
+ for (const end of [...breaks, items.length]) {
134
+ let text = "";
135
+ for (let i = start; i < end; i++) {
136
+ const item = items[i]!;
137
+ if (item.type !== "penalty") text += item.text;
138
+ }
139
+ const last = items[end];
140
+ if (last?.type === "penalty") text += last.text;
141
+ lines.push({ text, width: lineWidth(items, start, end), start, end });
142
+ start = lineStart(items, end);
143
+ }
144
+ return lines;
145
+ }
@@ -0,0 +1,3 @@
1
+ export { balanced, balancedBreaker, breakers, greedy, knuthPlass, totalFit, type BalancedOptions, type Breaker, type TotalFitOptions } from "./breaking/breakers.ts";
2
+ export { raggedness, spread } from "./breaking/measures.ts";
3
+ export { isBreak, lineStart, lineWidth, linesOf, paragraphOf, type Box, type Glue, type Item, type Line, type Paragraph, type ParagraphOptions, type Penalty, type WidthOf } from "./breaking/paragraph.ts";
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The entry of `@mark1russell7/typeset`. Each area of the package has its own subpath, for example
3
+ * `@mark1russell7/typeset/breaking`. This entry gives each area as one namespace.
4
+ */
5
+ export * as breaking from "./breaking.ts";