@weasel-js/text 1.13.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 { lazy, DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, blankGlyph, LineBox, doubles, graphemeCount, wrapLines, hangs, layoutRuns, tabAdvance, widthWithTabs } from './chunk-DOTR55MR.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-DOTR55MR.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,93 +376,33 @@ 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 } : {}
369
- });
370
- }
371
- return out;
372
- }
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 })
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
391
402
  });
392
- resolved.set(face, out);
393
403
  }
394
404
  return out;
395
405
  }
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(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
- return {
415
- add(g) {
416
- const style = runs[g.run];
417
- const paint = style.underline || style.strikethrough || style.overline ? paintOf(g.run) : null;
418
- if (paint === null) {
419
- if (open !== null) close();
420
- return;
421
- }
422
- const face = faceOf(g.run);
423
- 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;
424
- if (joins) {
425
- open.x1 = g.x + g.step;
426
- } else {
427
- close();
428
- open = { run: g.run, style, face, paint, baselineY: g.baselineY, x0: g.x, x1: g.x + g.step };
429
- }
430
- },
431
- end() {
432
- close();
433
- return rules;
434
- }
435
- };
436
- }
437
406
 
438
407
  // src/measure/verticalAlign.ts
439
408
  function verticalAlignOffset(align, boxHeight, textHeight) {
@@ -469,260 +438,6 @@ function layoutTextPose(pose, scale = 1) {
469
438
  y: pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height)
470
439
  };
471
440
  }
