@weasel-js/text 1.13.0-rc.0 → 1.14.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/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, layoutRuns, tabAdvance, widthWithTabs } from './chunk-ADRQIIHS.js';
2
- export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_TEXT_FILL, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, cachedLayoutRuns, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle } from './chunk-ADRQIIHS.js';
3
- import { resolveScreenLength, fillHasInk, strokeHasInk, paintKey, strokeKey } from '@weasel-js/paint';
4
- import { faceMetricsOf, resolveFontVariant, syntheticBoldGrowth, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName } from '@weasel-js/font';
5
- import { pathToD, pathFromD, mergeRegion, growRegion, poseFrameOf, transformPath, PATH_M, PATH_L, PATH_Z, pathSignedArea, reversePath } from '@weasel-js/geom';
1
+ import { DEFAULT_TAB_SIZE, shapesWithFeature, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, emboldenGlyph, layoutRuns, lineDecorations, runFaces, tabAdvance, widthWithTabs, DECORATION_KINDS, decorationRule, faceOutlineD, hasGlyphQuad } from './chunk-FODNMBHI.js';
2
+ export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_DECORATION_METRICS, DEFAULT_TEXT_FILL, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, QUAD_STRIDE, cachedLayoutRuns, cachedPaintRuns, decorationMetrics, dropRuns, emboldenGlyphD, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paintRuns, paragraphDirection, registerBidi, registerShaper, resolveAlign, resolveTextStyle, syntheticBoldOffset } from './chunk-FODNMBHI.js';
3
+ import { resolveScreenLength, paintKey, strokeKey } from '@weasel-js/paint';
4
+ import { faceMetricsOf, resolveFontVariant, loadFontOutlines, glyphGeneration, faceMetricsFor, outlineStatus, cssFamilyName } from '@weasel-js/font';
6
5
  import { MIXED, deepEqual } from '@weasel-js/registry';
7
6
  export { MIXED } from '@weasel-js/registry';
7
+ import { transformPath, joinPaths, pathFromD, pathSignedArea, reversePath, poseFrameOf, originFor, coordsFrom, PATH_M, PATH_L, PATH_Z } from '@weasel-js/geom';
8
8
 
9
9
  // src/runs.ts
