sketchmark 1.0.1 → 1.0.3

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.
Files changed (34) hide show
  1. package/README.md +23 -8
  2. package/dist/animation/index.d.ts +11 -0
  3. package/dist/animation/index.d.ts.map +1 -1
  4. package/dist/ast/types.d.ts +1 -1
  5. package/dist/ast/types.d.ts.map +1 -1
  6. package/dist/config.d.ts +2 -2
  7. package/dist/index.cjs +2590 -209
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.js +2590 -209
  10. package/dist/index.js.map +1 -1
  11. package/dist/layout/index.d.ts.map +1 -1
  12. package/dist/markdown/parser.d.ts +1 -0
  13. package/dist/markdown/parser.d.ts.map +1 -1
  14. package/dist/parser/tokenizer.d.ts.map +1 -1
  15. package/dist/renderer/canvas/index.d.ts.map +1 -1
  16. package/dist/renderer/shapes/box.d.ts.map +1 -1
  17. package/dist/renderer/shapes/circle.d.ts.map +1 -1
  18. package/dist/renderer/shapes/cylinder.d.ts.map +1 -1
  19. package/dist/renderer/shapes/diamond.d.ts.map +1 -1
  20. package/dist/renderer/shapes/hexagon.d.ts.map +1 -1
  21. package/dist/renderer/shapes/image.d.ts.map +1 -1
  22. package/dist/renderer/shapes/note.d.ts.map +1 -1
  23. package/dist/renderer/shapes/parallelogram.d.ts.map +1 -1
  24. package/dist/renderer/shapes/path.d.ts.map +1 -1
  25. package/dist/renderer/shapes/text-shape.d.ts.map +1 -1
  26. package/dist/renderer/shapes/triangle.d.ts.map +1 -1
  27. package/dist/renderer/shapes/types.d.ts +1 -1
  28. package/dist/renderer/shared.d.ts +2 -1
  29. package/dist/renderer/shared.d.ts.map +1 -1
  30. package/dist/renderer/svg/index.d.ts.map +1 -1
  31. package/dist/sketchmark.iife.js +2590 -209
  32. package/dist/utils/text-measure.d.ts +22 -0
  33. package/dist/utils/text-measure.d.ts.map +1 -0
  34. package/package.json +72 -69
@@ -78,6 +78,8 @@ var AIDiagram = (function (exports) {
78
78
  "underline",
79
79
  "crossout",
80
80
  "bracket",
81
+ "tick",
82
+ "strikeoff",
81
83
  ]);
82
84
  const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
83
85
  // Characters that can start an arrow pattern — used to decide whether a '-'