472
- var SYNTHETIC_BOLD_TOLERANCE = 5e-4;
473
- var CACHE_LIMIT = 4096;
474
- var cache = /* @__PURE__ */ new Map();
475
- var dCache = /* @__PURE__ */ new Map();
476
- function syntheticBoldOffset(resolved2) {
477
- if (!resolved2.synthetic.bold) return null;
478
- const { source } = resolved2;
479
- if (source !== "atlas" && source !== "canvas") return null;
480
- const { family, weight, style } = resolved2.resolved;
481
- const growth = syntheticBoldGrowth(source, family, weight, style);
482
- if (growth === void 0 || !(growth > 0)) return null;
483
- return { growth, join: source === "atlas" ? "miter" : "round" };
484
- }
485
- function emboldenGlyph(em, d, resolved2) {
486
- const offset = syntheticBoldOffset(resolved2);
487
- if (offset === null) return em;
488
- const key = `${offset.growth}|${offset.join}|${d}`;
489
- let grown = cache.get(key);
490
- if (grown) return grown;
491
- grown = mergeRegion(
492
- growRegion(em, offset.growth, { join: offset.join, tolerance: SYNTHETIC_BOLD_TOLERANCE }),
493
- { tolerance: SYNTHETIC_BOLD_TOLERANCE }
494
- );
495
- if (cache.size >= CACHE_LIMIT) cache.clear();
496
- cache.set(key, grown);
497
- return grown;
498
- }
499
- function emboldenGlyphD(d, resolved2) {
500
- const offset = syntheticBoldOffset(resolved2);
501
- if (offset === null) return null;
502
- const tag = `bold:${offset.growth}:${offset.join}`;
503
- const key = `${tag}|${d}`;
504
- let grown = dCache.get(key);
505
- if (grown === void 0) {
506
- grown = pathToD(emboldenGlyph(pathFromD(d), d, resolved2));
507
- if (dCache.size >= CACHE_LIMIT) dCache.clear();
508
- dCache.set(key, grown);
509
- }
510
- return { d: grown, tag };
511
- }
512
-
513
- // src/layout/paintRuns.ts
514
- function glyphOutlineD(g, run, minSize) {
515
- const outlineOnly = g.resolved.source === "outline";
516
- if (!outlineOnly) {
517
- if (minSize === void 0) return null;
518
- if (run.fontSize < minSize && !strokeHasInk(run.stroke)) return null;
519
- }
520
- const d = faceOutlineD(g);
521
- if (d === null) return null;
522
- const { family, weight, style } = g.resolved.resolved;
523
- const key = `${family}|${weight}|${style}|${g.cp}`;
524
- if (outlineOnly || !g.resolved.synthetic.bold) return { d, key };
525
- const bold = emboldenGlyphD(d, g.resolved);
526
- return bold && { d: bold.d, key: `${key}|${bold.tag}` };
527
- }
528
- function faceOutlineD(g) {
529
- if (g.glyph?.color) return null;
530
- const { family, weight, style } = g.resolved.resolved;
531
- if (g.resolved.source !== "outline" && g.glyph === null) return null;
532
- return glyphOutline(family, weight, style, g.cp);
533
- }
534
- var QUAD_STRIDE = 9;
535
- function hasGlyphQuad(g) {
536
- const { font, glyph } = g;
537
- return font !== null && glyph !== null && glyph.width !== 0 && glyph.page >= 0;
538
- }
539
- function writeGlyphQuad(g, out, at) {
540
- const font = g.font, glyph = g.glyph;
541
- const scale = g.fontSize / g.metrics.size;
542
- const x0 = g.x + glyph.xoffset * scale;
543
- const y0 = g.baselineY + (glyph.yoffset - g.metrics.base) * scale;
544
- out[at] = x0;
545
- out[at + 1] = y0;
546
- out[at + 2] = x0 + glyph.width * scale;
547
- out[at + 3] = y0 + glyph.height * scale;
548
- out[at + 4] = glyph.x / font.common.scaleW;
549
- out[at + 5] = glyph.y / font.common.scaleH;
550
- out[at + 6] = (glyph.x + glyph.width) / font.common.scaleW;
551
- out[at + 7] = (glyph.y + glyph.height) / font.common.scaleH;
552
- out[at + 8] = g.baselineY;
553
- }
554
- var groupViews = {
555
- quads: lazy("quads", (group) => {
556
- const d = group.quadData;
557
- const out = [];
558
- for (let at = 0, k = 0; at < d.length; at += QUAD_STRIDE, k++) {
559
- const q = {
560
- x0: d[at],
561
- y0: d[at + 1],
562
- x1: d[at + 2],
563
- y1: d[at + 3],
564
- u0: d[at + 4],
565
- v0: d[at + 5],
566
- u1: d[at + 6],
567
- v1: d[at + 7],
568
- baselineY: d[at + 8]
569
- };
570
- if (group.colorGlyphs) q.colorGlyph = group.colorGlyphs[k];
571
- out.push(q);
572
- }
573
- return out;
574
- })
575
- };
576
- function runFaces(runs) {
577
- const faces = runs.map((r) => faceMetricsOf(resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle)));
578
- return (run) => faces[run];
579
- }
580
- var isDecorated = (r) => r.underline || r.strikethrough || r.overline;
581
- function groupFor(groups, g, run, source, last) {
582
- const { family, weight, style } = g.resolved.resolved;
583
- const { synthetic } = g.resolved;
584
- const page = source === "outline" ? 0 : g.glyph.page;
585
- 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) {
586
- return last.group;
587
- }
588
- const stroke = source === "outline" && strokeHasInk(run.stroke) ? run.stroke : void 0;
589
- const key = `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${paintKey(run.fill?.paint)}|${strokeKey(stroke)}|${source}|${page}`;
590
- let group = groups.get(key);
591
- if (!group) {
592
- group = {
593
- family,
594
- weight,
595
- style,
596
- synthetic: { ...synthetic },
597
- source,
598
- page,
599
- fill: run.fill,
600
- ...stroke ? { stroke } : {},
601
- quadData: [],
602
- ...source === "color" ? { colorGlyphs: [] } : {},
603
- glyphs: []
604
- };
605
- groups.set(key, group);
606
- }
607
- last.group = group;
608
- last.run = g.run;
609
- last.family = family;
610
- last.weight = weight;
611
- last.style = style;
612
- last.bold = synthetic.bold;
613
- last.italic = synthetic.italic;
614
- last.source = source;
615
- last.page = page;
616
- return group;
617
- }
618
- function paintRuns(laid, runs, opts = {}) {
619
- const groups = /* @__PURE__ */ new Map();
620
- const last = {
621
- group: null,
622
- run: -1,
623
- family: "",
624
- weight: 0,
625
- style: "normal",
626
- bold: false,
627
- italic: false,
628
- source: "atlas",
629
- page: 0
630
- };
631
- const decorations = [];
632
- const faceOf = runs.some(isDecorated) ? runFaces(runs) : null;
633
- const rulePaint = (run) => fillHasInk(runs[run].fill) ? paintKey(runs[run].fill.paint) : null;
634
- const cursor = blankGlyph();
635
- let glyphCount = 0;
636
- for (const line of laid.lines) glyphCount += LineBox.count(line);
637
- let sized = null;
638
- let sizedAt = 0;
639
- for (const line of laid.lines) {
640
- const span = LineBox.span(line);
641
- const count = LineBox.count(line);
642
- const rules = faceOf && lineDecorations(runs, faceOf, rulePaint);
643
- for (let slot = 0; slot < count; slot++) {
644
- const g = span ? span.cols.glyph(span.start, span.cols.visual(span.start, slot), cursor) : line.glyphs[slot];
645
- rules?.add(g);
646
- const run = runs[g.run];
647
- const outline = glyphOutlineD(g, run, opts.outlineMinSize);
648
- if (!fillHasInk(run.fill) && !(outline !== null && strokeHasInk(run.stroke))) continue;
649
- if (outline !== null) {
650
- groupFor(groups, g, run, "outline", last).glyphs.push({
651
- ...outline,
652
- x: g.x,
653
- baselineY: g.baselineY,
654
- scale: g.fontSize
655
- });
656
- continue;
657
- }
658
- if (!hasGlyphQuad(g)) continue;
659
- const group = groupFor(groups, g, run, g.glyph.color ? "color" : g.resolved.source, last);
660
- if (sized === null) {
661
- sized = group;
662
- group.quadData = doubles(glyphCount * QUAD_STRIDE);
663
- }
664
- const quads = group.quadData;
665
- if (group === sized) {
666
- writeGlyphQuad(g, quads, sizedAt);
667
- sizedAt += QUAD_STRIDE;
668
- } else {
669
- writeGlyphQuad(g, quads, quads.length);
670
- }
671
- group.colorGlyphs?.push(g.glyph);
672
- }
673
- if (!rules) continue;
674
- for (const rule of rules.end()) {
675
- const { run, ...rect } = rule;
676
- decorations.push({ ...rect, fill: runs[run].fill });
677
- }
678
- }
679
- if (sized !== null) sized.quadData.length = sizedAt;
680
- const out = [];
681
- for (const group of groups.values()) out.push(Object.defineProperties(group, groupViews));
682
- return { groups: out, decorations };
683
- }
684
- var PAINT_CACHE_VARIANT_LIMIT = 32;
685
- var own = /* @__PURE__ */ new WeakMap();
686
- var shared = /* @__PURE__ */ new WeakMap();
687
- function outlineBucket(runs, min) {
688
- if (min === void 0) return -1;
689
- const sizes = /* @__PURE__ */ new Set();
690
- for (const r of runs) sizes.add(r.fontSize);
691
- let n = 0;
692
- for (const size of sizes) if (size >= min) n++;
693
- return n;
694
- }
695
- function variantKey(runs, opts) {
696
- let out = `${outlineBucket(runs, opts.outlineMinSize)}`;
697
- for (const r of runs) {
698
- out += `|${r.underline ? 1 : 0}${r.strikethrough ? 1 : 0}${r.overline ? 1 : 0}${paintKey(r.fill?.paint)}|${strokeKey(r.stroke)}`;
699
- }
700
- return out;
701
- }
702
- function cachedPaintRuns(laid, runs, opts = {}) {
703
- const generation = glyphGeneration();
704
- const key = variantKey(runs, opts);
705
- const fresh = (p) => p !== void 0 && p.generation === generation && p.key === key;
706
- let byLayout = own.get(runs);
707
- if (byLayout === void 0) {
708
- byLayout = /* @__PURE__ */ new WeakMap();
709
- own.set(runs, byLayout);
710
- }
711
- const mine = byLayout.get(laid);
712
- if (fresh(mine)) return mine.painted;
713
- let byKey = shared.get(laid);
714
- if (byKey === void 0) {
715
- byKey = /* @__PURE__ */ new Map();
716
- shared.set(laid, byKey);
717
- }
718
- let painting = byKey.get(key);
719
- byKey.delete(key);
720
- if (!fresh(painting)) painting = { generation, key, painted: paintRuns(laid, runs, opts) };
721
- if (byKey.size >= PAINT_CACHE_VARIANT_LIMIT) byKey.delete(byKey.keys().next().value);
722
- byKey.set(key, painting);
723
- byLayout.set(laid, painting);
724
- return painting.painted;
725
- }
726
441
 
