@weasel-js/text 1.9.3 → 1.10.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 +11 -9
- package/dist/{chunk-L6UJ4RQK.js → chunk-Z6YRJ33J.js} +481 -649
- package/dist/chunk-Z6YRJ33J.js.map +1 -0
- package/dist/index.d.ts +519 -52
- package/dist/index.js +796 -21
- package/dist/index.js.map +1 -1
- package/dist/{test-seams-DbGu8K5F.d.ts → test-seams-CuLNcDew.d.ts} +214 -348
- package/dist/test-seams.d.ts +1 -1
- package/dist/test-seams.js +1 -1
- package/package.json +6 -5
- package/dist/chunk-L6UJ4RQK.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { BREAK_ALLOWED, BREAK_MANDATORY,
|
|
3
|
-
import { resolveScreenLength } from '@weasel-js/paint';
|
|
4
|
-
import { faceMetricsOf, resolveFontVariant, faceMetricsFor, cssFamilyName } from '@weasel-js/font';
|
|
1
|
+
import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, tabAdvance, widthWithTabs, layoutRuns } from './chunk-Z6YRJ33J.js';
|
|
2
|
+
export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, cachedLayoutRuns, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle } from './chunk-Z6YRJ33J.js';
|
|
3
|
+
import { resolveScreenLength, fillHasInk, strokeHasInk, paintKey, strokeKey } from '@weasel-js/paint';
|
|
4
|
+
import { faceMetricsOf, resolveFontVariant, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName } from '@weasel-js/font';
|
|
5
|
+
import { MIXED, isPlainObject } from '@weasel-js/registry';
|
|
6
|
+
export { MIXED } from '@weasel-js/registry';
|
|
7
|
+
import { transformPath, poseFrameOf, pathFromD, pathSignedArea, reversePath, PATH_M, PATH_L, PATH_Z } from '@weasel-js/geom';
|
|
5
8
|
|
|
6
9
|
// src/runs.ts
|
|
7
10
|
function toRuns(input) {
|
|
@@ -52,6 +55,7 @@ function runsToMarkdown(runs, grammar = MARKDOWN_RUN_GRAMMAR) {
|
|
|
52
55
|
const chars = escapees(grammar);
|
|
53
56
|
let out = "";
|
|
54
57
|
for (const r of runs) {
|
|
58
|
+
if (r.text.length === 0) continue;
|
|
55
59
|
const escaped = escapeFor(r.text, chars);
|
|
56
60
|
const on = flagsOf(r, grammar);
|
|
57
61
|
if (on.size === 0) {
|
|
@@ -104,7 +108,12 @@ function markdownToRuns(input, grammar = MARKDOWN_RUN_GRAMMAR) {
|
|
|
104
108
|
if (markers) {
|
|
105
109
|
let count = 0;
|
|
106
110
|
while (i + count < input.length && input[i + count] === ch) count++;
|
|
107
|
-
const marker = markers.find((m) => m.repeat <= count)
|
|
111
|
+
const marker = markers.find((m) => m.repeat <= count);
|
|
112
|
+
if (!marker) {
|
|
113
|
+
buf += ch.repeat(count);
|
|
114
|
+
i += count;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
108
117
|
flush();
|
|
109
118
|
for (const f of marker.flags) {
|
|
110
119
|
if (active.has(f)) active.delete(f);
|
|
@@ -346,7 +355,7 @@ function resolveRuns(runs, style, viewScale = 1) {
|
|
|
346
355
|
...sizeMap ? { sizeMap } : {},
|
|
347
356
|
fontWeight,
|
|
348
357
|
fontStyle,
|
|
349
|
-
fill: run.fill
|
|
358
|
+
fill: run.fill !== void 0 ? run.fill : style.fill,
|
|
350
359
|
// Unlike the decorations below, a run's stroke *replaces* the node's
|
|
351
360
|
// rather than adding to it — there is only one outline to paint.
|
|
352
361
|
...(run.stroke ?? style.stroke) !== void 0 ? { stroke: run.stroke ?? style.stroke } : {},
|
|
@@ -362,6 +371,66 @@ function resolveRuns(runs, style, viewScale = 1) {
|
|
|
362
371
|
return out;
|
|
363
372
|
}
|
|
364
373
|
|
|
374
|
+
// src/layout/decorationMetrics.ts
|
|
375
|
+
var DECORATION_KINDS = ["underline", "strikethrough", "overline"];
|
|
376
|
+
var DEFAULT_DECORATION_METRICS = Object.freeze({
|
|
377
|
+
underline: Object.freeze({ offset: 0.1, thickness: 0.05 }),
|
|
378
|
+
strikethrough: Object.freeze({ offset: -0.3, thickness: 0.05 }),
|
|
379
|
+
overline: Object.freeze({ offset: -0.9, thickness: 0.05 })
|
|
380
|
+
});
|
|
381
|
+
var resolved = /* @__PURE__ */ new WeakMap();
|
|
382
|
+
function decorationMetrics(face) {
|
|
383
|
+
if (!face) return DEFAULT_DECORATION_METRICS;
|
|
384
|
+
let out = resolved.get(face);
|
|
385
|
+
if (!out) {
|
|
386
|
+
const underline = face.underline ?? DEFAULT_DECORATION_METRICS.underline;
|
|
387
|
+
out = Object.freeze({
|
|
388
|
+
underline,
|
|
389
|
+
strikethrough: face.strikethrough ?? DEFAULT_DECORATION_METRICS.strikethrough,
|
|
390
|
+
overline: Object.freeze({ offset: DEFAULT_DECORATION_METRICS.overline.offset, thickness: underline.thickness })
|
|
391
|
+
});
|
|
392
|
+
resolved.set(face, out);
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
function decorationRule(kind, baselineY, fontSize, face) {
|
|
397
|
+
const m = decorationMetrics(face)[kind];
|
|
398
|
+
const y0 = baselineY + fontSize * m.offset;
|
|
399
|
+
return { y0, y1: y0 + fontSize * m.thickness };
|
|
400
|
+
}
|
|
401
|
+
function lineDecorations(glyphs, runs, faceOf, paintOf) {
|
|
402
|
+
const rules = [];
|
|
403
|
+
let open = null;
|
|
404
|
+
const close = () => {
|
|
405
|
+
const s = open;
|
|
406
|
+
open = null;
|
|
407
|
+
if (!s || s.x1 <= s.x0) return;
|
|
408
|
+
for (const kind of DECORATION_KINDS) {
|
|
409
|
+
if (!s.style[kind]) continue;
|
|
410
|
+
const { y0, y1 } = decorationRule(kind, s.baselineY, s.style.fontSize, s.face);
|
|
411
|
+
rules.push({ kind, x0: s.x0, y0, x1: s.x1, y1, run: s.run });
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
for (const g of glyphs) {
|
|
415
|
+
const style = runs[g.run];
|
|
416
|
+
const paint = paintOf(g.run);
|
|
417
|
+
if (paint === null || !DECORATION_KINDS.some((kind) => style[kind])) {
|
|
418
|
+
close();
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const face = faceOf(g.run);
|
|
422
|
+
const joins = open !== null && DECORATION_KINDS.every((kind) => open.style[kind] === style[kind]) && open.style.fontSize === style.fontSize && open.face === face && open.baselineY === g.baselineY && open.paint === paint;
|
|
423
|
+
if (joins) {
|
|
424
|
+
open.x1 = g.x + g.step;
|
|
425
|
+
} else {
|
|
426
|
+
close();
|
|
427
|
+
open = { run: g.run, style, face, paint, baselineY: g.baselineY, x0: g.x, x1: g.x + g.step };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
close();
|
|
431
|
+
return rules;
|
|
432
|
+
}
|
|
433
|
+
|
|
365
434
|
// src/measure/verticalAlign.ts
|
|
366
435
|
function verticalAlignOffset(align, boxHeight, textHeight) {
|
|
367
436
|
if (align === void 0 || align === "top" || boxHeight === void 0) return 0;
|
|
@@ -380,7 +449,9 @@ function textPoseLayoutInput(pose, scale = 1) {
|
|
|
380
449
|
maxWidth: style.wrap ? pose.width : Infinity,
|
|
381
450
|
alignWidth: pose.width,
|
|
382
451
|
lineHeight: style.lineHeight,
|
|
383
|
-
align:
|
|
452
|
+
align: style.align,
|
|
453
|
+
direction: style.direction,
|
|
454
|
+
tabSize: style.tabSize,
|
|
384
455
|
justify: style.align === "justify"
|
|
385
456
|
}
|
|
386
457
|
};
|
|
@@ -394,6 +465,135 @@ function layoutTextPose(pose, scale = 1) {
|
|
|
394
465
|
y: pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height)
|
|
395
466
|
};
|
|
396
467
|
}
|
|
468
|
+
function glyphOutlineD(g, run, minSize) {
|
|
469
|
+
if (g.glyph?.color) return null;
|
|
470
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
471
|
+
if (g.resolved.source === "outline") return glyphOutline(family, weight, style, g.cp);
|
|
472
|
+
if (g.glyph === null) return null;
|
|
473
|
+
if (minSize === void 0) return null;
|
|
474
|
+
if (run.fontSize < minSize && !strokeHasInk(run.stroke)) return null;
|
|
475
|
+
if (g.resolved.synthetic.bold) return null;
|
|
476
|
+
return glyphOutline(family, weight, style, g.cp);
|
|
477
|
+
}
|
|
478
|
+
function glyphQuad(g) {
|
|
479
|
+
const { font, glyph } = g;
|
|
480
|
+
if (font === null || glyph === null || glyph.width === 0 || glyph.page < 0) return null;
|
|
481
|
+
const scale = g.fontSize / g.metrics.size;
|
|
482
|
+
const x0 = g.x + glyph.xoffset * scale;
|
|
483
|
+
const y0 = g.baselineY + (glyph.yoffset - g.metrics.base) * scale;
|
|
484
|
+
return {
|
|
485
|
+
x0,
|
|
486
|
+
y0,
|
|
487
|
+
x1: x0 + glyph.width * scale,
|
|
488
|
+
y1: y0 + glyph.height * scale,
|
|
489
|
+
u0: glyph.x / font.common.scaleW,
|
|
490
|
+
v0: glyph.y / font.common.scaleH,
|
|
491
|
+
u1: (glyph.x + glyph.width) / font.common.scaleW,
|
|
492
|
+
v1: (glyph.y + glyph.height) / font.common.scaleH,
|
|
493
|
+
baselineY: g.baselineY
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function runFaces(runs) {
|
|
497
|
+
const faces = runs.map((r) => faceMetricsOf(resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle)));
|
|
498
|
+
return (run) => faces[run];
|
|
499
|
+
}
|
|
500
|
+
function groupFor(groups, g, run, source) {
|
|
501
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
502
|
+
const { synthetic } = g.resolved;
|
|
503
|
+
const page = source === "outline" ? 0 : g.glyph.page;
|
|
504
|
+
const stroke = source === "outline" && strokeHasInk(run.stroke) ? run.stroke : void 0;
|
|
505
|
+
const key = `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${paintKey(run.fill?.paint)}|${strokeKey(stroke)}|${source}|${page}`;
|
|
506
|
+
let group = groups.get(key);
|
|
507
|
+
if (!group) {
|
|
508
|
+
group = {
|
|
509
|
+
family,
|
|
510
|
+
weight,
|
|
511
|
+
style,
|
|
512
|
+
synthetic: { ...synthetic },
|
|
513
|
+
source,
|
|
514
|
+
page,
|
|
515
|
+
fill: run.fill,
|
|
516
|
+
...stroke ? { stroke } : {},
|
|
517
|
+
quads: [],
|
|
518
|
+
glyphs: []
|
|
519
|
+
};
|
|
520
|
+
groups.set(key, group);
|
|
521
|
+
}
|
|
522
|
+
return group;
|
|
523
|
+
}
|
|
524
|
+
function paintRuns(laid, runs, opts = {}) {
|
|
525
|
+
const groups = /* @__PURE__ */ new Map();
|
|
526
|
+
const decorations = [];
|
|
527
|
+
const faceOf = runFaces(runs);
|
|
528
|
+
const rulePaint = (run) => fillHasInk(runs[run].fill) ? paintKey(runs[run].fill.paint) : null;
|
|
529
|
+
for (const line of laid.lines) {
|
|
530
|
+
for (const g of line.glyphs) {
|
|
531
|
+
const run = runs[g.run];
|
|
532
|
+
const d = glyphOutlineD(g, run, opts.outlineMinSize);
|
|
533
|
+
if (!fillHasInk(run.fill) && !(d !== null && strokeHasInk(run.stroke))) continue;
|
|
534
|
+
if (d !== null) {
|
|
535
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
536
|
+
groupFor(groups, g, run, "outline").glyphs.push({
|
|
537
|
+
d,
|
|
538
|
+
key: `${family}|${weight}|${style}|${g.cp}`,
|
|
539
|
+
x: g.x,
|
|
540
|
+
baselineY: g.baselineY,
|
|
541
|
+
scale: g.fontSize
|
|
542
|
+
});
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const quad = glyphQuad(g);
|
|
546
|
+
if (quad) groupFor(groups, g, run, g.glyph?.color ? "color" : g.resolved.source).quads.push(quad);
|
|
547
|
+
}
|
|
548
|
+
for (const rule of lineDecorations(line.glyphs, runs, faceOf, rulePaint)) {
|
|
549
|
+
const { run, ...rect } = rule;
|
|
550
|
+
decorations.push({ ...rect, fill: runs[run].fill });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { groups: [...groups.values()], decorations };
|
|
554
|
+
}
|
|
555
|
+
var PAINT_CACHE_VARIANT_LIMIT = 32;
|
|
556
|
+
var own = /* @__PURE__ */ new WeakMap();
|
|
557
|
+
var shared = /* @__PURE__ */ new WeakMap();
|
|
558
|
+
function outlineBucket(runs, min) {
|
|
559
|
+
if (min === void 0) return -1;
|
|
560
|
+
const sizes = /* @__PURE__ */ new Set();
|
|
561
|
+
for (const r of runs) sizes.add(r.fontSize);
|
|
562
|
+
let n = 0;
|
|
563
|
+
for (const size of sizes) if (size >= min) n++;
|
|
564
|
+
return n;
|
|
565
|
+
}
|
|
566
|
+
function variantKey(runs, opts) {
|
|
567
|
+
let out = `${outlineBucket(runs, opts.outlineMinSize)}`;
|
|
568
|
+
for (const r of runs) {
|
|
569
|
+
out += `|${r.underline ? 1 : 0}${r.strikethrough ? 1 : 0}${r.overline ? 1 : 0}${paintKey(r.fill?.paint)}|${strokeKey(r.stroke)}`;
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
function cachedPaintRuns(laid, runs, opts = {}) {
|
|
574
|
+
const generation = glyphGeneration();
|
|
575
|
+
const key = variantKey(runs, opts);
|
|
576
|
+
const fresh = (p) => p !== void 0 && p.generation === generation && p.key === key;
|
|
577
|
+
let byLayout = own.get(runs);
|
|
578
|
+
if (byLayout === void 0) {
|
|
579
|
+
byLayout = /* @__PURE__ */ new WeakMap();
|
|
580
|
+
own.set(runs, byLayout);
|
|
581
|
+
}
|
|
582
|
+
const mine = byLayout.get(laid);
|
|
583
|
+
if (fresh(mine)) return mine.painted;
|
|
584
|
+
let byKey = shared.get(laid);
|
|
585
|
+
if (byKey === void 0) {
|
|
586
|
+
byKey = /* @__PURE__ */ new Map();
|
|
587
|
+
shared.set(laid, byKey);
|
|
588
|
+
}
|
|
589
|
+
let painting = byKey.get(key);
|
|
590
|
+
byKey.delete(key);
|
|
591
|
+
if (!fresh(painting)) painting = { generation, key, painted: paintRuns(laid, runs, opts) };
|
|
592
|
+
if (byKey.size >= PAINT_CACHE_VARIANT_LIMIT) byKey.delete(byKey.keys().next().value);
|
|
593
|
+
byKey.set(key, painting);
|
|
594
|
+
byLayout.set(laid, painting);
|
|
595
|
+
return painting.painted;
|
|
596
|
+
}
|
|
397
597
|
|
|
398
598
|
// src/measure/measureText.ts
|
|
399
599
|
function measuredWidth(ctx, text, style) {
|
|
@@ -409,21 +609,40 @@ function measureText(ctx, text, maxWidth, style) {
|
|
|
409
609
|
u += ch.length;
|
|
410
610
|
}
|
|
411
611
|
at.push(u);
|
|
412
|
-
const
|
|
413
|
-
const
|
|
612
|
+
const flatWidth = (start, end) => measuredWidth(ctx, text.slice(at[start], at[end]), style);
|
|
613
|
+
const tabs = [];
|
|
614
|
+
cps.forEach((cp, k) => {
|
|
615
|
+
if (cp === 9) tabs.push(k);
|
|
616
|
+
});
|
|
617
|
+
const space = tabs.length === 0 ? 0 : ctx.measureText(" ").width;
|
|
618
|
+
const tab = { stop: style.tabSize * (space + style.letterSpacing), min: space / 2 };
|
|
619
|
+
const widthOf = (start, end, piece) => widthWithTabs(start, end, tabs, () => tab, flatWidth, piece);
|
|
620
|
+
const wrapped = wrapLines(cps, maxWidth, (start, end) => widthOf(start, end));
|
|
621
|
+
const lines = [];
|
|
622
|
+
const spans = [];
|
|
623
|
+
for (const l of wrapped) {
|
|
624
|
+
let end = l.end;
|
|
625
|
+
while (end > l.start && hangs(cps[end - 1])) end--;
|
|
626
|
+
lines.push(text.slice(at[l.start], at[end]));
|
|
627
|
+
const row = [];
|
|
628
|
+
widthOf(l.start, end, (from, to, x) => row.push({ text: text.slice(at[from], at[to]), x }));
|
|
629
|
+
spans.push(row);
|
|
630
|
+
}
|
|
414
631
|
const lineStarts = wrapped.map((l) => at[l.start]);
|
|
415
632
|
const height = lines.length * style.fontSize * style.lineHeight;
|
|
416
|
-
return { lines, lineStarts, height };
|
|
633
|
+
return { lines, spans, lineStarts, height };
|
|
417
634
|
}
|
|
418
635
|
|
|
419
636
|
// src/measure/measureTextBounds.ts
|
|
420
637
|
function measureTextBounds(text, style, opts) {
|
|
421
|
-
const
|
|
422
|
-
const runs = resolveRuns([{ text }],
|
|
638
|
+
const resolved2 = resolveTextStyle(style, void 0, opts?.scale);
|
|
639
|
+
const runs = resolveRuns([{ text }], resolved2, opts?.scale);
|
|
423
640
|
const { bounds } = cachedLayoutRuns(runs, {
|
|
424
641
|
maxWidth: opts?.maxWidth ?? Infinity,
|
|
425
|
-
lineHeight: opts?.lineHeight ??
|
|
426
|
-
align:
|
|
642
|
+
lineHeight: opts?.lineHeight ?? resolved2.lineHeight,
|
|
643
|
+
align: resolved2.align,
|
|
644
|
+
direction: resolved2.direction,
|
|
645
|
+
tabSize: resolved2.tabSize
|
|
427
646
|
});
|
|
428
647
|
return { width: bounds.width, height: bounds.height };
|
|
429
648
|
}
|
|
@@ -444,6 +663,115 @@ function textLineBoxes(pose, opts = {}) {
|
|
|
444
663
|
}
|
|
445
664
|
return out;
|
|
446
665
|
}
|
|
666
|
+
|
|
667
|
+
// src/hitTest.ts
|
|
668
|
+
function caretCells(cells) {
|
|
669
|
+
const out = [];
|
|
670
|
+
for (const c of cells) {
|
|
671
|
+
const prev = out[out.length - 1];
|
|
672
|
+
if (prev && (!prev.clusterEnd || prev.srcIndex === c.srcIndex && prev.srcEnd === c.srcEnd)) {
|
|
673
|
+
const x0 = Math.min(prev.x, c.x);
|
|
674
|
+
const x1 = Math.max(prev.x + prev.advance, c.x + c.advance);
|
|
675
|
+
out[out.length - 1] = {
|
|
676
|
+
...prev,
|
|
677
|
+
x: x0,
|
|
678
|
+
advance: x1 - x0,
|
|
679
|
+
srcIndex: Math.min(prev.srcIndex, c.srcIndex),
|
|
680
|
+
srcEnd: Math.max(prev.srcEnd, c.srcEnd),
|
|
681
|
+
clusterEnd: c.clusterEnd
|
|
682
|
+
};
|
|
683
|
+
} else {
|
|
684
|
+
out.push(c);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
function pointInTextPose(x, y, pose, opts = {}) {
|
|
690
|
+
const p = opts.padding ?? 0;
|
|
691
|
+
return x >= pose.x - p && x <= pose.x + pose.width + p && y >= pose.y - p && y <= pose.y + pose.height + p;
|
|
692
|
+
}
|
|
693
|
+
function caretIndexAt(x, y, pose) {
|
|
694
|
+
const { laid, x: dx, y: dy } = layoutTextPose(pose);
|
|
695
|
+
const lines = laid.lines;
|
|
696
|
+
if (lines.length === 0) return 0;
|
|
697
|
+
const last = lines[lines.length - 1];
|
|
698
|
+
if (y < dy + lines[0].y0) return lines[0].cells[0]?.srcIndex ?? lines[0].srcEnd;
|
|
699
|
+
if (y >= dy + last.y1) return last.srcEnd;
|
|
700
|
+
let line = last;
|
|
701
|
+
for (const candidate of lines) {
|
|
702
|
+
if (y < dy + candidate.y1) {
|
|
703
|
+
line = candidate;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (line.cells.length === 0) return line.srcEnd;
|
|
708
|
+
const cells = caretCells(line.cells);
|
|
709
|
+
const visual = cells.map((_, i) => i).sort((a, b) => cells[a].x - cells[b].x);
|
|
710
|
+
for (const i of visual) {
|
|
711
|
+
const c = cells[i];
|
|
712
|
+
if (x >= dx + c.x + c.advance / 2) continue;
|
|
713
|
+
return c.level % 2 === 1 ? c.srcEnd : c.srcIndex;
|
|
714
|
+
}
|
|
715
|
+
const trailing = cells[visual[visual.length - 1]];
|
|
716
|
+
return trailing.level % 2 === 1 ? trailing.srcIndex : trailing.srcEnd;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/fitTextPose.ts
|
|
720
|
+
function fitTextPose(pose, opts = {}) {
|
|
721
|
+
const axis = opts.axis ?? "height";
|
|
722
|
+
const { padX, padY } = resolvePadding(opts.padding);
|
|
723
|
+
const { bounds } = layoutTextPose(axis === "height" ? { ...pose, width: pose.width - padX * 2 } : { ...pose, style: { ...pose.style, wrap: false } }).laid;
|
|
724
|
+
if (axis === "height") return { ...pose, height: bounds.height + padY * 2 };
|
|
725
|
+
return {
|
|
726
|
+
...pose,
|
|
727
|
+
width: bounds.width + padX * 2,
|
|
728
|
+
height: bounds.height + padY * 2
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function resolvePadding(p) {
|
|
732
|
+
if (p == null) return { padX: 0, padY: 0 };
|
|
733
|
+
if (typeof p === "number") return { padX: p, padY: p };
|
|
734
|
+
return { padX: p.x ?? 0, padY: p.y ?? 0 };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/renderLabel.ts
|
|
738
|
+
function renderLabel(ctx, text, x, y, options = {}) {
|
|
739
|
+
const {
|
|
740
|
+
align = "center",
|
|
741
|
+
fontSize = 13,
|
|
742
|
+
renderText,
|
|
743
|
+
width,
|
|
744
|
+
height,
|
|
745
|
+
padX = 4,
|
|
746
|
+
padY = 1,
|
|
747
|
+
cornerRadius = 3
|
|
748
|
+
} = options;
|
|
749
|
+
ctx.save();
|
|
750
|
+
ctx.font = `${fontSize}px sans-serif`;
|
|
751
|
+
ctx.textBaseline = "top";
|
|
752
|
+
const contentW = width ?? ctx.measureText(text).width;
|
|
753
|
+
const contentH = height ?? fontSize;
|
|
754
|
+
const w = contentW + padX * 2;
|
|
755
|
+
const h = contentH + padY * 2;
|
|
756
|
+
const rx = align === "center" ? x - w / 2 : x - padX;
|
|
757
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
|
|
758
|
+
ctx.beginPath();
|
|
759
|
+
ctx.roundRect(rx, y - padY, w, h, cornerRadius);
|
|
760
|
+
ctx.fill();
|
|
761
|
+
ctx.textAlign = align === "center" ? "center" : "left";
|
|
762
|
+
const tx = align === "center" ? x : x;
|
|
763
|
+
(renderText ?? defaultLabelTextRenderer)(ctx, text, tx, y);
|
|
764
|
+
ctx.restore();
|
|
765
|
+
}
|
|
766
|
+
var defaultLabelTextRenderer = (ctx, text, x, y) => {
|
|
767
|
+
ctx.fillStyle = "#FFFFFF";
|
|
768
|
+
ctx.fillText(text, x, y);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// src/isTextData.ts
|
|
772
|
+
function isTextData(data) {
|
|
773
|
+
return typeof data === "object" && data !== null && typeof data.text === "string";
|
|
774
|
+
}
|
|
447
775
|
function runMetrics(run, fontSize, face) {
|
|
448
776
|
const script = run.script ? scriptMetrics(face)[run.script] : void 0;
|
|
449
777
|
const scale = run.fontScale ?? script?.size ?? 1;
|
|
@@ -455,7 +783,7 @@ function runMetrics(run, fontSize, face) {
|
|
|
455
783
|
y: shiftEm === 0 ? 0 : -shiftEm * fontSize
|
|
456
784
|
};
|
|
457
785
|
}
|
|
458
|
-
function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.3, faceOf) {
|
|
786
|
+
function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.3, faceOf, tabSize = DEFAULT_TAB_SIZE) {
|
|
459
787
|
if (runs.length === 0) return { lines: [], width: 0, height: 0 };
|
|
460
788
|
const shown = transformRunTexts(runs.map((r) => r.text), runs.map((r) => r.textTransform ?? "none"));
|
|
461
789
|
const segs = [];
|
|
@@ -506,7 +834,7 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
|
|
|
506
834
|
const out = [];
|
|
507
835
|
for (let k = from; k < to; k++) {
|
|
508
836
|
const last = out[out.length - 1];
|
|
509
|
-
if (last && last.run === runOf[k] && last.end === startOf[k]) {
|
|
837
|
+
if (last && last.run === runOf[k] && last.end === startOf[k] && cps[k] !== 9 && cps[k - 1] !== 9) {
|
|
510
838
|
out[out.length - 1] = { ...last, end: endOf[k] };
|
|
511
839
|
} else {
|
|
512
840
|
out.push({ run: runOf[k], start: startOf[k], end: endOf[k] });
|
|
@@ -521,19 +849,31 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
|
|
|
521
849
|
const positioned = [];
|
|
522
850
|
for (const p of line) {
|
|
523
851
|
const { run, face, size, y, runSize } = placed[p.run];
|
|
524
|
-
const
|
|
852
|
+
const tab = run.text[p.start] === " " ? stopOf(p.run) : null;
|
|
853
|
+
const width2 = tab ? tabAdvance(x, tab.stop, tab.min) : pieceWidth(p);
|
|
525
854
|
positioned.push({ ...run, text: run.text.slice(p.start, p.end), x, width: width2, y, size, ...face ? { face } : {} });
|
|
526
855
|
x += width2;
|
|
527
856
|
maxSize = Math.max(maxSize, runSize);
|
|
528
857
|
}
|
|
529
858
|
lines.push({ runs: positioned, width: x, height: (maxSize > 0 ? maxSize : fontSize) * lineHeightFactor });
|
|
530
859
|
}
|
|
531
|
-
const
|
|
860
|
+
const stopOf = (run) => {
|
|
861
|
+
const { run: r, size } = placed[run];
|
|
862
|
+
const space = measure(" ", size, r.bold ?? false, r.italic ?? false);
|
|
863
|
+
return { stop: tabSize * space, min: space / 2 };
|
|
864
|
+
};
|
|
865
|
+
const tabs = [];
|
|
866
|
+
cps.forEach((cp, k) => {
|
|
867
|
+
if (cp === 9) tabs.push(k);
|
|
868
|
+
});
|
|
869
|
+
const flatWidth = (start, end) => piecesOf(start, end).reduce((w, p) => w + pieceWidth(p), 0);
|
|
870
|
+
const widthOf = (start, end) => widthWithTabs(start, end, tabs, (k) => stopOf(runOf[k]), flatWidth);
|
|
532
871
|
for (const line of wrapLines(cps, maxWidth, widthOf)) {
|
|
533
872
|
let end = line.end;
|
|
534
|
-
if (line.wrapped) while (end > line.start && cps[end - 1]
|
|
873
|
+
if (line.wrapped) while (end > line.start && hangs(cps[end - 1])) end--;
|
|
535
874
|
commitLine(piecesOf(line.start, end));
|
|
536
875
|
}
|
|
876
|
+
if (lines.length === 0) return { lines: [], width: 0, height: 0 };
|
|
537
877
|
const width = Math.max(...lines.map((l) => l.width));
|
|
538
878
|
const height = lines.reduce((sum, l) => sum + l.height, 0);
|
|
539
879
|
return { lines, width, height };
|
|
@@ -584,7 +924,7 @@ function createMarkdownRenderer(ctx, text, fontSize, maxWidth = Infinity, fontOp
|
|
|
584
924
|
const family = fontOpts.family ?? "sans-serif";
|
|
585
925
|
const weight = numericWeight(fontOpts.weight ?? 400);
|
|
586
926
|
const faceOf = (bold, italic) => faceMetricsFor(family, bold ? 700 : weight, italic ? "italic" : "normal");
|
|
587
|
-
const layout = layoutMarkdown(parsed, maxWidth, fontSize, measure, fontOpts.lineHeight, faceOf);
|
|
927
|
+
const layout = layoutMarkdown(parsed, maxWidth, fontSize, measure, fontOpts.lineHeight, faceOf, fontOpts.tabSize);
|
|
588
928
|
function walk(c, x, y, drawRun) {
|
|
589
929
|
const align = c.textAlign;
|
|
590
930
|
const left = blockLeft(c, x, layout.width);
|
|
@@ -642,7 +982,442 @@ function createMarkdownRenderer(ctx, text, fontSize, maxWidth = Infinity, fontOp
|
|
|
642
982
|
};
|
|
643
983
|
return { renderer, strokeRenderer, width: layout.width, height: layout.height };
|
|
644
984
|
}
|
|
985
|
+
var STYLE_KEYS = [
|
|
986
|
+
"bold",
|
|
987
|
+
"fontWeight",
|
|
988
|
+
"italic",
|
|
989
|
+
"underline",
|
|
990
|
+
"strikethrough",
|
|
991
|
+
"overline",
|
|
992
|
+
"fontFamily",
|
|
993
|
+
"fontSize",
|
|
994
|
+
"letterSpacing",
|
|
995
|
+
"fill",
|
|
996
|
+
"stroke",
|
|
997
|
+
"script",
|
|
998
|
+
"baselineShift",
|
|
999
|
+
"fontScale",
|
|
1000
|
+
"textTransform",
|
|
1001
|
+
"fontVariantCaps"
|
|
1002
|
+
];
|
|
1003
|
+
var FLAG_KEYS = /* @__PURE__ */ new Set([
|
|
1004
|
+
"bold",
|
|
1005
|
+
"italic",
|
|
1006
|
+
"underline",
|
|
1007
|
+
"strikethrough",
|
|
1008
|
+
"overline"
|
|
1009
|
+
]);
|
|
1010
|
+
function deepEqual(a, b) {
|
|
1011
|
+
if (Object.is(a, b)) return true;
|
|
1012
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1013
|
+
return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
1014
|
+
}
|
|
1015
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
1016
|
+
const keys = Object.keys(a);
|
|
1017
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
1018
|
+
return keys.every((k) => k in b && deepEqual(a[k], b[k]));
|
|
1019
|
+
}
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
function valueAt(run, key) {
|
|
1023
|
+
const value = run[key];
|
|
1024
|
+
if (FLAG_KEYS.has(key)) return value ?? false;
|
|
1025
|
+
return value;
|
|
1026
|
+
}
|
|
1027
|
+
var STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["fill", "stroke"]);
|
|
1028
|
+
function sameValue(key, a, b) {
|
|
1029
|
+
return STRUCTURAL_KEYS.has(key) ? deepEqual(a, b) : Object.is(a, b);
|
|
1030
|
+
}
|
|
1031
|
+
function sameStyle(a, b) {
|
|
1032
|
+
return STYLE_KEYS.every((key) => sameValue(key, valueAt(a, key), valueAt(b, key)));
|
|
1033
|
+
}
|
|
1034
|
+
function totalLength(runs) {
|
|
1035
|
+
let n = 0;
|
|
1036
|
+
for (const run of runs) n += run.text.length;
|
|
1037
|
+
return n;
|
|
1038
|
+
}
|
|
1039
|
+
function styleAtRange(runs, start, end) {
|
|
1040
|
+
const lo = Math.max(0, start);
|
|
1041
|
+
const hi = end;
|
|
1042
|
+
if (!(lo < hi)) return {};
|
|
1043
|
+
const first = /* @__PURE__ */ new Map();
|
|
1044
|
+
const mixed = /* @__PURE__ */ new Set();
|
|
1045
|
+
let seen = false;
|
|
1046
|
+
let pos = 0;
|
|
1047
|
+
for (const run of runs) {
|
|
1048
|
+
const runEnd = pos + run.text.length;
|
|
1049
|
+
const a = Math.max(pos, lo);
|
|
1050
|
+
const b = Math.min(runEnd, hi);
|
|
1051
|
+
pos = runEnd;
|
|
1052
|
+
if (!(a < b)) continue;
|
|
1053
|
+
for (const key of STYLE_KEYS) {
|
|
1054
|
+
const value = valueAt(run, key);
|
|
1055
|
+
if (!seen) first.set(key, value);
|
|
1056
|
+
else if (!mixed.has(key) && !sameValue(key, first.get(key), value)) {
|
|
1057
|
+
mixed.add(key);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
seen = true;
|
|
1061
|
+
}
|
|
1062
|
+
if (!seen) return {};
|
|
1063
|
+
const style = {};
|
|
1064
|
+
for (const key of STYLE_KEYS) {
|
|
1065
|
+
if (mixed.has(key)) style[key] = MIXED;
|
|
1066
|
+
else {
|
|
1067
|
+
const value = first.get(key);
|
|
1068
|
+
if (value !== void 0) style[key] = value;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return style;
|
|
1072
|
+
}
|
|
1073
|
+
function runsCarryStyling(runs) {
|
|
1074
|
+
return runs.some(
|
|
1075
|
+
(run) => STYLE_KEYS.some((key) => {
|
|
1076
|
+
const value = run[key];
|
|
1077
|
+
return value !== void 0 && value !== false;
|
|
1078
|
+
})
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
function supersededKey(patch) {
|
|
1082
|
+
if ("bold" in patch && !("fontWeight" in patch)) return "fontWeight";
|
|
1083
|
+
if (patch.fontWeight !== void 0 && !("bold" in patch)) return "bold";
|
|
1084
|
+
return void 0;
|
|
1085
|
+
}
|
|
1086
|
+
function patchRangeStyle(range, patch) {
|
|
1087
|
+
const out = { ...range };
|
|
1088
|
+
const superseded = supersededKey(patch);
|
|
1089
|
+
if (superseded !== void 0) delete out[superseded];
|
|
1090
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1091
|
+
if (value !== void 0) out[key] = value;
|
|
1092
|
+
}
|
|
1093
|
+
return out;
|
|
1094
|
+
}
|
|
1095
|
+
function patchRun(run, patch) {
|
|
1096
|
+
const next = { ...run };
|
|
1097
|
+
const superseded = supersededKey(patch);
|
|
1098
|
+
if (superseded !== void 0) delete next[superseded];
|
|
1099
|
+
for (const key of STYLE_KEYS) {
|
|
1100
|
+
if (!(key in patch)) continue;
|
|
1101
|
+
const value = patch[key];
|
|
1102
|
+
if (value === void 0 || value === false) delete next[key];
|
|
1103
|
+
else next[key] = value;
|
|
1104
|
+
}
|
|
1105
|
+
return next;
|
|
1106
|
+
}
|
|
1107
|
+
function coalesceRuns(runs) {
|
|
1108
|
+
const out = [];
|
|
1109
|
+
for (const run of runs) {
|
|
1110
|
+
if (run.text.length === 0) continue;
|
|
1111
|
+
const prev = out[out.length - 1];
|
|
1112
|
+
if (prev && sameStyle(prev, run)) prev.text += run.text;
|
|
1113
|
+
else out.push({ ...run });
|
|
1114
|
+
}
|
|
1115
|
+
return out;
|
|
1116
|
+
}
|
|
1117
|
+
function applyStyleToRange(runs, start, end, patch) {
|
|
1118
|
+
const lo = Math.max(0, start);
|
|
1119
|
+
const hi = Math.min(totalLength(runs), end);
|
|
1120
|
+
if (!(lo < hi)) return coalesceRuns(runs);
|
|
1121
|
+
const out = [];
|
|
1122
|
+
let pos = 0;
|
|
1123
|
+
for (const run of runs) {
|
|
1124
|
+
const runEnd = pos + run.text.length;
|
|
1125
|
+
const a = Math.max(pos, lo);
|
|
1126
|
+
const b = Math.min(runEnd, hi);
|
|
1127
|
+
if (a < b) {
|
|
1128
|
+
if (pos < a) out.push({ ...run, text: run.text.slice(0, a - pos) });
|
|
1129
|
+
out.push(patchRun({ ...run, text: run.text.slice(a - pos, b - pos) }, patch));
|
|
1130
|
+
if (b < runEnd) out.push({ ...run, text: run.text.slice(b - pos) });
|
|
1131
|
+
} else {
|
|
1132
|
+
out.push({ ...run });
|
|
1133
|
+
}
|
|
1134
|
+
pos = runEnd;
|
|
1135
|
+
}
|
|
1136
|
+
return coalesceRuns(out);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/runs/flagRange.ts
|
|
1140
|
+
function nodeHasFlag(style, key) {
|
|
1141
|
+
switch (key) {
|
|
1142
|
+
case "bold":
|
|
1143
|
+
return style.fontWeight !== void 0 && isBoldWeight(style.fontWeight);
|
|
1144
|
+
case "italic":
|
|
1145
|
+
return style.fontStyle === "italic";
|
|
1146
|
+
case "underline":
|
|
1147
|
+
return style.underline === true;
|
|
1148
|
+
case "strikethrough":
|
|
1149
|
+
return style.strikethrough === true;
|
|
1150
|
+
case "overline":
|
|
1151
|
+
return style.overline === true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function unboldPatch(style) {
|
|
1155
|
+
return nodeHasFlag(style, "bold") ? { bold: false, fontWeight: 400 } : { bold: false };
|
|
1156
|
+
}
|
|
1157
|
+
function clearNodeFlag(style, key) {
|
|
1158
|
+
const next = { ...style };
|
|
1159
|
+
switch (key) {
|
|
1160
|
+
case "italic":
|
|
1161
|
+
next.fontStyle = "normal";
|
|
1162
|
+
break;
|
|
1163
|
+
case "underline":
|
|
1164
|
+
delete next.underline;
|
|
1165
|
+
break;
|
|
1166
|
+
case "strikethrough":
|
|
1167
|
+
delete next.strikethrough;
|
|
1168
|
+
break;
|
|
1169
|
+
case "overline":
|
|
1170
|
+
delete next.overline;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
return next;
|
|
1174
|
+
}
|
|
1175
|
+
function setFlagOverRange(runs, style, start, end, key, value) {
|
|
1176
|
+
if (value || !nodeHasFlag(style, key)) {
|
|
1177
|
+
return { runs: applyStyleToRange(runs, start, end, { [key]: value }), style };
|
|
1178
|
+
}
|
|
1179
|
+
if (key === "bold") {
|
|
1180
|
+
return { runs: applyStyleToRange(runs, start, end, unboldPatch(style)), style };
|
|
1181
|
+
}
|
|
1182
|
+
const total = runs.reduce((n, r) => n + r.text.length, 0);
|
|
1183
|
+
const lo = Math.max(0, start);
|
|
1184
|
+
const hi = Math.min(total, end);
|
|
1185
|
+
let next = applyStyleToRange(runs, 0, total, { [key]: true });
|
|
1186
|
+
if (lo < hi) next = applyStyleToRange(next, lo, hi, { [key]: false });
|
|
1187
|
+
return { runs: next, style: clearNodeFlag(style, key) };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/runs/effectiveRangeStyle.ts
|
|
1191
|
+
var FLAGS = ["italic", "underline", "strikethrough", "overline"];
|
|
1192
|
+
function rangeWeight(range, style) {
|
|
1193
|
+
const node = numericWeight(style?.fontWeight ?? 400);
|
|
1194
|
+
const own2 = range?.fontWeight;
|
|
1195
|
+
if (own2 === MIXED) return { fontWeight: MIXED, bold: MIXED };
|
|
1196
|
+
if (own2 !== void 0) return { fontWeight: own2, bold: isBoldWeight(own2) };
|
|
1197
|
+
const flag = range?.bold;
|
|
1198
|
+
if (flag === MIXED) {
|
|
1199
|
+
if (node === 700) return { fontWeight: 700, bold: true };
|
|
1200
|
+
return { fontWeight: MIXED, bold: isBoldWeight(node) ? true : MIXED };
|
|
1201
|
+
}
|
|
1202
|
+
const weight = flag === true ? 700 : node;
|
|
1203
|
+
return { fontWeight: weight, bold: isBoldWeight(weight) };
|
|
1204
|
+
}
|
|
1205
|
+
var OVERRIDES = [
|
|
1206
|
+
"fontFamily",
|
|
1207
|
+
"fontSize",
|
|
1208
|
+
"letterSpacing",
|
|
1209
|
+
"fill",
|
|
1210
|
+
"script",
|
|
1211
|
+
"baselineShift",
|
|
1212
|
+
"fontScale",
|
|
1213
|
+
"textTransform",
|
|
1214
|
+
"fontVariantCaps"
|
|
1215
|
+
];
|
|
1216
|
+
function effectiveRangeStyle(range, style, paint) {
|
|
1217
|
+
const resolved2 = resolveTextStyle(style, paint);
|
|
1218
|
+
const node = style ?? {};
|
|
1219
|
+
const out = {
|
|
1220
|
+
fontFamily: resolved2.fontFamily,
|
|
1221
|
+
fontSize: resolved2.fontSize,
|
|
1222
|
+
letterSpacing: resolved2.letterSpacing,
|
|
1223
|
+
textTransform: resolved2.textTransform,
|
|
1224
|
+
fontVariantCaps: resolved2.fontVariantCaps
|
|
1225
|
+
};
|
|
1226
|
+
if (resolved2.fill !== null) out.fill = resolved2.fill;
|
|
1227
|
+
Object.assign(out, rangeWeight(range, style));
|
|
1228
|
+
for (const key of FLAGS) {
|
|
1229
|
+
const on = nodeHasFlag(node, key);
|
|
1230
|
+
const run = range?.[key];
|
|
1231
|
+
out[key] = on || run === void 0 ? on : run;
|
|
1232
|
+
}
|
|
1233
|
+
if (range === null) return out;
|
|
1234
|
+
for (const key of OVERRIDES) {
|
|
1235
|
+
if (range[key] !== void 0) out[key] = range[key];
|
|
1236
|
+
}
|
|
1237
|
+
return out;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/syntheticItalic.ts
|
|
1241
|
+
var SYNTHETIC_ITALIC_RADIANS = 0.2094;
|
|
1242
|
+
var TextOutlinesError = class extends Error {
|
|
1243
|
+
reason;
|
|
1244
|
+
family;
|
|
1245
|
+
weight;
|
|
1246
|
+
style;
|
|
1247
|
+
constructor(reason, family, weight, style, detail) {
|
|
1248
|
+
super(`textToPath: "${family}" ${weight}/${style} \u2014 ${detail}`);
|
|
1249
|
+
this.name = "TextOutlinesError";
|
|
1250
|
+
this.reason = reason;
|
|
1251
|
+
this.family = family;
|
|
1252
|
+
this.weight = weight;
|
|
1253
|
+
this.style = style;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
function facesOf(runs) {
|
|
1257
|
+
const out = /* @__PURE__ */ new Map();
|
|
1258
|
+
const add = (family, weight, style) => {
|
|
1259
|
+
out.set(`${family}|${weight}|${style}`, { family, weight, style });
|
|
1260
|
+
};
|
|
1261
|
+
for (const r of runs) {
|
|
1262
|
+
add(r.fontFamily, r.fontWeight, r.fontStyle);
|
|
1263
|
+
const { family, weight, style } = resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle).resolved;
|
|
1264
|
+
add(family, weight, style);
|
|
1265
|
+
}
|
|
1266
|
+
return [...out.values()];
|
|
1267
|
+
}
|
|
1268
|
+
function assertOutlinable(run) {
|
|
1269
|
+
const { fontFamily, fontWeight, fontStyle } = run;
|
|
1270
|
+
const fail = (reason, detail) => {
|
|
1271
|
+
throw new TextOutlinesError(reason, fontFamily, fontWeight, fontStyle, detail);
|
|
1272
|
+
};
|
|
1273
|
+
const requested = outlineStatus(fontFamily, fontWeight, fontStyle);
|
|
1274
|
+
if (requested === "idle" || requested === "loading") {
|
|
1275
|
+
void loadFontOutlines(fontFamily, { weight: fontWeight, style: fontStyle });
|
|
1276
|
+
fail("outlines-loading", "its outlines have not loaded yet; await loadTextOutlines first.");
|
|
1277
|
+
}
|
|
1278
|
+
const r = resolveFontVariant(fontFamily, fontWeight, fontStyle);
|
|
1279
|
+
const { family, weight, style } = r.resolved;
|
|
1280
|
+
if (r.synthetic.bold) {
|
|
1281
|
+
fail("synthetic-bold", `drawn as a faux bold of ${family} ${weight}/${style}, which has no geometry to embolden. Register outlines for the real bold face.`);
|
|
1282
|
+
}
|
|
1283
|
+
if (r.source === "outline") return;
|
|
1284
|
+
const status = outlineStatus(family, weight, style);
|
|
1285
|
+
if (status === null) {
|
|
1286
|
+
fail("no-outlines", `served by ${family} ${weight}/${style}, which has no outlines registered. Call registerFontOutlines for it.`);
|
|
1287
|
+
}
|
|
1288
|
+
if (status === "idle" || status === "loading") {
|
|
1289
|
+
void loadFontOutlines(family, { weight, style });
|
|
1290
|
+
fail("outlines-loading", `the outlines of ${family} ${weight}/${style} have not loaded yet; await loadTextOutlines first.`);
|
|
1291
|
+
}
|
|
1292
|
+
if (status === "failed") {
|
|
1293
|
+
fail("outlines-failed", `the outlines registered for ${family} ${weight}/${style} failed to load.`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
var EM_CACHE_LIMIT = 4096;
|
|
1297
|
+
var emCache = /* @__PURE__ */ new Map();
|
|
1298
|
+
function emGlyph(d) {
|
|
1299
|
+
let p = emCache.get(d);
|
|
1300
|
+
if (p) return p;
|
|
1301
|
+
p = pathFromD(d);
|
|
1302
|
+
if (pathSignedArea(p) < 0) p = reversePath(p);
|
|
1303
|
+
if (emCache.size >= EM_CACHE_LIMIT) emCache.clear();
|
|
1304
|
+
emCache.set(d, p);
|
|
1305
|
+
return p;
|
|
1306
|
+
}
|
|
1307
|
+
function concat(parts) {
|
|
1308
|
+
let nc = 0, nk = 0;
|
|
1309
|
+
for (const p of parts) {
|
|
1310
|
+
nc += p.commands.length;
|
|
1311
|
+
nk += p.coords.length;
|
|
1312
|
+
}
|
|
1313
|
+
const commands = new Uint8Array(nc);
|
|
1314
|
+
const coords = new Float32Array(nk);
|
|
1315
|
+
let ci = 0, ki = 0;
|
|
1316
|
+
for (const p of parts) {
|
|
1317
|
+
commands.set(p.commands, ci);
|
|
1318
|
+
ci += p.commands.length;
|
|
1319
|
+
coords.set(p.coords, ki);
|
|
1320
|
+
ki += p.coords.length;
|
|
1321
|
+
}
|
|
1322
|
+
return { kind: "polygon", commands, coords, fillRule: "nonzero" };
|
|
1323
|
+
}
|
|
1324
|
+
function ruleContour(x0, y0, x1, y1) {
|
|
1325
|
+
return {
|
|
1326
|
+
kind: "polygon",
|
|
1327
|
+
commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
|
|
1328
|
+
coords: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
|
|
1329
|
+
fillRule: "nonzero"
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function walkOutlines(data, pose, opts) {
|
|
1333
|
+
const textPose = {
|
|
1334
|
+
x: pose.x,
|
|
1335
|
+
y: pose.y,
|
|
1336
|
+
width: pose.width,
|
|
1337
|
+
height: pose.height,
|
|
1338
|
+
text: data.text,
|
|
1339
|
+
runs: data.runs,
|
|
1340
|
+
style: data.style,
|
|
1341
|
+
verticalAlign: data.verticalAlign,
|
|
1342
|
+
fill: data.fill,
|
|
1343
|
+
stroke: data.stroke
|
|
1344
|
+
};
|
|
1345
|
+
const { runs, opts: layoutOpts } = textPoseLayoutInput(textPose, opts.scale ?? 1);
|
|
1346
|
+
for (const run of runs) assertOutlinable(run);
|
|
1347
|
+
const paints = [];
|
|
1348
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
1349
|
+
const paintOf = runs.map((run) => {
|
|
1350
|
+
const key = `${paintKey(run.fill?.paint)}|${strokeKey(run.stroke)}`;
|
|
1351
|
+
let i = indexOf.get(key);
|
|
1352
|
+
if (i === void 0) {
|
|
1353
|
+
i = paints.length;
|
|
1354
|
+
indexOf.set(key, i);
|
|
1355
|
+
paints.push(run.stroke !== void 0 ? { fill: run.fill, stroke: run.stroke } : { fill: run.fill });
|
|
1356
|
+
}
|
|
1357
|
+
return i;
|
|
1358
|
+
});
|
|
1359
|
+
const laid = layoutRuns(runs, layoutOpts);
|
|
1360
|
+
const dx = pose.x;
|
|
1361
|
+
const dy = pose.y + verticalAlignOffset(data.verticalAlign, pose.height, laid.bounds.height);
|
|
1362
|
+
const parts = paints.map(() => []);
|
|
1363
|
+
const rules = paints.map(() => []);
|
|
1364
|
+
const faceOf = runFaces(runs);
|
|
1365
|
+
for (const line of laid.lines) {
|
|
1366
|
+
for (const g of line.glyphs) {
|
|
1367
|
+
const d = glyphOutlineD(g, runs[g.run], 0);
|
|
1368
|
+
if (d === null) {
|
|
1369
|
+
if (glyphQuad(g) !== null) {
|
|
1370
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
1371
|
+
throw new TextOutlinesError(
|
|
1372
|
+
"no-outlines",
|
|
1373
|
+
family,
|
|
1374
|
+
weight,
|
|
1375
|
+
style,
|
|
1376
|
+
"a fallback face serving some of the text's characters has no outlines registered."
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
const s = g.fontSize;
|
|
1382
|
+
const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
|
|
1383
|
+
const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
|
|
1384
|
+
parts[paintOf[g.run]].push(transformPath(emGlyph(d), m));
|
|
1385
|
+
}
|
|
1386
|
+
for (const r of lineDecorations(line.glyphs, runs, faceOf, (run) => String(paintOf[run]))) {
|
|
1387
|
+
rules[paintOf[r.run]].push(ruleContour(r.x0 + dx, r.y0 + dy, r.x1 + dx, r.y1 + dy));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const frame = poseFrameOf(pose);
|
|
1391
|
+
const paths = parts.map((glyphs, i) => {
|
|
1392
|
+
const path = concat([...glyphs, ...rules[i]]);
|
|
1393
|
+
if (!frame || path.commands.length === 0) return path;
|
|
1394
|
+
return transformPath(path, frame);
|
|
1395
|
+
});
|
|
1396
|
+
return { paints, paths };
|
|
1397
|
+
}
|
|
1398
|
+
function textToPath(data, pose, opts = {}) {
|
|
1399
|
+
return concat(walkOutlines(data, pose, opts).paths);
|
|
1400
|
+
}
|
|
1401
|
+
function textToPathsByPaint(data, pose, opts = {}) {
|
|
1402
|
+
const { paints, paths } = walkOutlines(data, pose, opts);
|
|
1403
|
+
const out = [];
|
|
1404
|
+
paints.forEach((paint, i) => {
|
|
1405
|
+
if (paths[i].commands.length > 0) out.push({ ...paint, path: paths[i] });
|
|
1406
|
+
});
|
|
1407
|
+
return out;
|
|
1408
|
+
}
|
|
1409
|
+
async function loadTextOutlines(data, opts = {}) {
|
|
1410
|
+
const textPose = {
|
|
1411
|
+
width: 0,
|
|
1412
|
+
text: data.text,
|
|
1413
|
+
runs: data.runs,
|
|
1414
|
+
style: data.style
|
|
1415
|
+
};
|
|
1416
|
+
const { runs } = textPoseLayoutInput(textPose, opts.scale ?? 1);
|
|
1417
|
+
await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
|
|
1418
|
+
await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
|
|
1419
|
+
}
|
|
645
1420
|
|
|
646
|
-
export { MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, createMarkdownRenderer, isBoldWeight, isSmallCapsLetter, layoutMarkdown, layoutTextPose, markdownToRuns, measureText, measureTextBounds, measuredWidth, numericWeight, resolveRunFace, resolveRuns, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, smallCapsScale, smallCapsScaleFor, smallCapsText, textLineBoxes, textPoseLayoutInput, toRuns, transformRunTexts, verticalAlignOffset };
|
|
1421
|
+
export { DEFAULT_DECORATION_METRICS, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, SYNTHETIC_ITALIC_RADIANS, TextOutlinesError, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, fitTextPose, isBoldWeight, isSmallCapsLetter, isTextData, layoutMarkdown, layoutTextPose, loadTextOutlines, markdownToRuns, measureText, measureTextBounds, measuredWidth, nodeHasFlag, numericWeight, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, resolveRunFace, resolveRuns, runsCarryStyling, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, setFlagOverRange, smallCapsScale, smallCapsScaleFor, smallCapsText, styleAtRange, supersededKey, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
|
|
647
1422
|
//# sourceMappingURL=index.js.map
|
|
648
1423
|
//# sourceMappingURL=index.js.map
|