@@ -1258,8 +1260,7 @@ var AIDiagram = (function (exports) {
1258
1260
  // ── Node sizing ────────────────────────────────────────────
1259
1261
  const NODE = {
1260
1262
  minW: 90, // minimum auto-sized node width (px)
1261
- maxW: 180, // maximum auto-sized node width (px)
1262
- fontPxPerChar: 8.6, // approximate px per character for label width
1263
+ maxW: 300, // maximum auto-sized node width (px)
1263
1264
  basePad: 26, // base padding added to label width (px)
1264
1265
  };
1265
1266
  // ── Shape-specific sizing ──────────────────────────────────
@@ -1284,7 +1285,6 @@ var AIDiagram = (function (exports) {
1284
1285
  lineH: 20, // line height for note text (px)
1285
1286
  padX: 16, // horizontal padding (px)
1286
1287
  padY: 12, // vertical padding (px)
1287
- fontPxPerChar: 7.5, // approx px per char for note text
1288
1288
  fold: 14, // fold corner size (px)
1289
1289
  minW: 120, // minimum note width (px)
1290
1290
  };
@@ -1323,7 +1323,7 @@ var AIDiagram = (function (exports) {
1323
1323
  fontSize: { h1: 40, h2: 28, h3: 20, p: 15, blank: 0 },
1324
1324
  fontWeight: { h1: 700, h2: 600, h3: 600, p: 400, blank: 400 },
1325
1325
  spacing: { h1: 52, h2: 38, h3: 28, p: 22, blank: 10 },
1326
- defaultPad: 16,
1326
+ defaultPad: 0,
1327
1327
  };
1328
1328
  // ── Rough.js rendering ─────────────────────────────────────
1329
1329
  const ROUGH = {
@@ -1387,6 +1387,2102 @@ var AIDiagram = (function (exports) {
1387
1387
  // ── SVG namespace ──────────────────────────────────────────
1388
1388
  const SVG_NS$1 = "http://www.w3.org/2000/svg";
1389
1389
 
1390
+ // Simplified bidi metadata helper for the rich prepareWithSegments() path,
1391
+ // forked from pdf.js via Sebastian's text-layout. It classifies characters
1392
+ // into bidi types, computes embedding levels, and maps them onto prepared
1393
+ // segments for custom rendering. The line-breaking engine does not consume
1394
+ // these levels.
1395
+ const baseTypes = [
1396
+ 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'S', 'B', 'S', 'WS',
1397
+ 'B', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN',
1398
+ 'BN', 'BN', 'B', 'B', 'B', 'S', 'WS', 'ON', 'ON', 'ET', 'ET', 'ET', 'ON',
1399
+ 'ON', 'ON', 'ON', 'ON', 'ON', 'CS', 'ON', 'CS', 'ON', 'EN', 'EN', 'EN',
1400
+ 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'ON', 'ON', 'ON', 'ON', 'ON',
1401
+ 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
1402
+ 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'ON', 'ON',
1403
+ 'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
1404
+ 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
1405
+ 'L', 'ON', 'ON', 'ON', 'ON', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'B', 'BN',
1406
+ 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN',
1407
+ 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN',
1408
+ 'BN', 'CS', 'ON', 'ET', 'ET', 'ET', 'ET', 'ON', 'ON', 'ON', 'ON', 'L', 'ON',
1409
+ 'ON', 'ON', 'ON', 'ON', 'ET', 'ET', 'EN', 'EN', 'ON', 'L', 'ON', 'ON', 'ON',
1410
+ 'EN', 'L', 'ON', 'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
1411
+ 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
1412
+ 'L', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
1413
+ 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
1414
+ 'L', 'L', 'L', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L'
1415
+ ];
1416
+ const arabicTypes = [
1417
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1418
+ 'CS', 'AL', 'ON', 'ON', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AL',
1419
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1420
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1421
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1422
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1423
+ 'AL', 'AL', 'AL', 'AL', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM',
1424
+ 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AL', 'AL', 'AL', 'AL',
1425
+ 'AL', 'AL', 'AL', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN',
1426
+ 'AN', 'ET', 'AN', 'AN', 'AL', 'AL', 'AL', 'NSM', 'AL', 'AL', 'AL', 'AL',
1427
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1428
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1429
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1430
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1431
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1432
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1433
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1434
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1435
+ 'AL', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM',
1436
+ 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'ON', 'NSM',
1437
+ 'NSM', 'NSM', 'NSM', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL',
1438
+ 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL'
1439
+ ];
1440
+ function classifyChar(charCode) {
1441
+ if (charCode <= 0x00ff)
1442
+ return baseTypes[charCode];
1443
+ if (0x0590 <= charCode && charCode <= 0x05f4)
1444
+ return 'R';
1445
+ if (0x0600 <= charCode && charCode <= 0x06ff)
1446
+ return arabicTypes[charCode & 0xff];
1447
+ if (0x0700 <= charCode && charCode <= 0x08AC)
1448
+ return 'AL';
1449
+ return 'L';
1450
+ }
1451
+ function computeBidiLevels(str) {
1452
+ const len = str.length;
1453
+ if (len === 0)
1454
+ return null;
1455
+ // eslint-disable-next-line unicorn/no-new-array
1456
+ const types = new Array(len);
1457
+ let numBidi = 0;
1458
+ for (let i = 0; i < len; i++) {
1459
+ const t = classifyChar(str.charCodeAt(i));
1460
+ if (t === 'R' || t === 'AL' || t === 'AN')
1461
+ numBidi++;
1462
+ types[i] = t;
1463
+ }
1464
+ if (numBidi === 0)
1465
+ return null;
1466
+ const startLevel = (len / numBidi) < 0.3 ? 0 : 1;
1467
+ const levels = new Int8Array(len);
1468
+ for (let i = 0; i < len; i++)
1469
+ levels[i] = startLevel;
1470
+ const e = (startLevel & 1) ? 'R' : 'L';
1471
+ const sor = e;
1472
+ // W1-W7
1473
+ let lastType = sor;
1474
+ for (let i = 0; i < len; i++) {
1475
+ if (types[i] === 'NSM')
1476
+ types[i] = lastType;
1477
+ else
1478
+ lastType = types[i];
1479
+ }
1480
+ lastType = sor;
1481
+ for (let i = 0; i < len; i++) {
1482
+ const t = types[i];
1483
+ if (t === 'EN')
1484
+ types[i] = lastType === 'AL' ? 'AN' : 'EN';
1485
+ else if (t === 'R' || t === 'L' || t === 'AL')
1486
+ lastType = t;
1487
+ }
1488
+ for (let i = 0; i < len; i++) {
1489
+ if (types[i] === 'AL')
1490
+ types[i] = 'R';
1491
+ }
1492
+ for (let i = 1; i < len - 1; i++) {
1493
+ if (types[i] === 'ES' && types[i - 1] === 'EN' && types[i + 1] === 'EN') {
1494
+ types[i] = 'EN';
1495
+ }
1496
+ if (types[i] === 'CS' &&
1497
+ (types[i - 1] === 'EN' || types[i - 1] === 'AN') &&
1498
+ types[i + 1] === types[i - 1]) {
1499
+ types[i] = types[i - 1];
1500
+ }
1501
+ }
1502
+ for (let i = 0; i < len; i++) {
1503
+ if (types[i] !== 'EN')
1504
+ continue;
1505
+ let j;
1506
+ for (j = i - 1; j >= 0 && types[j] === 'ET'; j--)
1507
+ types[j] = 'EN';
1508
+ for (j = i + 1; j < len && types[j] === 'ET'; j++)
1509
+ types[j] = 'EN';
1510
+ }
1511
+ for (let i = 0; i < len; i++) {
1512
+ const t = types[i];
1513
+ if (t === 'WS' || t === 'ES' || t === 'ET' || t === 'CS')
1514
+ types[i] = 'ON';
1515
+ }
1516
+ lastType = sor;
1517
+ for (let i = 0; i < len; i++) {
1518
+ const t = types[i];
1519
+ if (t === 'EN')
1520
+ types[i] = lastType === 'L' ? 'L' : 'EN';
1521
+ else if (t === 'R' || t === 'L')
1522
+ lastType = t;
1523
+ }
1524
+ // N1-N2
1525
+ for (let i = 0; i < len; i++) {
1526
+ if (types[i] !== 'ON')
1527
+ continue;
1528
+ let end = i + 1;
1529
+ while (end < len && types[end] === 'ON')
1530
+ end++;
1531
+ const before = i > 0 ? types[i - 1] : sor;
1532
+ const after = end < len ? types[end] : sor;
1533
+ const bDir = before !== 'L' ? 'R' : 'L';
1534
+ const aDir = after !== 'L' ? 'R' : 'L';
1535
+ if (bDir === aDir) {
1536
+ for (let j = i; j < end; j++)
1537
+ types[j] = bDir;
1538
+ }
1539
+ i = end - 1;
1540
+ }
1541
+ for (let i = 0; i < len; i++) {
1542
+ if (types[i] === 'ON')
1543
+ types[i] = e;
1544
+ }
1545
+ // I1-I2
1546
+ for (let i = 0; i < len; i++) {
1547
+ const t = types[i];
1548
+ if ((levels[i] & 1) === 0) {
1549
+ if (t === 'R')
1550
+ levels[i]++;
1551
+ else if (t === 'AN' || t === 'EN')
1552
+ levels[i] += 2;
1553
+ }
1554
+ else if (t === 'L' || t === 'AN' || t === 'EN') {
1555
+ levels[i]++;
1556
+ }
1557
+ }
1558
+ return levels;
1559
+ }
1560
+ function computeSegmentLevels(normalized, segStarts) {
1561
+ const bidiLevels = computeBidiLevels(normalized);
1562
+ if (bidiLevels === null)
1563
+ return null;
1564
+ const segLevels = new Int8Array(segStarts.length);
1565
+ for (let i = 0; i < segStarts.length; i++) {
1566
+ segLevels[i] = bidiLevels[segStarts[i]];
1567
+ }
1568
+ return segLevels;
1569
+ }
1570
+
1571
+ const collapsibleWhitespaceRunRe = /[ \t\n\r\f]+/g;
1572
+ const needsWhitespaceNormalizationRe = /[\t\n\r\f]| {2,}|^ | $/;
1573
+ function getWhiteSpaceProfile(whiteSpace) {
1574
+ const mode = whiteSpace ?? 'normal';
1575
+ return mode === 'pre-wrap'
1576
+ ? { mode, preserveOrdinarySpaces: true, preserveHardBreaks: true }
1577
+ : { mode, preserveOrdinarySpaces: false, preserveHardBreaks: false };
1578
+ }
1579
+ function normalizeWhitespaceNormal(text) {
1580
+ if (!needsWhitespaceNormalizationRe.test(text))
1581
+ return text;
1582
+ let normalized = text.replace(collapsibleWhitespaceRunRe, ' ');
1583
+ if (normalized.charCodeAt(0) === 0x20) {
1584
+ normalized = normalized.slice(1);
1585
+ }
1586
+ if (normalized.length > 0 && normalized.charCodeAt(normalized.length - 1) === 0x20) {
1587
+ normalized = normalized.slice(0, -1);
1588
+ }
1589
+ return normalized;
1590
+ }
1591
+ function normalizeWhitespacePreWrap(text) {
1592
+ if (!/[\r\f]/.test(text))
1593
+ return text.replace(/\r\n/g, '\n');
1594
+ return text
1595
+ .replace(/\r\n/g, '\n')
1596
+ .replace(/[\r\f]/g, '\n');
1597
+ }
1598
+ let sharedWordSegmenter = null;
1599
+ let segmenterLocale;
1600
+ function getSharedWordSegmenter() {
1601
+ if (sharedWordSegmenter === null) {
1602
+ sharedWordSegmenter = new Intl.Segmenter(segmenterLocale, { granularity: 'word' });
1603
+ }
1604
+ return sharedWordSegmenter;
1605
+ }
1606
+ const arabicScriptRe = /\p{Script=Arabic}/u;
1607
+ const combiningMarkRe = /\p{M}/u;
1608
+ const decimalDigitRe = /\p{Nd}/u;
1609
+ function containsArabicScript(text) {
1610
+ return arabicScriptRe.test(text);
1611
+ }
1612
+ function isCJK(s) {
1613
+ for (const ch of s) {
1614
+ const c = ch.codePointAt(0);
1615
+ if ((c >= 0x4E00 && c <= 0x9FFF) ||
1616
+ (c >= 0x3400 && c <= 0x4DBF) ||
1617
+ (c >= 0x20000 && c <= 0x2A6DF) ||
1618
+ (c >= 0x2A700 && c <= 0x2B73F) ||
1619
+ (c >= 0x2B740 && c <= 0x2B81F) ||
1620
+ (c >= 0x2B820 && c <= 0x2CEAF) ||
1621
+ (c >= 0x2CEB0 && c <= 0x2EBEF) ||
1622
+ (c >= 0x30000 && c <= 0x3134F) ||
1623
+ (c >= 0xF900 && c <= 0xFAFF) ||
1624
+ (c >= 0x2F800 && c <= 0x2FA1F) ||
1625
+ (c >= 0x3000 && c <= 0x303F) ||
1626
+ (c >= 0x3040 && c <= 0x309F) ||
1627
+ (c >= 0x30A0 && c <= 0x30FF) ||
1628
+ (c >= 0xAC00 && c <= 0xD7AF) ||
1629
+ (c >= 0xFF00 && c <= 0xFFEF)) {
1630
+ return true;
1631
+ }
1632
+ }
1633
+ return false;
1634
+ }
1635
+ const kinsokuStart = new Set([
1636
+ '\uFF0C',
1637
+ '\uFF0E',
1638
+ '\uFF01',
1639
+ '\uFF1A',
1640
+ '\uFF1B',
1641
+ '\uFF1F',
1642
+ '\u3001',
1643
+ '\u3002',
1644
+ '\u30FB',
1645
+ '\uFF09',
1646
+ '\u3015',
1647
+ '\u3009',
1648
+ '\u300B',
1649
+ '\u300D',
1650
+ '\u300F',
1651
+ '\u3011',
1652
+ '\u3017',
1653
+ '\u3019',
1654
+ '\u301B',
1655
+ '\u30FC',
1656
+ '\u3005',
1657
+ '\u303B',
1658
+ '\u309D',
1659
+ '\u309E',
1660
+ '\u30FD',
1661
+ '\u30FE',
1662
+ ]);
1663
+ const kinsokuEnd = new Set([
1664
+ '"',
1665
+ '(', '[', '{',
1666
+ '“', '‘', '«', '‹',
1667
+ '\uFF08',
1668
+ '\u3014',
1669
+ '\u3008',
1670
+ '\u300A',
1671
+ '\u300C',
1672
+ '\u300E',
1673
+ '\u3010',
1674
+ '\u3016',
1675
+ '\u3018',
1676
+ '\u301A',
1677
+ ]);
1678
+ const forwardStickyGlue = new Set([
1679
+ "'", '’',
1680
+ ]);
1681
+ const leftStickyPunctuation = new Set([
1682
+ '.', ',', '!', '?', ':', ';',
1683
+ '\u060C',
1684
+ '\u061B',
1685
+ '\u061F',
1686
+ '\u0964',
1687
+ '\u0965',
1688
+ '\u104A',
1689
+ '\u104B',
1690
+ '\u104C',
1691
+ '\u104D',
1692
+ '\u104F',
1693
+ ')', ']', '}',
1694
+ '%',
1695
+ '"',
1696
+ '”', '’', '»', '›',
1697
+ '…',
1698
+ ]);
1699
+ const arabicNoSpaceTrailingPunctuation = new Set([
1700
+ ':',
1701
+ '.',
1702
+ '\u060C',
1703
+ '\u061B',
1704
+ ]);
1705
+ const myanmarMedialGlue = new Set([
1706
+ '\u104F',
1707
+ ]);
1708
+ const closingQuoteChars = new Set([
1709
+ '”', '’', '»', '›',
1710
+ '\u300D',
1711
+ '\u300F',
1712
+ '\u3011',
1713
+ '\u300B',
1714
+ '\u3009',
1715
+ '\u3015',
1716
+ '\uFF09',
1717
+ ]);
1718
+ function isLeftStickyPunctuationSegment(segment) {
1719
+ if (isEscapedQuoteClusterSegment(segment))
1720
+ return true;
1721
+ let sawPunctuation = false;
1722
+ for (const ch of segment) {
1723
+ if (leftStickyPunctuation.has(ch)) {
1724
+ sawPunctuation = true;
1725
+ continue;
1726
+ }
1727
+ if (sawPunctuation && combiningMarkRe.test(ch))
1728
+ continue;
1729
+ return false;
1730
+ }
1731
+ return sawPunctuation;
1732
+ }
1733
+ function isCJKLineStartProhibitedSegment(segment) {
1734
+ for (const ch of segment) {
1735
+ if (!kinsokuStart.has(ch) && !leftStickyPunctuation.has(ch))
1736
+ return false;
1737
+ }
1738
+ return segment.length > 0;
1739
+ }
1740
+ function isForwardStickyClusterSegment(segment) {
1741
+ if (isEscapedQuoteClusterSegment(segment))
1742
+ return true;
1743
+ for (const ch of segment) {
1744
+ if (!kinsokuEnd.has(ch) && !forwardStickyGlue.has(ch) && !combiningMarkRe.test(ch))
1745
+ return false;
1746
+ }
1747
+ return segment.length > 0;
1748
+ }
1749
+ function isEscapedQuoteClusterSegment(segment) {
1750
+ let sawQuote = false;
1751
+ for (const ch of segment) {
1752
+ if (ch === '\\' || combiningMarkRe.test(ch))
1753
+ continue;
1754
+ if (kinsokuEnd.has(ch) || leftStickyPunctuation.has(ch) || forwardStickyGlue.has(ch)) {
1755
+ sawQuote = true;
1756
+ continue;
1757
+ }
1758
+ return false;
1759
+ }
1760
+ return sawQuote;
1761
+ }
1762
+ function splitTrailingForwardStickyCluster(text) {
1763
+ const chars = Array.from(text);
1764
+ let splitIndex = chars.length;
1765
+ while (splitIndex > 0) {
1766
+ const ch = chars[splitIndex - 1];
1767
+ if (combiningMarkRe.test(ch)) {
1768
+ splitIndex--;
1769
+ continue;
1770
+ }
1771
+ if (kinsokuEnd.has(ch) || forwardStickyGlue.has(ch)) {
1772
+ splitIndex--;
1773
+ continue;
1774
+ }
1775
+ break;
1776
+ }
1777
+ if (splitIndex <= 0 || splitIndex === chars.length)
1778
+ return null;
1779
+ return {
1780
+ head: chars.slice(0, splitIndex).join(''),
1781
+ tail: chars.slice(splitIndex).join(''),
1782
+ };
1783
+ }
1784
+ function isRepeatedSingleCharRun(segment, ch) {
1785
+ if (segment.length === 0)
1786
+ return false;
1787
+ for (const part of segment) {
1788
+ if (part !== ch)
1789
+ return false;
1790
+ }
1791
+ return true;
1792
+ }
1793
+ function endsWithArabicNoSpacePunctuation(segment) {
1794
+ if (!containsArabicScript(segment) || segment.length === 0)
1795
+ return false;
1796
+ return arabicNoSpaceTrailingPunctuation.has(segment[segment.length - 1]);
1797
+ }
1798
+ function endsWithMyanmarMedialGlue(segment) {
1799
+ if (segment.length === 0)
1800
+ return false;
1801
+ return myanmarMedialGlue.has(segment[segment.length - 1]);
1802
+ }
1803
+ function splitLeadingSpaceAndMarks(segment) {
1804
+ if (segment.length < 2 || segment[0] !== ' ')
1805
+ return null;
1806
+ const marks = segment.slice(1);
1807
+ if (/^\p{M}+$/u.test(marks)) {
1808
+ return { space: ' ', marks };
1809
+ }
1810
+ return null;
1811
+ }
1812
+ function endsWithClosingQuote(text) {
1813
+ for (let i = text.length - 1; i >= 0; i--) {
1814
+ const ch = text[i];
1815
+ if (closingQuoteChars.has(ch))
1816
+ return true;
1817
+ if (!leftStickyPunctuation.has(ch))
1818
+ return false;
1819
+ }
1820
+ return false;
1821
+ }
1822
+ function classifySegmentBreakChar(ch, whiteSpaceProfile) {
1823
+ if (whiteSpaceProfile.preserveOrdinarySpaces || whiteSpaceProfile.preserveHardBreaks) {
1824
+ if (ch === ' ')
1825
+ return 'preserved-space';
1826
+ if (ch === '\t')
1827
+ return 'tab';
1828
+ if (whiteSpaceProfile.preserveHardBreaks && ch === '\n')
1829
+ return 'hard-break';
1830
+ }
1831
+ if (ch === ' ')
1832
+ return 'space';
1833
+ if (ch === '\u00A0' || ch === '\u202F' || ch === '\u2060' || ch === '\uFEFF') {
1834
+ return 'glue';
1835
+ }
1836
+ if (ch === '\u200B')
1837
+ return 'zero-width-break';
1838
+ if (ch === '\u00AD')
1839
+ return 'soft-hyphen';
1840
+ return 'text';
1841
+ }
1842
+ function joinTextParts(parts) {
1843
+ return parts.length === 1 ? parts[0] : parts.join('');
1844
+ }
1845
+ function splitSegmentByBreakKind(segment, isWordLike, start, whiteSpaceProfile) {
1846
+ const pieces = [];
1847
+ let currentKind = null;
1848
+ let currentTextParts = [];
1849
+ let currentStart = start;
1850
+ let currentWordLike = false;
1851
+ let offset = 0;
1852
+ for (const ch of segment) {
1853
+ const kind = classifySegmentBreakChar(ch, whiteSpaceProfile);
1854
+ const wordLike = kind === 'text' && isWordLike;
1855
+ if (currentKind !== null && kind === currentKind && wordLike === currentWordLike) {
1856
+ currentTextParts.push(ch);
1857
+ offset += ch.length;
1858
+ continue;
1859
+ }
1860
+ if (currentKind !== null) {
1861
+ pieces.push({
1862
+ text: joinTextParts(currentTextParts),
1863
+ isWordLike: currentWordLike,
1864
+ kind: currentKind,
1865
+ start: currentStart,
1866
+ });
1867
+ }
1868
+ currentKind = kind;
1869
+ currentTextParts = [ch];
1870
+ currentStart = start + offset;
1871
+ currentWordLike = wordLike;
1872
+ offset += ch.length;
1873
+ }
1874
+ if (currentKind !== null) {
1875
+ pieces.push({
1876
+ text: joinTextParts(currentTextParts),
1877
+ isWordLike: currentWordLike,
1878
+ kind: currentKind,
1879
+ start: currentStart,
1880
+ });
1881
+ }
1882
+ return pieces;
1883
+ }
1884
+ function isTextRunBoundary(kind) {
1885
+ return (kind === 'space' ||
1886
+ kind === 'preserved-space' ||
1887
+ kind === 'zero-width-break' ||
1888
+ kind === 'hard-break');
1889
+ }
1890
+ const urlSchemeSegmentRe = /^[A-Za-z][A-Za-z0-9+.-]*:$/;
1891
+ function isUrlLikeRunStart(segmentation, index) {
1892
+ const text = segmentation.texts[index];
1893
+ if (text.startsWith('www.'))
1894
+ return true;
1895
+ return (urlSchemeSegmentRe.test(text) &&
1896
+ index + 1 < segmentation.len &&
1897
+ segmentation.kinds[index + 1] === 'text' &&
1898
+ segmentation.texts[index + 1] === '//');
1899
+ }
1900
+ function isUrlQueryBoundarySegment(text) {
1901
+ return text.includes('?') && (text.includes('://') || text.startsWith('www.'));
1902
+ }
1903
+ function mergeUrlLikeRuns(segmentation) {
1904
+ const texts = segmentation.texts.slice();
1905
+ const isWordLike = segmentation.isWordLike.slice();
1906
+ const kinds = segmentation.kinds.slice();
1907
+ const starts = segmentation.starts.slice();
1908
+ for (let i = 0; i < segmentation.len; i++) {
1909
+ if (kinds[i] !== 'text' || !isUrlLikeRunStart(segmentation, i))
1910
+ continue;
1911
+ const mergedParts = [texts[i]];
1912
+ let j = i + 1;
1913
+ while (j < segmentation.len && !isTextRunBoundary(kinds[j])) {
1914
+ mergedParts.push(texts[j]);
1915
+ isWordLike[i] = true;
1916
+ const endsQueryPrefix = texts[j].includes('?');
1917
+ kinds[j] = 'text';
1918
+ texts[j] = '';
1919
+ j++;
1920
+ if (endsQueryPrefix)
1921
+ break;
1922
+ }
1923
+ texts[i] = joinTextParts(mergedParts);
1924
+ }
1925
+ let compactLen = 0;
1926
+ for (let read = 0; read < texts.length; read++) {
1927
+ const text = texts[read];
1928
+ if (text.length === 0)
1929
+ continue;
1930
+ if (compactLen !== read) {
1931
+ texts[compactLen] = text;
1932
+ isWordLike[compactLen] = isWordLike[read];
1933
+ kinds[compactLen] = kinds[read];
1934
+ starts[compactLen] = starts[read];
1935
+ }
1936
+ compactLen++;
1937
+ }
1938
+ texts.length = compactLen;
1939
+ isWordLike.length = compactLen;
1940
+ kinds.length = compactLen;
1941
+ starts.length = compactLen;
1942
+ return {
1943
+ len: compactLen,
1944
+ texts,
1945
+ isWordLike,
1946
+ kinds,
1947
+ starts,
1948
+ };
1949
+ }
1950
+ function mergeUrlQueryRuns(segmentation) {
1951
+ const texts = [];
1952
+ const isWordLike = [];
1953
+ const kinds = [];
1954
+ const starts = [];
1955
+ for (let i = 0; i < segmentation.len; i++) {
1956
+ const text = segmentation.texts[i];
1957
+ texts.push(text);
1958
+ isWordLike.push(segmentation.isWordLike[i]);
1959
+ kinds.push(segmentation.kinds[i]);
1960
+ starts.push(segmentation.starts[i]);
1961
+ if (!isUrlQueryBoundarySegment(text))
1962
+ continue;
1963
+ const nextIndex = i + 1;
1964
+ if (nextIndex >= segmentation.len ||
1965
+ isTextRunBoundary(segmentation.kinds[nextIndex])) {
1966
+ continue;
1967
+ }
1968
+ const queryParts = [];
1969
+ const queryStart = segmentation.starts[nextIndex];
1970
+ let j = nextIndex;
1971
+ while (j < segmentation.len && !isTextRunBoundary(segmentation.kinds[j])) {
1972
+ queryParts.push(segmentation.texts[j]);
1973
+ j++;
1974
+ }
1975
+ if (queryParts.length > 0) {
1976
+ texts.push(joinTextParts(queryParts));
1977
+ isWordLike.push(true);
1978
+ kinds.push('text');
1979
+ starts.push(queryStart);
1980
+ i = j - 1;
1981
+ }
1982
+ }
1983
+ return {
1984
+ len: texts.length,
1985
+ texts,
1986
+ isWordLike,
1987
+ kinds,
1988
+ starts,
1989
+ };
1990
+ }
1991
+ const numericJoinerChars = new Set([
1992
+ ':', '-', '/', '×', ',', '.', '+',
1993
+ '\u2013',
1994
+ '\u2014',
1995
+ ]);
1996
+ const asciiPunctuationChainSegmentRe = /^[A-Za-z0-9_]+[,:;]*$/;
1997
+ const asciiPunctuationChainTrailingJoinersRe = /[,:;]+$/;
1998
+ function segmentContainsDecimalDigit(text) {
1999
+ for (const ch of text) {
2000
+ if (decimalDigitRe.test(ch))
2001
+ return true;
2002
+ }
2003
+ return false;
2004
+ }
2005
+ function isNumericRunSegment(text) {
2006
+ if (text.length === 0)
2007
+ return false;
2008
+ for (const ch of text) {
2009
+ if (decimalDigitRe.test(ch) || numericJoinerChars.has(ch))
2010
+ continue;
2011
+ return false;
2012
+ }
2013
+ return true;
2014
+ }
2015
+ function mergeNumericRuns(segmentation) {
2016
+ const texts = [];
2017
+ const isWordLike = [];
2018
+ const kinds = [];
2019
+ const starts = [];
2020
+ for (let i = 0; i < segmentation.len; i++) {
2021
+ const text = segmentation.texts[i];
2022
+ const kind = segmentation.kinds[i];
2023
+ if (kind === 'text' && isNumericRunSegment(text) && segmentContainsDecimalDigit(text)) {
2024
+ const mergedParts = [text];
2025
+ let j = i + 1;
2026
+ while (j < segmentation.len &&
2027
+ segmentation.kinds[j] === 'text' &&
2028
+ isNumericRunSegment(segmentation.texts[j])) {
2029
+ mergedParts.push(segmentation.texts[j]);
2030
+ j++;
2031
+ }
2032
+ texts.push(joinTextParts(mergedParts));
2033
+ isWordLike.push(true);
2034
+ kinds.push('text');
2035
+ starts.push(segmentation.starts[i]);
2036
+ i = j - 1;
2037
+ continue;
2038
+ }
2039
+ texts.push(text);
2040
+ isWordLike.push(segmentation.isWordLike[i]);
2041
+ kinds.push(kind);
2042
+ starts.push(segmentation.starts[i]);
2043
+ }
2044
+ return {
2045
+ len: texts.length,
2046
+ texts,
2047
+ isWordLike,
2048
+ kinds,
2049
+ starts,
2050
+ };
2051
+ }
2052
+ function mergeAsciiPunctuationChains(segmentation) {
2053
+ const texts = [];
2054
+ const isWordLike = [];
2055
+ const kinds = [];
2056
+ const starts = [];
2057
+ for (let i = 0; i < segmentation.len; i++) {
2058
+ const text = segmentation.texts[i];
2059
+ const kind = segmentation.kinds[i];
2060
+ const wordLike = segmentation.isWordLike[i];
2061
+ if (kind === 'text' && wordLike && asciiPunctuationChainSegmentRe.test(text)) {
2062
+ const mergedParts = [text];
2063
+ let endsWithJoiners = asciiPunctuationChainTrailingJoinersRe.test(text);
2064
+ let j = i + 1;
2065
+ while (endsWithJoiners &&
2066
+ j < segmentation.len &&
2067
+ segmentation.kinds[j] === 'text' &&
2068
+ segmentation.isWordLike[j] &&
2069
+ asciiPunctuationChainSegmentRe.test(segmentation.texts[j])) {
2070
+ const nextText = segmentation.texts[j];
2071
+ mergedParts.push(nextText);
2072
+ endsWithJoiners = asciiPunctuationChainTrailingJoinersRe.test(nextText);
2073
+ j++;
2074
+ }
2075
+ texts.push(joinTextParts(mergedParts));
2076
+ isWordLike.push(true);
2077
+ kinds.push('text');
2078
+ starts.push(segmentation.starts[i]);
2079
+ i = j - 1;
2080
+ continue;
2081
+ }
2082
+ texts.push(text);
2083
+ isWordLike.push(wordLike);
2084
+ kinds.push(kind);
2085
+ starts.push(segmentation.starts[i]);
2086
+ }
2087
+ return {
2088
+ len: texts.length,
2089
+ texts,
2090
+ isWordLike,
2091
+ kinds,
2092
+ starts,
2093
+ };
2094
+ }
2095
+ function splitHyphenatedNumericRuns(segmentation) {
2096
+ const texts = [];
2097
+ const isWordLike = [];
2098
+ const kinds = [];
2099
+ const starts = [];
2100
+ for (let i = 0; i < segmentation.len; i++) {
2101
+ const text = segmentation.texts[i];
2102
+ if (segmentation.kinds[i] === 'text' && text.includes('-')) {
2103
+ const parts = text.split('-');
2104
+ let shouldSplit = parts.length > 1;
2105
+ for (let j = 0; j < parts.length; j++) {
2106
+ const part = parts[j];
2107
+ if (!shouldSplit)
2108
+ break;
2109
+ if (part.length === 0 ||
2110
+ !segmentContainsDecimalDigit(part) ||
2111
+ !isNumericRunSegment(part)) {
2112
+ shouldSplit = false;
2113
+ }
2114
+ }
2115
+ if (shouldSplit) {
2116
+ let offset = 0;
2117
+ for (let j = 0; j < parts.length; j++) {
2118
+ const part = parts[j];
2119
+ const splitText = j < parts.length - 1 ? `${part}-` : part;
2120
+ texts.push(splitText);
2121
+ isWordLike.push(true);
2122
+ kinds.push('text');
2123
+ starts.push(segmentation.starts[i] + offset);
2124
+ offset += splitText.length;
2125
+ }
2126
+ continue;
2127
+ }
2128
+ }
2129
+ texts.push(text);
2130
+ isWordLike.push(segmentation.isWordLike[i]);
2131
+ kinds.push(segmentation.kinds[i]);
2132
+ starts.push(segmentation.starts[i]);
2133
+ }
2134
+ return {
2135
+ len: texts.length,
2136
+ texts,
2137
+ isWordLike,
2138
+ kinds,
2139
+ starts,
2140
+ };
2141
+ }
2142
+ function mergeGlueConnectedTextRuns(segmentation) {
2143
+ const texts = [];
2144
+ const isWordLike = [];
2145
+ const kinds = [];
2146
+ const starts = [];
2147
+ let read = 0;
2148
+ while (read < segmentation.len) {
2149
+ const textParts = [segmentation.texts[read]];
2150
+ let wordLike = segmentation.isWordLike[read];
2151
+ let kind = segmentation.kinds[read];
2152
+ let start = segmentation.starts[read];
2153
+ if (kind === 'glue') {
2154
+ const glueParts = [textParts[0]];
2155
+ const glueStart = start;
2156
+ read++;
2157
+ while (read < segmentation.len && segmentation.kinds[read] === 'glue') {
2158
+ glueParts.push(segmentation.texts[read]);
2159
+ read++;
2160
+ }
2161
+ const glueText = joinTextParts(glueParts);
2162
+ if (read < segmentation.len && segmentation.kinds[read] === 'text') {
2163
+ textParts[0] = glueText;
2164
+ textParts.push(segmentation.texts[read]);
2165
+ wordLike = segmentation.isWordLike[read];
2166
+ kind = 'text';
2167
+ start = glueStart;
2168
+ read++;
2169
+ }
2170
+ else {
2171
+ texts.push(glueText);
2172
+ isWordLike.push(false);
2173
+ kinds.push('glue');
2174
+ starts.push(glueStart);
2175
+ continue;
2176
+ }
2177
+ }
2178
+ else {
2179
+ read++;
2180
+ }
2181
+ if (kind === 'text') {
2182
+ while (read < segmentation.len && segmentation.kinds[read] === 'glue') {
2183
+ const glueParts = [];
2184
+ while (read < segmentation.len && segmentation.kinds[read] === 'glue') {
2185
+ glueParts.push(segmentation.texts[read]);
2186
+ read++;
2187
+ }
2188
+ const glueText = joinTextParts(glueParts);
2189
+ if (read < segmentation.len && segmentation.kinds[read] === 'text') {
2190
+ textParts.push(glueText, segmentation.texts[read]);
2191
+ wordLike = wordLike || segmentation.isWordLike[read];
2192
+ read++;
2193
+ continue;
2194
+ }
2195
+ textParts.push(glueText);
2196
+ }
2197
+ }
2198
+ texts.push(joinTextParts(textParts));
2199
+ isWordLike.push(wordLike);
2200
+ kinds.push(kind);
2201
+ starts.push(start);
2202
+ }
2203
+ return {
2204
+ len: texts.length,
2205
+ texts,
2206
+ isWordLike,
2207
+ kinds,
2208
+ starts,
2209
+ };
2210
+ }
2211
+ function carryTrailingForwardStickyAcrossCJKBoundary(segmentation) {
2212
+ const texts = segmentation.texts.slice();
2213
+ const isWordLike = segmentation.isWordLike.slice();
2214
+ const kinds = segmentation.kinds.slice();
2215
+ const starts = segmentation.starts.slice();
2216
+ for (let i = 0; i < texts.length - 1; i++) {
2217
+ if (kinds[i] !== 'text' || kinds[i + 1] !== 'text')
2218
+ continue;
2219
+ if (!isCJK(texts[i]) || !isCJK(texts[i + 1]))
2220
+ continue;
2221
+ const split = splitTrailingForwardStickyCluster(texts[i]);
2222
+ if (split === null)
2223
+ continue;
2224
+ texts[i] = split.head;
2225
+ texts[i + 1] = split.tail + texts[i + 1];
2226
+ starts[i + 1] = starts[i] + split.head.length;
2227
+ }
2228
+ return {
2229
+ len: texts.length,
2230
+ texts,
2231
+ isWordLike,
2232
+ kinds,
2233
+ starts,
2234
+ };
2235
+ }
2236
+ function buildMergedSegmentation(normalized, profile, whiteSpaceProfile) {
2237
+ const wordSegmenter = getSharedWordSegmenter();
2238
+ let mergedLen = 0;
2239
+ const mergedTexts = [];
2240
+ const mergedWordLike = [];
2241
+ const mergedKinds = [];
2242
+ const mergedStarts = [];
2243
+ for (const s of wordSegmenter.segment(normalized)) {
2244
+ for (const piece of splitSegmentByBreakKind(s.segment, s.isWordLike ?? false, s.index, whiteSpaceProfile)) {
2245
+ const isText = piece.kind === 'text';
2246
+ if (profile.carryCJKAfterClosingQuote &&
2247
+ isText &&
2248
+ mergedLen > 0 &&
2249
+ mergedKinds[mergedLen - 1] === 'text' &&
2250
+ isCJK(piece.text) &&
2251
+ isCJK(mergedTexts[mergedLen - 1]) &&
2252
+ endsWithClosingQuote(mergedTexts[mergedLen - 1])) {
2253
+ mergedTexts[mergedLen - 1] += piece.text;
2254
+ mergedWordLike[mergedLen - 1] = mergedWordLike[mergedLen - 1] || piece.isWordLike;
2255
+ }
2256
+ else if (isText &&
2257
+ mergedLen > 0 &&
2258
+ mergedKinds[mergedLen - 1] === 'text' &&
2259
+ isCJKLineStartProhibitedSegment(piece.text) &&
2260
+ isCJK(mergedTexts[mergedLen - 1])) {
2261
+ mergedTexts[mergedLen - 1] += piece.text;
2262
+ mergedWordLike[mergedLen - 1] = mergedWordLike[mergedLen - 1] || piece.isWordLike;
2263
+ }
2264
+ else if (isText &&
2265
+ mergedLen > 0 &&
2266
+ mergedKinds[mergedLen - 1] === 'text' &&
2267
+ endsWithMyanmarMedialGlue(mergedTexts[mergedLen - 1])) {
2268
+ mergedTexts[mergedLen - 1] += piece.text;
2269
+ mergedWordLike[mergedLen - 1] = mergedWordLike[mergedLen - 1] || piece.isWordLike;
2270
+ }
2271
+ else if (isText &&
2272
+ mergedLen > 0 &&
2273
+ mergedKinds[mergedLen - 1] === 'text' &&
2274
+ piece.isWordLike &&
2275
+ containsArabicScript(piece.text) &&
2276
+ endsWithArabicNoSpacePunctuation(mergedTexts[mergedLen - 1])) {
2277
+ mergedTexts[mergedLen - 1] += piece.text;
2278
+ mergedWordLike[mergedLen - 1] = true;
2279
+ }
2280
+ else if (isText &&
2281
+ !piece.isWordLike &&
2282
+ mergedLen > 0 &&
2283
+ mergedKinds[mergedLen - 1] === 'text' &&
2284
+ piece.text.length === 1 &&
2285
+ piece.text !== '-' &&
2286
+ piece.text !== '—' &&
2287
+ isRepeatedSingleCharRun(mergedTexts[mergedLen - 1], piece.text)) {
2288
+ mergedTexts[mergedLen - 1] += piece.text;
2289
+ }
2290
+ else if (isText &&
2291
+ !piece.isWordLike &&
2292
+ mergedLen > 0 &&
2293
+ mergedKinds[mergedLen - 1] === 'text' &&
2294
+ (isLeftStickyPunctuationSegment(piece.text) ||
2295
+ (piece.text === '-' && mergedWordLike[mergedLen - 1]))) {
2296
+ mergedTexts[mergedLen - 1] += piece.text;
2297
+ }
2298
+ else {
2299
+ mergedTexts[mergedLen] = piece.text;
2300
+ mergedWordLike[mergedLen] = piece.isWordLike;
2301
+ mergedKinds[mergedLen] = piece.kind;
2302
+ mergedStarts[mergedLen] = piece.start;
2303
+ mergedLen++;
2304
+ }
2305
+ }
2306
+ }
2307
+ for (let i = 1; i < mergedLen; i++) {
2308
+ if (mergedKinds[i] === 'text' &&
2309
+ !mergedWordLike[i] &&
2310
+ isEscapedQuoteClusterSegment(mergedTexts[i]) &&
2311
+ mergedKinds[i - 1] === 'text') {
2312
+ mergedTexts[i - 1] += mergedTexts[i];
2313
+ mergedWordLike[i - 1] = mergedWordLike[i - 1] || mergedWordLike[i];
2314
+ mergedTexts[i] = '';
2315
+ }
2316
+ }
2317
+ for (let i = mergedLen - 2; i >= 0; i--) {
2318
+ if (mergedKinds[i] === 'text' && !mergedWordLike[i] && isForwardStickyClusterSegment(mergedTexts[i])) {
2319
+ let j = i + 1;
2320
+ while (j < mergedLen && mergedTexts[j] === '')
2321
+ j++;
2322
+ if (j < mergedLen && mergedKinds[j] === 'text') {
2323
+ mergedTexts[j] = mergedTexts[i] + mergedTexts[j];
2324
+ mergedStarts[j] = mergedStarts[i];
2325
+ mergedTexts[i] = '';
2326
+ }
2327
+ }
2328
+ }
2329
+ let compactLen = 0;
2330
+ for (let read = 0; read < mergedLen; read++) {
2331
+ const text = mergedTexts[read];
2332
+ if (text.length === 0)
2333
+ continue;
2334
+ if (compactLen !== read) {
2335
+ mergedTexts[compactLen] = text;
2336
+ mergedWordLike[compactLen] = mergedWordLike[read];
2337
+ mergedKinds[compactLen] = mergedKinds[read];
2338
+ mergedStarts[compactLen] = mergedStarts[read];
2339
+ }
2340
+ compactLen++;
2341
+ }
2342
+ mergedTexts.length = compactLen;
2343
+ mergedWordLike.length = compactLen;
2344
+ mergedKinds.length = compactLen;
2345
+ mergedStarts.length = compactLen;
2346
+ const compacted = mergeGlueConnectedTextRuns({
2347
+ len: compactLen,
2348
+ texts: mergedTexts,
2349
+ isWordLike: mergedWordLike,
2350
+ kinds: mergedKinds,
2351
+ starts: mergedStarts,
2352
+ });
2353
+ const withMergedUrls = carryTrailingForwardStickyAcrossCJKBoundary(mergeAsciiPunctuationChains(splitHyphenatedNumericRuns(mergeNumericRuns(mergeUrlQueryRuns(mergeUrlLikeRuns(compacted))))));
2354
+ for (let i = 0; i < withMergedUrls.len - 1; i++) {
2355
+ const split = splitLeadingSpaceAndMarks(withMergedUrls.texts[i]);
2356
+ if (split === null)
2357
+ continue;
2358
+ if ((withMergedUrls.kinds[i] !== 'space' && withMergedUrls.kinds[i] !== 'preserved-space') ||
2359
+ withMergedUrls.kinds[i + 1] !== 'text' ||
2360
+ !containsArabicScript(withMergedUrls.texts[i + 1])) {
2361
+ continue;
2362
+ }
2363
+ withMergedUrls.texts[i] = split.space;
2364
+ withMergedUrls.isWordLike[i] = false;
2365
+ withMergedUrls.kinds[i] = withMergedUrls.kinds[i] === 'preserved-space' ? 'preserved-space' : 'space';
2366
+ withMergedUrls.texts[i + 1] = split.marks + withMergedUrls.texts[i + 1];
2367
+ withMergedUrls.starts[i + 1] = withMergedUrls.starts[i] + split.space.length;
2368
+ }
2369
+ return withMergedUrls;
2370
+ }
2371
+ function compileAnalysisChunks(segmentation, whiteSpaceProfile) {
2372
+ if (segmentation.len === 0)
2373
+ return [];
2374
+ if (!whiteSpaceProfile.preserveHardBreaks) {
2375
+ return [{
2376
+ startSegmentIndex: 0,
2377
+ endSegmentIndex: segmentation.len,
2378
+ consumedEndSegmentIndex: segmentation.len,
2379
+ }];
2380
+ }
2381
+ const chunks = [];
2382
+ let startSegmentIndex = 0;
2383
+ for (let i = 0; i < segmentation.len; i++) {
2384
+ if (segmentation.kinds[i] !== 'hard-break')
2385
+ continue;
2386
+ chunks.push({
2387
+ startSegmentIndex,
2388
+ endSegmentIndex: i,
2389
+ consumedEndSegmentIndex: i + 1,
2390
+ });
2391
+ startSegmentIndex = i + 1;
2392
+ }
2393
+ if (startSegmentIndex < segmentation.len) {
2394
+ chunks.push({
2395
+ startSegmentIndex,
2396
+ endSegmentIndex: segmentation.len,
2397
+ consumedEndSegmentIndex: segmentation.len,
2398
+ });
2399
+ }
2400
+ return chunks;
2401
+ }
2402
+ function analyzeText(text, profile, whiteSpace = 'normal') {
2403
+ const whiteSpaceProfile = getWhiteSpaceProfile(whiteSpace);
2404
+ const normalized = whiteSpaceProfile.mode === 'pre-wrap'
2405
+ ? normalizeWhitespacePreWrap(text)
2406
+ : normalizeWhitespaceNormal(text);
2407
+ if (normalized.length === 0) {
2408
+ return {
2409
+ normalized,
2410
+ chunks: [],
2411
+ len: 0,
2412
+ texts: [],
2413
+ isWordLike: [],
2414
+ kinds: [],
2415
+ starts: [],
2416
+ };
2417
+ }
2418
+ const segmentation = buildMergedSegmentation(normalized, profile, whiteSpaceProfile);
2419
+ return {
2420
+ normalized,
2421
+ chunks: compileAnalysisChunks(segmentation, whiteSpaceProfile),
2422
+ ...segmentation,
2423
+ };
2424
+ }
2425
+
2426
+ let measureContext = null;
2427
+ const segmentMetricCaches = new Map();
2428
+ let cachedEngineProfile = null;
2429
+ const emojiPresentationRe = /\p{Emoji_Presentation}/u;
2430
+ const maybeEmojiRe = /[\p{Emoji_Presentation}\p{Extended_Pictographic}\p{Regional_Indicator}\uFE0F\u20E3]/u;
2431
+ let sharedGraphemeSegmenter$1 = null;
2432
+ const emojiCorrectionCache = new Map();
2433
+ function getMeasureContext() {
2434
+ if (measureContext !== null)
2435
+ return measureContext;
2436
+ if (typeof OffscreenCanvas !== 'undefined') {
2437
+ measureContext = new OffscreenCanvas(1, 1).getContext('2d');
2438
+ return measureContext;
2439
+ }
2440
+ if (typeof document !== 'undefined') {
2441
+ measureContext = document.createElement('canvas').getContext('2d');
2442
+ return measureContext;
2443
+ }
2444
+ throw new Error('Text measurement requires OffscreenCanvas or a DOM canvas context.');
2445
+ }
2446
+ function getSegmentMetricCache(font) {
2447
+ let cache = segmentMetricCaches.get(font);
2448
+ if (!cache) {
2449
+ cache = new Map();
2450
+ segmentMetricCaches.set(font, cache);
2451
+ }
2452
+ return cache;
2453
+ }
2454
+ function getSegmentMetrics(seg, cache) {
2455
+ let metrics = cache.get(seg);
2456
+ if (metrics === undefined) {
2457
+ const ctx = getMeasureContext();
2458
+ metrics = {
2459
+ width: ctx.measureText(seg).width,
2460
+ containsCJK: isCJK(seg),
2461
+ };
2462
+ cache.set(seg, metrics);
2463
+ }
2464
+ return metrics;
2465
+ }
2466
+ function getEngineProfile() {
2467
+ if (cachedEngineProfile !== null)
2468
+ return cachedEngineProfile;
2469
+ if (typeof navigator === 'undefined') {
2470
+ cachedEngineProfile = {
2471
+ lineFitEpsilon: 0.005,
2472
+ carryCJKAfterClosingQuote: false,
2473
+ preferPrefixWidthsForBreakableRuns: false,
2474
+ preferEarlySoftHyphenBreak: false,
2475
+ };
2476
+ return cachedEngineProfile;
2477
+ }
2478
+ const ua = navigator.userAgent;
2479
+ const vendor = navigator.vendor;
2480
+ const isSafari = vendor === 'Apple Computer, Inc.' &&
2481
+ ua.includes('Safari/') &&
2482
+ !ua.includes('Chrome/') &&
2483
+ !ua.includes('Chromium/') &&
2484
+ !ua.includes('CriOS/') &&
2485
+ !ua.includes('FxiOS/') &&
2486
+ !ua.includes('EdgiOS/');
2487
+ const isChromium = ua.includes('Chrome/') ||
2488
+ ua.includes('Chromium/') ||
2489
+ ua.includes('CriOS/') ||
2490
+ ua.includes('Edg/');
2491
+ cachedEngineProfile = {
2492
+ lineFitEpsilon: isSafari ? 1 / 64 : 0.005,
2493
+ carryCJKAfterClosingQuote: isChromium,
2494
+ preferPrefixWidthsForBreakableRuns: isSafari,
2495
+ preferEarlySoftHyphenBreak: isSafari,
2496
+ };
2497
+ return cachedEngineProfile;
2498
+ }
2499
+ function parseFontSize(font) {
2500
+ const m = font.match(/(\d+(?:\.\d+)?)\s*px/);
2501
+ return m ? parseFloat(m[1]) : 16;
2502
+ }
2503
+ function getSharedGraphemeSegmenter$1() {
2504
+ if (sharedGraphemeSegmenter$1 === null) {
2505
+ sharedGraphemeSegmenter$1 = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
2506
+ }
2507
+ return sharedGraphemeSegmenter$1;
2508
+ }
2509
+ function isEmojiGrapheme(g) {
2510
+ return emojiPresentationRe.test(g) || g.includes('\uFE0F');
2511
+ }
2512
+ function textMayContainEmoji(text) {
2513
+ return maybeEmojiRe.test(text);
2514
+ }
2515
+ function getEmojiCorrection(font, fontSize) {
2516
+ let correction = emojiCorrectionCache.get(font);
2517
+ if (correction !== undefined)
2518
+ return correction;
2519
+ const ctx = getMeasureContext();
2520
+ ctx.font = font;
2521
+ const canvasW = ctx.measureText('\u{1F600}').width;
2522
+ correction = 0;
2523
+ if (canvasW > fontSize + 0.5 &&
2524
+ typeof document !== 'undefined' &&
2525
+ document.body !== null) {
2526
+ const span = document.createElement('span');
2527
+ span.style.font = font;
2528
+ span.style.display = 'inline-block';
2529
+ span.style.visibility = 'hidden';
2530
+ span.style.position = 'absolute';
2531
+ span.textContent = '\u{1F600}';
2532
+ document.body.appendChild(span);
2533
+ const domW = span.getBoundingClientRect().width;
2534
+ document.body.removeChild(span);
2535
+ if (canvasW - domW > 0.5) {
2536
+ correction = canvasW - domW;
2537
+ }
2538
+ }
2539
+ emojiCorrectionCache.set(font, correction);
2540
+ return correction;
2541
+ }
2542
+ function countEmojiGraphemes(text) {
2543
+ let count = 0;
2544
+ const graphemeSegmenter = getSharedGraphemeSegmenter$1();
2545
+ for (const g of graphemeSegmenter.segment(text)) {
2546
+ if (isEmojiGrapheme(g.segment))
2547
+ count++;
2548
+ }
2549
+ return count;
2550
+ }
2551
+ function getEmojiCount(seg, metrics) {
2552
+ if (metrics.emojiCount === undefined) {
2553
+ metrics.emojiCount = countEmojiGraphemes(seg);
2554
+ }
2555
+ return metrics.emojiCount;
2556
+ }
2557
+ function getCorrectedSegmentWidth(seg, metrics, emojiCorrection) {
2558
+ if (emojiCorrection === 0)
2559
+ return metrics.width;
2560
+ return metrics.width - getEmojiCount(seg, metrics) * emojiCorrection;
2561
+ }
2562
+ function getSegmentGraphemeWidths(seg, metrics, cache, emojiCorrection) {
2563
+ if (metrics.graphemeWidths !== undefined)
2564
+ return metrics.graphemeWidths;
2565
+ const widths = [];
2566
+ const graphemeSegmenter = getSharedGraphemeSegmenter$1();
2567
+ for (const gs of graphemeSegmenter.segment(seg)) {
2568
+ const graphemeMetrics = getSegmentMetrics(gs.segment, cache);
2569
+ widths.push(getCorrectedSegmentWidth(gs.segment, graphemeMetrics, emojiCorrection));
2570
+ }
2571
+ metrics.graphemeWidths = widths.length > 1 ? widths : null;
2572
+ return metrics.graphemeWidths;
2573
+ }
2574
+ function getSegmentGraphemePrefixWidths(seg, metrics, cache, emojiCorrection) {
2575
+ if (metrics.graphemePrefixWidths !== undefined)
2576
+ return metrics.graphemePrefixWidths;
2577
+ const prefixWidths = [];
2578
+ const graphemeSegmenter = getSharedGraphemeSegmenter$1();
2579
+ let prefix = '';
2580
+ for (const gs of graphemeSegmenter.segment(seg)) {
2581
+ prefix += gs.segment;
2582
+ const prefixMetrics = getSegmentMetrics(prefix, cache);
2583
+ prefixWidths.push(getCorrectedSegmentWidth(prefix, prefixMetrics, emojiCorrection));
2584
+ }
2585
+ metrics.graphemePrefixWidths = prefixWidths.length > 1 ? prefixWidths : null;
2586
+ return metrics.graphemePrefixWidths;
2587
+ }
2588
+ function getFontMeasurementState(font, needsEmojiCorrection) {
2589
+ const ctx = getMeasureContext();
2590
+ ctx.font = font;
2591
+ const cache = getSegmentMetricCache(font);
2592
+ const fontSize = parseFontSize(font);
2593
+ const emojiCorrection = needsEmojiCorrection ? getEmojiCorrection(font, fontSize) : 0;
2594
+ return { cache, fontSize, emojiCorrection };
2595
+ }
2596
+
2597
+ function canBreakAfter(kind) {
2598
+ return (kind === 'space' ||
2599
+ kind === 'preserved-space' ||
2600
+ kind === 'tab' ||
2601
+ kind === 'zero-width-break' ||
2602
+ kind === 'soft-hyphen');
2603
+ }
2604
+ function normalizeSimpleLineStartSegmentIndex(prepared, segmentIndex) {
2605
+ while (segmentIndex < prepared.widths.length) {
2606
+ const kind = prepared.kinds[segmentIndex];
2607
+ if (kind !== 'space' && kind !== 'zero-width-break' && kind !== 'soft-hyphen')
2608
+ break;
2609
+ segmentIndex++;
2610
+ }
2611
+ return segmentIndex;
2612
+ }
2613
+ function getTabAdvance(lineWidth, tabStopAdvance) {
2614
+ if (tabStopAdvance <= 0)
2615
+ return 0;
2616
+ const remainder = lineWidth % tabStopAdvance;
2617
+ if (Math.abs(remainder) <= 1e-6)
2618
+ return tabStopAdvance;
2619
+ return tabStopAdvance - remainder;
2620
+ }
2621
+ function getBreakableAdvance(graphemeWidths, graphemePrefixWidths, graphemeIndex, preferPrefixWidths) {
2622
+ if (!preferPrefixWidths || graphemePrefixWidths === null) {
2623
+ return graphemeWidths[graphemeIndex];
2624
+ }
2625
+ return graphemePrefixWidths[graphemeIndex] - (graphemeIndex > 0 ? graphemePrefixWidths[graphemeIndex - 1] : 0);
2626
+ }
2627
+ function fitSoftHyphenBreak(graphemeWidths, initialWidth, maxWidth, lineFitEpsilon, discretionaryHyphenWidth, cumulativeWidths) {
2628
+ let fitCount = 0;
2629
+ let fittedWidth = initialWidth;
2630
+ while (fitCount < graphemeWidths.length) {
2631
+ const nextWidth = cumulativeWidths
2632
+ ? initialWidth + graphemeWidths[fitCount]
2633
+ : fittedWidth + graphemeWidths[fitCount];
2634
+ const nextLineWidth = fitCount + 1 < graphemeWidths.length
2635
+ ? nextWidth + discretionaryHyphenWidth
2636
+ : nextWidth;
2637
+ if (nextLineWidth > maxWidth + lineFitEpsilon)
2638
+ break;
2639
+ fittedWidth = nextWidth;
2640
+ fitCount++;
2641
+ }
2642
+ return { fitCount, fittedWidth };
2643
+ }
2644
+ function walkPreparedLinesSimple(prepared, maxWidth, onLine) {
2645
+ const { widths, kinds, breakableWidths, breakablePrefixWidths } = prepared;
2646
+ if (widths.length === 0)
2647
+ return 0;
2648
+ const engineProfile = getEngineProfile();
2649
+ const lineFitEpsilon = engineProfile.lineFitEpsilon;
2650
+ let lineCount = 0;
2651
+ let lineW = 0;
2652
+ let hasContent = false;
2653
+ let lineStartSegmentIndex = 0;
2654
+ let lineStartGraphemeIndex = 0;
2655
+ let lineEndSegmentIndex = 0;
2656
+ let lineEndGraphemeIndex = 0;
2657
+ let pendingBreakSegmentIndex = -1;
2658
+ let pendingBreakPaintWidth = 0;
2659
+ function clearPendingBreak() {
2660
+ pendingBreakSegmentIndex = -1;
2661
+ pendingBreakPaintWidth = 0;
2662
+ }
2663
+ function emitCurrentLine(endSegmentIndex = lineEndSegmentIndex, endGraphemeIndex = lineEndGraphemeIndex, width = lineW) {
2664
+ lineCount++;
2665
+ onLine?.({
2666
+ startSegmentIndex: lineStartSegmentIndex,
2667
+ startGraphemeIndex: lineStartGraphemeIndex,
2668
+ endSegmentIndex,
2669
+ endGraphemeIndex,
2670
+ width,
2671
+ });
2672
+ lineW = 0;
2673
+ hasContent = false;
2674
+ clearPendingBreak();
2675
+ }
2676
+ function startLineAtSegment(segmentIndex, width) {
2677
+ hasContent = true;
2678
+ lineStartSegmentIndex = segmentIndex;
2679
+ lineStartGraphemeIndex = 0;
2680
+ lineEndSegmentIndex = segmentIndex + 1;
2681
+ lineEndGraphemeIndex = 0;
2682
+ lineW = width;
2683
+ }
2684
+ function startLineAtGrapheme(segmentIndex, graphemeIndex, width) {
2685
+ hasContent = true;
2686
+ lineStartSegmentIndex = segmentIndex;
2687
+ lineStartGraphemeIndex = graphemeIndex;
2688
+ lineEndSegmentIndex = segmentIndex;
2689
+ lineEndGraphemeIndex = graphemeIndex + 1;
2690
+ lineW = width;
2691
+ }
2692
+ function appendWholeSegment(segmentIndex, width) {
2693
+ if (!hasContent) {
2694
+ startLineAtSegment(segmentIndex, width);
2695
+ return;
2696
+ }
2697
+ lineW += width;
2698
+ lineEndSegmentIndex = segmentIndex + 1;
2699
+ lineEndGraphemeIndex = 0;
2700
+ }
2701
+ function updatePendingBreak(segmentIndex, segmentWidth) {
2702
+ if (!canBreakAfter(kinds[segmentIndex]))
2703
+ return;
2704
+ pendingBreakSegmentIndex = segmentIndex + 1;
2705
+ pendingBreakPaintWidth = lineW - segmentWidth;
2706
+ }
2707
+ function appendBreakableSegment(segmentIndex) {
2708
+ appendBreakableSegmentFrom(segmentIndex, 0);
2709
+ }
2710
+ function appendBreakableSegmentFrom(segmentIndex, startGraphemeIndex) {
2711
+ const gWidths = breakableWidths[segmentIndex];
2712
+ const gPrefixWidths = breakablePrefixWidths[segmentIndex] ?? null;
2713
+ for (let g = startGraphemeIndex; g < gWidths.length; g++) {
2714
+ const gw = getBreakableAdvance(gWidths, gPrefixWidths, g, engineProfile.preferPrefixWidthsForBreakableRuns);
2715
+ if (!hasContent) {
2716
+ startLineAtGrapheme(segmentIndex, g, gw);
2717
+ continue;
2718
+ }
2719
+ if (lineW + gw > maxWidth + lineFitEpsilon) {
2720
+ emitCurrentLine();
2721
+ startLineAtGrapheme(segmentIndex, g, gw);
2722
+ }
2723
+ else {
2724
+ lineW += gw;
2725
+ lineEndSegmentIndex = segmentIndex;
2726
+ lineEndGraphemeIndex = g + 1;
2727
+ }
2728
+ }
2729
+ if (hasContent && lineEndSegmentIndex === segmentIndex && lineEndGraphemeIndex === gWidths.length) {
2730
+ lineEndSegmentIndex = segmentIndex + 1;
2731
+ lineEndGraphemeIndex = 0;
2732
+ }
2733
+ }
2734
+ let i = 0;
2735
+ while (i < widths.length) {
2736
+ if (!hasContent) {
2737
+ i = normalizeSimpleLineStartSegmentIndex(prepared, i);
2738
+ if (i >= widths.length)
2739
+ break;
2740
+ }
2741
+ const w = widths[i];
2742
+ const kind = kinds[i];
2743
+ if (!hasContent) {
2744
+ if (w > maxWidth && breakableWidths[i] !== null) {
2745
+ appendBreakableSegment(i);
2746
+ }
2747
+ else {
2748
+ startLineAtSegment(i, w);
2749
+ }
2750
+ updatePendingBreak(i, w);
2751
+ i++;
2752
+ continue;
2753
+ }
2754
+ const newW = lineW + w;
2755
+ if (newW > maxWidth + lineFitEpsilon) {
2756
+ if (canBreakAfter(kind)) {
2757
+ appendWholeSegment(i, w);
2758
+ emitCurrentLine(i + 1, 0, lineW - w);
2759
+ i++;
2760
+ continue;
2761
+ }
2762
+ if (pendingBreakSegmentIndex >= 0) {
2763
+ if (lineEndSegmentIndex > pendingBreakSegmentIndex ||
2764
+ (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)) {
2765
+ emitCurrentLine();
2766
+ continue;
2767
+ }
2768
+ emitCurrentLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth);
2769
+ continue;
2770
+ }
2771
+ if (w > maxWidth && breakableWidths[i] !== null) {
2772
+ emitCurrentLine();
2773
+ appendBreakableSegment(i);
2774
+ i++;
2775
+ continue;
2776
+ }
2777
+ emitCurrentLine();
2778
+ continue;
2779
+ }
2780
+ appendWholeSegment(i, w);
2781
+ updatePendingBreak(i, w);
2782
+ i++;
2783
+ }
2784
+ if (hasContent)
2785
+ emitCurrentLine();
2786
+ return lineCount;
2787
+ }
2788
+ function walkPreparedLines(prepared, maxWidth, onLine) {
2789
+ if (prepared.simpleLineWalkFastPath) {
2790
+ return walkPreparedLinesSimple(prepared, maxWidth, onLine);
2791
+ }
2792
+ const { widths, lineEndFitAdvances, lineEndPaintAdvances, kinds, breakableWidths, breakablePrefixWidths, discretionaryHyphenWidth, tabStopAdvance, chunks, } = prepared;
2793
+ if (widths.length === 0 || chunks.length === 0)
2794
+ return 0;
2795
+ const engineProfile = getEngineProfile();
2796
+ const lineFitEpsilon = engineProfile.lineFitEpsilon;
2797
+ let lineCount = 0;
2798
+ let lineW = 0;
2799
+ let hasContent = false;
2800
+ let lineStartSegmentIndex = 0;
2801
+ let lineStartGraphemeIndex = 0;
2802
+ let lineEndSegmentIndex = 0;
2803
+ let lineEndGraphemeIndex = 0;
2804
+ let pendingBreakSegmentIndex = -1;
2805
+ let pendingBreakFitWidth = 0;
2806
+ let pendingBreakPaintWidth = 0;
2807
+ let pendingBreakKind = null;
2808
+ function clearPendingBreak() {
2809
+ pendingBreakSegmentIndex = -1;
2810
+ pendingBreakFitWidth = 0;
2811
+ pendingBreakPaintWidth = 0;
2812
+ pendingBreakKind = null;
2813
+ }
2814
+ function emitCurrentLine(endSegmentIndex = lineEndSegmentIndex, endGraphemeIndex = lineEndGraphemeIndex, width = lineW) {
2815
+ lineCount++;
2816
+ onLine?.({
2817
+ startSegmentIndex: lineStartSegmentIndex,
2818
+ startGraphemeIndex: lineStartGraphemeIndex,
2819
+ endSegmentIndex,
2820
+ endGraphemeIndex,
2821
+ width,
2822
+ });
2823
+ lineW = 0;
2824
+ hasContent = false;
2825
+ clearPendingBreak();
2826
+ }
2827
+ function startLineAtSegment(segmentIndex, width) {
2828
+ hasContent = true;
2829
+ lineStartSegmentIndex = segmentIndex;
2830
+ lineStartGraphemeIndex = 0;
2831
+ lineEndSegmentIndex = segmentIndex + 1;
2832
+ lineEndGraphemeIndex = 0;
2833
+ lineW = width;
2834
+ }
2835
+ function startLineAtGrapheme(segmentIndex, graphemeIndex, width) {
2836
+ hasContent = true;
2837
+ lineStartSegmentIndex = segmentIndex;
2838
+ lineStartGraphemeIndex = graphemeIndex;
2839
+ lineEndSegmentIndex = segmentIndex;
2840
+ lineEndGraphemeIndex = graphemeIndex + 1;
2841
+ lineW = width;
2842
+ }
2843
+ function appendWholeSegment(segmentIndex, width) {
2844
+ if (!hasContent) {
2845
+ startLineAtSegment(segmentIndex, width);
2846
+ return;
2847
+ }
2848
+ lineW += width;
2849
+ lineEndSegmentIndex = segmentIndex + 1;
2850
+ lineEndGraphemeIndex = 0;
2851
+ }
2852
+ function updatePendingBreakForWholeSegment(segmentIndex, segmentWidth) {
2853
+ if (!canBreakAfter(kinds[segmentIndex]))
2854
+ return;
2855
+ const fitAdvance = kinds[segmentIndex] === 'tab' ? 0 : lineEndFitAdvances[segmentIndex];
2856
+ const paintAdvance = kinds[segmentIndex] === 'tab' ? segmentWidth : lineEndPaintAdvances[segmentIndex];
2857
+ pendingBreakSegmentIndex = segmentIndex + 1;
2858
+ pendingBreakFitWidth = lineW - segmentWidth + fitAdvance;
2859
+ pendingBreakPaintWidth = lineW - segmentWidth + paintAdvance;
2860
+ pendingBreakKind = kinds[segmentIndex];
2861
+ }
2862
+ function appendBreakableSegment(segmentIndex) {
2863
+ appendBreakableSegmentFrom(segmentIndex, 0);
2864
+ }
2865
+ function appendBreakableSegmentFrom(segmentIndex, startGraphemeIndex) {
2866
+ const gWidths = breakableWidths[segmentIndex];
2867
+ const gPrefixWidths = breakablePrefixWidths[segmentIndex] ?? null;
2868
+ for (let g = startGraphemeIndex; g < gWidths.length; g++) {
2869
+ const gw = getBreakableAdvance(gWidths, gPrefixWidths, g, engineProfile.preferPrefixWidthsForBreakableRuns);
2870
+ if (!hasContent) {
2871
+ startLineAtGrapheme(segmentIndex, g, gw);
2872
+ continue;
2873
+ }
2874
+ if (lineW + gw > maxWidth + lineFitEpsilon) {
2875
+ emitCurrentLine();
2876
+ startLineAtGrapheme(segmentIndex, g, gw);
2877
+ }
2878
+ else {
2879
+ lineW += gw;
2880
+ lineEndSegmentIndex = segmentIndex;
2881
+ lineEndGraphemeIndex = g + 1;
2882
+ }
2883
+ }
2884
+ if (hasContent && lineEndSegmentIndex === segmentIndex && lineEndGraphemeIndex === gWidths.length) {
2885
+ lineEndSegmentIndex = segmentIndex + 1;
2886
+ lineEndGraphemeIndex = 0;
2887
+ }
2888
+ }
2889
+ function continueSoftHyphenBreakableSegment(segmentIndex) {
2890
+ if (pendingBreakKind !== 'soft-hyphen')
2891
+ return false;
2892
+ const gWidths = breakableWidths[segmentIndex];
2893
+ if (gWidths === null)
2894
+ return false;
2895
+ const fitWidths = engineProfile.preferPrefixWidthsForBreakableRuns
2896
+ ? breakablePrefixWidths[segmentIndex] ?? gWidths
2897
+ : gWidths;
2898
+ const usesPrefixWidths = fitWidths !== gWidths;
2899
+ const { fitCount, fittedWidth } = fitSoftHyphenBreak(fitWidths, lineW, maxWidth, lineFitEpsilon, discretionaryHyphenWidth, usesPrefixWidths);
2900
+ if (fitCount === 0)
2901
+ return false;
2902
+ lineW = fittedWidth;
2903
+ lineEndSegmentIndex = segmentIndex;
2904
+ lineEndGraphemeIndex = fitCount;
2905
+ clearPendingBreak();
2906
+ if (fitCount === gWidths.length) {
2907
+ lineEndSegmentIndex = segmentIndex + 1;
2908
+ lineEndGraphemeIndex = 0;
2909
+ return true;
2910
+ }
2911
+ emitCurrentLine(segmentIndex, fitCount, fittedWidth + discretionaryHyphenWidth);
2912
+ appendBreakableSegmentFrom(segmentIndex, fitCount);
2913
+ return true;
2914
+ }
2915
+ function emitEmptyChunk(chunk) {
2916
+ lineCount++;
2917
+ onLine?.({
2918
+ startSegmentIndex: chunk.startSegmentIndex,
2919
+ startGraphemeIndex: 0,
2920
+ endSegmentIndex: chunk.consumedEndSegmentIndex,
2921
+ endGraphemeIndex: 0,
2922
+ width: 0,
2923
+ });
2924
+ clearPendingBreak();
2925
+ }
2926
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
2927
+ const chunk = chunks[chunkIndex];
2928
+ if (chunk.startSegmentIndex === chunk.endSegmentIndex) {
2929
+ emitEmptyChunk(chunk);
2930
+ continue;
2931
+ }
2932
+ hasContent = false;
2933
+ lineW = 0;
2934
+ lineStartSegmentIndex = chunk.startSegmentIndex;
2935
+ lineStartGraphemeIndex = 0;
2936
+ lineEndSegmentIndex = chunk.startSegmentIndex;
2937
+ lineEndGraphemeIndex = 0;
2938
+ clearPendingBreak();
2939
+ let i = chunk.startSegmentIndex;
2940
+ while (i < chunk.endSegmentIndex) {
2941
+ const kind = kinds[i];
2942
+ const w = kind === 'tab' ? getTabAdvance(lineW, tabStopAdvance) : widths[i];
2943
+ if (kind === 'soft-hyphen') {
2944
+ if (hasContent) {
2945
+ lineEndSegmentIndex = i + 1;
2946
+ lineEndGraphemeIndex = 0;
2947
+ pendingBreakSegmentIndex = i + 1;
2948
+ pendingBreakFitWidth = lineW + discretionaryHyphenWidth;
2949
+ pendingBreakPaintWidth = lineW + discretionaryHyphenWidth;
2950
+ pendingBreakKind = kind;
2951
+ }
2952
+ i++;
2953
+ continue;
2954
+ }
2955
+ if (!hasContent) {
2956
+ if (w > maxWidth && breakableWidths[i] !== null) {
2957
+ appendBreakableSegment(i);
2958
+ }
2959
+ else {
2960
+ startLineAtSegment(i, w);
2961
+ }
2962
+ updatePendingBreakForWholeSegment(i, w);
2963
+ i++;
2964
+ continue;
2965
+ }
2966
+ const newW = lineW + w;
2967
+ if (newW > maxWidth + lineFitEpsilon) {
2968
+ const currentBreakFitWidth = lineW + (kind === 'tab' ? 0 : lineEndFitAdvances[i]);
2969
+ const currentBreakPaintWidth = lineW + (kind === 'tab' ? w : lineEndPaintAdvances[i]);
2970
+ if (pendingBreakKind === 'soft-hyphen' &&
2971
+ engineProfile.preferEarlySoftHyphenBreak &&
2972
+ pendingBreakFitWidth <= maxWidth + lineFitEpsilon) {
2973
+ emitCurrentLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth);
2974
+ continue;
2975
+ }
2976
+ if (pendingBreakKind === 'soft-hyphen' && continueSoftHyphenBreakableSegment(i)) {
2977
+ i++;
2978
+ continue;
2979
+ }
2980
+ if (canBreakAfter(kind) && currentBreakFitWidth <= maxWidth + lineFitEpsilon) {
2981
+ appendWholeSegment(i, w);
2982
+ emitCurrentLine(i + 1, 0, currentBreakPaintWidth);
2983
+ i++;
2984
+ continue;
2985
+ }
2986
+ if (pendingBreakSegmentIndex >= 0 && pendingBreakFitWidth <= maxWidth + lineFitEpsilon) {
2987
+ if (lineEndSegmentIndex > pendingBreakSegmentIndex ||
2988
+ (lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0)) {
2989
+ emitCurrentLine();
2990
+ continue;
2991
+ }
2992
+ const nextSegmentIndex = pendingBreakSegmentIndex;
2993
+ emitCurrentLine(nextSegmentIndex, 0, pendingBreakPaintWidth);
2994
+ i = nextSegmentIndex;
2995
+ continue;
2996
+ }
2997
+ if (w > maxWidth && breakableWidths[i] !== null) {
2998
+ emitCurrentLine();
2999
+ appendBreakableSegment(i);
3000
+ i++;
3001
+ continue;
3002
+ }
3003
+ emitCurrentLine();
3004
+ continue;
3005
+ }
3006
+ appendWholeSegment(i, w);
3007
+ updatePendingBreakForWholeSegment(i, w);
3008
+ i++;
3009
+ }
3010
+ if (hasContent) {
3011
+ const finalPaintWidth = pendingBreakSegmentIndex === chunk.consumedEndSegmentIndex
3012
+ ? pendingBreakPaintWidth
3013
+ : lineW;
3014
+ emitCurrentLine(chunk.consumedEndSegmentIndex, 0, finalPaintWidth);
3015
+ }
3016
+ }
3017
+ return lineCount;
3018
+ }
3019
+
3020
+ // Text measurement for browser environments using canvas measureText.
3021
+ //
3022
+ // Problem: DOM-based text measurement (getBoundingClientRect, offsetHeight)
3023
+ // forces synchronous layout reflow. When components independently measure text,
3024
+ // each measurement triggers a reflow of the entire document. This creates
3025
+ // read/write interleaving that can cost 30ms+ per frame for 500 text blocks.
3026
+ //
3027
+ // Solution: two-phase measurement centered around canvas measureText.
3028
+ // prepare(text, font) — segments text via Intl.Segmenter, measures each word
3029
+ // via canvas, caches widths, and does one cached DOM calibration read per
3030
+ // font when emoji correction is needed. Call once when text first appears.
3031
+ // layout(prepared, maxWidth, lineHeight) — walks cached word widths with pure
3032
+ // arithmetic to count lines and compute height. Call on every resize.
3033
+ // ~0.0002ms per text.
3034
+ //
3035
+ // i18n: Intl.Segmenter handles CJK (per-character breaking), Thai, Arabic, etc.
3036
+ // Bidi: simplified rich-path metadata for mixed LTR/RTL custom rendering.
3037
+ // Punctuation merging: "better." measured as one unit (matches CSS behavior).
3038
+ // Trailing whitespace: hangs past line edge without triggering breaks (CSS behavior).
3039
+ // overflow-wrap: pre-measured grapheme widths enable character-level word breaking.
3040
+ //
3041
+ // Emoji correction: Chrome/Firefox canvas measures emoji wider than DOM at font
3042
+ // sizes <24px on macOS (Apple Color Emoji). The inflation is constant per emoji
3043
+ // grapheme at a given size, font-independent. Auto-detected by comparing canvas
3044
+ // vs actual DOM emoji width (one cached DOM read per font). Safari canvas and
3045
+ // DOM agree (both wider than fontSize), so correction = 0 there.
3046
+ //
3047
+ // Limitations:
3048
+ // - system-ui font: canvas resolves to different optical variants than DOM on macOS.
3049
+ // Use named fonts (Helvetica, Inter, etc.) for guaranteed accuracy.
3050
+ // See RESEARCH.md "Discovery: system-ui font resolution mismatch".
3051
+ //
3052
+ // Based on Sebastian Markbage's text-layout research (github.com/chenglou/text-layout).
3053
+ let sharedGraphemeSegmenter = null;
3054
+ // Rich-path only. Reuses grapheme splits while materializing multiple lines
3055
+ // from the same prepared handle, without pushing that cache into the API.
3056
+ let sharedLineTextCaches = new WeakMap();
3057
+ function getSharedGraphemeSegmenter() {
3058
+ if (sharedGraphemeSegmenter === null) {
3059
+ sharedGraphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
3060
+ }
3061
+ return sharedGraphemeSegmenter;
3062
+ }
3063
+ // --- Public API ---
3064
+ function createEmptyPrepared(includeSegments) {
3065
+ {
3066
+ return {
3067
+ widths: [],
3068
+ lineEndFitAdvances: [],
3069
+ lineEndPaintAdvances: [],
3070
+ kinds: [],
3071
+ simpleLineWalkFastPath: true,
3072
+ segLevels: null,
3073
+ breakableWidths: [],
3074
+ breakablePrefixWidths: [],
3075
+ discretionaryHyphenWidth: 0,
3076
+ tabStopAdvance: 0,
3077
+ chunks: [],
3078
+ segments: [],
3079
+ };
3080
+ }
3081
+ }
3082
+ function measureAnalysis(analysis, font, includeSegments) {
3083
+ const graphemeSegmenter = getSharedGraphemeSegmenter();
3084
+ const engineProfile = getEngineProfile();
3085
+ const { cache, emojiCorrection } = getFontMeasurementState(font, textMayContainEmoji(analysis.normalized));
3086
+ const discretionaryHyphenWidth = getCorrectedSegmentWidth('-', getSegmentMetrics('-', cache), emojiCorrection);
3087
+ const spaceWidth = getCorrectedSegmentWidth(' ', getSegmentMetrics(' ', cache), emojiCorrection);
3088
+ const tabStopAdvance = spaceWidth * 8;
3089
+ if (analysis.len === 0)
3090
+ return createEmptyPrepared();
3091
+ const widths = [];
3092
+ const lineEndFitAdvances = [];
3093
+ const lineEndPaintAdvances = [];
3094
+ const kinds = [];
3095
+ let simpleLineWalkFastPath = analysis.chunks.length <= 1;
3096
+ const segStarts = [] ;
3097
+ const breakableWidths = [];
3098
+ const breakablePrefixWidths = [];
3099
+ const segments = includeSegments ? [] : null;
3100
+ const preparedStartByAnalysisIndex = Array.from({ length: analysis.len });
3101
+ const preparedEndByAnalysisIndex = Array.from({ length: analysis.len });
3102
+ function pushMeasuredSegment(text, width, lineEndFitAdvance, lineEndPaintAdvance, kind, start, breakable, breakablePrefix) {
3103
+ if (kind !== 'text' && kind !== 'space' && kind !== 'zero-width-break') {
3104
+ simpleLineWalkFastPath = false;
3105
+ }
3106
+ widths.push(width);
3107
+ lineEndFitAdvances.push(lineEndFitAdvance);
3108
+ lineEndPaintAdvances.push(lineEndPaintAdvance);
3109
+ kinds.push(kind);
3110
+ segStarts?.push(start);
3111
+ breakableWidths.push(breakable);
3112
+ breakablePrefixWidths.push(breakablePrefix);
3113
+ if (segments !== null)
3114
+ segments.push(text);
3115
+ }
3116
+ for (let mi = 0; mi < analysis.len; mi++) {
3117
+ preparedStartByAnalysisIndex[mi] = widths.length;
3118
+ const segText = analysis.texts[mi];
3119
+ const segWordLike = analysis.isWordLike[mi];
3120
+ const segKind = analysis.kinds[mi];
3121
+ const segStart = analysis.starts[mi];
3122
+ if (segKind === 'soft-hyphen') {
3123
+ pushMeasuredSegment(segText, 0, discretionaryHyphenWidth, discretionaryHyphenWidth, segKind, segStart, null, null);
3124
+ preparedEndByAnalysisIndex[mi] = widths.length;
3125
+ continue;
3126
+ }
3127
+ if (segKind === 'hard-break') {
3128
+ pushMeasuredSegment(segText, 0, 0, 0, segKind, segStart, null, null);
3129
+ preparedEndByAnalysisIndex[mi] = widths.length;
3130
+ continue;
3131
+ }
3132
+ if (segKind === 'tab') {
3133
+ pushMeasuredSegment(segText, 0, 0, 0, segKind, segStart, null, null);
3134
+ preparedEndByAnalysisIndex[mi] = widths.length;
3135
+ continue;
3136
+ }
3137
+ const segMetrics = getSegmentMetrics(segText, cache);
3138
+ if (segKind === 'text' && segMetrics.containsCJK) {
3139
+ let unitText = '';
3140
+ let unitStart = 0;
3141
+ for (const gs of graphemeSegmenter.segment(segText)) {
3142
+ const grapheme = gs.segment;
3143
+ if (unitText.length === 0) {
3144
+ unitText = grapheme;
3145
+ unitStart = gs.index;
3146
+ continue;
3147
+ }
3148
+ if (kinsokuEnd.has(unitText) ||
3149
+ kinsokuStart.has(grapheme) ||
3150
+ leftStickyPunctuation.has(grapheme) ||
3151
+ (engineProfile.carryCJKAfterClosingQuote &&
3152
+ isCJK(grapheme) &&
3153
+ endsWithClosingQuote(unitText))) {
3154
+ unitText += grapheme;
3155
+ continue;
3156
+ }
3157
+ const unitMetrics = getSegmentMetrics(unitText, cache);
3158
+ const w = getCorrectedSegmentWidth(unitText, unitMetrics, emojiCorrection);
3159
+ pushMeasuredSegment(unitText, w, w, w, 'text', segStart + unitStart, null, null);
3160
+ unitText = grapheme;
3161
+ unitStart = gs.index;
3162
+ }
3163
+ if (unitText.length > 0) {
3164
+ const unitMetrics = getSegmentMetrics(unitText, cache);
3165
+ const w = getCorrectedSegmentWidth(unitText, unitMetrics, emojiCorrection);
3166
+ pushMeasuredSegment(unitText, w, w, w, 'text', segStart + unitStart, null, null);
3167
+ }
3168
+ preparedEndByAnalysisIndex[mi] = widths.length;
3169
+ continue;
3170
+ }
3171
+ const w = getCorrectedSegmentWidth(segText, segMetrics, emojiCorrection);
3172
+ const lineEndFitAdvance = segKind === 'space' || segKind === 'preserved-space' || segKind === 'zero-width-break'
3173
+ ? 0
3174
+ : w;
3175
+ const lineEndPaintAdvance = segKind === 'space' || segKind === 'zero-width-break'
3176
+ ? 0
3177
+ : w;
3178
+ if (segWordLike && segText.length > 1) {
3179
+ const graphemeWidths = getSegmentGraphemeWidths(segText, segMetrics, cache, emojiCorrection);
3180
+ const graphemePrefixWidths = engineProfile.preferPrefixWidthsForBreakableRuns
3181
+ ? getSegmentGraphemePrefixWidths(segText, segMetrics, cache, emojiCorrection)
3182
+ : null;
3183
+ pushMeasuredSegment(segText, w, lineEndFitAdvance, lineEndPaintAdvance, segKind, segStart, graphemeWidths, graphemePrefixWidths);
3184
+ }
3185
+ else {
3186
+ pushMeasuredSegment(segText, w, lineEndFitAdvance, lineEndPaintAdvance, segKind, segStart, null, null);
3187
+ }
3188
+ preparedEndByAnalysisIndex[mi] = widths.length;
3189
+ }
3190
+ const chunks = mapAnalysisChunksToPreparedChunks(analysis.chunks, preparedStartByAnalysisIndex, preparedEndByAnalysisIndex);
3191
+ const segLevels = segStarts === null ? null : computeSegmentLevels(analysis.normalized, segStarts);
3192
+ if (segments !== null) {
3193
+ return {
3194
+ widths,
3195
+ lineEndFitAdvances,
3196
+ lineEndPaintAdvances,
3197
+ kinds,
3198
+ simpleLineWalkFastPath,
3199
+ segLevels,
3200
+ breakableWidths,
3201
+ breakablePrefixWidths,
3202
+ discretionaryHyphenWidth,
3203
+ tabStopAdvance,
3204
+ chunks,
3205
+ segments,
3206
+ };
3207
+ }
3208
+ return {
3209
+ widths,
3210
+ lineEndFitAdvances,
3211
+ lineEndPaintAdvances,
3212
+ kinds,
3213
+ simpleLineWalkFastPath,
3214
+ segLevels,
3215
+ breakableWidths,
3216
+ breakablePrefixWidths,
3217
+ discretionaryHyphenWidth,
3218
+ tabStopAdvance,
3219
+ chunks,
3220
+ };
3221
+ }
3222
+ function mapAnalysisChunksToPreparedChunks(chunks, preparedStartByAnalysisIndex, preparedEndByAnalysisIndex) {
3223
+ const preparedChunks = [];
3224
+ for (let i = 0; i < chunks.length; i++) {
3225
+ const chunk = chunks[i];
3226
+ const startSegmentIndex = chunk.startSegmentIndex < preparedStartByAnalysisIndex.length
3227
+ ? preparedStartByAnalysisIndex[chunk.startSegmentIndex]
3228
+ : preparedEndByAnalysisIndex[preparedEndByAnalysisIndex.length - 1] ?? 0;
3229
+ const endSegmentIndex = chunk.endSegmentIndex < preparedStartByAnalysisIndex.length
3230
+ ? preparedStartByAnalysisIndex[chunk.endSegmentIndex]
3231
+ : preparedEndByAnalysisIndex[preparedEndByAnalysisIndex.length - 1] ?? 0;
3232
+ const consumedEndSegmentIndex = chunk.consumedEndSegmentIndex < preparedStartByAnalysisIndex.length
3233
+ ? preparedStartByAnalysisIndex[chunk.consumedEndSegmentIndex]
3234
+ : preparedEndByAnalysisIndex[preparedEndByAnalysisIndex.length - 1] ?? 0;
3235
+ preparedChunks.push({
3236
+ startSegmentIndex,
3237
+ endSegmentIndex,
3238
+ consumedEndSegmentIndex,
3239
+ });
3240
+ }
3241
+ return preparedChunks;
3242
+ }
3243
+ function prepareInternal(text, font, includeSegments, options) {
3244
+ const analysis = analyzeText(text, getEngineProfile(), options?.whiteSpace);
3245
+ return measureAnalysis(analysis, font, includeSegments);
3246
+ }
3247
+ // Rich variant used by callers that need enough information to render the
3248
+ // laid-out lines themselves.
3249
+ function prepareWithSegments(text, font, options) {
3250
+ return prepareInternal(text, font, true, options);
3251
+ }
3252
+ function getInternalPrepared(prepared) {
3253
+ return prepared;
3254
+ }
3255
+ function getSegmentGraphemes(segmentIndex, segments, cache) {
3256
+ let graphemes = cache.get(segmentIndex);
3257
+ if (graphemes !== undefined)
3258
+ return graphemes;
3259
+ graphemes = [];
3260
+ const graphemeSegmenter = getSharedGraphemeSegmenter();
3261
+ for (const gs of graphemeSegmenter.segment(segments[segmentIndex])) {
3262
+ graphemes.push(gs.segment);
3263
+ }
3264
+ cache.set(segmentIndex, graphemes);
3265
+ return graphemes;
3266
+ }
3267
+ function getLineTextCache(prepared) {
3268
+ let cache = sharedLineTextCaches.get(prepared);
3269
+ if (cache !== undefined)
3270
+ return cache;
3271
+ cache = new Map();
3272
+ sharedLineTextCaches.set(prepared, cache);
3273
+ return cache;
3274
+ }
3275
+ function lineHasDiscretionaryHyphen(kinds, startSegmentIndex, startGraphemeIndex, endSegmentIndex) {
3276
+ return (endSegmentIndex > 0 &&
3277
+ kinds[endSegmentIndex - 1] === 'soft-hyphen' &&
3278
+ !(startSegmentIndex === endSegmentIndex && startGraphemeIndex > 0));
3279
+ }
3280
+ function buildLineTextFromRange(segments, kinds, cache, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) {
3281
+ let text = '';
3282
+ const endsWithDiscretionaryHyphen = lineHasDiscretionaryHyphen(kinds, startSegmentIndex, startGraphemeIndex, endSegmentIndex);
3283
+ for (let i = startSegmentIndex; i < endSegmentIndex; i++) {
3284
+ if (kinds[i] === 'soft-hyphen' || kinds[i] === 'hard-break')
3285
+ continue;
3286
+ if (i === startSegmentIndex && startGraphemeIndex > 0) {
3287
+ text += getSegmentGraphemes(i, segments, cache).slice(startGraphemeIndex).join('');
3288
+ }
3289
+ else {
3290
+ text += segments[i];
3291
+ }
3292
+ }
3293
+ if (endGraphemeIndex > 0) {
3294
+ if (endsWithDiscretionaryHyphen)
3295
+ text += '-';
3296
+ text += getSegmentGraphemes(endSegmentIndex, segments, cache).slice(startSegmentIndex === endSegmentIndex ? startGraphemeIndex : 0, endGraphemeIndex).join('');
3297
+ }
3298
+ else if (endsWithDiscretionaryHyphen) {
3299
+ text += '-';
3300
+ }
3301
+ return text;
3302
+ }
3303
+ function createLayoutLine(prepared, cache, width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) {
3304
+ return {
3305
+ text: buildLineTextFromRange(prepared.segments, prepared.kinds, cache, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex),
3306
+ width,
3307
+ start: {
3308
+ segmentIndex: startSegmentIndex,
3309
+ graphemeIndex: startGraphemeIndex,
3310
+ },
3311
+ end: {
3312
+ segmentIndex: endSegmentIndex,
3313
+ graphemeIndex: endGraphemeIndex,
3314
+ },
3315
+ };
3316
+ }
3317
+ function materializeLayoutLine(prepared, cache, line) {
3318
+ return createLayoutLine(prepared, cache, line.width, line.startSegmentIndex, line.startGraphemeIndex, line.endSegmentIndex, line.endGraphemeIndex);
3319
+ }
3320
+ function toLayoutLineRange(line) {
3321
+ return {
3322
+ width: line.width,
3323
+ start: {
3324
+ segmentIndex: line.startSegmentIndex,
3325
+ graphemeIndex: line.startGraphemeIndex,
3326
+ },
3327
+ end: {
3328
+ segmentIndex: line.endSegmentIndex,
3329
+ graphemeIndex: line.endGraphemeIndex,
3330
+ },
3331
+ };
3332
+ }
3333
+ // Batch low-level line geometry pass. This is the non-materializing counterpart
3334
+ // to layoutWithLines(), useful for shrinkwrap and other aggregate geometry work.
3335
+ function walkLineRanges(prepared, maxWidth, onLine) {
3336
+ if (prepared.widths.length === 0)
3337
+ return 0;
3338
+ return walkPreparedLines(getInternalPrepared(prepared), maxWidth, line => {
3339
+ onLine(toLayoutLineRange(line));
3340
+ });
3341
+ }
3342
+ // Rich layout API for callers that want the actual line contents and widths.
3343
+ // Caller still supplies lineHeight at layout time. Mirrors layout()'s break
3344
+ // decisions, but keeps extra per-line bookkeeping so it should stay off the
3345
+ // resize hot path.
3346
+ function layoutWithLines(prepared, maxWidth, lineHeight) {
3347
+ const lines = [];
3348
+ if (prepared.widths.length === 0)
3349
+ return { lineCount: 0, height: 0, lines };
3350
+ const graphemeCache = getLineTextCache(prepared);
3351
+ const lineCount = walkPreparedLines(getInternalPrepared(prepared), maxWidth, line => {
3352
+ lines.push(materializeLayoutLine(prepared, graphemeCache, line));
3353
+ });
3354
+ return { lineCount, height: lineCount * lineHeight, lines };
3355
+ }
3356
+
3357
+ // ============================================================
3358
+ // sketchmark — Text Measurement (pretext-powered)
3359
+ //
3360
+ // Standalone module with no dependency on layout or renderer,
3361
+ // safe to import from any layer without circular deps.
3362
+ // ============================================================
3363
+ /** Build a CSS font shorthand from fontSize, fontWeight and fontFamily */
3364
+ function buildFontStr(fontSize, fontWeight, fontFamily) {
3365
+ return `${fontWeight} ${fontSize}px ${fontFamily}`;
3366
+ }
3367
+ /** Measure the natural (unwrapped) width of text using pretext */
3368
+ function measureTextWidth(text, font) {
3369
+ const prepared = prepareWithSegments(text, font);
3370
+ let maxW = 0;
3371
+ walkLineRanges(prepared, 1e6, line => { if (line.width > maxW)
3372
+ maxW = line.width; });
3373
+ return maxW;
3374
+ }
3375
+ /** Word-wrap text using pretext, with fallback to character approximation */
3376
+ function wrapText(text, maxWidth, fontSize, font) {
3377
+ if (font) {
3378
+ try {
3379
+ const prepared = prepareWithSegments(text, font);
3380
+ const lineHeight = fontSize * 1.5;
3381
+ const { lines } = layoutWithLines(prepared, maxWidth, lineHeight);
3382
+ return lines.length ? lines.map(l => l.text) : [text];
3383
+ }
3384
+ catch (_) {
3385
+ // fall through to approximation
3386
+ }
3387
+ }
3388
+ // Fallback: character-width approximation
3389
+ const charWidth = fontSize * 0.55;
3390
+ const maxChars = Math.floor(maxWidth / charWidth);
3391
+ const words = text.split(' ');
3392
+ const lines = [];
3393
+ let current = '';
3394
+ for (const word of words) {
3395
+ const test = current ? `${current} ${word}` : word;
3396
+ if (test.length > maxChars && current) {
3397
+ lines.push(current);
3398
+ current = word;
3399
+ }
3400
+ else {
3401
+ current = test;
3402
+ }
3403
+ }
3404
+ if (current)
3405
+ lines.push(current);
3406
+ return lines.length ? lines : [text];
3407
+ }
3408
+
3409
+ // ============================================================
3410
+ // sketchmark — Font Registry
3411
+ // ============================================================
3412
+ // built-in named fonts — user can reference these by short name
3413
+ const BUILTIN_FONTS = {
3414
+ // hand-drawn
3415
+ caveat: {
3416
+ family: "'Caveat', cursive",
3417
+ url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
3418
+ },
3419
+ handlee: {
3420
+ family: "'Handlee', cursive",
3421
+ url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
3422
+ },
3423
+ 'indie-flower': {
3424
+ family: "'Indie Flower', cursive",
3425
+ url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
3426
+ },
3427
+ 'patrick-hand': {
3428
+ family: "'Patrick Hand', cursive",
3429
+ url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
3430
+ },
3431
+ // clean / readable
3432
+ 'dm-mono': {
3433
+ family: "'DM Mono', monospace",
3434
+ url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
3435
+ },
3436
+ 'jetbrains': {
3437
+ family: "'JetBrains Mono', monospace",
3438
+ url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
3439
+ },
3440
+ 'instrument': {
3441
+ family: "'Instrument Serif', serif",
3442
+ url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
3443
+ },
3444
+ 'playfair': {
3445
+ family: "'Playfair Display', serif",
3446
+ url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
3447
+ },
3448
+ // system fallbacks (no URL needed)
3449
+ system: { family: 'system-ui, sans-serif' },
3450
+ mono: { family: "'Courier New', monospace" },
3451
+ serif: { family: 'Georgia, serif' },
3452
+ };
3453
+ // default — what renders when no font is specified
3454
+ const DEFAULT_FONT = 'system-ui, sans-serif';
3455
+ // resolve a short name or pass-through a quoted CSS family
3456
+ function resolveFont(nameOrFamily) {
3457
+ const key = nameOrFamily.toLowerCase().trim();
3458
+ if (BUILTIN_FONTS[key])
3459
+ return BUILTIN_FONTS[key].family;
3460
+ return nameOrFamily; // treat as raw CSS font-family
3461
+ }
3462
+ // inject a <link> into <head> for a built-in font (browser only)
3463
+ function loadFont(name) {
3464
+ if (typeof document === 'undefined')
3465
+ return;
3466
+ const key = name.toLowerCase().trim();
3467
+ const def = BUILTIN_FONTS[key];
3468
+ if (!def?.url || def.loaded)
3469
+ return;
3470
+ if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
3471
+ return;
3472
+ const link = document.createElement('link');
3473
+ link.rel = 'stylesheet';
3474
+ link.href = def.url;
3475
+ link.setAttribute('data-sketchmark-font', key);
3476
+ document.head.appendChild(link);
3477
+ def.loaded = true;
3478
+ }
3479
+ // user registers their own font (already loaded via CSS/link)
3480
+ function registerFont(name, family, url) {
3481
+ BUILTIN_FONTS[name.toLowerCase()] = { family, url };
3482
+ if (url)
3483
+ loadFont(name);
3484
+ }
3485
+
1390
3486
  // ============================================================
1391
3487
  // sketchmark — Markdown inline parser
1392
3488
  // Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
@@ -1452,6 +3548,20 @@ var AIDiagram = (function (exports) {
1452
3548
  h += LINE_SPACING[line.kind];
1453
3549
  return h;
1454
3550
  }
3551
+ // ── Calculate natural width of a parsed block using pretext ──
3552
+ function calcMarkdownWidth(lines, fontFamily = DEFAULT_FONT, pad = MARKDOWN.defaultPad) {
3553
+ let maxW = 0;
3554
+ for (const line of lines) {
3555
+ if (line.kind === 'blank')
3556
+ continue;
3557
+ const text = line.runs.map(r => r.text).join('');
3558
+ const font = buildFontStr(LINE_FONT_SIZE[line.kind], LINE_FONT_WEIGHT[line.kind], fontFamily);
3559
+ const w = measureTextWidth(text, font);
3560
+ if (w > maxW)
3561
+ maxW = w;
3562
+ }
3563
+ return Math.ceil(maxW) + pad * 2;
3564
+ }
1455
3565
 
1456
3566
  // ============================================================
1457
3567
  // sketchmark — Scene Graph
@@ -1645,8 +3755,20 @@ var AIDiagram = (function (exports) {
1645
3755
 
1646
3756
  const boxShape = {
1647
3757
  size(n, labelW) {
1648
- n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1649
- n.h = n.h || 52;
3758
+ const w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
3759
+ n.w = w;
3760
+ if (!n.h) {
3761
+ // If label overflows width, estimate extra height for wrapped lines
3762
+ if (labelW > w) {
3763
+ const fontSize = Number(n.style?.fontSize ?? 14);
3764
+ const lineH = fontSize * 1.5;
3765
+ const lines = Math.ceil(labelW / (w - 16)); // 16px inner padding
3766
+ n.h = Math.max(52, lines * lineH + 20);
3767
+ }
3768
+ else {
3769
+ n.h = 52;
3770
+ }
3771
+ }
1650
3772
  },
1651
3773
  renderSVG(rc, n, _palette, opts) {
1652
3774
  return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
@@ -1659,7 +3781,18 @@ var AIDiagram = (function (exports) {
1659
3781
  const circleShape = {
1660
3782
  size(n, labelW) {
1661
3783
  n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
1662
- n.h = n.h || n.w;
3784
+ if (!n.h) {
3785
+ if (labelW > n.w) {
3786
+ const fontSize = Number(n.style?.fontSize ?? 14);
3787
+ const lineH = fontSize * 1.5;
3788
+ const innerW = n.w * 0.65;
3789
+ const lines = Math.ceil(labelW / innerW);
3790
+ n.h = Math.max(n.w, lines * lineH + 30);
3791
+ }
3792
+ else {
3793
+ n.h = n.w;
3794
+ }
3795
+ }
1663
3796
  },
1664
3797
  renderSVG(rc, n, _palette, opts) {
1665
3798
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
@@ -1674,7 +3807,19 @@ var AIDiagram = (function (exports) {
1674
3807
  const diamondShape = {
1675
3808
  size(n, labelW) {
1676
3809
  n.w = n.w || Math.max(SHAPES.diamond.minW, Math.min(MAX_W, labelW + SHAPES.diamond.labelPad));
1677
- n.h = n.h || Math.max(SHAPES.diamond.minH, n.w * SHAPES.diamond.aspect);
3810
+ if (!n.h) {
3811
+ const baseH = Math.max(SHAPES.diamond.minH, n.w * SHAPES.diamond.aspect);
3812
+ const innerW = n.w * 0.45;
3813
+ if (labelW > innerW) {
3814
+ const fontSize = Number(n.style?.fontSize ?? 14);
3815
+ const lineH = fontSize * 1.5;
3816
+ const lines = Math.ceil(labelW / innerW);
3817
+ n.h = Math.max(baseH, lines * lineH + 30);
3818
+ }
3819
+ else {
3820
+ n.h = baseH;
3821
+ }
3822
+ }
1678
3823
  },
1679
3824
  renderSVG(rc, n, _palette, opts) {
1680
3825
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
@@ -1691,7 +3836,19 @@ var AIDiagram = (function (exports) {
1691
3836
  const hexagonShape = {
1692
3837
  size(n, labelW) {
1693
3838
  n.w = n.w || Math.max(SHAPES.hexagon.minW, Math.min(MAX_W, labelW + SHAPES.hexagon.labelPad));
1694
- n.h = n.h || Math.max(SHAPES.hexagon.minH, n.w * SHAPES.hexagon.aspect);
3839
+ if (!n.h) {
3840
+ const baseH = Math.max(SHAPES.hexagon.minH, n.w * SHAPES.hexagon.aspect);
3841
+ const innerW = n.w * 0.55;
3842
+ if (labelW > innerW) {
3843
+ const fontSize = Number(n.style?.fontSize ?? 14);
3844
+ const lineH = fontSize * 1.5;
3845
+ const lines = Math.ceil(labelW / innerW);
3846
+ n.h = Math.max(baseH, lines * lineH + 20);
3847
+ }
3848
+ else {
3849
+ n.h = baseH;
3850
+ }
3851
+ }
1695
3852
  },
1696
3853
  renderSVG(rc, n, _palette, opts) {
1697
3854
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
@@ -1716,7 +3873,19 @@ var AIDiagram = (function (exports) {
1716
3873
  const triangleShape = {
1717
3874
  size(n, labelW) {
1718
3875
  n.w = n.w || Math.max(SHAPES.triangle.minW, Math.min(MAX_W, labelW + SHAPES.triangle.labelPad));
1719
- n.h = n.h || Math.max(SHAPES.triangle.minH, n.w * SHAPES.triangle.aspect);
3876
+ if (!n.h) {
3877
+ const baseH = Math.max(SHAPES.triangle.minH, n.w * SHAPES.triangle.aspect);
3878
+ const innerW = n.w * 0.40;
3879
+ if (labelW > innerW) {
3880
+ const fontSize = Number(n.style?.fontSize ?? 14);
3881
+ const lineH = fontSize * 1.5;
3882
+ const lines = Math.ceil(labelW / innerW);
3883
+ n.h = Math.max(baseH, lines * lineH + 30);
3884
+ }
3885
+ else {
3886
+ n.h = baseH;
3887
+ }
3888
+ }
1720
3889
  },
1721
3890
  renderSVG(rc, n, _palette, opts) {
1722
3891
  const cx = n.x + n.w / 2;
@@ -1738,8 +3907,18 @@ var AIDiagram = (function (exports) {
1738
3907
 
1739
3908
  const cylinderShape = {
1740
3909
  size(n, labelW) {
1741
- n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1742
- n.h = n.h || SHAPES.cylinder.defaultH;
3910
+ const w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
3911
+ n.w = w;
3912
+ if (!n.h) {
3913
+ if (labelW > w) {
3914
+ const fontSize = Number(n.style?.fontSize ?? 14);
3915
+ const lines = Math.ceil(labelW / (w - 16));
3916
+ n.h = Math.max(SHAPES.cylinder.defaultH, lines * fontSize * 1.5 + 20);
3917
+ }
3918
+ else {
3919
+ n.h = SHAPES.cylinder.defaultH;
3920
+ }
3921
+ }
1743
3922
  },
1744
3923
  renderSVG(rc, n, _palette, opts) {
1745
3924
  const cx = n.x + n.w / 2;
@@ -1761,8 +3940,18 @@ var AIDiagram = (function (exports) {
1761
3940
 
1762
3941
  const parallelogramShape = {
1763
3942
  size(n, labelW) {
1764
- n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + SHAPES.parallelogram.labelPad));
1765
- n.h = n.h || SHAPES.parallelogram.defaultH;
3943
+ const w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + SHAPES.parallelogram.labelPad));
3944
+ n.w = w;
3945
+ if (!n.h) {
3946
+ if (labelW + SHAPES.parallelogram.labelPad > w) {
3947
+ const fontSize = Number(n.style?.fontSize ?? 14);
3948
+ const lines = Math.ceil(labelW / (w - SHAPES.parallelogram.labelPad));
3949
+ n.h = Math.max(SHAPES.parallelogram.defaultH, lines * fontSize * 1.5 + 20);
3950
+ }
3951
+ else {
3952
+ n.h = SHAPES.parallelogram.defaultH;
3953
+ }
3954
+ }
1766
3955
  },
1767
3956
  renderSVG(rc, n, _palette, opts) {
1768
3957
  return [rc.polygon([
@@ -1781,18 +3970,35 @@ var AIDiagram = (function (exports) {
1781
3970
  const textShape = {
1782
3971
  size(n, _labelW) {
1783
3972
  const fontSize = Number(n.style?.fontSize ?? 13);
1784
- const charWidth = fontSize * 0.55;
1785
- const pad = Number(n.style?.padding ?? 8) * 2;
3973
+ const fontWeight = n.style?.fontWeight ?? 400;
3974
+ const fontFamily = String(n.style?.font ?? DEFAULT_FONT);
3975
+ const font = buildFontStr(fontSize, fontWeight, fontFamily);
3976
+ const pad = Number(n.style?.padding ?? 0) * 2;
3977
+ const lineHeight = fontSize * 1.5;
1786
3978
  if (n.width) {
1787
- const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
3979
+ const lines = n.label.includes("\\n")
3980
+ ? n.label.split("\\n")
3981
+ : wrapText(n.label, Math.max(1, n.width - pad), fontSize, font);
1788
3982
  n.w = n.width;
1789
- n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
3983
+ n.h = n.height ?? Math.max(24, lines.length * lineHeight + pad);
1790
3984
  }
1791
3985
  else {
1792
- const lines = n.label.split("\\n");
1793
- const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
1794
- n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
1795
- n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
3986
+ if (n.label.includes("\\n")) {
3987
+ const lines = n.label.split("\\n");
3988
+ let maxW = 0;
3989
+ for (const line of lines) {
3990
+ const w = measureTextWidth(line, font);
3991
+ if (w > maxW)
3992
+ maxW = w;
3993
+ }
3994
+ n.w = Math.ceil(maxW) + pad;
3995
+ n.h = n.height ?? Math.max(24, lines.length * lineHeight + pad);
3996
+ }
3997
+ else {
3998
+ const textW = measureTextWidth(n.label, font);
3999
+ n.w = Math.ceil(textW) + pad;
4000
+ n.h = n.height ?? Math.max(24, lineHeight + pad);
4001
+ }
1796
4002
  }
1797
4003
  },
1798
4004
  renderSVG(_rc, _n, _palette, _opts) {
@@ -1905,8 +4111,18 @@ var AIDiagram = (function (exports) {
1905
4111
 
1906
4112
  const imageShape = {
1907
4113
  size(n, labelW) {
1908
- n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1909
- n.h = n.h || 52;
4114
+ const w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
4115
+ n.w = w;
4116
+ if (!n.h) {
4117
+ if (labelW > w) {
4118
+ const fontSize = Number(n.style?.fontSize ?? 14);
4119
+ const lines = Math.ceil(labelW / (w - 16));
4120
+ n.h = Math.max(52, lines * fontSize * 1.5 + 20);
4121
+ }
4122
+ else {
4123
+ n.h = 52;
4124
+ }
4125
+ }
1910
4126
  },
1911
4127
  renderSVG(rc, n, _palette, opts) {
1912
4128
  const s = n.style ?? {};
@@ -1977,83 +4193,6 @@ var AIDiagram = (function (exports) {
1977
4193
  },
1978
4194
  };
1979
4195
 
1980
- // ============================================================
1981
- // sketchmark — Font Registry
1982
- // ============================================================
1983
- // built-in named fonts — user can reference these by short name
1984
- const BUILTIN_FONTS = {
1985
- // hand-drawn
1986
- caveat: {
1987
- family: "'Caveat', cursive",
1988
- url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
1989
- },
1990
- handlee: {
1991
- family: "'Handlee', cursive",
1992
- url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
1993
- },
1994
- 'indie-flower': {
1995
- family: "'Indie Flower', cursive",
1996
- url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
1997
- },
1998
- 'patrick-hand': {
1999
- family: "'Patrick Hand', cursive",
2000
- url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
2001
- },
2002
- // clean / readable
2003
- 'dm-mono': {
2004
- family: "'DM Mono', monospace",
2005
- url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
2006
- },
2007
- 'jetbrains': {
2008
- family: "'JetBrains Mono', monospace",
2009
- url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
2010
- },
2011
- 'instrument': {
2012
- family: "'Instrument Serif', serif",
2013
- url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
2014
- },
2015
- 'playfair': {
2016
- family: "'Playfair Display', serif",
2017
- url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
2018
- },
2019
- // system fallbacks (no URL needed)
2020
- system: { family: 'system-ui, sans-serif' },
2021
- mono: { family: "'Courier New', monospace" },
2022
- serif: { family: 'Georgia, serif' },
2023
- };
2024
- // default — what renders when no font is specified
2025
- const DEFAULT_FONT = 'system-ui, sans-serif';
2026
- // resolve a short name or pass-through a quoted CSS family
2027
- function resolveFont(nameOrFamily) {
2028
- const key = nameOrFamily.toLowerCase().trim();
2029
- if (BUILTIN_FONTS[key])
2030
- return BUILTIN_FONTS[key].family;
2031
- return nameOrFamily; // treat as raw CSS font-family
2032
- }
2033
- // inject a <link> into <head> for a built-in font (browser only)
2034
- function loadFont(name) {
2035
- if (typeof document === 'undefined')
2036
- return;
2037
- const key = name.toLowerCase().trim();
2038
- const def = BUILTIN_FONTS[key];
2039
- if (!def?.url || def.loaded)
2040
- return;
2041
- if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
2042
- return;
2043
- const link = document.createElement('link');
2044
- link.rel = 'stylesheet';
2045
- link.href = def.url;
2046
- link.setAttribute('data-sketchmark-font', key);
2047
- document.head.appendChild(link);
2048
- def.loaded = true;
2049
- }
2050
- // user registers their own font (already loaded via CSS/link)
2051
- function registerFont(name, family, url) {
2052
- BUILTIN_FONTS[name.toLowerCase()] = { family, url };
2053
- if (url)
2054
- loadFont(name);
2055
- }
2056
-
2057
4196
  // ============================================================
2058
4197
  // sketchmark — Shared Renderer Utilities
2059
4198
  //
@@ -2083,26 +4222,18 @@ var AIDiagram = (function (exports) {
2083
4222
  loadFont(raw);
2084
4223
  return resolveFont(raw);
2085
4224
  }
2086
- // ── Soft word-wrap ───────────────────────────────────────────────────────
2087
- function wrapText(text, maxWidth, fontSize) {
2088
- const charWidth = fontSize * 0.55;
2089
- const maxChars = Math.floor(maxWidth / charWidth);
2090
- const words = text.split(' ');
2091
- const lines = [];
2092
- let current = '';
2093
- for (const word of words) {
2094
- const test = current ? `${current} ${word}` : word;
2095
- if (test.length > maxChars && current) {
2096
- lines.push(current);
2097
- current = word;
2098
- }
2099
- else {
2100
- current = test;
2101
- }
2102
- }
2103
- if (current)
2104
- lines.push(current);
2105
- return lines.length ? lines : [text];
4225
+ // ── Inner text width per shape (for wrapping inside non-rectangular shapes)
4226
+ const SHAPE_TEXT_RATIO = {
4227
+ circle: 0.65,
4228
+ diamond: 0.45,
4229
+ hexagon: 0.55,
4230
+ triangle: 0.40,
4231
+ };
4232
+ function shapeInnerTextWidth(shape, w, padding) {
4233
+ const ratio = SHAPE_TEXT_RATIO[shape];
4234
+ if (ratio)
4235
+ return w * ratio;
4236
+ return w - padding * 2;
2106
4237
  }
2107
4238
  // ── Arrow direction from connector ───────────────────────────────────────
2108
4239
  function connMeta(connector) {
@@ -2157,9 +4288,18 @@ var AIDiagram = (function (exports) {
2157
4288
  idPrefix: "note",
2158
4289
  cssClass: "ntg",
2159
4290
  size(n, _labelW) {
4291
+ const fontSize = Number(n.style?.fontSize ?? 12);
4292
+ const fontWeight = n.style?.fontWeight ?? 400;
4293
+ const fontFamily = String(n.style?.font ?? DEFAULT_FONT);
4294
+ const font = buildFontStr(fontSize, fontWeight, fontFamily);
2160
4295
  const lines = n.label.split("\n");
2161
- const maxChars = Math.max(...lines.map((l) => l.length));
2162
- n.w = n.w || Math.max(NOTE.minW, Math.ceil(maxChars * NOTE.fontPxPerChar) + NOTE.padX * 2);
4296
+ let maxLineW = 0;
4297
+ for (const line of lines) {
4298
+ const w = measureTextWidth(line, font);
4299
+ if (w > maxLineW)
4300
+ maxLineW = w;
4301
+ }
4302
+ n.w = n.w || Math.max(NOTE.minW, Math.ceil(maxLineW) + NOTE.padX * 2);
2163
4303
  n.h = n.h || lines.length * NOTE.lineH + NOTE.padY * 2;
2164
4304
  if (n.width && n.w < n.width)
2165
4305
  n.w = n.width;
@@ -2232,8 +4372,18 @@ var AIDiagram = (function (exports) {
2232
4372
  const pathShape = {
2233
4373
  size(n, labelW) {
2234
4374
  // User should provide width/height; defaults to 100x100
2235
- n.w = n.width ?? Math.max(100, labelW + 20);
2236
- n.h = n.height ?? 100;
4375
+ const w = n.width ?? Math.max(100, Math.min(300, labelW + 20));
4376
+ n.w = w;
4377
+ if (!n.h) {
4378
+ if (!n.width && labelW + 20 > w) {
4379
+ const fontSize = Number(n.style?.fontSize ?? 14);
4380
+ const lines = Math.ceil(labelW / (w - 20));
4381
+ n.h = Math.max(100, lines * fontSize * 1.5 + 20);
4382
+ }
4383
+ else {
4384
+ n.h = n.height ?? 100;
4385
+ }
4386
+ }
2237
4387
  },
2238
4388
  renderSVG(rc, n, _palette, opts) {
2239
4389
  const d = n.pathData;
@@ -2305,14 +4455,19 @@ var AIDiagram = (function (exports) {
2305
4455
  n.w = n.width;
2306
4456
  if (n.height && n.height > 0)
2307
4457
  n.h = n.height;
2308
- const labelW = Math.round(n.label.length * NODE.fontPxPerChar + NODE.basePad);
4458
+ // Use pretext for accurate label measurement
4459
+ const fontSize = Number(n.style?.fontSize ?? 14);
4460
+ const fontWeight = n.style?.fontWeight ?? 500;
4461
+ const fontFamily = String(n.style?.font ?? DEFAULT_FONT);
4462
+ const font = buildFontStr(fontSize, fontWeight, fontFamily);
4463
+ const labelW = Math.round(measureTextWidth(n.label, font) + NODE.basePad);
2309
4464
  const shape = getShape(n.shape);
2310
4465
  if (shape) {
2311
4466
  shape.size(n, labelW);
2312
4467
  }
2313
4468
  else {
2314
4469
  // fallback for unknown shapes — box-like default
2315
- n.w = n.w || Math.max(90, Math.min(180, labelW));
4470
+ n.w = n.w || Math.max(90, Math.min(300, labelW));
2316
4471
  n.h = n.h || 52;
2317
4472
  }
2318
4473
  }
@@ -2341,8 +4496,9 @@ var AIDiagram = (function (exports) {
2341
4496
  // defaults already applied in buildSceneGraph
2342
4497
  }
2343
4498
  function sizeMarkdown(m) {
2344
- const pad = Number(m.style?.padding ?? 16);
2345
- m.w = m.width ?? 400;
4499
+ const pad = Number(m.style?.padding ?? 0);
4500
+ const fontFamily = String(m.style?.font ?? DEFAULT_FONT);
4501
+ m.w = m.width ?? calcMarkdownWidth(m.lines, fontFamily, pad);
2346
4502
  m.h = m.height ?? calcMarkdownHeight(m.lines, pad);
2347
4503
  }
2348
4504
  // ── Item size helpers (entity-map based) ─────────────────
@@ -5548,6 +7704,21 @@ var AIDiagram = (function (exports) {
5548
7704
  g.setAttribute("class", cls);
5549
7705
  return g;
5550
7706
  }
7707
+ function buildParentGroupLookup(sg) {
7708
+ const parentGroups = new Map();
7709
+ for (const g of sg.groups) {
7710
+ if (g.parentId)
7711
+ parentGroups.set(`group:${g.id}`, g.parentId);
7712
+ for (const child of g.children) {
7713
+ parentGroups.set(`${child.kind}:${child.id}`, g.id);
7714
+ }
7715
+ }
7716
+ return parentGroups;
7717
+ }
7718
+ function setParentGroupData(el, groupId) {
7719
+ if (groupId)
7720
+ el.dataset.parentGroup = groupId;
7721
+ }
5551
7722
  // ── Node shapes ───────────────────────────────────────────────────────────
5552
7723
  function renderShape$1(rc, n, palette) {
5553
7724
  const s = n.style ?? {};
@@ -5644,6 +7815,7 @@ var AIDiagram = (function (exports) {
5644
7815
  }
5645
7816
  // ── Groups ───────────────────────────────────────────────
5646
7817
  const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
7818
+ const parentGroups = buildParentGroupLookup(sg);
5647
7819
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gmMap) - groupDepth(b, gmMap));
5648
7820
  const GL = mkGroup("grp-layer");
5649
7821
  for (const g of sortedGroups) {
@@ -5651,6 +7823,7 @@ var AIDiagram = (function (exports) {
5651
7823
  continue;
5652
7824
  const gs = g.style ?? {};
5653
7825
  const gg = mkGroup(`group-${g.id}`, "gg");
7826
+ setParentGroupData(gg, g.parentId);
5654
7827
  if (gs.opacity != null)
5655
7828
  gg.setAttribute("opacity", String(gs.opacity));
5656
7829
  gg.appendChild(rc.rectangle(g.x, g.y, g.w, g.h, {
@@ -5755,6 +7928,7 @@ var AIDiagram = (function (exports) {
5755
7928
  const idPrefix = shapeDef?.idPrefix ?? "node";
5756
7929
  const cssClass = shapeDef?.cssClass ?? "ng";
5757
7930
  const ng = mkGroup(`${idPrefix}-${n.id}`, cssClass);
7931
+ setParentGroupData(ng, n.groupId ?? parentGroups.get(`node:${n.id}`));
5758
7932
  ng.dataset.nodeShape = n.shape;
5759
7933
  ng.dataset.x = String(n.x);
5760
7934
  ng.dataset.y = String(n.y);
@@ -5791,9 +7965,9 @@ var AIDiagram = (function (exports) {
5791
7965
  fontSize: isText ? 13 : isNote ? 12 : 14,
5792
7966
  fontWeight: isText || isNote ? 400 : 500,
5793
7967
  textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
5794
- textAlign: isNote ? "left" : undefined,
7968
+ textAlign: isText || isNote ? "left" : undefined,
5795
7969
  lineHeight: isNote ? 1.4 : undefined,
5796
- padding: isNote ? 12 : undefined,
7970
+ padding: isText ? 0 : isNote ? 12 : undefined,
5797
7971
  verticalAlign: isNote ? "top" : undefined,
5798
7972
  }, diagramFont, palette.nodeText);
5799
7973
  // Note textX accounts for fold corner
@@ -5803,8 +7977,11 @@ var AIDiagram = (function (exports) {
5803
7977
  : typo.textAlign === "center" ? n.x + (n.w - FOLD) / 2
5804
7978
  : n.x + typo.padding)
5805
7979
  : computeTextX(typo, n.x, n.w);
5806
- const lines = n.shape === 'text' && !n.label.includes('\n')
5807
- ? wrapText(n.label, n.w - typo.padding * 2, typo.fontSize)
7980
+ const fontStr = buildFontStr(typo.fontSize, typo.fontWeight, typo.font);
7981
+ const shouldWrap = !isMediaShape && !n.label.includes('\n');
7982
+ const innerW = shapeInnerTextWidth(n.shape, n.w, typo.padding);
7983
+ const lines = shouldWrap
7984
+ ? wrapText(n.label, innerW, typo.fontSize, fontStr)
5808
7985
  : n.label.split('\n');
5809
7986
  const textCY = isMediaShape
5810
7987
  ? n.y + n.h - 10
@@ -5833,6 +8010,7 @@ var AIDiagram = (function (exports) {
5833
8010
  const TL = mkGroup("table-layer");
5834
8011
  for (const t of sg.tables) {
5835
8012
  const tg = mkGroup(`table-${t.id}`, "tg");
8013
+ setParentGroupData(tg, parentGroups.get(`table:${t.id}`));
5836
8014
  const gs = t.style ?? {};
5837
8015
  const fill = String(gs.fill ?? palette.tableFill);
5838
8016
  const strk = String(gs.stroke ?? palette.tableStroke);
@@ -5933,6 +8111,7 @@ var AIDiagram = (function (exports) {
5933
8111
  const MDL = mkGroup('markdown-layer');
5934
8112
  for (const m of sg.markdowns) {
5935
8113
  const mg = mkGroup(`markdown-${m.id}`, 'mdg');
8114
+ setParentGroupData(mg, parentGroups.get(`markdown:${m.id}`));
5936
8115
  const gs = m.style ?? {};
5937
8116
  const mFont = resolveStyleFont(gs, diagramFont);
5938
8117
  const baseColor = String(gs.color ?? palette.nodeText);
@@ -5940,7 +8119,7 @@ var AIDiagram = (function (exports) {
5940
8119
  const anchor = textAlign === 'right' ? 'end'
5941
8120
  : textAlign === 'center' ? 'middle'
5942
8121
  : 'start';
5943
- const PAD = Number(gs.padding ?? 16);
8122
+ const PAD = Number(gs.padding ?? 0);
5944
8123
  const mLetterSpacing = gs.letterSpacing;
5945
8124
  if (gs.opacity != null)
5946
8125
  mg.setAttribute('opacity', String(gs.opacity));
@@ -5996,7 +8175,9 @@ var AIDiagram = (function (exports) {
5996
8175
  // ── Charts ────────────────────────────────────────────────
5997
8176
  const CL = mkGroup("chart-layer");
5998
8177
  for (const c of sg.charts) {
5999
- CL.appendChild(renderRoughChartSVG(rc, c, palette, themeName !== "light"));
8178
+ const cg = renderRoughChartSVG(rc, c, palette, themeName !== "light");
8179
+ setParentGroupData(cg, parentGroups.get(`chart:${c.id}`));
8180
+ CL.appendChild(cg);
6000
8181
  }
6001
8182
  svg.appendChild(CL);
6002
8183
  return svg;
@@ -6479,9 +8660,9 @@ var AIDiagram = (function (exports) {
6479
8660
  fontSize: isText ? 13 : isNote ? 12 : 14,
6480
8661
  fontWeight: isText || isNote ? 400 : 500,
6481
8662
  textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
6482
- textAlign: isNote ? "left" : undefined,
8663
+ textAlign: isText || isNote ? "left" : undefined,
6483
8664
  lineHeight: isNote ? 1.4 : undefined,
6484
- padding: isNote ? 12 : undefined,
8665
+ padding: isText ? 0 : isNote ? 12 : undefined,
6485
8666
  verticalAlign: isNote ? "top" : undefined,
6486
8667
  }, diagramFont, palette.nodeText);
6487
8668
  // Note textX accounts for fold corner
@@ -6491,9 +8672,12 @@ var AIDiagram = (function (exports) {
6491
8672
  : typo.textAlign === 'center' ? n.x + (n.w - FOLD) / 2
6492
8673
  : n.x + typo.padding)
6493
8674
  : computeTextX(typo, n.x, n.w);
8675
+ const fontStr = buildFontStr(typo.fontSize, typo.fontWeight, typo.font);
8676
+ const shouldWrap = !isMediaShape && !n.label.includes('\n');
8677
+ const innerW = shapeInnerTextWidth(n.shape, n.w, typo.padding);
6494
8678
  const rawLines = n.label.split('\n');
6495
- const lines = n.shape === 'text' && rawLines.length === 1
6496
- ? wrapText(n.label, n.w - typo.padding * 2, typo.fontSize)
8679
+ const lines = shouldWrap && rawLines.length === 1
8680
+ ? wrapText(n.label, innerW, typo.fontSize, fontStr)
6497
8681
  : rawLines;
6498
8682
  const textCY = isMediaShape
6499
8683
  ? n.y + n.h - 10
@@ -6588,7 +8772,7 @@ var AIDiagram = (function (exports) {
6588
8772
  const mFont = resolveStyleFont(gs, diagramFont);
6589
8773
  const baseColor = String(gs.color ?? palette.nodeText);
6590
8774
  const textAlign = String(gs.textAlign ?? 'left');
6591
- const PAD = Number(gs.padding ?? 16);
8775
+ const PAD = Number(gs.padding ?? 0);
6592
8776
  const mLetterSpacing = gs.letterSpacing;
6593
8777
  if (gs.opacity != null)
6594
8778
  ctx.globalAlpha = Number(gs.opacity);
@@ -6706,18 +8890,33 @@ var AIDiagram = (function (exports) {
6706
8890
  const getNoteEl = (svg, id) => getEl(svg, `note-${id}`);
6707
8891
  const getChartEl = (svg, id) => getEl(svg, `chart-${id}`);
6708
8892
  const getMarkdownEl = (svg, id) => getEl(svg, `markdown-${id}`);
8893
+ const POSITIONABLE_SELECTOR = ".ng, .gg, .tg, .ntg, .cg, .mdg";
8894
+ function resolveNonEdgeDrawEl(svg, target) {
8895
+ return (getGroupEl(svg, target) ??
8896
+ getTableEl(svg, target) ??
8897
+ getNoteEl(svg, target) ??
8898
+ getChartEl(svg, target) ??
8899
+ getMarkdownEl(svg, target) ??
8900
+ getNodeEl(svg, target) ??
8901
+ null);
8902
+ }
8903
+ function hideDrawEl(el) {
8904
+ if (el.classList.contains("ng")) {
8905
+ el.classList.add("hidden");
8906
+ return;
8907
+ }
8908
+ el.classList.add("gg-hidden");
8909
+ }
8910
+ function showDrawEl(el) {
8911
+ el.classList.remove("hidden", "gg-hidden");
8912
+ }
6709
8913
  function resolveEl(svg, target) {
6710
8914
  // check edge first — target contains connector like "a-->b"
6711
8915
  const edge = parseEdgeTarget(target);
6712
8916
  if (edge)
6713
8917
  return getEdgeEl(svg, edge.from, edge.to);
6714
8918
  // everything else resolved by prefixed id
6715
- return (getNodeEl(svg, target) ??
6716
- getGroupEl(svg, target) ??
6717
- getTableEl(svg, target) ??
6718
- getNoteEl(svg, target) ??
6719
- getChartEl(svg, target) ??
6720
- getMarkdownEl(svg, target) ??
8919
+ return (resolveNonEdgeDrawEl(svg, target) ??
6721
8920
  null);
6722
8921
  }
6723
8922
  function pathLength(p) {
@@ -6907,6 +9106,7 @@ var AIDiagram = (function (exports) {
6907
9106
  el.appendChild(guide);
6908
9107
  }
6909
9108
  function revealNodeInstant(el) {
9109
+ showDrawEl(el);
6910
9110
  clearNodeDrawStyles(el);
6911
9111
  }
6912
9112
  // ── Text writing reveal (clipPath) ───────────────────────
@@ -6955,6 +9155,7 @@ var AIDiagram = (function (exports) {
6955
9155
  }, delayMs);
6956
9156
  }
6957
9157
  function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
9158
+ showDrawEl(el);
6958
9159
  const guide = nodeGuidePathEl(el);
6959
9160
  if (!guide) {
6960
9161
  const firstPath = el.querySelector("path");
@@ -7009,6 +9210,15 @@ var AIDiagram = (function (exports) {
7009
9210
  }
7010
9211
  return out;
7011
9212
  }
9213
+ function forEachPlaybackStep(items, visit) {
9214
+ items.forEach((item, stepIndex) => {
9215
+ if (item.kind === "beat") {
9216
+ item.children.forEach((child) => visit(child, stepIndex));
9217
+ return;
9218
+ }
9219
+ visit(item, stepIndex);
9220
+ });
9221
+ }
7012
9222
  // ── Draw target helpers ───────────────────────────────────
7013
9223
  function getDrawTargetEdgeIds(steps) {
7014
9224
  const ids = new Set();
@@ -7193,7 +9403,7 @@ var AIDiagram = (function (exports) {
7193
9403
  for (const s of flattenSteps(steps)) {
7194
9404
  if (s.action !== "draw" || parseEdgeTarget(s.target))
7195
9405
  continue;
7196
- if (svg.querySelector(`#group-${s.target}`)) {
9406
+ if (resolveNonEdgeDrawEl(svg, s.target)?.id === `group-${s.target}`) {
7197
9407
  this.drawTargetGroups.add(`group-${s.target}`);
7198
9408
  this.drawTargetNodes.delete(`node-${s.target}`);
7199
9409
  }
@@ -7214,6 +9424,10 @@ var AIDiagram = (function (exports) {
7214
9424
  this.drawTargetNodes.delete(`node-${s.target}`);
7215
9425
  }
7216
9426
  }
9427
+ this._drawStepIndexByElementId = this._buildDrawStepIndex();
9428
+ const { parentGroupByElementId, groupDescendantIds } = this._buildGroupVisibilityIndex();
9429
+ this._parentGroupByElementId = parentGroupByElementId;
9430
+ this._groupDescendantIds = groupDescendantIds;
7217
9431
  this._clearAll();
7218
9432
  // Init narration caption
7219
9433
  if (this._container)
@@ -7232,6 +9446,104 @@ var AIDiagram = (function (exports) {
7232
9446
  if (this._tts)
7233
9447
  this._warmUpSpeech();
7234
9448
  }
9449
+ _buildDrawStepIndex() {
9450
+ const drawStepIndexByElementId = new Map();
9451
+ forEachPlaybackStep(this.steps, (step, stepIndex) => {
9452
+ if (step.action !== "draw" || parseEdgeTarget(step.target))
9453
+ return;
9454
+ const el = resolveNonEdgeDrawEl(this.svg, step.target);
9455
+ if (el && !drawStepIndexByElementId.has(el.id)) {
9456
+ drawStepIndexByElementId.set(el.id, stepIndex);
9457
+ }
9458
+ });
9459
+ return drawStepIndexByElementId;
9460
+ }
9461
+ _buildGroupVisibilityIndex() {
9462
+ const parentGroupByElementId = new Map();
9463
+ const directChildIdsByGroup = new Map();
9464
+ this.svg.querySelectorAll(POSITIONABLE_SELECTOR).forEach((el) => {
9465
+ const parentGroupId = el.dataset.parentGroup;
9466
+ if (!parentGroupId)
9467
+ return;
9468
+ const parentGroupElId = `group-${parentGroupId}`;
9469
+ parentGroupByElementId.set(el.id, parentGroupElId);
9470
+ const children = directChildIdsByGroup.get(parentGroupElId) ?? new Set();
9471
+ children.add(el.id);
9472
+ directChildIdsByGroup.set(parentGroupElId, children);
9473
+ });
9474
+ const groupDescendantIds = new Map();
9475
+ const visit = (groupElId) => {
9476
+ if (groupDescendantIds.has(groupElId))
9477
+ return groupDescendantIds.get(groupElId);
9478
+ const descendants = new Set();
9479
+ const directChildren = directChildIdsByGroup.get(groupElId);
9480
+ if (directChildren) {
9481
+ for (const childId of directChildren) {
9482
+ descendants.add(childId);
9483
+ if (childId.startsWith("group-")) {
9484
+ visit(childId).forEach((nestedId) => descendants.add(nestedId));
9485
+ }
9486
+ }
9487
+ }
9488
+ groupDescendantIds.set(groupElId, descendants);
9489
+ return descendants;
9490
+ };
9491
+ this.svg.querySelectorAll(".gg").forEach((el) => {
9492
+ visit(el.id);
9493
+ });
9494
+ return { parentGroupByElementId, groupDescendantIds };
9495
+ }
9496
+ _hideGroupDescendants(groupElId) {
9497
+ const descendants = this._groupDescendantIds.get(groupElId);
9498
+ if (!descendants)
9499
+ return;
9500
+ for (const descendantId of descendants) {
9501
+ const el = getEl(this.svg, descendantId);
9502
+ if (el)
9503
+ hideDrawEl(el);
9504
+ }
9505
+ }
9506
+ _isDeferredForGroupReveal(elementId, stepIndex, groupElId) {
9507
+ let currentId = elementId;
9508
+ while (currentId) {
9509
+ const firstDrawStep = this._drawStepIndexByElementId.get(currentId);
9510
+ if (firstDrawStep != null && firstDrawStep > stepIndex)
9511
+ return true;
9512
+ if (currentId === groupElId)
9513
+ break;
9514
+ currentId = this._parentGroupByElementId.get(currentId);
9515
+ }
9516
+ return false;
9517
+ }
9518
+ _revealGroupSubtree(groupElId, stepIndex) {
9519
+ const descendants = this._groupDescendantIds.get(groupElId);
9520
+ if (!descendants)
9521
+ return;
9522
+ for (const descendantId of descendants) {
9523
+ if (this._isDeferredForGroupReveal(descendantId, stepIndex, groupElId))
9524
+ continue;
9525
+ const el = getEl(this.svg, descendantId);
9526
+ if (el)
9527
+ showDrawEl(el);
9528
+ }
9529
+ }
9530
+ _resolveCascadeTargets(target) {
9531
+ const edge = parseEdgeTarget(target);
9532
+ if (edge) {
9533
+ const el = getEdgeEl(this.svg, edge.from, edge.to);
9534
+ return el ? [el] : [];
9535
+ }
9536
+ const el = resolveEl(this.svg, target);
9537
+ if (!el)
9538
+ return [];
9539
+ if (!el.id.startsWith("group-"))
9540
+ return [el];
9541
+ const ids = new Set([el.id]);
9542
+ this._groupDescendantIds.get(el.id)?.forEach((id) => ids.add(id));
9543
+ return Array.from(ids)
9544
+ .map((id) => getEl(this.svg, id))
9545
+ .filter((candidate) => candidate != null);
9546
+ }
7235
9547
  /** The narration caption element — mount it anywhere via `yourContainer.appendChild(anim.captionElement)` */
7236
9548
  get captionElement() {
7237
9549
  return this._captionEl;
@@ -7363,7 +9675,8 @@ var AIDiagram = (function (exports) {
7363
9675
  minNeeded = Math.max(typingMs, ttsMs);
7364
9676
  }
7365
9677
  else if (step.action === "circle" || step.action === "underline" ||
7366
- step.action === "crossout" || step.action === "bracket") {
9678
+ step.action === "crossout" || step.action === "bracket" ||
9679
+ step.action === "tick" || step.action === "strikeoff") {
7367
9680
  // Annotation guide draw + rough reveal + pointer fade
7368
9681
  minNeeded = ANIMATION.annotationStrokeDur + 120 + 200;
7369
9682
  }
@@ -7500,6 +9813,9 @@ var AIDiagram = (function (exports) {
7500
9813
  el.style.opacity = "";
7501
9814
  el.classList.remove("hl", "faded");
7502
9815
  });
9816
+ for (const groupElId of this.drawTargetGroups) {
9817
+ this._hideGroupDescendants(groupElId);
9818
+ }
7503
9819
  // Clear narration caption
7504
9820
  if (this._captionEl) {
7505
9821
  this._captionEl.style.opacity = "0";
@@ -7562,7 +9878,7 @@ var AIDiagram = (function (exports) {
7562
9878
  this._doDraw(s, silent);
7563
9879
  break;
7564
9880
  case "erase":
7565
- this._doErase(s.target, s.duration);
9881
+ this._doErase(s.target, silent, s.duration);
7566
9882
  break;
7567
9883
  case "show":
7568
9884
  this._doShowHide(s.target, true, silent, s.duration);
@@ -7601,6 +9917,12 @@ var AIDiagram = (function (exports) {
7601
9917
  case "bracket":
7602
9918
  this._doAnnotationBracket(s.target, s.target2 ?? "", silent);
7603
9919
  break;
9920
+ case "tick":
9921
+ this._doAnnotationTick(s.target, silent);
9922
+ break;
9923
+ case "strikeoff":
9924
+ this._doAnnotationStrikeoff(s.target, silent);
9925
+ break;
7604
9926
  }
7605
9927
  }
7606
9928
  // ── highlight ────────────────────────────────────────────
@@ -7612,7 +9934,9 @@ var AIDiagram = (function (exports) {
7612
9934
  }
7613
9935
  // ── fade / unfade ─────────────────────────────────────────
7614
9936
  _doFade(target, doFade) {
7615
- resolveEl(this.svg, target)?.classList.toggle("faded", doFade);
9937
+ for (const el of this._resolveCascadeTargets(target)) {
9938
+ el.classList.toggle("faded", doFade);
9939
+ }
7616
9940
  }
7617
9941
  _writeTransform(el, target, silent, duration = 420) {
7618
9942
  const t = this._transforms.get(target) ?? {
@@ -7711,11 +10035,12 @@ var AIDiagram = (function (exports) {
7711
10035
  // Check if target is a group (has #group-{target} element)
7712
10036
  const groupEl = getGroupEl(this.svg, target);
7713
10037
  if (groupEl) {
10038
+ showDrawEl(groupEl);
10039
+ this._revealGroupSubtree(groupEl.id, this._step);
7714
10040
  // ── Group draw ──────────────────────────────────────
7715
10041
  if (silent) {
7716
10042
  clearDrawStyles(groupEl);
7717
10043
  groupEl.style.transition = "none";
7718
- groupEl.classList.remove("gg-hidden");
7719
10044
  groupEl.style.opacity = "1";
7720
10045
  requestAnimationFrame(() => requestAnimationFrame(() => {
7721
10046
  groupEl.style.transition = "";
@@ -7723,7 +10048,6 @@ var AIDiagram = (function (exports) {
7723
10048
  }));
7724
10049
  }
7725
10050
  else {
7726
- groupEl.classList.remove("gg-hidden");
7727
10051
  // Groups use slightly longer stroke-draw (bigger box, dashed border = more paths)
7728
10052
  const firstPath = groupEl.querySelector("path");
7729
10053
  if (!firstPath?.style.strokeDasharray)
@@ -7834,6 +10158,7 @@ var AIDiagram = (function (exports) {
7834
10158
  const nodeEl = getNodeEl(this.svg, target);
7835
10159
  if (!nodeEl)
7836
10160
  return;
10161
+ showDrawEl(nodeEl);
7837
10162
  if (silent) {
7838
10163
  revealNodeInstant(nodeEl);
7839
10164
  }
@@ -7845,20 +10170,20 @@ var AIDiagram = (function (exports) {
7845
10170
  }
7846
10171
  }
7847
10172
  // ── erase ─────────────────────────────────────────────────
7848
- _doErase(target, duration = 400) {
7849
- const el = resolveEl(this.svg, target); // handles edges too now
7850
- if (el) {
7851
- el.style.transition = `opacity ${duration}ms`;
10173
+ _doErase(target, silent, duration = 400) {
10174
+ for (const el of this._resolveCascadeTargets(target)) {
10175
+ el.style.transition = silent ? "none" : `opacity ${duration}ms`;
7852
10176
  el.style.opacity = "0";
7853
10177
  }
7854
10178
  }
7855
10179
  // ── show / hide ───────────────────────────────────────────
7856
10180
  _doShowHide(target, show, silent, duration = 400) {
7857
- const el = resolveEl(this.svg, target);
7858
- if (!el)
7859
- return;
7860
- el.style.transition = silent ? "none" : `opacity ${duration}ms`;
7861
- el.style.opacity = show ? "1" : "0";
10181
+ for (const el of this._resolveCascadeTargets(target)) {
10182
+ if (show)
10183
+ showDrawEl(el);
10184
+ el.style.transition = silent ? "none" : `opacity ${duration}ms`;
10185
+ el.style.opacity = show ? "1" : "0";
10186
+ }
7862
10187
  }
7863
10188
  // ── pulse ─────────────────────────────────────────────────
7864
10189
  _doPulse(target, duration = 500) {
@@ -7909,18 +10234,18 @@ var AIDiagram = (function (exports) {
7909
10234
  document.querySelector('.skm-caption')?.remove();
7910
10235
  const cap = document.createElement("div");
7911
10236
  cap.className = "skm-caption";
7912
- cap.style.cssText = `
7913
- position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
7914
- z-index: 9999; max-width: 600px; width: max-content;
7915
- padding: 10px 24px; box-sizing: border-box;
7916
- font-family: var(--font-sans, system-ui, sans-serif);
7917
- font-size: 15px; line-height: 1.5;
7918
- color: #fde68a; background: #1a1208;
7919
- border-radius: 8px;
7920
- box-shadow: 0 4px 24px rgba(0,0,0,0.35);
7921
- opacity: 0; transition: opacity ${ANIMATION.narrationFadeMs}ms ease;
7922
- pointer-events: none; user-select: none;
7923
- text-align: center;
10237
+ cap.style.cssText = `
10238
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
10239
+ z-index: 9999; max-width: 600px; width: max-content;
10240
+ padding: 10px 24px; box-sizing: border-box;
10241
+ font-family: var(--font-sans, system-ui, sans-serif);
10242
+ font-size: 15px; line-height: 1.5;
10243
+ color: #fde68a; background: #1a1208;
10244
+ border-radius: 8px;
10245
+ box-shadow: 0 4px 24px rgba(0,0,0,0.35);
10246
+ opacity: 0; transition: opacity ${ANIMATION.narrationFadeMs}ms ease;
10247
+ pointer-events: none; user-select: none;
10248
+ text-align: center;
7924
10249
  `;
7925
10250
  const span = document.createElement("span");
7926
10251
  cap.appendChild(span);
@@ -8128,6 +10453,62 @@ var AIDiagram = (function (exports) {
8128
10453
  `M ${m.x + m.w + pad} ${m.y - pad} L ${m.x - pad} ${m.y + m.h + pad}`;
8129
10454
  this._animateAnnotation(roughG, guideD, silent);
8130
10455
  }
10456
+ _doAnnotationTick(target, silent) {
10457
+ const el = resolveEl(this.svg, target);
10458
+ if (!el || !this._rc || !this._annotationLayer)
10459
+ return;
10460
+ const m = this._nodeMetrics(el);
10461
+ if (!m)
10462
+ return;
10463
+ // Tick mark on the left side of the node, like a teacher's check mark (✓)
10464
+ // The tick sits just to the left of the node, vertically centered
10465
+ const tickH = m.h * 0.5; // total tick height
10466
+ const tickW = tickH * 0.7; // total tick width
10467
+ const gap = 8; // gap between tick and node
10468
+ // Key points of the tick: start (top-left), valley (bottom), end (top-right)
10469
+ const endX = m.x - gap; // tip of the long upstroke (closest to node)
10470
+ const endY = m.y + m.h * 0.25; // top of the long stroke
10471
+ const valleyX = endX - tickW * 0.4; // bottom of the V
10472
+ const valleyY = m.y + m.h * 0.75; // bottom of the V
10473
+ const startX = valleyX - tickW * 0.3; // top of the short downstroke
10474
+ const startY = valleyY - tickH * 0.3; // slightly above valley
10475
+ const roughG = document.createElementNS(SVG_NS$1, "g");
10476
+ // Short down-stroke
10477
+ const line1 = this._rc.line(startX, startY, valleyX, valleyY, {
10478
+ roughness: 1.5, stroke: ANIMATION.annotationColor,
10479
+ strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
10480
+ });
10481
+ // Long up-stroke
10482
+ const line2 = this._rc.line(valleyX, valleyY, endX, endY, {
10483
+ roughness: 1.5, stroke: ANIMATION.annotationColor,
10484
+ strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now() + 1,
10485
+ });
10486
+ roughG.appendChild(line1);
10487
+ roughG.appendChild(line2);
10488
+ this._annotationLayer.appendChild(roughG);
10489
+ this._annotations.push(roughG);
10490
+ // Guide path: short stroke down then long stroke up (single continuous path)
10491
+ const guideD = `M ${startX} ${startY} L ${valleyX} ${valleyY} L ${endX} ${endY}`;
10492
+ this._animateAnnotation(roughG, guideD, silent);
10493
+ }
10494
+ _doAnnotationStrikeoff(target, silent) {
10495
+ const el = resolveEl(this.svg, target);
10496
+ if (!el || !this._rc || !this._annotationLayer)
10497
+ return;
10498
+ const m = this._nodeMetrics(el);
10499
+ if (!m)
10500
+ return;
10501
+ const pad = 6;
10502
+ const lineY = m.y + m.h / 2;
10503
+ const roughEl = this._rc.line(m.x - pad, lineY, m.x + m.w + pad, lineY, {
10504
+ roughness: 1.5, stroke: ANIMATION.annotationColor,
10505
+ strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
10506
+ });
10507
+ this._annotationLayer.appendChild(roughEl);
10508
+ this._annotations.push(roughEl);
10509
+ const guideD = `M ${m.x - pad} ${lineY} L ${m.x + m.w + pad} ${lineY}`;
10510
+ this._animateAnnotation(roughEl, guideD, silent);
10511
+ }
8131
10512
  _doAnnotationBracket(target1, target2, silent) {
8132
10513
  const el1 = resolveEl(this.svg, target1);
8133
10514
  const el2 = resolveEl(this.svg, target2);
@@ -8194,42 +10575,42 @@ var AIDiagram = (function (exports) {
8194
10575
  }
8195
10576
  }
8196
10577
  }
8197
- const ANIMATION_CSS = `
8198
- .ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
8199
- transform-box: fill-box;
8200
- transform-origin: center;
8201
- transition: filter 0.3s, opacity 0.35s;
8202
- }
8203
-
8204
- /* highlight */
8205
- .ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
8206
- .tg.hl path, .tg.hl rect,
8207
- .ntg.hl path, .ntg.hl polygon,
8208
- .cg.hl path, .cg.hl rect,
8209
- .mdg.hl text,
8210
- .eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
8211
-
8212
- .ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
8213
- animation: ng-pulse 1.4s ease-in-out infinite;
8214
- }
8215
- @keyframes ng-pulse {
8216
- 0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
8217
- 50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
8218
- }
8219
-
8220
- /* fade */
8221
- .ng.faded, .gg.faded, .tg.faded, .ntg.faded,
8222
- .cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
8223
-
8224
- .ng.hidden { opacity: 0; pointer-events: none; }
8225
- .gg.gg-hidden { opacity: 0; }
8226
- .tg.gg-hidden { opacity: 0; }
8227
- .ntg.gg-hidden { opacity: 0; }
8228
- .cg.gg-hidden { opacity: 0; }
8229
- .mdg.gg-hidden { opacity: 0; }
8230
-
8231
- /* narration caption */
8232
- .skm-caption { pointer-events: none; user-select: none; }
10578
+ const ANIMATION_CSS = `
10579
+ .ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
10580
+ transform-box: fill-box;
10581
+ transform-origin: center;
10582
+ transition: filter 0.3s, opacity 0.35s;
10583
+ }
10584
+
10585
+ /* highlight */
10586
+ .ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
10587
+ .tg.hl path, .tg.hl rect,
10588
+ .ntg.hl path, .ntg.hl polygon,
10589
+ .cg.hl path, .cg.hl rect,
10590
+ .mdg.hl text,
10591
+ .eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
10592
+
10593
+ .ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
10594
+ animation: ng-pulse 1.4s ease-in-out infinite;
10595
+ }
10596
+ @keyframes ng-pulse {
10597
+ 0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
10598
+ 50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
10599
+ }
10600
+
10601
+ /* fade */
10602
+ .ng.faded, .gg.faded, .tg.faded, .ntg.faded,
10603
+ .cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
10604
+
10605
+ .ng.hidden { opacity: 0; pointer-events: none; }
10606
+ .gg.gg-hidden { opacity: 0; }
10607
+ .tg.gg-hidden { opacity: 0; }
10608
+ .ntg.gg-hidden { opacity: 0; }
10609
+ .cg.gg-hidden { opacity: 0; }
10610
+ .mdg.gg-hidden { opacity: 0; }
10611
+
10612
+ /* narration caption */
10613
+ .skm-caption { pointer-events: none; user-select: none; }
8233
10614
  `;
8234
10615
 
8235
10616
  // ============================================================