@mark1russell7/typeset 0.1.0 → 0.1.1
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/README.md +18 -0
- package/build/breaking.d.ts +2 -1
- package/build/chunks/breaking.d.ts +2 -77
- package/build/chunks/breaking.d.ts.map +1 -1
- package/build/chunks/paragraph.d.ts +78 -0
- package/build/chunks/paragraph.d.ts.map +1 -0
- package/build/chunks/text.d.ts +48 -0
- package/build/chunks/text.d.ts.map +1 -0
- package/build/chunks/text.js +237 -0
- package/build/chunks/text.js.map +1 -0
- package/build/index.d.ts +2 -1
- package/build/index.js +3 -1
- package/build/text.d.ts +2 -0
- package/build/text.js +1 -0
- package/package.json +8 -3
- package/src/index.ts +1 -0
- package/src/text/rope.ts +291 -0
- package/src/text.ts +1 -0
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ This package measures text and breaks it into lines. Each area has its own subpa
|
|
|
5
5
|
| Subpath | What it has |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| `@mark1russell7/typeset/breaking` | The paragraph model of boxes, glue and penalties, and the greedy, total fit and balanced line breakers. |
|
|
8
|
+
| `@mark1russell7/typeset/text` | The `Text` rope: a text with its width, and edits that measure only the changed parts. |
|
|
8
9
|
|
|
9
10
|
## Line breaking
|
|
10
11
|
|
|
@@ -20,4 +21,21 @@ const lines = linesOf(paragraph, knuthPlass.breaks(paragraph, 120));
|
|
|
20
21
|
- `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
22
|
- The option `hyphenate` gives the offsets of the hyphen breaks of a word.
|
|
22
23
|
|
|
24
|
+
## The Text rope
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Text } from "@mark1russell7/typeset/text";
|
|
28
|
+
|
|
29
|
+
const widthOf = (text: string): number => 8 * [...text].length;
|
|
30
|
+
const text = Text.from("The quick brown fox", widthOf);
|
|
31
|
+
const edited = text.insert(4, "very ").remove(0, 4);
|
|
32
|
+
console.log(edited.toString(), edited.width);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Each edit gives a new rope. The old rope does not change, and the two share the parts that the edit does not touch.
|
|
36
|
+
- An edit uses the width function of the order of `log n` times, on the changed leaves and on short windows.
|
|
37
|
+
- The width is exact when the width function obeys the separator law: `width(p + s + q) = width(p + s) + width(s + q) - width(s)`.
|
|
38
|
+
- The separator `s` is a space or a tab. The option `separators` changes the set.
|
|
39
|
+
- The width model of the repository obeys the law for its shaping `dom` and `canvas`, with the sum `exact`.
|
|
40
|
+
|
|
23
41
|
The repository [mark1russell7/typeset](https://github.com/mark1russell7/typeset) has the report of the breakers in `docs/BREAKING.md`.
|
package/build/breaking.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as Paragraph, c as WidthOf, d as lineWidth, f as linesOf, i as Line, l as isBreak, n as Glue, o as ParagraphOptions, p as paragraphOf, r as Item, s as Penalty, t as Box, u as lineStart } from "./chunks/paragraph.js";
|
|
2
|
+
import { a as Breaker, c as balancedBreaker, d as knuthPlass, f as totalFit, i as BalancedOptions, l as breakers, n as raggedness, o as TotalFitOptions, r as spread, s as balanced, u as greedy } from "./chunks/breaking.js";
|
|
2
3
|
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 };
|
|
@@ -1,79 +1,4 @@
|
|
|
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[];
|
|
1
|
+
import { a as Paragraph, c as WidthOf, d as lineWidth, f as linesOf, i as Line, l as isBreak, n as Glue, o as ParagraphOptions, p as paragraphOf, r as Item, s as Penalty, t as Box, u as lineStart } from "./paragraph.js";
|
|
77
2
|
/**
|
|
78
3
|
* A breaker: a strategy that chooses the breaks of a paragraph for the line width `width`. It gives the indices of the
|
|
79
4
|
* items where the lines end, in order. A breaker reads only the items, so any width function and any builder work with it.
|
|
@@ -126,5 +51,5 @@ declare function spread(lines: readonly Line[]): number;
|
|
|
126
51
|
declare namespace breaking_d_exports {
|
|
127
52
|
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
53
|
}
|
|
129
|
-
export {
|
|
54
|
+
export { Breaker as a, balancedBreaker as c, knuthPlass as d, totalFit as f, BalancedOptions as i, breakers as l, raggedness as n, TotalFitOptions as o, spread as r, balanced as s, breaking_d_exports as t, greedy as u };
|
|
130
55
|
//# sourceMappingURL=breaking.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"breaking.d.ts","names":[],"sources":["../../src/breaking/
|
|
1
|
+
{"version":3,"file":"breaking.d.ts","names":[],"sources":["../../src/breaking/breakers.ts","../../src/breaking/measures.ts","../../src/breaking.ts"],"mappings":";;;;;UAMiB;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,78 @@
|
|
|
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
|
+
export { Paragraph as a, WidthOf as c, lineWidth as d, linesOf as f, Line as i, isBreak as l, Glue as n, ParagraphOptions as o, paragraphOf as p, Item as r, Penalty as s, Box as t, lineStart as u };
|
|
78
|
+
//# sourceMappingURL=paragraph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paragraph.d.ts","names":[],"sources":["../../src/breaking/paragraph.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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { c as WidthOf } from "./paragraph.js";
|
|
2
|
+
/** The options of a rope. */
|
|
3
|
+
interface TextOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The separators of the separator law: each character is one UTF-16 code unit. The default is a space and a tab. An
|
|
6
|
+
* empty string makes each seam measure the full text of the two sides.
|
|
7
|
+
*/
|
|
8
|
+
readonly separators?: string;
|
|
9
|
+
/** The largest count of UTF-16 code units in a leaf. The default is 256. */
|
|
10
|
+
readonly chunk?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A text with its width: a persistent balanced rope. Each edit gives a new `Text` and does not change this one. The offsets
|
|
14
|
+
* are in UTF-16 code units, as the offsets of a JavaScript string.
|
|
15
|
+
*/
|
|
16
|
+
declare class Text {
|
|
17
|
+
#private;
|
|
18
|
+
private constructor();
|
|
19
|
+
/** This function makes the rope of `text` with the width function `width`. */
|
|
20
|
+
static from(text: string, width: WidthOf, options?: TextOptions): Text;
|
|
21
|
+
/** The count of UTF-16 code units of the text. */
|
|
22
|
+
get length(): number;
|
|
23
|
+
/** The width of the text. It is equal to the width of the flat string when the width function obeys the separator law. */
|
|
24
|
+
get width(): number;
|
|
25
|
+
/** The height of the tree: 0 for a text of one leaf. The tests use it to examine the balance. */
|
|
26
|
+
get height(): number;
|
|
27
|
+
/** This function gives the flat string. */
|
|
28
|
+
toString(): string;
|
|
29
|
+
/** This function gives the strings of the leaves in the order of the text. */
|
|
30
|
+
chunks(): string[];
|
|
31
|
+
/** This function gives the text from `start` to `end`, as a new rope. */
|
|
32
|
+
slice(start: number, end?: number): Text;
|
|
33
|
+
/** This function gives the width of the text from `start` to `end`. */
|
|
34
|
+
widthOf(start: number, end?: number): number;
|
|
35
|
+
/** This function gives this text and then `other`. The two ropes must have the same width function and options. */
|
|
36
|
+
concat(other: Text): Text;
|
|
37
|
+
/** This function puts `text` at the offset `at`. */
|
|
38
|
+
insert(at: number, text: string): Text;
|
|
39
|
+
/** This function removes the text from `start` to `end`. */
|
|
40
|
+
remove(start: number, end: number): Text;
|
|
41
|
+
/** This function puts `text` in place of the text from `start` to `end`. */
|
|
42
|
+
replace(start: number, end: number, text: string): Text;
|
|
43
|
+
}
|
|
44
|
+
declare namespace text_d_exports {
|
|
45
|
+
export { Text, TextOptions };
|
|
46
|
+
}
|
|
47
|
+
export { Text as n, TextOptions as r, text_d_exports as t };
|
|
48
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","names":[],"sources":["../../src/text/rope.ts","../../src/text.ts"],"mappings":";;UAYiB;;;;;WAKN;;WAEA;;;;;;cAkLE;;UAIH;;SAMD,KAAK,cAAc,OAAO,SAAS,UAAS,cAAmB;;MAalE;;MAKA;;MAKA;;EAKJ;;EAKA;;EAKA,MAAM,eAAe,eAA4B;;EAQjD,QAAQ,eAAe;;EAKvB,OAAO,OAAO,OAAO;;EASrB,OAAO,YAAY,eAAe;;EAKlC,OAAO,eAAe,cAAc;;EAKpC,QAAQ,eAAe,aAAa,eAAe"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
|
+
// src/text/rope.ts
|
|
3
|
+
function isSeparator(context, code) {
|
|
4
|
+
for (let i = 0; i < context.separators.length; i++) if (context.separators.charCodeAt(i) === code) return true;
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
function leafOf(context, text) {
|
|
8
|
+
const width = context.width(text);
|
|
9
|
+
let first = -1;
|
|
10
|
+
let last = -1;
|
|
11
|
+
for (let i = 0; i < text.length; i++) {
|
|
12
|
+
if (!isSeparator(context, text.charCodeAt(i))) continue;
|
|
13
|
+
if (first < 0) first = i;
|
|
14
|
+
last = i;
|
|
15
|
+
}
|
|
16
|
+
if (first < 0) return {
|
|
17
|
+
kind: "leaf",
|
|
18
|
+
text,
|
|
19
|
+
height: 0,
|
|
20
|
+
length: text.length,
|
|
21
|
+
width,
|
|
22
|
+
separated: false,
|
|
23
|
+
head: text,
|
|
24
|
+
headWidth: width,
|
|
25
|
+
tail: text,
|
|
26
|
+
tailWidth: width
|
|
27
|
+
};
|
|
28
|
+
const head = text.slice(0, first + 1);
|
|
29
|
+
const tail = text.slice(last);
|
|
30
|
+
return {
|
|
31
|
+
kind: "leaf",
|
|
32
|
+
text,
|
|
33
|
+
height: 0,
|
|
34
|
+
length: text.length,
|
|
35
|
+
width,
|
|
36
|
+
separated: true,
|
|
37
|
+
head,
|
|
38
|
+
headWidth: context.width(head),
|
|
39
|
+
tail,
|
|
40
|
+
tailWidth: context.width(tail)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* This function makes the branch of two subtrees. It measures one window: the tail of the left side and the head of the right
|
|
45
|
+
* side. The separator law then gives the width of all the text from the widths of the two sides.
|
|
46
|
+
*/
|
|
47
|
+
function branchOf(context, left, right) {
|
|
48
|
+
const seam = left.tail + right.head;
|
|
49
|
+
const seamWidth = context.width(seam);
|
|
50
|
+
let width;
|
|
51
|
+
if (!left.separated && !right.separated) width = seamWidth;
|
|
52
|
+
else if (!left.separated) width = right.width - right.headWidth + seamWidth;
|
|
53
|
+
else if (!right.separated) width = left.width - left.tailWidth + seamWidth;
|
|
54
|
+
else width = left.width + right.width - left.tailWidth - right.headWidth + seamWidth;
|
|
55
|
+
return {
|
|
56
|
+
kind: "branch",
|
|
57
|
+
left,
|
|
58
|
+
right,
|
|
59
|
+
height: Math.max(left.height, right.height) + 1,
|
|
60
|
+
length: left.length + right.length,
|
|
61
|
+
width,
|
|
62
|
+
separated: left.separated || right.separated,
|
|
63
|
+
head: left.separated ? left.head : seam,
|
|
64
|
+
headWidth: left.separated ? left.headWidth : seamWidth,
|
|
65
|
+
tail: right.separated ? right.tail : seam,
|
|
66
|
+
tailWidth: right.separated ? right.tailWidth : seamWidth
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** This function joins two trees in the order of the text and keeps the heights of the two children within 1. */
|
|
70
|
+
function join(context, left, right) {
|
|
71
|
+
if (left === void 0) return right;
|
|
72
|
+
if (right === void 0) return left;
|
|
73
|
+
if (left.kind === "leaf" && right.kind === "leaf" && left.length + right.length <= context.chunk) return leafOf(context, left.text + right.text);
|
|
74
|
+
if (left.height > right.height + 1) {
|
|
75
|
+
const branch = left;
|
|
76
|
+
const joined = join(context, branch.right, right);
|
|
77
|
+
if (joined.height <= branch.left.height + 1) return branchOf(context, branch.left, joined);
|
|
78
|
+
return rotateLeft(context, branch.left, joined);
|
|
79
|
+
}
|
|
80
|
+
if (right.height > left.height + 1) {
|
|
81
|
+
const branch = right;
|
|
82
|
+
const joined = join(context, left, branch.left);
|
|
83
|
+
if (joined.height <= branch.right.height + 1) return branchOf(context, joined, branch.right);
|
|
84
|
+
return rotateRight(context, joined, branch.right);
|
|
85
|
+
}
|
|
86
|
+
return branchOf(context, left, right);
|
|
87
|
+
}
|
|
88
|
+
/** This function balances the branch of `left` and `heavy`, when `heavy` is 2 levels higher than `left`. */
|
|
89
|
+
function rotateLeft(context, left, heavy) {
|
|
90
|
+
if (heavy.left.height <= heavy.right.height) return branchOf(context, branchOf(context, left, heavy.left), heavy.right);
|
|
91
|
+
const middle = heavy.left;
|
|
92
|
+
return branchOf(context, branchOf(context, left, middle.left), branchOf(context, middle.right, heavy.right));
|
|
93
|
+
}
|
|
94
|
+
/** This function balances the branch of `heavy` and `right`, when `heavy` is 2 levels higher than `right`. */
|
|
95
|
+
function rotateRight(context, heavy, right) {
|
|
96
|
+
if (heavy.right.height <= heavy.left.height) return branchOf(context, heavy.left, branchOf(context, heavy.right, right));
|
|
97
|
+
const middle = heavy.right;
|
|
98
|
+
return branchOf(context, branchOf(context, heavy.left, middle.left), branchOf(context, middle.right, right));
|
|
99
|
+
}
|
|
100
|
+
/** This function divides a tree at the UTF-16 offset `at` into the text before and the text after. */
|
|
101
|
+
function split(context, node, at) {
|
|
102
|
+
if (node === void 0) return [void 0, void 0];
|
|
103
|
+
if (at <= 0) return [void 0, node];
|
|
104
|
+
if (at >= node.length) return [node, void 0];
|
|
105
|
+
if (node.kind === "leaf") return [leafOf(context, node.text.slice(0, at)), leafOf(context, node.text.slice(at))];
|
|
106
|
+
if (at === node.left.length) return [node.left, node.right];
|
|
107
|
+
if (at < node.left.length) {
|
|
108
|
+
const [before, after] = split(context, node.left, at);
|
|
109
|
+
return [before, join(context, after, node.right)];
|
|
110
|
+
}
|
|
111
|
+
const [before, after] = split(context, node.right, at - node.left.length);
|
|
112
|
+
return [join(context, node.left, before), after];
|
|
113
|
+
}
|
|
114
|
+
/** This function gives the end of the next leaf of `text` from `start`: after a separator when it can, and not in a surrogate pair. */
|
|
115
|
+
function chunkEnd(context, text, start) {
|
|
116
|
+
const limit = start + context.chunk;
|
|
117
|
+
if (limit >= text.length) return text.length;
|
|
118
|
+
for (let i = limit - 1; i >= start + context.chunk / 2; i--) if (isSeparator(context, text.charCodeAt(i))) return i + 1;
|
|
119
|
+
const code = text.charCodeAt(limit - 1);
|
|
120
|
+
return code >= 55296 && code <= 56319 ? limit - 1 : limit;
|
|
121
|
+
}
|
|
122
|
+
/** This function makes a balanced tree of a text: leaves of at most `chunk` code units, and then a level of branches at a time. */
|
|
123
|
+
function build(context, text) {
|
|
124
|
+
let level = [];
|
|
125
|
+
for (let start = 0; start < text.length;) {
|
|
126
|
+
const end = chunkEnd(context, text, start);
|
|
127
|
+
level.push(leafOf(context, text.slice(start, end)));
|
|
128
|
+
start = end;
|
|
129
|
+
}
|
|
130
|
+
if (level.length === 0) return void 0;
|
|
131
|
+
while (level.length > 1) {
|
|
132
|
+
const next = [];
|
|
133
|
+
for (let i = 0; i + 1 < level.length; i += 2) next.push(branchOf(context, level[i], level[i + 1]));
|
|
134
|
+
if (level.length % 2 === 1) next.push(level[level.length - 1]);
|
|
135
|
+
level = next;
|
|
136
|
+
}
|
|
137
|
+
return level[0];
|
|
138
|
+
}
|
|
139
|
+
function leaves(node, out) {
|
|
140
|
+
if (node === void 0) return out;
|
|
141
|
+
const stack = [node];
|
|
142
|
+
while (stack.length > 0) {
|
|
143
|
+
const top = stack.pop();
|
|
144
|
+
if (top.kind === "leaf") out.push(top.text);
|
|
145
|
+
else stack.push(top.right, top.left);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A text with its width: a persistent balanced rope. Each edit gives a new `Text` and does not change this one. The offsets
|
|
151
|
+
* are in UTF-16 code units, as the offsets of a JavaScript string.
|
|
152
|
+
*/
|
|
153
|
+
var Text = class Text {
|
|
154
|
+
#context;
|
|
155
|
+
#root;
|
|
156
|
+
constructor(context, root) {
|
|
157
|
+
this.#context = context;
|
|
158
|
+
this.#root = root;
|
|
159
|
+
}
|
|
160
|
+
/** This function makes the rope of `text` with the width function `width`. */
|
|
161
|
+
static from(text, width, options = {}) {
|
|
162
|
+
const separators = options.separators ?? " ";
|
|
163
|
+
const chunk = options.chunk ?? 256;
|
|
164
|
+
if (!Number.isInteger(chunk) || chunk < 2) throw new RangeError(`the chunk is ${chunk}, not an integer of 2 or more`);
|
|
165
|
+
for (let i = 0; i < separators.length; i++) {
|
|
166
|
+
const code = separators.charCodeAt(i);
|
|
167
|
+
if (code >= 55296 && code <= 57343) throw new RangeError("a separator is one UTF-16 code unit, not a half of a surrogate pair");
|
|
168
|
+
}
|
|
169
|
+
const context = {
|
|
170
|
+
width,
|
|
171
|
+
separators,
|
|
172
|
+
chunk
|
|
173
|
+
};
|
|
174
|
+
return new Text(context, build(context, text));
|
|
175
|
+
}
|
|
176
|
+
/** The count of UTF-16 code units of the text. */
|
|
177
|
+
get length() {
|
|
178
|
+
return this.#root?.length ?? 0;
|
|
179
|
+
}
|
|
180
|
+
/** The width of the text. It is equal to the width of the flat string when the width function obeys the separator law. */
|
|
181
|
+
get width() {
|
|
182
|
+
return this.#root?.width ?? 0;
|
|
183
|
+
}
|
|
184
|
+
/** The height of the tree: 0 for a text of one leaf. The tests use it to examine the balance. */
|
|
185
|
+
get height() {
|
|
186
|
+
return this.#root?.height ?? 0;
|
|
187
|
+
}
|
|
188
|
+
/** This function gives the flat string. */
|
|
189
|
+
toString() {
|
|
190
|
+
return leaves(this.#root, []).join("");
|
|
191
|
+
}
|
|
192
|
+
/** This function gives the strings of the leaves in the order of the text. */
|
|
193
|
+
chunks() {
|
|
194
|
+
return leaves(this.#root, []);
|
|
195
|
+
}
|
|
196
|
+
/** This function gives the text from `start` to `end`, as a new rope. */
|
|
197
|
+
slice(start, end = this.length) {
|
|
198
|
+
this.#check(start, end);
|
|
199
|
+
const [, after] = split(this.#context, this.#root, start);
|
|
200
|
+
const [middle] = split(this.#context, after, end - start);
|
|
201
|
+
return new Text(this.#context, middle);
|
|
202
|
+
}
|
|
203
|
+
/** This function gives the width of the text from `start` to `end`. */
|
|
204
|
+
widthOf(start, end = this.length) {
|
|
205
|
+
return this.slice(start, end).width;
|
|
206
|
+
}
|
|
207
|
+
/** This function gives this text and then `other`. The two ropes must have the same width function and options. */
|
|
208
|
+
concat(other) {
|
|
209
|
+
const context = other.#context;
|
|
210
|
+
if (context.width !== this.#context.width || context.separators !== this.#context.separators || context.chunk !== this.#context.chunk) throw new TypeError("the two ropes have different width functions or options");
|
|
211
|
+
return new Text(this.#context, join(this.#context, this.#root, other.#root));
|
|
212
|
+
}
|
|
213
|
+
/** This function puts `text` at the offset `at`. */
|
|
214
|
+
insert(at, text) {
|
|
215
|
+
return this.replace(at, at, text);
|
|
216
|
+
}
|
|
217
|
+
/** This function removes the text from `start` to `end`. */
|
|
218
|
+
remove(start, end) {
|
|
219
|
+
return this.replace(start, end, "");
|
|
220
|
+
}
|
|
221
|
+
/** This function puts `text` in place of the text from `start` to `end`. */
|
|
222
|
+
replace(start, end, text) {
|
|
223
|
+
this.#check(start, end);
|
|
224
|
+
const [before, rest] = split(this.#context, this.#root, start);
|
|
225
|
+
const [, after] = split(this.#context, rest, end - start);
|
|
226
|
+
const middle = build(this.#context, text);
|
|
227
|
+
return new Text(this.#context, join(this.#context, join(this.#context, before, middle), after));
|
|
228
|
+
}
|
|
229
|
+
#check(start, end) {
|
|
230
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > this.length) throw new RangeError(`the range ${start} to ${end} is not in the text of length ${this.length}`);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
// src/text.ts
|
|
234
|
+
var text_exports = /* @__PURE__ */ __exportAll({ Text: () => Text });
|
|
235
|
+
export { Text as n, text_exports as t };
|
|
236
|
+
|
|
237
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../src/text/rope.ts","../../src/text.ts"],"sourcesContent":["/**\n * The `Text` rope: a persistent balanced tree of strings with the width and the length of each subtree. An edit makes a\n * new rope. The new rope shares the subtrees that do not change. A width function gives the width of a run of text. The\n * rope uses it only for the leaves and for the windows at the seams. Thus an edit uses it of the order of `log n` times.\n *\n * The seams are exact when the width function obeys the separator law. For each text `p + s + q` with a separator `s`:\n * `width(p + s + q) = width(p + s) + width(s + q) - width(s)`. The model of `@typeset/fonts` obeys it for a space and a\n * tab. It shapes each word alone, and its kerning goes only between two adjacent glyphs.\n */\nimport type { WidthOf } from \"../breaking/paragraph.ts\";\n\n/** The options of a rope. */\nexport interface TextOptions {\n /**\n * The separators of the separator law: each character is one UTF-16 code unit. The default is a space and a tab. An\n * empty string makes each seam measure the full text of the two sides.\n */\n readonly separators?: string;\n /** The largest count of UTF-16 code units in a leaf. The default is 256. */\n readonly chunk?: number;\n}\n\n/** The summary of a subtree: its length, its width, and the two ends that a seam measures again. */\ninterface Summary {\n /** The count of UTF-16 code units. */\n readonly length: number;\n readonly width: number;\n /** This value is `true` when the text has a separator. */\n readonly separated: boolean;\n /** The text through the first separator, or all the text when the text has no separator. */\n readonly head: string;\n readonly headWidth: number;\n /** The text from the last separator, or all the text when the text has no separator. */\n readonly tail: string;\n readonly tailWidth: number;\n}\n\ninterface Leaf extends Summary {\n readonly kind: \"leaf\";\n readonly text: string;\n readonly height: 0;\n}\n\ninterface Branch extends Summary {\n readonly kind: \"branch\";\n readonly left: Node;\n readonly right: Node;\n readonly height: number;\n}\n\ntype Node = Leaf | Branch;\n\n/** The shared state of the ropes of one width function and one set of options. */\ninterface Context {\n readonly width: WidthOf;\n readonly separators: string;\n readonly chunk: number;\n}\n\nfunction isSeparator(context: Context, code: number): boolean {\n for (let i = 0; i < context.separators.length; i++) if (context.separators.charCodeAt(i) === code) return true;\n return false;\n}\n\nfunction leafOf(context: Context, text: string): Leaf {\n const width = context.width(text);\n let first = -1;\n let last = -1;\n for (let i = 0; i < text.length; i++) {\n if (!isSeparator(context, text.charCodeAt(i))) continue;\n if (first < 0) first = i;\n last = i;\n }\n if (first < 0) return { kind: \"leaf\", text, height: 0, length: text.length, width, separated: false, head: text, headWidth: width, tail: text, tailWidth: width };\n const head = text.slice(0, first + 1);\n const tail = text.slice(last);\n return { kind: \"leaf\", text, height: 0, length: text.length, width, separated: true, head, headWidth: context.width(head), tail, tailWidth: context.width(tail) };\n}\n\n/**\n * This function makes the branch of two subtrees. It measures one window: the tail of the left side and the head of the right\n * side. The separator law then gives the width of all the text from the widths of the two sides.\n */\nfunction branchOf(context: Context, left: Node, right: Node): Branch {\n const seam = left.tail + right.head;\n const seamWidth = context.width(seam);\n let width: number;\n if (!left.separated && !right.separated) width = seamWidth;\n else if (!left.separated) width = right.width - right.headWidth + seamWidth;\n else if (!right.separated) width = left.width - left.tailWidth + seamWidth;\n else width = left.width + right.width - left.tailWidth - right.headWidth + seamWidth;\n return {\n kind: \"branch\",\n left,\n right,\n height: Math.max(left.height, right.height) + 1,\n length: left.length + right.length,\n width,\n separated: left.separated || right.separated,\n head: left.separated ? left.head : seam,\n headWidth: left.separated ? left.headWidth : seamWidth,\n tail: right.separated ? right.tail : seam,\n tailWidth: right.separated ? right.tailWidth : seamWidth,\n };\n}\n\n/** This function joins two trees in the order of the text and keeps the heights of the two children within 1. */\nfunction join(context: Context, left: Node | undefined, right: Node | undefined): Node | undefined {\n if (left === undefined) return right;\n if (right === undefined) return left;\n if (left.kind === \"leaf\" && right.kind === \"leaf\" && left.length + right.length <= context.chunk) return leafOf(context, left.text + right.text);\n if (left.height > right.height + 1) {\n const branch = left as Branch;\n const joined = join(context, branch.right, right)!;\n if (joined.height <= branch.left.height + 1) return branchOf(context, branch.left, joined);\n return rotateLeft(context, branch.left, joined as Branch);\n }\n if (right.height > left.height + 1) {\n const branch = right as Branch;\n const joined = join(context, left, branch.left)!;\n if (joined.height <= branch.right.height + 1) return branchOf(context, joined, branch.right);\n return rotateRight(context, joined as Branch, branch.right);\n }\n return branchOf(context, left, right);\n}\n\n/** This function balances the branch of `left` and `heavy`, when `heavy` is 2 levels higher than `left`. */\nfunction rotateLeft(context: Context, left: Node, heavy: Branch): Branch {\n if (heavy.left.height <= heavy.right.height) return branchOf(context, branchOf(context, left, heavy.left), heavy.right);\n const middle = heavy.left as Branch;\n return branchOf(context, branchOf(context, left, middle.left), branchOf(context, middle.right, heavy.right));\n}\n\n/** This function balances the branch of `heavy` and `right`, when `heavy` is 2 levels higher than `right`. */\nfunction rotateRight(context: Context, heavy: Branch, right: Node): Branch {\n if (heavy.right.height <= heavy.left.height) return branchOf(context, heavy.left, branchOf(context, heavy.right, right));\n const middle = heavy.right as Branch;\n return branchOf(context, branchOf(context, heavy.left, middle.left), branchOf(context, middle.right, right));\n}\n\n/** This function divides a tree at the UTF-16 offset `at` into the text before and the text after. */\nfunction split(context: Context, node: Node | undefined, at: number): [Node | undefined, Node | undefined] {\n if (node === undefined) return [undefined, undefined];\n if (at <= 0) return [undefined, node];\n if (at >= node.length) return [node, undefined];\n if (node.kind === \"leaf\") return [leafOf(context, node.text.slice(0, at)), leafOf(context, node.text.slice(at))];\n if (at === node.left.length) return [node.left, node.right];\n if (at < node.left.length) {\n const [before, after] = split(context, node.left, at);\n return [before, join(context, after, node.right)];\n }\n const [before, after] = split(context, node.right, at - node.left.length);\n return [join(context, node.left, before), after];\n}\n\n/** This function gives the end of the next leaf of `text` from `start`: after a separator when it can, and not in a surrogate pair. */\nfunction chunkEnd(context: Context, text: string, start: number): number {\n const limit = start + context.chunk;\n if (limit >= text.length) return text.length;\n for (let i = limit - 1; i >= start + context.chunk / 2; i--) if (isSeparator(context, text.charCodeAt(i))) return i + 1;\n const code = text.charCodeAt(limit - 1);\n return code >= 0xd800 && code <= 0xdbff ? limit - 1 : limit;\n}\n\n/** This function makes a balanced tree of a text: leaves of at most `chunk` code units, and then a level of branches at a time. */\nfunction build(context: Context, text: string): Node | undefined {\n let level: Node[] = [];\n for (let start = 0; start < text.length; ) {\n const end = chunkEnd(context, text, start);\n level.push(leafOf(context, text.slice(start, end)));\n start = end;\n }\n if (level.length === 0) return undefined;\n while (level.length > 1) {\n const next: Node[] = [];\n for (let i = 0; i + 1 < level.length; i += 2) next.push(branchOf(context, level[i]!, level[i + 1]!));\n if (level.length % 2 === 1) next.push(level[level.length - 1]!);\n level = next;\n }\n return level[0];\n}\n\nfunction leaves(node: Node | undefined, out: string[]): string[] {\n if (node === undefined) return out;\n const stack: Node[] = [node];\n while (stack.length > 0) {\n const top = stack.pop()!;\n if (top.kind === \"leaf\") out.push(top.text);\n else stack.push(top.right, top.left);\n }\n return out;\n}\n\n/**\n * A text with its width: a persistent balanced rope. Each edit gives a new `Text` and does not change this one. The offsets\n * are in UTF-16 code units, as the offsets of a JavaScript string.\n */\nexport class Text {\n readonly #context: Context;\n readonly #root: Node | undefined;\n\n private constructor(context: Context, root: Node | undefined) {\n this.#context = context;\n this.#root = root;\n }\n\n /** This function makes the rope of `text` with the width function `width`. */\n static from(text: string, width: WidthOf, options: TextOptions = {}): Text {\n const separators = options.separators ?? \" \\t\";\n const chunk = options.chunk ?? 256;\n if (!Number.isInteger(chunk) || chunk < 2) throw new RangeError(`the chunk is ${chunk}, not an integer of 2 or more`);\n for (let i = 0; i < separators.length; i++) {\n const code = separators.charCodeAt(i);\n if (code >= 0xd800 && code <= 0xdfff) throw new RangeError(\"a separator is one UTF-16 code unit, not a half of a surrogate pair\");\n }\n const context: Context = { width, separators, chunk };\n return new Text(context, build(context, text));\n }\n\n /** The count of UTF-16 code units of the text. */\n get length(): number {\n return this.#root?.length ?? 0;\n }\n\n /** The width of the text. It is equal to the width of the flat string when the width function obeys the separator law. */\n get width(): number {\n return this.#root?.width ?? 0;\n }\n\n /** The height of the tree: 0 for a text of one leaf. The tests use it to examine the balance. */\n get height(): number {\n return this.#root?.height ?? 0;\n }\n\n /** This function gives the flat string. */\n toString(): string {\n return leaves(this.#root, []).join(\"\");\n }\n\n /** This function gives the strings of the leaves in the order of the text. */\n chunks(): string[] {\n return leaves(this.#root, []);\n }\n\n /** This function gives the text from `start` to `end`, as a new rope. */\n slice(start: number, end: number = this.length): Text {\n this.#check(start, end);\n const [, after] = split(this.#context, this.#root, start);\n const [middle] = split(this.#context, after, end - start);\n return new Text(this.#context, middle);\n }\n\n /** This function gives the width of the text from `start` to `end`. */\n widthOf(start: number, end: number = this.length): number {\n return this.slice(start, end).width;\n }\n\n /** This function gives this text and then `other`. The two ropes must have the same width function and options. */\n concat(other: Text): Text {\n const context = other.#context;\n if (context.width !== this.#context.width || context.separators !== this.#context.separators || context.chunk !== this.#context.chunk) {\n throw new TypeError(\"the two ropes have different width functions or options\");\n }\n return new Text(this.#context, join(this.#context, this.#root, other.#root));\n }\n\n /** This function puts `text` at the offset `at`. */\n insert(at: number, text: string): Text {\n return this.replace(at, at, text);\n }\n\n /** This function removes the text from `start` to `end`. */\n remove(start: number, end: number): Text {\n return this.replace(start, end, \"\");\n }\n\n /** This function puts `text` in place of the text from `start` to `end`. */\n replace(start: number, end: number, text: string): Text {\n this.#check(start, end);\n const [before, rest] = split(this.#context, this.#root, start);\n const [, after] = split(this.#context, rest, end - start);\n const middle = build(this.#context, text);\n return new Text(this.#context, join(this.#context, join(this.#context, before, middle), after));\n }\n\n #check(start: number, end: number): void {\n if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > this.length) {\n throw new RangeError(`the range ${start} to ${end} is not in the text of length ${this.length}`);\n }\n }\n}\n","export { Text, type TextOptions } from \"./text/rope.ts\";\n"],"mappings":"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"}
|
package/build/index.d.ts
CHANGED
package/build/index.js
CHANGED
package/build/text.d.ts
ADDED
package/build/text.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as Text } from "./chunks/text.js";
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/typeset",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"description": "Text measurement and line breaking. The subpath ./breaking has the
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"description": "Text measurement and line breaking. The subpath ./breaking has the line breakers, and ./text has a rope with the width of each part, for any width function.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"typeset",
|
|
8
8
|
"line-breaking",
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"types": "./build/breaking.d.ts",
|
|
35
35
|
"default": "./build/breaking.js"
|
|
36
36
|
},
|
|
37
|
+
"./text": {
|
|
38
|
+
"types": "./build/text.d.ts",
|
|
39
|
+
"default": "./build/text.js"
|
|
40
|
+
},
|
|
37
41
|
"./package.json": "./package.json"
|
|
38
42
|
},
|
|
39
43
|
"files": [
|
|
@@ -50,7 +54,8 @@
|
|
|
50
54
|
},
|
|
51
55
|
"devDependencies": {
|
|
52
56
|
"@mark1russell7/cue": "^1.0.2",
|
|
53
|
-
"@mark1russell7/layout-contract": "^0.2
|
|
57
|
+
"@mark1russell7/layout-contract": "^0.3.2",
|
|
58
|
+
"@mark1russell7/rigor-generators": "^0.1.0",
|
|
54
59
|
"@types/node": "^26.6.5",
|
|
55
60
|
"@typeset/corpus": "0.1.0",
|
|
56
61
|
"tsdown": "^0.23.1",
|
package/src/index.ts
CHANGED
package/src/text/rope.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Text` rope: a persistent balanced tree of strings with the width and the length of each subtree. An edit makes a
|
|
3
|
+
* new rope. The new rope shares the subtrees that do not change. A width function gives the width of a run of text. The
|
|
4
|
+
* rope uses it only for the leaves and for the windows at the seams. Thus an edit uses it of the order of `log n` times.
|
|
5
|
+
*
|
|
6
|
+
* The seams are exact when the width function obeys the separator law. For each text `p + s + q` with a separator `s`:
|
|
7
|
+
* `width(p + s + q) = width(p + s) + width(s + q) - width(s)`. The model of `@typeset/fonts` obeys it for a space and a
|
|
8
|
+
* tab. It shapes each word alone, and its kerning goes only between two adjacent glyphs.
|
|
9
|
+
*/
|
|
10
|
+
import type { WidthOf } from "../breaking/paragraph.ts";
|
|
11
|
+
|
|
12
|
+
/** The options of a rope. */
|
|
13
|
+
export interface TextOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The separators of the separator law: each character is one UTF-16 code unit. The default is a space and a tab. An
|
|
16
|
+
* empty string makes each seam measure the full text of the two sides.
|
|
17
|
+
*/
|
|
18
|
+
readonly separators?: string;
|
|
19
|
+
/** The largest count of UTF-16 code units in a leaf. The default is 256. */
|
|
20
|
+
readonly chunk?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The summary of a subtree: its length, its width, and the two ends that a seam measures again. */
|
|
24
|
+
interface Summary {
|
|
25
|
+
/** The count of UTF-16 code units. */
|
|
26
|
+
readonly length: number;
|
|
27
|
+
readonly width: number;
|
|
28
|
+
/** This value is `true` when the text has a separator. */
|
|
29
|
+
readonly separated: boolean;
|
|
30
|
+
/** The text through the first separator, or all the text when the text has no separator. */
|
|
31
|
+
readonly head: string;
|
|
32
|
+
readonly headWidth: number;
|
|
33
|
+
/** The text from the last separator, or all the text when the text has no separator. */
|
|
34
|
+
readonly tail: string;
|
|
35
|
+
readonly tailWidth: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Leaf extends Summary {
|
|
39
|
+
readonly kind: "leaf";
|
|
40
|
+
readonly text: string;
|
|
41
|
+
readonly height: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Branch extends Summary {
|
|
45
|
+
readonly kind: "branch";
|
|
46
|
+
readonly left: Node;
|
|
47
|
+
readonly right: Node;
|
|
48
|
+
readonly height: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Node = Leaf | Branch;
|
|
52
|
+
|
|
53
|
+
/** The shared state of the ropes of one width function and one set of options. */
|
|
54
|
+
interface Context {
|
|
55
|
+
readonly width: WidthOf;
|
|
56
|
+
readonly separators: string;
|
|
57
|
+
readonly chunk: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isSeparator(context: Context, code: number): boolean {
|
|
61
|
+
for (let i = 0; i < context.separators.length; i++) if (context.separators.charCodeAt(i) === code) return true;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function leafOf(context: Context, text: string): Leaf {
|
|
66
|
+
const width = context.width(text);
|
|
67
|
+
let first = -1;
|
|
68
|
+
let last = -1;
|
|
69
|
+
for (let i = 0; i < text.length; i++) {
|
|
70
|
+
if (!isSeparator(context, text.charCodeAt(i))) continue;
|
|
71
|
+
if (first < 0) first = i;
|
|
72
|
+
last = i;
|
|
73
|
+
}
|
|
74
|
+
if (first < 0) return { kind: "leaf", text, height: 0, length: text.length, width, separated: false, head: text, headWidth: width, tail: text, tailWidth: width };
|
|
75
|
+
const head = text.slice(0, first + 1);
|
|
76
|
+
const tail = text.slice(last);
|
|
77
|
+
return { kind: "leaf", text, height: 0, length: text.length, width, separated: true, head, headWidth: context.width(head), tail, tailWidth: context.width(tail) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* This function makes the branch of two subtrees. It measures one window: the tail of the left side and the head of the right
|
|
82
|
+
* side. The separator law then gives the width of all the text from the widths of the two sides.
|
|
83
|
+
*/
|
|
84
|
+
function branchOf(context: Context, left: Node, right: Node): Branch {
|
|
85
|
+
const seam = left.tail + right.head;
|
|
86
|
+
const seamWidth = context.width(seam);
|
|
87
|
+
let width: number;
|
|
88
|
+
if (!left.separated && !right.separated) width = seamWidth;
|
|
89
|
+
else if (!left.separated) width = right.width - right.headWidth + seamWidth;
|
|
90
|
+
else if (!right.separated) width = left.width - left.tailWidth + seamWidth;
|
|
91
|
+
else width = left.width + right.width - left.tailWidth - right.headWidth + seamWidth;
|
|
92
|
+
return {
|
|
93
|
+
kind: "branch",
|
|
94
|
+
left,
|
|
95
|
+
right,
|
|
96
|
+
height: Math.max(left.height, right.height) + 1,
|
|
97
|
+
length: left.length + right.length,
|
|
98
|
+
width,
|
|
99
|
+
separated: left.separated || right.separated,
|
|
100
|
+
head: left.separated ? left.head : seam,
|
|
101
|
+
headWidth: left.separated ? left.headWidth : seamWidth,
|
|
102
|
+
tail: right.separated ? right.tail : seam,
|
|
103
|
+
tailWidth: right.separated ? right.tailWidth : seamWidth,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** This function joins two trees in the order of the text and keeps the heights of the two children within 1. */
|
|
108
|
+
function join(context: Context, left: Node | undefined, right: Node | undefined): Node | undefined {
|
|
109
|
+
if (left === undefined) return right;
|
|
110
|
+
if (right === undefined) return left;
|
|
111
|
+
if (left.kind === "leaf" && right.kind === "leaf" && left.length + right.length <= context.chunk) return leafOf(context, left.text + right.text);
|
|
112
|
+
if (left.height > right.height + 1) {
|
|
113
|
+
const branch = left as Branch;
|
|
114
|
+
const joined = join(context, branch.right, right)!;
|
|
115
|
+
if (joined.height <= branch.left.height + 1) return branchOf(context, branch.left, joined);
|
|
116
|
+
return rotateLeft(context, branch.left, joined as Branch);
|
|
117
|
+
}
|
|
118
|
+
if (right.height > left.height + 1) {
|
|
119
|
+
const branch = right as Branch;
|
|
120
|
+
const joined = join(context, left, branch.left)!;
|
|
121
|
+
if (joined.height <= branch.right.height + 1) return branchOf(context, joined, branch.right);
|
|
122
|
+
return rotateRight(context, joined as Branch, branch.right);
|
|
123
|
+
}
|
|
124
|
+
return branchOf(context, left, right);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** This function balances the branch of `left` and `heavy`, when `heavy` is 2 levels higher than `left`. */
|
|
128
|
+
function rotateLeft(context: Context, left: Node, heavy: Branch): Branch {
|
|
129
|
+
if (heavy.left.height <= heavy.right.height) return branchOf(context, branchOf(context, left, heavy.left), heavy.right);
|
|
130
|
+
const middle = heavy.left as Branch;
|
|
131
|
+
return branchOf(context, branchOf(context, left, middle.left), branchOf(context, middle.right, heavy.right));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** This function balances the branch of `heavy` and `right`, when `heavy` is 2 levels higher than `right`. */
|
|
135
|
+
function rotateRight(context: Context, heavy: Branch, right: Node): Branch {
|
|
136
|
+
if (heavy.right.height <= heavy.left.height) return branchOf(context, heavy.left, branchOf(context, heavy.right, right));
|
|
137
|
+
const middle = heavy.right as Branch;
|
|
138
|
+
return branchOf(context, branchOf(context, heavy.left, middle.left), branchOf(context, middle.right, right));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** This function divides a tree at the UTF-16 offset `at` into the text before and the text after. */
|
|
142
|
+
function split(context: Context, node: Node | undefined, at: number): [Node | undefined, Node | undefined] {
|
|
143
|
+
if (node === undefined) return [undefined, undefined];
|
|
144
|
+
if (at <= 0) return [undefined, node];
|
|
145
|
+
if (at >= node.length) return [node, undefined];
|
|
146
|
+
if (node.kind === "leaf") return [leafOf(context, node.text.slice(0, at)), leafOf(context, node.text.slice(at))];
|
|
147
|
+
if (at === node.left.length) return [node.left, node.right];
|
|
148
|
+
if (at < node.left.length) {
|
|
149
|
+
const [before, after] = split(context, node.left, at);
|
|
150
|
+
return [before, join(context, after, node.right)];
|
|
151
|
+
}
|
|
152
|
+
const [before, after] = split(context, node.right, at - node.left.length);
|
|
153
|
+
return [join(context, node.left, before), after];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** This function gives the end of the next leaf of `text` from `start`: after a separator when it can, and not in a surrogate pair. */
|
|
157
|
+
function chunkEnd(context: Context, text: string, start: number): number {
|
|
158
|
+
const limit = start + context.chunk;
|
|
159
|
+
if (limit >= text.length) return text.length;
|
|
160
|
+
for (let i = limit - 1; i >= start + context.chunk / 2; i--) if (isSeparator(context, text.charCodeAt(i))) return i + 1;
|
|
161
|
+
const code = text.charCodeAt(limit - 1);
|
|
162
|
+
return code >= 0xd800 && code <= 0xdbff ? limit - 1 : limit;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** This function makes a balanced tree of a text: leaves of at most `chunk` code units, and then a level of branches at a time. */
|
|
166
|
+
function build(context: Context, text: string): Node | undefined {
|
|
167
|
+
let level: Node[] = [];
|
|
168
|
+
for (let start = 0; start < text.length; ) {
|
|
169
|
+
const end = chunkEnd(context, text, start);
|
|
170
|
+
level.push(leafOf(context, text.slice(start, end)));
|
|
171
|
+
start = end;
|
|
172
|
+
}
|
|
173
|
+
if (level.length === 0) return undefined;
|
|
174
|
+
while (level.length > 1) {
|
|
175
|
+
const next: Node[] = [];
|
|
176
|
+
for (let i = 0; i + 1 < level.length; i += 2) next.push(branchOf(context, level[i]!, level[i + 1]!));
|
|
177
|
+
if (level.length % 2 === 1) next.push(level[level.length - 1]!);
|
|
178
|
+
level = next;
|
|
179
|
+
}
|
|
180
|
+
return level[0];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function leaves(node: Node | undefined, out: string[]): string[] {
|
|
184
|
+
if (node === undefined) return out;
|
|
185
|
+
const stack: Node[] = [node];
|
|
186
|
+
while (stack.length > 0) {
|
|
187
|
+
const top = stack.pop()!;
|
|
188
|
+
if (top.kind === "leaf") out.push(top.text);
|
|
189
|
+
else stack.push(top.right, top.left);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A text with its width: a persistent balanced rope. Each edit gives a new `Text` and does not change this one. The offsets
|
|
196
|
+
* are in UTF-16 code units, as the offsets of a JavaScript string.
|
|
197
|
+
*/
|
|
198
|
+
export class Text {
|
|
199
|
+
readonly #context: Context;
|
|
200
|
+
readonly #root: Node | undefined;
|
|
201
|
+
|
|
202
|
+
private constructor(context: Context, root: Node | undefined) {
|
|
203
|
+
this.#context = context;
|
|
204
|
+
this.#root = root;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** This function makes the rope of `text` with the width function `width`. */
|
|
208
|
+
static from(text: string, width: WidthOf, options: TextOptions = {}): Text {
|
|
209
|
+
const separators = options.separators ?? " \t";
|
|
210
|
+
const chunk = options.chunk ?? 256;
|
|
211
|
+
if (!Number.isInteger(chunk) || chunk < 2) throw new RangeError(`the chunk is ${chunk}, not an integer of 2 or more`);
|
|
212
|
+
for (let i = 0; i < separators.length; i++) {
|
|
213
|
+
const code = separators.charCodeAt(i);
|
|
214
|
+
if (code >= 0xd800 && code <= 0xdfff) throw new RangeError("a separator is one UTF-16 code unit, not a half of a surrogate pair");
|
|
215
|
+
}
|
|
216
|
+
const context: Context = { width, separators, chunk };
|
|
217
|
+
return new Text(context, build(context, text));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The count of UTF-16 code units of the text. */
|
|
221
|
+
get length(): number {
|
|
222
|
+
return this.#root?.length ?? 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The width of the text. It is equal to the width of the flat string when the width function obeys the separator law. */
|
|
226
|
+
get width(): number {
|
|
227
|
+
return this.#root?.width ?? 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The height of the tree: 0 for a text of one leaf. The tests use it to examine the balance. */
|
|
231
|
+
get height(): number {
|
|
232
|
+
return this.#root?.height ?? 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** This function gives the flat string. */
|
|
236
|
+
toString(): string {
|
|
237
|
+
return leaves(this.#root, []).join("");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** This function gives the strings of the leaves in the order of the text. */
|
|
241
|
+
chunks(): string[] {
|
|
242
|
+
return leaves(this.#root, []);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** This function gives the text from `start` to `end`, as a new rope. */
|
|
246
|
+
slice(start: number, end: number = this.length): Text {
|
|
247
|
+
this.#check(start, end);
|
|
248
|
+
const [, after] = split(this.#context, this.#root, start);
|
|
249
|
+
const [middle] = split(this.#context, after, end - start);
|
|
250
|
+
return new Text(this.#context, middle);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** This function gives the width of the text from `start` to `end`. */
|
|
254
|
+
widthOf(start: number, end: number = this.length): number {
|
|
255
|
+
return this.slice(start, end).width;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** This function gives this text and then `other`. The two ropes must have the same width function and options. */
|
|
259
|
+
concat(other: Text): Text {
|
|
260
|
+
const context = other.#context;
|
|
261
|
+
if (context.width !== this.#context.width || context.separators !== this.#context.separators || context.chunk !== this.#context.chunk) {
|
|
262
|
+
throw new TypeError("the two ropes have different width functions or options");
|
|
263
|
+
}
|
|
264
|
+
return new Text(this.#context, join(this.#context, this.#root, other.#root));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** This function puts `text` at the offset `at`. */
|
|
268
|
+
insert(at: number, text: string): Text {
|
|
269
|
+
return this.replace(at, at, text);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** This function removes the text from `start` to `end`. */
|
|
273
|
+
remove(start: number, end: number): Text {
|
|
274
|
+
return this.replace(start, end, "");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** This function puts `text` in place of the text from `start` to `end`. */
|
|
278
|
+
replace(start: number, end: number, text: string): Text {
|
|
279
|
+
this.#check(start, end);
|
|
280
|
+
const [before, rest] = split(this.#context, this.#root, start);
|
|
281
|
+
const [, after] = split(this.#context, rest, end - start);
|
|
282
|
+
const middle = build(this.#context, text);
|
|
283
|
+
return new Text(this.#context, join(this.#context, join(this.#context, before, middle), after));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#check(start: number, end: number): void {
|
|
287
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > this.length) {
|
|
288
|
+
throw new RangeError(`the range ${start} to ${end} is not in the text of length ${this.length}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/text.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Text, type TextOptions } from "./text/rope.ts";
|