727
442
  // src/measure/measureText.ts
728
443
  function measuredWidth(ctx, text, style) {
@@ -764,15 +479,15 @@ function measureText(ctx, text, maxWidth, style) {
764
479
 
765
480
  // src/measure/measureTextBounds.ts
766
481
  function measureTextBounds(text, style, opts) {
767
- const resolved2 = resolveTextStyle(style, void 0, opts?.scale);
768
- const runs = resolveRuns([{ text }], resolved2, opts?.scale);
482
+ const resolved = resolveTextStyle(style, void 0, opts?.scale);
483
+ const runs = resolveRuns([{ text }], resolved, opts?.scale);
769
484
  const { bounds } = cachedLayoutRuns(runs, {
770
485
  maxWidth: opts?.maxWidth ?? Infinity,
771
- lineHeight: opts?.lineHeight ?? resolved2.lineHeight,
772
- align: resolved2.align,
773
- direction: resolved2.direction,
774
- tabSize: resolved2.tabSize
775
- });
486
+ lineHeight: opts?.lineHeight ?? resolved.lineHeight,
487
+ align: resolved.align,
488
+ direction: resolved.direction,
489
+ tabSize: resolved.tabSize
490
+ }, "read");
776
491
  return { width: bounds.width, height: bounds.height };
777
492
  }
