@jasy/pdf 1.0.0-beta.2 → 1.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/content.d.ts +5 -5
- package/dist/api/content.js +12 -6
- package/dist/api/font-url.d.ts +1 -2
- package/dist/api/font-url.js +8 -74
- package/dist/api/forms.d.ts +3 -0
- package/dist/api/forms.js +3 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +1 -0
- package/dist/api/layout.d.ts +6 -0
- package/dist/api/layout.js +7 -2
- package/dist/api/page-builder.d.ts +4 -0
- package/dist/api/page-builder.js +4 -0
- package/dist/api/structure.d.ts +10 -2
- package/dist/api/structure.js +9 -6
- package/dist/api/table.d.ts +5 -3
- package/dist/api/table.js +99 -22
- package/dist/common/diagnostics.d.ts +53 -0
- package/dist/common/diagnostics.js +77 -0
- package/dist/edit/acroform-reader.js +7 -7
- package/dist/edit/appearance.js +3 -3
- package/dist/edit/attachments.d.ts +52 -0
- package/dist/edit/attachments.js +178 -0
- package/dist/edit/document.d.ts +38 -8
- package/dist/edit/document.js +208 -51
- package/dist/edit/fill.js +8 -23
- package/dist/edit/filters.d.ts +15 -0
- package/dist/edit/filters.js +157 -0
- package/dist/edit/index.d.ts +4 -1
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +3 -4
- package/dist/edit/objects.js +5 -18
- package/dist/elements/canvas-element.d.ts +3 -0
- package/dist/elements/canvas-element.js +14 -3
- package/dist/elements/container-element.d.ts +5 -1
- package/dist/elements/container-element.js +35 -15
- package/dist/elements/forms/checkbox-element.d.ts +2 -0
- package/dist/elements/forms/checkbox-element.js +12 -3
- package/dist/elements/forms/choice-element.d.ts +6 -1
- package/dist/elements/forms/choice-element.js +17 -6
- package/dist/elements/forms/field-font.d.ts +8 -0
- package/dist/elements/forms/field-font.js +5 -0
- package/dist/elements/forms/push-button-element.d.ts +6 -1
- package/dist/elements/forms/push-button-element.js +16 -4
- package/dist/elements/forms/radio-element.d.ts +2 -0
- package/dist/elements/forms/radio-element.js +12 -3
- package/dist/elements/forms/signature-element.d.ts +6 -1
- package/dist/elements/forms/signature-element.js +16 -4
- package/dist/elements/forms/text-field-element.d.ts +6 -1
- package/dist/elements/forms/text-field-element.js +16 -4
- package/dist/elements/image-element.d.ts +39 -1
- package/dist/elements/image-element.js +95 -12
- package/dist/elements/layout/anchor-element.d.ts +8 -10
- package/dist/elements/layout/anchor-element.js +10 -18
- package/dist/elements/layout/bookmark-element.d.ts +9 -11
- package/dist/elements/layout/bookmark-element.js +11 -20
- package/dist/elements/layout/default-text-style-element.d.ts +11 -13
- package/dist/elements/layout/default-text-style-element.js +14 -31
- package/dist/elements/layout/deferred-element.d.ts +12 -4
- package/dist/elements/layout/deferred-element.js +24 -7
- package/dist/elements/layout/expanded-element.d.ts +7 -0
- package/dist/elements/layout/expanded-element.js +40 -18
- package/dist/elements/layout/keep-together-element.d.ts +8 -18
- package/dist/elements/layout/keep-together-element.js +21 -45
- package/dist/elements/layout/link-element.d.ts +8 -13
- package/dist/elements/layout/link-element.js +9 -19
- package/dist/elements/layout/padding-element.d.ts +15 -1
- package/dist/elements/layout/padding-element.js +55 -27
- package/dist/elements/layout/page-break-element.d.ts +4 -1
- package/dist/elements/layout/page-break-element.js +6 -1
- package/dist/elements/layout/page-builder-element.d.ts +5 -0
- package/dist/elements/layout/page-builder-element.js +11 -0
- package/dist/elements/layout/positioned-element.d.ts +14 -2
- package/dist/elements/layout/positioned-element.js +36 -10
- package/dist/elements/layout/proxy-element.d.ts +43 -0
- package/dist/elements/layout/proxy-element.js +94 -0
- package/dist/elements/layout/repeating-header-element.d.ts +6 -0
- package/dist/elements/layout/repeating-header-element.js +40 -13
- package/dist/elements/layout/rotated-box-element.d.ts +6 -0
- package/dist/elements/layout/rotated-box-element.js +27 -14
- package/dist/elements/layout/rotated-element.d.ts +7 -0
- package/dist/elements/layout/rotated-element.js +25 -2
- package/dist/elements/layout/struct-group.d.ts +9 -17
- package/dist/elements/layout/struct-group.js +10 -35
- package/dist/elements/line-element.d.ts +1 -0
- package/dist/elements/line-element.js +6 -0
- package/dist/elements/page-element.d.ts +10 -3
- package/dist/elements/page-element.js +39 -31
- package/dist/elements/pdf-document-element.d.ts +3 -0
- package/dist/elements/pdf-document-element.js +7 -0
- package/dist/elements/pdf-element.d.ts +75 -15
- package/dist/elements/pdf-element.js +68 -18
- package/dist/elements/rectangle-element.d.ts +14 -0
- package/dist/elements/rectangle-element.js +97 -52
- package/dist/elements/row-element.d.ts +11 -0
- package/dist/elements/row-element.js +58 -8
- package/dist/elements/svg-element.d.ts +30 -6
- package/dist/elements/svg-element.js +88 -17
- package/dist/elements/text-element.d.ts +40 -13
- package/dist/elements/text-element.js +144 -99
- package/dist/forms/acroform.d.ts +9 -1
- package/dist/forms/acroform.js +83 -34
- package/dist/forms/appearance.d.ts +17 -6
- package/dist/forms/appearance.js +18 -16
- package/dist/ir/display-list.d.ts +12 -2
- package/dist/layout/box-constraints.d.ts +5 -0
- package/dist/layout/box-constraints.js +14 -0
- package/dist/layout/collect-images.d.ts +6 -4
- package/dist/layout/collect-images.js +6 -4
- package/dist/layout/flex-item.d.ts +9 -0
- package/dist/layout/flex-item.js +39 -0
- package/dist/layout/fragmentation.d.ts +12 -4
- package/dist/layout/fragmentation.js +37 -14
- package/dist/layout/layout-cache.d.ts +53 -0
- package/dist/layout/layout-cache.js +107 -0
- package/dist/renderer/anchor-renderer.d.ts +2 -1
- package/dist/renderer/anchor-renderer.js +5 -4
- package/dist/renderer/bookmark-renderer.d.ts +2 -1
- package/dist/renderer/bookmark-renderer.js +5 -4
- package/dist/renderer/canvas-renderer.d.ts +2 -1
- package/dist/renderer/canvas-renderer.js +5 -2
- package/dist/renderer/container-renderer.d.ts +2 -1
- package/dist/renderer/container-renderer.js +6 -5
- package/dist/renderer/default-text-style-renderer.d.ts +2 -1
- package/dist/renderer/default-text-style-renderer.js +2 -3
- package/dist/renderer/deferred-renderer.d.ts +2 -1
- package/dist/renderer/deferred-renderer.js +2 -3
- package/dist/renderer/expanded-renderer.d.ts +2 -1
- package/dist/renderer/expanded-renderer.js +2 -3
- package/dist/renderer/form-field-renderer.d.ts +2 -1
- package/dist/renderer/form-field-renderer.js +16 -3
- package/dist/renderer/image-renderer.d.ts +2 -1
- package/dist/renderer/image-renderer.js +5 -2
- package/dist/renderer/keep-together-renderer.d.ts +2 -1
- package/dist/renderer/keep-together-renderer.js +2 -3
- package/dist/renderer/line-renderer.d.ts +2 -1
- package/dist/renderer/line-renderer.js +7 -2
- package/dist/renderer/link-renderer.d.ts +2 -1
- package/dist/renderer/link-renderer.js +36 -5
- package/dist/renderer/padding-renderer.d.ts +2 -1
- package/dist/renderer/padding-renderer.js +5 -5
- package/dist/renderer/page-break-renderer.d.ts +6 -5
- package/dist/renderer/page-break-renderer.js +14 -7
- package/dist/renderer/page-builder-renderer.d.ts +2 -1
- package/dist/renderer/page-builder-renderer.js +2 -3
- package/dist/renderer/page-renderer.js +28 -7
- package/dist/renderer/pdf-backend.d.ts +4 -3
- package/dist/renderer/pdf-backend.js +5 -5
- package/dist/renderer/pdf-document-renderer.d.ts +1 -8
- package/dist/renderer/pdf-document-renderer.js +15 -16
- package/dist/renderer/pdf-renderer.js +11 -11
- package/dist/renderer/positioned-renderer.d.ts +6 -1
- package/dist/renderer/positioned-renderer.js +6 -5
- package/dist/renderer/rectangle-renderer.d.ts +2 -1
- package/dist/renderer/rectangle-renderer.js +9 -4
- package/dist/renderer/repeating-header-renderer.d.ts +2 -1
- package/dist/renderer/repeating-header-renderer.js +6 -5
- package/dist/renderer/rotated-renderer.d.ts +2 -1
- package/dist/renderer/rotated-renderer.js +8 -5
- package/dist/renderer/row-renderer.d.ts +2 -1
- package/dist/renderer/row-renderer.js +6 -5
- package/dist/renderer/struct-group-renderer.d.ts +2 -1
- package/dist/renderer/struct-group-renderer.js +2 -3
- package/dist/renderer/svg-renderer.d.ts +2 -1
- package/dist/renderer/svg-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +4 -6
- package/dist/renderer/text-renderer.js +101 -111
- package/dist/svg/gradients.js +5 -0
- package/dist/svg/index.js +10 -1
- package/dist/svg/style.d.ts +2 -0
- package/dist/svg/style.js +11 -0
- package/dist/text/advance.d.ts +4 -0
- package/dist/text/advance.js +49 -4
- package/dist/text/bidi.d.ts +0 -2
- package/dist/text/bidi.js +0 -10
- package/dist/text/glyph-coverage.d.ts +1 -1
- package/dist/text/glyph-coverage.js +7 -3
- package/dist/text/hyphen-breaks.d.ts +17 -0
- package/dist/text/hyphen-breaks.js +54 -0
- package/dist/text/line-breaker.d.ts +34 -31
- package/dist/text/line-breaker.js +364 -176
- package/dist/text/line-metrics.d.ts +6 -3
- package/dist/text/line-metrics.js +7 -2
- package/dist/text/pdf-text-string.d.ts +21 -0
- package/dist/text/pdf-text-string.js +114 -0
- package/dist/text/text-decoration.d.ts +5 -5
- package/dist/text/text-decoration.js +5 -5
- package/dist/text/word-splitting.d.ts +15 -2
- package/dist/text/word-splitting.js +34 -8
- package/dist/utils/afm-parser.d.ts +3 -0
- package/dist/utils/afm-parser.js +19 -1
- package/dist/utils/dest-registry.d.ts +1 -1
- package/dist/utils/dest-registry.js +3 -2
- package/dist/utils/fetch-bytes.d.ts +19 -0
- package/dist/utils/fetch-bytes.js +79 -0
- package/dist/utils/flex-layout.d.ts +13 -13
- package/dist/utils/flex-layout.js +318 -216
- package/dist/utils/font-metrics.d.ts +4 -3
- package/dist/utils/image-helper.js +2 -1
- package/dist/utils/pdf-object-manager.d.ts +11 -3
- package/dist/utils/pdf-object-manager.js +41 -15
- package/dist/utils/renderer-registry.d.ts +43 -2
- package/dist/utils/renderer-registry.js +40 -0
- package/dist/utils/struct-tree.d.ts +24 -0
- package/dist/utils/struct-tree.js +61 -9
- package/dist/utils/ttf-parser.d.ts +4 -0
- package/dist/utils/ttf-parser.js +27 -4
- package/dist/validators/element-validator.js +2 -1
- package/package.json +1 -1
- package/dist/elements/layout/sized-container-element.d.ts +0 -14
- package/dist/elements/layout/sized-container-element.js +0 -32
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { runAdvance } from "./advance.js";
|
|
2
|
-
import { splitLongWord } from "./word-splitting.js";
|
|
1
|
+
import { runAdvance, SOFT_HYPHEN, withoutSoftHyphens } from "./advance.js";
|
|
2
|
+
import { HYPHEN, splitLongWord } from "./word-splitting.js";
|
|
3
|
+
import { headAt, hyphenBreaks, mayBreak, NO_BREAKS } from "./hyphen-breaks.js";
|
|
3
4
|
/**
|
|
4
5
|
* How far a JUSTIFIED line's spaces may be squeezed to keep one more word on it.
|
|
5
6
|
*
|
|
@@ -16,17 +17,11 @@ export const MAX_SPACE_SHRINK = 1 / 4;
|
|
|
16
17
|
/** Three ASCII dots, NOT the "…" glyph (U+2026): plain dots encode in every font - standard-14
|
|
17
18
|
* (WinAnsi) and any embedded TTF - whereas U+2026 needs a glyph the font may not carry. */
|
|
18
19
|
const ELLIPSIS = "...";
|
|
20
|
+
/** The spaces that end a line: they hang past its edge, neither drawn nor in its width. */
|
|
21
|
+
const TRAILING_SPACES = / +$/;
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* Single source of truth for plain-string wrapping: both height measurement and
|
|
24
|
-
* rendering call this, so they can never disagree. Depends only on `FontMetrics`,
|
|
25
|
-
* not the PDF byte writer - the future fragmentation pass can reuse it.
|
|
26
|
-
*/
|
|
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
|
|
23
|
+
* The width of `text` set as ONE line, accumulated in EXACTLY the order the breaker builds a line: the
|
|
24
|
+
* first word, then `+ (space + word)` each time. The order matters because floating-point
|
|
30
25
|
* addition is not associative - a box sized by a differently-grouped sum can be one bit too small for
|
|
31
26
|
* the very line the breaker then measures, and the text wraps inside a box made to fit it. That is not
|
|
32
27
|
* hypothetical: it is what split a footer sized to its own content.
|
|
@@ -43,105 +38,163 @@ export function singleLineWidth(text, font, metrics, letterSpacing = 0, wordSpac
|
|
|
43
38
|
: width + (space + runAdvance(metrics, word, font, letterSpacing)), 0);
|
|
44
39
|
}
|
|
45
40
|
export function wrapStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics, maxLines, overflow, letterSpacing = 0, options = {}) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
});
|
|
118
|
-
});
|
|
119
|
-
if (currentLine)
|
|
120
|
-
lines.push(currentLine.trim());
|
|
121
|
-
// Open-end by default; cap only when maxLines is set (the others get undefined → untouched).
|
|
122
|
-
if (maxLines == null || lines.length <= maxLines)
|
|
123
|
-
return lines;
|
|
124
|
-
const kept = lines.slice(0, maxLines);
|
|
125
|
-
if (overflow === "ellipsis") {
|
|
126
|
-
const last = kept.length - 1;
|
|
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);
|
|
130
|
-
}
|
|
131
|
-
return kept;
|
|
41
|
+
return breakStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics, maxLines, overflow, letterSpacing, options).map((line) => line.text);
|
|
42
|
+
}
|
|
43
|
+
/** How a line that ends at break `k` of `word` ends: a soft hyphen adds a hyphen the source lacks. */
|
|
44
|
+
const endAt = (word, k) => word[k - 1] === SOFT_HYPHEN && word[k - 2] !== "-" ? "hyphen" : "soft";
|
|
45
|
+
/**
|
|
46
|
+
* `wrapStringIntoLines`, with each line's source offset and ending. The ONE breaking pass both share,
|
|
47
|
+
* so a line's position in the source can never disagree with the line itself.
|
|
48
|
+
*/
|
|
49
|
+
export function breakStringIntoLines(text, fontFamily, fontSize, fontStyle, maxWidth, metrics, maxLines, overflow, letterSpacing = 0, options = {}) {
|
|
50
|
+
// A string is one span: same breaker, same lines.
|
|
51
|
+
return breakSegmentsIntoLines([{ content: text }], { fontFamily, fontSize, fontStyle, letterSpacing, ...options }, maxWidth, metrics, maxLines, overflow).map((line) => ({
|
|
52
|
+
text: line.segments.map((segment) => segment.content).join(""),
|
|
53
|
+
start: line.start.offset,
|
|
54
|
+
end: line.end,
|
|
55
|
+
}));
|
|
132
56
|
}
|
|
133
57
|
/**
|
|
134
|
-
* Break
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
58
|
+
* Break text - spans; a plain string is one - into lines that fit within `maxWidth`, greedily: a word
|
|
59
|
+
* stays on the current line unless it would overflow. Each line keeps the segments that landed on it,
|
|
60
|
+
* which is what `line-metrics.ts` derives the line's height from. The one breaker: measuring,
|
|
61
|
+
* fragmenting and drawing all call this.
|
|
138
62
|
*/
|
|
139
63
|
export function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics, maxLines, overflow) {
|
|
140
64
|
const lines = [];
|
|
65
|
+
const { indent = 0, wordSpacing = 0, shrink = 0 } = defaults;
|
|
141
66
|
let width = 0;
|
|
67
|
+
let gaps = 0; // spaces on the line, which is what a justified line can squeeze
|
|
142
68
|
let lineSegments = [];
|
|
143
69
|
let combined = "";
|
|
144
|
-
|
|
70
|
+
let lineStart = { segment: 0, offset: 0 }; // where the current line begins
|
|
71
|
+
// Whether the line holds a word yet - an empty one too: a space is text as written, so a leading one
|
|
72
|
+
// and both of a double space count.
|
|
73
|
+
let open = false;
|
|
74
|
+
// Whether anything DRAWN is on the line. Only then does a word that does not fit start a new one:
|
|
75
|
+
// a line of spaces alone is empty, and closing it would push a blank line before the word.
|
|
76
|
+
let inked = false;
|
|
77
|
+
let ink = 0; // the line's width up to its last drawn glyph - the spaces after it hang
|
|
78
|
+
// A space that did not fit was not added (it hangs), so the line's text no longer shows that it ends
|
|
79
|
+
// in a break opportunity - this does.
|
|
80
|
+
let hung = false;
|
|
81
|
+
// An empty piece (what a span leaves after its `\n`) has nothing to draw, and its font must not size
|
|
82
|
+
// the line it lands on - a big span ending in `\n` made the next line as tall as itself. Only a blank
|
|
83
|
+
// line keeps one: it is what gives that line its height.
|
|
84
|
+
const close = (segs, lineWidth, end, glued = false) => {
|
|
85
|
+
let width = lineWidth;
|
|
86
|
+
let ends = end;
|
|
87
|
+
// A span boundary is a break too: a line that ends there right after a soft hyphen shows it as a real
|
|
88
|
+
// hyphen, exactly as a break inside a word does. Only when `glued` - the next piece follows with no
|
|
89
|
+
// space between; a line that breaks at a space ends there, and the soft hyphen stays invisible.
|
|
90
|
+
let lastIndex = segs.length - 1;
|
|
91
|
+
while (lastIndex >= 0 && segs[lastIndex].content === "")
|
|
92
|
+
lastIndex--;
|
|
93
|
+
const last = segs[lastIndex];
|
|
94
|
+
if (glued &&
|
|
95
|
+
last?.content.endsWith(SOFT_HYPHEN) &&
|
|
96
|
+
!withoutSoftHyphens(last.content).endsWith("-")) {
|
|
97
|
+
const font = {
|
|
98
|
+
fontFamily: last.fontFamily || defaults.fontFamily,
|
|
99
|
+
fontSize: last.fontSize || defaults.fontSize,
|
|
100
|
+
fontStyle: last.fontStyle || defaults.fontStyle,
|
|
101
|
+
ligatures: defaults.ligatures,
|
|
102
|
+
};
|
|
103
|
+
width += runAdvance(metrics, HYPHEN, font, last.letterSpacing ?? defaults.letterSpacing ?? 0);
|
|
104
|
+
segs = segs.map((seg, i) => i === lastIndex ? { ...seg, content: withoutSoftHyphens(seg.content) + HYPHEN } : seg);
|
|
105
|
+
ends = "hyphen";
|
|
106
|
+
}
|
|
107
|
+
// The spaces the line ends in hang: not drawn, and `width` (the ink) already stops before them.
|
|
108
|
+
for (let i = segs.length - 1; i >= 0; i--) {
|
|
109
|
+
const kept = segs[i].content.replace(TRAILING_SPACES, "");
|
|
110
|
+
if (kept !== segs[i].content)
|
|
111
|
+
segs = segs.map((seg, j) => (j === i ? { ...seg, content: kept } : seg));
|
|
112
|
+
if (kept !== "")
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
// Soft hyphens leave the line here - a blank line's piece included.
|
|
116
|
+
const stripped = segs.map((seg) => seg.content.includes(SOFT_HYPHEN)
|
|
117
|
+
? { ...seg, content: withoutSoftHyphens(seg.content) }
|
|
118
|
+
: seg);
|
|
119
|
+
const drawn = stripped.filter((seg) => seg.content !== "");
|
|
120
|
+
lines.push({
|
|
121
|
+
segments: drawn.length > 0 ? drawn : stripped.slice(0, 1),
|
|
122
|
+
width,
|
|
123
|
+
start: lineStart,
|
|
124
|
+
end: ends,
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
// Where each piece of the line begins in the source: a word that runs across a span boundary can move
|
|
128
|
+
// to the next line together with its earlier part, which then starts that line in an earlier span.
|
|
129
|
+
const origin = new WeakMap();
|
|
130
|
+
const started = (piece, from) => {
|
|
131
|
+
origin.set(piece, from);
|
|
132
|
+
return piece;
|
|
133
|
+
};
|
|
134
|
+
/** The width and ink of `pieces`, summed exactly as `placeWord` builds a line: word by word, a space
|
|
135
|
+
* joining the words inside a piece, nothing between pieces. */
|
|
136
|
+
const measure = (pieces) => {
|
|
137
|
+
let total = 0;
|
|
138
|
+
let ink = 0;
|
|
139
|
+
let open = false;
|
|
140
|
+
for (const piece of pieces) {
|
|
141
|
+
const font = {
|
|
142
|
+
fontFamily: piece.fontFamily || defaults.fontFamily,
|
|
143
|
+
fontSize: piece.fontSize || defaults.fontSize,
|
|
144
|
+
fontStyle: piece.fontStyle || defaults.fontStyle,
|
|
145
|
+
ligatures: defaults.ligatures,
|
|
146
|
+
};
|
|
147
|
+
const spacing = piece.letterSpacing ?? defaults.letterSpacing ?? 0;
|
|
148
|
+
const space = runAdvance(metrics, " ", font, spacing) + wordSpacing;
|
|
149
|
+
piece.content.split(" ").forEach((word, i) => {
|
|
150
|
+
total = total + ((open && i > 0 ? space : 0) + runAdvance(metrics, word, font, spacing));
|
|
151
|
+
open = true;
|
|
152
|
+
if (word !== "")
|
|
153
|
+
ink = total;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return { width: total, ink };
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* The line is full and the next word follows the line's last piece with NO space: it continues a word
|
|
160
|
+
* that began in an earlier span (`span("Rech") + span("nung")`). A span boundary is no break
|
|
161
|
+
* opportunity, so the break belongs at the last one before that word - a space, or a hyphen or soft
|
|
162
|
+
* hyphen ending a span - and the word's earlier part moves down with it. `null`: the line has none
|
|
163
|
+
* before it, and the word stays, as the same text in one string would.
|
|
164
|
+
*/
|
|
165
|
+
const gluedTail = (next, afterSpace) => {
|
|
166
|
+
const pieces = lineSegments.slice(0, -1); // the last is the current span's own, still empty
|
|
167
|
+
const moved = [];
|
|
168
|
+
let following = next;
|
|
169
|
+
const inks = (keep) => keep.some((p) => withoutSoftHyphens(p.content).trim() !== "");
|
|
170
|
+
if (afterSpace)
|
|
171
|
+
return inks(pieces) ? { keep: pieces, moved, hyphen: false } : null;
|
|
172
|
+
for (let i = pieces.length - 1; i >= 0; i--) {
|
|
173
|
+
const piece = pieces[i];
|
|
174
|
+
const c = piece.content;
|
|
175
|
+
const soft = c.endsWith(SOFT_HYPHEN);
|
|
176
|
+
if (c.endsWith(" ") || soft || (c.endsWith("-") && !/^\d/.test(following))) {
|
|
177
|
+
const keep = pieces.slice(0, i + 1);
|
|
178
|
+
return inks(keep)
|
|
179
|
+
? { keep, moved, from: moved[0] && origin.get(moved[0]), hyphen: soft }
|
|
180
|
+
: null;
|
|
181
|
+
}
|
|
182
|
+
const space = c.lastIndexOf(" ");
|
|
183
|
+
if (space >= 0) {
|
|
184
|
+
const from = origin.get(piece);
|
|
185
|
+
const head = started({ ...piece, content: c.slice(0, space + 1) }, from);
|
|
186
|
+
const tail = started({ ...piece, content: c.slice(space + 1) }, { segment: from.segment, offset: from.offset + space + 1 });
|
|
187
|
+
const keep = [...pieces.slice(0, i), head];
|
|
188
|
+
return inks(keep)
|
|
189
|
+
? { keep, moved: [tail, ...moved], from: origin.get(tail), hyphen: false }
|
|
190
|
+
: null;
|
|
191
|
+
}
|
|
192
|
+
moved.unshift(piece);
|
|
193
|
+
following = c + following;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
};
|
|
197
|
+
segments.forEach((segment, segmentIndex) => {
|
|
145
198
|
const family = segment.fontFamily || defaults.fontFamily;
|
|
146
199
|
const size = segment.fontSize || defaults.fontSize;
|
|
147
200
|
const style = segment.fontStyle || defaults.fontStyle;
|
|
@@ -152,109 +205,242 @@ export function breakSegmentsIntoLines(segments, defaults, maxWidth, metrics, ma
|
|
|
152
205
|
fontStyle: style,
|
|
153
206
|
ligatures: defaults.ligatures,
|
|
154
207
|
};
|
|
155
|
-
const spaceWidth = runAdvance(metrics, " ", font, letterSpacing);
|
|
156
|
-
|
|
157
|
-
// sits, whatever else was already on it, and start
|
|
208
|
+
const spaceWidth = runAdvance(metrics, " ", font, letterSpacing) + wordSpacing;
|
|
209
|
+
const at = (offset) => ({ segment: segmentIndex, offset });
|
|
210
|
+
// A `\n` is a HARD break: close the line where it sits, whatever else was already on it, and start
|
|
211
|
+
// the next one with this segment's style. An EMPTY paragraph still makes a line, or a deliberate
|
|
212
|
+
// blank line between two blocks silently disappears.
|
|
213
|
+
let paragraphStart = 0;
|
|
158
214
|
segment.content.split("\n").forEach((paragraph, paragraphIndex) => {
|
|
159
215
|
if (paragraphIndex > 0) {
|
|
160
|
-
|
|
216
|
+
close(lineSegments, ink, "hard");
|
|
161
217
|
width = 0;
|
|
218
|
+
gaps = 0;
|
|
219
|
+
open = false;
|
|
220
|
+
inked = false;
|
|
221
|
+
ink = 0;
|
|
222
|
+
hung = false;
|
|
162
223
|
lineSegments = [];
|
|
224
|
+
lineStart = at(paragraphStart);
|
|
163
225
|
}
|
|
164
226
|
const words = paragraph.split(" ");
|
|
227
|
+
let wordStart = paragraphStart;
|
|
165
228
|
// Start this segment's piece empty; its content is filled word-by-word below. (Not
|
|
166
229
|
// the original content - otherwise a segment whose FIRST word overflows would carry
|
|
167
230
|
// its whole text into the line that just closed.)
|
|
168
|
-
lineSegments.push({ ...segment, fontFamily: family, content: "" });
|
|
231
|
+
lineSegments.push(started({ ...segment, fontFamily: family, content: "" }, at(paragraphStart)));
|
|
169
232
|
combined = "";
|
|
170
|
-
|
|
233
|
+
/** Close the line before a word whose earlier part (`glued.moved`) moves down with it. */
|
|
234
|
+
const carryOver = (glued, wordAt) => {
|
|
235
|
+
close(glued.keep, measure(glued.keep).ink, "soft", glued.hyphen);
|
|
236
|
+
const carried = measure(glued.moved);
|
|
237
|
+
lineSegments = [
|
|
238
|
+
...glued.moved,
|
|
239
|
+
started({ ...segment, fontFamily: family, content: "" }, at(wordAt)),
|
|
240
|
+
];
|
|
241
|
+
combined = "";
|
|
242
|
+
lineStart = glued.from;
|
|
243
|
+
width = carried.width;
|
|
244
|
+
ink = carried.ink;
|
|
245
|
+
gaps = 0;
|
|
246
|
+
open = true;
|
|
247
|
+
inked = true;
|
|
248
|
+
};
|
|
249
|
+
// One word, placed at source offset `wordAt`; `joined`: a space joins it to the word before.
|
|
250
|
+
const placeWord = (word, wordAt, joined) => {
|
|
251
|
+
const afterSpace = hung;
|
|
252
|
+
hung = false;
|
|
253
|
+
// The space before this word hung past the edge, so the line is full: the word starts the next
|
|
254
|
+
// one. It must not slip in behind the space it was written after, however narrow it is.
|
|
255
|
+
if (afterSpace && inked && word !== "") {
|
|
256
|
+
close(lineSegments, ink, "soft");
|
|
257
|
+
lineSegments = [started({ ...segment, fontFamily: family, content: "" }, at(wordAt))];
|
|
258
|
+
combined = "";
|
|
259
|
+
width = 0;
|
|
260
|
+
gaps = 0;
|
|
261
|
+
open = false;
|
|
262
|
+
inked = false;
|
|
263
|
+
ink = 0;
|
|
264
|
+
lineStart = at(wordAt);
|
|
265
|
+
}
|
|
266
|
+
// Only a word that follows something on this line is joined to it by the space.
|
|
267
|
+
const joins = open && joined;
|
|
171
268
|
const wordWidth = runAdvance(metrics, word, font, letterSpacing);
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
const
|
|
269
|
+
// CSS `text-indent`: the FIRST line has the indent taken out of its room; the pieces of a split
|
|
270
|
+
// word land on the first line only when it is still empty.
|
|
271
|
+
const room = lines.length === 0 ? maxWidth - indent : maxWidth;
|
|
272
|
+
// How much a JUSTIFIED line may be squeezed to keep one more word: the spaces already on the
|
|
273
|
+
// line plus the one that would join it, each giving up at most `shrink` of its width. Zero for
|
|
274
|
+
// every other alignment, where a squeezed line would simply overflow instead.
|
|
275
|
+
const squeeze = (gaps + (joins ? 1 : 0)) * spaceWidth * shrink;
|
|
276
|
+
// A hyphen inside the word is a break opportunity, taken only when the whole word does not fit
|
|
277
|
+
// here - the same test, squeeze included, as below.
|
|
278
|
+
const fitsHere = (w) => !open ? w <= room : width + ((joins ? spaceWidth : 0) + w) - squeeze <= room;
|
|
279
|
+
const breaks = mayBreak(word) ? hyphenBreaks(word) : NO_BREAKS;
|
|
280
|
+
if (breaks.length > 0 && !fitsHere(wordWidth)) {
|
|
281
|
+
const freshLine = (startAt) => {
|
|
282
|
+
lineSegments = [started({ ...segment, fontFamily: family, content: "" }, at(startAt))];
|
|
283
|
+
combined = "";
|
|
284
|
+
width = 0;
|
|
285
|
+
gaps = 0;
|
|
286
|
+
open = false;
|
|
287
|
+
inked = false;
|
|
288
|
+
ink = 0;
|
|
289
|
+
lineStart = at(startAt);
|
|
290
|
+
};
|
|
291
|
+
let head = 0;
|
|
292
|
+
for (const k of breaks) {
|
|
293
|
+
if (!fitsHere(runAdvance(metrics, headAt(word, k), font, letterSpacing)))
|
|
294
|
+
break;
|
|
295
|
+
head = k;
|
|
296
|
+
}
|
|
297
|
+
if (head > 0) {
|
|
298
|
+
const part = headAt(word, head);
|
|
299
|
+
const joiner = joins ? spaceWidth : 0;
|
|
300
|
+
if (!open)
|
|
301
|
+
lineStart = at(wordAt);
|
|
302
|
+
width += joiner + runAdvance(metrics, part, font, letterSpacing);
|
|
303
|
+
ink = width;
|
|
304
|
+
combined += joins ? " " + part : part;
|
|
305
|
+
if (lineSegments.length === 0)
|
|
306
|
+
lineSegments.push(started({ ...segment, fontFamily: family, content: "" }, at(wordAt)));
|
|
307
|
+
lineSegments[lineSegments.length - 1].content = combined;
|
|
308
|
+
close(lineSegments, ink, endAt(word, head));
|
|
309
|
+
freshLine(wordAt + head);
|
|
310
|
+
placeWord(word.slice(head), wordAt + head, false);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (inked) {
|
|
314
|
+
const glued = joined ? undefined : gluedTail(word, afterSpace);
|
|
315
|
+
if (glued && glued.moved.length > 0) {
|
|
316
|
+
carryOver(glued, wordAt);
|
|
317
|
+
placeWord(word, wordAt, false);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (glued !== null) {
|
|
321
|
+
close(lineSegments, ink, "soft", !joined);
|
|
322
|
+
freshLine(wordAt);
|
|
323
|
+
placeWord(word, wordAt, false);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
placeWord(headAt(word, breaks[0]), wordAt, joined);
|
|
328
|
+
close(lineSegments, ink, endAt(word, breaks[0]));
|
|
329
|
+
freshLine(wordAt + breaks[0]);
|
|
330
|
+
placeWord(word.slice(breaks[0]), wordAt + breaks[0], false);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const splitRoom = lines.length === 0 && !open ? room : maxWidth;
|
|
334
|
+
// A word too wide for the box is split into pieces that fit (hyphenation first, break-word as
|
|
335
|
+
// the floor). Each finished piece closes the line it sits on, keeping this segment's style. Off
|
|
336
|
+
// by default, so a document that asks for neither never gets here.
|
|
337
|
+
const split = splitLongWord(word, splitRoom, font, metrics, letterSpacing, defaults.splitting ?? {});
|
|
175
338
|
if (split) {
|
|
176
|
-
// Close whatever is already on the line FIRST
|
|
177
|
-
// not against what is left of this line
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
lines.push({ segments: lineSegments, width });
|
|
182
|
-
lineSegments = [{ ...segment, fontFamily: family, content: "" }];
|
|
339
|
+
// Close whatever is already on the line FIRST: the pieces were measured against the full box,
|
|
340
|
+
// not against what is left of this line.
|
|
341
|
+
if (inked) {
|
|
342
|
+
close(lineSegments, ink, "soft", !joined);
|
|
343
|
+
lineSegments = [started({ ...segment, fontFamily: family, content: "" }, at(wordAt))];
|
|
183
344
|
combined = "";
|
|
184
345
|
}
|
|
346
|
+
// Each piece starts where the source text of the previous one ended.
|
|
347
|
+
let pieceAt = wordAt;
|
|
185
348
|
for (const piece of split.slice(0, -1)) {
|
|
186
|
-
lineSegments[lineSegments.length - 1].content = piece;
|
|
349
|
+
lineSegments[lineSegments.length - 1].content = piece.text;
|
|
350
|
+
lineStart = at(pieceAt);
|
|
187
351
|
// The width a line REPORTS is what alignment and line-metrics work from: measure it.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
});
|
|
192
|
-
lineSegments = [{ ...segment, fontFamily: family, content: "" }];
|
|
352
|
+
close(lineSegments, runAdvance(metrics, piece.text, font, letterSpacing), piece.hyphen ? "hyphen" : "split");
|
|
353
|
+
pieceAt += piece.source;
|
|
354
|
+
lineSegments = [started({ ...segment, fontFamily: family, content: "" }, at(pieceAt))];
|
|
193
355
|
}
|
|
194
|
-
|
|
356
|
+
lineStart = at(pieceAt);
|
|
357
|
+
combined = split[split.length - 1].text;
|
|
195
358
|
width = runAdvance(metrics, combined, font, letterSpacing);
|
|
359
|
+
gaps = 0;
|
|
360
|
+
open = true;
|
|
361
|
+
inked = true;
|
|
362
|
+
ink = width;
|
|
196
363
|
lineSegments[lineSegments.length - 1].content = combined;
|
|
197
364
|
return;
|
|
198
365
|
}
|
|
199
366
|
// A space joins this word to what precedes it only INSIDE a segment; segments butt together
|
|
200
367
|
// with nothing between them, which is what `span("a") + span("b")` draws.
|
|
201
|
-
const joiner =
|
|
368
|
+
const joiner = joins ? spaceWidth : 0;
|
|
202
369
|
// ONE expression for the test and for the running total, grouped exactly as `singleLineWidth`
|
|
203
370
|
// groups it - see the note there. A width built any other way can be a bit off the one the box
|
|
204
371
|
// was sized with, and the text wraps inside a box made to hold it.
|
|
205
372
|
const candidate = width + (joiner + wordWidth);
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 });
|
|
373
|
+
// Break before a word that won't fit - counting the space that would join it. A word wider than
|
|
374
|
+
// the whole box still sits on its (empty) line and overflows, rather than pushing a phantom
|
|
375
|
+
// empty line before it.
|
|
376
|
+
if (candidate - squeeze > room && inked) {
|
|
377
|
+
// A space that does not fit hangs past the edge, as in CSS. Closing the line on it would end
|
|
378
|
+
// the paragraph's last line "soft" (stretched when justified) or push a blank line before `\n`.
|
|
379
|
+
if (word === "") {
|
|
380
|
+
hung = true;
|
|
381
|
+
return;
|
|
220
382
|
}
|
|
221
|
-
|
|
383
|
+
const glued = joined ? undefined : gluedTail(word, afterSpace);
|
|
384
|
+
if (glued && glued.moved.length > 0) {
|
|
385
|
+
carryOver(glued, wordAt);
|
|
386
|
+
placeWord(word, wordAt, false);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// `null`: nowhere to break before this word - it stays on the line, below.
|
|
390
|
+
if (glued !== null) {
|
|
391
|
+
close(lineSegments, ink, "soft", !joined);
|
|
392
|
+
lineStart = at(wordAt);
|
|
393
|
+
width = wordWidth;
|
|
394
|
+
gaps = 0;
|
|
395
|
+
ink = wordWidth;
|
|
396
|
+
inked = true;
|
|
397
|
+
lineSegments = [];
|
|
398
|
+
combined = word;
|
|
399
|
+
lineSegments.push(started({ ...segment, content: combined }, at(wordAt)));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
combined += joins ? " " + word : word;
|
|
404
|
+
width = candidate;
|
|
405
|
+
if (joins)
|
|
406
|
+
gaps += 1;
|
|
407
|
+
open = true;
|
|
408
|
+
if (word !== "") {
|
|
409
|
+
ink = width;
|
|
410
|
+
inked = true;
|
|
411
|
+
}
|
|
412
|
+
if (lineSegments.length === 0) {
|
|
413
|
+
lineSegments.push(started({ ...segment, fontFamily: family, content: combined }, at(wordAt)));
|
|
222
414
|
}
|
|
415
|
+
lineSegments[lineSegments.length - 1].content = combined;
|
|
416
|
+
};
|
|
417
|
+
words.forEach((word, wordIndex) => {
|
|
418
|
+
const wordAt = wordStart;
|
|
419
|
+
wordStart += word.length + 1;
|
|
420
|
+
placeWord(word, wordAt, wordIndex > 0);
|
|
223
421
|
});
|
|
422
|
+
paragraphStart += paragraph.length + 1;
|
|
224
423
|
});
|
|
225
424
|
});
|
|
226
|
-
// Only if something actually landed on it
|
|
227
|
-
//
|
|
228
|
-
// paragraph in the MIDDLE still becomes a line; that one is pushed at the break, not here.
|
|
425
|
+
// Only if something actually landed on it: a text ending in `\n` is ONE line. An empty paragraph in
|
|
426
|
+
// the MIDDLE still becomes a line; that one is pushed at the break, not here.
|
|
229
427
|
if (lineSegments.some((seg) => seg.content !== "")) {
|
|
230
|
-
|
|
428
|
+
close(lineSegments, ink, "end");
|
|
231
429
|
}
|
|
232
430
|
// Open-end by default; cap only when maxLines is set.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
* that re-wraps to exactly those lines. The wrap consumed the space at each line break,
|
|
243
|
-
* so re-insert one between lines (unless the piece already ends with one) - otherwise the
|
|
244
|
-
* last word of a line and the first of the next would fuse ("a b" + "c d" -> "a bc d").
|
|
245
|
-
* Used by text fragmentation to rebuild the fitted/remainder halves of a split paragraph.
|
|
246
|
-
*/
|
|
247
|
-
export function segmentLinesToSegments(lines) {
|
|
248
|
-
const result = [];
|
|
249
|
-
lines.forEach((line, lineIndex) => {
|
|
250
|
-
line.segments.forEach((segment) => result.push({ ...segment }));
|
|
251
|
-
if (lineIndex < lines.length - 1) {
|
|
252
|
-
const last = result[result.length - 1];
|
|
253
|
-
if (last && !last.content.endsWith(" "))
|
|
254
|
-
last.content += " ";
|
|
431
|
+
let kept = lines;
|
|
432
|
+
if (maxLines != null && lines.length > maxLines) {
|
|
433
|
+
// The text ends at the cap, as far as this element goes: its last line is not a wrapped one.
|
|
434
|
+
kept = lines.slice(0, maxLines);
|
|
435
|
+
kept[kept.length - 1] = { ...kept[kept.length - 1], end: "end" };
|
|
436
|
+
if (overflow === "ellipsis") {
|
|
437
|
+
// The same room that line was broken against: the first one lost the indent to it.
|
|
438
|
+
const room = kept.length === 1 ? maxWidth - indent : maxWidth;
|
|
439
|
+
ellipsizeLine(kept[kept.length - 1], defaults, room, metrics);
|
|
255
440
|
}
|
|
256
|
-
}
|
|
257
|
-
|
|
441
|
+
}
|
|
442
|
+
// A piece of a paragraph split across pages draws only its own lines.
|
|
443
|
+
return defaults.shownLines === undefined ? kept : kept.slice(0, defaults.shownLines);
|
|
258
444
|
}
|
|
259
445
|
/** Appends "…" to a single line, dropping trailing words (then characters) until the line plus the
|
|
260
446
|
* ellipsis fits `maxWidth`. Falls back to a bare "…" if not even one character fits. */
|
|
@@ -282,22 +468,24 @@ function ellipsize(line, fontFamily, fontSize, fontStyle, maxWidth, metrics, let
|
|
|
282
468
|
}
|
|
283
469
|
return ELLIPSIS;
|
|
284
470
|
}
|
|
285
|
-
/** Ellipsizes the LAST segment of a truncated
|
|
286
|
-
*
|
|
287
|
-
function
|
|
471
|
+
/** Ellipsizes the LAST segment of a truncated line in place (within the width left by the segments
|
|
472
|
+
* before it) and recomputes the line width. */
|
|
473
|
+
function ellipsizeLine(line, defaults, maxWidth, metrics) {
|
|
288
474
|
const segs = line.segments;
|
|
289
475
|
if (segs.length === 0)
|
|
290
476
|
return;
|
|
291
477
|
const lsOf = (seg) => seg.letterSpacing ?? defaults.letterSpacing ?? 0;
|
|
292
|
-
|
|
478
|
+
// Summed word by word, as the breaker sums a line - one owner for a line's width.
|
|
479
|
+
const widthOf = (seg) => singleLineWidth(seg.content, {
|
|
293
480
|
fontFamily: seg.fontFamily || defaults.fontFamily,
|
|
294
481
|
fontSize: seg.fontSize || defaults.fontSize,
|
|
295
482
|
fontStyle: seg.fontStyle || defaults.fontStyle,
|
|
296
|
-
|
|
483
|
+
ligatures: defaults.ligatures,
|
|
484
|
+
}, metrics, lsOf(seg), defaults.wordSpacing);
|
|
297
485
|
let prefix = 0;
|
|
298
486
|
for (let i = 0; i < segs.length - 1; i++)
|
|
299
487
|
prefix += widthOf(segs[i]);
|
|
300
488
|
const last = segs[segs.length - 1];
|
|
301
|
-
last.content = ellipsize(last.content, last.fontFamily || defaults.fontFamily, last.fontSize || defaults.fontSize, last.fontStyle || defaults.fontStyle, maxWidth - prefix, metrics, lsOf(last));
|
|
489
|
+
last.content = ellipsize(last.content, last.fontFamily || defaults.fontFamily, last.fontSize || defaults.fontSize, last.fontStyle || defaults.fontStyle, maxWidth - prefix, metrics, lsOf(last), defaults.wordSpacing, defaults.ligatures);
|
|
302
490
|
line.width = prefix + widthOf(last);
|
|
303
491
|
}
|