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