sketchmark 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/animation/index.d.ts +3 -0
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/ast/types.d.ts +1 -1
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/index.cjs +2437 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2437 -198
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/markdown/parser.d.ts +1 -0
- package/dist/markdown/parser.d.ts.map +1 -1
- package/dist/parser/tokenizer.d.ts.map +1 -1
- package/dist/renderer/canvas/index.d.ts.map +1 -1
- package/dist/renderer/shapes/box.d.ts.map +1 -1
- package/dist/renderer/shapes/circle.d.ts.map +1 -1
- package/dist/renderer/shapes/cylinder.d.ts.map +1 -1
- package/dist/renderer/shapes/diamond.d.ts.map +1 -1
- package/dist/renderer/shapes/hexagon.d.ts.map +1 -1
- package/dist/renderer/shapes/image.d.ts.map +1 -1
- package/dist/renderer/shapes/note.d.ts.map +1 -1
- package/dist/renderer/shapes/parallelogram.d.ts.map +1 -1
- package/dist/renderer/shapes/path.d.ts.map +1 -1
- package/dist/renderer/shapes/text-shape.d.ts.map +1 -1
- package/dist/renderer/shapes/triangle.d.ts.map +1 -1
- package/dist/renderer/shapes/types.d.ts +1 -1
- package/dist/renderer/shared.d.ts +2 -1
- package/dist/renderer/shared.d.ts.map +1 -1
- package/dist/renderer/svg/index.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +2437 -198
- package/dist/utils/text-measure.d.ts +22 -0
- package/dist/utils/text-measure.d.ts.map +1 -0
- package/package.json +72 -69
package/dist/sketchmark.iife.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
1649
|
-
n.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1742
|
-
n.
|
|
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
|
-
|
|
1765
|
-
n.
|
|
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
|
|
1785
|
-
const
|
|
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
|
|
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,
|
|
3983
|
+
n.h = n.height ?? Math.max(24, lines.length * lineHeight + pad);
|
|
1790
3984
|
}
|
|
1791
3985
|
else {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
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
|
-
|
|
1909
|
-
n.
|
|
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
|
-
// ──
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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
|
-
|
|
2162
|
-
|
|
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
|
-
|
|
2236
|
-
n.
|
|
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
|
-
|
|
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(
|
|
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 ??
|
|
2345
|
-
|
|
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) ─────────────────
|
|
@@ -5791,9 +7947,9 @@ var AIDiagram = (function (exports) {
|
|
|
5791
7947
|
fontSize: isText ? 13 : isNote ? 12 : 14,
|
|
5792
7948
|
fontWeight: isText || isNote ? 400 : 500,
|
|
5793
7949
|
textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
|
|
5794
|
-
textAlign: isNote ? "left" : undefined,
|
|
7950
|
+
textAlign: isText || isNote ? "left" : undefined,
|
|
5795
7951
|
lineHeight: isNote ? 1.4 : undefined,
|
|
5796
|
-
padding: isNote ? 12 : undefined,
|
|
7952
|
+
padding: isText ? 0 : isNote ? 12 : undefined,
|
|
5797
7953
|
verticalAlign: isNote ? "top" : undefined,
|
|
5798
7954
|
}, diagramFont, palette.nodeText);
|
|
5799
7955
|
// Note textX accounts for fold corner
|
|
@@ -5803,8 +7959,11 @@ var AIDiagram = (function (exports) {
|
|
|
5803
7959
|
: typo.textAlign === "center" ? n.x + (n.w - FOLD) / 2
|
|
5804
7960
|
: n.x + typo.padding)
|
|
5805
7961
|
: computeTextX(typo, n.x, n.w);
|
|
5806
|
-
const
|
|
5807
|
-
|
|
7962
|
+
const fontStr = buildFontStr(typo.fontSize, typo.fontWeight, typo.font);
|
|
7963
|
+
const shouldWrap = !isMediaShape && !n.label.includes('\n');
|
|
7964
|
+
const innerW = shapeInnerTextWidth(n.shape, n.w, typo.padding);
|
|
7965
|
+
const lines = shouldWrap
|
|
7966
|
+
? wrapText(n.label, innerW, typo.fontSize, fontStr)
|
|
5808
7967
|
: n.label.split('\n');
|
|
5809
7968
|
const textCY = isMediaShape
|
|
5810
7969
|
? n.y + n.h - 10
|
|
@@ -5940,7 +8099,7 @@ var AIDiagram = (function (exports) {
|
|
|
5940
8099
|
const anchor = textAlign === 'right' ? 'end'
|
|
5941
8100
|
: textAlign === 'center' ? 'middle'
|
|
5942
8101
|
: 'start';
|
|
5943
|
-
const PAD = Number(gs.padding ??
|
|
8102
|
+
const PAD = Number(gs.padding ?? 0);
|
|
5944
8103
|
const mLetterSpacing = gs.letterSpacing;
|
|
5945
8104
|
if (gs.opacity != null)
|
|
5946
8105
|
mg.setAttribute('opacity', String(gs.opacity));
|
|
@@ -6479,9 +8638,9 @@ var AIDiagram = (function (exports) {
|
|
|
6479
8638
|
fontSize: isText ? 13 : isNote ? 12 : 14,
|
|
6480
8639
|
fontWeight: isText || isNote ? 400 : 500,
|
|
6481
8640
|
textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
|
|
6482
|
-
textAlign: isNote ? "left" : undefined,
|
|
8641
|
+
textAlign: isText || isNote ? "left" : undefined,
|
|
6483
8642
|
lineHeight: isNote ? 1.4 : undefined,
|
|
6484
|
-
padding: isNote ? 12 : undefined,
|
|
8643
|
+
padding: isText ? 0 : isNote ? 12 : undefined,
|
|
6485
8644
|
verticalAlign: isNote ? "top" : undefined,
|
|
6486
8645
|
}, diagramFont, palette.nodeText);
|
|
6487
8646
|
// Note textX accounts for fold corner
|
|
@@ -6491,9 +8650,12 @@ var AIDiagram = (function (exports) {
|
|
|
6491
8650
|
: typo.textAlign === 'center' ? n.x + (n.w - FOLD) / 2
|
|
6492
8651
|
: n.x + typo.padding)
|
|
6493
8652
|
: computeTextX(typo, n.x, n.w);
|
|
8653
|
+
const fontStr = buildFontStr(typo.fontSize, typo.fontWeight, typo.font);
|
|
8654
|
+
const shouldWrap = !isMediaShape && !n.label.includes('\n');
|
|
8655
|
+
const innerW = shapeInnerTextWidth(n.shape, n.w, typo.padding);
|
|
6494
8656
|
const rawLines = n.label.split('\n');
|
|
6495
|
-
const lines =
|
|
6496
|
-
? wrapText(n.label,
|
|
8657
|
+
const lines = shouldWrap && rawLines.length === 1
|
|
8658
|
+
? wrapText(n.label, innerW, typo.fontSize, fontStr)
|
|
6497
8659
|
: rawLines;
|
|
6498
8660
|
const textCY = isMediaShape
|
|
6499
8661
|
? n.y + n.h - 10
|
|
@@ -6588,7 +8750,7 @@ var AIDiagram = (function (exports) {
|
|
|
6588
8750
|
const mFont = resolveStyleFont(gs, diagramFont);
|
|
6589
8751
|
const baseColor = String(gs.color ?? palette.nodeText);
|
|
6590
8752
|
const textAlign = String(gs.textAlign ?? 'left');
|
|
6591
|
-
const PAD = Number(gs.padding ??
|
|
8753
|
+
const PAD = Number(gs.padding ?? 0);
|
|
6592
8754
|
const mLetterSpacing = gs.letterSpacing;
|
|
6593
8755
|
if (gs.opacity != null)
|
|
6594
8756
|
ctx.globalAlpha = Number(gs.opacity);
|
|
@@ -7181,6 +9343,7 @@ var AIDiagram = (function (exports) {
|
|
|
7181
9343
|
this._pointerType = 'none';
|
|
7182
9344
|
// ── TTS ──
|
|
7183
9345
|
this._tts = false;
|
|
9346
|
+
this._speechDone = null;
|
|
7184
9347
|
this.drawTargetEdges = getDrawTargetEdgeIds(steps);
|
|
7185
9348
|
this.drawTargetNodes = getDrawTargetNodeIds(steps);
|
|
7186
9349
|
// Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
|
|
@@ -7311,7 +9474,11 @@ var AIDiagram = (function (exports) {
|
|
|
7311
9474
|
while (this.canNext) {
|
|
7312
9475
|
const nextStep = this.steps[this._step + 1];
|
|
7313
9476
|
this.next();
|
|
7314
|
-
|
|
9477
|
+
// Wait for timer AND speech to finish (whichever is longer)
|
|
9478
|
+
await Promise.all([
|
|
9479
|
+
new Promise((r) => setTimeout(r, this._playbackWaitMs(nextStep, msPerStep))),
|
|
9480
|
+
this._speechDone ?? Promise.resolve(),
|
|
9481
|
+
]);
|
|
7315
9482
|
}
|
|
7316
9483
|
}
|
|
7317
9484
|
goTo(index) {
|
|
@@ -7349,11 +9516,17 @@ var AIDiagram = (function (exports) {
|
|
|
7349
9516
|
// Compute minimum time the step actually needs to finish
|
|
7350
9517
|
let minNeeded = 0;
|
|
7351
9518
|
if (step.action === "narrate") {
|
|
9519
|
+
const text = step.value ?? "";
|
|
7352
9520
|
// Typing effect: chars × typeMs + fade buffer
|
|
7353
|
-
|
|
9521
|
+
const typingMs = text.length * ANIMATION.narrationTypeMs + ANIMATION.narrationFadeMs;
|
|
9522
|
+
// TTS estimate: ~150ms per word + 500ms buffer for engine latency
|
|
9523
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
9524
|
+
const ttsMs = this._tts ? wordCount * 150 + 500 : 0;
|
|
9525
|
+
minNeeded = Math.max(typingMs, ttsMs);
|
|
7354
9526
|
}
|
|
7355
9527
|
else if (step.action === "circle" || step.action === "underline" ||
|
|
7356
|
-
step.action === "crossout" || step.action === "bracket"
|
|
9528
|
+
step.action === "crossout" || step.action === "bracket" ||
|
|
9529
|
+
step.action === "tick" || step.action === "strikeoff") {
|
|
7357
9530
|
// Annotation guide draw + rough reveal + pointer fade
|
|
7358
9531
|
minNeeded = ANIMATION.annotationStrokeDur + 120 + 200;
|
|
7359
9532
|
}
|
|
@@ -7591,6 +9764,12 @@ var AIDiagram = (function (exports) {
|
|
|
7591
9764
|
case "bracket":
|
|
7592
9765
|
this._doAnnotationBracket(s.target, s.target2 ?? "", silent);
|
|
7593
9766
|
break;
|
|
9767
|
+
case "tick":
|
|
9768
|
+
this._doAnnotationTick(s.target, silent);
|
|
9769
|
+
break;
|
|
9770
|
+
case "strikeoff":
|
|
9771
|
+
this._doAnnotationStrikeoff(s.target, silent);
|
|
9772
|
+
break;
|
|
7594
9773
|
}
|
|
7595
9774
|
}
|
|
7596
9775
|
// ── highlight ────────────────────────────────────────────
|
|
@@ -7895,22 +10074,22 @@ var AIDiagram = (function (exports) {
|
|
|
7895
10074
|
}
|
|
7896
10075
|
// ── narration ───────────────────────────────────────────
|
|
7897
10076
|
_initCaption() {
|
|
7898
|
-
|
|
7899
|
-
|
|
10077
|
+
// Remove any leftover caption from a previous instance
|
|
10078
|
+
document.querySelector('.skm-caption')?.remove();
|
|
7900
10079
|
const cap = document.createElement("div");
|
|
7901
10080
|
cap.className = "skm-caption";
|
|
7902
|
-
cap.style.cssText = `
|
|
7903
|
-
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
|
7904
|
-
z-index: 9999; max-width: 600px; width: max-content;
|
|
7905
|
-
padding: 10px 24px; box-sizing: border-box;
|
|
7906
|
-
font-family: var(--font-sans, system-ui, sans-serif);
|
|
7907
|
-
font-size: 15px; line-height: 1.5;
|
|
7908
|
-
color: #fde68a; background: #1a1208;
|
|
7909
|
-
border-radius: 8px;
|
|
7910
|
-
box-shadow: 0 4px 24px rgba(0,0,0,0.35);
|
|
7911
|
-
opacity: 0; transition: opacity ${ANIMATION.narrationFadeMs}ms ease;
|
|
7912
|
-
pointer-events: none; user-select: none;
|
|
7913
|
-
text-align: center;
|
|
10081
|
+
cap.style.cssText = `
|
|
10082
|
+
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
|
10083
|
+
z-index: 9999; max-width: 600px; width: max-content;
|
|
10084
|
+
padding: 10px 24px; box-sizing: border-box;
|
|
10085
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
10086
|
+
font-size: 15px; line-height: 1.5;
|
|
10087
|
+
color: #fde68a; background: #1a1208;
|
|
10088
|
+
border-radius: 8px;
|
|
10089
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.35);
|
|
10090
|
+
opacity: 0; transition: opacity ${ANIMATION.narrationFadeMs}ms ease;
|
|
10091
|
+
pointer-events: none; user-select: none;
|
|
10092
|
+
text-align: center;
|
|
7914
10093
|
`;
|
|
7915
10094
|
const span = document.createElement("span");
|
|
7916
10095
|
cap.appendChild(span);
|
|
@@ -7926,7 +10105,7 @@ var AIDiagram = (function (exports) {
|
|
|
7926
10105
|
this._captionTextEl.textContent = text;
|
|
7927
10106
|
return;
|
|
7928
10107
|
}
|
|
7929
|
-
// Fire TTS
|
|
10108
|
+
// Fire TTS as full sentence — play() waits for _speechDone
|
|
7930
10109
|
if (this._tts && text)
|
|
7931
10110
|
this._speak(text);
|
|
7932
10111
|
// Typing effect
|
|
@@ -7949,11 +10128,17 @@ var AIDiagram = (function (exports) {
|
|
|
7949
10128
|
utter.rate = 0.95;
|
|
7950
10129
|
utter.pitch = 1;
|
|
7951
10130
|
utter.lang = "en-US";
|
|
10131
|
+
// Track when speech actually finishes
|
|
10132
|
+
this._speechDone = new Promise((resolve) => {
|
|
10133
|
+
utter.onend = () => resolve();
|
|
10134
|
+
utter.onerror = () => resolve();
|
|
10135
|
+
});
|
|
7952
10136
|
speechSynthesis.speak(utter);
|
|
7953
10137
|
}
|
|
7954
10138
|
_cancelSpeech() {
|
|
7955
10139
|
if (typeof speechSynthesis !== "undefined")
|
|
7956
10140
|
speechSynthesis.cancel();
|
|
10141
|
+
this._speechDone = null;
|
|
7957
10142
|
}
|
|
7958
10143
|
/** Pre-warm the speech engine with a silent utterance to eliminate cold-start delay */
|
|
7959
10144
|
_warmUpSpeech() {
|
|
@@ -8112,6 +10297,62 @@ var AIDiagram = (function (exports) {
|
|
|
8112
10297
|
`M ${m.x + m.w + pad} ${m.y - pad} L ${m.x - pad} ${m.y + m.h + pad}`;
|
|
8113
10298
|
this._animateAnnotation(roughG, guideD, silent);
|
|
8114
10299
|
}
|
|
10300
|
+
_doAnnotationTick(target, silent) {
|
|
10301
|
+
const el = resolveEl(this.svg, target);
|
|
10302
|
+
if (!el || !this._rc || !this._annotationLayer)
|
|
10303
|
+
return;
|
|
10304
|
+
const m = this._nodeMetrics(el);
|
|
10305
|
+
if (!m)
|
|
10306
|
+
return;
|
|
10307
|
+
// Tick mark on the left side of the node, like a teacher's check mark (✓)
|
|
10308
|
+
// The tick sits just to the left of the node, vertically centered
|
|
10309
|
+
const tickH = m.h * 0.5; // total tick height
|
|
10310
|
+
const tickW = tickH * 0.7; // total tick width
|
|
10311
|
+
const gap = 8; // gap between tick and node
|
|
10312
|
+
// Key points of the tick: start (top-left), valley (bottom), end (top-right)
|
|
10313
|
+
const endX = m.x - gap; // tip of the long upstroke (closest to node)
|
|
10314
|
+
const endY = m.y + m.h * 0.25; // top of the long stroke
|
|
10315
|
+
const valleyX = endX - tickW * 0.4; // bottom of the V
|
|
10316
|
+
const valleyY = m.y + m.h * 0.75; // bottom of the V
|
|
10317
|
+
const startX = valleyX - tickW * 0.3; // top of the short downstroke
|
|
10318
|
+
const startY = valleyY - tickH * 0.3; // slightly above valley
|
|
10319
|
+
const roughG = document.createElementNS(SVG_NS$1, "g");
|
|
10320
|
+
// Short down-stroke
|
|
10321
|
+
const line1 = this._rc.line(startX, startY, valleyX, valleyY, {
|
|
10322
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
10323
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
|
|
10324
|
+
});
|
|
10325
|
+
// Long up-stroke
|
|
10326
|
+
const line2 = this._rc.line(valleyX, valleyY, endX, endY, {
|
|
10327
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
10328
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now() + 1,
|
|
10329
|
+
});
|
|
10330
|
+
roughG.appendChild(line1);
|
|
10331
|
+
roughG.appendChild(line2);
|
|
10332
|
+
this._annotationLayer.appendChild(roughG);
|
|
10333
|
+
this._annotations.push(roughG);
|
|
10334
|
+
// Guide path: short stroke down then long stroke up (single continuous path)
|
|
10335
|
+
const guideD = `M ${startX} ${startY} L ${valleyX} ${valleyY} L ${endX} ${endY}`;
|
|
10336
|
+
this._animateAnnotation(roughG, guideD, silent);
|
|
10337
|
+
}
|
|
10338
|
+
_doAnnotationStrikeoff(target, silent) {
|
|
10339
|
+
const el = resolveEl(this.svg, target);
|
|
10340
|
+
if (!el || !this._rc || !this._annotationLayer)
|
|
10341
|
+
return;
|
|
10342
|
+
const m = this._nodeMetrics(el);
|
|
10343
|
+
if (!m)
|
|
10344
|
+
return;
|
|
10345
|
+
const pad = 6;
|
|
10346
|
+
const lineY = m.y + m.h / 2;
|
|
10347
|
+
const roughEl = this._rc.line(m.x - pad, lineY, m.x + m.w + pad, lineY, {
|
|
10348
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
10349
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
|
|
10350
|
+
});
|
|
10351
|
+
this._annotationLayer.appendChild(roughEl);
|
|
10352
|
+
this._annotations.push(roughEl);
|
|
10353
|
+
const guideD = `M ${m.x - pad} ${lineY} L ${m.x + m.w + pad} ${lineY}`;
|
|
10354
|
+
this._animateAnnotation(roughEl, guideD, silent);
|
|
10355
|
+
}
|
|
8115
10356
|
_doAnnotationBracket(target1, target2, silent) {
|
|
8116
10357
|
const el1 = resolveEl(this.svg, target1);
|
|
8117
10358
|
const el2 = resolveEl(this.svg, target2);
|
|
@@ -8178,42 +10419,42 @@ var AIDiagram = (function (exports) {
|
|
|
8178
10419
|
}
|
|
8179
10420
|
}
|
|
8180
10421
|
}
|
|
8181
|
-
const ANIMATION_CSS = `
|
|
8182
|
-
.ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
|
|
8183
|
-
transform-box: fill-box;
|
|
8184
|
-
transform-origin: center;
|
|
8185
|
-
transition: filter 0.3s, opacity 0.35s;
|
|
8186
|
-
}
|
|
8187
|
-
|
|
8188
|
-
/* highlight */
|
|
8189
|
-
.ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
|
|
8190
|
-
.tg.hl path, .tg.hl rect,
|
|
8191
|
-
.ntg.hl path, .ntg.hl polygon,
|
|
8192
|
-
.cg.hl path, .cg.hl rect,
|
|
8193
|
-
.mdg.hl text,
|
|
8194
|
-
.eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
|
|
8195
|
-
|
|
8196
|
-
.ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
|
|
8197
|
-
animation: ng-pulse 1.4s ease-in-out infinite;
|
|
8198
|
-
}
|
|
8199
|
-
@keyframes ng-pulse {
|
|
8200
|
-
0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
|
|
8201
|
-
50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
|
|
8202
|
-
}
|
|
8203
|
-
|
|
8204
|
-
/* fade */
|
|
8205
|
-
.ng.faded, .gg.faded, .tg.faded, .ntg.faded,
|
|
8206
|
-
.cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
|
|
8207
|
-
|
|
8208
|
-
.ng.hidden { opacity: 0; pointer-events: none; }
|
|
8209
|
-
.gg.gg-hidden { opacity: 0; }
|
|
8210
|
-
.tg.gg-hidden { opacity: 0; }
|
|
8211
|
-
.ntg.gg-hidden { opacity: 0; }
|
|
8212
|
-
.cg.gg-hidden { opacity: 0; }
|
|
8213
|
-
.mdg.gg-hidden { opacity: 0; }
|
|
8214
|
-
|
|
8215
|
-
/* narration caption */
|
|
8216
|
-
.skm-caption { pointer-events: none; user-select: none; }
|
|
10422
|
+
const ANIMATION_CSS = `
|
|
10423
|
+
.ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
|
|
10424
|
+
transform-box: fill-box;
|
|
10425
|
+
transform-origin: center;
|
|
10426
|
+
transition: filter 0.3s, opacity 0.35s;
|
|
10427
|
+
}
|
|
10428
|
+
|
|
10429
|
+
/* highlight */
|
|
10430
|
+
.ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
|
|
10431
|
+
.tg.hl path, .tg.hl rect,
|
|
10432
|
+
.ntg.hl path, .ntg.hl polygon,
|
|
10433
|
+
.cg.hl path, .cg.hl rect,
|
|
10434
|
+
.mdg.hl text,
|
|
10435
|
+
.eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
|
|
10436
|
+
|
|
10437
|
+
.ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
|
|
10438
|
+
animation: ng-pulse 1.4s ease-in-out infinite;
|
|
10439
|
+
}
|
|
10440
|
+
@keyframes ng-pulse {
|
|
10441
|
+
0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
|
|
10442
|
+
50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
|
|
10443
|
+
}
|
|
10444
|
+
|
|
10445
|
+
/* fade */
|
|
10446
|
+
.ng.faded, .gg.faded, .tg.faded, .ntg.faded,
|
|
10447
|
+
.cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
|
|
10448
|
+
|
|
10449
|
+
.ng.hidden { opacity: 0; pointer-events: none; }
|
|
10450
|
+
.gg.gg-hidden { opacity: 0; }
|
|
10451
|
+
.tg.gg-hidden { opacity: 0; }
|
|
10452
|
+
.ntg.gg-hidden { opacity: 0; }
|
|
10453
|
+
.cg.gg-hidden { opacity: 0; }
|
|
10454
|
+
.mdg.gg-hidden { opacity: 0; }
|
|
10455
|
+
|
|
10456
|
+
/* narration caption */
|
|
10457
|
+
.skm-caption { pointer-events: none; user-select: none; }
|
|
8217
10458
|
`;
|
|
8218
10459
|
|
|
8219
10460
|
// ============================================================
|
|
@@ -8460,12 +10701,10 @@ var AIDiagram = (function (exports) {
|
|
|
8460
10701
|
interactive: true,
|
|
8461
10702
|
onNodeClick,
|
|
8462
10703
|
});
|
|
8463
|
-
// Create rough.js instance for annotations
|
|
10704
|
+
// Create rough.js instance for annotations (same import as SVG renderer)
|
|
8464
10705
|
let rc = null;
|
|
8465
10706
|
try {
|
|
8466
|
-
|
|
8467
|
-
if (roughMod?.svg)
|
|
8468
|
-
rc = roughMod.svg(svg);
|
|
10707
|
+
rc = rough.svg(svg);
|
|
8469
10708
|
}
|
|
8470
10709
|
catch { /* rough.js not available — annotations disabled */ }
|
|
8471
10710
|
const containerEl = el instanceof SVGSVGElement ? undefined : el;
|
|
@@ -8474,7 +10713,7 @@ var AIDiagram = (function (exports) {
|
|
|
8474
10713
|
onReady?.(anim, svg);
|
|
8475
10714
|
const instance = {
|
|
8476
10715
|
scene, anim, svg, canvas,
|
|
8477
|
-
update: (newDsl) => render({ ...options, dsl: newDsl }),
|
|
10716
|
+
update: (newDsl) => { anim?.destroy(); return render({ ...options, dsl: newDsl }); },
|
|
8478
10717
|
exportSVG: (filename = 'diagram.svg') => {
|
|
8479
10718
|
if (svg) {
|
|
8480
10719
|
Promise.resolve().then(function () { return index; }).then(m => m.exportSVG(svg, { filename }));
|