@effing/canvas 0.42.0 → 0.44.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/README.md +37 -26
- package/dist/index.d.ts +8 -6
- package/dist/index.js +1196 -1130
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
package/dist/index.js
CHANGED
|
@@ -130,6 +130,7 @@ var INHERITABLE_PROPS = [
|
|
|
130
130
|
"letterSpacing",
|
|
131
131
|
"whiteSpace",
|
|
132
132
|
"wordBreak",
|
|
133
|
+
"overflowWrap",
|
|
133
134
|
"textOverflow",
|
|
134
135
|
"lineClamp",
|
|
135
136
|
"textBoxTrim",
|
|
@@ -138,8 +139,22 @@ var INHERITABLE_PROPS = [
|
|
|
138
139
|
"WebkitTextStrokeWidth",
|
|
139
140
|
"WebkitTextStrokeColor"
|
|
140
141
|
];
|
|
142
|
+
var CSS_NUMBER = /^[+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/;
|
|
143
|
+
var FONT_WEIGHT_KEYWORDS = /* @__PURE__ */ new Set(["normal", "bold", "bolder", "lighter"]);
|
|
144
|
+
function isValidFontWeight(weight) {
|
|
145
|
+
if (typeof weight === "string") {
|
|
146
|
+
const text = weight.trim();
|
|
147
|
+
if (FONT_WEIGHT_KEYWORDS.has(text.toLowerCase())) return true;
|
|
148
|
+
if (!CSS_NUMBER.test(text)) return false;
|
|
149
|
+
weight = Number(text);
|
|
150
|
+
}
|
|
151
|
+
return Number.isFinite(weight) && weight >= 1 && weight <= 1e3;
|
|
152
|
+
}
|
|
141
153
|
function resolveStyle(rawStyle, parentStyle) {
|
|
142
154
|
const style = { ...rawStyle };
|
|
155
|
+
if (style.fontWeight !== void 0 && !isValidFontWeight(style.fontWeight)) {
|
|
156
|
+
delete style.fontWeight;
|
|
157
|
+
}
|
|
143
158
|
for (const prop of INHERITABLE_PROPS) {
|
|
144
159
|
if (style[prop] === void 0 && parentStyle[prop] !== void 0) {
|
|
145
160
|
style[prop] = parentStyle[prop];
|
|
@@ -301,7 +316,9 @@ var LENGTH_STRING_PROPS = [
|
|
|
301
316
|
["transformOrigin", ""],
|
|
302
317
|
["clipPath", "px"],
|
|
303
318
|
["filter", "px"],
|
|
304
|
-
["backdropFilter", "px"]
|
|
319
|
+
["backdropFilter", "px"],
|
|
320
|
+
["boxShadow", "px"],
|
|
321
|
+
["textShadow", "px"]
|
|
305
322
|
];
|
|
306
323
|
function resolveLengthUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize, suffix = "") {
|
|
307
324
|
return transform.replace(
|
|
@@ -320,176 +337,35 @@ function resolveLengthUnits(transform, viewportWidth, viewportHeight, fontSize,
|
|
|
320
337
|
);
|
|
321
338
|
}
|
|
322
339
|
|
|
323
|
-
// src/jsx/
|
|
324
|
-
import {
|
|
325
|
-
|
|
326
|
-
// src/jsx/font-metrics.ts
|
|
327
|
-
import { inflateSync } from "zlib";
|
|
328
|
-
function fontMetricsToPx(metrics, fontSize) {
|
|
329
|
-
return {
|
|
330
|
-
ascent: metrics.ascender / metrics.unitsPerEm * fontSize,
|
|
331
|
-
descent: -metrics.descender / metrics.unitsPerEm * fontSize
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
var WOFF_SIGNATURE = 2001684038;
|
|
335
|
-
function readWoffTable(view, entry) {
|
|
336
|
-
const compressed = entry.compLength < entry.origLength;
|
|
337
|
-
if (compressed) {
|
|
338
|
-
try {
|
|
339
|
-
const compData = new Uint8Array(
|
|
340
|
-
view.buffer,
|
|
341
|
-
view.byteOffset + entry.offset,
|
|
342
|
-
entry.compLength
|
|
343
|
-
);
|
|
344
|
-
const decompressed = inflateSync(compData);
|
|
345
|
-
return new DataView(
|
|
346
|
-
decompressed.buffer,
|
|
347
|
-
decompressed.byteOffset,
|
|
348
|
-
decompressed.byteLength
|
|
349
|
-
);
|
|
350
|
-
} catch {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return new DataView(
|
|
355
|
-
view.buffer,
|
|
356
|
-
view.byteOffset + entry.offset,
|
|
357
|
-
entry.origLength
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
function parseWoff(view) {
|
|
361
|
-
if (view.byteLength < 44) return null;
|
|
362
|
-
const numTables = view.getUint16(12);
|
|
363
|
-
if (view.byteLength < 44 + numTables * 20) return null;
|
|
364
|
-
let headEntry = null;
|
|
365
|
-
let hheaEntry = null;
|
|
366
|
-
for (let i = 0; i < numTables; i++) {
|
|
367
|
-
const r = 44 + i * 20;
|
|
368
|
-
const tag = String.fromCharCode(view.getUint8(r)) + String.fromCharCode(view.getUint8(r + 1)) + String.fromCharCode(view.getUint8(r + 2)) + String.fromCharCode(view.getUint8(r + 3));
|
|
369
|
-
const offset = view.getUint32(r + 4);
|
|
370
|
-
const compLength = view.getUint32(r + 8);
|
|
371
|
-
const origLength = view.getUint32(r + 12);
|
|
372
|
-
if (tag === "head") headEntry = { offset, compLength, origLength };
|
|
373
|
-
else if (tag === "hhea") hheaEntry = { offset, compLength, origLength };
|
|
374
|
-
}
|
|
375
|
-
if (!headEntry || !hheaEntry) return null;
|
|
376
|
-
const headView = readWoffTable(view, headEntry);
|
|
377
|
-
const hheaView = readWoffTable(view, hheaEntry);
|
|
378
|
-
if (!headView || !hheaView) return null;
|
|
379
|
-
if (headView.byteLength < 20 || hheaView.byteLength < 8) return null;
|
|
380
|
-
const unitsPerEm = headView.getUint16(18);
|
|
381
|
-
const ascender = hheaView.getInt16(4);
|
|
382
|
-
const descender = hheaView.getInt16(6);
|
|
383
|
-
return { unitsPerEm, ascender, descender };
|
|
384
|
-
}
|
|
385
|
-
function parseSfnt(view) {
|
|
386
|
-
const numTables = view.getUint16(4);
|
|
387
|
-
if (view.byteLength < 12 + numTables * 16) return null;
|
|
388
|
-
let headOffset = -1;
|
|
389
|
-
let hheaOffset = -1;
|
|
390
|
-
for (let i = 0; i < numTables; i++) {
|
|
391
|
-
const recordOffset = 12 + i * 16;
|
|
392
|
-
const tag = String.fromCharCode(view.getUint8(recordOffset)) + String.fromCharCode(view.getUint8(recordOffset + 1)) + String.fromCharCode(view.getUint8(recordOffset + 2)) + String.fromCharCode(view.getUint8(recordOffset + 3));
|
|
393
|
-
const offset = view.getUint32(recordOffset + 8);
|
|
394
|
-
if (tag === "head") headOffset = offset;
|
|
395
|
-
else if (tag === "hhea") hheaOffset = offset;
|
|
396
|
-
}
|
|
397
|
-
if (headOffset < 0 || hheaOffset < 0) return null;
|
|
398
|
-
if (view.byteLength < headOffset + 20) return null;
|
|
399
|
-
const unitsPerEm = view.getUint16(headOffset + 18);
|
|
400
|
-
if (view.byteLength < hheaOffset + 8) return null;
|
|
401
|
-
const ascender = view.getInt16(hheaOffset + 4);
|
|
402
|
-
const descender = view.getInt16(hheaOffset + 6);
|
|
403
|
-
return { unitsPerEm, ascender, descender };
|
|
404
|
-
}
|
|
405
|
-
function parseFontMetrics(data) {
|
|
406
|
-
const buf = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
407
|
-
const view = new DataView(buf);
|
|
408
|
-
if (buf.byteLength < 12) return null;
|
|
409
|
-
const signature = view.getUint32(0);
|
|
410
|
-
if (signature === WOFF_SIGNATURE) {
|
|
411
|
-
return parseWoff(view);
|
|
412
|
-
}
|
|
413
|
-
return parseSfnt(view);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// src/jsx/font.ts
|
|
417
|
-
var registeredFonts = /* @__PURE__ */ new Set();
|
|
418
|
-
var metricsCache = /* @__PURE__ */ new Map();
|
|
419
|
-
function registerFont(font) {
|
|
420
|
-
const key = `${font.name}:${font.weight}:${font.style}`;
|
|
421
|
-
if (registeredFonts.has(key)) return;
|
|
422
|
-
const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
|
|
423
|
-
GlobalFonts.register(buffer, font.name);
|
|
424
|
-
const metrics = parseFontMetrics(font.data);
|
|
425
|
-
if (metrics) {
|
|
426
|
-
metricsCache.set(key, metrics);
|
|
427
|
-
}
|
|
428
|
-
registeredFonts.add(key);
|
|
429
|
-
}
|
|
430
|
-
function getFontMetrics(family, weight, style) {
|
|
431
|
-
const families = family.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, ""));
|
|
432
|
-
for (const name of families) {
|
|
433
|
-
const exact = metricsCache.get(`${name}:${weight}:${style}`);
|
|
434
|
-
if (exact) return exact;
|
|
435
|
-
for (const [key, metrics] of metricsCache) {
|
|
436
|
-
if (key.startsWith(`${name}:`)) return metrics;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
function registerFontFromPath(path, nameAlias) {
|
|
442
|
-
GlobalFonts.registerFromPath(path, nameAlias ?? "");
|
|
443
|
-
}
|
|
444
|
-
function registeredFamilies() {
|
|
445
|
-
return GlobalFonts.families.map((f) => f.family);
|
|
446
|
-
}
|
|
447
|
-
function ensureFontsRegistered(fonts) {
|
|
448
|
-
for (const font of fonts) {
|
|
449
|
-
registerFont(font);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
340
|
+
// src/jsx/text/native.ts
|
|
341
|
+
import { Paragraph } from "@effing/skia/extensions";
|
|
452
342
|
|
|
453
343
|
// src/jsx/language.ts
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
// src/jsx/text/linebreak.ts
|
|
480
|
-
import LineBreaker from "linebreak";
|
|
481
|
-
function findBreakOpportunities(text) {
|
|
482
|
-
const breaker = new LineBreaker(text);
|
|
483
|
-
const opportunities = [];
|
|
484
|
-
let bk = breaker.nextBreak();
|
|
485
|
-
while (bk) {
|
|
486
|
-
opportunities.push({
|
|
487
|
-
position: bk.position,
|
|
488
|
-
required: bk.required ?? false
|
|
489
|
-
});
|
|
490
|
-
bk = breaker.nextBreak();
|
|
491
|
-
}
|
|
492
|
-
return opportunities;
|
|
344
|
+
var UNASSIGNED_PICTOGRAPHIC = String.raw`(?=\p{Cn})\p{Extended_Pictographic}`;
|
|
345
|
+
var EMOJI_CHAR = String.raw`[\p{Emoji}\p{Extended_Pictographic}]`;
|
|
346
|
+
var KEYCAP_BASE = "[0-9#*]";
|
|
347
|
+
var TEXT_EMOJI_CHAR = String.raw`[0-9#*\u00A9\u00AE\u2122]`;
|
|
348
|
+
var TAGS = String.raw`[\u{E0020}-\u{E007E}]+\u{E007F}`;
|
|
349
|
+
var PRESENTED_EMOJI = [
|
|
350
|
+
// Keycap: 1️⃣, and the unqualified 1⃣.
|
|
351
|
+
String.raw`${KEYCAP_BASE}\uFE0F?\u20E3`,
|
|
352
|
+
// Flag: a pair of regional indicators.
|
|
353
|
+
String.raw`\p{Regional_Indicator}{2}`,
|
|
354
|
+
// Emoji presentation sequence (©️), modifier sequence (👍🏽, ☝🏽) or tag
|
|
355
|
+
// sequence (🏴).
|
|
356
|
+
String.raw`(?!${KEYCAP_BASE})${EMOJI_CHAR}(?:\uFE0F|\p{Emoji_Modifier}|${TAGS})`,
|
|
357
|
+
// An emoji character on its own, emoji presentation by default (🌍, ⭐) or
|
|
358
|
+
// text (❤, ☎, 🕵), but not one that stays text.
|
|
359
|
+
String.raw`(?!${TEXT_EMOJI_CHAR})\p{Emoji}`,
|
|
360
|
+
UNASSIGNED_PICTOGRAPHIC
|
|
361
|
+
].join("|");
|
|
362
|
+
var ZWJ_ELEMENT = `(?:${PRESENTED_EMOJI}|(?!${KEYCAP_BASE})${EMOJI_CHAR})`;
|
|
363
|
+
var EMOJI_RE = new RegExp(
|
|
364
|
+
`^(?:${PRESENTED_EMOJI}|${ZWJ_ELEMENT}(?:\\u200D${ZWJ_ELEMENT})+)$`,
|
|
365
|
+
"u"
|
|
366
|
+
);
|
|
367
|
+
function isEmoji(grapheme) {
|
|
368
|
+
return EMOJI_RE.test(grapheme);
|
|
493
369
|
}
|
|
494
370
|
|
|
495
371
|
// src/jsx/text/measure.ts
|
|
@@ -501,16 +377,6 @@ function getScratchCtx() {
|
|
|
501
377
|
}
|
|
502
378
|
return scratchCtx;
|
|
503
379
|
}
|
|
504
|
-
function withUnsnappedText(ctx, fn) {
|
|
505
|
-
const previous = ctx.textRendering;
|
|
506
|
-
if (previous === "geometricPrecision") return fn();
|
|
507
|
-
ctx.textRendering = "geometricPrecision";
|
|
508
|
-
try {
|
|
509
|
-
return fn();
|
|
510
|
-
} finally {
|
|
511
|
-
ctx.textRendering = previous;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
380
|
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
515
381
|
"serif",
|
|
516
382
|
"sans-serif",
|
|
@@ -536,34 +402,18 @@ function quoteFontFamilies(fontFamily) {
|
|
|
536
402
|
function setFont(ctx, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal") {
|
|
537
403
|
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${quoteFontFamilies(fontFamily)}`;
|
|
538
404
|
}
|
|
539
|
-
function
|
|
540
|
-
const c =
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
405
|
+
function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, line, edge) {
|
|
406
|
+
const c = getScratchCtx();
|
|
407
|
+
const measure = (text) => {
|
|
408
|
+
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
409
|
+
const previous = c.textRendering;
|
|
410
|
+
c.textRendering = "geometricPrecision";
|
|
411
|
+
try {
|
|
412
|
+
return c.measureText(text);
|
|
413
|
+
} finally {
|
|
414
|
+
c.textRendering = previous;
|
|
415
|
+
}
|
|
550
416
|
};
|
|
551
|
-
}
|
|
552
|
-
function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx, fontMetrics) {
|
|
553
|
-
const c = ctx ?? getScratchCtx();
|
|
554
|
-
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
555
|
-
const measure = (text) => withUnsnappedText(c, () => c.measureText(text));
|
|
556
|
-
const refMetrics = measure("M");
|
|
557
|
-
let fontAscent;
|
|
558
|
-
let fontDescent;
|
|
559
|
-
if (fontMetrics) {
|
|
560
|
-
const px = fontMetricsToPx(fontMetrics, fontSize);
|
|
561
|
-
fontAscent = px.ascent;
|
|
562
|
-
fontDescent = px.descent;
|
|
563
|
-
} else {
|
|
564
|
-
fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
|
|
565
|
-
fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
|
|
566
|
-
}
|
|
567
417
|
const parts = edge.trim().split(/\s+/);
|
|
568
418
|
const overEdge = parts[0] ?? "text";
|
|
569
419
|
const underEdge = parts.length > 1 ? parts[1] : overEdge;
|
|
@@ -583,7 +433,7 @@ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHei
|
|
|
583
433
|
case "ideographic-ink":
|
|
584
434
|
case "text":
|
|
585
435
|
default:
|
|
586
|
-
targetAscent =
|
|
436
|
+
targetAscent = line.ascent;
|
|
587
437
|
break;
|
|
588
438
|
}
|
|
589
439
|
let targetDescent;
|
|
@@ -595,48 +445,24 @@ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHei
|
|
|
595
445
|
case "ideographic-ink":
|
|
596
446
|
case "text":
|
|
597
447
|
default:
|
|
598
|
-
targetDescent =
|
|
448
|
+
targetDescent = line.descent;
|
|
599
449
|
break;
|
|
600
450
|
}
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
const underTrim = halfLeading + (fontDescent - targetDescent);
|
|
451
|
+
const overTrim = line.baseline - targetAscent;
|
|
452
|
+
const underTrim = line.lineHeight - line.baseline - targetDescent;
|
|
604
453
|
return {
|
|
605
454
|
overTrim: Math.max(0, overTrim),
|
|
606
455
|
underTrim: Math.max(0, underTrim)
|
|
607
456
|
};
|
|
608
457
|
}
|
|
609
|
-
function measureWord(word, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx, letterSpacing = 0) {
|
|
610
|
-
const base = measureText(
|
|
611
|
-
word,
|
|
612
|
-
fontSize,
|
|
613
|
-
fontFamily,
|
|
614
|
-
fontWeight,
|
|
615
|
-
fontStyle,
|
|
616
|
-
ctx
|
|
617
|
-
).width;
|
|
618
|
-
if (letterSpacing === 0 || word.length === 0) return base;
|
|
619
|
-
return base + letterSpacing * word.length;
|
|
620
|
-
}
|
|
621
458
|
|
|
622
459
|
// src/jsx/text/native.ts
|
|
623
|
-
import { Paragraph } from "@effing/skia/extensions";
|
|
624
|
-
function canLayoutNatively(text, style, emojiEnabled) {
|
|
625
|
-
if (text === "") return false;
|
|
626
|
-
if (style.wordBreak === "break-all") return false;
|
|
627
|
-
if (style.whiteSpace === "pre" && /[ \t](?:\n|$)/.test(text)) return false;
|
|
628
|
-
if (emojiEnabled) {
|
|
629
|
-
for (const char of text) {
|
|
630
|
-
if (isEmoji(char)) return false;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return true;
|
|
634
|
-
}
|
|
635
460
|
function toWeight(weight) {
|
|
636
461
|
if (typeof weight === "number") return weight;
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
462
|
+
const text = String(weight ?? "400").trim().toLowerCase();
|
|
463
|
+
if (text === "bold") return 700;
|
|
464
|
+
const parsed = text === "" ? NaN : Number(text);
|
|
465
|
+
return Number.isFinite(parsed) ? parsed : 400;
|
|
640
466
|
}
|
|
641
467
|
function toFontStyle(fontStyle) {
|
|
642
468
|
if (fontStyle?.startsWith("italic")) return "italic";
|
|
@@ -654,7 +480,17 @@ var TEXT_ALIGNS = /* @__PURE__ */ new Set([
|
|
|
654
480
|
function toTextAlign(textAlign) {
|
|
655
481
|
return textAlign !== void 0 && TEXT_ALIGNS.has(textAlign) ? textAlign : "left";
|
|
656
482
|
}
|
|
483
|
+
function toWordBreak(style) {
|
|
484
|
+
const { wordBreak } = style;
|
|
485
|
+
return wordBreak === "break-all" || wordBreak === "keep-all" ? wordBreak : "normal";
|
|
486
|
+
}
|
|
487
|
+
function toOverflowWrap(style) {
|
|
488
|
+
const { overflowWrap, wordBreak } = style;
|
|
489
|
+
return overflowWrap === "break-word" || overflowWrap === "anywhere" || wordBreak === "break-word" ? "break-word" : "normal";
|
|
490
|
+
}
|
|
657
491
|
var MIN_WIDTH = 0.01;
|
|
492
|
+
var MIN_FONT_SIZE = 1e-3;
|
|
493
|
+
var EMOJI_DROP = 0.1;
|
|
658
494
|
var MAX_PENDING_PARAGRAPHS = 2e3;
|
|
659
495
|
var pendingParagraphs = 0;
|
|
660
496
|
var turnWatched = false;
|
|
@@ -671,35 +507,119 @@ async function releaseParagraphs() {
|
|
|
671
507
|
if (pendingParagraphs < MAX_PENDING_PARAGRAPHS) return;
|
|
672
508
|
await new Promise((resolve) => setImmediate(resolve));
|
|
673
509
|
}
|
|
674
|
-
function
|
|
675
|
-
const
|
|
510
|
+
function halfLeadingLineBox(lineHeight, ascent, descent) {
|
|
511
|
+
const a = Math.round(ascent);
|
|
512
|
+
const d = Math.round(descent);
|
|
513
|
+
const leading = (lineHeight - a - d) * 64;
|
|
514
|
+
return {
|
|
515
|
+
lineHeight,
|
|
516
|
+
baseline: a + Math.floor(Math.trunc(leading / 2) / 64),
|
|
517
|
+
ascent: a,
|
|
518
|
+
descent: d
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
var graphemeSegmenter = new Intl.Segmenter(void 0, {
|
|
522
|
+
granularity: "grapheme"
|
|
523
|
+
});
|
|
524
|
+
function splitEmoji(text, fontSize, letterSpacing) {
|
|
525
|
+
const items = [];
|
|
526
|
+
const emoji = [];
|
|
527
|
+
const marks = [];
|
|
528
|
+
let run = "";
|
|
529
|
+
let paragraphIndex = 0;
|
|
530
|
+
let textIndex = 0;
|
|
531
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
532
|
+
if (!isEmoji(segment)) {
|
|
533
|
+
run += segment;
|
|
534
|
+
paragraphIndex += segment.length;
|
|
535
|
+
textIndex += segment.length;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (run) items.push(run);
|
|
539
|
+
run = "";
|
|
540
|
+
items.push({
|
|
541
|
+
// Letter spacing follows every character, an emoji too, but a
|
|
542
|
+
// paragraph doesn't add it to a placeholder.
|
|
543
|
+
width: Math.max(0, fontSize + letterSpacing),
|
|
544
|
+
height: fontSize,
|
|
545
|
+
verticalAlign: "baseline",
|
|
546
|
+
baselineOffset: fontSize * (1 - EMOJI_DROP),
|
|
547
|
+
// Lines break around it as around an emoji, not an inline-block: it
|
|
548
|
+
// stays with the punctuation next to it, so "Hi 🎉! ok" breaks as
|
|
549
|
+
// "Hi | 🎉! | ok" and "(🎉)" stays whole, as in browsers.
|
|
550
|
+
lineBreak: "emoji"
|
|
551
|
+
});
|
|
552
|
+
emoji.push(segment);
|
|
553
|
+
marks.push([paragraphIndex, textIndex]);
|
|
554
|
+
paragraphIndex += 1;
|
|
555
|
+
textIndex += segment.length;
|
|
556
|
+
marks.push([paragraphIndex, textIndex]);
|
|
557
|
+
}
|
|
558
|
+
if (run) items.push(run);
|
|
559
|
+
if (emoji.length === 0) return { items: [text], emoji };
|
|
560
|
+
return {
|
|
561
|
+
items,
|
|
562
|
+
emoji,
|
|
563
|
+
toTextIndex: (index) => {
|
|
564
|
+
let mark = [0, 0];
|
|
565
|
+
for (const m of marks) {
|
|
566
|
+
if (m[0] > index) break;
|
|
567
|
+
mark = m;
|
|
568
|
+
}
|
|
569
|
+
return mark[1] + (index - mark[0]);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
|
|
574
|
+
const styledFontSize = style.fontSize ?? 16;
|
|
575
|
+
const fontSize = Number.isFinite(styledFontSize) ? Math.max(styledFontSize, MIN_FONT_SIZE) : 16;
|
|
676
576
|
const fontFamily = style.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
677
577
|
const fontWeight = style.fontWeight ?? 400;
|
|
678
578
|
const fontStyle = style.fontStyle ?? "normal";
|
|
679
|
-
const letterSpacing = typeof style.letterSpacing === "number" ? style.letterSpacing : 0;
|
|
579
|
+
const letterSpacing = typeof style.letterSpacing === "number" && Number.isFinite(style.letterSpacing) ? style.letterSpacing : 0;
|
|
680
580
|
const whiteSpace = style.whiteSpace ?? "normal";
|
|
681
581
|
const noWrap = whiteSpace === "nowrap" || whiteSpace === "pre";
|
|
682
|
-
const lineClamp = style.lineClamp && style.lineClamp
|
|
582
|
+
const lineClamp = style.lineClamp && style.lineClamp >= 1 ? Math.floor(style.lineClamp) : void 0;
|
|
683
583
|
const ellipsis = lineClamp !== void 0 || noWrap && style.textOverflow === "ellipsis" ? "\u2026" : void 0;
|
|
684
584
|
const width = maxWidth > 0 ? maxWidth : MIN_WIDTH;
|
|
685
|
-
const
|
|
585
|
+
const content = emojiEnabled ? splitEmoji(text, fontSize, letterSpacing) : { items: [text], emoji: [] };
|
|
586
|
+
const paragraph = new Paragraph(content.items, {
|
|
686
587
|
fontFamily: quoteFontFamilies(fontFamily),
|
|
687
588
|
fontSize,
|
|
688
589
|
fontWeight: toWeight(fontWeight),
|
|
689
590
|
fontStyle: toFontStyle(fontStyle),
|
|
690
591
|
letterSpacing,
|
|
691
|
-
|
|
592
|
+
// Undefined for `normal`, which the paragraph sizes as Chrome does.
|
|
593
|
+
lineHeight,
|
|
692
594
|
textAlign: toTextAlign(style.textAlign),
|
|
693
595
|
noWrap,
|
|
694
596
|
maxLines: lineClamp,
|
|
695
|
-
ellipsis
|
|
597
|
+
ellipsis,
|
|
598
|
+
// `pre` and `pre-wrap` keep the spaces before a line break in the line.
|
|
599
|
+
keepTrailingWhitespace: whiteSpace === "pre" || whiteSpace === "pre-wrap",
|
|
600
|
+
wordBreak: toWordBreak(style),
|
|
601
|
+
overflowWrap: toOverflowWrap(style)
|
|
696
602
|
});
|
|
697
603
|
countParagraph();
|
|
698
604
|
const layout = paragraph.layout(width);
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
605
|
+
const box = halfLeadingLineBox(
|
|
606
|
+
layout.lineHeight,
|
|
607
|
+
layout.ascent,
|
|
608
|
+
layout.descent
|
|
609
|
+
);
|
|
610
|
+
const lines = layout.lines.length > 0 ? layout.lines : [
|
|
611
|
+
{
|
|
612
|
+
left: 0,
|
|
613
|
+
width: 0,
|
|
614
|
+
baseline: box.baseline,
|
|
615
|
+
startIndex: 0,
|
|
616
|
+
endIndex: 0,
|
|
617
|
+
hardBreak: true
|
|
618
|
+
}
|
|
619
|
+
];
|
|
620
|
+
const toTextIndex = content.toTextIndex ?? ((index) => index);
|
|
621
|
+
const segments = lines.map((line, i) => ({
|
|
622
|
+
text: text.slice(toTextIndex(line.startIndex), toTextIndex(line.endIndex)),
|
|
703
623
|
x: line.left,
|
|
704
624
|
y: line.baseline,
|
|
705
625
|
width: line.width,
|
|
@@ -714,32 +634,37 @@ function layoutTextNative(text, style, maxWidth, lineHeight) {
|
|
|
714
634
|
letterSpacing,
|
|
715
635
|
lineIndex: i
|
|
716
636
|
}));
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
637
|
+
const emoji = [];
|
|
638
|
+
layout.placeholders.forEach((placed, i) => {
|
|
639
|
+
if (!placed) return;
|
|
640
|
+
emoji.push({
|
|
641
|
+
grapheme: content.emoji[i],
|
|
642
|
+
x: placed.x,
|
|
643
|
+
y: placed.y,
|
|
644
|
+
size: fontSize,
|
|
645
|
+
baseline: lines[placed.line].baseline
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
let height = lines.length * layout.lineHeight;
|
|
721
649
|
let paragraphOffsetY = 0;
|
|
722
650
|
const textBoxTrim = style.textBoxTrim;
|
|
723
|
-
if (textBoxTrim && textBoxTrim !== "none"
|
|
724
|
-
const fontMetrics = {
|
|
725
|
-
unitsPerEm: fontSize,
|
|
726
|
-
ascender: layout.ascent,
|
|
727
|
-
descender: -layout.descent
|
|
728
|
-
};
|
|
651
|
+
if (textBoxTrim && textBoxTrim !== "none") {
|
|
729
652
|
const trim = measureTrimMetrics(
|
|
730
653
|
fontSize,
|
|
731
654
|
fontFamily,
|
|
732
655
|
fontWeight,
|
|
733
656
|
fontStyle,
|
|
734
|
-
|
|
735
|
-
style.textBoxEdge ?? "text"
|
|
736
|
-
void 0,
|
|
737
|
-
fontMetrics
|
|
657
|
+
box,
|
|
658
|
+
style.textBoxEdge ?? "text"
|
|
738
659
|
);
|
|
739
660
|
if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
|
|
740
661
|
for (const seg of segments) seg.y -= trim.overTrim;
|
|
662
|
+
for (const e of emoji) {
|
|
663
|
+
e.y -= trim.overTrim;
|
|
664
|
+
e.baseline -= trim.overTrim;
|
|
665
|
+
}
|
|
741
666
|
height -= trim.overTrim;
|
|
742
|
-
paragraphOffsetY
|
|
667
|
+
paragraphOffsetY -= trim.overTrim;
|
|
743
668
|
}
|
|
744
669
|
if (textBoxTrim === "trim-end" || textBoxTrim === "trim-both") {
|
|
745
670
|
height -= trim.underTrim;
|
|
@@ -749,271 +674,51 @@ function layoutTextNative(text, style, maxWidth, lineHeight) {
|
|
|
749
674
|
segments,
|
|
750
675
|
width: segments.reduce((w, s) => Math.max(w, s.width), 0),
|
|
751
676
|
height,
|
|
677
|
+
maxContentWidth: layout.maxIntrinsicWidth,
|
|
678
|
+
minContentWidth: layout.minIntrinsicWidth,
|
|
752
679
|
paragraph,
|
|
753
|
-
paragraphOffsetY
|
|
680
|
+
paragraphOffsetY,
|
|
681
|
+
emoji
|
|
754
682
|
};
|
|
755
683
|
}
|
|
756
684
|
|
|
757
|
-
// src/jsx/text/
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
let totalWidth = 0;
|
|
761
|
-
let textBuffer = "";
|
|
762
|
-
for (const { segment } of segmenter.segment(word)) {
|
|
763
|
-
let isEmojiSegment = false;
|
|
764
|
-
for (const char of segment) {
|
|
765
|
-
if (isEmoji(char)) {
|
|
766
|
-
isEmojiSegment = true;
|
|
767
|
-
break;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
if (isEmojiSegment) {
|
|
771
|
-
if (textBuffer) {
|
|
772
|
-
totalWidth += measureWord(
|
|
773
|
-
textBuffer,
|
|
774
|
-
fontSize,
|
|
775
|
-
fontFamily,
|
|
776
|
-
fontWeight,
|
|
777
|
-
fontStyle,
|
|
778
|
-
ctx,
|
|
779
|
-
letterSpacing
|
|
780
|
-
);
|
|
781
|
-
textBuffer = "";
|
|
782
|
-
}
|
|
783
|
-
totalWidth += fontSize;
|
|
784
|
-
} else {
|
|
785
|
-
textBuffer += segment;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
if (textBuffer) {
|
|
789
|
-
totalWidth += measureWord(
|
|
790
|
-
textBuffer,
|
|
791
|
-
fontSize,
|
|
792
|
-
fontFamily,
|
|
793
|
-
fontWeight,
|
|
794
|
-
fontStyle,
|
|
795
|
-
ctx,
|
|
796
|
-
letterSpacing
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
return totalWidth;
|
|
685
|
+
// src/jsx/text/white-space.ts
|
|
686
|
+
function isWhiteSpaceOnly(text) {
|
|
687
|
+
return /^[ \t\n\r\f\v]*$/.test(text);
|
|
800
688
|
}
|
|
801
|
-
function
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
lineHeight
|
|
812
|
-
);
|
|
813
|
-
if (result) {
|
|
814
|
-
const textStrokeWidth = resolveTextStrokeWidth(style, fontSize);
|
|
815
|
-
for (const seg of result.segments) {
|
|
816
|
-
seg.textStrokeWidth = textStrokeWidth;
|
|
817
|
-
seg.textStrokeColor = style.WebkitTextStrokeColor;
|
|
818
|
-
}
|
|
819
|
-
return result;
|
|
820
|
-
}
|
|
689
|
+
function collapseWhiteSpace(text, whiteSpace) {
|
|
690
|
+
const pieces = typeof text === "string" ? [text] : text;
|
|
691
|
+
return pieces.map((piece) => collapsePiece(piece, whiteSpace)).join("\n").replace(/\r\n/g, "\n").replace(FINAL_BREAK, "").replace(SEPARATORS, SEPARATOR_SPACE);
|
|
692
|
+
}
|
|
693
|
+
function collapsePiece(text, whiteSpace) {
|
|
694
|
+
let result = text.replace(/[\v\f]/g, "").replace(/\r\n/g, "\n");
|
|
695
|
+
if (whiteSpace !== "pre" && whiteSpace !== "pre-wrap" && /[\t\n\r]| {2}|^ | $/.test(result)) {
|
|
696
|
+
result = result.replace(/[\t\r]/g, " ").replace(/ *\n */g, "\n");
|
|
697
|
+
if (whiteSpace !== "pre-line") result = result.replace(/\n+/g, " ");
|
|
698
|
+
result = result.replace(/ {2,}/g, " ").replace(/^ | $/g, "");
|
|
821
699
|
}
|
|
822
|
-
return
|
|
700
|
+
return result;
|
|
823
701
|
}
|
|
824
|
-
|
|
702
|
+
var FINAL_BREAK = /\n$/;
|
|
703
|
+
var SEPARATORS = /[\u2028\u2029]/g;
|
|
704
|
+
var SEPARATOR_SPACE = "\xA0\u200B";
|
|
705
|
+
|
|
706
|
+
// src/jsx/text/index.ts
|
|
707
|
+
function layoutText(text, style, maxWidth, emojiEnabled) {
|
|
825
708
|
const fontSize = style.fontSize ?? 16;
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
"M",
|
|
833
|
-
fontSize,
|
|
834
|
-
fontFamily,
|
|
835
|
-
fontWeight,
|
|
836
|
-
fontStyle,
|
|
837
|
-
ctx
|
|
838
|
-
);
|
|
839
|
-
const fontMetrics = getFontMetrics(fontFamily, fontWeight, fontStyle);
|
|
840
|
-
const lineHeightPx = resolveLineHeight(
|
|
841
|
-
style.lineHeight,
|
|
842
|
-
fontSize,
|
|
843
|
-
refMetrics,
|
|
844
|
-
fontMetrics
|
|
709
|
+
const result = layoutTextNative(
|
|
710
|
+
applyTextTransform(collapseWhiteSpace(text, style.whiteSpace), style),
|
|
711
|
+
style,
|
|
712
|
+
maxWidth,
|
|
713
|
+
resolveLineHeight(style.lineHeight, fontSize),
|
|
714
|
+
emojiEnabled
|
|
845
715
|
);
|
|
846
|
-
const letterSpacing = typeof style.letterSpacing === "number" ? style.letterSpacing : 0;
|
|
847
|
-
const whiteSpace = style.whiteSpace ?? "normal";
|
|
848
|
-
const wordBreak = style.wordBreak ?? "normal";
|
|
849
|
-
const textOverflow = style.textOverflow ?? "clip";
|
|
850
|
-
const textDecoration = style.textDecoration;
|
|
851
716
|
const textStrokeWidth = resolveTextStrokeWidth(style, fontSize);
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
fontSize,
|
|
856
|
-
fontFamily,
|
|
857
|
-
fontWeight,
|
|
858
|
-
fontStyle,
|
|
859
|
-
ctx,
|
|
860
|
-
ls ?? letterSpacing
|
|
861
|
-
) : (word, ls) => measureWord(
|
|
862
|
-
word,
|
|
863
|
-
fontSize,
|
|
864
|
-
fontFamily,
|
|
865
|
-
fontWeight,
|
|
866
|
-
fontStyle,
|
|
867
|
-
ctx,
|
|
868
|
-
ls ?? letterSpacing
|
|
869
|
-
);
|
|
870
|
-
const processedText = applyTextTransform(text, style);
|
|
871
|
-
const noWrap = whiteSpace === "nowrap" || whiteSpace === "pre";
|
|
872
|
-
const paragraphs = processedText.split("\n");
|
|
873
|
-
const lines = [];
|
|
874
|
-
for (const paragraph of paragraphs) {
|
|
875
|
-
if (noWrap) {
|
|
876
|
-
lines.push(paragraph);
|
|
877
|
-
continue;
|
|
878
|
-
}
|
|
879
|
-
const wrapped = wrapText(
|
|
880
|
-
paragraph,
|
|
881
|
-
maxWidth,
|
|
882
|
-
fontSize,
|
|
883
|
-
fontFamily,
|
|
884
|
-
fontWeight,
|
|
885
|
-
fontStyle,
|
|
886
|
-
letterSpacing,
|
|
887
|
-
wordBreak,
|
|
888
|
-
ctx,
|
|
889
|
-
measure
|
|
890
|
-
);
|
|
891
|
-
lines.push(...wrapped);
|
|
892
|
-
}
|
|
893
|
-
if (textOverflow === "ellipsis" && noWrap && lines.length === 1) {
|
|
894
|
-
const line = lines[0];
|
|
895
|
-
const lineWidth = measure(line);
|
|
896
|
-
if (lineWidth > maxWidth) {
|
|
897
|
-
lines[0] = truncateWithEllipsis(
|
|
898
|
-
line,
|
|
899
|
-
maxWidth,
|
|
900
|
-
fontSize,
|
|
901
|
-
fontFamily,
|
|
902
|
-
fontWeight,
|
|
903
|
-
fontStyle,
|
|
904
|
-
ctx,
|
|
905
|
-
letterSpacing
|
|
906
|
-
);
|
|
907
|
-
}
|
|
717
|
+
for (const seg of result.segments) {
|
|
718
|
+
seg.textStrokeWidth = textStrokeWidth;
|
|
719
|
+
seg.textStrokeColor = style.WebkitTextStrokeColor;
|
|
908
720
|
}
|
|
909
|
-
|
|
910
|
-
if (lineClamp && lineClamp > 0 && lines.length > lineClamp) {
|
|
911
|
-
const lastLineText = lines.slice(lineClamp - 1).join(" ");
|
|
912
|
-
lines.length = lineClamp;
|
|
913
|
-
lines[lineClamp - 1] = truncateWithEllipsis(
|
|
914
|
-
lastLineText,
|
|
915
|
-
maxWidth,
|
|
916
|
-
fontSize,
|
|
917
|
-
fontFamily,
|
|
918
|
-
fontWeight,
|
|
919
|
-
fontStyle,
|
|
920
|
-
ctx,
|
|
921
|
-
letterSpacing
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
const segments = [];
|
|
925
|
-
let totalHeight = 0;
|
|
926
|
-
let maxLineWidth = 0;
|
|
927
|
-
const isAutoLineHeight = style.lineHeight === void 0 || style.lineHeight === "normal";
|
|
928
|
-
const fontPx = fontMetrics ? fontMetricsToPx(fontMetrics, fontSize) : null;
|
|
929
|
-
let lastPositioningDescent;
|
|
930
|
-
for (let i = 0; i < lines.length; i++) {
|
|
931
|
-
const line = lines[i];
|
|
932
|
-
const lineWidth = measure(line);
|
|
933
|
-
const slack = Math.max(0, maxWidth - lineWidth);
|
|
934
|
-
let x = 0;
|
|
935
|
-
if (textAlign === "center") {
|
|
936
|
-
x = slack / 2;
|
|
937
|
-
} else if (textAlign === "right") {
|
|
938
|
-
x = slack;
|
|
939
|
-
}
|
|
940
|
-
const metrics = measureText(
|
|
941
|
-
line || "M",
|
|
942
|
-
fontSize,
|
|
943
|
-
fontFamily,
|
|
944
|
-
fontWeight,
|
|
945
|
-
fontStyle,
|
|
946
|
-
ctx
|
|
947
|
-
);
|
|
948
|
-
const positioningAscent = fontPx?.ascent ?? metrics.ascent;
|
|
949
|
-
const positioningDescent = fontPx?.descent ?? metrics.descent;
|
|
950
|
-
lastPositioningDescent = positioningDescent;
|
|
951
|
-
const contentHeight = positioningAscent + positioningDescent;
|
|
952
|
-
let effectiveAscent = positioningAscent;
|
|
953
|
-
let effectiveDescent = positioningDescent;
|
|
954
|
-
if (isAutoLineHeight && contentHeight > lineHeightPx) {
|
|
955
|
-
const scale = lineHeightPx / contentHeight;
|
|
956
|
-
effectiveAscent = positioningAscent * scale;
|
|
957
|
-
effectiveDescent = positioningDescent * scale;
|
|
958
|
-
}
|
|
959
|
-
const baselineY = totalHeight + (lineHeightPx + effectiveAscent - effectiveDescent) / 2;
|
|
960
|
-
segments.push({
|
|
961
|
-
text: line,
|
|
962
|
-
x,
|
|
963
|
-
y: baselineY,
|
|
964
|
-
width: lineWidth,
|
|
965
|
-
height: lineHeightPx,
|
|
966
|
-
fontSize,
|
|
967
|
-
fontFamily,
|
|
968
|
-
fontWeight,
|
|
969
|
-
fontStyle,
|
|
970
|
-
color,
|
|
971
|
-
ascent: metrics.ascent,
|
|
972
|
-
textDecoration,
|
|
973
|
-
letterSpacing,
|
|
974
|
-
lineIndex: i,
|
|
975
|
-
textStrokeWidth,
|
|
976
|
-
textStrokeColor
|
|
977
|
-
});
|
|
978
|
-
totalHeight += lineHeightPx;
|
|
979
|
-
maxLineWidth = Math.max(maxLineWidth, lineWidth);
|
|
980
|
-
}
|
|
981
|
-
if (isAutoLineHeight && lastPositioningDescent !== void 0 && segments.length > 0) {
|
|
982
|
-
const lastSeg = segments[segments.length - 1];
|
|
983
|
-
const descentOverflow = lastSeg.y + lastPositioningDescent - totalHeight;
|
|
984
|
-
if (descentOverflow > 0) {
|
|
985
|
-
totalHeight += descentOverflow;
|
|
986
|
-
}
|
|
987
|
-
totalHeight = Math.ceil(totalHeight);
|
|
988
|
-
}
|
|
989
|
-
const textBoxTrim = style.textBoxTrim;
|
|
990
|
-
if (textBoxTrim && textBoxTrim !== "none" && segments.length > 0) {
|
|
991
|
-
const textBoxEdge = style.textBoxEdge ?? "text";
|
|
992
|
-
const trimMetrics = measureTrimMetrics(
|
|
993
|
-
fontSize,
|
|
994
|
-
fontFamily,
|
|
995
|
-
fontWeight,
|
|
996
|
-
fontStyle,
|
|
997
|
-
lineHeightPx,
|
|
998
|
-
textBoxEdge,
|
|
999
|
-
ctx,
|
|
1000
|
-
fontMetrics
|
|
1001
|
-
);
|
|
1002
|
-
if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
|
|
1003
|
-
for (const seg of segments) {
|
|
1004
|
-
seg.y -= trimMetrics.overTrim;
|
|
1005
|
-
}
|
|
1006
|
-
totalHeight -= trimMetrics.overTrim;
|
|
1007
|
-
}
|
|
1008
|
-
if (textBoxTrim === "trim-end" || textBoxTrim === "trim-both") {
|
|
1009
|
-
totalHeight -= trimMetrics.underTrim;
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
return {
|
|
1013
|
-
segments,
|
|
1014
|
-
width: maxLineWidth,
|
|
1015
|
-
height: totalHeight
|
|
1016
|
-
};
|
|
721
|
+
return result;
|
|
1017
722
|
}
|
|
1018
723
|
function resolveTextStrokeWidth(style, fontSize) {
|
|
1019
724
|
const raw = style.WebkitTextStrokeWidth;
|
|
@@ -1030,157 +735,232 @@ function applyTextTransform(text, style) {
|
|
|
1030
735
|
}
|
|
1031
736
|
return text;
|
|
1032
737
|
}
|
|
1033
|
-
function resolveLineHeight(lineHeight, fontSize
|
|
1034
|
-
if (lineHeight === void 0 || lineHeight === "normal")
|
|
1035
|
-
|
|
1036
|
-
const px = fontMetricsToPx(fontMetrics, fontSize);
|
|
1037
|
-
return px.ascent + px.descent;
|
|
1038
|
-
}
|
|
1039
|
-
return canvasMetrics ? canvasMetrics.ascent + canvasMetrics.descent : fontSize * 1.2;
|
|
1040
|
-
}
|
|
738
|
+
function resolveLineHeight(lineHeight, fontSize) {
|
|
739
|
+
if (lineHeight === void 0 || lineHeight === "normal") return void 0;
|
|
740
|
+
let px;
|
|
1041
741
|
if (typeof lineHeight === "number") {
|
|
1042
|
-
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
text.length,
|
|
1102
|
-
maxWidth,
|
|
1103
|
-
fontSize,
|
|
1104
|
-
fontFamily,
|
|
1105
|
-
fontWeight,
|
|
1106
|
-
fontStyle,
|
|
1107
|
-
ctx,
|
|
1108
|
-
letterSpacing,
|
|
1109
|
-
measureFn
|
|
1110
|
-
);
|
|
1111
|
-
lines.push(...broken.lines);
|
|
742
|
+
px = lineHeight > 5 ? lineHeight : lineHeight * fontSize;
|
|
743
|
+
} else {
|
|
744
|
+
px = parseFloat(lineHeight);
|
|
745
|
+
}
|
|
746
|
+
return Number.isFinite(px) && px >= 0 ? px : void 0;
|
|
747
|
+
}
|
|
748
|
+
function toLayoutGrid(width) {
|
|
749
|
+
return Math.ceil(width * 64) / 64;
|
|
750
|
+
}
|
|
751
|
+
var graphemeSegmenter2 = new Intl.Segmenter(void 0, {
|
|
752
|
+
granularity: "grapheme"
|
|
753
|
+
});
|
|
754
|
+
var TextMeasure = class {
|
|
755
|
+
constructor(text, style, emojiEnabled) {
|
|
756
|
+
this.text = text;
|
|
757
|
+
this.style = style;
|
|
758
|
+
this.emojiEnabled = emojiEnabled;
|
|
759
|
+
}
|
|
760
|
+
text;
|
|
761
|
+
style;
|
|
762
|
+
emojiEnabled;
|
|
763
|
+
/**
|
|
764
|
+
* Yoga's measurements by width (Infinity for unbounded), and those at
|
|
765
|
+
* exactly a width.
|
|
766
|
+
*/
|
|
767
|
+
sizes = /* @__PURE__ */ new Map();
|
|
768
|
+
exactSizes = /* @__PURE__ */ new Map();
|
|
769
|
+
/**
|
|
770
|
+
* The heights reported to Yoga since the node was last marked dirty (see
|
|
771
|
+
* `settle`): Yoga may size the node, and its ancestors, from any of them,
|
|
772
|
+
* cached, without measuring it again.
|
|
773
|
+
*/
|
|
774
|
+
reported = /* @__PURE__ */ new Set();
|
|
775
|
+
pinnedHeight;
|
|
776
|
+
settled;
|
|
777
|
+
/**
|
|
778
|
+
* The text's intrinsic widths, from whichever layout came first: its
|
|
779
|
+
* max-content width, and a bound on its min-content width, which is the
|
|
780
|
+
* min-content width but where finding it takes more (see `widestGrapheme`).
|
|
781
|
+
*/
|
|
782
|
+
intrinsic;
|
|
783
|
+
/**
|
|
784
|
+
* Measure the text for Yoga.
|
|
785
|
+
*
|
|
786
|
+
* @param maxWidth - The width available, Infinity for unbounded
|
|
787
|
+
* @param exact - Whether the node is exactly `maxWidth` wide, whatever its
|
|
788
|
+
* text: it's then measured for its height there
|
|
789
|
+
*/
|
|
790
|
+
measure(maxWidth, exact = false) {
|
|
791
|
+
const sizes = exact ? this.exactSizes : this.sizes;
|
|
792
|
+
let size = sizes.get(maxWidth);
|
|
793
|
+
if (!size) {
|
|
794
|
+
let result = this.layOut(maxWidth);
|
|
795
|
+
let width;
|
|
796
|
+
if (exact) {
|
|
797
|
+
width = maxWidth;
|
|
798
|
+
} else if (result.maxContentWidth > maxWidth) {
|
|
799
|
+
width = this.minContentBound <= maxWidth ? maxWidth : Math.max(maxWidth, this.minContentWidth);
|
|
800
|
+
if (width > maxWidth) result = this.layOut(width);
|
|
1112
801
|
} else {
|
|
1113
|
-
|
|
802
|
+
width = Math.min(
|
|
803
|
+
toLayoutGrid(Math.min(result.width, result.maxContentWidth)),
|
|
804
|
+
maxWidth
|
|
805
|
+
);
|
|
1114
806
|
}
|
|
807
|
+
size = { width, height: result.height };
|
|
808
|
+
sizes.set(maxWidth, size);
|
|
1115
809
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
let pos = start;
|
|
1131
|
-
while (pos < end) {
|
|
1132
|
-
let breakPos = pos + 1;
|
|
1133
|
-
while (breakPos < end) {
|
|
1134
|
-
const chunk = text.slice(pos, breakPos + 1);
|
|
1135
|
-
const w = mw(chunk);
|
|
1136
|
-
if (w > maxWidth) break;
|
|
1137
|
-
breakPos++;
|
|
810
|
+
const height = this.pinnedHeight ?? size.height;
|
|
811
|
+
this.reported.add(height);
|
|
812
|
+
return { width: size.width, height };
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* The height of the text's lines at exactly `width`, as drawn there,
|
|
816
|
+
* whatever height later measurements are pinned to.
|
|
817
|
+
*/
|
|
818
|
+
heightAt(width) {
|
|
819
|
+
if (this.settled?.width === width) return this.settled.result.height;
|
|
820
|
+
let size = this.exactSizes.get(width);
|
|
821
|
+
if (!size) {
|
|
822
|
+
size = { width, height: this.layOut(width).height };
|
|
823
|
+
this.exactSizes.set(width, size);
|
|
1138
824
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
825
|
+
return size.height;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* The text's min-content width: as narrow as it gets, each run of it that
|
|
829
|
+
* can't break on a line of its own. As CSS has it, that's the widest word,
|
|
830
|
+
* or the widest line for text that doesn't wrap, and under
|
|
831
|
+
* `overflow-wrap: anywhere` (or `word-break: break-word`) and
|
|
832
|
+
* `word-break: break-all` the widest letter. Text truncated with an
|
|
833
|
+
* ellipsis (`text-overflow: ellipsis` without wrapping, or a line clamp)
|
|
834
|
+
* has none: it's drawn at whatever width it has, which CSS gives it in
|
|
835
|
+
* the `overflow: hidden` box an ellipsis needs.
|
|
836
|
+
*
|
|
837
|
+
* Never wider than the text's widest line, and on the layout grid (see
|
|
838
|
+
* `toLayoutGrid`), as the widths `measure` reports are.
|
|
839
|
+
*/
|
|
840
|
+
get minContentWidth() {
|
|
841
|
+
const intrinsic = this.intrinsic ?? this.measureIntrinsic();
|
|
842
|
+
intrinsic.min ??= Math.min(
|
|
843
|
+
toLayoutGrid(this.widestGrapheme()),
|
|
844
|
+
intrinsic.minBound
|
|
845
|
+
);
|
|
846
|
+
return intrinsic.min;
|
|
847
|
+
}
|
|
848
|
+
/** At least the text's min-content width, and found without more work. */
|
|
849
|
+
get minContentBound() {
|
|
850
|
+
return (this.intrinsic ?? this.measureIntrinsic()).minBound;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* The text's max-content width: its widest line, were no line to wrap, on
|
|
854
|
+
* the layout grid.
|
|
855
|
+
*/
|
|
856
|
+
get maxContentWidth() {
|
|
857
|
+
return (this.intrinsic ?? this.measureIntrinsic()).max;
|
|
858
|
+
}
|
|
859
|
+
/** Whether the text can't be narrower than its widest line. */
|
|
860
|
+
get unbreakable() {
|
|
861
|
+
return this.minContentBound >= this.maxContentWidth;
|
|
862
|
+
}
|
|
863
|
+
measureIntrinsic() {
|
|
864
|
+
this.layOut(Infinity);
|
|
865
|
+
return this.intrinsic;
|
|
866
|
+
}
|
|
867
|
+
layOut(width) {
|
|
868
|
+
const result = layoutText(this.text, this.style, width, this.emojiEnabled);
|
|
869
|
+
this.intrinsic ??= this.intrinsicOf(result);
|
|
870
|
+
return result;
|
|
871
|
+
}
|
|
872
|
+
/** The intrinsic widths of the text, from a layout of it at any width. */
|
|
873
|
+
intrinsicOf(result) {
|
|
874
|
+
const { style } = this;
|
|
875
|
+
const max = toLayoutGrid(result.maxContentWidth);
|
|
876
|
+
const noWrap = style.whiteSpace === "nowrap" || style.whiteSpace === "pre";
|
|
877
|
+
if (style.lineClamp !== void 0 && style.lineClamp >= 1 || noWrap && style.textOverflow === "ellipsis") {
|
|
878
|
+
return { max, minBound: 0, min: 0 };
|
|
879
|
+
}
|
|
880
|
+
const minBound = Math.min(
|
|
881
|
+
toLayoutGrid(
|
|
882
|
+
noWrap ? Math.min(result.minContentWidth, result.width) : result.minContentWidth
|
|
883
|
+
),
|
|
884
|
+
max
|
|
885
|
+
);
|
|
886
|
+
const anywhere = !noWrap && (style.overflowWrap === "anywhere" || style.wordBreak === "break-word");
|
|
887
|
+
return { max, minBound, min: anywhere ? void 0 : minBound };
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* The width of the text's widest grapheme cluster: its min-content where
|
|
891
|
+
* a line may break between any two. Each one is laid out once, as a word
|
|
892
|
+
* of its own.
|
|
893
|
+
*/
|
|
894
|
+
widestGrapheme() {
|
|
895
|
+
const text = applyTextTransform(
|
|
896
|
+
collapseWhiteSpace(this.text, this.style.whiteSpace),
|
|
897
|
+
this.style
|
|
898
|
+
);
|
|
899
|
+
const graphemes = new Set(
|
|
900
|
+
/^[\s!-\u02ff]*$/.test(text) ? text : Array.from(graphemeSegmenter2.segment(text), ({ segment }) => segment)
|
|
1167
901
|
);
|
|
1168
|
-
|
|
1169
|
-
|
|
902
|
+
for (const grapheme of graphemes) {
|
|
903
|
+
if (/^\s+$/.test(grapheme)) graphemes.delete(grapheme);
|
|
1170
904
|
}
|
|
905
|
+
if (graphemes.size === 0) return 0;
|
|
906
|
+
return layoutText(
|
|
907
|
+
[...graphemes].join(" "),
|
|
908
|
+
{
|
|
909
|
+
...this.style,
|
|
910
|
+
whiteSpace: "normal",
|
|
911
|
+
wordBreak: "normal",
|
|
912
|
+
overflowWrap: "normal",
|
|
913
|
+
textTransform: "none"
|
|
914
|
+
},
|
|
915
|
+
Infinity,
|
|
916
|
+
this.emojiEnabled
|
|
917
|
+
).minContentWidth;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Lay the text out for drawing at the node's final content width, and
|
|
921
|
+
* check it against the heights Yoga was given since the node was last
|
|
922
|
+
* marked dirty: Yoga reuses a node's measurements until then, also when
|
|
923
|
+
* the node ends up at another width, so the heights of earlier layouts
|
|
924
|
+
* count too, not just those of the last.
|
|
925
|
+
*
|
|
926
|
+
* @param pin - Whether to pin later measurements to the drawn height when
|
|
927
|
+
* they disagree
|
|
928
|
+
* @returns Whether Yoga sized the node from another height. If `pin`, the
|
|
929
|
+
* node's measurements are now pinned to the drawn height, and the caller
|
|
930
|
+
* must mark the node dirty and compute the layout again.
|
|
931
|
+
*/
|
|
932
|
+
settle(width, pin) {
|
|
933
|
+
if (this.settled?.width !== width) {
|
|
934
|
+
this.settled = {
|
|
935
|
+
width,
|
|
936
|
+
result: layoutText(this.text, this.style, width, this.emojiEnabled)
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const { height } = this.settled.result;
|
|
940
|
+
const agrees = this.reported.size === 0 || this.reported.size === 1 && this.reported.has(height);
|
|
941
|
+
if (agrees) return false;
|
|
942
|
+
if (pin) {
|
|
943
|
+
this.pinnedHeight = height;
|
|
944
|
+
this.reported.clear();
|
|
945
|
+
}
|
|
946
|
+
return true;
|
|
1171
947
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
return
|
|
1182
|
-
}
|
|
1183
|
-
|
|
948
|
+
/**
|
|
949
|
+
* Drop the pinned height, so that Yoga sees the measured heights again.
|
|
950
|
+
*
|
|
951
|
+
* @returns Whether there was a pin, for the caller to mark the node dirty
|
|
952
|
+
*/
|
|
953
|
+
unpin() {
|
|
954
|
+
const pinned = this.pinnedHeight !== void 0;
|
|
955
|
+
this.pinnedHeight = void 0;
|
|
956
|
+
if (pinned) this.reported.clear();
|
|
957
|
+
return pinned;
|
|
958
|
+
}
|
|
959
|
+
/** The text as laid out by the last `settle`. */
|
|
960
|
+
get layout() {
|
|
961
|
+
return this.settled?.result;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
1184
964
|
|
|
1185
965
|
// src/jsx/draw/clip.ts
|
|
1186
966
|
function applyClip(ctx, x, y, width, height, borderRadius) {
|
|
@@ -1433,33 +1213,113 @@ function drawBorders(ctx, x, y, width, height, style, borderRadius) {
|
|
|
1433
1213
|
}
|
|
1434
1214
|
}
|
|
1435
1215
|
}
|
|
1436
|
-
var
|
|
1437
|
-
function drawBoxShadow(ctx, x, y, width, height,
|
|
1438
|
-
const
|
|
1439
|
-
if (
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1216
|
+
var NO_COLOR = "rgba(0, 0, 0, 0)";
|
|
1217
|
+
function drawBoxShadow(ctx, x, y, width, height, shadows, borderRadius) {
|
|
1218
|
+
const outer = shadows.filter((s) => !s.inset);
|
|
1219
|
+
if (outer.length === 0) return;
|
|
1220
|
+
const radii = clampRadii(borderRadius, width, height);
|
|
1221
|
+
let margin = 0;
|
|
1222
|
+
for (const s of outer) {
|
|
1223
|
+
margin = Math.max(margin, reach(s));
|
|
1224
|
+
}
|
|
1225
|
+
ctx.save();
|
|
1226
|
+
ctx.beginPath();
|
|
1227
|
+
ctx.rect(x - margin, y - margin, width + margin * 2, height + margin * 2);
|
|
1228
|
+
roundedRect(ctx, x, y, width, height, ...radii);
|
|
1229
|
+
ctx.clip("evenodd");
|
|
1230
|
+
for (let i = outer.length - 1; i >= 0; i--) {
|
|
1231
|
+
const s = outer[i];
|
|
1232
|
+
const w = width + s.spread * 2;
|
|
1233
|
+
const h = height + s.spread * 2;
|
|
1234
|
+
if (w <= 0 || h <= 0) continue;
|
|
1235
|
+
ctx.save();
|
|
1236
|
+
setShadowPaint(ctx, s);
|
|
1237
|
+
ctx.beginPath();
|
|
1238
|
+
roundedRect(
|
|
1239
|
+
ctx,
|
|
1240
|
+
x - s.spread + s.offsetX,
|
|
1241
|
+
y - s.spread + s.offsetY,
|
|
1242
|
+
w,
|
|
1243
|
+
h,
|
|
1244
|
+
...spreadRadii(radii, s.spread)
|
|
1245
|
+
);
|
|
1246
|
+
ctx.fill();
|
|
1247
|
+
ctx.restore();
|
|
1248
|
+
}
|
|
1249
|
+
ctx.restore();
|
|
1250
|
+
}
|
|
1251
|
+
function drawInsetBoxShadow(ctx, x, y, width, height, shadows, borderRadius, style) {
|
|
1252
|
+
const inset = shadows.filter((s) => s.inset);
|
|
1253
|
+
if (inset.length === 0) return;
|
|
1254
|
+
const bt = resolveBoxValue(style.borderTopWidth, width);
|
|
1255
|
+
const br = resolveBoxValue(style.borderRightWidth, width);
|
|
1256
|
+
const bb = resolveBoxValue(style.borderBottomWidth, width);
|
|
1257
|
+
const bl = resolveBoxValue(style.borderLeftWidth, width);
|
|
1258
|
+
const px = x + bl;
|
|
1259
|
+
const py = y + bt;
|
|
1260
|
+
const pw = width - bl - br;
|
|
1261
|
+
const ph = height - bt - bb;
|
|
1262
|
+
if (pw <= 0 || ph <= 0) return;
|
|
1263
|
+
const [tl, tr, brr, bll] = clampRadii(borderRadius, width, height);
|
|
1264
|
+
const inner = [
|
|
1265
|
+
Math.max(0, tl - Math.max(bt, bl)),
|
|
1266
|
+
Math.max(0, tr - Math.max(bt, br)),
|
|
1267
|
+
Math.max(0, brr - Math.max(bb, br)),
|
|
1268
|
+
Math.max(0, bll - Math.max(bb, bl))
|
|
1269
|
+
];
|
|
1270
|
+
ctx.save();
|
|
1271
|
+
ctx.beginPath();
|
|
1272
|
+
roundedRect(ctx, px, py, pw, ph, ...inner);
|
|
1273
|
+
ctx.clip();
|
|
1274
|
+
for (let i = inset.length - 1; i >= 0; i--) {
|
|
1275
|
+
const s = inset[i];
|
|
1276
|
+
const m = reach(s);
|
|
1277
|
+
ctx.save();
|
|
1278
|
+
setShadowPaint(ctx, s);
|
|
1279
|
+
ctx.beginPath();
|
|
1280
|
+
ctx.rect(px - m, py - m, pw + m * 2, ph + m * 2);
|
|
1281
|
+
const w = pw - s.spread * 2;
|
|
1282
|
+
const h = ph - s.spread * 2;
|
|
1283
|
+
if (w > 0 && h > 0) {
|
|
1284
|
+
roundedRect(
|
|
1285
|
+
ctx,
|
|
1286
|
+
px + s.spread + s.offsetX,
|
|
1287
|
+
py + s.spread + s.offsetY,
|
|
1288
|
+
w,
|
|
1289
|
+
h,
|
|
1290
|
+
...spreadRadii(inner, -s.spread)
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
ctx.fill("evenodd");
|
|
1294
|
+
ctx.restore();
|
|
1295
|
+
}
|
|
1296
|
+
ctx.restore();
|
|
1297
|
+
}
|
|
1298
|
+
function reach(s) {
|
|
1299
|
+
return s.blur * 2 + Math.abs(s.offsetX) + Math.abs(s.offsetY) + Math.abs(s.spread) + 1;
|
|
1300
|
+
}
|
|
1301
|
+
function setShadowPaint(ctx, s) {
|
|
1302
|
+
ctx.fillStyle = NO_COLOR;
|
|
1303
|
+
ctx.fillStyle = s.color;
|
|
1304
|
+
if (s.blur > 0) ctx.filter = `blur(${s.blur / 2}px)`;
|
|
1305
|
+
}
|
|
1306
|
+
function clampRadii(r, width, height) {
|
|
1307
|
+
const max = maxCornerRadius(width, height);
|
|
1308
|
+
return [
|
|
1309
|
+
Math.min(r.topLeft, max),
|
|
1310
|
+
Math.min(r.topRight, max),
|
|
1311
|
+
Math.min(r.bottomRight, max),
|
|
1312
|
+
Math.min(r.bottomLeft, max)
|
|
1449
1313
|
];
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
ctx.beginPath();
|
|
1460
|
-
ctx.roundRect(x, y, width, height, radii);
|
|
1461
|
-
ctx.fill();
|
|
1462
|
-
ctx.restore();
|
|
1314
|
+
}
|
|
1315
|
+
function spreadRadii(radii, spread) {
|
|
1316
|
+
return radii.map((r) => spreadRadius(r, spread));
|
|
1317
|
+
}
|
|
1318
|
+
function spreadRadius(r, spread) {
|
|
1319
|
+
if (spread <= 0) return Math.max(0, r + spread);
|
|
1320
|
+
if (r >= spread) return r + spread;
|
|
1321
|
+
const ratio = r / spread;
|
|
1322
|
+
return r + spread * (1 + (ratio - 1) ** 3);
|
|
1463
1323
|
}
|
|
1464
1324
|
|
|
1465
1325
|
// src/jsx/draw/clip-path.ts
|
|
@@ -2377,6 +2237,80 @@ async function drawImage(ctx, src, x, y, width, height, context, style) {
|
|
|
2377
2237
|
}
|
|
2378
2238
|
}
|
|
2379
2239
|
|
|
2240
|
+
// src/jsx/draw/shadow.ts
|
|
2241
|
+
var BOX_SHADOW = { maxLengths: 4, inset: true };
|
|
2242
|
+
var TEXT_SHADOW = { maxLengths: 3, inset: false };
|
|
2243
|
+
function parseBoxShadow(value, currentColor) {
|
|
2244
|
+
return parseShadowList(value, currentColor, BOX_SHADOW);
|
|
2245
|
+
}
|
|
2246
|
+
function parseTextShadow(value, currentColor) {
|
|
2247
|
+
return parseShadowList(value, currentColor, TEXT_SHADOW);
|
|
2248
|
+
}
|
|
2249
|
+
function parseShadowList(value, currentColor, syntax) {
|
|
2250
|
+
const text = value.trim();
|
|
2251
|
+
if (text === "" || text.toLowerCase() === "none") return [];
|
|
2252
|
+
const shadows = [];
|
|
2253
|
+
for (const item of splitTopLevel(text, ",")) {
|
|
2254
|
+
const shadow = parseShadow(item, currentColor, syntax);
|
|
2255
|
+
if (!shadow) return [];
|
|
2256
|
+
shadows.push(shadow);
|
|
2257
|
+
}
|
|
2258
|
+
return shadows;
|
|
2259
|
+
}
|
|
2260
|
+
var LENGTH = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(px)?$/i;
|
|
2261
|
+
var NUMERIC = /^[+-]?\.?\d/;
|
|
2262
|
+
function parseShadow(item, currentColor, syntax) {
|
|
2263
|
+
const tokens = splitTopLevel(item, " ");
|
|
2264
|
+
const lengths = [];
|
|
2265
|
+
let lengthsEnded = false;
|
|
2266
|
+
let inset = false;
|
|
2267
|
+
let color;
|
|
2268
|
+
for (const token of tokens) {
|
|
2269
|
+
if (NUMERIC.test(token)) {
|
|
2270
|
+
const match = LENGTH.exec(token);
|
|
2271
|
+
if (!match || lengthsEnded) return null;
|
|
2272
|
+
lengths.push(parseFloat(match[1]));
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
2275
|
+
if (lengths.length > 0) lengthsEnded = true;
|
|
2276
|
+
if (token.toLowerCase() === "inset") {
|
|
2277
|
+
if (!syntax.inset || inset) return null;
|
|
2278
|
+
inset = true;
|
|
2279
|
+
} else {
|
|
2280
|
+
if (color !== void 0) return null;
|
|
2281
|
+
color = token;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
if (lengths.length < 2 || lengths.length > syntax.maxLengths) return null;
|
|
2285
|
+
const [offsetX, offsetY, blur = 0, spread = 0] = lengths;
|
|
2286
|
+
if (blur < 0) return null;
|
|
2287
|
+
if (color === void 0 || color.toLowerCase() === "currentcolor") {
|
|
2288
|
+
color = currentColor;
|
|
2289
|
+
}
|
|
2290
|
+
return { offsetX, offsetY, blur, spread, inset, color };
|
|
2291
|
+
}
|
|
2292
|
+
function splitTopLevel(text, separator) {
|
|
2293
|
+
const parts = [];
|
|
2294
|
+
let depth = 0;
|
|
2295
|
+
let current = "";
|
|
2296
|
+
for (const ch of text) {
|
|
2297
|
+
if (ch === "(") depth++;
|
|
2298
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
2299
|
+
const splits = depth === 0 && (separator === "," ? ch === "," : /\s/.test(ch));
|
|
2300
|
+
if (splits) {
|
|
2301
|
+
parts.push(current);
|
|
2302
|
+
current = "";
|
|
2303
|
+
} else {
|
|
2304
|
+
current += ch;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
parts.push(current);
|
|
2308
|
+
if (separator === ",") {
|
|
2309
|
+
return parts.map((p) => p.trim());
|
|
2310
|
+
}
|
|
2311
|
+
return parts.filter((p) => p !== "");
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2380
2314
|
// src/jsx/draw/svg/style.ts
|
|
2381
2315
|
function resolveCurrentColor(value, color) {
|
|
2382
2316
|
if (value?.toLowerCase() === "currentcolor") return color;
|
|
@@ -3408,106 +3342,70 @@ import { fillParagraph, strokeParagraph } from "@effing/skia/extensions";
|
|
|
3408
3342
|
import parseCssColor2 from "parse-css-color";
|
|
3409
3343
|
|
|
3410
3344
|
// src/jsx/emoji.ts
|
|
3411
|
-
var
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
var
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
c = unicodeSurrogates.charCodeAt(i++);
|
|
3429
|
-
if (p) {
|
|
3430
|
-
r.push((65536 + (p - 55296 << 10) + (c - 56320)).toString(16));
|
|
3431
|
-
p = 0;
|
|
3432
|
-
} else if (55296 <= c && c <= 56319) {
|
|
3433
|
-
p = c;
|
|
3434
|
-
} else {
|
|
3435
|
-
r.push(c.toString(16));
|
|
3345
|
+
var FE0F = 65039;
|
|
3346
|
+
var ZWJ = 8205;
|
|
3347
|
+
var KEYCAP = 8419;
|
|
3348
|
+
var EMOJI_CHAR2 = /[\p{Emoji}\p{Extended_Pictographic}]/u;
|
|
3349
|
+
var EMOJI_PRESENTATION = new RegExp(
|
|
3350
|
+
String.raw`\p{Emoji_Presentation}|${UNASSIGNED_PICTOGRAPHIC}`,
|
|
3351
|
+
"u"
|
|
3352
|
+
);
|
|
3353
|
+
var EMOJI_MODIFIER = /\p{Emoji_Modifier}/u;
|
|
3354
|
+
function qualifiedCodePoints(emoji) {
|
|
3355
|
+
const chars = [...emoji].filter((c) => c.codePointAt(0) !== FE0F);
|
|
3356
|
+
const codePoints = [];
|
|
3357
|
+
chars.forEach((char, i) => {
|
|
3358
|
+
codePoints.push(char.codePointAt(0));
|
|
3359
|
+
const startsElement = i === 0 || chars[i - 1] === "\u200D";
|
|
3360
|
+
if (startsElement && EMOJI_CHAR2.test(char) && !EMOJI_PRESENTATION.test(char) && !EMOJI_MODIFIER.test(chars[i + 1] ?? "")) {
|
|
3361
|
+
codePoints.push(FE0F);
|
|
3436
3362
|
}
|
|
3437
|
-
}
|
|
3438
|
-
return
|
|
3363
|
+
});
|
|
3364
|
+
return codePoints;
|
|
3439
3365
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
const key = type + ":" + code;
|
|
3443
|
-
if (key in emojiCache) return emojiCache[key];
|
|
3444
|
-
const api = emojiApis[type];
|
|
3445
|
-
const url = typeof api === "function" ? api(code) : `${api}${code.toUpperCase()}.svg`;
|
|
3446
|
-
return emojiCache[key] = fetch(url).then((r) => r.text());
|
|
3366
|
+
function hex(codePoints, pad = 0) {
|
|
3367
|
+
return codePoints.map((cp) => cp.toString(16).padStart(pad, "0")).join("-");
|
|
3447
3368
|
}
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
x: currentX,
|
|
3475
|
-
width: emojiSize
|
|
3476
|
-
});
|
|
3477
|
-
currentX += emojiSize + letterSpacing;
|
|
3478
|
-
textStartX = currentX;
|
|
3479
|
-
} else {
|
|
3480
|
-
if (!currentText) textStartX = currentX;
|
|
3481
|
-
currentText += segment;
|
|
3482
|
-
currentGraphemeCount++;
|
|
3483
|
-
}
|
|
3484
|
-
}
|
|
3485
|
-
if (currentText) {
|
|
3486
|
-
const textWidth = measureText2(currentText);
|
|
3487
|
-
runs.push({
|
|
3488
|
-
kind: "text",
|
|
3489
|
-
text: currentText,
|
|
3490
|
-
x: textStartX,
|
|
3491
|
-
width: textWidth + letterSpacing * currentGraphemeCount
|
|
3492
|
-
});
|
|
3493
|
-
}
|
|
3494
|
-
return runs;
|
|
3369
|
+
function twemojiCode(codePoints) {
|
|
3370
|
+
const code = hex(
|
|
3371
|
+
codePoints.includes(ZWJ) ? codePoints : codePoints.filter((cp) => cp !== FE0F)
|
|
3372
|
+
);
|
|
3373
|
+
return code === "1f441-fe0f-200d-1f5e8-fe0f" ? "1f441-200d-1f5e8" : code;
|
|
3374
|
+
}
|
|
3375
|
+
function svgmojiCode(codePoints) {
|
|
3376
|
+
const keepFE0F = codePoints.includes(ZWJ) || codePoints.includes(KEYCAP);
|
|
3377
|
+
return hex(
|
|
3378
|
+
keepFE0F ? codePoints : codePoints.filter((cp) => cp !== FE0F),
|
|
3379
|
+
4
|
|
3380
|
+
).toUpperCase();
|
|
3381
|
+
}
|
|
3382
|
+
function fluentCode(codePoints) {
|
|
3383
|
+
return encodeURIComponent(String.fromCodePoint(...codePoints));
|
|
3384
|
+
}
|
|
3385
|
+
var emojiUrls = {
|
|
3386
|
+
twemoji: (cps) => `https://cdnjs.cloudflare.com/ajax/libs/twemoji/16.0.1/svg/${twemojiCode(cps)}.svg`,
|
|
3387
|
+
openmoji: (cps) => `https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/${svgmojiCode(cps)}.svg`,
|
|
3388
|
+
blobmoji: (cps) => `https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/${svgmojiCode(cps)}.svg`,
|
|
3389
|
+
noto: (cps) => `https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/${svgmojiCode(cps)}.svg`,
|
|
3390
|
+
fluent: (cps) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${fluentCode(cps)}_color.svg`,
|
|
3391
|
+
fluentFlat: (cps) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${fluentCode(cps)}_flat.svg`
|
|
3392
|
+
};
|
|
3393
|
+
function emojiUrl(style, emoji) {
|
|
3394
|
+
return emojiUrls[style](qualifiedCodePoints(emoji));
|
|
3495
3395
|
}
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
return false;
|
|
3396
|
+
var emojiCache = {};
|
|
3397
|
+
async function loadEmoji(style, emoji) {
|
|
3398
|
+
const url = emojiUrl(style, emoji);
|
|
3399
|
+
return emojiCache[url] ??= fetch(url).then((r) => r.text());
|
|
3501
3400
|
}
|
|
3502
3401
|
|
|
3503
3402
|
// src/jsx/draw/text.ts
|
|
3504
3403
|
var emojiImageCache = /* @__PURE__ */ new Map();
|
|
3505
|
-
function loadEmojiImage(style,
|
|
3506
|
-
const
|
|
3507
|
-
const key = style + ":" + code;
|
|
3404
|
+
function loadEmojiImage(style, emoji) {
|
|
3405
|
+
const key = emojiUrl(style, emoji);
|
|
3508
3406
|
let cached = emojiImageCache.get(key);
|
|
3509
3407
|
if (!cached) {
|
|
3510
|
-
cached = loadEmoji(style,
|
|
3408
|
+
cached = loadEmoji(style, emoji).then((svgText) => {
|
|
3511
3409
|
if (!svgText || !svgText.includes("<svg")) return null;
|
|
3512
3410
|
const dataUri = "data:image/svg+xml;base64," + Buffer.from(svgText).toString("base64");
|
|
3513
3411
|
return loadImage2(dataUri);
|
|
@@ -3516,38 +3414,18 @@ function loadEmojiImage(style, char) {
|
|
|
3516
3414
|
}
|
|
3517
3415
|
return cached;
|
|
3518
3416
|
}
|
|
3519
|
-
async function drawText(ctx,
|
|
3520
|
-
|
|
3521
|
-
drawParagraph(
|
|
3522
|
-
ctx,
|
|
3523
|
-
native.paragraph,
|
|
3524
|
-
segments,
|
|
3525
|
-
offsetX,
|
|
3526
|
-
offsetY,
|
|
3527
|
-
native.offsetY,
|
|
3528
|
-
textShadow
|
|
3529
|
-
);
|
|
3530
|
-
return;
|
|
3531
|
-
}
|
|
3532
|
-
const textRendering = ctx.textRendering;
|
|
3533
|
-
ctx.textRendering = "geometricPrecision";
|
|
3534
|
-
try {
|
|
3535
|
-
await drawSegments(ctx, segments, offsetX, offsetY, textShadow, emojiStyle);
|
|
3536
|
-
} finally {
|
|
3537
|
-
ctx.textRendering = textRendering;
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
function drawParagraph(ctx, paragraph, segments, offsetX, offsetY, paragraphOffsetY, textShadow) {
|
|
3417
|
+
async function drawText(ctx, layout, offsetX, offsetY, textShadow, emojiStyle) {
|
|
3418
|
+
const { paragraph, segments, emoji } = layout;
|
|
3541
3419
|
const first = segments[0];
|
|
3542
3420
|
if (!first) return;
|
|
3543
3421
|
const x = offsetX;
|
|
3544
|
-
const y = offsetY + paragraphOffsetY;
|
|
3545
|
-
const
|
|
3422
|
+
const y = offsetY + layout.paragraphOffsetY;
|
|
3423
|
+
const shadows = textShadow ? parseTextShadow(textShadow, first.color) : [];
|
|
3546
3424
|
ctx.fillStyle = first.color;
|
|
3547
|
-
|
|
3425
|
+
for (let i = shadows.length - 1; i >= 0; i--) {
|
|
3548
3426
|
drawShadowPass(
|
|
3549
3427
|
ctx,
|
|
3550
|
-
|
|
3428
|
+
shadows[i],
|
|
3551
3429
|
getColorAlpha(first.color),
|
|
3552
3430
|
() => fillParagraph(ctx, paragraph, x, y)
|
|
3553
3431
|
);
|
|
@@ -3561,184 +3439,37 @@ function drawParagraph(ctx, paragraph, segments, offsetX, offsetY, paragraphOffs
|
|
|
3561
3439
|
ctx.restore();
|
|
3562
3440
|
}
|
|
3563
3441
|
fillParagraph(ctx, paragraph, x, y);
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
drawTextDecoration(ctx, seg, offsetX, offsetY);
|
|
3567
|
-
}
|
|
3442
|
+
if (emojiStyle && emoji.length > 0) {
|
|
3443
|
+
await drawEmoji(ctx, emoji, first, offsetX, offsetY, emojiStyle);
|
|
3568
3444
|
}
|
|
3569
|
-
}
|
|
3570
|
-
async function drawSegments(ctx, segments, offsetX, offsetY, textShadow, emojiStyle) {
|
|
3571
|
-
const shadow = textShadow ? parseShadow(textShadow) : null;
|
|
3572
3445
|
for (const seg of segments) {
|
|
3573
|
-
if (!seg.text) continue;
|
|
3574
|
-
setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
|
|
3575
|
-
ctx.fillStyle = seg.color;
|
|
3576
|
-
const x = offsetX + seg.x;
|
|
3577
|
-
const y = offsetY + seg.y;
|
|
3578
|
-
const textAlpha = shadow ? getColorAlpha(seg.color) : 1;
|
|
3579
|
-
const hasStroke = seg.textStrokeWidth !== void 0 && seg.textStrokeWidth > 0;
|
|
3580
|
-
if (emojiStyle) {
|
|
3581
|
-
await drawSegmentWithEmoji(
|
|
3582
|
-
ctx,
|
|
3583
|
-
seg,
|
|
3584
|
-
x,
|
|
3585
|
-
y,
|
|
3586
|
-
textShadow,
|
|
3587
|
-
emojiStyle,
|
|
3588
|
-
hasStroke
|
|
3589
|
-
);
|
|
3590
|
-
} else if (seg.letterSpacing && seg.letterSpacing !== 0) {
|
|
3591
|
-
if (shadow) {
|
|
3592
|
-
drawShadowPass(
|
|
3593
|
-
ctx,
|
|
3594
|
-
shadow,
|
|
3595
|
-
textAlpha,
|
|
3596
|
-
() => drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing)
|
|
3597
|
-
);
|
|
3598
|
-
}
|
|
3599
|
-
if (hasStroke) {
|
|
3600
|
-
drawStrokeWithLetterSpacing(
|
|
3601
|
-
ctx,
|
|
3602
|
-
seg.text,
|
|
3603
|
-
x,
|
|
3604
|
-
y,
|
|
3605
|
-
seg.letterSpacing,
|
|
3606
|
-
seg.textStrokeWidth,
|
|
3607
|
-
seg.textStrokeColor ?? seg.color
|
|
3608
|
-
);
|
|
3609
|
-
}
|
|
3610
|
-
drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing);
|
|
3611
|
-
} else {
|
|
3612
|
-
if (shadow) {
|
|
3613
|
-
drawShadowPass(
|
|
3614
|
-
ctx,
|
|
3615
|
-
shadow,
|
|
3616
|
-
textAlpha,
|
|
3617
|
-
() => ctx.fillText(seg.text, x, y)
|
|
3618
|
-
);
|
|
3619
|
-
}
|
|
3620
|
-
if (hasStroke) {
|
|
3621
|
-
ctx.save();
|
|
3622
|
-
ctx.lineWidth = seg.textStrokeWidth;
|
|
3623
|
-
ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
|
|
3624
|
-
ctx.lineJoin = "round";
|
|
3625
|
-
ctx.strokeText(seg.text, x, y);
|
|
3626
|
-
ctx.restore();
|
|
3627
|
-
}
|
|
3628
|
-
ctx.fillText(seg.text, x, y);
|
|
3629
|
-
}
|
|
3630
3446
|
if (seg.textDecoration) {
|
|
3631
3447
|
drawTextDecoration(ctx, seg, offsetX, offsetY);
|
|
3632
3448
|
}
|
|
3633
3449
|
}
|
|
3634
3450
|
}
|
|
3635
|
-
async function
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
seg.text,
|
|
3639
|
-
(text) => {
|
|
3640
|
-
setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
|
|
3641
|
-
return ctx.measureText(text).width;
|
|
3642
|
-
},
|
|
3643
|
-
seg.fontSize,
|
|
3644
|
-
letterSpacing
|
|
3451
|
+
async function drawEmoji(ctx, emoji, font, offsetX, offsetY, emojiStyle) {
|
|
3452
|
+
const images = await Promise.all(
|
|
3453
|
+
emoji.map((e) => loadEmojiImage(emojiStyle, e.grapheme))
|
|
3645
3454
|
);
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
if (letterSpacing !== 0) {
|
|
3652
|
-
drawShadowPass(
|
|
3653
|
-
ctx,
|
|
3654
|
-
shadow,
|
|
3655
|
-
textAlpha,
|
|
3656
|
-
() => drawTextWithLetterSpacing(
|
|
3657
|
-
ctx,
|
|
3658
|
-
run.text,
|
|
3659
|
-
x + run.x,
|
|
3660
|
-
y,
|
|
3661
|
-
letterSpacing
|
|
3662
|
-
)
|
|
3663
|
-
);
|
|
3664
|
-
} else {
|
|
3665
|
-
drawShadowPass(
|
|
3666
|
-
ctx,
|
|
3667
|
-
shadow,
|
|
3668
|
-
textAlpha,
|
|
3669
|
-
() => ctx.fillText(run.text, x + run.x, y)
|
|
3670
|
-
);
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3673
|
-
if (hasStroke) {
|
|
3674
|
-
if (letterSpacing !== 0) {
|
|
3675
|
-
drawStrokeWithLetterSpacing(
|
|
3676
|
-
ctx,
|
|
3677
|
-
run.text,
|
|
3678
|
-
x + run.x,
|
|
3679
|
-
y,
|
|
3680
|
-
letterSpacing,
|
|
3681
|
-
seg.textStrokeWidth,
|
|
3682
|
-
seg.textStrokeColor ?? seg.color
|
|
3683
|
-
);
|
|
3684
|
-
} else {
|
|
3685
|
-
ctx.save();
|
|
3686
|
-
ctx.lineWidth = seg.textStrokeWidth;
|
|
3687
|
-
ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
|
|
3688
|
-
ctx.lineJoin = "round";
|
|
3689
|
-
ctx.strokeText(run.text, x + run.x, y);
|
|
3690
|
-
ctx.restore();
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
if (letterSpacing !== 0) {
|
|
3694
|
-
drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
|
|
3695
|
-
} else {
|
|
3696
|
-
ctx.fillText(run.text, x + run.x, y);
|
|
3697
|
-
}
|
|
3698
|
-
} else {
|
|
3699
|
-
const img = await loadEmojiImage(emojiStyle, run.char);
|
|
3700
|
-
if (img) {
|
|
3701
|
-
const emojiSize = seg.fontSize;
|
|
3702
|
-
const emojiY = y - seg.ascent + (seg.height - seg.fontSize) / 2;
|
|
3703
|
-
ctx.drawImage(img, x + run.x, emojiY, emojiSize, emojiSize);
|
|
3704
|
-
} else {
|
|
3705
|
-
ctx.fillText(run.char, x + run.x, y);
|
|
3706
|
-
}
|
|
3455
|
+
emoji.forEach((e, i) => {
|
|
3456
|
+
const img = images[i];
|
|
3457
|
+
if (img) {
|
|
3458
|
+
ctx.drawImage(img, offsetX + e.x, offsetY + e.y, e.size, e.size);
|
|
3459
|
+
return;
|
|
3707
3460
|
}
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
ctx.lineWidth = strokeWidth;
|
|
3721
|
-
ctx.strokeStyle = strokeColor;
|
|
3722
|
-
ctx.lineJoin = "round";
|
|
3723
|
-
let currentX = x;
|
|
3724
|
-
for (const char of text) {
|
|
3725
|
-
ctx.strokeText(char, currentX, y);
|
|
3726
|
-
const metrics = ctx.measureText(char);
|
|
3727
|
-
currentX += metrics.width + letterSpacing;
|
|
3728
|
-
}
|
|
3729
|
-
ctx.restore();
|
|
3730
|
-
}
|
|
3731
|
-
function parseShadow(shadow) {
|
|
3732
|
-
const parts = shadow.match(
|
|
3733
|
-
/(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(.*)/
|
|
3734
|
-
);
|
|
3735
|
-
if (!parts) return null;
|
|
3736
|
-
return {
|
|
3737
|
-
offsetX: parseFloat(parts[1]),
|
|
3738
|
-
offsetY: parseFloat(parts[2]),
|
|
3739
|
-
blur: parseFloat(parts[3]),
|
|
3740
|
-
color: parts[4].trim()
|
|
3741
|
-
};
|
|
3461
|
+
setFont(
|
|
3462
|
+
ctx,
|
|
3463
|
+
font.fontSize,
|
|
3464
|
+
font.fontFamily,
|
|
3465
|
+
font.fontWeight,
|
|
3466
|
+
font.fontStyle
|
|
3467
|
+
);
|
|
3468
|
+
const textRendering = ctx.textRendering;
|
|
3469
|
+
ctx.textRendering = "geometricPrecision";
|
|
3470
|
+
ctx.fillText(e.grapheme, offsetX + e.x, offsetY + e.baseline);
|
|
3471
|
+
ctx.textRendering = textRendering;
|
|
3472
|
+
});
|
|
3742
3473
|
}
|
|
3743
3474
|
function getColorAlpha(color) {
|
|
3744
3475
|
const parsed = parseCssColor2(color);
|
|
@@ -3746,6 +3477,7 @@ function getColorAlpha(color) {
|
|
|
3746
3477
|
}
|
|
3747
3478
|
function drawShadowPass(ctx, shadow, textAlpha, drawFn) {
|
|
3748
3479
|
ctx.save();
|
|
3480
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0)";
|
|
3749
3481
|
ctx.fillStyle = shadow.color;
|
|
3750
3482
|
if (textAlpha < 1) ctx.globalAlpha *= textAlpha;
|
|
3751
3483
|
if (shadow.blur > 0) ctx.filter = `blur(${shadow.blur / 2}px)`;
|
|
@@ -3835,9 +3567,8 @@ async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, gr
|
|
|
3835
3567
|
);
|
|
3836
3568
|
}
|
|
3837
3569
|
group.open = beginElementGroup(ctx, opacity, style.filter);
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
}
|
|
3570
|
+
const boxShadows = style.boxShadow ? parseBoxShadow(style.boxShadow, style.color ?? "black") : [];
|
|
3571
|
+
drawBoxShadow(ctx, x, y, width, height, boxShadows, borderRadius);
|
|
3841
3572
|
const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
|
|
3842
3573
|
if (isClipped) {
|
|
3843
3574
|
applyClip(ctx, x, y, width, height, borderRadius);
|
|
@@ -3872,15 +3603,14 @@ async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, gr
|
|
|
3872
3603
|
} else {
|
|
3873
3604
|
const urlMatch = layer.match(/url\(["']?(.*?)["']?\)/);
|
|
3874
3605
|
if (urlMatch) {
|
|
3875
|
-
if (hasRadius(borderRadius)) {
|
|
3876
|
-
applyClip(ctx, x, y, width, height, borderRadius);
|
|
3877
|
-
}
|
|
3878
3606
|
const image = await cachedLoadImage(
|
|
3879
3607
|
renderContext.imageCache,
|
|
3880
3608
|
urlMatch[1],
|
|
3881
3609
|
renderContext.userAgent
|
|
3882
3610
|
);
|
|
3883
3611
|
const bgSize = style.backgroundSize;
|
|
3612
|
+
ctx.save();
|
|
3613
|
+
applyClip(ctx, x, y, width, height, borderRadius);
|
|
3884
3614
|
if (bgSize === "cover") {
|
|
3885
3615
|
const r = computeCover(
|
|
3886
3616
|
image.width,
|
|
@@ -3921,19 +3651,24 @@ async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, gr
|
|
|
3921
3651
|
tileW = image.width;
|
|
3922
3652
|
tileH = image.height;
|
|
3923
3653
|
}
|
|
3654
|
+
const drawable = tileW > 0 && tileH > 0 && Number.isFinite(tileW) && Number.isFinite(tileH);
|
|
3924
3655
|
const repeat = style.backgroundRepeat ?? "repeat";
|
|
3925
3656
|
const stepX = repeat === "repeat" || repeat === "repeat-x" ? tileW : width;
|
|
3926
3657
|
const stepY = repeat === "repeat" || repeat === "repeat-y" ? tileH : height;
|
|
3927
|
-
|
|
3928
|
-
for (let
|
|
3929
|
-
|
|
3658
|
+
if (drawable) {
|
|
3659
|
+
for (let ty = y; ty < y + height; ty += stepY) {
|
|
3660
|
+
for (let tx = x; tx < x + width; tx += stepX) {
|
|
3661
|
+
ctx.drawImage(image, tx, ty, tileW, tileH);
|
|
3662
|
+
}
|
|
3930
3663
|
}
|
|
3931
3664
|
}
|
|
3932
3665
|
}
|
|
3666
|
+
ctx.restore();
|
|
3933
3667
|
}
|
|
3934
3668
|
}
|
|
3935
3669
|
}
|
|
3936
3670
|
}
|
|
3671
|
+
drawInsetBoxShadow(ctx, x, y, width, height, boxShadows, borderRadius, style);
|
|
3937
3672
|
if (renderContext.debug) {
|
|
3938
3673
|
ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
|
|
3939
3674
|
ctx.lineWidth = 1;
|
|
@@ -3949,24 +3684,14 @@ async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, gr
|
|
|
3949
3684
|
const contentX = x + paddingLeft + borderLeftW;
|
|
3950
3685
|
const contentY = y + paddingTop + borderTopW;
|
|
3951
3686
|
const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
|
|
3952
|
-
const textLayout = layoutText(
|
|
3953
|
-
node.textContent,
|
|
3954
|
-
style,
|
|
3955
|
-
contentWidth,
|
|
3956
|
-
ctx,
|
|
3957
|
-
!!emojiStyle
|
|
3958
|
-
);
|
|
3687
|
+
const textLayout = node.textLayout && contentWidth === node.width ? node.textLayout : layoutText(node.textContent, style, contentWidth, !!emojiStyle);
|
|
3959
3688
|
await drawText(
|
|
3960
3689
|
ctx,
|
|
3961
|
-
textLayout
|
|
3690
|
+
textLayout,
|
|
3962
3691
|
contentX,
|
|
3963
3692
|
contentY,
|
|
3964
3693
|
style.textShadow,
|
|
3965
|
-
emojiStyle
|
|
3966
|
-
textLayout.paragraph && {
|
|
3967
|
-
paragraph: textLayout.paragraph,
|
|
3968
|
-
offsetY: textLayout.paragraphOffsetY ?? 0
|
|
3969
|
-
}
|
|
3694
|
+
emojiStyle
|
|
3970
3695
|
);
|
|
3971
3696
|
}
|
|
3972
3697
|
if (node.type === "img" && node.props.src) {
|
|
@@ -4071,6 +3796,28 @@ function parseAngle(value) {
|
|
|
4071
3796
|
return parseFloat(value);
|
|
4072
3797
|
}
|
|
4073
3798
|
|
|
3799
|
+
// src/jsx/font.ts
|
|
3800
|
+
import { GlobalFonts } from "@effing/skia";
|
|
3801
|
+
var registeredFonts = /* @__PURE__ */ new Set();
|
|
3802
|
+
function registerFont(font) {
|
|
3803
|
+
const key = `${font.name}:${font.weight}:${font.style}`;
|
|
3804
|
+
if (registeredFonts.has(key)) return;
|
|
3805
|
+
const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
|
|
3806
|
+
GlobalFonts.register(buffer, font.name);
|
|
3807
|
+
registeredFonts.add(key);
|
|
3808
|
+
}
|
|
3809
|
+
function registerFontFromPath(path, nameAlias) {
|
|
3810
|
+
GlobalFonts.registerFromPath(path, nameAlias ?? "");
|
|
3811
|
+
}
|
|
3812
|
+
function registeredFamilies() {
|
|
3813
|
+
return GlobalFonts.families.map((f) => f.family);
|
|
3814
|
+
}
|
|
3815
|
+
function ensureFontsRegistered(fonts) {
|
|
3816
|
+
for (const font of fonts) {
|
|
3817
|
+
registerFont(font);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
|
|
4074
3821
|
// src/jsx/style/expand.ts
|
|
4075
3822
|
var SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
4076
3823
|
function parseSides(value) {
|
|
@@ -4264,6 +4011,10 @@ function expandStyle(raw, fontFamilies) {
|
|
|
4264
4011
|
style.backdropFilter = style.WebkitBackdropFilter;
|
|
4265
4012
|
delete style.WebkitBackdropFilter;
|
|
4266
4013
|
}
|
|
4014
|
+
if (style.wordWrap !== void 0) {
|
|
4015
|
+
if (style.overflowWrap === void 0) style.overflowWrap = style.wordWrap;
|
|
4016
|
+
delete style.wordWrap;
|
|
4017
|
+
}
|
|
4267
4018
|
if (style.WebkitClipPath !== void 0) {
|
|
4268
4019
|
if (style.clipPath === void 0) style.clipPath = style.WebkitClipPath;
|
|
4269
4020
|
delete style.WebkitClipPath;
|
|
@@ -4388,18 +4139,32 @@ import Yoga, {
|
|
|
4388
4139
|
FlexDirection,
|
|
4389
4140
|
Gutter,
|
|
4390
4141
|
Justify,
|
|
4142
|
+
MeasureMode,
|
|
4391
4143
|
Overflow,
|
|
4392
4144
|
PositionType,
|
|
4145
|
+
Unit,
|
|
4393
4146
|
Wrap
|
|
4394
4147
|
} from "yoga-layout";
|
|
4395
4148
|
function createYogaNode() {
|
|
4396
4149
|
return Yoga.Node.create();
|
|
4397
4150
|
}
|
|
4151
|
+
var textConfig;
|
|
4152
|
+
function createTextYogaNode() {
|
|
4153
|
+
if (!textConfig) {
|
|
4154
|
+
textConfig = Yoga.Config.create();
|
|
4155
|
+
textConfig.setPointScaleFactor(0);
|
|
4156
|
+
}
|
|
4157
|
+
return Yoga.Node.create(textConfig);
|
|
4158
|
+
}
|
|
4398
4159
|
function freeYogaNode(node) {
|
|
4399
4160
|
node.freeRecursive();
|
|
4400
4161
|
}
|
|
4401
4162
|
|
|
4402
4163
|
// src/jsx/style/properties.ts
|
|
4164
|
+
function toLayoutUnit(px) {
|
|
4165
|
+
if (!Number.isFinite(px)) return px;
|
|
4166
|
+
return Math.trunc(Math.round(px * 64 * 1e6) / 1e6) / 64;
|
|
4167
|
+
}
|
|
4403
4168
|
function applyStylesToYoga(node, style) {
|
|
4404
4169
|
if (style.display === "none") {
|
|
4405
4170
|
node.setDisplay(Display.None);
|
|
@@ -4477,14 +4242,14 @@ function applyStylesToYoga(node, style) {
|
|
|
4477
4242
|
node.setFlexShrink(style.flexShrink ?? 0);
|
|
4478
4243
|
if (style.flexBasis !== void 0) {
|
|
4479
4244
|
if (typeof style.flexBasis === "number") {
|
|
4480
|
-
node.setFlexBasis(style.flexBasis);
|
|
4245
|
+
node.setFlexBasis(toLayoutUnit(style.flexBasis));
|
|
4481
4246
|
} else if (String(style.flexBasis).endsWith("%")) {
|
|
4482
4247
|
node.setFlexBasis(String(style.flexBasis));
|
|
4483
4248
|
} else if (style.flexBasis === "auto") {
|
|
4484
4249
|
node.setFlexBasis("auto");
|
|
4485
4250
|
} else {
|
|
4486
4251
|
const n = parseFloat(String(style.flexBasis));
|
|
4487
|
-
if (!isNaN(n)) node.setFlexBasis(n);
|
|
4252
|
+
if (!isNaN(n)) node.setFlexBasis(toLayoutUnit(n));
|
|
4488
4253
|
}
|
|
4489
4254
|
}
|
|
4490
4255
|
applyDimension(node, "setWidth", style.width);
|
|
@@ -4511,16 +4276,20 @@ function applyStylesToYoga(node, style) {
|
|
|
4511
4276
|
applyEdgeValue(node, "setPadding", Edge.Bottom, style.paddingBottom);
|
|
4512
4277
|
applyEdgeValue(node, "setPadding", Edge.Left, style.paddingLeft);
|
|
4513
4278
|
if (style.borderTopWidth !== void 0)
|
|
4514
|
-
node.setBorder(Edge.Top, style.borderTopWidth);
|
|
4279
|
+
node.setBorder(Edge.Top, toLayoutUnit(style.borderTopWidth));
|
|
4515
4280
|
if (style.borderRightWidth !== void 0)
|
|
4516
|
-
node.setBorder(Edge.Right, style.borderRightWidth);
|
|
4281
|
+
node.setBorder(Edge.Right, toLayoutUnit(style.borderRightWidth));
|
|
4517
4282
|
if (style.borderBottomWidth !== void 0)
|
|
4518
|
-
node.setBorder(
|
|
4283
|
+
node.setBorder(
|
|
4284
|
+
Edge.Bottom,
|
|
4285
|
+
toLayoutUnit(style.borderBottomWidth)
|
|
4286
|
+
);
|
|
4519
4287
|
if (style.borderLeftWidth !== void 0)
|
|
4520
|
-
node.setBorder(Edge.Left, style.borderLeftWidth);
|
|
4521
|
-
if (style.rowGap !== void 0)
|
|
4288
|
+
node.setBorder(Edge.Left, toLayoutUnit(style.borderLeftWidth));
|
|
4289
|
+
if (style.rowGap !== void 0)
|
|
4290
|
+
node.setGap(Gutter.Row, toLayoutUnit(style.rowGap));
|
|
4522
4291
|
if (style.columnGap !== void 0)
|
|
4523
|
-
node.setGap(Gutter.Column, style.columnGap);
|
|
4292
|
+
node.setGap(Gutter.Column, toLayoutUnit(style.columnGap));
|
|
4524
4293
|
if (style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden") {
|
|
4525
4294
|
node.setOverflow(Overflow.Hidden);
|
|
4526
4295
|
} else {
|
|
@@ -4536,7 +4305,7 @@ function applyDimension(node, setter, value) {
|
|
|
4536
4305
|
return;
|
|
4537
4306
|
}
|
|
4538
4307
|
if (typeof value === "number") {
|
|
4539
|
-
node[setter](value);
|
|
4308
|
+
node[setter](toLayoutUnit(value));
|
|
4540
4309
|
return;
|
|
4541
4310
|
}
|
|
4542
4311
|
const s = String(value);
|
|
@@ -4544,7 +4313,7 @@ function applyDimension(node, setter, value) {
|
|
|
4544
4313
|
node[setter](s);
|
|
4545
4314
|
} else {
|
|
4546
4315
|
const n = parseFloat(s);
|
|
4547
|
-
if (!isNaN(n)) node[setter](n);
|
|
4316
|
+
if (!isNaN(n)) node[setter](toLayoutUnit(n));
|
|
4548
4317
|
}
|
|
4549
4318
|
}
|
|
4550
4319
|
function applyEdgeValue(node, setter, edge, value) {
|
|
@@ -4554,7 +4323,7 @@ function applyEdgeValue(node, setter, edge, value) {
|
|
|
4554
4323
|
return;
|
|
4555
4324
|
}
|
|
4556
4325
|
if (typeof value === "number") {
|
|
4557
|
-
node[setter](edge, value);
|
|
4326
|
+
node[setter](edge, toLayoutUnit(value));
|
|
4558
4327
|
return;
|
|
4559
4328
|
}
|
|
4560
4329
|
const s = String(value);
|
|
@@ -4562,25 +4331,25 @@ function applyEdgeValue(node, setter, edge, value) {
|
|
|
4562
4331
|
node[setter](edge, s);
|
|
4563
4332
|
} else {
|
|
4564
4333
|
const n = parseFloat(s);
|
|
4565
|
-
if (!isNaN(n)) node[setter](edge, n);
|
|
4334
|
+
if (!isNaN(n)) node[setter](edge, toLayoutUnit(n));
|
|
4566
4335
|
}
|
|
4567
4336
|
}
|
|
4568
4337
|
|
|
4569
4338
|
// src/jsx/layout.ts
|
|
4570
|
-
async function buildLayoutTree(element, containerWidth, containerHeight,
|
|
4339
|
+
async function buildLayoutTree(element, containerWidth, containerHeight, emojiEnabled, fontFamilies, context) {
|
|
4571
4340
|
const renderContext = context ?? {
|
|
4572
4341
|
imageCache: /* @__PURE__ */ new Map(),
|
|
4573
4342
|
debug: false
|
|
4574
4343
|
};
|
|
4575
|
-
const
|
|
4344
|
+
const rootElement = renderComponents(element);
|
|
4345
|
+
const elementYogaNode = isText(rootElement) ? createTextYogaNode() : createYogaNode();
|
|
4576
4346
|
const rootStyle = fontFamilies?.length ? { ...DEFAULT_STYLE, fontFamily: fontFamilies.join(", ") } : DEFAULT_STYLE;
|
|
4577
4347
|
const elementNode = await buildNode(
|
|
4578
|
-
|
|
4348
|
+
rootElement,
|
|
4579
4349
|
rootStyle,
|
|
4580
4350
|
elementYogaNode,
|
|
4581
4351
|
containerWidth,
|
|
4582
4352
|
containerHeight,
|
|
4583
|
-
ctx,
|
|
4584
4353
|
emojiEnabled,
|
|
4585
4354
|
fontFamilies,
|
|
4586
4355
|
renderContext
|
|
@@ -4590,7 +4359,46 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
|
|
|
4590
4359
|
rootYogaNode.setHeight(containerHeight);
|
|
4591
4360
|
rootYogaNode.setFlexDirection(FlexDirection.Row);
|
|
4592
4361
|
rootYogaNode.insertChild(elementYogaNode, 0);
|
|
4593
|
-
|
|
4362
|
+
let layouts = 0;
|
|
4363
|
+
const compute = () => {
|
|
4364
|
+
layouts++;
|
|
4365
|
+
rootYogaNode.calculateLayout(containerWidth, containerHeight);
|
|
4366
|
+
};
|
|
4367
|
+
const autoMinimums = collectAutoMinimums(elementNode);
|
|
4368
|
+
const layOut = () => {
|
|
4369
|
+
compute();
|
|
4370
|
+
for (const node of autoMinimums) node.autoMinimum.refreshed = false;
|
|
4371
|
+
for (let pass = 0; pass < MAX_AUTO_MINIMUM_RELAYOUTS && enforceAutoMinimums(autoMinimums); pass++) {
|
|
4372
|
+
compute();
|
|
4373
|
+
}
|
|
4374
|
+
};
|
|
4375
|
+
layOut();
|
|
4376
|
+
settleText(elementNode, layOut, renderContext.debug);
|
|
4377
|
+
const limit = RUNAWAY_SIZE * Math.max(containerWidth, containerHeight);
|
|
4378
|
+
for (let pass = 0; pass < MAX_RUNAWAY_RELAYOUTS; pass++) {
|
|
4379
|
+
const runaway2 = findRunawayItems(elementNode, limit);
|
|
4380
|
+
if (runaway2.length === 0) break;
|
|
4381
|
+
for (const item of runaway2) pinAtMinimum(item);
|
|
4382
|
+
layOut();
|
|
4383
|
+
settleText(elementNode, layOut, renderContext.debug);
|
|
4384
|
+
}
|
|
4385
|
+
const runaway = findRunawayItems(elementNode, limit);
|
|
4386
|
+
if (runaway.length > 0) {
|
|
4387
|
+
const lines = new Set(runaway.map((item) => item.parent));
|
|
4388
|
+
const lifted = [...lines].flatMap((line) => line.children).filter((node) => node.autoMinimum && liftAutoMinimum(node));
|
|
4389
|
+
if (lifted.length > 0) {
|
|
4390
|
+
layOut();
|
|
4391
|
+
settleText(elementNode, layOut, renderContext.debug);
|
|
4392
|
+
}
|
|
4393
|
+
if (renderContext.debug) {
|
|
4394
|
+
console.warn(
|
|
4395
|
+
"[@effing/canvas] Yoga sized flex items out of all proportion" + (lifted.length > 0 ? ": laid their text out without minimums" : "")
|
|
4396
|
+
);
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
if (renderContext.debug && layouts > 5) {
|
|
4400
|
+
console.warn(`[@effing/canvas] computed the layout ${layouts} times`);
|
|
4401
|
+
}
|
|
4594
4402
|
const elementLayout = extractLayout(elementNode, elementYogaNode);
|
|
4595
4403
|
freeYogaNode(rootYogaNode);
|
|
4596
4404
|
const tree = {
|
|
@@ -4605,7 +4413,7 @@ async function buildLayoutTree(element, containerWidth, containerHeight, ctx, em
|
|
|
4605
4413
|
};
|
|
4606
4414
|
return { tree, imageCache: renderContext.imageCache };
|
|
4607
4415
|
}
|
|
4608
|
-
async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight,
|
|
4416
|
+
async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewportHeight, emojiEnabled, fontFamilies, context) {
|
|
4609
4417
|
if (element === null || element === void 0 || typeof element === "boolean") {
|
|
4610
4418
|
return {
|
|
4611
4419
|
type: "empty",
|
|
@@ -4615,38 +4423,12 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4615
4423
|
yogaNode
|
|
4616
4424
|
};
|
|
4617
4425
|
}
|
|
4618
|
-
if (
|
|
4619
|
-
const text = String(element);
|
|
4620
|
-
|
|
4621
|
-
const measureFunc = createTextMeasureFunc(text, style2, ctx, emojiEnabled);
|
|
4622
|
-
yogaNode.setMeasureFunc(measureFunc);
|
|
4623
|
-
return {
|
|
4624
|
-
type: "text",
|
|
4625
|
-
style: style2,
|
|
4626
|
-
children: [],
|
|
4627
|
-
textContent: text,
|
|
4628
|
-
props: {},
|
|
4629
|
-
yogaNode
|
|
4630
|
-
};
|
|
4426
|
+
if (isText(element) || isTextRun(element)) {
|
|
4427
|
+
const text = isTextRun(element) ? textContentOf(element) : String(element);
|
|
4428
|
+
return buildTextNode(text, parentStyle, yogaNode, emojiEnabled);
|
|
4631
4429
|
}
|
|
4632
4430
|
const el = element;
|
|
4633
4431
|
const type = el.type;
|
|
4634
|
-
if (typeof type === "function") {
|
|
4635
|
-
const rendered = type(
|
|
4636
|
-
el.props ?? {}
|
|
4637
|
-
);
|
|
4638
|
-
return await buildNode(
|
|
4639
|
-
rendered,
|
|
4640
|
-
parentStyle,
|
|
4641
|
-
yogaNode,
|
|
4642
|
-
viewportWidth,
|
|
4643
|
-
viewportHeight,
|
|
4644
|
-
ctx,
|
|
4645
|
-
emojiEnabled,
|
|
4646
|
-
fontFamilies,
|
|
4647
|
-
context
|
|
4648
|
-
);
|
|
4649
|
-
}
|
|
4650
4432
|
const props = el.props ?? {};
|
|
4651
4433
|
const rawStyle = props.style ?? {};
|
|
4652
4434
|
const expanded = expandStyle(rawStyle, fontFamilies);
|
|
@@ -4740,17 +4522,18 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4740
4522
|
yogaNode
|
|
4741
4523
|
};
|
|
4742
4524
|
}
|
|
4743
|
-
const
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4525
|
+
const items = flattenChildren(props.children).filter(
|
|
4526
|
+
(item) => !isTextRun(item) || !isBlankRun(item)
|
|
4527
|
+
);
|
|
4528
|
+
const onlyText = items.length === 1 && isTextRun(items[0]) ? items[0] : null;
|
|
4529
|
+
if (onlyText !== null) {
|
|
4530
|
+
const childYogaNode = createTextYogaNode();
|
|
4531
|
+
const child = buildTextNode(
|
|
4532
|
+
textContentOf(onlyText),
|
|
4533
|
+
style,
|
|
4534
|
+
childYogaNode,
|
|
4751
4535
|
emojiEnabled
|
|
4752
4536
|
);
|
|
4753
|
-
childYogaNode.setMeasureFunc(measureFunc);
|
|
4754
4537
|
const jc = style.justifyContent;
|
|
4755
4538
|
if (!jc || jc === "flex-start") {
|
|
4756
4539
|
childYogaNode.setFlexGrow(1);
|
|
@@ -4760,54 +4543,42 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4760
4543
|
if (style.flexShrink === void 0) {
|
|
4761
4544
|
yogaNode.setFlexShrink(1);
|
|
4762
4545
|
}
|
|
4546
|
+
const textMeasure = child.textMeasure;
|
|
4547
|
+
child.autoMinimum = {
|
|
4548
|
+
textMeasure,
|
|
4549
|
+
column: !isRow(style),
|
|
4550
|
+
text: childYogaNode
|
|
4551
|
+
};
|
|
4763
4552
|
return {
|
|
4764
4553
|
type: tagName,
|
|
4765
4554
|
style,
|
|
4766
|
-
children: [
|
|
4767
|
-
{
|
|
4768
|
-
type: "text",
|
|
4769
|
-
style: childStyle,
|
|
4770
|
-
children: [],
|
|
4771
|
-
textContent,
|
|
4772
|
-
props: {},
|
|
4773
|
-
yogaNode: childYogaNode
|
|
4774
|
-
}
|
|
4775
|
-
],
|
|
4555
|
+
children: [child],
|
|
4776
4556
|
props,
|
|
4777
|
-
yogaNode
|
|
4557
|
+
yogaNode,
|
|
4558
|
+
autoMinimum: hasAutoMinimum(style, parentStyle) ? {
|
|
4559
|
+
textMeasure,
|
|
4560
|
+
style,
|
|
4561
|
+
column: !isRow(parentStyle),
|
|
4562
|
+
text: childYogaNode
|
|
4563
|
+
} : void 0
|
|
4778
4564
|
};
|
|
4779
4565
|
}
|
|
4780
4566
|
const children = [];
|
|
4781
|
-
const
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
yogaNode.insertChild(childYogaNode, children.length);
|
|
4797
|
-
children.push(
|
|
4798
|
-
await buildNode(
|
|
4799
|
-
processedChild,
|
|
4800
|
-
style,
|
|
4801
|
-
childYogaNode,
|
|
4802
|
-
viewportWidth,
|
|
4803
|
-
viewportHeight,
|
|
4804
|
-
ctx,
|
|
4805
|
-
emojiEnabled,
|
|
4806
|
-
fontFamilies,
|
|
4807
|
-
context
|
|
4808
|
-
)
|
|
4809
|
-
);
|
|
4810
|
-
}
|
|
4567
|
+
for (const rendered of items) {
|
|
4568
|
+
const childYogaNode = isTextRun(rendered) ? createTextYogaNode() : createYogaNode();
|
|
4569
|
+
yogaNode.insertChild(childYogaNode, children.length);
|
|
4570
|
+
children.push(
|
|
4571
|
+
await buildNode(
|
|
4572
|
+
rendered,
|
|
4573
|
+
style,
|
|
4574
|
+
childYogaNode,
|
|
4575
|
+
viewportWidth,
|
|
4576
|
+
viewportHeight,
|
|
4577
|
+
emojiEnabled,
|
|
4578
|
+
fontFamilies,
|
|
4579
|
+
context
|
|
4580
|
+
)
|
|
4581
|
+
);
|
|
4811
4582
|
}
|
|
4812
4583
|
return {
|
|
4813
4584
|
type: tagName,
|
|
@@ -4817,8 +4588,339 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
4817
4588
|
yogaNode
|
|
4818
4589
|
};
|
|
4819
4590
|
}
|
|
4591
|
+
var MAX_TEXT_RELAYOUTS = 2;
|
|
4592
|
+
function settleText(root, relayout, debug) {
|
|
4593
|
+
const textNodes = [];
|
|
4594
|
+
const collect = (node) => {
|
|
4595
|
+
if (node.style.display === "none") return;
|
|
4596
|
+
if (node.textMeasure) textNodes.push(node);
|
|
4597
|
+
node.children.forEach(collect);
|
|
4598
|
+
};
|
|
4599
|
+
collect(root);
|
|
4600
|
+
const settle = (node, pin) => node.textMeasure.settle(node.yogaNode.getComputedWidth(), pin);
|
|
4601
|
+
for (let pass = 0; pass < MAX_TEXT_RELAYOUTS; pass++) {
|
|
4602
|
+
let unsettled2 = false;
|
|
4603
|
+
for (const node of textNodes) {
|
|
4604
|
+
if (settle(node, true)) {
|
|
4605
|
+
node.yogaNode.markDirty();
|
|
4606
|
+
unsettled2 = true;
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
if (!unsettled2) return;
|
|
4610
|
+
relayout();
|
|
4611
|
+
}
|
|
4612
|
+
if (!textNodes.some((node) => settle(node, false))) return;
|
|
4613
|
+
for (const node of textNodes) {
|
|
4614
|
+
if (node.textMeasure.unpin()) node.yogaNode.markDirty();
|
|
4615
|
+
}
|
|
4616
|
+
relayout();
|
|
4617
|
+
const unsettled = textNodes.filter((node) => settle(node, false)).length;
|
|
4618
|
+
if (debug && unsettled > 0) {
|
|
4619
|
+
console.warn(
|
|
4620
|
+
`[@effing/canvas] ${unsettled} text node(s) sized for other lines than drawn: the layout didn't settle in ${MAX_TEXT_RELAYOUTS} relayouts`
|
|
4621
|
+
);
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
function buildTextNode(text, parentStyle, yogaNode, emojiEnabled) {
|
|
4625
|
+
const style = resolveStyle(void 0, parentStyle);
|
|
4626
|
+
const textMeasure = new TextMeasure(text, style, emojiEnabled);
|
|
4627
|
+
setTextMeasure(yogaNode, textMeasure);
|
|
4628
|
+
return {
|
|
4629
|
+
type: "text",
|
|
4630
|
+
style,
|
|
4631
|
+
children: [],
|
|
4632
|
+
textContent: text,
|
|
4633
|
+
textMeasure,
|
|
4634
|
+
props: {},
|
|
4635
|
+
yogaNode
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
var isRow = (style) => style.flexDirection === void 0 || style.flexDirection === "row" || style.flexDirection === "row-reverse";
|
|
4639
|
+
function hasAutoMinimum(style, parentStyle) {
|
|
4640
|
+
const min = isRow(parentStyle) ? style.minWidth : style.minHeight;
|
|
4641
|
+
return style.position !== "absolute" && (min === void 0 || min === "auto") && ![style.overflow, style.overflowX, style.overflowY].some(
|
|
4642
|
+
(overflow) => overflow === "hidden" || overflow === "scroll" || overflow === "auto"
|
|
4643
|
+
);
|
|
4644
|
+
}
|
|
4645
|
+
var MAX_AUTO_MINIMUM_RELAYOUTS = 4;
|
|
4646
|
+
function collectAutoMinimums(root) {
|
|
4647
|
+
const nodes = [];
|
|
4648
|
+
const collect = (node) => {
|
|
4649
|
+
if (node.style.display === "none") return;
|
|
4650
|
+
if (node.autoMinimum) nodes.push(node);
|
|
4651
|
+
node.children.forEach(collect);
|
|
4652
|
+
};
|
|
4653
|
+
collect(root);
|
|
4654
|
+
return nodes;
|
|
4655
|
+
}
|
|
4656
|
+
function enforceAutoMinimums(nodes) {
|
|
4657
|
+
let enforced = false;
|
|
4658
|
+
for (const node of nodes) {
|
|
4659
|
+
const autoMinimum = node.autoMinimum;
|
|
4660
|
+
if (autoMinimum.lifted) continue;
|
|
4661
|
+
if (autoMinimum.column) {
|
|
4662
|
+
if (enforceAutoMinimumHeight(node)) enforced = true;
|
|
4663
|
+
continue;
|
|
4664
|
+
}
|
|
4665
|
+
if (autoMinimum.lift) continue;
|
|
4666
|
+
const { yogaNode } = node;
|
|
4667
|
+
const { textMeasure, style } = autoMinimum;
|
|
4668
|
+
const width = yogaNode.getComputedWidth();
|
|
4669
|
+
const tolerance = style ? 1 : 1e-3;
|
|
4670
|
+
const edges = style ? horizontalEdges(yogaNode, style) : 0;
|
|
4671
|
+
if (width + tolerance >= textMeasure.minContentBound + edges) continue;
|
|
4672
|
+
const minimum = autoMinimumWidth(node);
|
|
4673
|
+
if (minimum === void 0 || width + tolerance >= minimum) continue;
|
|
4674
|
+
autoMinimum.lift = holdAtMinimum(yogaNode, minimum, {
|
|
4675
|
+
atBasis: textMeasure.unbreakable && (!style || isAuto(style.width) && isAuto(style.flexBasis)),
|
|
4676
|
+
// The text in an element sizes the element's own flex basis, which a
|
|
4677
|
+
// pin would take from it.
|
|
4678
|
+
pin: style !== void 0,
|
|
4679
|
+
column: false
|
|
4680
|
+
});
|
|
4681
|
+
enforced = true;
|
|
4682
|
+
}
|
|
4683
|
+
return enforced;
|
|
4684
|
+
}
|
|
4685
|
+
function enforceAutoMinimumHeight(node) {
|
|
4686
|
+
const autoMinimum = node.autoMinimum;
|
|
4687
|
+
const { yogaNode } = node;
|
|
4688
|
+
const { style } = autoMinimum;
|
|
4689
|
+
const minimum = autoMinimumHeight(node);
|
|
4690
|
+
if (minimum === void 0) return false;
|
|
4691
|
+
const height = yogaNode.getComputedHeight();
|
|
4692
|
+
const tolerance = style ? 0.5 : 1e-3;
|
|
4693
|
+
const short = height + tolerance < minimum;
|
|
4694
|
+
const atBasis = !style || isAuto(style.height) && isAuto(style.flexBasis);
|
|
4695
|
+
if (autoMinimum.lift) {
|
|
4696
|
+
const { held } = autoMinimum;
|
|
4697
|
+
if (held === void 0) {
|
|
4698
|
+
if (!short) return false;
|
|
4699
|
+
} else if (Math.abs(held - minimum) < tolerance || // In a column that wraps, the heights decide the widths: an item is
|
|
4700
|
+
// only ever held higher there, or they could swing back and forth.
|
|
4701
|
+
minimum < held && yogaNode.getParent()?.getFlexWrap() !== Wrap.NoWrap) {
|
|
4702
|
+
if (height + (style ? 1 : 1e-3) >= held || autoMinimum.refreshed) {
|
|
4703
|
+
return false;
|
|
4704
|
+
}
|
|
4705
|
+
autoMinimum.text.markDirty();
|
|
4706
|
+
autoMinimum.refreshed = true;
|
|
4707
|
+
return true;
|
|
4708
|
+
}
|
|
4709
|
+
autoMinimum.lift();
|
|
4710
|
+
} else if (!short) {
|
|
4711
|
+
return false;
|
|
4712
|
+
} else if (atBasis) {
|
|
4713
|
+
autoMinimum.lift = holdAtMinimum(yogaNode, minimum, {
|
|
4714
|
+
atBasis: true,
|
|
4715
|
+
pin: false,
|
|
4716
|
+
column: true
|
|
4717
|
+
});
|
|
4718
|
+
return true;
|
|
4719
|
+
}
|
|
4720
|
+
autoMinimum.held = minimum;
|
|
4721
|
+
autoMinimum.lift = holdAtMinimum(yogaNode, minimum, {
|
|
4722
|
+
atBasis: false,
|
|
4723
|
+
pin: !atBasis,
|
|
4724
|
+
column: true
|
|
4725
|
+
});
|
|
4726
|
+
return true;
|
|
4727
|
+
}
|
|
4728
|
+
var isAuto = (value) => value === void 0 || value === "auto";
|
|
4729
|
+
function horizontalEdges(yogaNode, style) {
|
|
4730
|
+
const lengths = style && [
|
|
4731
|
+
style.paddingLeft,
|
|
4732
|
+
style.paddingRight,
|
|
4733
|
+
style.borderLeftWidth,
|
|
4734
|
+
style.borderRightWidth
|
|
4735
|
+
];
|
|
4736
|
+
if (lengths?.every((v) => v === void 0 || typeof v === "number")) {
|
|
4737
|
+
return lengths.reduce(
|
|
4738
|
+
(sum, v) => sum + toLayoutUnit(v ?? 0),
|
|
4739
|
+
0
|
|
4740
|
+
);
|
|
4741
|
+
}
|
|
4742
|
+
return yogaNode.getComputedPadding(Edge.Left) + yogaNode.getComputedPadding(Edge.Right) + yogaNode.getComputedBorder(Edge.Left) + yogaNode.getComputedBorder(Edge.Right);
|
|
4743
|
+
}
|
|
4744
|
+
function autoMinimumWidth(node) {
|
|
4745
|
+
const { yogaNode } = node;
|
|
4746
|
+
const { textMeasure, style } = node.autoMinimum;
|
|
4747
|
+
if (!style) return textMeasure.minContentWidth;
|
|
4748
|
+
const parent = yogaNode.getParent();
|
|
4749
|
+
return capped(
|
|
4750
|
+
textMeasure.minContentWidth + horizontalEdges(yogaNode, style),
|
|
4751
|
+
[style.width, style.maxWidth],
|
|
4752
|
+
parent ? parent.getComputedWidth() - horizontalEdges(parent) : NaN
|
|
4753
|
+
);
|
|
4754
|
+
}
|
|
4755
|
+
function autoMinimumHeight(node) {
|
|
4756
|
+
const { yogaNode } = node;
|
|
4757
|
+
const { textMeasure, style, text } = node.autoMinimum;
|
|
4758
|
+
const height = textMeasure.heightAt(text.getComputedWidth());
|
|
4759
|
+
if (!style) return height;
|
|
4760
|
+
const parent = yogaNode.getParent();
|
|
4761
|
+
return capped(
|
|
4762
|
+
height + verticalEdges(yogaNode),
|
|
4763
|
+
[style.height, style.maxHeight],
|
|
4764
|
+
parent ? parent.getComputedHeight() - verticalEdges(parent) : NaN
|
|
4765
|
+
);
|
|
4766
|
+
}
|
|
4767
|
+
function capped(minimum, sizes, parentContentSize) {
|
|
4768
|
+
for (const value of sizes) {
|
|
4769
|
+
if (isAuto(value)) continue;
|
|
4770
|
+
let cap;
|
|
4771
|
+
if (typeof value === "number") {
|
|
4772
|
+
cap = toLayoutUnit(value);
|
|
4773
|
+
} else if (value.endsWith("%") && Number.isFinite(parentContentSize)) {
|
|
4774
|
+
cap = parseFloat(value) / 100 * parentContentSize;
|
|
4775
|
+
} else {
|
|
4776
|
+
return void 0;
|
|
4777
|
+
}
|
|
4778
|
+
if (!Number.isFinite(cap)) return void 0;
|
|
4779
|
+
minimum = Math.min(minimum, cap);
|
|
4780
|
+
}
|
|
4781
|
+
return minimum;
|
|
4782
|
+
}
|
|
4783
|
+
var verticalEdges = (yogaNode) => yogaNode.getComputedPadding(Edge.Top) + yogaNode.getComputedPadding(Edge.Bottom) + yogaNode.getComputedBorder(Edge.Top) + yogaNode.getComputedBorder(Edge.Bottom);
|
|
4784
|
+
function holdAtMinimum(yogaNode, minimum, { atBasis, pin, column }) {
|
|
4785
|
+
const flexShrink = yogaNode.getFlexShrink();
|
|
4786
|
+
if (atBasis) {
|
|
4787
|
+
yogaNode.setFlexShrink(0);
|
|
4788
|
+
return () => yogaNode.setFlexShrink(flexShrink);
|
|
4789
|
+
}
|
|
4790
|
+
const wraps = yogaNode.getParent()?.getFlexWrap() !== Wrap.NoWrap;
|
|
4791
|
+
if (!pin || wraps) {
|
|
4792
|
+
const setMin = (value) => column ? yogaNode.setMinHeight(value) : yogaNode.setMinWidth(value);
|
|
4793
|
+
setMin(minimum);
|
|
4794
|
+
return () => setMin(void 0);
|
|
4795
|
+
}
|
|
4796
|
+
const flexGrow = yogaNode.getFlexGrow();
|
|
4797
|
+
const flexBasis = yogaNode.getFlexBasis();
|
|
4798
|
+
yogaNode.setFlexBasis(minimum);
|
|
4799
|
+
yogaNode.setFlexShrink(0);
|
|
4800
|
+
yogaNode.setFlexGrow(0);
|
|
4801
|
+
return () => {
|
|
4802
|
+
yogaNode.setFlexShrink(flexShrink);
|
|
4803
|
+
yogaNode.setFlexGrow(flexGrow);
|
|
4804
|
+
if (flexBasis.unit === Unit.Point) yogaNode.setFlexBasis(flexBasis.value);
|
|
4805
|
+
else if (flexBasis.unit === Unit.Percent)
|
|
4806
|
+
yogaNode.setFlexBasis(`${flexBasis.value}%`);
|
|
4807
|
+
else yogaNode.setFlexBasis("auto");
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
function liftAutoMinimum(node) {
|
|
4811
|
+
const autoMinimum = node.autoMinimum;
|
|
4812
|
+
const { lift } = autoMinimum;
|
|
4813
|
+
lift?.();
|
|
4814
|
+
autoMinimum.lift = void 0;
|
|
4815
|
+
autoMinimum.lifted = true;
|
|
4816
|
+
return lift !== void 0;
|
|
4817
|
+
}
|
|
4818
|
+
var RUNAWAY_SIZE = 100;
|
|
4819
|
+
var MAX_RUNAWAY_RELAYOUTS = 2;
|
|
4820
|
+
var isRunaway = (size, limit) => !(Math.abs(size) <= limit);
|
|
4821
|
+
function findRunawayItems(root, limit) {
|
|
4822
|
+
const items = [];
|
|
4823
|
+
const visit = (node, parent) => {
|
|
4824
|
+
if (node.style.display === "none") return;
|
|
4825
|
+
const { yogaNode } = node;
|
|
4826
|
+
if (parent) {
|
|
4827
|
+
const row = [FlexDirection.Row, FlexDirection.RowReverse].includes(
|
|
4828
|
+
parent.yogaNode.getFlexDirection()
|
|
4829
|
+
);
|
|
4830
|
+
const size = (n) => row ? n.getComputedWidth() : n.getComputedHeight();
|
|
4831
|
+
if (yogaNode.getFlexShrink() > 0 && isRunaway(size(yogaNode), limit) && !isRunaway(size(parent.yogaNode), limit)) {
|
|
4832
|
+
items.push({ node, parent, row });
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
for (const child of node.children) visit(child, node);
|
|
4837
|
+
};
|
|
4838
|
+
visit(root);
|
|
4839
|
+
return items;
|
|
4840
|
+
}
|
|
4841
|
+
function pinAtMinimum({ node, parent, row }) {
|
|
4842
|
+
const { yogaNode } = node;
|
|
4843
|
+
const autoMinimum = node.autoMinimum;
|
|
4844
|
+
const auto = autoMinimum && autoMinimum.column !== row ? row ? autoMinimumWidth(node) : autoMinimumHeight(node) : 0;
|
|
4845
|
+
yogaNode.setFlexBasis(
|
|
4846
|
+
Math.max(minimumSize(yogaNode, parent.yogaNode, row), auto ?? 0)
|
|
4847
|
+
);
|
|
4848
|
+
yogaNode.setFlexShrink(0);
|
|
4849
|
+
}
|
|
4850
|
+
function minimumSize(yogaNode, parent, row) {
|
|
4851
|
+
const [start, end] = row ? [Edge.Left, Edge.Right] : [Edge.Top, Edge.Bottom];
|
|
4852
|
+
const edges = (node) => node.getComputedPadding(start) + node.getComputedPadding(end) + node.getComputedBorder(start) + node.getComputedBorder(end);
|
|
4853
|
+
const min = row ? yogaNode.getMinWidth() : yogaNode.getMinHeight();
|
|
4854
|
+
let minimum = 0;
|
|
4855
|
+
if (min.unit === Unit.Point) {
|
|
4856
|
+
minimum = min.value;
|
|
4857
|
+
} else if (min.unit === Unit.Percent) {
|
|
4858
|
+
const parentSize = row ? parent.getComputedWidth() : parent.getComputedHeight();
|
|
4859
|
+
minimum = min.value / 100 * (parentSize - edges(parent));
|
|
4860
|
+
}
|
|
4861
|
+
return Math.max(minimum, edges(yogaNode));
|
|
4862
|
+
}
|
|
4863
|
+
function setTextMeasure(yogaNode, textMeasure) {
|
|
4864
|
+
yogaNode.setMeasureFunc(
|
|
4865
|
+
(width, widthMode) => widthMode === MeasureMode.Undefined || Number.isNaN(width) ? textMeasure.measure(Infinity) : textMeasure.measure(width, widthMode === MeasureMode.Exactly)
|
|
4866
|
+
);
|
|
4867
|
+
}
|
|
4868
|
+
var isText = (node) => typeof node === "string" || typeof node === "number";
|
|
4869
|
+
function renderComponents(node) {
|
|
4870
|
+
while (node !== null && typeof node === "object" && "type" in node && typeof node.type === "function") {
|
|
4871
|
+
const el = node;
|
|
4872
|
+
node = el.type(
|
|
4873
|
+
el.props ?? {}
|
|
4874
|
+
);
|
|
4875
|
+
}
|
|
4876
|
+
return node;
|
|
4877
|
+
}
|
|
4878
|
+
var TextRun = class {
|
|
4879
|
+
pieces;
|
|
4880
|
+
constructor(...pieces) {
|
|
4881
|
+
this.pieces = pieces;
|
|
4882
|
+
}
|
|
4883
|
+
};
|
|
4884
|
+
var isTextRun = (node) => node instanceof TextRun;
|
|
4885
|
+
var textContentOf = (run) => run.pieces.length === 1 ? run.pieces[0] : run.pieces;
|
|
4886
|
+
var isBlankRun = (run) => run.pieces.length === 1 && isWhiteSpaceOnly(run.pieces[0]);
|
|
4887
|
+
function flattenChildren(node, items = []) {
|
|
4888
|
+
const rendered = renderComponents(node);
|
|
4889
|
+
if (rendered === null || rendered === void 0 || typeof rendered === "boolean") {
|
|
4890
|
+
return items;
|
|
4891
|
+
}
|
|
4892
|
+
if (Array.isArray(rendered)) {
|
|
4893
|
+
for (const child of rendered) flattenChildren(child, items);
|
|
4894
|
+
return items;
|
|
4895
|
+
}
|
|
4896
|
+
const el = rendered;
|
|
4897
|
+
if (typeof rendered === "object" && el.type === REACT_FRAGMENT) {
|
|
4898
|
+
return flattenChildren(el.props?.children, items);
|
|
4899
|
+
}
|
|
4900
|
+
const last = items[items.length - 1];
|
|
4901
|
+
if (isText(rendered)) {
|
|
4902
|
+
if (isTextRun(last)) {
|
|
4903
|
+
last.pieces[last.pieces.length - 1] += String(rendered);
|
|
4904
|
+
} else {
|
|
4905
|
+
items.push(new TextRun(String(rendered)));
|
|
4906
|
+
}
|
|
4907
|
+
} else if (!isBreak(rendered)) {
|
|
4908
|
+
items.push(rendered);
|
|
4909
|
+
} else if (!isHidden(rendered)) {
|
|
4910
|
+
if (isTextRun(last)) last.pieces.push("");
|
|
4911
|
+
else items.push(new TextRun("", ""));
|
|
4912
|
+
}
|
|
4913
|
+
return items;
|
|
4914
|
+
}
|
|
4915
|
+
var isBreak = (node) => typeof node === "object" && node !== null && node.type === "br";
|
|
4916
|
+
var isHidden = (el) => el.props.style?.display === "none";
|
|
4917
|
+
function floorToPixel(value) {
|
|
4918
|
+
const rounded = Math.round(value);
|
|
4919
|
+
return Math.abs(value - rounded) < 1e-4 ? rounded : Math.floor(value);
|
|
4920
|
+
}
|
|
4820
4921
|
function extractLayout(node, yogaNode) {
|
|
4821
4922
|
const layout = yogaNode.getComputedLayout();
|
|
4923
|
+
const place = node.textMeasure ? floorToPixel : (v) => v;
|
|
4822
4924
|
return {
|
|
4823
4925
|
type: node.type,
|
|
4824
4926
|
style: node.style,
|
|
@@ -4827,37 +4929,14 @@ function extractLayout(node, yogaNode) {
|
|
|
4827
4929
|
return extractLayout(child, childYoga);
|
|
4828
4930
|
}),
|
|
4829
4931
|
textContent: node.textContent,
|
|
4932
|
+
textLayout: node.textMeasure?.layout,
|
|
4830
4933
|
props: node.props,
|
|
4831
|
-
x: layout.left,
|
|
4832
|
-
y: layout.top,
|
|
4934
|
+
x: place(layout.left),
|
|
4935
|
+
y: place(layout.top),
|
|
4833
4936
|
width: layout.width,
|
|
4834
4937
|
height: layout.height
|
|
4835
4938
|
};
|
|
4836
4939
|
}
|
|
4837
|
-
function isBrElement(child) {
|
|
4838
|
-
if (child === null || child === void 0 || typeof child !== "object")
|
|
4839
|
-
return false;
|
|
4840
|
-
return child.type === "br";
|
|
4841
|
-
}
|
|
4842
|
-
function extractTextContent(children) {
|
|
4843
|
-
if (children === void 0 || children === null) return void 0;
|
|
4844
|
-
if (typeof children === "string") return children;
|
|
4845
|
-
if (typeof children === "number") return String(children);
|
|
4846
|
-
if (Array.isArray(children)) {
|
|
4847
|
-
const parts = [];
|
|
4848
|
-
for (const child of children) {
|
|
4849
|
-
if (typeof child === "string") {
|
|
4850
|
-
parts.push(child);
|
|
4851
|
-
} else if (typeof child === "number") {
|
|
4852
|
-
parts.push(String(child));
|
|
4853
|
-
} else if (child !== null && child !== void 0 && typeof child !== "boolean") {
|
|
4854
|
-
return void 0;
|
|
4855
|
-
}
|
|
4856
|
-
}
|
|
4857
|
-
return parts.join("");
|
|
4858
|
-
}
|
|
4859
|
-
return void 0;
|
|
4860
|
-
}
|
|
4861
4940
|
var REACT_FRAGMENT = /* @__PURE__ */ Symbol.for("react.fragment");
|
|
4862
4941
|
function resolveSvgTree(node) {
|
|
4863
4942
|
if (node === null || node === void 0 || typeof node === "boolean")
|
|
@@ -4901,18 +4980,6 @@ function resolveSvgTree(node) {
|
|
|
4901
4980
|
}
|
|
4902
4981
|
return el;
|
|
4903
4982
|
}
|
|
4904
|
-
function hasElementChildren(children) {
|
|
4905
|
-
if (children === void 0 || children === null || typeof children === "boolean")
|
|
4906
|
-
return false;
|
|
4907
|
-
if (typeof children === "string" || typeof children === "number")
|
|
4908
|
-
return false;
|
|
4909
|
-
if (Array.isArray(children)) {
|
|
4910
|
-
return children.some(
|
|
4911
|
-
(child) => child !== null && child !== void 0 && typeof child !== "boolean" && typeof child !== "string" && typeof child !== "number"
|
|
4912
|
-
);
|
|
4913
|
-
}
|
|
4914
|
-
return typeof children === "object";
|
|
4915
|
-
}
|
|
4916
4983
|
|
|
4917
4984
|
// src/jsx/index.ts
|
|
4918
4985
|
async function renderReactElement(ctx, element, options = {}) {
|
|
@@ -4931,7 +4998,6 @@ async function renderReactElement(ctx, element, options = {}) {
|
|
|
4931
4998
|
element,
|
|
4932
4999
|
width,
|
|
4933
5000
|
height,
|
|
4934
|
-
ctx,
|
|
4935
5001
|
!!emojiStyle,
|
|
4936
5002
|
fontFamilies,
|
|
4937
5003
|
renderContext
|