10
10
  function toRuns(input) {
@@ -173,10 +173,18 @@ var TITLE = /* @__PURE__ */ new Map([
173
173
  [8180, "\u038F\u0345"],
174
174
  [8183, "\u03A9\u0342\u0345"]
175
175
  ]);
176
- function titlecase(ch) {
176
+ function titlecase(ch, locale) {
177
177
  const cp = ch.codePointAt(0);
178
178
  if (cp >= 8064 && cp <= 8111) return String.fromCodePoint(cp | 8);
179
- return TITLE.get(cp) ?? ch.toUpperCase();
179
+ return TITLE.get(cp) ?? ch.toLocaleUpperCase(locale);
180
+ }
181
+ function caseLocale(lang) {
182
+ if (!lang) return void 0;
183
+ try {
184
+ return Intl.getCanonicalLocales(lang)[0];
185
+ } catch {
186
+ return void 0;
187
+ }
180
188
  }
181
189
  var WORD_FALLBACK = /[\p{L}\p{N}][\p{L}\p{N}\p{M}\p{Pc}'’]*/gu;
182
190
  function wordStarts(text) {
@@ -194,13 +202,15 @@ function alignsPerCodePoint(texts, whole, map) {
194
202
  for (const t of texts) for (const ch of t) n += map(ch).length;
195
203
  return n === whole.length;
196
204
  }
197
- var upper = (ch) => ch.toUpperCase();
198
- var lower = (ch) => ch.toLowerCase();
199
- function transformRunTexts(texts, transforms) {
205
+ function transformRunTexts(texts, transforms, langs = []) {
200
206
  if (transforms.every((t) => t === "none")) return texts.map((text) => ({ text }));
207
+ const locales = texts.map((_, i) => caseLocale(langs[i]));
208
+ const shared = locales.every((l) => l === locales[0]) ? locales[0] : void 0;
209
+ const upper = (ch, l = shared) => ch.toLocaleUpperCase(l);
210
+ const lower = (ch, l = shared) => ch.toLocaleLowerCase(l);
201
211
  const full = texts.join("");
202
- const wholeUpper = transforms.includes("uppercase") ? full.toUpperCase() : "";
203
- const wholeLower = transforms.includes("lowercase") ? full.toLowerCase() : "";
212
+ const wholeUpper = transforms.includes("uppercase") ? full.toLocaleUpperCase(shared) : "";
213
+ const wholeLower = transforms.includes("lowercase") ? full.toLocaleLowerCase(shared) : "";
204
214
  const upperAligned = wholeUpper !== "" && alignsPerCodePoint(texts, wholeUpper, upper);
205
215
  const lowerAligned = wholeLower !== "" && alignsPerCodePoint(texts, wholeLower, lower);
206
216
  const starts = transforms.includes("capitalize") ? wordStarts(full) : null;
@@ -211,6 +221,8 @@ function transformRunTexts(texts, transforms) {
211
221
  for (let i = 0; i < texts.length; i++) {
212
222
  const source = texts[i];
213
223
  const t = transforms[i] ?? "none";
224
+ const locale = locales[i];
225
+ const own = locale === shared;
214
226
  let text = "";
215
227
  const srcStarts = [];
216
228
  const srcEnds = [];
@@ -221,11 +233,11 @@ function transformRunTexts(texts, transforms) {
221
233
  const lo = lower(ch);
222
234
  let piece = ch;
223
235
  if (t === "uppercase") {
224
- piece = upperAligned ? wholeUpper.slice(upOff, upOff + up.length) : up;
236
+ piece = upperAligned && own ? wholeUpper.slice(upOff, upOff + up.length) : upper(ch, locale);
225
237
  } else if (t === "lowercase") {
226
- piece = lowerAligned ? wholeLower.slice(lowOff, lowOff + lo.length) : lo;
227
- } else if (t === "capitalize" && starts.has(base + local) && up !== ch) {
228
- piece = titlecase(ch);
238
+ piece = lowerAligned && own ? wholeLower.slice(lowOff, lowOff + lo.length) : lower(ch, locale);
239
+ } else if (t === "capitalize" && starts.has(base + local) && upper(ch, locale) !== ch) {
240
+ piece = titlecase(ch, locale);
229
241
  }
230
242
  upOff += up.length;
231
243
  lowOff += lo.length;
@@ -252,10 +264,11 @@ function smallCapsScale(face) {
252
264
  function smallCapsScaleFor(family, weight, style) {
253
265
  return smallCapsScale(faceMetricsOf(resolveFontVariant(family, weight, style)));
254
266
  }
255
- function isSmallCapsLetter(ch) {
256
- return ch.toUpperCase() !== ch;
267
+ function isSmallCapsLetter(ch, lang) {
268
+ return ch.toLocaleUpperCase(caseLocale(lang)) !== ch;
257
269
  }
258
- function smallCapsText(text, srcMap) {
270
+ function smallCapsText(text, srcMap, lang) {
271
+ const locale = caseLocale(lang);
259
272
  let out = "";
260
273
  const small = [];
261
274
  const starts = [];
@@ -264,8 +277,9 @@ function smallCapsText(text, srcMap) {
264
277
  let anySmall = false;
265
278
  let at = 0;
266
279
  for (const ch of text) {
267
- const shrink = isSmallCapsLetter(ch);
268
- const piece = shrink ? ch.toUpperCase() : ch;
280
+ const capital = ch.toLocaleUpperCase(locale);
281
+ const shrink = capital !== ch;
282
+ const piece = shrink ? capital : ch;
269
283
  if (piece.length !== ch.length) resized = true;
270
284
  anySmall ||= shrink;
271
285
  const start = srcMap ? srcMap.starts[at] : at;
@@ -284,6 +298,7 @@ function smallCapsText(text, srcMap) {
284
298
  }
285
299
 
286
300
  // src/runs/resolveRuns.ts
301
+ var SMALL_CAPS_FEATURES = Object.freeze(["smcp"]);
287
302
  var SCRIPT_METRICS = Object.freeze({
288
303
  super: Object.freeze({ size: 0.583, shift: 0.333 }),
289
304
  sub: Object.freeze({ size: 0.583, shift: -0.333 })
@@ -304,6 +319,11 @@ function scriptMetrics(face) {
304
319
  function scriptMetricsFor(family, weight, style) {
305
320
  return scriptMetrics(faceMetricsOf(resolveFontVariant(family, weight, style)));
306
321
  }
322
+ function halfLeadingFor(family, weight, style, fontSize, lineHeight) {
323
+ const m = faceMetricsOf(resolveFontVariant(family, weight, style));
324
+ if (m?.ascent === void 0 || m.descent === void 0) return void 0;
325
+ return (lineHeight - m.ascent - m.descent) / 2 * fontSize;
326
+ }
307
327
  function numericWeight(w) {
308
328
  if (typeof w === "number") return w;
309
329
  if (w === "bold" || w === "bolder") return 700;
@@ -321,15 +341,20 @@ function resolveRunFace(run, style) {
321
341
  fontStyle: run.italic ? "italic" : style.fontStyle
322
342
  };
323
343
  }
344
+ var NOTHING = Object.freeze({});
324
345
  function resolveRuns(runs, style, viewScale = 1) {
325
346
  const out = [];
326
- const shown = transformRunTexts(
347
+ let transformed = false;
348
+ for (const r of runs) if ((r.textTransform ?? style.textTransform) !== "none") transformed = true;
349
+ const shown = transformed ? transformRunTexts(
327
350
  runs.map((r) => r.text),
328
- runs.map((r) => r.textTransform ?? style.textTransform)
329
- );
351
+ runs.map((r) => r.textTransform ?? style.textTransform),
352
+ runs.map((r) => r.lang ?? style.lang)
353
+ ) : null;
330
354
  for (let i = 0; i < runs.length; i++) {
331
355
  const run = runs[i];
332
- let { text, srcMap } = shown[i];
356
+ let text = shown ? shown[i].text : run.text;
357
+ let srcMap = shown?.[i].srcMap;
333
358
  const scriptKey = run.script ?? style.script;
334
359
  const { fontFamily, fontWeight, fontStyle } = resolveRunFace(run, style);
335
360
  const script = scriptKey ? scriptMetricsFor(fontFamily, fontWeight, fontStyle)[scriptKey] : void 0;
@@ -338,8 +363,12 @@ function resolveRuns(runs, style, viewScale = 1) {
338
363
  const relative = run.fontSize === void 0 && scale !== 1;
339
364
  const fontSize = run.fontSize !== void 0 ? resolveScreenLength(run.fontSize, viewScale) : style.fontSize * scale;
340
365
  let sizeMap;
341
- if ((run.fontVariantCaps ?? style.fontVariantCaps) === "small-caps") {
342
- const caps = smallCapsText(text, srcMap);
366
+ let fontFeatures;
367
+ const smallCaps = (run.fontVariantCaps ?? style.fontVariantCaps) === "small-caps";
368
+ if (smallCaps && shapesWithFeature(fontFamily, fontWeight, fontStyle, "smcp")) {
369
+ fontFeatures = SMALL_CAPS_FEATURES;
370
+ } else if (smallCaps) {
371
+ const caps = smallCapsText(text, srcMap, run.lang ?? style.lang);
343
372
  text = caps.text;
344
373
  srcMap = caps.srcMap;
345
374
  if (caps.small) {
@@ -347,90 +376,34 @@ function resolveRuns(runs, style, viewScale = 1) {
347
376
  sizeMap = caps.small.map((s) => s ? small : fontSize);
348
377
  }
349
378
  }
379
+ const lang = run.lang ?? style.lang;
350
380
  out.push({
351
381
  text,
352
- ...srcMap ? { srcMap } : {},
382
+ ...srcMap ? { srcMap } : NOTHING,
353
383
  fontFamily,
354
384
  fontSize,
355
- ...sizeMap ? { sizeMap } : {},
385
+ ...sizeMap ? { sizeMap } : NOTHING,
356
386
  fontWeight,
357
387
  fontStyle,
358
388
  fill: run.fill !== void 0 ? run.fill : style.fill,
359
389
  // Unlike the decorations below, a run's stroke *replaces* the node's
360
390
  // rather than adding to it — there is only one outline to paint.
361
- ...(run.stroke ?? style.stroke) !== void 0 ? { stroke: run.stroke ?? style.stroke } : {},
391
+ ...(run.stroke ?? style.stroke) !== void 0 ? { stroke: run.stroke ?? style.stroke } : NOTHING,
362
392
  letterSpacing: run.letterSpacing !== void 0 ? resolveScreenLength(run.letterSpacing, viewScale) : style.letterSpacing,
363
393
  // `||`, not `??`: run-level decorations are additive over the node style.
364
394
  underline: run.underline || style.underline,
365
395
  strikethrough: run.strikethrough || style.strikethrough,
366
396
  overline: run.overline || style.overline,
367
397
  baselineShift: shiftEm * style.fontSize,
368
- ...relative ? { strutSize: style.fontSize } : {}
398
+ ...relative ? { strutSize: style.fontSize } : NOTHING,
399
+ ...run.rotate && Number.isFinite(run.rotate) ? { rotate: run.rotate } : NOTHING,
400
+ ...fontFeatures ? { fontFeatures } : NOTHING,
401
+ ...lang ? { lang } : NOTHING
369
402
  });
370
403
  }
371
404
  return out;
372
405
  }
373
406
 
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 = style.underline || style.strikethrough || style.overline ? paintOf(g.run) : null;
417
- if (paint === null) {
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
-
434
407
  // src/measure/verticalAlign.ts
435
408
  function verticalAlignOffset(align, boxHeight, textHeight) {
436
409
  if (align === void 0 || align === "top" || boxHeight === void 0) return 0;
@@ -465,210 +438,6 @@ function layoutTextPose(pose, scale = 1) {
465
438
  y: pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height)
466
439
  };
467
440
  }
468
- var SYNTHETIC_BOLD_TOLERANCE = 5e-4;
469
- var CACHE_LIMIT = 4096;
470
- var cache = /* @__PURE__ */ new Map();
471
- var dCache = /* @__PURE__ */ new Map();
472
- function syntheticBoldOffset(resolved2) {
473
- if (!resolved2.synthetic.bold) return null;
474
- const { source } = resolved2;
475
- if (source !== "atlas" && source !== "canvas") return null;
476
- const { family, weight, style } = resolved2.resolved;
477
- const growth = syntheticBoldGrowth(source, family, weight, style);
478
- if (growth === void 0 || !(growth > 0)) return null;
479
- return { growth, join: source === "atlas" ? "miter" : "round" };
480
- }
481
- function emboldenGlyph(em, d, resolved2) {
482
- const offset = syntheticBoldOffset(resolved2);
483
- if (offset === null) return em;
484
- const key = `${offset.growth}|${offset.join}|${d}`;
485
- let grown = cache.get(key);
486
- if (grown) return grown;
487
- grown = mergeRegion(
488
- growRegion(em, offset.growth, { join: offset.join, tolerance: SYNTHETIC_BOLD_TOLERANCE }),
489
- { tolerance: SYNTHETIC_BOLD_TOLERANCE }
490
- );
491
- if (cache.size >= CACHE_LIMIT) cache.clear();
492
- cache.set(key, grown);
493
- return grown;
494
- }
495
- function emboldenGlyphD(d, resolved2) {
496
- const offset = syntheticBoldOffset(resolved2);
497
- if (offset === null) return null;
498
- const tag = `bold:${offset.growth}:${offset.join}`;
499
- const key = `${tag}|${d}`;
500
- let grown = dCache.get(key);
501
- if (grown === void 0) {
502
- grown = pathToD(emboldenGlyph(pathFromD(d), d, resolved2));
503
- if (dCache.size >= CACHE_LIMIT) dCache.clear();
504
- dCache.set(key, grown);
505
- }
506
- return { d: grown, tag };
507
- }
508
-
509
- // src/layout/paintRuns.ts
510
- function glyphOutlineD(g, run, minSize) {
511
- const outlineOnly = g.resolved.source === "outline";
512
- if (!outlineOnly) {
513
- if (minSize === void 0) return null;
514
- if (run.fontSize < minSize && !strokeHasInk(run.stroke)) return null;
515
- }
516
- const d = faceOutlineD(g);
517
- if (d === null) return null;
518
- const { family, weight, style } = g.resolved.resolved;
519
- const key = `${family}|${weight}|${style}|${g.cp}`;
520
- if (outlineOnly || !g.resolved.synthetic.bold) return { d, key };
521
- const bold = emboldenGlyphD(d, g.resolved);
522
- return bold && { d: bold.d, key: `${key}|${bold.tag}` };
523
- }
524
- function faceOutlineD(g) {
525
- if (g.glyph?.color) return null;
526
- const { family, weight, style } = g.resolved.resolved;
527
- if (g.resolved.source !== "outline" && g.glyph === null) return null;
528
- return glyphOutline(family, weight, style, g.cp);
529
- }
530
- function glyphQuad(g) {
531
- const { font, glyph } = g;
532
- if (font === null || glyph === null || glyph.width === 0 || glyph.page < 0) return null;
533
- const scale = g.fontSize / g.metrics.size;
534
- const x0 = g.x + glyph.xoffset * scale;
535
- const y0 = g.baselineY + (glyph.yoffset - g.metrics.base) * scale;
536
- return {
537
- x0,
538
- y0,
539
- x1: x0 + glyph.width * scale,
540
- y1: y0 + glyph.height * scale,
541
- u0: glyph.x / font.common.scaleW,
542
- v0: glyph.y / font.common.scaleH,
543
- u1: (glyph.x + glyph.width) / font.common.scaleW,
544
- v1: (glyph.y + glyph.height) / font.common.scaleH,
545
- baselineY: g.baselineY
546
- };
547
- }
548
- function runFaces(runs) {
549
- const faces = runs.map((r) => faceMetricsOf(resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle)));
550
- return (run) => faces[run];
551
- }
552
- function groupFor(groups, g, run, source, last) {
553
- const { family, weight, style } = g.resolved.resolved;
554
- const { synthetic } = g.resolved;
555
- const page = source === "outline" ? 0 : g.glyph.page;
556
- if (last.group !== null && last.run === g.run && last.source === source && last.page === page && last.family === family && last.weight === weight && last.style === style && last.bold === synthetic.bold && last.italic === synthetic.italic) {
557
- return last.group;
558
- }
559
- const stroke = source === "outline" && strokeHasInk(run.stroke) ? run.stroke : void 0;
560
- const key = `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${paintKey(run.fill?.paint)}|${strokeKey(stroke)}|${source}|${page}`;
561
- let group = groups.get(key);
562
- if (!group) {
563
- group = {
564
- family,
565
- weight,
566
- style,
567
- synthetic: { ...synthetic },
568
- source,
569
- page,
570
- fill: run.fill,
571
- ...stroke ? { stroke } : {},
572
- quads: [],
573
- glyphs: []
574
- };
575
- groups.set(key, group);
576
- }
577
- last.group = group;
578
- last.run = g.run;
579
- last.family = family;
580
- last.weight = weight;
581
- last.style = style;
582
- last.bold = synthetic.bold;
583
- last.italic = synthetic.italic;
584
- last.source = source;
585
- last.page = page;
586
- return group;
587
- }
588
- function paintRuns(laid, runs, opts = {}) {
589
- const groups = /* @__PURE__ */ new Map();
590
- const last = {
591
- group: null,
592
- run: -1,
593
- family: "",
594
- weight: 0,
595
- style: "normal",
596
- bold: false,
597
- italic: false,
598
- source: "atlas",
599
- page: 0
600
- };
601
- const decorations = [];
602
- const faceOf = runFaces(runs);
603
- const rulePaint = (run) => fillHasInk(runs[run].fill) ? paintKey(runs[run].fill.paint) : null;
604
- for (const line of laid.lines) {
605
- for (const g of line.glyphs) {
606
- const run = runs[g.run];
607
- const outline = glyphOutlineD(g, run, opts.outlineMinSize);
608
- if (!fillHasInk(run.fill) && !(outline !== null && strokeHasInk(run.stroke))) continue;
609
- if (outline !== null) {
610
- groupFor(groups, g, run, "outline", last).glyphs.push({
611
- ...outline,
612
- x: g.x,
613
- baselineY: g.baselineY,
614
- scale: g.fontSize
615
- });
616
- continue;
617
- }
618
- const quad = glyphQuad(g);
619
- if (!quad) continue;
620
- if (g.glyph?.color) groupFor(groups, g, run, "color", last).quads.push({ ...quad, colorGlyph: g.glyph });
621
- else groupFor(groups, g, run, g.resolved.source, last).quads.push(quad);
622
- }
623
- for (const rule of lineDecorations(line.glyphs, runs, faceOf, rulePaint)) {
624
- const { run, ...rect } = rule;
625
- decorations.push({ ...rect, fill: runs[run].fill });
626
- }
627
- }
628
- return { groups: [...groups.values()], decorations };
629
- }
630
- var PAINT_CACHE_VARIANT_LIMIT = 32;
631
- var own = /* @__PURE__ */ new WeakMap();
632
- var shared = /* @__PURE__ */ new WeakMap();
633
- function outlineBucket(runs, min) {
634
- if (min === void 0) return -1;
635
- const sizes = /* @__PURE__ */ new Set();
636
- for (const r of runs) sizes.add(r.fontSize);
637
- let n = 0;
638
- for (const size of sizes) if (size >= min) n++;
639
- return n;
640
- }
641
- function variantKey(runs, opts) {
642
- let out = `${outlineBucket(runs, opts.outlineMinSize)}`;
643
- for (const r of runs) {
644
- out += `|${r.underline ? 1 : 0}${r.strikethrough ? 1 : 0}${r.overline ? 1 : 0}${paintKey(r.fill?.paint)}|${strokeKey(r.stroke)}`;
645
- }
646
- return out;
647
- }
648
- function cachedPaintRuns(laid, runs, opts = {}) {
649
- const generation = glyphGeneration();
650
- const key = variantKey(runs, opts);
651
- const fresh = (p) => p !== void 0 && p.generation === generation && p.key === key;
652
- let byLayout = own.get(runs);
653
- if (byLayout === void 0) {
654
- byLayout = /* @__PURE__ */ new WeakMap();
655
- own.set(runs, byLayout);
656
- }
657
- const mine = byLayout.get(laid);
658
- if (fresh(mine)) return mine.painted;
659
- let byKey = shared.get(laid);
660
- if (byKey === void 0) {
661
- byKey = /* @__PURE__ */ new Map();
662
- shared.set(laid, byKey);
663
- }
664
- let painting = byKey.get(key);
665
- byKey.delete(key);
666
- if (!fresh(painting)) painting = { generation, key, painted: paintRuns(laid, runs, opts) };
667
- if (byKey.size >= PAINT_CACHE_VARIANT_LIMIT) byKey.delete(byKey.keys().next().value);
668
- byKey.set(key, painting);
669
- byLayout.set(laid, painting);
670
- return painting.painted;
671
- }
672
441
 
673
442
  // src/measure/measureText.ts
674
443
  function measuredWidth(ctx, text, style) {
@@ -710,15 +479,15 @@ function measureText(ctx, text, maxWidth, style) {
710
479
 
711
480
  // src/measure/measureTextBounds.ts
712
481
  function measureTextBounds(text, style, opts) {
713
- const resolved2 = resolveTextStyle(style, void 0, opts?.scale);
714
- const runs = resolveRuns([{ text }], resolved2, opts?.scale);
482
+ const resolved = resolveTextStyle(style, void 0, opts?.scale);
483
+ const runs = resolveRuns([{ text }], resolved, opts?.scale);
715
484
  const { bounds } = cachedLayoutRuns(runs, {
716
485
  maxWidth: opts?.maxWidth ?? Infinity,
717
- lineHeight: opts?.lineHeight ?? resolved2.lineHeight,
718
- align: resolved2.align,
719
- direction: resolved2.direction,
720
- tabSize: resolved2.tabSize
721
- });
486
+ lineHeight: opts?.lineHeight ?? resolved.lineHeight,
487
+ align: resolved.align,
488
+ direction: resolved.direction,
489
+ tabSize: resolved.tabSize
490
+ }, "read");
722
491
  return { width: bounds.width, height: bounds.height };
723
492
  }
724
493
 
@@ -745,8 +514,7 @@ function caretCells(cells) {
745
514
  for (const c of cells) {
746
515
  const prev = out[out.length - 1];
747
516
  if (prev && (!prev.clusterEnd || prev.srcIndex === c.srcIndex && prev.srcEnd === c.srcEnd)) {
748
- const x0 = Math.min(prev.x, c.x);
749
- const x1 = Math.max(prev.x + prev.advance, c.x + c.advance);
517
+ const [x0, x1] = c.advance === 0 ? [prev.x, prev.x + prev.advance] : prev.advance === 0 ? [c.x, c.x + c.advance] : [Math.min(prev.x, c.x), Math.max(prev.x + prev.advance, c.x + c.advance)];
750
518
  out[out.length - 1] = {
751
519
  ...prev,
752
520
  x: x0,
@@ -860,7 +628,11 @@ function runMetrics(run, fontSize, face) {
860
628
  }
861
629
  function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.3, faceOf, tabSize = DEFAULT_TAB_SIZE) {
862
630
  if (runs.length === 0) return { lines: [], width: 0, height: 0 };
863
- const shown = transformRunTexts(runs.map((r) => r.text), runs.map((r) => r.textTransform ?? "none"));
631
+ const shown = transformRunTexts(
632
+ runs.map((r) => r.text),
633
+ runs.map((r) => r.textTransform ?? "none"),
634
+ runs.map((r) => r.lang)
635
+ );
864
636
  const segs = [];
865
637
  runs.forEach((r, i) => {
866
638
  const text = shown[i].text;
@@ -868,7 +640,7 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
868
640
  segs.push(text === r.text ? r : { ...r, text });
869
641
  return;
870
642
  }
871
- const caps = smallCapsText(text);
643
+ const caps = smallCapsText(text, void 0, r.lang);
872
644
  if (!caps.small) {
873
645
  segs.push({ ...r, text: caps.text });
874
646
  return;
@@ -1073,7 +845,9 @@ var STYLE_KEYS = [
1073
845
  "baselineShift",
1074
846
  "fontScale",
1075
847
  "textTransform",
1076
- "fontVariantCaps"
848
+ "fontVariantCaps",
849
+ "rotate",
850
+ "lang"
1077
851
  ];
1078
852
  var FLAG_KEYS = /* @__PURE__ */ new Set([
1079
853
  "bold",
@@ -1254,9 +1028,9 @@ function setFlagOverRange(runs, style, start, end, key, value) {
1254
1028
  var FLAGS = ["italic", "underline", "strikethrough", "overline"];
1255
1029
  function rangeWeight(range, style) {
1256
1030
  const node = numericWeight(style?.fontWeight ?? 400);
1257
- const own2 = range?.fontWeight;
1258
- if (own2 === MIXED) return { fontWeight: MIXED, bold: MIXED };
1259
- if (own2 !== void 0) return { fontWeight: own2, bold: isBoldWeight(own2) };
1031
+ const own = range?.fontWeight;
1032
+ if (own === MIXED) return { fontWeight: MIXED, bold: MIXED };
1033
+ if (own !== void 0) return { fontWeight: own, bold: isBoldWeight(own) };
1260
1034
  const flag = range?.bold;
1261
1035
  if (flag === MIXED) {
1262
1036
  if (node === 700) return { fontWeight: 700, bold: true };
@@ -1274,19 +1048,22 @@ var OVERRIDES = [
1274
1048
  "baselineShift",
1275
1049
  "fontScale",
1276
1050
  "textTransform",
1277
- "fontVariantCaps"
1051
+ "fontVariantCaps",
1052
+ "rotate",
1053
+ "lang"
1278
1054
  ];
1279
1055
  function effectiveRangeStyle(range, style, paint) {
1280
- const resolved2 = resolveTextStyle(style, paint);
1056
+ const resolved = resolveTextStyle(style, paint);
1281
1057
  const node = style ?? {};
1282
1058
  const out = {
1283
- fontFamily: resolved2.fontFamily,
1284
- fontSize: resolved2.fontSize,
1285
- letterSpacing: resolved2.letterSpacing,
1286
- textTransform: resolved2.textTransform,
1287
- fontVariantCaps: resolved2.fontVariantCaps
1059
+ fontFamily: resolved.fontFamily,
1060
+ fontSize: resolved.fontSize,
1061
+ letterSpacing: resolved.letterSpacing,
1062
+ textTransform: resolved.textTransform,
1063
+ fontVariantCaps: resolved.fontVariantCaps
1288
1064
  };
1289
- if (resolved2.fill !== null) out.fill = resolved2.fill;
1065
+ if (resolved.fill !== null) out.fill = resolved.fill;
1066
+ if (resolved.lang !== void 0) out.lang = resolved.lang;
1290
1067
  Object.assign(out, rangeWeight(range, style));
1291
1068
  for (const key of FLAGS) {
1292
1069
  const on = nodeHasFlag(node, key);
@@ -1302,6 +1079,14 @@ function effectiveRangeStyle(range, style, paint) {
1302
1079
 
1303
1080
  // src/syntheticItalic.ts
1304
1081
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
1082
+
1083
+ // src/glyphTurn.ts
1084
+ function glyphTurn(rotate) {
1085
+ if (!rotate) return UPRIGHT;
1086
+ const rad = rotate * Math.PI / 180;
1087
+ return [Math.cos(rad), Math.sin(rad)];
1088
+ }
1089
+ var UPRIGHT = [1, 0];
1305
1090
  var TextOutlinesError = class extends Error {
1306
1091
  reason;
1307
1092
  family;
@@ -1370,7 +1155,7 @@ function emGlyph(d) {
1370
1155
  function placedGlyphOutline(g, dx, dy, caller, blankFallback = false) {
1371
1156
  const d = faceOutlineD(g);
1372
1157
  if (d === null) {
1373
- if (!blankFallback && glyphQuad(g) !== null) {
1158
+ if (!blankFallback && hasGlyphQuad(g)) {
1374
1159
  const { family, weight, style } = g.resolved.resolved;
1375
1160
  throw new TextOutlinesError(
1376
1161
  "no-outlines",
@@ -1383,9 +1168,20 @@ function placedGlyphOutline(g, dx, dy, caller, blankFallback = false) {
1383
1168
  }
1384
1169
  return null;
1385
1170
  }
1171
+ return glyphOutlineAt(d, g, dx, dy);
1172
+ }
1173
+ function glyphOutlineAt(d, g, dx, dy) {
1386
1174
  const s = g.fontSize;
1387
1175
  const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
1388
- const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
1176
+ const [cos, sin] = glyphTurn(g.rotate);
1177
+ const m = [
1178
+ cos * s,
1179
+ sin * s,
1180
+ -(cos * shear + sin) * s,
1181
+ (cos - sin * shear) * s,
1182
+ g.x + dx,
1183
+ g.baselineY + dy
1184
+ ];
1389
1185
  return transformPath(emboldenGlyph(emGlyph(d), d, g.resolved), m);
1390
1186
  }
1391
1187
  function colorGlyphError(run, caller) {
@@ -1398,25 +1194,6 @@ function colorGlyphError(run, caller) {
1398
1194
  caller
1399
1195
  );
1400
1196
  }
1401
- function concat(parts) {
1402
- let nc = 0, nk = 0;
1403
- for (const p of parts) {
1404
- nc += p.commands.length;
1405
- nk += p.coords.length;
1406
- }
1407
- const commands = new Uint8Array(nc);
1408
- const coords = new Float32Array(nk);
1409
- let ci = 0, ki = 0;
1410
- for (const p of parts) {
1411
- commands.set(p.commands, ci);
1412
- ci += p.commands.length;
1413
- coords.set(p.coords, ki);
1414
- ki += p.coords.length;
1415
- }
1416
- return { kind: "polygon", commands, coords, fillRule: "nonzero" };
1417
- }
1418
-
1419
- // src/textToPath.ts
1420
1197
  function facesOf(runs) {
1421
1198
  const out = /* @__PURE__ */ new Map();
1422
1199
  const add = (family, weight, style) => {
@@ -1447,11 +1224,14 @@ function outlineLayoutInput(data, box, opts, caller) {
1447
1224
  return input;
1448
1225
  }
1449
1226
  function ruleContour(x0, y0, x1, y1) {
1227
+ const xy = [x0, y0, x1, y0, x1, y1, x0, y1];
1228
+ const origin = originFor(xy);
1450
1229
  return {
1451
1230
  kind: "polygon",
1452
1231
  commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
1453
- coords: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
1454
- fillRule: "nonzero"
1232
+ coords: coordsFrom(origin, xy),
1233
+ fillRule: "nonzero",
1234
+ ...origin ? { origin } : {}
1455
1235
  };
1456
1236
  }
1457
1237
  function walkOutlines(data, pose, opts) {
@@ -1464,25 +1244,27 @@ function walkOutlines(data, pose, opts) {
1464
1244
  const rules = paints.map(() => []);
1465
1245
  const faceOf = runFaces(runs);
1466
1246
  for (const line of laid.lines) {
1247
+ const deco = lineDecorations(runs, faceOf, (run) => String(paintOf[run]));
1467
1248
  for (const g of line.glyphs) {
1249
+ deco.add(g);
1468
1250
  if (g.glyph?.color) throw colorGlyphError(runs[g.run], "textToPath");
1469
1251
  const glyph = placedGlyphOutline(g, dx, dy, "textToPath");
1470
1252
  if (glyph) parts[paintOf[g.run]].push(glyph);
1471
1253
  }
1472
- for (const r of lineDecorations(line.glyphs, runs, faceOf, (run) => String(paintOf[run]))) {
1254
+ for (const r of deco.end()) {
1473
1255
  rules[paintOf[r.run]].push(ruleContour(r.x0 + dx, r.y0 + dy, r.x1 + dx, r.y1 + dy));
1474
1256
  }
1475
1257
  }
1476
1258
  const frame = poseFrameOf(pose);
1477
1259
  const paths = parts.map((glyphs, i) => {
1478
- const path = concat([...glyphs, ...rules[i]]);
1260
+ const path = joinPaths([...glyphs, ...rules[i]]);
1479
1261
  if (!frame || path.commands.length === 0) return path;
1480
1262
  return transformPath(path, frame);
1481
1263
  });
1482
1264
  return { paints, paths };
1483
1265
  }
1484
1266
  function textToPath(data, pose, opts = {}) {
1485
- return concat(walkOutlines(data, pose, opts).paths);
1267
+ return joinPaths(walkOutlines(data, pose, opts).paths);
1486
1268
  }
1487
1269
  function textToPathsByPaint(data, pose, opts = {}) {
1488
1270
  const { paints, paths } = walkOutlines(data, pose, opts);
@@ -1505,11 +1287,26 @@ async function loadTextOutlines(data, opts = {}) {
1505
1287
  }
1506
1288
  var MIDDLE_FALLBACK = 0.25;
1507
1289
  function textClusters(data, opts = {}) {
1508
- const box = { x: 0, y: 0, width: opts.width ?? Infinity, height: 0 };
1290
+ const width = opts.width ?? Infinity;
1291
+ const scale = opts.scale ?? 1;
1292
+ const generation = glyphGeneration();
1293
+ const hit = remembered.get(data);
1294
+ if (hit && hit.width === width && hit.scale === scale && hit.generation === generation) return hit.clusters;
1295
+ const clusters = clustersOf(data, width, scale);
1296
+ remembered.set(data, { width, scale, generation, clusters });
1297
+ return clusters;
1298
+ }
1299
+ var remembered = /* @__PURE__ */ new WeakMap();
1300
+ function clustersOf(data, width, scale) {
1301
+ const opts = { scale };
1302
+ const box = { x: 0, y: 0, width, height: 0 };
1509
1303
  const { runs, opts: layoutOpts } = outlineLayoutInput(data, box, opts, "textClusters");
1510
1304
  const line = layoutRuns(runs, layoutOpts).lines[0];
1511
1305
  if (!line || line.cells.length === 0) return [];
1512
1306
  const { paints, paintOf } = paintsOf(runs);
1307
+ const deco = lineDecorations(runs, runFaces(runs), (run) => String(paintOf[run]));
1308
+ for (const g of line.glyphs) deco.add(g);
1309
+ const lineRules = deco.end();
1513
1310
  const glyphOf = new Array(line.cells.length);
1514
1311
  for (const g of line.glyphs) glyphOf[g.cell] = g;
1515
1312
  const groups = [];
@@ -1532,19 +1329,36 @@ function textClusters(data, opts = {}) {
1532
1329
  }
1533
1330
  const paths = [];
1534
1331
  parts.forEach((p, k) => {
1535
- if (p.length > 0) paths.push({ ...paints[k], path: concat(p) });
1332
+ if (p.length > 0) paths.push({ ...paints[k], path: joinPaths(p) });
1536
1333
  });
1334
+ const rules = [];
1335
+ for (const r of lineRules) {
1336
+ const x0 = Math.max(r.x0, left);
1337
+ const x1 = Math.min(r.x1, right);
1338
+ if (!(x1 > x0)) continue;
1339
+ const { fill, stroke } = paints[paintOf[r.run]];
1340
+ rules.push({
1341
+ kind: r.kind,
1342
+ x0: x0 - left,
1343
+ x1: x1 - left,
1344
+ y0: r.y0 - line.baselineY,
1345
+ y1: r.y1 - line.baselineY,
1346
+ fill,
1347
+ ...stroke ? { stroke } : {}
1348
+ });
1349
+ }
1537
1350
  const first = glyphs[0];
1538
1351
  const xHeight = faceMetricsOf(first.resolved)?.xHeight;
1539
1352
  return {
1540
1353
  x: left - line.x0,
1541
1354
  advance: right - left,
1542
1355
  paths,
1543
- middle: (xHeight === void 0 ? MIDDLE_FALLBACK : xHeight / 2) * first.fontSize
1356
+ middle: (xHeight === void 0 ? MIDDLE_FALLBACK : xHeight / 2) * first.fontSize,
1357
+ rules
1544
1358
  };
1545
1359
  });
1546
1360
  }
1547
1361
 
1548
- export { DEFAULT_DECORATION_METRICS, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, SYNTHETIC_ITALIC_RADIANS, TextOutlinesError, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, emboldenGlyphD, 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, syntheticBoldOffset, textClusters, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
1362
+ export { MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, SYNTHETIC_ITALIC_RADIANS, TextOutlinesError, applyStyleToRange, caretIndexAt, createMarkdownRenderer, defaultLabelTextRenderer, effectiveRangeStyle, fitTextPose, glyphOutlineAt, glyphTurn, halfLeadingFor, isBoldWeight, isSmallCapsLetter, isTextData, layoutMarkdown, layoutTextPose, loadTextOutlines, markdownToRuns, measureText, measureTextBounds, measuredWidth, nodeHasFlag, numericWeight, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, resolveRunFace, resolveRuns, runsCarryStyling, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, setFlagOverRange, smallCapsScale, smallCapsScaleFor, smallCapsText, styleAtRange, supersededKey, textClusters, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
1549
1363
  //# sourceMappingURL=index.js.map
1550
1364
  //# sourceMappingURL=index.js.map