ntk 8.22.0 → 8.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/text/fontmanager.js +10 -1
- package/lib/text/layout.js +91 -2
- package/lib/text/shape.js +16 -3
- package/package.json +1 -1
package/lib/text/fontmanager.js
CHANGED
|
@@ -565,6 +565,15 @@ export default class FontManager {
|
|
|
565
565
|
return new TextLayout(this, content, style, options);
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Whether `layout` takes `justify`, and fills a line with its word
|
|
570
|
+
* separators itself. True; a caller that asks before it passes the option
|
|
571
|
+
* keeps its own way of justifying for an engine without it.
|
|
572
|
+
*/
|
|
573
|
+
get justifies() {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
568
577
|
/**
|
|
569
578
|
* Memoized shaping used by TextLayout and the canvas text path, bounded at
|
|
570
579
|
* two generations of `SHAPE_GENERATION` words. `levelsKey` is a compact
|
|
@@ -671,7 +680,7 @@ export default class FontManager {
|
|
|
671
680
|
*/
|
|
672
681
|
_shapeCachedWhole(text, style) {
|
|
673
682
|
const emb = embeddingLevels(text, style.direction);
|
|
674
|
-
const baseLevel = emb.paragraphs
|
|
683
|
+
const baseLevel = emb.paragraphs[0].level;
|
|
675
684
|
const shaped = this._shapeCached(text, style, normalizedLevels(emb.levels, 0, text.length));
|
|
676
685
|
return shaped.baseLevel === baseLevel ? shaped : { ...shaped, baseLevel };
|
|
677
686
|
}
|
package/lib/text/layout.js
CHANGED
|
@@ -12,6 +12,29 @@ const WS = new Set([0x20, 0x09]);
|
|
|
12
12
|
const HARD_BREAKS = /[\n\r\u2028\u2029]+$/;
|
|
13
13
|
const TRAILING_WS = /[ \t]+$/;
|
|
14
14
|
|
|
15
|
+
// The word separators a justified line's room is shared out at (CSS Text 3,
|
|
16
|
+
// 7.4, `text-justify: auto` as a browser has it for the scripts that space
|
|
17
|
+
// their words): a space and a no-break space.
|
|
18
|
+
const SEPARATORS = new Set([0x20, 0xa0]);
|
|
19
|
+
const separates = (g) => g.codePoints.length === 1 && SEPARATORS.has(g.codePoints[0]);
|
|
20
|
+
|
|
21
|
+
// What ends a line `justify` sets as it does a paragraph's last: a line feed,
|
|
22
|
+
// a carriage return or a paragraph separator, as a browser's forced break
|
|
23
|
+
// does. A line separator, U+2028, is not one, so a caller can ask a line to
|
|
24
|
+
// break there and be justified.
|
|
25
|
+
const JUSTIFY_STOPS = /[\n\r\u2029]$/;
|
|
26
|
+
|
|
27
|
+
// `justify`'s lines as bits: those that go on to another (REST), and the
|
|
28
|
+
// paragraph's last with those a forced break ends (LAST).
|
|
29
|
+
const REST = 1;
|
|
30
|
+
const LAST = 2;
|
|
31
|
+
function justifyBits(justify) {
|
|
32
|
+
if (justify === true || justify === 'rest') return REST;
|
|
33
|
+
if (justify === 'last') return LAST;
|
|
34
|
+
if (justify === 'all') return REST | LAST;
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
/** Scripts written without spaces between words, whose line breaking
|
|
16
39
|
* needs a dictionary (UAX #14's SA class, and Javanese, Balinese and
|
|
17
40
|
* Buginese, which are AL there): a token of theirs is a run of words. */
|
|
@@ -162,6 +185,44 @@ function isWsGlyph(g) {
|
|
|
162
185
|
return g.codePoints.length > 0 && g.codePoints.every((cp) => WS.has(cp));
|
|
163
186
|
}
|
|
164
187
|
|
|
188
|
+
/**
|
|
189
|
+
* A line set to fill `width`: what it leaves of it shared equally among its
|
|
190
|
+
* word separators (`SEPARATORS`), each glyph's advance widened by its share
|
|
191
|
+
* — the white space it ends on hangs, and takes none. Its runs are made
|
|
192
|
+
* anew, those with a separator in them over glyphs of their own, since a
|
|
193
|
+
* shaped run is the paragraph's and shared by every layout of it; caret
|
|
194
|
+
* positions, hit testing, drawing and coverage all read the advances, and
|
|
195
|
+
* agree. A line with no separator, or with no room left, keeps its
|
|
196
|
+
* alignment. Answers the line's width.
|
|
197
|
+
*/
|
|
198
|
+
function justifyLine(line, width) {
|
|
199
|
+
const free = width - line.width;
|
|
200
|
+
if (!(free > 0)) return line.width;
|
|
201
|
+
let count = 0;
|
|
202
|
+
for (const r of line.runs) {
|
|
203
|
+
for (const g of r.run.glyphs) if (separates(g)) count++;
|
|
204
|
+
}
|
|
205
|
+
if (count === 0) return line.width;
|
|
206
|
+
const extra = free / count;
|
|
207
|
+
let shift = 0;
|
|
208
|
+
line.runs = line.runs.map((r) => {
|
|
209
|
+
const moved = shift ? { ...r, x: r.x + shift } : r;
|
|
210
|
+
let k = 0;
|
|
211
|
+
for (const g of r.run.glyphs) if (separates(g)) k++;
|
|
212
|
+
if (k === 0) return moved;
|
|
213
|
+
shift += k * extra;
|
|
214
|
+
const glyphs = r.run.glyphs.map((g) => (separates(g) ? { ...g, ax: g.ax + extra } : g));
|
|
215
|
+
return {
|
|
216
|
+
...moved,
|
|
217
|
+
width: r.width + k * extra,
|
|
218
|
+
run: { ...r.run, glyphs, width: r.run.width + k * extra }
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
line.width += count * extra;
|
|
222
|
+
line.advance += count * extra;
|
|
223
|
+
return line.width;
|
|
224
|
+
}
|
|
225
|
+
|
|
165
226
|
/**
|
|
166
227
|
* Multi-line text layout: breaks (possibly styled) text into lines for a
|
|
167
228
|
* target container width, with full shaping — kerning, ligatures, complex
|
|
@@ -205,6 +266,13 @@ function isWsGlyph(g) {
|
|
|
205
266
|
* past the line's end, as CSS's `overflow-wrap: normal` does — but for
|
|
206
267
|
* text in a script written without spaces (Thai, Khmer, Javanese…), whose
|
|
207
268
|
* words the breaker cannot find, which is cut as a fallback
|
|
269
|
+
* - `justify` — the lines set to fill `maxWidth`, what each leaves of it
|
|
270
|
+
* shared equally among its word separators, a space and a no-break space:
|
|
271
|
+
* `true` (or `'rest'`) for every line but the paragraph's last and each a
|
|
272
|
+
* forced break ends, as CSS's `text-align: justify`; `'last'` for only
|
|
273
|
+
* those; `'all'` for every line. A line with no separator, or none of the
|
|
274
|
+
* width to spare, keeps `align`'s place, as do the lines not justified.
|
|
275
|
+
* The breaks are the layout's at its width; justifying moves none
|
|
208
276
|
* - `fit` — 'items' fits and measures a line as a browser does: each span's
|
|
209
277
|
* part of it rounded up to a 64th of a pixel before it is added, as Blink
|
|
210
278
|
* rounds an element's text on a line up to a LayoutUnit; a line's `width`
|
|
@@ -436,6 +504,11 @@ export class TextLayout {
|
|
|
436
504
|
natural = m.lineHeight;
|
|
437
505
|
}
|
|
438
506
|
if (runs.length) fitted += round(item);
|
|
507
|
+
// a line's place in the paragraph, for `justify`: its last, which a
|
|
508
|
+
// `maxLines` cut leaves no line, or one a forced break ends
|
|
509
|
+
const ends =
|
|
510
|
+
(li === lineTokens.length - 1 && !this.truncated) ||
|
|
511
|
+
JUSTIFY_STOPS.test(text.slice(Math.max(lineStart, lineEnd - 1), lineEnd));
|
|
439
512
|
if (fitted > layoutWidth) layoutWidth = fitted;
|
|
440
513
|
// Half-leading (CSS Inline Layout 3): the slack between the glyphs and
|
|
441
514
|
// the line box is split evenly above and below, rather than all of it
|
|
@@ -462,11 +535,27 @@ export class TextLayout {
|
|
|
462
535
|
start: lineStart,
|
|
463
536
|
end: lineEnd,
|
|
464
537
|
_contentEnd: contentEnd,
|
|
465
|
-
_trailing: trailing
|
|
538
|
+
_trailing: trailing,
|
|
539
|
+
_ends: ends,
|
|
540
|
+
_elided: !!ellipsis
|
|
466
541
|
});
|
|
467
542
|
y += box;
|
|
468
543
|
}
|
|
469
544
|
|
|
545
|
+
// ---- justification ----
|
|
546
|
+
// After the lines are broken, so a width the lines are set at is no
|
|
547
|
+
// shaping: a paragraph laid out again at another width breaks the glyphs
|
|
548
|
+
// it has into other lines and spaces them, where one that spaced its
|
|
549
|
+
// words through letter spacing was shaped again at every width.
|
|
550
|
+
const justify = justifyBits(options.justify);
|
|
551
|
+
if (justify && Number.isFinite(maxWidth)) {
|
|
552
|
+
for (const line of this.lines) {
|
|
553
|
+
if (line._elided || !(justify & (line._ends ? LAST : REST))) continue;
|
|
554
|
+
const width = justifyLine(line, maxWidth);
|
|
555
|
+
if (width > layoutWidth) layoutWidth = width;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
470
559
|
this.width = layoutWidth;
|
|
471
560
|
this.height = y;
|
|
472
561
|
|
|
@@ -537,7 +626,7 @@ export class TextLayout {
|
|
|
537
626
|
const text = spans.map((s) => s.text).join('');
|
|
538
627
|
const emb = embeddingLevels(text, direction);
|
|
539
628
|
const levels = emb.levels;
|
|
540
|
-
const baseLevel = emb.paragraphs
|
|
629
|
+
const baseLevel = emb.paragraphs[0].level & 1;
|
|
541
630
|
|
|
542
631
|
// span boundaries, for binary search by char position
|
|
543
632
|
const spanStarts = [];
|
package/lib/text/shape.js
CHANGED
|
@@ -21,17 +21,30 @@ function neverSwitches(cp) {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* UAX#9 embedding levels for a string. `direction` is 'ltr', 'rtl' or 'auto'
|
|
24
|
-
* (first-strong detection). Returns bidi-js's result object
|
|
24
|
+
* (first-strong detection). Returns bidi-js's result object, which always
|
|
25
|
+
* has a first paragraph: its level is the text's base level.
|
|
25
26
|
*/
|
|
26
27
|
export function embeddingLevels(text, direction = 'auto') {
|
|
27
28
|
const explicit = direction === 'ltr' || direction === 'rtl' ? direction : null;
|
|
29
|
+
// An empty text is a paragraph too, at the level its direction names.
|
|
30
|
+
// bidi-js finds no paragraph in it, and a base level read as 0 for want
|
|
31
|
+
// of one set an empty right-to-left field's line, and the caret on it,
|
|
32
|
+
// at the left — where the same field holding only a line break had it
|
|
33
|
+
// at the right. Under 'auto' there is no strong character to find, and
|
|
34
|
+
// the paragraph is left-to-right (UAX#9 P3).
|
|
35
|
+
if (!text.length) {
|
|
36
|
+
return {
|
|
37
|
+
levels: new Uint8Array(0),
|
|
38
|
+
paragraphs: [{ start: 0, end: -1, level: explicit === 'rtl' ? 1 : 0 }]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
28
41
|
// Text that nothing can make right-to-left is level 0 throughout, which
|
|
29
42
|
// is what the algorithm would work out a character at a time: every
|
|
30
43
|
// layout asks, and most text is Latin, CJK or digits with no bidi at all.
|
|
31
44
|
if (explicit !== 'rtl' && !MAY_REVERSE.test(text)) {
|
|
32
45
|
return {
|
|
33
46
|
levels: new Uint8Array(text.length),
|
|
34
|
-
paragraphs:
|
|
47
|
+
paragraphs: [{ start: 0, end: text.length - 1, level: 0 }]
|
|
35
48
|
};
|
|
36
49
|
}
|
|
37
50
|
return bidi.getEmbeddingLevels(text, explicit);
|
|
@@ -161,7 +174,7 @@ export function shapeText(fonts, text, style, levels) {
|
|
|
161
174
|
if (!levels) {
|
|
162
175
|
const emb = embeddingLevels(text, style.direction);
|
|
163
176
|
levels = emb.levels;
|
|
164
|
-
baseLevel = emb.paragraphs
|
|
177
|
+
baseLevel = emb.paragraphs[0].level;
|
|
165
178
|
} else {
|
|
166
179
|
baseLevel = levels.length ? levels[0] & ~1 : 0;
|
|
167
180
|
}
|