@embedpdf/web 3.0.0-next.12 → 3.0.0-next.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1157,6 +1157,756 @@ function indexedDbByteStore(dbName, opts = {}) {
1157
1157
  };
1158
1158
  }
1159
1159
  //#endregion
1160
- export { attachSelectionHandle, bindPaintedImage, computeReleaseVelocity, copySelection, createDefaultActionsUiAdapter, createStageGestureController, createStageSurface, indexedDbByteStore, observeClientGeometry, pickFile, pickImageFile, positionAnchoredRect, projectAnchoredTarget, sanitizeExternalUri, saveFile, svgCursor, vibrationFeedback, wheelZoomFactor, wireSelectionClipboard, wkFeedback };
1160
+ //#region src/web-font.ts
1161
+ /**
1162
+ * Mount a registered font for the DOM: the engine lays text out with the
1163
+ * font's bytes, the browser needs the same face as a `@font-face` so the
1164
+ * live editor and the vector renderer show the glyphs the appearance stream
1165
+ * will bake. The family name is the font's KEY — the CSS family the
1166
+ * annotation plugin emits for a registered font (`cssFontFamily`) — so
1167
+ * mounting under the key is what makes the two agree.
1168
+ *
1169
+ * Returns the unmount. Idempotent per (document, key): a second mount of a
1170
+ * key already in the document's font set is a no-op that still returns an
1171
+ * unmount of its own (the face leaves when the LAST holder unmounts).
1172
+ */
1173
+ async function mountWebFont(key, data, options = {}) {
1174
+ const doc = options.document ?? document;
1175
+ const fonts = doc.fonts;
1176
+ if (!fonts || typeof FontFace === "undefined") return () => {};
1177
+ const holders = holdersOf(doc);
1178
+ const existing = holders.get(key);
1179
+ if (existing) {
1180
+ existing.count++;
1181
+ return () => release(holders, key);
1182
+ }
1183
+ const buffer = data instanceof Uint8Array ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : data;
1184
+ const face = new FontFace(key, buffer, {
1185
+ weight: options.weight !== void 0 ? String(options.weight) : void 0,
1186
+ style: options.style
1187
+ });
1188
+ holders.set(key, {
1189
+ face,
1190
+ doc,
1191
+ count: 1
1192
+ });
1193
+ try {
1194
+ await face.load();
1195
+ fonts.add(face);
1196
+ } catch {
1197
+ holders.delete(key);
1198
+ return () => {};
1199
+ }
1200
+ notifyFontChange(doc);
1201
+ return () => release(holders, key);
1202
+ }
1203
+ const HOLDERS = /* @__PURE__ */ new WeakMap();
1204
+ const LISTENERS = /* @__PURE__ */ new WeakMap();
1205
+ /** Internal: refresh editor styles for CSS loads and explicitly mounted faces.
1206
+ * Adding an already loaded FontFace does not trigger `loadingdone`. */
1207
+ function observeWebFonts(doc, onChange) {
1208
+ let listeners = LISTENERS.get(doc);
1209
+ if (!listeners) {
1210
+ listeners = /* @__PURE__ */ new Set();
1211
+ LISTENERS.set(doc, listeners);
1212
+ }
1213
+ listeners.add(onChange);
1214
+ doc.fonts?.addEventListener("loadingdone", onChange);
1215
+ doc.fonts?.addEventListener("loadingerror", onChange);
1216
+ return () => {
1217
+ listeners.delete(onChange);
1218
+ doc.fonts?.removeEventListener("loadingdone", onChange);
1219
+ doc.fonts?.removeEventListener("loadingerror", onChange);
1220
+ };
1221
+ }
1222
+ function notifyFontChange(doc) {
1223
+ for (const listener of LISTENERS.get(doc) ?? []) listener();
1224
+ }
1225
+ function holdersOf(doc) {
1226
+ let map = HOLDERS.get(doc);
1227
+ if (!map) {
1228
+ map = /* @__PURE__ */ new Map();
1229
+ HOLDERS.set(doc, map);
1230
+ }
1231
+ return map;
1232
+ }
1233
+ function release(holders, key) {
1234
+ const holder = holders.get(key);
1235
+ if (!holder) return;
1236
+ holder.count--;
1237
+ if (holder.count > 0) return;
1238
+ holders.delete(key);
1239
+ try {
1240
+ holder.doc.fonts.delete(holder.face);
1241
+ } catch {}
1242
+ notifyFontChange(holder.doc);
1243
+ }
1244
+ //#endregion
1245
+ //#region src/web-font-metrics.ts
1246
+ /** Acrobat's leading: a constant share of the size, never the font's lineGap. */
1247
+ const ACROBAT_LEADING = .2;
1248
+ /** The engine's metrics for the standard families (Acrobat's substitutes'
1249
+ * `hhea`), keyed the engine's way: no case, spaces, hyphens, quotes. */
1250
+ const STANDARD_METRICS = {
1251
+ helvetica: {
1252
+ ascent: .83,
1253
+ descent: .17
1254
+ },
1255
+ arial: {
1256
+ ascent: .83,
1257
+ descent: .17
1258
+ },
1259
+ arialmt: {
1260
+ ascent: .83,
1261
+ descent: .17
1262
+ },
1263
+ times: {
1264
+ ascent: .784,
1265
+ descent: .216
1266
+ },
1267
+ timesroman: {
1268
+ ascent: .784,
1269
+ descent: .216
1270
+ },
1271
+ timesnewroman: {
1272
+ ascent: .784,
1273
+ descent: .216
1274
+ },
1275
+ courier: {
1276
+ ascent: .627,
1277
+ descent: .373
1278
+ },
1279
+ couriernew: {
1280
+ ascent: .627,
1281
+ descent: .373
1282
+ }
1283
+ };
1284
+ /** What the browser resolves when nothing can be measured: a one-em content
1285
+ * area, the ascent of the engine's Helvetica. */
1286
+ const FALLBACK_METRICS = {
1287
+ ascent: .83,
1288
+ descent: .17
1289
+ };
1290
+ function familyKey(family) {
1291
+ return family.toLowerCase().replace(/[\s\-_'"]/g, "");
1292
+ }
1293
+ /** The first family of a CSS family list, unquoted. */
1294
+ function firstFamily$1(cssFamily) {
1295
+ return (cssFamily.split(",")[0] ?? "").trim().replace(/^["']|["']$/g, "");
1296
+ }
1297
+ const CONTEXTS = /* @__PURE__ */ new WeakMap();
1298
+ /**
1299
+ * The browser's own ascent and descent for a CSS family list, per em,
1300
+ * measured through a canvas. `null` when the platform cannot measure
1301
+ * (no canvas, no `fontBoundingBoxAscent`). Reads the currently resolved face
1302
+ * on every call, including while the intended font is loading.
1303
+ */
1304
+ function webFontMetrics(cssFamily, doc = typeof document === "undefined" ? void 0 : document, face = {}) {
1305
+ if (!doc) return null;
1306
+ let ctx = CONTEXTS.get(doc);
1307
+ if (!ctx) {
1308
+ const context = doc.createElement("canvas").getContext("2d");
1309
+ if (!context) return null;
1310
+ ctx = context;
1311
+ CONTEXTS.set(doc, ctx);
1312
+ }
1313
+ ctx.font = `${face.style ?? "normal"} ${face.weight ?? 400} 100px ${cssFamily}`;
1314
+ const m = ctx.measureText("Hg");
1315
+ if (typeof m.fontBoundingBoxAscent !== "number" || typeof m.fontBoundingBoxDescent !== "number") return null;
1316
+ return {
1317
+ ascent: m.fontBoundingBoxAscent / 100,
1318
+ descent: m.fontBoundingBoxDescent / 100
1319
+ };
1320
+ }
1321
+ /**
1322
+ * The standard families' engine metrics, or a browser approximation for
1323
+ * other families. `measure` is injectable for tests and resolved face styles.
1324
+ */
1325
+ function lineModelFor(cssFamily, measure = webFontMetrics) {
1326
+ const metrics = STANDARD_METRICS[familyKey(firstFamily$1(cssFamily))] ?? measure(cssFamily) ?? FALLBACK_METRICS;
1327
+ return {
1328
+ ascent: metrics.ascent,
1329
+ descent: metrics.descent,
1330
+ lineHeight: Math.round((metrics.ascent + metrics.descent + ACROBAT_LEADING) * 1e3) / 1e3
1331
+ };
1332
+ }
1333
+ /**
1334
+ * Screen px to raise a box's first line by so its baseline matches the
1335
+ * engine's: CSS puts half the leading above the glyphs and draws them with
1336
+ * the browser's face, whose ascent differs from the engine's for the
1337
+ * standard families. `fontSize` in screen px.
1338
+ */
1339
+ function firstLineShiftFor(cssFamily, fontSize, measure = webFontMetrics) {
1340
+ const model = lineModelFor(cssFamily, measure);
1341
+ const web = measure(cssFamily) ?? {
1342
+ ascent: model.ascent,
1343
+ descent: model.descent
1344
+ };
1345
+ const cssBaseline = (model.lineHeight - (web.ascent + web.descent)) / 2 + web.ascent;
1346
+ return Math.round((cssBaseline - model.ascent) * fontSize * 100) / 100;
1347
+ }
1348
+ //#endregion
1349
+ //#region src/rich-text-editor.ts
1350
+ /**
1351
+ * Rich text editor binding — the ONE contentEditable mechanics module for a
1352
+ * FreeText annotation, shared by every framework adapter (React, Angular,
1353
+ * Vue, Svelte…) so editing can never drift between them:
1354
+ *
1355
+ * • render a rich document (runs of style deltas over a body) into the
1356
+ * element as paragraphs of inline-styled spans
1357
+ * • serialise the element back into runs on `input` — inline styles only,
1358
+ * never computed styles, so the editor cannot invent properties
1359
+ * • map the DOM selection to flat offsets over the plain projection (and
1360
+ * back), and keep the caret through a model-driven re-render
1361
+ * • hold re-renders during IME composition, paste as plain text, forward
1362
+ * Cmd/Ctrl+B/I/U as commands
1363
+ *
1364
+ * The browser owns typing; this module only translates. The POLICY — what a
1365
+ * command does to the document, when to commit, what a family maps to —
1366
+ * belongs to the host (the annotation plugin), reached through the
1367
+ * structural {@link RichTextEditorHost}: @embedpdf/web imports no plugin,
1368
+ * per the layering law. The return shape is a Svelte action; the other
1369
+ * frameworks call the same two functions from their effects.
1370
+ *
1371
+ * Offsets: positions in the plain projection — paragraphs joined by `\r`,
1372
+ * runs concatenated, a hard break inside a run is a `\r` too. The same
1373
+ * arithmetic the core's run algebra uses.
1374
+ *
1375
+ * Known limits (by design, recorded in the plan): the browser's native undo
1376
+ * stack does not span a host-driven restyle; DOM line wrapping approximates
1377
+ * the engine's layout, the appearance stream is the truth.
1378
+ */
1379
+ const ELEMENT_NODE = 1;
1380
+ const TEXT_NODE = 3;
1381
+ function isBlock(node) {
1382
+ if (node.nodeType !== ELEMENT_NODE) return false;
1383
+ const name = node.nodeName.toUpperCase();
1384
+ return name === "DIV" || name === "P";
1385
+ }
1386
+ function isBreak(node) {
1387
+ return node.nodeType === ELEMENT_NODE && node.nodeName.toUpperCase() === "BR";
1388
+ }
1389
+ function lastChild(node) {
1390
+ return node.childNodes[node.childNodes.length - 1];
1391
+ }
1392
+ /**
1393
+ * A `<br>` that nothing follows inside its block (or the root) is the
1394
+ * browser's caret placeholder, not content — even wrapped in a span, as
1395
+ * Chrome does for a new styled line. Every other break is a hard line break.
1396
+ */
1397
+ function isTrailingBreak(node, root) {
1398
+ for (let n = node;;) {
1399
+ const parent = n.parentNode;
1400
+ if (!parent) return false;
1401
+ if (lastChild(parent) !== n) return false;
1402
+ if (parent === root || isBlock(parent)) return true;
1403
+ n = parent;
1404
+ }
1405
+ }
1406
+ function contains(root, node) {
1407
+ for (let n = node; n; n = n.parentNode) if (n === root) return true;
1408
+ return false;
1409
+ }
1410
+ const HEX = /^#([0-9a-f]{6})$/i;
1411
+ const RGB = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i;
1412
+ function cssColorToHex(css) {
1413
+ const trimmed = css.trim();
1414
+ if (HEX.test(trimmed)) return trimmed.toUpperCase();
1415
+ const m = RGB.exec(trimmed);
1416
+ if (!m) return void 0;
1417
+ const hex = (n) => Math.max(0, Math.min(255, Number(n))).toString(16).padStart(2, "0");
1418
+ return `#${hex(m[1])}${hex(m[2])}${hex(m[3])}`.toUpperCase();
1419
+ }
1420
+ function firstFamily(css) {
1421
+ const first = css.split(",")[0]?.trim().replace(/^["']|["']$/g, "");
1422
+ return first ? first : void 0;
1423
+ }
1424
+ function round2(n) {
1425
+ return Math.round(n * 100) / 100;
1426
+ }
1427
+ /** The inline style declarations a span carries, as a style delta. Only what
1428
+ * the span itself sets: an unset property means "inherit", never a value. */
1429
+ function styleDeltaOf(style, scale) {
1430
+ const delta = {};
1431
+ if (!style) return delta;
1432
+ const weight = style.fontWeight;
1433
+ if (weight) {
1434
+ const parsed = weight === "bold" ? 700 : weight === "normal" ? 400 : Number(weight);
1435
+ if (Number.isFinite(parsed) && parsed > 0) delta.weight = parsed;
1436
+ }
1437
+ if (style.fontStyle) delta.italic = style.fontStyle === "italic" || style.fontStyle === "oblique";
1438
+ const decoration = style.textDecorationLine || style.textDecoration;
1439
+ if (decoration) delta.decoration = decoration.split(/\s+/).filter((p) => p === "underline" || p === "line-through");
1440
+ if (style.color) {
1441
+ const hex = cssColorToHex(style.color);
1442
+ if (hex) delta.color = hex;
1443
+ }
1444
+ const script = style.verticalAlign;
1445
+ if (script === "sub" || script === "super") delta.script = script;
1446
+ if (style.fontSize && !delta.script) {
1447
+ const px = parseFloat(style.fontSize);
1448
+ if (Number.isFinite(px) && scale > 0) delta.size = round2(px / scale);
1449
+ }
1450
+ if (style.fontFamily) {
1451
+ const family = firstFamily(style.fontFamily);
1452
+ if (family) delta.family = family;
1453
+ }
1454
+ if (style.letterSpacing) {
1455
+ const px = parseFloat(style.letterSpacing);
1456
+ if (Number.isFinite(px) && scale > 0) delta.letterSpacing = round2(px / scale);
1457
+ }
1458
+ return delta;
1459
+ }
1460
+ function applyStyleDelta(style, delta, scale, cssFontFamily) {
1461
+ if (delta.weight !== void 0) style.fontWeight = String(delta.weight);
1462
+ if (delta.italic !== void 0) style.fontStyle = delta.italic ? "italic" : "normal";
1463
+ if (delta.decoration !== void 0) {
1464
+ const lines = delta.decoration.map((d) => d === "word" ? "underline" : d);
1465
+ style.textDecoration = lines.length ? [...new Set(lines)].join(" ") : "none";
1466
+ }
1467
+ if (delta.color !== void 0) style.color = delta.color;
1468
+ if (delta.script !== void 0 && delta.script !== "normal") {
1469
+ style.verticalAlign = delta.script;
1470
+ style.fontSize = "0.66em";
1471
+ } else if (delta.size !== void 0) style.fontSize = `${delta.size * scale}px`;
1472
+ if (delta.family !== void 0) style.fontFamily = cssFontFamily(delta.family);
1473
+ if (delta.letterSpacing !== void 0) style.letterSpacing = `${delta.letterSpacing * scale}px`;
1474
+ }
1475
+ function mergeDelta(base, over) {
1476
+ return {
1477
+ ...base,
1478
+ ...over
1479
+ };
1480
+ }
1481
+ function sameDelta(a, b) {
1482
+ const da = a ?? {};
1483
+ const db = b ?? {};
1484
+ const keys = /* @__PURE__ */ new Set([...Object.keys(da), ...Object.keys(db)]);
1485
+ for (const key of keys) {
1486
+ const va = da[key];
1487
+ const vb = db[key];
1488
+ if (Array.isArray(va) || Array.isArray(vb)) {
1489
+ if ([...va ?? []].sort().join(" ") !== [...vb ?? []].sort().join(" ")) return false;
1490
+ } else if (va !== vb) return false;
1491
+ }
1492
+ return true;
1493
+ }
1494
+ /** Render a document into the element: one block per paragraph, one span per
1495
+ * styled run, `<br>` for a hard break, a placeholder `<br>` at the end of a
1496
+ * block that is empty or ends on a break so the caret has a home. */
1497
+ function renderRichText(root, doc, options) {
1498
+ const factory = root.ownerDocument;
1499
+ if (!factory) return;
1500
+ const blocks = [];
1501
+ const paragraphs = doc.paragraphs.length ? doc.paragraphs : [{ runs: [{ text: "" }] }];
1502
+ for (const paragraph of paragraphs) {
1503
+ const block = factory.createElement("div");
1504
+ if (paragraph.align) block.style.textAlign = paragraph.align;
1505
+ if (paragraph.dir) block.style.direction = paragraph.dir;
1506
+ let endsWithBreak = false;
1507
+ for (const run of paragraph.runs) {
1508
+ let container = block;
1509
+ if (run.style && Object.keys(run.style).length) {
1510
+ container = factory.createElement("span");
1511
+ applyStyleDelta(container.style, run.style, options.scale, options.cssFontFamily);
1512
+ block.appendChild(container);
1513
+ }
1514
+ const pieces = run.text.split(/\r\n|\r|\n/);
1515
+ for (let i = 0; i < pieces.length; i++) {
1516
+ if (i > 0) {
1517
+ container.appendChild(factory.createElement("br"));
1518
+ endsWithBreak = true;
1519
+ }
1520
+ const piece = pieces[i];
1521
+ if (!piece) continue;
1522
+ container.appendChild(factory.createTextNode(piece));
1523
+ endsWithBreak = false;
1524
+ }
1525
+ }
1526
+ if (endsWithBreak || !lastChild(block)) block.appendChild(factory.createElement("br"));
1527
+ blocks.push(block);
1528
+ }
1529
+ if (root.replaceChildren) root.replaceChildren(...blocks);
1530
+ else {
1531
+ while (root.firstChild && root.removeChild) root.removeChild(root.firstChild);
1532
+ for (const block of blocks) root.appendChild(block);
1533
+ }
1534
+ }
1535
+ /** Keep the baseline adjustment on the first block after Enter clones it. */
1536
+ function applyFirstLineShift(root, shift) {
1537
+ for (let i = 0; i < root.childNodes.length; i++) {
1538
+ const node = root.childNodes[i];
1539
+ if (!node.style) continue;
1540
+ node.style.marginTop = i === 0 && shift ? `${-shift}px` : "";
1541
+ }
1542
+ }
1543
+ function pushText(walk, text, style) {
1544
+ if (!walk.current) {
1545
+ walk.current = { runs: [] };
1546
+ walk.paragraphs.push(walk.current);
1547
+ }
1548
+ const runs = walk.current.runs;
1549
+ const delta = Object.keys(style).length ? style : void 0;
1550
+ const last = runs[runs.length - 1];
1551
+ if (last && sameDelta(last.style, delta)) last.text += text;
1552
+ else runs.push(delta ? {
1553
+ text,
1554
+ style: delta
1555
+ } : { text });
1556
+ }
1557
+ function walkNode(node, inherited, walk) {
1558
+ if (node.nodeType === TEXT_NODE) {
1559
+ const text = (node.nodeValue ?? "").replace(/\r\n|\n/g, "\r");
1560
+ if (text) pushText(walk, text, inherited);
1561
+ return;
1562
+ }
1563
+ if (node.nodeType !== ELEMENT_NODE) return;
1564
+ if (isBreak(node)) {
1565
+ if (!isTrailingBreak(node, walk.root)) pushText(walk, "\r", inherited);
1566
+ return;
1567
+ }
1568
+ const name = node.nodeName.toUpperCase();
1569
+ let style = inherited;
1570
+ if (name === "B" || name === "STRONG") style = mergeDelta(style, { weight: 700 });
1571
+ if (name === "I" || name === "EM") style = mergeDelta(style, { italic: true });
1572
+ if (name === "U") style = mergeDelta(style, { decoration: ["underline"] });
1573
+ if (name === "SUB") style = mergeDelta(style, { script: "sub" });
1574
+ if (name === "SUP") style = mergeDelta(style, { script: "super" });
1575
+ style = mergeDelta(style, styleDeltaOf(node.style, walk.scale));
1576
+ if (isBlock(node)) {
1577
+ const paragraph = { runs: [] };
1578
+ const align = node.style?.textAlign;
1579
+ if (align === "left" || align === "center" || align === "right" || align === "justify") paragraph.align = align;
1580
+ if (node.style?.direction === "rtl") paragraph.dir = "rtl";
1581
+ walk.paragraphs.push(paragraph);
1582
+ walk.current = paragraph;
1583
+ for (let i = 0; i < node.childNodes.length; i++) walkNode(node.childNodes[i], style, walk);
1584
+ walk.current = null;
1585
+ return;
1586
+ }
1587
+ for (let i = 0; i < node.childNodes.length; i++) walkNode(node.childNodes[i], style, walk);
1588
+ }
1589
+ /** The element's content as a document. Inline styles (and the semantic
1590
+ * b/i/u/sub/sup tags a browser may insert) only. */
1591
+ function serializeRichText(root, scale) {
1592
+ const walk = {
1593
+ root,
1594
+ scale,
1595
+ paragraphs: [],
1596
+ current: null
1597
+ };
1598
+ for (let i = 0; i < root.childNodes.length; i++) walkNode(root.childNodes[i], {}, walk);
1599
+ for (const paragraph of walk.paragraphs) if (paragraph.runs.length === 0) paragraph.runs.push({ text: "" });
1600
+ if (walk.paragraphs.length === 0) walk.paragraphs.push({ runs: [{ text: "" }] });
1601
+ return { paragraphs: walk.paragraphs };
1602
+ }
1603
+ /** The element in document order, each node with its flat offset — counted
1604
+ * like the projection: text length, 1 per hard break, 1 between blocks. */
1605
+ function collectOffsets(root) {
1606
+ const events = [];
1607
+ let offset = 0;
1608
+ let blocks = 0;
1609
+ const visit = (node) => {
1610
+ if (node.nodeType === TEXT_NODE) {
1611
+ const length = (node.nodeValue ?? "").length;
1612
+ events.push({
1613
+ kind: "text",
1614
+ node,
1615
+ at: offset,
1616
+ length
1617
+ });
1618
+ offset += length;
1619
+ return;
1620
+ }
1621
+ if (node.nodeType !== ELEMENT_NODE) return;
1622
+ if (isBreak(node)) {
1623
+ if (isTrailingBreak(node, root)) return;
1624
+ events.push({
1625
+ kind: "break",
1626
+ node,
1627
+ at: offset,
1628
+ length: 1
1629
+ });
1630
+ offset += 1;
1631
+ return;
1632
+ }
1633
+ if (isBlock(node)) {
1634
+ if (blocks > 0) offset += 1;
1635
+ blocks++;
1636
+ }
1637
+ events.push({
1638
+ kind: "enter",
1639
+ node,
1640
+ at: offset,
1641
+ length: 0
1642
+ });
1643
+ for (let i = 0; i < node.childNodes.length; i++) visit(node.childNodes[i]);
1644
+ events.push({
1645
+ kind: "exit",
1646
+ node,
1647
+ at: offset,
1648
+ length: 0
1649
+ });
1650
+ };
1651
+ for (let i = 0; i < root.childNodes.length; i++) visit(root.childNodes[i]);
1652
+ return events;
1653
+ }
1654
+ /** The flat offset of a DOM position (a node and an offset within it). */
1655
+ function offsetOfPosition(root, position) {
1656
+ if (!contains(root, position.node)) return 0;
1657
+ const events = collectOffsets(root);
1658
+ if (position.node.nodeType === TEXT_NODE) {
1659
+ const event = events.find((e) => e.kind === "text" && e.node === position.node);
1660
+ return event ? event.at + Math.min(position.offset, event.length) : 0;
1661
+ }
1662
+ if (isBreak(position.node)) {
1663
+ const event = events.find((e) => e.kind === "break" && e.node === position.node);
1664
+ if (event) return event.at + (position.offset > 0 ? 1 : 0);
1665
+ const exit = events.find((e) => e.kind === "exit" && e.node === position.node.parentNode);
1666
+ return exit ? exit.at : 0;
1667
+ }
1668
+ const child = position.node.childNodes[position.offset];
1669
+ if (child) {
1670
+ const event = events.find((e) => e.node === child || e.kind !== "exit" && contains(child, e.node));
1671
+ if (event) return event.at;
1672
+ }
1673
+ if (position.node === root) {
1674
+ const last = events[events.length - 1];
1675
+ return last ? last.at + last.length : 0;
1676
+ }
1677
+ const exit = events.find((e) => e.kind === "exit" && e.node === position.node);
1678
+ return exit ? exit.at : 0;
1679
+ }
1680
+ function indexIn(parent, node) {
1681
+ for (let i = 0; i < parent.childNodes.length; i++) if (parent.childNodes[i] === node) return i;
1682
+ return 0;
1683
+ }
1684
+ /** The DOM position of a flat offset: inside a text node when one covers it,
1685
+ * else the empty block / the slot after a hard break the offset names. */
1686
+ function positionOfOffset(root, offset) {
1687
+ const events = collectOffsets(root);
1688
+ const text = events.find((e) => e.kind === "text" && offset >= e.at && offset <= e.at + e.length);
1689
+ if (text) return {
1690
+ node: text.node,
1691
+ offset: offset - text.at
1692
+ };
1693
+ const block = events.find((e) => e.kind === "enter" && isBlock(e.node) && e.at === offset);
1694
+ if (block) return {
1695
+ node: block.node,
1696
+ offset: 0
1697
+ };
1698
+ const after = events.find((e) => e.kind === "break" && e.at + 1 === offset);
1699
+ if (after && after.node.parentNode) return {
1700
+ node: after.node.parentNode,
1701
+ offset: indexIn(after.node.parentNode, after.node) + 1
1702
+ };
1703
+ for (let i = events.length - 1; i >= 0; i--) {
1704
+ const e = events[i];
1705
+ if (e.kind === "text") return {
1706
+ node: e.node,
1707
+ offset: e.length
1708
+ };
1709
+ }
1710
+ const first = root.childNodes[0];
1711
+ return first ? {
1712
+ node: first,
1713
+ offset: 0
1714
+ } : null;
1715
+ }
1716
+ /** The selection's range as the shadow tree sees it: the standard
1717
+ * `getComposedRanges` (options form, then the earlier positional form). */
1718
+ function composedRange(sel, shadowRoot) {
1719
+ const s = sel;
1720
+ if (typeof s.getComposedRanges !== "function") return null;
1721
+ for (const args of [[{ shadowRoots: [shadowRoot] }], [shadowRoot]]) try {
1722
+ const ranges = s.getComposedRanges(...args);
1723
+ if (ranges.length) return ranges[0];
1724
+ } catch {}
1725
+ return null;
1726
+ }
1727
+ function documentsEqual(a, b) {
1728
+ if (a.paragraphs.length !== b.paragraphs.length) return false;
1729
+ for (let i = 0; i < a.paragraphs.length; i++) {
1730
+ const pa = a.paragraphs[i];
1731
+ const pb = b.paragraphs[i];
1732
+ if ((pa.align ?? "left") !== (pb.align ?? "left") || (pa.dir ?? "ltr") !== (pb.dir ?? "ltr")) return false;
1733
+ if (pa.runs.length !== pb.runs.length) return false;
1734
+ for (let j = 0; j < pa.runs.length; j++) {
1735
+ const ra = pa.runs[j];
1736
+ const rb = pb.runs[j];
1737
+ if (ra.text !== rb.text || !sameDelta(ra.style, rb.style)) return false;
1738
+ }
1739
+ }
1740
+ return true;
1741
+ }
1742
+ /** Bind one contentEditable element. Returns the Svelte-action-shaped handle. */
1743
+ function attachRichTextEditor(el, host, initial) {
1744
+ const root = el;
1745
+ const doc = el.ownerDocument;
1746
+ const rootNode = el.getRootNode();
1747
+ const activeElement = () => rootNode.activeElement;
1748
+ const liveRange = () => {
1749
+ const sel = doc.getSelection();
1750
+ if (!sel) return null;
1751
+ if (rootNode !== doc) {
1752
+ const composed = composedRange(sel, rootNode);
1753
+ if (composed) return composed;
1754
+ const shadowSel = rootNode.getSelection?.();
1755
+ if (shadowSel && shadowSel.rangeCount > 0) return shadowSel.getRangeAt(0);
1756
+ }
1757
+ return sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
1758
+ };
1759
+ let props = initial;
1760
+ let rendered = null;
1761
+ let composing = false;
1762
+ const currentSelection = () => {
1763
+ const range = liveRange();
1764
+ if (!range) return null;
1765
+ const anchor = range.startContainer;
1766
+ const focus = range.endContainer;
1767
+ if (!contains(root, anchor) || !contains(root, focus)) return null;
1768
+ const start = offsetOfPosition(root, {
1769
+ node: anchor,
1770
+ offset: range.startOffset
1771
+ });
1772
+ const end = offsetOfPosition(root, {
1773
+ node: focus,
1774
+ offset: range.endOffset
1775
+ });
1776
+ return {
1777
+ start: Math.min(start, end),
1778
+ end: Math.max(start, end)
1779
+ };
1780
+ };
1781
+ const select = (range) => {
1782
+ const sel = doc.getSelection();
1783
+ if (!sel) return;
1784
+ const start = positionOfOffset(root, range.start);
1785
+ const end = positionOfOffset(root, range.end);
1786
+ if (!start || !end) return;
1787
+ if (typeof sel.setBaseAndExtent === "function") {
1788
+ sel.setBaseAndExtent(start.node, start.offset, end.node, end.offset);
1789
+ return;
1790
+ }
1791
+ const domRange = doc.createRange();
1792
+ domRange.setStart(start.node, start.offset);
1793
+ domRange.setEnd(end.node, end.offset);
1794
+ sel.removeAllRanges();
1795
+ sel.addRange(domRange);
1796
+ };
1797
+ const refreshLineModel = () => {
1798
+ const bodyStyle = doc.defaultView?.getComputedStyle(el) ?? el.style;
1799
+ const body = {
1800
+ family: bodyStyle.fontFamily || "Helvetica",
1801
+ weight: bodyStyle.fontWeight || "400",
1802
+ style: bodyStyle.fontStyle || "normal"
1803
+ };
1804
+ const size = parseFloat(bodyStyle.fontSize || "16") || 16;
1805
+ const measured = /* @__PURE__ */ new Map();
1806
+ const measureFor = (face) => (family) => {
1807
+ const key = JSON.stringify([
1808
+ family,
1809
+ face.weight,
1810
+ face.style
1811
+ ]);
1812
+ if (!measured.has(key)) measured.set(key, webFontMetrics(family, doc, face));
1813
+ return measured.get(key);
1814
+ };
1815
+ const runs = Array.from(el.querySelectorAll("span, b, strong, i, em")).map((node) => {
1816
+ const style = doc.defaultView?.getComputedStyle(node) ?? node.style;
1817
+ const face = {
1818
+ family: style.fontFamily || body.family,
1819
+ weight: style.fontWeight || body.weight,
1820
+ style: style.fontStyle || body.style
1821
+ };
1822
+ return {
1823
+ node,
1824
+ lineHeight: lineModelFor(face.family, measureFor(face)).lineHeight
1825
+ };
1826
+ });
1827
+ const measure = measureFor(body);
1828
+ const lineHeight = lineModelFor(body.family, measure).lineHeight;
1829
+ const shift = firstLineShiftFor(body.family, size, measure);
1830
+ el.style.lineHeight = String(lineHeight);
1831
+ for (const run of runs) run.node.style.lineHeight = String(run.lineHeight);
1832
+ applyFirstLineShift(root, shift);
1833
+ };
1834
+ const render = (document, keepSelection) => {
1835
+ const selection = keepSelection ? currentSelection() : null;
1836
+ renderRichText(root, document, {
1837
+ scale: props.scale,
1838
+ cssFontFamily: host.cssFontFamily
1839
+ });
1840
+ refreshLineModel();
1841
+ rendered = document;
1842
+ if (selection) select(selection);
1843
+ };
1844
+ const serialiseAndReport = () => {
1845
+ refreshLineModel();
1846
+ const document = serializeRichText(root, props.scale);
1847
+ rendered = document;
1848
+ host.onInput(document);
1849
+ };
1850
+ const onInput = () => {
1851
+ if (!composing) serialiseAndReport();
1852
+ };
1853
+ const onCompositionStart = () => {
1854
+ composing = true;
1855
+ };
1856
+ const onCompositionEnd = () => {
1857
+ composing = false;
1858
+ serialiseAndReport();
1859
+ };
1860
+ const onKeyDown = (e) => {
1861
+ if (!(e.metaKey || e.ctrlKey) || e.altKey) return;
1862
+ const key = e.key.toLowerCase();
1863
+ const command = key === "b" ? "bold" : key === "i" ? "italic" : key === "u" ? "underline" : null;
1864
+ if (!command) return;
1865
+ e.preventDefault();
1866
+ host.onCommand(command);
1867
+ };
1868
+ const onPaste = (e) => {
1869
+ e.preventDefault();
1870
+ const text = e.clipboardData?.getData("text/plain") ?? "";
1871
+ doc.execCommand("insertText", false, text);
1872
+ };
1873
+ const onSelectionChange = () => {
1874
+ if (activeElement() !== el) return;
1875
+ host.onSelectionChange(currentSelection());
1876
+ };
1877
+ el.addEventListener("input", onInput);
1878
+ el.addEventListener("compositionstart", onCompositionStart);
1879
+ el.addEventListener("compositionend", onCompositionEnd);
1880
+ el.addEventListener("keydown", onKeyDown);
1881
+ el.addEventListener("paste", onPaste);
1882
+ doc.addEventListener("selectionchange", onSelectionChange);
1883
+ render(initial.document, false);
1884
+ const stopObservingFonts = observeWebFonts(doc, refreshLineModel);
1885
+ return {
1886
+ update(next) {
1887
+ const scaleChanged = next.scale !== props.scale;
1888
+ props = next;
1889
+ if (composing) return;
1890
+ if (!rendered || scaleChanged || !documentsEqual(next.document, rendered)) {
1891
+ render(next.document, activeElement() === el);
1892
+ return;
1893
+ }
1894
+ refreshLineModel();
1895
+ },
1896
+ selection: currentSelection,
1897
+ select,
1898
+ detach() {
1899
+ stopObservingFonts();
1900
+ el.removeEventListener("input", onInput);
1901
+ el.removeEventListener("compositionstart", onCompositionStart);
1902
+ el.removeEventListener("compositionend", onCompositionEnd);
1903
+ el.removeEventListener("keydown", onKeyDown);
1904
+ el.removeEventListener("paste", onPaste);
1905
+ doc.removeEventListener("selectionchange", onSelectionChange);
1906
+ }
1907
+ };
1908
+ }
1909
+ //#endregion
1910
+ export { applyFirstLineShift, attachRichTextEditor, attachSelectionHandle, bindPaintedImage, computeReleaseVelocity, copySelection, createDefaultActionsUiAdapter, createStageGestureController, createStageSurface, firstLineShiftFor, indexedDbByteStore, lineModelFor, mountWebFont, observeClientGeometry, offsetOfPosition, pickFile, pickImageFile, positionAnchoredRect, positionOfOffset, projectAnchoredTarget, renderRichText, sanitizeExternalUri, saveFile, serializeRichText, styleDeltaOf, svgCursor, vibrationFeedback, webFontMetrics, wheelZoomFactor, wireSelectionClipboard, wkFeedback };
1161
1911
 
1162
1912
  //# sourceMappingURL=index.js.map