778
493
 
@@ -799,8 +514,7 @@ function caretCells(cells) {
799
514
  for (const c of cells) {
800
515
  const prev = out[out.length - 1];
801
516
  if (prev && (!prev.clusterEnd || prev.srcIndex === c.srcIndex && prev.srcEnd === c.srcEnd)) {
802
- const x0 = Math.min(prev.x, c.x);
803
- 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)];
804
518
  out[out.length - 1] = {
805
519
  ...prev,
806
520
  x: x0,
@@ -914,7 +628,11 @@ function runMetrics(run, fontSize, face) {
914
628
  }
915
629
  function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.3, faceOf, tabSize = DEFAULT_TAB_SIZE) {
916
630
  if (runs.length === 0) return { lines: [], width: 0, height: 0 };
917
- 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
+ );
918
636
  const segs = [];
919
637
  runs.forEach((r, i) => {
920
638
  const text = shown[i].text;
@@ -922,7 +640,7 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
922
640
  segs.push(text === r.text ? r : { ...r, text });
923
641
  return;
924
642
  }
925
- const caps = smallCapsText(text);
643
+ const caps = smallCapsText(text, void 0, r.lang);
926
644
  if (!caps.small) {
927
645
  segs.push({ ...r, text: caps.text });
928
646
  return;
@@ -1127,7 +845,9 @@ var STYLE_KEYS = [
1127
845
  "baselineShift",
1128
846
  "fontScale",
1129
847
  "textTransform",
1130
- "fontVariantCaps"
848
+ "fontVariantCaps",
849
+ "rotate",
850
+ "lang"
1131
851
  ];
1132
852
  var FLAG_KEYS = /* @__PURE__ */ new Set([
1133
853
  "bold",
@@ -1308,9 +1028,9 @@ function setFlagOverRange(runs, style, start, end, key, value) {
1308
1028
  var FLAGS = ["italic", "underline", "strikethrough", "overline"];
1309
1029
  function rangeWeight(range, style) {
1310
1030
  const node = numericWeight(style?.fontWeight ?? 400);
1311
- const own2 = range?.fontWeight;
1312
- if (own2 === MIXED) return { fontWeight: MIXED, bold: MIXED };
1313
- 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) };
1314
1034
  const flag = range?.bold;
1315
1035
  if (flag === MIXED) {
1316
1036
  if (node === 700) return { fontWeight: 700, bold: true };
@@ -1328,19 +1048,22 @@ var OVERRIDES = [
1328
1048
  "baselineShift",
1329
1049
  "fontScale",
1330
1050
  "textTransform",
1331
- "fontVariantCaps"
1051
+ "fontVariantCaps",
1052
+ "rotate",
1053
+ "lang"
1332
1054
  ];
1333
1055
  function effectiveRangeStyle(range, style, paint) {
1334
- const resolved2 = resolveTextStyle(style, paint);
1056
+ const resolved = resolveTextStyle(style, paint);
1335
1057
  const node = style ?? {};
1336
1058
  const out = {
1337
- fontFamily: resolved2.fontFamily,
1338
- fontSize: resolved2.fontSize,
1339
- letterSpacing: resolved2.letterSpacing,
1340
- textTransform: resolved2.textTransform,
1341
- fontVariantCaps: resolved2.fontVariantCaps
1059
+ fontFamily: resolved.fontFamily,
1060
+ fontSize: resolved.fontSize,
1061
+ letterSpacing: resolved.letterSpacing,
1062
+ textTransform: resolved.textTransform,
1063
+ fontVariantCaps: resolved.fontVariantCaps
1342
1064
  };
1343
- 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;
1344
1067
  Object.assign(out, rangeWeight(range, style));
1345
1068
  for (const key of FLAGS) {
1346
1069
  const on = nodeHasFlag(node, key);
@@ -1356,6 +1079,14 @@ function effectiveRangeStyle(range, style, paint) {
1356
1079
 
1357
1080
  // src/syntheticItalic.ts
1358
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];
1359
1090
  var TextOutlinesError = class extends Error {
1360
1091
  reason;
1361
1092
  family;
@@ -1437,9 +1168,20 @@ function placedGlyphOutline(g, dx, dy, caller, blankFallback = false) {
1437
1168
  }
1438
1169
  return null;
1439
1170
  }
1171
+ return glyphOutlineAt(d, g, dx, dy);
1172
+ }
1173
+ function glyphOutlineAt(d, g, dx, dy) {
1440
1174
  const s = g.fontSize;
1441
1175
  const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
1442
- 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
+ ];
1443
1185
  return transformPath(emboldenGlyph(emGlyph(d), d, g.resolved), m);
1444
1186
  }
1445
1187
  function colorGlyphError(run, caller) {
@@ -1452,25 +1194,6 @@ function colorGlyphError(run, caller) {
1452
1194
  caller
1453
1195
  );
1454
1196
  }
1455
- function concat(parts) {
1456
- let nc = 0, nk = 0;
1457
- for (const p of parts) {
1458
- nc += p.commands.length;
1459
- nk += p.coords.length;
1460
- }
1461
- const commands = new Uint8Array(nc);
1462
- const coords = new Float32Array(nk);
1463
- let ci = 0, ki = 0;
1464
- for (const p of parts) {
1465
- commands.set(p.commands, ci);
1466
- ci += p.commands.length;
1467
- coords.set(p.coords, ki);
1468
- ki += p.coords.length;
1469
- }
1470
- return { kind: "polygon", commands, coords, fillRule: "nonzero" };
1471
- }
1472
-
1473
- // src/textToPath.ts
1474
1197
  function facesOf(runs) {
1475
1198
  const out = /* @__PURE__ */ new Map();
1476
1199
  const add = (family, weight, style) => {
@@ -1501,11 +1224,14 @@ function outlineLayoutInput(data, box, opts, caller) {
1501
1224
  return input;
1502
1225
  }
1503
1226
  function ruleContour(x0, y0, x1, y1) {
1227
+ const xy = [x0, y0, x1, y0, x1, y1, x0, y1];
1228
+ const origin = originFor(xy);
1504
1229
  return {
1505
1230
  kind: "polygon",
1506
1231
  commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
1507
- coords: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
1508
- fillRule: "nonzero"
1232
+ coords: coordsFrom(origin, xy),
1233
+ fillRule: "nonzero",
1234
+ ...origin ? { origin } : {}
1509
1235
  };
1510
1236
  }
1511
1237
  function walkOutlines(data, pose, opts) {
@@ -1531,14 +1257,14 @@ function walkOutlines(data, pose, opts) {
1531
1257
  }
1532
1258
  const frame = poseFrameOf(pose);
1533
1259
  const paths = parts.map((glyphs, i) => {
1534
- const path = concat([...glyphs, ...rules[i]]);
1260
+ const path = joinPaths([...glyphs, ...rules[i]]);
1535
1261
  if (!frame || path.commands.length === 0) return path;
1536
1262
  return transformPath(path, frame);
1537
1263
  });
1538
1264
  return { paints, paths };
1539
1265
  }
1540
1266
  function textToPath(data, pose, opts = {}) {
1541
- return concat(walkOutlines(data, pose, opts).paths);
1267
+ return joinPaths(walkOutlines(data, pose, opts).paths);
1542
1268
  }
1543
1269
  function textToPathsByPaint(data, pose, opts = {}) {
1544
1270
  const { paints, paths } = walkOutlines(data, pose, opts);
@@ -1561,11 +1287,26 @@ async function loadTextOutlines(data, opts = {}) {
1561
1287
  }
1562
1288
  var MIDDLE_FALLBACK = 0.25;
1563
1289
  function textClusters(data, opts = {}) {
1564
- 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 };
1565
1303
  const { runs, opts: layoutOpts } = outlineLayoutInput(data, box, opts, "textClusters");
1566
1304
  const line = layoutRuns(runs, layoutOpts).lines[0];
1567
1305
  if (!line || line.cells.length === 0) return [];
1568
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();
1569
1310
  const glyphOf = new Array(line.cells.length);
1570
1311
  for (const g of line.glyphs) glyphOf[g.cell] = g;
1571
1312
  const groups = [];
@@ -1588,19 +1329,36 @@ function textClusters(data, opts = {}) {
1588
1329
  }
1589
1330
  const paths = [];
1590
1331
  parts.forEach((p, k) => {
1591
- if (p.length > 0) paths.push({ ...paints[k], path: concat(p) });
1332
+ if (p.length > 0) paths.push({ ...paints[k], path: joinPaths(p) });
1592
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
+ }
1593
1350
  const first = glyphs[0];
1594
1351
  const xHeight = faceMetricsOf(first.resolved)?.xHeight;
1595
1352
  return {
1596
1353
  x: left - line.x0,
1597
1354
  advance: right - left,
1598
1355
  paths,
1599
- middle: (xHeight === void 0 ? MIDDLE_FALLBACK : xHeight / 2) * first.fontSize
1356
+ middle: (xHeight === void 0 ? MIDDLE_FALLBACK : xHeight / 2) * first.fontSize,
1357
+ rules
1600
1358
  };
1601
1359
  });
1602
1360
  }
1603
1361
 
1604
- export { DEFAULT_DECORATION_METRICS, MARKDOWN_RUN_GRAMMAR, QUAD_STRIDE, 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 };
1605
1363
  //# sourceMappingURL=index.js.map
1606
1364
  //# sourceMappingURL=index.js.map