@jasy/pdf 1.0.0-alpha.8 → 1.0.0-beta.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 +29 -10
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +45 -5
- package/dist/api/content.js +96 -7
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +57 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +135 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +12 -0
- package/dist/api/structure.js +54 -7
- package/dist/api/text.d.ts +65 -8
- package/dist/api/text.js +38 -5
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/edit/document.d.ts +14 -4
- package/dist/edit/document.js +15 -22
- package/dist/edit/fill.d.ts +19 -2
- package/dist/edit/fill.js +63 -34
- package/dist/edit/flatten.d.ts +13 -1
- package/dist/edit/flatten.js +38 -13
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +5 -0
- package/dist/edit/objects.js +27 -0
- package/dist/edit/writer.d.ts +7 -0
- package/dist/edit/writer.js +23 -1
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +23 -1
- package/dist/elements/container-element.js +31 -10
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +5 -1
- package/dist/elements/layout/expanded-element.js +16 -3
- package/dist/elements/layout/keep-together-element.d.ts +2 -0
- package/dist/elements/layout/keep-together-element.js +4 -0
- package/dist/elements/layout/padding-element.d.ts +9 -2
- package/dist/elements/layout/padding-element.js +12 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/layout/struct-group.d.ts +2 -0
- package/dist/elements/layout/struct-group.js +4 -0
- package/dist/elements/pdf-element.d.ts +75 -1
- package/dist/elements/pdf-element.js +80 -1
- package/dist/elements/rectangle-element.d.ts +22 -6
- package/dist/elements/rectangle-element.js +31 -12
- package/dist/elements/row-element.d.ts +26 -1
- package/dist/elements/row-element.js +31 -10
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +92 -1
- package/dist/elements/text-element.js +208 -33
- package/dist/ir/display-list.d.ts +67 -11
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/layout/min-intrinsic.d.ts +29 -0
- package/dist/layout/min-intrinsic.js +37 -0
- package/dist/renderer/anchor-renderer.js +1 -1
- package/dist/renderer/bookmark-renderer.js +1 -1
- package/dist/renderer/canvas-renderer.d.ts +14 -0
- package/dist/renderer/canvas-renderer.js +36 -0
- package/dist/renderer/container-renderer.js +1 -3
- package/dist/renderer/default-text-style-renderer.js +1 -1
- package/dist/renderer/deferred-renderer.js +1 -1
- package/dist/renderer/expanded-renderer.js +1 -1
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.js +1 -1
- package/dist/renderer/link-renderer.js +1 -1
- package/dist/renderer/padding-renderer.js +1 -1
- package/dist/renderer/page-builder-renderer.js +1 -1
- package/dist/renderer/page-renderer.js +1 -3
- package/dist/renderer/pdf-backend.d.ts +21 -4
- package/dist/renderer/pdf-backend.js +198 -61
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +9 -5
- package/dist/renderer/repeating-header-renderer.js +1 -2
- package/dist/renderer/rotated-renderer.js +1 -1
- package/dist/renderer/row-renderer.js +1 -3
- package/dist/renderer/struct-group-renderer.js +1 -1
- package/dist/renderer/svg-renderer.d.ts +11 -0
- package/dist/renderer/svg-renderer.js +27 -0
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +176 -57
- package/dist/svg/css.d.ts +38 -0
- package/dist/svg/css.js +115 -0
- package/dist/svg/errors.d.ts +24 -0
- package/dist/svg/errors.js +30 -0
- package/dist/svg/gradients.d.ts +34 -0
- package/dist/svg/gradients.js +156 -0
- package/dist/svg/index.d.ts +22 -0
- package/dist/svg/index.js +331 -0
- package/dist/svg/shapes.d.ts +6 -0
- package/dist/svg/shapes.js +177 -0
- package/dist/svg/style.d.ts +61 -0
- package/dist/svg/style.js +144 -0
- package/dist/svg/transform.d.ts +13 -0
- package/dist/svg/transform.js +68 -0
- package/dist/svg/units.d.ts +11 -0
- package/dist/svg/units.js +73 -0
- package/dist/svg/xml.d.ts +24 -0
- package/dist/svg/xml.js +158 -0
- package/dist/text/advance.d.ts +3 -0
- package/dist/text/advance.js +8 -4
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +43 -1
- package/dist/text/line-breaker.js +181 -45
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +91 -0
- package/dist/text/text-style.d.ts +38 -0
- package/dist/text/text-style.js +50 -1
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/text/word-splitting.d.ts +42 -0
- package/dist/text/word-splitting.js +72 -0
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +218 -7
- package/dist/utils/font-metrics.d.ts +13 -2
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/pdf-object-manager.d.ts +44 -3
- package/dist/utils/pdf-object-manager.js +176 -14
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/validators/element-validator.js +9 -2
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +6 -2
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { runAdvance } from "./advance.js";
|
|
2
|
+
import { splitLongWord } from "./word-splitting.js";
|
|
3
|
+
/**
|
|
4
|
+
* How far a JUSTIFIED line's spaces may be squeezed to keep one more word on it.
|
|
5
|
+
*
|
|
6
|
+
* A quarter, because our breaker is GREEDY: it will use the whole allowance every time that saves a
|
|
7
|
+
* line, so the limit has to be the tightest word space still worth reading - about three quarters of
|
|
8
|
+
* natural, the usual typographic floor. TeX allows a third, but its badness model rarely spends it.
|
|
9
|
+
* For reference, react-pdf reaches -19% in practice (measured: a 3.18pt space squeezed to 2.58pt),
|
|
10
|
+
* which this covers. Its packing can still differ by a word, because it spreads the squeeze over the
|
|
11
|
+
* CHARACTERS as well as the spaces.
|
|
12
|
+
*
|
|
13
|
+
* Only justified text uses it: under any other alignment a squeezed line would simply overflow.
|
|
14
|
+
*/
|
|
15
|
+
export const MAX_SPACE_SHRINK = 1 / 4;
|
|
2
16
|
/** Three ASCII dots, NOT the "…" glyph (U+2026): plain dots encode in every font - standard-14
|
|
3
17
|
* (WinAnsi) and any embedded TTF - whereas U+2026 needs a glyph the font may not carry. */
|
|
4
18
|
const ELLIPSIS = "...";
|
|
@@ -10,29 +24,97 @@ const ELLIPSIS = "...";
|
|
|
10
24
|
* rendering call this, so they can never disagree. Depends only on `FontMetrics`,
|
|
11
25
|
* not the PDF byte writer - the future fragmentation pass can reuse it.
|
|
12
26
|
*/
|
|
13
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The width of `text` set as ONE line, accumulated in EXACTLY the order `wrapStringIntoLines` builds a
|
|
29
|
+
* line: the first word, then `+ (space + word)` each time. The order matters because floating-point
|
|
30
|
+
* addition is not associative - a box sized by a differently-grouped sum can be one bit too small for
|
|
31
|
+
* the very line the breaker then measures, and the text wraps inside a box made to fit it. That is not
|
|
32
|
+
* hypothetical: it is what split a footer sized to its own content.
|
|
33
|
+
*/
|
|
34
|
+
export function singleLineWidth(text, font, metrics, letterSpacing = 0, wordSpacing = 0) {
|
|
35
|
+
// A hard break makes this several lines; the natural width is the widest of them.
|
|
36
|
+
if (text.includes("\n"))
|
|
37
|
+
return Math.max(...text.split("\n").map((p) => singleLineWidth(p, font, metrics, letterSpacing, wordSpacing)));
|
|
38
|
+
const space = runAdvance(metrics, " ", font, letterSpacing) + wordSpacing;
|
|
39
|
+
return text
|
|
40
|
+
.split(" ")
|
|
41
|
+
.reduce((width, word, i) => i === 0
|
|
42
|
+
? runAdvance(metrics, word, font, letterSpacing)
|
|
43
|
+
: width + (space + runAdvance(metrics, word, font, letterSpacing)), 0);
|
|
44
|
+
}
|
|
45
|
+
export function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics, maxLines, overflow, letterSpacing = 0, options = {}) {
|
|
46
|
+
const { wordSpacing = 0, indent = 0, shrink = 0, splitting = {}, ligatures } = options;
|
|
14
47
|
let currentLine = "";
|
|
15
48
|
let currentWidth = 0;
|
|
49
|
+
let gaps = 0; // spaces on the current line, which is what a justified line can squeeze
|
|
16
50
|
const lines = [];
|
|
17
|
-
const font = { fontFamily, fontSize, fontStyle };
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const wordWidth = runAdvance(metrics, word, font, letterSpacing);
|
|
23
|
-
const spaceWidth = runAdvance(metrics, " ", font, letterSpacing);
|
|
24
|
-
// Break before a word that won't fit - but only once the line has content. A single word wider
|
|
25
|
-
// than maxWidth must sit on its (empty) line and overflow, not push a phantom empty line before
|
|
26
|
-
// it (which would over-count the height by a line and shift the text down at render).
|
|
27
|
-
if (currentWidth + wordWidth > maxWidth && currentLine !== "") {
|
|
51
|
+
const font = { fontFamily, fontSize, fontStyle, ligatures };
|
|
52
|
+
// A `\n` is a HARD break: wrap each paragraph on its own, then cut. An EMPTY paragraph still has to
|
|
53
|
+
// produce a line, or a deliberate blank line between two blocks silently disappears.
|
|
54
|
+
text.split("\n").forEach((paragraph, paragraphIndex) => {
|
|
55
|
+
if (paragraphIndex > 0) {
|
|
28
56
|
lines.push(currentLine.trim());
|
|
29
|
-
currentLine =
|
|
30
|
-
currentWidth =
|
|
31
|
-
|
|
32
|
-
else {
|
|
33
|
-
currentLine += index === 0 ? word : " " + word;
|
|
34
|
-
currentWidth += wordWidth + spaceWidth;
|
|
57
|
+
currentLine = "";
|
|
58
|
+
currentWidth = 0;
|
|
59
|
+
gaps = 0;
|
|
35
60
|
}
|
|
61
|
+
const words = paragraph.split(" ");
|
|
62
|
+
words.forEach((word) => {
|
|
63
|
+
// Word and space advances come from the one shared primitive (`advance.ts`), the same one
|
|
64
|
+
// `naturalWidth` uses - so a bounded and an unbounded layout of the same text agree bit for bit.
|
|
65
|
+
const wordWidth = runAdvance(metrics, word, font, letterSpacing);
|
|
66
|
+
const spaceWidth = runAdvance(metrics, " ", font, letterSpacing) + wordSpacing;
|
|
67
|
+
// The FIRST line has the indent taken out of its room; every later one gets the full box.
|
|
68
|
+
const room = lines.length === 0 ? maxWidth - indent : maxWidth;
|
|
69
|
+
// Break before a word that won't fit - counting the SPACE that would join it, which the old test
|
|
70
|
+
// forgot. It went unnoticed on the first line, where the very first word added a space too many
|
|
71
|
+
// and cancelled the error out; after a break `currentWidth` is the bare word, so every following
|
|
72
|
+
// test was short by one space and the line could overrun its box by that much.
|
|
73
|
+
//
|
|
74
|
+
// A single word wider than maxWidth still sits on its (empty) line and overflows, rather than
|
|
75
|
+
// pushing a phantom empty line before it (which would over-count the height by a line).
|
|
76
|
+
// A word too wide for the box, split into pieces that fit (hyphenation first, break-word as the
|
|
77
|
+
// floor). Every piece but the last closes a line; the last carries on as an ordinary word. Off by
|
|
78
|
+
// default, so a document that asks for neither reaches the untouched path below.
|
|
79
|
+
// The pieces land on lines that are NOT the first one whenever the current line is closed below,
|
|
80
|
+
// so they get the full box - `room` only loses the indent while we are genuinely still filling
|
|
81
|
+
// the indented first line.
|
|
82
|
+
const splitRoom = lines.length === 0 && currentLine === "" ? room : maxWidth;
|
|
83
|
+
const split = splitLongWord(word, splitRoom, font, metrics, letterSpacing, splitting);
|
|
84
|
+
if (split) {
|
|
85
|
+
if (currentLine !== "")
|
|
86
|
+
lines.push(currentLine.trim());
|
|
87
|
+
for (const piece of split.slice(0, -1))
|
|
88
|
+
lines.push(piece);
|
|
89
|
+
currentLine = split[split.length - 1];
|
|
90
|
+
currentWidth = runAdvance(metrics, currentLine, font, letterSpacing);
|
|
91
|
+
gaps = 0;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const candidate = currentWidth + (spaceWidth + wordWidth);
|
|
95
|
+
if (currentLine === "") {
|
|
96
|
+
currentLine = word;
|
|
97
|
+
currentWidth = wordWidth;
|
|
98
|
+
gaps = 0;
|
|
99
|
+
// How much a JUSTIFIED line may be squeezed to keep one more word: the spaces already on the
|
|
100
|
+
// line plus the one that would join it, each giving up at most `shrink` of its width. Zero for
|
|
101
|
+
// every other alignment, where a squeezed line would simply overflow instead.
|
|
102
|
+
// ONE expression, used for the test AND for the running total. Adding the same three numbers in
|
|
103
|
+
// a different order gives a different last bit, and a line that lands exactly on `maxWidth` is
|
|
104
|
+
// then broken by that bit alone - which is how a footer sized to its own text wrapped.
|
|
105
|
+
}
|
|
106
|
+
else if (candidate - (gaps + 1) * spaceWidth * shrink > room) {
|
|
107
|
+
lines.push(currentLine.trim());
|
|
108
|
+
currentLine = word;
|
|
109
|
+
currentWidth = wordWidth;
|
|
110
|
+
gaps = 0;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
currentLine += " " + word;
|
|
114
|
+
currentWidth = candidate;
|
|
115
|
+
gaps += 1;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
36
118
|
});
|
|
37
119
|
if (currentLine)
|
|
38
120
|
lines.push(currentLine.trim());
|
|
@@ -42,7 +124,9 @@ export function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWi
|
|
|
42
124
|
const kept = lines.slice(0, maxLines);
|
|
43
125
|
if (overflow === "ellipsis") {
|
|
44
126
|
const last = kept.length - 1;
|
|
45
|
-
kept[last] = ellipsize(kept[last], fontFamily, fontSize, fontStyle,
|
|
127
|
+
kept[last] = ellipsize(kept[last], fontFamily, fontSize, fontStyle,
|
|
128
|
+
// The same room that line was broken against: the first one lost the indent to it.
|
|
129
|
+
last === 0 ? maxWidth - indent : maxWidth, metrics, letterSpacing, wordSpacing, ligatures);
|
|
46
130
|
}
|
|
47
131
|
return kept;
|
|
48
132
|
}
|
|
@@ -62,37 +146,87 @@ export function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics, ma
|
|
|
62
146
|
const size = segment.fontSize || defaults.fontSize;
|
|
63
147
|
const style = segment.fontStyle || defaults.fontStyle;
|
|
64
148
|
const letterSpacing = segment.letterSpacing ?? defaults.letterSpacing ?? 0;
|
|
65
|
-
const font = {
|
|
149
|
+
const font = {
|
|
150
|
+
fontFamily: family,
|
|
151
|
+
fontSize: size,
|
|
152
|
+
fontStyle: style,
|
|
153
|
+
ligatures: defaults.ligatures,
|
|
154
|
+
};
|
|
66
155
|
const spaceWidth = runAdvance(metrics, " ", font, letterSpacing);
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
lineSegments.push({ ...segment, fontFamily: family, content: "" });
|
|
72
|
-
combined = "";
|
|
73
|
-
words.forEach((word, wordIndex) => {
|
|
74
|
-
const wordWidth = runAdvance(metrics, word, font, letterSpacing);
|
|
75
|
-
// Same guard as the string path: don't open a phantom empty line for an over-wide first word -
|
|
76
|
-
// place it (overflowing) on the current empty line instead.
|
|
77
|
-
if (width + wordWidth > maxWidth && width > 0) {
|
|
156
|
+
// A `\n` inside a span is a HARD break, exactly as in the string path: close the line where it
|
|
157
|
+
// sits, whatever else was already on it, and start the next one with this segment's style.
|
|
158
|
+
segment.content.split("\n").forEach((paragraph, paragraphIndex) => {
|
|
159
|
+
if (paragraphIndex > 0) {
|
|
78
160
|
lines.push({ segments: lineSegments, width });
|
|
79
161
|
width = 0;
|
|
80
162
|
lineSegments = [];
|
|
81
|
-
combined = word;
|
|
82
|
-
width += wordWidth + spaceWidth;
|
|
83
|
-
lineSegments.push({ ...segment, content: combined });
|
|
84
163
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
164
|
+
const words = paragraph.split(" ");
|
|
165
|
+
// Start this segment's piece empty; its content is filled word-by-word below. (Not
|
|
166
|
+
// the original content - otherwise a segment whose FIRST word overflows would carry
|
|
167
|
+
// its whole text into the line that just closed.)
|
|
168
|
+
lineSegments.push({ ...segment, fontFamily: family, content: "" });
|
|
169
|
+
combined = "";
|
|
170
|
+
words.forEach((word, wordIndex) => {
|
|
171
|
+
const wordWidth = runAdvance(metrics, word, font, letterSpacing);
|
|
172
|
+
// Same treatment as the string path: a word too wide for the box is split into pieces that fit.
|
|
173
|
+
// Each finished piece closes the line it sits on, keeping this segment's style.
|
|
174
|
+
const split = splitLongWord(word, maxWidth, font, metrics, letterSpacing, defaults.splitting ?? {});
|
|
175
|
+
if (split) {
|
|
176
|
+
// Close whatever is already on the line FIRST. The pieces were measured against the full box,
|
|
177
|
+
// not against what is left of this line - appending the first one put a line 80 wide into a
|
|
178
|
+
// 50 box, and reported 50, which is exactly why it went unnoticed. The string path has always
|
|
179
|
+
// pushed its current line before splitting; this is the same move.
|
|
180
|
+
if (width > 0) {
|
|
181
|
+
lines.push({ segments: lineSegments, width });
|
|
182
|
+
lineSegments = [{ ...segment, fontFamily: family, content: "" }];
|
|
183
|
+
combined = "";
|
|
184
|
+
}
|
|
185
|
+
for (const piece of split.slice(0, -1)) {
|
|
186
|
+
lineSegments[lineSegments.length - 1].content = piece;
|
|
187
|
+
// The width a line REPORTS is what alignment and line-metrics work from: measure it.
|
|
188
|
+
lines.push({
|
|
189
|
+
segments: lineSegments,
|
|
190
|
+
width: runAdvance(metrics, piece, font, letterSpacing),
|
|
191
|
+
});
|
|
192
|
+
lineSegments = [{ ...segment, fontFamily: family, content: "" }];
|
|
193
|
+
}
|
|
194
|
+
combined = split[split.length - 1];
|
|
195
|
+
width = runAdvance(metrics, combined, font, letterSpacing);
|
|
196
|
+
lineSegments[lineSegments.length - 1].content = combined;
|
|
197
|
+
return;
|
|
90
198
|
}
|
|
91
|
-
|
|
92
|
-
|
|
199
|
+
// A space joins this word to what precedes it only INSIDE a segment; segments butt together
|
|
200
|
+
// with nothing between them, which is what `span("a") + span("b")` draws.
|
|
201
|
+
const joiner = wordIndex > 0 ? spaceWidth : 0;
|
|
202
|
+
// ONE expression for the test and for the running total, grouped exactly as `singleLineWidth`
|
|
203
|
+
// groups it - see the note there. A width built any other way can be a bit off the one the box
|
|
204
|
+
// was sized with, and the text wraps inside a box made to hold it.
|
|
205
|
+
const candidate = width + (joiner + wordWidth);
|
|
206
|
+
// Same guard as the string path: don't open a phantom empty line for an over-wide first word -
|
|
207
|
+
// place it (overflowing) on the current empty line instead.
|
|
208
|
+
if (candidate > maxWidth && width > 0) {
|
|
209
|
+
lines.push({ segments: lineSegments, width });
|
|
210
|
+
width = wordWidth;
|
|
211
|
+
lineSegments = [];
|
|
212
|
+
combined = word;
|
|
213
|
+
lineSegments.push({ ...segment, content: combined });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
combined += wordIndex === 0 ? word : " " + word;
|
|
217
|
+
width = candidate;
|
|
218
|
+
if (lineSegments.length === 0) {
|
|
219
|
+
lineSegments.push({ ...segment, fontFamily: family, content: combined });
|
|
220
|
+
}
|
|
221
|
+
lineSegments[lineSegments.length - 1].content = combined;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
93
224
|
});
|
|
94
225
|
});
|
|
95
|
-
if
|
|
226
|
+
// Only if something actually landed on it. The string path ends with `if (currentLine)`, so a text
|
|
227
|
+
// ending in `\n` is ONE line there - the two must not disagree about the same text. An empty
|
|
228
|
+
// paragraph in the MIDDLE still becomes a line; that one is pushed at the break, not here.
|
|
229
|
+
if (lineSegments.some((seg) => seg.content !== "")) {
|
|
96
230
|
lines.push({ segments: lineSegments, width });
|
|
97
231
|
}
|
|
98
232
|
// Open-end by default; cap only when maxLines is set.
|
|
@@ -124,9 +258,11 @@ export function segmentLinesToSegments(lines) {
|
|
|
124
258
|
}
|
|
125
259
|
/** Appends "…" to a single line, dropping trailing words (then characters) until the line plus the
|
|
126
260
|
* ellipsis fits `maxWidth`. Falls back to a bare "…" if not even one character fits. */
|
|
127
|
-
function ellipsize(line, fontFamily, fontSize, fontStyle, maxWidth, metrics, letterSpacing = 0) {
|
|
128
|
-
const font = { fontFamily, fontSize, fontStyle };
|
|
129
|
-
|
|
261
|
+
function ellipsize(line, fontFamily, fontSize, fontStyle, maxWidth, metrics, letterSpacing = 0, wordSpacing = 0, ligatures) {
|
|
262
|
+
const font = { fontFamily, fontSize, fontStyle, ligatures };
|
|
263
|
+
// Measured through `singleLineWidth`, so the ellipsised line is judged by the same sum that decides
|
|
264
|
+
// every other line - word-spacing included.
|
|
265
|
+
const fits = (s) => singleLineWidth(s + ELLIPSIS, font, metrics, letterSpacing, wordSpacing) <= maxWidth;
|
|
130
266
|
if (fits(line))
|
|
131
267
|
return line + ELLIPSIS;
|
|
132
268
|
const words = line.split(" ");
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphan and widow control for a paragraph split across a page break.
|
|
3
|
+
*
|
|
4
|
+
* An **orphan** is the first line of a paragraph left alone at the bottom of a page; a **widow** is
|
|
5
|
+
* the last line pushed alone to the top of the next. Both read as mistakes, and neither is prevented
|
|
6
|
+
* by splitting at line boxes - which is all the fragmenter does on its own.
|
|
7
|
+
*
|
|
8
|
+
* This is a correction of the split INDEX, not a second mechanism: the fragmenter works out how many
|
|
9
|
+
* lines fit, and this decides whether that number is acceptable.
|
|
10
|
+
*/
|
|
11
|
+
/** How many lines must stay behind / must carry over. Both default to 2, the CSS initial value. */
|
|
12
|
+
export interface OrphanWidowRule {
|
|
13
|
+
orphans: number;
|
|
14
|
+
widows: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const DEFAULT_ORPHANS = 2;
|
|
17
|
+
export declare const DEFAULT_WIDOWS = 2;
|
|
18
|
+
/**
|
|
19
|
+
* Correct a line count so the split leaves at least `orphans` lines behind and carries at least
|
|
20
|
+
* `widows` over. Returns 0 to mean "none of it fits here" - the caller then moves the whole paragraph
|
|
21
|
+
* on, exactly as it already does when not even one line fits.
|
|
22
|
+
*
|
|
23
|
+
* Terminating by construction: every answer is either the natural count, a SMALLER one, or 0. It
|
|
24
|
+
* never asks for more lines than fit, and the page driver's guard already handles a paragraph that
|
|
25
|
+
* does not fit on a whole page (it places it anyway rather than looping).
|
|
26
|
+
*/
|
|
27
|
+
export declare function adjustForOrphansWidows(fitted: number, total: number, { orphans, widows }: OrphanWidowRule): number;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphan and widow control for a paragraph split across a page break.
|
|
3
|
+
*
|
|
4
|
+
* An **orphan** is the first line of a paragraph left alone at the bottom of a page; a **widow** is
|
|
5
|
+
* the last line pushed alone to the top of the next. Both read as mistakes, and neither is prevented
|
|
6
|
+
* by splitting at line boxes - which is all the fragmenter does on its own.
|
|
7
|
+
*
|
|
8
|
+
* This is a correction of the split INDEX, not a second mechanism: the fragmenter works out how many
|
|
9
|
+
* lines fit, and this decides whether that number is acceptable.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_ORPHANS = 2;
|
|
12
|
+
export const DEFAULT_WIDOWS = 2;
|
|
13
|
+
/**
|
|
14
|
+
* Correct a line count so the split leaves at least `orphans` lines behind and carries at least
|
|
15
|
+
* `widows` over. Returns 0 to mean "none of it fits here" - the caller then moves the whole paragraph
|
|
16
|
+
* on, exactly as it already does when not even one line fits.
|
|
17
|
+
*
|
|
18
|
+
* Terminating by construction: every answer is either the natural count, a SMALLER one, or 0. It
|
|
19
|
+
* never asks for more lines than fit, and the page driver's guard already handles a paragraph that
|
|
20
|
+
* does not fit on a whole page (it places it anyway rather than looping).
|
|
21
|
+
*/
|
|
22
|
+
export function adjustForOrphansWidows(fitted, total, { orphans, widows }) {
|
|
23
|
+
// Both are LINE COUNTS and end up as a slice index, so anything but a positive whole number is
|
|
24
|
+
// meaningless: 0 or less silently disables the rule it belongs to, and a fraction would be truncated
|
|
25
|
+
// somewhere downstream rather than here. Named at the boundary instead.
|
|
26
|
+
check(orphans, "orphans");
|
|
27
|
+
check(widows, "widows");
|
|
28
|
+
if (fitted <= 0)
|
|
29
|
+
return 0; // nothing fits; the caller decides
|
|
30
|
+
if (fitted >= total)
|
|
31
|
+
return total; // no split needed, nothing to protect
|
|
32
|
+
// NOTE: an explicit "too short to satisfy both ends" rule (total < orphans + widows) sat here and was
|
|
33
|
+
// removed - it is unreachable. Either fewer lines stay than `orphans` allows, and the next check
|
|
34
|
+
// returns 0; or at least `orphans` stay, and then fewer than `widows` must carry, so the pull-back
|
|
35
|
+
// below lands under `orphans` and returns 0 as well. react-pdf spells the case out; we do not need to.
|
|
36
|
+
//
|
|
37
|
+
// Fewer lines would stay than an orphan is allowed to be.
|
|
38
|
+
if (fitted < orphans)
|
|
39
|
+
return 0;
|
|
40
|
+
// Fewer lines would carry over than a widow is allowed to be: pull the cut back so exactly `widows`
|
|
41
|
+
// go over - unless that would leave too few behind, in which case the paragraph moves whole.
|
|
42
|
+
if (total - fitted < widows) {
|
|
43
|
+
const pulled = total - widows;
|
|
44
|
+
return pulled >= orphans ? pulled : 0;
|
|
45
|
+
}
|
|
46
|
+
return fitted;
|
|
47
|
+
}
|
|
48
|
+
function check(value, name) {
|
|
49
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
50
|
+
throw new Error(`Invalid ${name}: ${value}. It is a number of LINES, so it must be a whole number of at least 1 ` +
|
|
51
|
+
`(1 means no protection at that end).`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GsubTable } from "../utils/gsub.js";
|
|
2
|
+
/**
|
|
3
|
+
* SHAPING: code points in, the glyphs actually drawn out. Where the two halves meet - `arabic.ts`
|
|
4
|
+
* says WHICH positional form a letter takes, `gsub.ts` says which GLYPH that form is in this font.
|
|
5
|
+
*
|
|
6
|
+
* Not a string, because a ligature is one glyph for several code points. `codePoints` carries the
|
|
7
|
+
* origin so `ToUnicode` can still say what the glyph means.
|
|
8
|
+
*/
|
|
9
|
+
/** One drawn glyph, with the code points it came from. */
|
|
10
|
+
export interface ShapedGlyph {
|
|
11
|
+
glyph: number;
|
|
12
|
+
/** Advance in FONT UNITS - the caller scales by fontSize / unitsPerEm, as everywhere else. */
|
|
13
|
+
advance: number;
|
|
14
|
+
/** The code points this glyph stands for; more than one only for a ligature. */
|
|
15
|
+
codePoints: number[];
|
|
16
|
+
}
|
|
17
|
+
/** The little a font has to answer for shaping - deliberately not the whole `TTFParser`. */
|
|
18
|
+
export interface ShapingFont {
|
|
19
|
+
gsub(): GsubTable | undefined;
|
|
20
|
+
getGlyphIndex(codePoint: number): number;
|
|
21
|
+
getAdvanceWidth(glyph: number): number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shape a run, or `undefined` when there is nothing to do - so untouched documents keep their path.
|
|
25
|
+
*
|
|
26
|
+
* Arabic always applies `rlig` - lam-alef is wrong when drawn apart. Latin applies whatever `features`
|
|
27
|
+
* asks for, which is `liga` today; the list is the seam a general `fontFeatures` would use.
|
|
28
|
+
*/
|
|
29
|
+
export declare function shapeRun(codePoints: readonly number[], font: ShapingFont, features?: readonly string[]): ShapedGlyph[] | undefined;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { joiningForms, needsJoining } from "./arabic.js";
|
|
2
|
+
/** The features that realise the four positional forms, in the order a shaper applies them. */
|
|
3
|
+
const FORM_FEATURES = {
|
|
4
|
+
isol: "isol",
|
|
5
|
+
init: "init",
|
|
6
|
+
medi: "medi",
|
|
7
|
+
fina: "fina",
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Shape a run, or `undefined` when there is nothing to do - so untouched documents keep their path.
|
|
11
|
+
*
|
|
12
|
+
* Arabic always applies `rlig` - lam-alef is wrong when drawn apart. Latin applies whatever `features`
|
|
13
|
+
* asks for, which is `liga` today; the list is the seam a general `fontFeatures` would use.
|
|
14
|
+
*/
|
|
15
|
+
export function shapeRun(codePoints, font, features = []) {
|
|
16
|
+
const gsub = font.gsub();
|
|
17
|
+
if (!gsub)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (!needsJoining(codePoints)) {
|
|
20
|
+
// Latin: no joining, only the substitutions the caller asked for.
|
|
21
|
+
return features.length > 0 ? shapeLatin(codePoints, font, gsub, features) : undefined;
|
|
22
|
+
}
|
|
23
|
+
if (!gsub.hasScript("arab"))
|
|
24
|
+
return undefined;
|
|
25
|
+
const forms = joiningForms(codePoints);
|
|
26
|
+
let glyphs = codePoints.map((cp, i) => {
|
|
27
|
+
let glyph = font.getGlyphIndex(cp);
|
|
28
|
+
const form = forms[i];
|
|
29
|
+
if (form) {
|
|
30
|
+
// First lookup that covers the glyph wins. A missing `isol` is normal: the plain glyph already
|
|
31
|
+
// IS that form.
|
|
32
|
+
for (const lookup of gsub.lookups("arab", FORM_FEATURES[form])) {
|
|
33
|
+
const substituted = gsub.substituteSingle(lookup, glyph);
|
|
34
|
+
if (substituted !== null) {
|
|
35
|
+
glyph = substituted;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { glyph, advance: font.getAdvanceWidth(glyph), codePoints: [cp] };
|
|
41
|
+
});
|
|
42
|
+
glyphs = applyLigatures(glyphs, gsub, font, "arab", "rlig");
|
|
43
|
+
return glyphs;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Latin: no joining, only ligatures. Returns undefined when the font offers none, so a document that
|
|
47
|
+
* asks for them but uses a font without them keeps the plain path and stays byte-identical.
|
|
48
|
+
*/
|
|
49
|
+
function shapeLatin(codePoints, font, gsub, features) {
|
|
50
|
+
if (!gsub.hasScript("latn"))
|
|
51
|
+
return undefined;
|
|
52
|
+
const wanted = features.filter((f) => gsub.lookups("latn", f).length > 0);
|
|
53
|
+
if (wanted.length === 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
const plain = codePoints.map((cp) => {
|
|
56
|
+
const glyph = font.getGlyphIndex(cp);
|
|
57
|
+
return { glyph, advance: font.getAdvanceWidth(glyph), codePoints: [cp] };
|
|
58
|
+
});
|
|
59
|
+
let shaped = plain;
|
|
60
|
+
for (const feature of wanted)
|
|
61
|
+
shaped = applyLigatures(shaped, gsub, font, "latn", feature);
|
|
62
|
+
// Nothing merged: hand back undefined so the caller measures and draws the untouched run.
|
|
63
|
+
return shaped.length === plain.length ? undefined : shaped;
|
|
64
|
+
}
|
|
65
|
+
/** Collapse ligatures, longest match first, carrying every component's code points along. */
|
|
66
|
+
function applyLigatures(glyphs, gsub, font, script, feature) {
|
|
67
|
+
const lookups = gsub.lookups(script, feature);
|
|
68
|
+
if (lookups.length === 0)
|
|
69
|
+
return glyphs;
|
|
70
|
+
const ids = glyphs.map((g) => g.glyph);
|
|
71
|
+
const out = [];
|
|
72
|
+
for (let i = 0; i < glyphs.length;) {
|
|
73
|
+
let applied = false;
|
|
74
|
+
for (const lookup of lookups) {
|
|
75
|
+
const match = gsub.substituteLigature(lookup, ids, i);
|
|
76
|
+
if (!match)
|
|
77
|
+
continue;
|
|
78
|
+
out.push({
|
|
79
|
+
glyph: match.glyph,
|
|
80
|
+
advance: font.getAdvanceWidth(match.glyph),
|
|
81
|
+
codePoints: glyphs.slice(i, i + match.consumed).flatMap((g) => g.codePoints),
|
|
82
|
+
});
|
|
83
|
+
i += match.consumed;
|
|
84
|
+
applied = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (!applied)
|
|
88
|
+
out.push(glyphs[i++]);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { Color } from "../common/color.js";
|
|
2
2
|
import { FontStyle } from "../utils/pdf-object-manager.js";
|
|
3
3
|
import { HorizontalAlignment } from "../elements/pdf-element.js";
|
|
4
|
+
import type { Direction } from "./bidi.js";
|
|
5
|
+
import type { Hyphenator } from "./word-splitting.js";
|
|
6
|
+
/** CSS `text-transform`. `capitalize` upper-cases the first letter of every word, as CSS does. */
|
|
7
|
+
export type TextTransform = "none" | "uppercase" | "lowercase" | "capitalize";
|
|
8
|
+
/**
|
|
9
|
+
* Apply a `text-transform`. One place, so the measured text and the drawn text are the same string.
|
|
10
|
+
*
|
|
11
|
+
* `atWordStart` carries the capitalisation state ACROSS pieces: a word split over two spans
|
|
12
|
+
* (`span("hel") + span("lo")`) is still one word, and only its first letter is raised.
|
|
13
|
+
*/
|
|
14
|
+
export declare function applyTextTransform(text: string, transform: TextTransform, atWordStart?: boolean): {
|
|
15
|
+
text: string;
|
|
16
|
+
atWordStart: boolean;
|
|
17
|
+
};
|
|
4
18
|
/**
|
|
5
19
|
* The inheritable text properties - the same set CSS and Flutter cascade. A `Text` resolves each of
|
|
6
20
|
* its own (possibly unset) properties against the nearest cascaded style: explicit > inherited >
|
|
@@ -10,6 +24,8 @@ import { HorizontalAlignment } from "../elements/pdf-element.js";
|
|
|
10
24
|
export interface ResolvedTextStyle {
|
|
11
25
|
fontSize: number;
|
|
12
26
|
fontFamily: string;
|
|
27
|
+
/** The rest of the family stack: tried, in order, for a code point `fontFamily` cannot draw. */
|
|
28
|
+
fontFallback: string[];
|
|
13
29
|
fontStyle: FontStyle;
|
|
14
30
|
color: Color;
|
|
15
31
|
textAlignment: HorizontalAlignment;
|
|
@@ -25,6 +41,28 @@ export interface ResolvedTextStyle {
|
|
|
25
41
|
skipInk: boolean;
|
|
26
42
|
/** Extra space after every glyph, in points (CSS `letter-spacing`). Default 0. */
|
|
27
43
|
letterSpacing: number;
|
|
44
|
+
/** Base writing direction (CSS `direction`). It decides where a line STARTS and how neutral
|
|
45
|
+
* characters between two scripts resolve; the reordering itself follows Unicode UAX #9 either
|
|
46
|
+
* way, so Hebrew inside an `ltr` paragraph still comes out right. */
|
|
47
|
+
direction: Direction;
|
|
48
|
+
/** CSS `text-transform`: recase the text before it is measured or drawn. */
|
|
49
|
+
textTransform: TextTransform;
|
|
50
|
+
/** CSS `word-spacing`, in points: extra advance at every space. Negative tightens. */
|
|
51
|
+
wordSpacing: number;
|
|
52
|
+
/** CSS `text-indent`, in points: how far the FIRST line of a paragraph starts in. */
|
|
53
|
+
textIndent: number;
|
|
54
|
+
/** The ligatures the font's designer drew (`fi`, `fl`, `ffi`), from its GSUB. ON, like kerning: CSS,
|
|
55
|
+
* browsers and every other renderer apply them, and nobody should need the word to get text set
|
|
56
|
+
* properly. `false` opts a document, a subtree or one `Text` out. Embedded fonts only. */
|
|
57
|
+
ligatures: boolean;
|
|
58
|
+
/** CSS `overflow-wrap: break-word`: split a word that is wider than its box, anywhere, no hyphen.
|
|
59
|
+
* The floor under `hyphenate` - it is what handles an e-mail, an IBAN or an invoice number, which
|
|
60
|
+
* have no valid hyphenation point anywhere. Default off: a too-wide word overflows, as in CSS. */
|
|
61
|
+
breakWord: boolean;
|
|
62
|
+
/** Language-aware word splitting: given a word, return its parts (`["Rechts", "schutz", ...]`). A
|
|
63
|
+
* hyphen is drawn at the break. Pluggable on purpose - no pattern data ships in this package, so
|
|
64
|
+
* there is no licence question and nothing is added to a bundle that never asks for it. */
|
|
65
|
+
hyphenate?: Hyphenator;
|
|
28
66
|
}
|
|
29
67
|
/**
|
|
30
68
|
* The root of the cascade: what a `Text` falls back to when neither it nor any ancestor sets a
|
package/dist/text/text-style.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { Color } from "../common/color.js";
|
|
2
2
|
import { FontStyle } from "../utils/pdf-object-manager.js";
|
|
3
3
|
import { HorizontalAlignment } from "../elements/pdf-element.js";
|
|
4
|
+
/**
|
|
5
|
+
* Apply a `text-transform`. One place, so the measured text and the drawn text are the same string.
|
|
6
|
+
*
|
|
7
|
+
* `atWordStart` carries the capitalisation state ACROSS pieces: a word split over two spans
|
|
8
|
+
* (`span("hel") + span("lo")`) is still one word, and only its first letter is raised.
|
|
9
|
+
*/
|
|
10
|
+
export function applyTextTransform(text, transform, atWordStart = true) {
|
|
11
|
+
const ends = text === "" ? atWordStart : /\s$/u.test(text);
|
|
12
|
+
if (transform === "none")
|
|
13
|
+
return { text, atWordStart: ends };
|
|
14
|
+
if (transform === "uppercase")
|
|
15
|
+
return { text: text.toUpperCase(), atWordStart: ends };
|
|
16
|
+
if (transform === "lowercase")
|
|
17
|
+
return { text: text.toLowerCase(), atWordStart: ends };
|
|
18
|
+
// CSS raises the first typographic LETTER of every word. Punctuation is not one, so a leading
|
|
19
|
+
// bracket or quote does not consume the word start - `(hello)` becomes `(Hello)`, as in a browser.
|
|
20
|
+
let start = atWordStart;
|
|
21
|
+
let out = "";
|
|
22
|
+
for (const ch of text) {
|
|
23
|
+
if (/\s/u.test(ch)) {
|
|
24
|
+
out += ch;
|
|
25
|
+
start = true;
|
|
26
|
+
}
|
|
27
|
+
else if (start && /\p{L}|\p{N}/u.test(ch)) {
|
|
28
|
+
out += ch.toUpperCase();
|
|
29
|
+
start = false;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
out += ch;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { text: out, atWordStart: start };
|
|
36
|
+
}
|
|
4
37
|
/**
|
|
5
38
|
* The root of the cascade: what a `Text` falls back to when neither it nor any ancestor sets a
|
|
6
39
|
* property.
|
|
@@ -8,14 +41,22 @@ import { HorizontalAlignment } from "../elements/pdf-element.js";
|
|
|
8
41
|
export const DEFAULT_TEXT_STYLE = {
|
|
9
42
|
fontSize: 12,
|
|
10
43
|
fontFamily: "Helvetica",
|
|
44
|
+
fontFallback: [],
|
|
11
45
|
fontStyle: FontStyle.Normal,
|
|
12
46
|
color: new Color(0, 0, 0),
|
|
13
|
-
textAlignment: HorizontalAlignment.
|
|
47
|
+
textAlignment: HorizontalAlignment.start,
|
|
14
48
|
lineHeight: undefined,
|
|
15
49
|
underline: false,
|
|
16
50
|
strikethrough: false,
|
|
17
51
|
skipInk: false,
|
|
18
52
|
letterSpacing: 0,
|
|
53
|
+
direction: "ltr",
|
|
54
|
+
textTransform: "none",
|
|
55
|
+
wordSpacing: 0,
|
|
56
|
+
textIndent: 0,
|
|
57
|
+
ligatures: true,
|
|
58
|
+
breakWord: false,
|
|
59
|
+
hyphenate: undefined,
|
|
19
60
|
};
|
|
20
61
|
/** Layers a partial override onto a complete style; an unset (undefined) field keeps the base. */
|
|
21
62
|
export function mergeTextStyle(base, override) {
|
|
@@ -24,6 +65,7 @@ export function mergeTextStyle(base, override) {
|
|
|
24
65
|
return {
|
|
25
66
|
fontSize: override.fontSize ?? base.fontSize,
|
|
26
67
|
fontFamily: override.fontFamily ?? base.fontFamily,
|
|
68
|
+
fontFallback: override.fontFallback ?? base.fontFallback,
|
|
27
69
|
fontStyle: override.fontStyle ?? base.fontStyle,
|
|
28
70
|
color: override.color ?? base.color,
|
|
29
71
|
textAlignment: override.textAlignment ?? base.textAlignment,
|
|
@@ -32,5 +74,12 @@ export function mergeTextStyle(base, override) {
|
|
|
32
74
|
strikethrough: override.strikethrough ?? base.strikethrough,
|
|
33
75
|
skipInk: override.skipInk ?? base.skipInk,
|
|
34
76
|
letterSpacing: override.letterSpacing ?? base.letterSpacing,
|
|
77
|
+
direction: override.direction ?? base.direction,
|
|
78
|
+
textTransform: override.textTransform ?? base.textTransform,
|
|
79
|
+
wordSpacing: override.wordSpacing ?? base.wordSpacing,
|
|
80
|
+
textIndent: override.textIndent ?? base.textIndent,
|
|
81
|
+
ligatures: override.ligatures ?? base.ligatures,
|
|
82
|
+
breakWord: override.breakWord ?? base.breakWord,
|
|
83
|
+
hyphenate: override.hyphenate ?? base.hyphenate,
|
|
35
84
|
};
|
|
36
85
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TextSegment } from "../elements/text-element.js";
|
|
2
|
+
export declare function normalizeWhitespace(text: string): string;
|
|
3
|
+
/** The same, for content that may already be split into styled spans. */
|
|
4
|
+
export declare function normalizeContent(content: string | TextSegment[]): string | TextSegment[];
|