@bendyline/squisq 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +14 -1
  2. package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
  3. package/dist/{Doc-BsMkmzH8.d.ts → Doc-BBVGq1_9.d.ts} +94 -15
  4. package/dist/{ImageEditDoc-DvoeHMP9.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +1 -1
  5. package/dist/{chunk-4ZFRZBU2.js → chunk-5TYS5V3G.js} +866 -192
  6. package/dist/chunk-5TYS5V3G.js.map +1 -0
  7. package/dist/{chunk-7MQGNR76.js → chunk-A4O7GIWE.js} +2 -27
  8. package/dist/chunk-A4O7GIWE.js.map +1 -0
  9. package/dist/{chunk-OH43RPTP.js → chunk-BEQODJWV.js} +20 -3
  10. package/dist/chunk-BEQODJWV.js.map +1 -0
  11. package/dist/chunk-ET53IIEP.js +1 -0
  12. package/dist/chunk-ET53IIEP.js.map +1 -0
  13. package/dist/{chunk-KZNXZ5SM.js → chunk-GPLTCSXO.js} +18 -7
  14. package/dist/chunk-GPLTCSXO.js.map +1 -0
  15. package/dist/{chunk-OKJOQXK4.js → chunk-HWVFJAAH.js} +38 -2
  16. package/dist/chunk-HWVFJAAH.js.map +1 -0
  17. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  18. package/dist/chunk-IMSCRLLV.js.map +1 -0
  19. package/dist/{chunk-4AKUQPEE.js → chunk-KKNUBQ6Y.js} +1 -1
  20. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  21. package/dist/{chunk-TVSGLUID.js → chunk-LDQ2HJIX.js} +3 -3
  22. package/dist/{chunk-RUDYOTA6.js → chunk-LTN6I7QW.js} +56 -1
  23. package/dist/chunk-LTN6I7QW.js.map +1 -0
  24. package/dist/{chunk-2OIBZYKP.js → chunk-P3K7NLHF.js} +2 -2
  25. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  26. package/dist/chunk-T5UK6YOB.js.map +1 -0
  27. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  28. package/dist/chunk-TQWLI6S2.js.map +1 -0
  29. package/dist/{chunk-LRCV7N7F.js → chunk-UY7KGQ5R.js} +2 -2
  30. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  31. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  32. package/dist/doc/index.d.ts +4 -4
  33. package/dist/doc/index.js +24 -9
  34. package/dist/generate/index.d.ts +2 -80
  35. package/dist/generate/index.js +2 -7
  36. package/dist/imageEdit/index.d.ts +4 -4
  37. package/dist/imageEdit/index.js +2 -2
  38. package/dist/index.d.ts +8 -8
  39. package/dist/index.js +41 -26
  40. package/dist/jsonForm/index.d.ts +42 -1
  41. package/dist/jsonForm/index.js +9 -1
  42. package/dist/markdown/index.d.ts +18 -2
  43. package/dist/markdown/index.js +5 -3
  44. package/dist/recommend/index.js +2 -2
  45. package/dist/schemas/index.d.ts +4 -4
  46. package/dist/schemas/index.js +3 -5
  47. package/dist/storage/index.d.ts +2 -2
  48. package/dist/storage/index.js +1 -1
  49. package/dist/story/index.d.ts +151 -13
  50. package/dist/story/index.js +24 -9
  51. package/dist/{themeLibrary-DQQKuRMx.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  52. package/dist/timing/index.js +3 -5
  53. package/dist/transform/index.d.ts +2 -2
  54. package/dist/transform/index.js +3 -3
  55. package/dist/versions/index.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/src/__tests__/contentExtractor.test.ts +60 -0
  58. package/src/__tests__/coverBlock.test.ts +79 -3
  59. package/src/__tests__/customTemplateSample.test.ts +63 -2
  60. package/src/__tests__/customTemplatesFrontmatter.test.ts +31 -0
  61. package/src/__tests__/customThemesFrontmatter.test.ts +27 -0
  62. package/src/__tests__/inputDescriptors.test.ts +162 -0
  63. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  64. package/src/__tests__/markdown.test.ts +64 -0
  65. package/src/__tests__/markdownSanitize.test.ts +15 -0
  66. package/src/__tests__/markdownToDoc.test.ts +173 -2
  67. package/src/__tests__/mediaAnnotations.test.ts +27 -0
  68. package/src/__tests__/shapeGeometry.test.ts +37 -0
  69. package/src/__tests__/structuredData.test.ts +73 -3
  70. package/src/__tests__/templateMetadata.test.ts +11 -1
  71. package/src/__tests__/templates.test.ts +112 -0
  72. package/src/__tests__/validateDoc.test.ts +160 -0
  73. package/src/doc/annotationBlocks.ts +117 -0
  74. package/src/doc/customTemplatesFrontmatter.ts +7 -1
  75. package/src/doc/customThemesFrontmatter.ts +6 -1
  76. package/src/doc/docToMarkdown.ts +78 -29
  77. package/src/doc/getLayers.ts +11 -2
  78. package/src/doc/index.ts +12 -0
  79. package/src/doc/markdownToDoc.ts +80 -3
  80. package/src/doc/mediaAnnotations.ts +15 -33
  81. package/src/doc/standaloneAnnotation.ts +58 -0
  82. package/src/doc/structuredData.ts +92 -21
  83. package/src/doc/templates/__tests__/customTemplate.test.ts +21 -2
  84. package/src/doc/templates/comparisonBar.ts +8 -1
  85. package/src/doc/templates/coverBlock.ts +78 -15
  86. package/src/doc/templates/customTemplate.ts +10 -4
  87. package/src/doc/templates/diagramBlock.ts +2 -2
  88. package/src/doc/templates/drawingBlock.ts +2 -2
  89. package/src/doc/templates/factCard.ts +4 -2
  90. package/src/doc/templates/imageWithCaption.ts +10 -8
  91. package/src/doc/templates/index.ts +12 -2
  92. package/src/doc/templates/inputDescriptors.ts +432 -0
  93. package/src/doc/templates/photoGrid.ts +16 -39
  94. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +267 -1
  95. package/src/doc/templates/tokens/resolveTokens.ts +0 -0
  96. package/src/doc/templates/videoWithCaption.ts +15 -14
  97. package/src/doc/utils/nearest.ts +59 -0
  98. package/src/doc/utils/shapeGeometry.ts +90 -2
  99. package/src/doc/validate.ts +153 -54
  100. package/src/generate/contentExtractor.ts +148 -35
  101. package/src/generate/index.ts +0 -3
  102. package/src/jsonForm/index.ts +2 -0
  103. package/src/jsonForm/tokens.ts +78 -0
  104. package/src/markdown/annotationCoercion.ts +1 -1
  105. package/src/markdown/attrTokens.ts +19 -0
  106. package/src/markdown/convert.ts +11 -1
  107. package/src/markdown/index.ts +2 -1
  108. package/src/markdown/sanitize.ts +22 -2
  109. package/src/markdown/stringify.ts +12 -3
  110. package/src/markdown/utils.ts +62 -1
  111. package/src/schemas/CustomTemplates.ts +56 -4
  112. package/src/schemas/Doc.ts +39 -41
  113. package/src/schemas/themeCompile.ts +1 -1
  114. package/src/storage/ContentContainer.ts +6 -2
  115. package/src/transform/types.ts +1 -1
  116. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  117. package/dist/chunk-3E5F2XMR.js.map +0 -1
  118. package/dist/chunk-4AKUQPEE.js.map +0 -1
  119. package/dist/chunk-4X3JQXNM.js +0 -35
  120. package/dist/chunk-4X3JQXNM.js.map +0 -1
  121. package/dist/chunk-4ZFRZBU2.js.map +0 -1
  122. package/dist/chunk-7MQGNR76.js.map +0 -1
  123. package/dist/chunk-FBKP5CAO.js +0 -145
  124. package/dist/chunk-FBKP5CAO.js.map +0 -1
  125. package/dist/chunk-FR2RBTKO.js.map +0 -1
  126. package/dist/chunk-K32VJONL.js.map +0 -1
  127. package/dist/chunk-KZNXZ5SM.js.map +0 -1
  128. package/dist/chunk-OH43RPTP.js.map +0 -1
  129. package/dist/chunk-OKJOQXK4.js.map +0 -1
  130. package/dist/chunk-RUDYOTA6.js.map +0 -1
  131. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  132. package/src/generate/slideshowGenerator.ts +0 -280
  133. /package/dist/{chunk-TVSGLUID.js.map → chunk-LDQ2HJIX.js.map} +0 -0
  134. /package/dist/{chunk-2OIBZYKP.js.map → chunk-P3K7NLHF.js.map} +0 -0
  135. /package/dist/{chunk-LRCV7N7F.js.map → chunk-UY7KGQ5R.js.map} +0 -0
@@ -2,7 +2,7 @@ import {
2
2
  extractContent,
3
3
  mapElementToBlock,
4
4
  stripMarkdown
5
- } from "./chunk-FR2RBTKO.js";
5
+ } from "./chunk-TQWLI6S2.js";
6
6
  import {
7
7
  SeededRandom,
8
8
  hashString
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  extractPlainText,
15
15
  getChildren
16
- } from "./chunk-OKJOQXK4.js";
16
+ } from "./chunk-HWVFJAAH.js";
17
17
 
18
18
  // src/transform/styles/documentary.ts
19
19
  var documentaryStyle = {
@@ -593,4 +593,4 @@ export {
593
593
  analyzeBlocks,
594
594
  applyTransform
595
595
  };
596
- //# sourceMappingURL=chunk-TVSGLUID.js.map
596
+ //# sourceMappingURL=chunk-LDQ2HJIX.js.map
@@ -308,6 +308,58 @@ function clipPoint(from, to) {
308
308
  function clipEndpoints(a, b) {
309
309
  return { start: clipPoint(a, b), end: clipPoint(b, a) };
310
310
  }
311
+ function snapPoints(box) {
312
+ const left = box.cx - box.rx;
313
+ const right = box.cx + box.rx;
314
+ const top = box.cy - box.ry;
315
+ const bottom = box.cy + box.ry;
316
+ return [
317
+ { port: "top", x: box.cx, y: top },
318
+ { port: "right", x: right, y: box.cy },
319
+ { port: "bottom", x: box.cx, y: bottom },
320
+ { port: "left", x: left, y: box.cy },
321
+ { port: "top-left", x: left, y: top },
322
+ { port: "top-right", x: right, y: top },
323
+ { port: "bottom-right", x: right, y: bottom },
324
+ { port: "bottom-left", x: left, y: bottom }
325
+ ];
326
+ }
327
+ function nearestSnapPoint(box, point) {
328
+ let best = snapPoints(box)[0];
329
+ let bestScore = Number.POSITIVE_INFINITY;
330
+ for (const candidate of snapPoints(box)) {
331
+ const score = distanceSq(candidate, point) + portTieBreak(candidate.port);
332
+ if (score < bestScore) {
333
+ best = candidate;
334
+ bestScore = score;
335
+ }
336
+ }
337
+ return best;
338
+ }
339
+ function snapEndpoints(a, b) {
340
+ let bestStart = snapPoints(a)[0];
341
+ let bestEnd = snapPoints(b)[0];
342
+ let bestScore = Number.POSITIVE_INFINITY;
343
+ for (const start of snapPoints(a)) {
344
+ for (const end of snapPoints(b)) {
345
+ const score = distanceSq(start, end) + portTieBreak(start.port) + portTieBreak(end.port);
346
+ if (score < bestScore) {
347
+ bestStart = start;
348
+ bestEnd = end;
349
+ bestScore = score;
350
+ }
351
+ }
352
+ }
353
+ return { start: bestStart, end: bestEnd };
354
+ }
355
+ function distanceSq(a, b) {
356
+ const dx = a.x - b.x;
357
+ const dy = a.y - b.y;
358
+ return dx * dx + dy * dy;
359
+ }
360
+ function portTieBreak(port) {
361
+ return port.includes("-") ? 0.02 : 0;
362
+ }
311
363
  function connectorPath(routing, start, end) {
312
364
  if (routing === "straight") {
313
365
  return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
@@ -339,7 +391,10 @@ export {
339
391
  markerPath,
340
392
  clipPoint,
341
393
  clipEndpoints,
394
+ snapPoints,
395
+ nearestSnapPoint,
396
+ snapEndpoints,
342
397
  connectorPath,
343
398
  lineStyleDasharray
344
399
  };
345
- //# sourceMappingURL=chunk-RUDYOTA6.js.map
400
+ //# sourceMappingURL=chunk-LTN6I7QW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/doc/utils/shapeGeometry.ts"],"sourcesContent":["/**\n * shapeGeometry — pure SVG geometry for the drawing/diagram shape vocabulary.\n *\n * Three concerns, all deterministic and dependency-free:\n * - `shapePath(kind, x, y, w, h)` → an SVG path `d` for a named shape inscribed\n * in a bounding box. Returns null for the natively-rendered primitives\n * (rect / circle / line / text / path) so the caller can keep using a\n * `ShapeLayer` for those; everything else becomes a computed `PathLayer`.\n * - `markerPath(style, dir)` → the `<marker>` path for a line end-style, in the\n * standard 0–10 marker viewBox (shared by the SSR `PathLayer` renderer and the\n * editor's edge renderer).\n * - `connectorPath(routing, start, end)` + `snapEndpoints(a, b)` → the line/edge\n * geometry between two shapes (straight / orthogonal / curved), lifted from the\n * diagram template so drawings and diagrams draw identical connectors. The older\n * `clipEndpoints(a, b)` helper remains for free-angle clipping, but rendered\n * connectors use snapped ports so authored lines land on stable attachment points.\n */\n\nimport type { MarkerStyle } from '../../schemas/Doc.js';\n\n// ============================================\n// Shapes\n// ============================================\n\n/** Connector routing styles. */\nexport type ConnectorRouting = 'straight' | 'orthogonal' | 'curved';\n\n/**\n * Shape kinds rendered as computed paths (everything except the primitives the\n * renderer draws natively). Used by templates to decide ShapeLayer vs PathLayer.\n */\nexport const PATH_SHAPE_KINDS = new Set([\n 'triangle',\n 'right-triangle',\n 'diamond',\n 'pentagon',\n 'hexagon',\n 'octagon',\n 'star',\n 'star4',\n 'star6',\n 'parallelogram',\n 'trapezoid',\n 'plus',\n 'chevron',\n 'arrow-right',\n 'arrow-left',\n 'arrow-up',\n 'arrow-down',\n 'double-arrow',\n 'callout',\n 'cylinder',\n 'cloud',\n 'heart',\n 'lightning',\n]);\n\n/** Round to 1 decimal for compact, stable path output. */\nfunction r(n: number): number {\n return Math.round(n * 10) / 10;\n}\n\n/** Close a list of points into an `M … L … Z` polygon path. */\nfunction poly(points: ReadonlyArray<readonly [number, number]>): string {\n if (points.length === 0) return '';\n const [first, ...rest] = points;\n const head = `M ${r(first[0])} ${r(first[1])}`;\n const tail = rest.map(([x, y]) => `L ${r(x)} ${r(y)}`).join(' ');\n return `${head} ${tail} Z`;\n}\n\n/** Regular polygon points inscribed in the box ellipse, `rotation` in radians. */\nfunction regular(\n cx: number,\n cy: number,\n rx: number,\n ry: number,\n sides: number,\n rotation: number,\n): Array<[number, number]> {\n const pts: Array<[number, number]> = [];\n for (let i = 0; i < sides; i++) {\n const a = rotation + (i * 2 * Math.PI) / sides;\n pts.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);\n }\n return pts;\n}\n\n/** Star points alternating outer/inner radius. */\nfunction star(\n cx: number,\n cy: number,\n rx: number,\n ry: number,\n spikes: number,\n innerRatio: number,\n): Array<[number, number]> {\n const pts: Array<[number, number]> = [];\n const rot = -Math.PI / 2; // first point up\n for (let i = 0; i < spikes * 2; i++) {\n const outer = i % 2 === 0;\n const a = rot + (i * Math.PI) / spikes;\n const fx = outer ? rx : rx * innerRatio;\n const fy = outer ? ry : ry * innerRatio;\n pts.push([cx + fx * Math.cos(a), cy + fy * Math.sin(a)]);\n }\n return pts;\n}\n\n/**\n * SVG path `d` for a named shape inscribed in `[x, y, w, h]`, or null when the\n * kind is rendered natively (rect / circle / line / text / path / arrow).\n */\nexport function shapePath(kind: string, x: number, y: number, w: number, h: number): string | null {\n const cx = x + w / 2;\n const cy = y + h / 2;\n const rx = w / 2;\n const ry = h / 2;\n const right = x + w;\n const bottom = y + h;\n\n switch (kind) {\n case 'triangle':\n return poly([\n [cx, y],\n [right, bottom],\n [x, bottom],\n ]);\n case 'right-triangle':\n return poly([\n [x, y],\n [x, bottom],\n [right, bottom],\n ]);\n case 'diamond':\n return poly([\n [cx, y],\n [right, cy],\n [cx, bottom],\n [x, cy],\n ]);\n case 'pentagon':\n return poly(regular(cx, cy, rx, ry, 5, -Math.PI / 2));\n case 'hexagon': {\n // Flat top/bottom hexagon: inset the left/right vertices.\n const inset = w * 0.25;\n return poly([\n [x + inset, y],\n [right - inset, y],\n [right, cy],\n [right - inset, bottom],\n [x + inset, bottom],\n [x, cy],\n ]);\n }\n case 'octagon': {\n const cut = Math.min(w, h) * 0.29;\n return poly([\n [x + cut, y],\n [right - cut, y],\n [right, y + cut],\n [right, bottom - cut],\n [right - cut, bottom],\n [x + cut, bottom],\n [x, bottom - cut],\n [x, y + cut],\n ]);\n }\n case 'star':\n return poly(star(cx, cy, rx, ry, 5, 0.4));\n case 'star4':\n return poly(star(cx, cy, rx, ry, 4, 0.4));\n case 'star6':\n return poly(star(cx, cy, rx, ry, 6, 0.5));\n case 'parallelogram': {\n const off = w * 0.25;\n return poly([\n [x + off, y],\n [right, y],\n [right - off, bottom],\n [x, bottom],\n ]);\n }\n case 'trapezoid': {\n const off = w * 0.25;\n return poly([\n [x + off, y],\n [right - off, y],\n [right, bottom],\n [x, bottom],\n ]);\n }\n case 'plus': {\n const t = Math.min(w, h) / 3; // arm thickness\n const lx = x + (w - t) / 2;\n const rxp = lx + t;\n const ty = y + (h - t) / 2;\n const byp = ty + t;\n return poly([\n [lx, y],\n [rxp, y],\n [rxp, ty],\n [right, ty],\n [right, byp],\n [rxp, byp],\n [rxp, bottom],\n [lx, bottom],\n [lx, byp],\n [x, byp],\n [x, ty],\n [lx, ty],\n ]);\n }\n case 'chevron': {\n const notch = w * 0.25;\n return poly([\n [x, y],\n [right - notch, y],\n [right, cy],\n [right - notch, bottom],\n [x, bottom],\n [x + notch, cy],\n ]);\n }\n case 'arrow-right':\n return blockArrow(x, y, w, h, 'right');\n case 'arrow-left':\n return blockArrow(x, y, w, h, 'left');\n case 'arrow-up':\n return blockArrow(x, y, w, h, 'up');\n case 'arrow-down':\n return blockArrow(x, y, w, h, 'down');\n case 'double-arrow': {\n const head = w * 0.3;\n const bodyT = h * 0.4;\n const ty = cy - bodyT / 2;\n const by = cy + bodyT / 2;\n return poly([\n [x, cy],\n [x + head, y],\n [x + head, ty],\n [right - head, ty],\n [right - head, y],\n [right, cy],\n [right - head, bottom],\n [right - head, by],\n [x + head, by],\n [x + head, bottom],\n ]);\n }\n case 'callout': {\n // Speech bubble: rounded-ish body (square corners for path simplicity)\n // with a tail off the lower-left.\n const bodyH = h * 0.78;\n const bb = y + bodyH;\n const tailX = x + w * 0.25;\n return `M ${r(x)} ${r(y)} L ${r(right)} ${r(y)} L ${r(right)} ${r(bb)} L ${r(tailX + w * 0.12)} ${r(bb)} L ${r(tailX)} ${r(bottom)} L ${r(tailX - w * 0.02)} ${r(bb)} L ${r(x)} ${r(bb)} Z`;\n }\n case 'cylinder': {\n const ell = h * 0.16; // ellipse half-height\n return (\n `M ${r(x)} ${r(y + ell)} ` +\n `A ${r(rx)} ${r(ell)} 0 0 1 ${r(right)} ${r(y + ell)} ` +\n `L ${r(right)} ${r(bottom - ell)} ` +\n `A ${r(rx)} ${r(ell)} 0 0 1 ${r(x)} ${r(bottom - ell)} ` +\n `Z ` +\n // top ellipse outline\n `M ${r(x)} ${r(y + ell)} ` +\n `A ${r(rx)} ${r(ell)} 0 0 0 ${r(right)} ${r(y + ell)}`\n );\n }\n case 'cloud': {\n // Four bumps along the top + a flat-ish bottom, via arcs.\n const b = h * 0.5;\n return (\n `M ${r(x + w * 0.2)} ${r(bottom)} ` +\n `A ${r(w * 0.2)} ${r(b)} 0 0 1 ${r(x + w * 0.15)} ${r(cy)} ` +\n `A ${r(w * 0.2)} ${r(b)} 0 0 1 ${r(x + w * 0.45)} ${r(y + h * 0.2)} ` +\n `A ${r(w * 0.2)} ${r(b)} 0 0 1 ${r(x + w * 0.8)} ${r(y + h * 0.25)} ` +\n `A ${r(w * 0.18)} ${r(b)} 0 0 1 ${r(x + w * 0.85)} ${r(bottom)} ` +\n `Z`\n );\n }\n case 'heart': {\n const topY = y + h * 0.3;\n return (\n `M ${r(cx)} ${r(bottom)} ` +\n `C ${r(x - w * 0.1)} ${r(cy)} ${r(x)} ${r(y)} ${r(cx)} ${r(topY)} ` +\n `C ${r(right)} ${r(y)} ${r(right + w * 0.1)} ${r(cy)} ${r(cx)} ${r(bottom)} ` +\n `Z`\n );\n }\n case 'lightning':\n return poly([\n [x + w * 0.55, y],\n [x + w * 0.2, y + h * 0.55],\n [x + w * 0.45, y + h * 0.55],\n [x + w * 0.3, bottom],\n [right, y + h * 0.4],\n [x + w * 0.55, y + h * 0.4],\n ]);\n default:\n return null;\n }\n}\n\n/** Block arrow (body rectangle + triangular head) pointing in `dir`. */\nfunction blockArrow(\n x: number,\n y: number,\n w: number,\n h: number,\n dir: 'right' | 'left' | 'up' | 'down',\n): string {\n const right = x + w;\n const bottom = y + h;\n const cx = x + w / 2;\n const cy = y + h / 2;\n if (dir === 'right' || dir === 'left') {\n const head = w * 0.4;\n const bodyT = h * 0.5;\n const ty = cy - bodyT / 2;\n const by = cy + bodyT / 2;\n if (dir === 'right') {\n return poly([\n [x, ty],\n [right - head, ty],\n [right - head, y],\n [right, cy],\n [right - head, bottom],\n [right - head, by],\n [x, by],\n ]);\n }\n return poly([\n [right, ty],\n [x + head, ty],\n [x + head, y],\n [x, cy],\n [x + head, bottom],\n [x + head, by],\n [right, by],\n ]);\n }\n const head = h * 0.4;\n const bodyT = w * 0.5;\n const lx = cx - bodyT / 2;\n const rxp = cx + bodyT / 2;\n if (dir === 'down') {\n return poly([\n [lx, y],\n [rxp, y],\n [rxp, bottom - head],\n [right, bottom - head],\n [cx, bottom],\n [x, bottom - head],\n [lx, bottom - head],\n ]);\n }\n return poly([\n [lx, bottom],\n [rxp, bottom],\n [rxp, y + head],\n [right, y + head],\n [cx, y],\n [x, y + head],\n [lx, y + head],\n ]);\n}\n\n// ============================================\n// End markers\n// ============================================\n\n/**\n * The `<marker>` path for a line end-style, in the 0–10 marker viewBox.\n * `dir` selects orientation (`end` points along +x; `start` is mirrored).\n * Returns null for `'none'`. `filled` shapes paint with the stroke color;\n * non-filled (`open`) draw as a stroked outline.\n */\nexport function markerPath(\n style: MarkerStyle,\n dir: 'start' | 'end',\n): { d: string; filled: boolean } | null {\n const mirror = dir === 'start';\n const mx = (n: number) => (mirror ? 10 - n : n);\n const tri = (closed: boolean) => `M ${mx(0)} 0 L ${mx(10)} 5 L ${mx(0)} 10${closed ? ' z' : ''}`;\n switch (style) {\n case 'arrow':\n return { d: tri(true), filled: true };\n case 'open':\n return { d: tri(false), filled: false };\n case 'diamond':\n return { d: 'M 0 5 L 5 0 L 10 5 L 5 10 z', filled: true };\n case 'square':\n return { d: 'M 1 1 L 9 1 L 9 9 L 1 9 z', filled: true };\n case 'circle':\n return { d: 'M 0 5 A 5 5 0 1 0 10 5 A 5 5 0 1 0 0 5 z', filled: true };\n case 'none':\n default:\n return null;\n }\n}\n\n// ============================================\n// Connector routing\n// ============================================\n\nexport interface ClipBox {\n cx: number;\n cy: number;\n rx: number;\n ry: number;\n}\n\nexport type ConnectorPort =\n | 'top'\n | 'right'\n | 'bottom'\n | 'left'\n | 'top-left'\n | 'top-right'\n | 'bottom-right'\n | 'bottom-left';\n\nexport interface ConnectorSnapPoint {\n port: ConnectorPort;\n x: number;\n y: number;\n}\n\n/** Intersection of the center-to-center line with `from`'s bounding box. */\nexport function clipPoint(from: ClipBox, to: ClipBox): { x: number; y: number } {\n const dx = to.cx - from.cx;\n const dy = to.cy - from.cy;\n if (dx === 0 && dy === 0) return { x: from.cx, y: from.cy };\n const sx = Math.abs(dx) > 0 ? from.rx / Math.abs(dx) : Infinity;\n const sy = Math.abs(dy) > 0 ? from.ry / Math.abs(dy) : Infinity;\n const s = Math.min(sx, sy);\n return { x: from.cx + dx * s, y: from.cy + dy * s };\n}\n\n/** Clip both endpoints of an edge to the two shapes it joins. */\nexport function clipEndpoints(\n a: ClipBox,\n b: ClipBox,\n): { start: { x: number; y: number }; end: { x: number; y: number } } {\n return { start: clipPoint(a, b), end: clipPoint(b, a) };\n}\n\n/** The stable connector ports exposed around a shape/card box. */\nexport function snapPoints(box: ClipBox): ConnectorSnapPoint[] {\n const left = box.cx - box.rx;\n const right = box.cx + box.rx;\n const top = box.cy - box.ry;\n const bottom = box.cy + box.ry;\n return [\n { port: 'top', x: box.cx, y: top },\n { port: 'right', x: right, y: box.cy },\n { port: 'bottom', x: box.cx, y: bottom },\n { port: 'left', x: left, y: box.cy },\n { port: 'top-left', x: left, y: top },\n { port: 'top-right', x: right, y: top },\n { port: 'bottom-right', x: right, y: bottom },\n { port: 'bottom-left', x: left, y: bottom },\n ];\n}\n\n/** Nearest stable connector port on `box` to an arbitrary point. */\nexport function nearestSnapPoint(\n box: ClipBox,\n point: { x: number; y: number },\n): ConnectorSnapPoint {\n let best = snapPoints(box)[0];\n let bestScore = Number.POSITIVE_INFINITY;\n for (const candidate of snapPoints(box)) {\n const score = distanceSq(candidate, point) + portTieBreak(candidate.port);\n if (score < bestScore) {\n best = candidate;\n bestScore = score;\n }\n }\n return best;\n}\n\n/**\n * Pick the pair of connector ports, one on each box, with the shortest\n * distance between them. This gives stable attachments to side/corner handles\n * instead of arbitrary center-ray intersections.\n */\nexport function snapEndpoints(\n a: ClipBox,\n b: ClipBox,\n): { start: ConnectorSnapPoint; end: ConnectorSnapPoint } {\n let bestStart = snapPoints(a)[0];\n let bestEnd = snapPoints(b)[0];\n let bestScore = Number.POSITIVE_INFINITY;\n for (const start of snapPoints(a)) {\n for (const end of snapPoints(b)) {\n const score = distanceSq(start, end) + portTieBreak(start.port) + portTieBreak(end.port);\n if (score < bestScore) {\n bestStart = start;\n bestEnd = end;\n bestScore = score;\n }\n }\n }\n return { start: bestStart, end: bestEnd };\n}\n\nfunction distanceSq(a: { x: number; y: number }, b: { x: number; y: number }): number {\n const dx = a.x - b.x;\n const dy = a.y - b.y;\n return dx * dx + dy * dy;\n}\n\nfunction portTieBreak(port: ConnectorPort): number {\n return port.includes('-') ? 0.02 : 0;\n}\n\n/** Build the SVG `d` for a connector between two clipped endpoints. */\nexport function connectorPath(\n routing: ConnectorRouting,\n start: { x: number; y: number },\n end: { x: number; y: number },\n): string {\n if (routing === 'straight') {\n return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;\n }\n if (routing === 'orthogonal') {\n const midX = (start.x + end.x) / 2;\n return `M ${r(start.x)} ${r(start.y)} L ${r(midX)} ${r(start.y)} L ${r(midX)} ${r(end.y)} L ${r(end.x)} ${r(end.y)}`;\n }\n // curved\n const dx = Math.abs(end.x - start.x);\n const cp = Math.max(40, dx / 2);\n return `M ${r(start.x)} ${r(start.y)} C ${r(start.x + cp)} ${r(start.y)}, ${r(end.x - cp)} ${r(end.y)}, ${r(end.x)} ${r(end.y)}`;\n}\n\n/** Map a `lineStyle` (solid|dashed|dotted) to an SVG `stroke-dasharray`. */\nexport function lineStyleDasharray(style: string | undefined): string | undefined {\n switch (style?.toLowerCase()) {\n case 'dashed':\n case 'dash':\n return '8 6';\n case 'dotted':\n case 'dot':\n return '2 6';\n default:\n return undefined;\n }\n}\n"],"mappings":"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","names":["head","bodyT"]}
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  markerPath,
7
7
  shapePath
8
- } from "./chunk-RUDYOTA6.js";
8
+ } from "./chunk-LTN6I7QW.js";
9
9
 
10
10
  // src/imageEdit/state.ts
11
11
  var IMAGE_EDIT_STATE_FILENAME = "state.json";
@@ -590,4 +590,4 @@ export {
590
590
  exportImageEditDoc,
591
591
  buildSvgString
592
592
  };
593
- //# sourceMappingURL=chunk-2OIBZYKP.js.map
593
+ //# sourceMappingURL=chunk-P3K7NLHF.js.map
@@ -1,3 +1,9 @@
1
+ import {
2
+ DEFAULT_THEME,
3
+ applySurface,
4
+ resolveFontFamily
5
+ } from "./chunk-KKNUBQ6Y.js";
6
+
1
7
  // src/jsonForm/chooseControl.ts
2
8
  var ENUM_SEGMENTED_LIMIT = 4;
3
9
  var MULTILINE_LENGTH_THRESHOLD = 200;
@@ -177,6 +183,37 @@ function inferSchema(sample, options = {}) {
177
183
  return mergeSchemas([base, ...extras]);
178
184
  }
179
185
 
186
+ // src/jsonForm/tokens.ts
187
+ function buildJsonFormTokens(theme, surface, options) {
188
+ const { prefix } = options;
189
+ const baseTheme = theme ?? DEFAULT_THEME;
190
+ const finalTheme = surface ? applySurface(baseTheme, surface) : baseTheme;
191
+ const titleFont = resolveFontFamily(finalTheme.typography.titleFont, "system-ui, sans-serif");
192
+ const bodyFont = resolveFontFamily(finalTheme.typography.bodyFont, "system-ui, sans-serif");
193
+ const monoFont = resolveFontFamily(
194
+ finalTheme.typography.monoFont,
195
+ "ui-monospace, Consolas, monospace"
196
+ );
197
+ return {
198
+ [`${prefix}-bg`]: finalTheme.colors.background,
199
+ [`${prefix}-text`]: finalTheme.colors.text,
200
+ [`${prefix}-muted`]: finalTheme.colors.textMuted,
201
+ [`${prefix}-primary`]: finalTheme.colors.primary,
202
+ [`${prefix}-accent`]: finalTheme.colors.secondary,
203
+ [`${prefix}-warning`]: finalTheme.colors.warning,
204
+ [`${prefix}-border`]: `color-mix(in srgb, ${finalTheme.colors.textMuted} 35%, transparent)`,
205
+ [`${prefix}-input-bg`]: finalTheme.colors.backgroundLight,
206
+ [`${prefix}-title-font`]: titleFont,
207
+ [`${prefix}-body-font`]: bodyFont,
208
+ [`${prefix}-mono-font`]: monoFont,
209
+ [`${prefix}-radius`]: `${finalTheme.style.borderRadius ?? 8}px`
210
+ };
211
+ }
212
+ function resolveJsonFormTheme(theme, surface) {
213
+ const baseTheme = theme ?? DEFAULT_THEME;
214
+ return surface ? applySurface(baseTheme, surface) : baseTheme;
215
+ }
216
+
180
217
  export {
181
218
  chooseControl,
182
219
  primaryType,
@@ -188,6 +225,8 @@ export {
188
225
  resolveRef,
189
226
  evaluateWhen,
190
227
  resolveFlag,
191
- inferSchema
228
+ inferSchema,
229
+ buildJsonFormTokens,
230
+ resolveJsonFormTheme
192
231
  };
193
- //# sourceMappingURL=chunk-K32VJONL.js.map
232
+ //# sourceMappingURL=chunk-T5UK6YOB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/jsonForm/chooseControl.ts","../src/jsonForm/pointer.ts","../src/jsonForm/evaluateWhen.ts","../src/jsonForm/inferSchema.ts","../src/jsonForm/tokens.ts"],"sourcesContent":["/**\n * Pick a `ControlKind` for a schema node. The single dispatcher used by\n * both `<JsonView>` and `<JsonEditor>` so view and edit modes always\n * agree on what each field _is_, even when their UIs differ.\n *\n * Order of precedence:\n * 1. Explicit `squisq.control` hint\n * 2. `format` keyword (color/date/time/datetime/markdown/textarea)\n * 3. Enum cardinality (segmented vs. combobox)\n * 4. Numeric range (slider when both min and max present)\n * 5. String length (multiline when maxLength > 200)\n * 6. Type-default (object/array/boolean/string/number)\n */\n\nimport type { ControlKind, SquisqAnnotatedSchema } from './types.js';\n\nconst ENUM_SEGMENTED_LIMIT = 4;\nconst MULTILINE_LENGTH_THRESHOLD = 200;\n\nexport function chooseControl(schema: SquisqAnnotatedSchema): ControlKind {\n const hint = schema.squisq?.control;\n if (hint) return hint;\n\n const type = primaryType(schema);\n\n if (schema.format && type === 'string') {\n switch (schema.format) {\n case 'color':\n return 'color';\n case 'date':\n return 'date';\n case 'time':\n return 'time';\n case 'date-time':\n return 'datetime';\n case 'markdown':\n return 'richtext';\n case 'textarea':\n return 'multiline';\n }\n }\n\n if (schema.enum && schema.enum.length > 0) {\n return schema.enum.length <= ENUM_SEGMENTED_LIMIT ? 'segmented' : 'combobox';\n }\n\n if (schema.oneOf || schema.anyOf) return 'tabs';\n\n switch (type) {\n case 'array':\n return arrayItemKind(schema) === 'object' ? 'card-stack' : 'chip-bin';\n case 'object':\n return 'group';\n case 'boolean':\n return 'toggle';\n case 'integer':\n case 'number':\n return hasFiniteRange(schema) ? 'slider' : 'stepper';\n case 'string':\n if (typeof schema.maxLength === 'number' && schema.maxLength > MULTILINE_LENGTH_THRESHOLD) {\n return 'multiline';\n }\n return 'text';\n default:\n return 'text';\n }\n}\n\n/** Return the first concrete type of a schema, ignoring `null` for nullable shorthand. */\nexport function primaryType(schema: SquisqAnnotatedSchema): string | undefined {\n const t = schema.type;\n if (typeof t === 'string') return t;\n if (Array.isArray(t)) return t.find((x) => x !== 'null');\n return undefined;\n}\n\n/** Inspect `items` to decide whether an array holds objects or primitives. */\nexport function arrayItemKind(schema: SquisqAnnotatedSchema): 'object' | 'primitive' {\n const items = Array.isArray(schema.items) ? schema.items[0] : schema.items;\n if (!items) return 'primitive';\n return primaryType(items) === 'object' ? 'object' : 'primitive';\n}\n\nfunction hasFiniteRange(schema: SquisqAnnotatedSchema): boolean {\n const min = schema.minimum ?? schema.exclusiveMinimum;\n const max = schema.maximum ?? schema.exclusiveMaximum;\n return typeof min === 'number' && typeof max === 'number';\n}\n","/**\n * JSON Pointer helpers (RFC 6901) plus immutable get/set, plus a tiny\n * dotted-path normalizer so hosts can write `user.role` instead of\n * `/user/role`. `$ref` resolution is local-only (`#/$defs/Foo`).\n */\n\nimport type { SquisqAnnotatedSchema } from './types.js';\n\n/** Normalize a path to a JSON Pointer string. Accepts both forms. */\nexport function toPointer(path: string): string {\n if (path === '' || path === '/') return '';\n if (path.startsWith('/')) return path;\n // Dotted form: split on `.` and escape per RFC 6901.\n return (\n '/' +\n path\n .split('.')\n .map((seg) => seg.replace(/~/g, '~0').replace(/\\//g, '~1'))\n .join('/')\n );\n}\n\n/** Split a JSON Pointer into decoded segments. Empty string → []. */\nexport function pointerSegments(pointer: string): string[] {\n if (pointer === '') return [];\n return pointer\n .slice(1)\n .split('/')\n .map((seg) => seg.replace(/~1/g, '/').replace(/~0/g, '~'));\n}\n\n/** Read a value from data by path (dotted or pointer). Returns undefined if missing. */\nexport function getByPointer(data: unknown, path: string): unknown {\n const segments = pointerSegments(toPointer(path));\n let cur: unknown = data;\n for (const seg of segments) {\n if (cur === null || cur === undefined) return undefined;\n if (Array.isArray(cur)) {\n const idx = Number(seg);\n if (!Number.isInteger(idx) || idx < 0 || idx >= cur.length) return undefined;\n cur = cur[idx];\n } else if (typeof cur === 'object') {\n cur = (cur as Record<string, unknown>)[seg];\n } else {\n return undefined;\n }\n }\n return cur;\n}\n\n/**\n * Return a structurally-shared copy of `data` with the value at `path`\n * replaced. Intermediate objects/arrays are cloned along the path; the\n * rest is shared by reference. Creates intermediate objects if missing.\n */\nexport function setByPointer<T>(data: T, path: string, value: unknown): T {\n const segments = pointerSegments(toPointer(path));\n if (segments.length === 0) return value as T;\n return setRec(data, segments, 0, value) as T;\n}\n\nfunction setRec(node: unknown, segments: string[], i: number, value: unknown): unknown {\n const seg = segments[i];\n const isLast = i === segments.length - 1;\n\n // Decide whether the slot we're about to write into is an array index.\n const nextSeg = isLast ? undefined : segments[i + 1];\n const writingArrayChild = nextSeg !== undefined && /^\\d+$/.test(nextSeg);\n\n if (Array.isArray(node)) {\n const idx = Number(seg);\n const next = node.slice();\n next[idx] = isLast\n ? value\n : setRec(node[idx] ?? (writingArrayChild ? [] : {}), segments, i + 1, value);\n return next;\n }\n\n const obj = (node && typeof node === 'object' ? { ...(node as object) } : {}) as Record<\n string,\n unknown\n >;\n obj[seg] = isLast\n ? value\n : setRec(obj[seg] ?? (writingArrayChild ? [] : {}), segments, i + 1, value);\n return obj;\n}\n\n/**\n * Resolve a local `$ref` (e.g. `#/$defs/Foo`) against the root schema.\n * Returns the original schema if no `$ref` is present, or `undefined`\n * if the reference cannot be resolved. Cross-document refs are not\n * supported.\n */\nexport function resolveRef(\n schema: SquisqAnnotatedSchema,\n root: SquisqAnnotatedSchema,\n): SquisqAnnotatedSchema | undefined {\n if (!schema.$ref) return schema;\n const ref = schema.$ref;\n if (!ref.startsWith('#/')) return undefined;\n const segments = pointerSegments(ref.slice(1));\n let cur: unknown = root;\n for (const seg of segments) {\n if (cur === null || cur === undefined || typeof cur !== 'object') return undefined;\n cur = (cur as Record<string, unknown>)[seg];\n }\n return (cur as SquisqAnnotatedSchema | undefined) ?? undefined;\n}\n","/**\n * Evaluate a `SquisqWhen` rule against the current root data. Pure;\n * renderers call this on every render with fresh data.\n */\n\nimport type { SquisqWhen } from './types.js';\nimport { getByPointer } from './pointer.js';\n\nexport function evaluateWhen(when: SquisqWhen, rootData: unknown): boolean {\n const actual = getByPointer(rootData, when.field);\n\n if (when.equals !== undefined) {\n return deepEqual(actual, when.equals);\n }\n if (when.oneOf !== undefined) {\n return when.oneOf.some((v) => deepEqual(actual, v));\n }\n if (when.matches !== undefined) {\n if (typeof actual !== 'string') return false;\n try {\n return new RegExp(when.matches).test(actual);\n } catch {\n return false;\n }\n }\n if (when.truthy !== undefined) {\n return Boolean(actual) === when.truthy;\n }\n // No operator specified — defaults to \"field has any defined value\".\n return actual !== undefined && actual !== null;\n}\n\n/**\n * Resolve a `boolean | SquisqWhen` flag (used by `hidden`/`disabled`)\n * to a final boolean. Literal booleans pass through.\n */\nexport function resolveFlag(flag: boolean | SquisqWhen | undefined, rootData: unknown): boolean {\n if (flag === undefined) return false;\n if (typeof flag === 'boolean') return flag;\n return evaluateWhen(flag, rootData);\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n if (typeof a !== 'object') return false;\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false;\n return a.every((v, i) => deepEqual(v, b[i]));\n }\n if (Array.isArray(b)) return false;\n const ao = a as Record<string, unknown>;\n const bo = b as Record<string, unknown>;\n const ak = Object.keys(ao);\n const bk = Object.keys(bo);\n if (ak.length !== bk.length) return false;\n return ak.every((k) => deepEqual(ao[k], bo[k]));\n}\n","/**\n * Infer a JSON Schema from an example JSON value. Thin facade over\n * `genson-js` so consumers don't depend on it directly and we can swap\n * the implementation later.\n */\n\nimport { createSchema, mergeSchemas } from 'genson-js';\nimport type { SquisqAnnotatedSchema } from './types.js';\n\nexport interface InferSchemaOptions {\n /** Additional samples merged into the inferred schema for broader coverage. */\n additionalSamples?: readonly unknown[];\n}\n\nexport function inferSchema(\n sample: unknown,\n options: InferSchemaOptions = {},\n): SquisqAnnotatedSchema {\n const base = createSchema(sample) as SquisqAnnotatedSchema;\n if (!options.additionalSamples || options.additionalSamples.length === 0) {\n return base;\n }\n const extras = options.additionalSamples.map((s) => createSchema(s) as SquisqAnnotatedSchema);\n return mergeSchemas([base, ...extras] as never[]) as SquisqAnnotatedSchema;\n}\n","/**\n * Pure CSS-custom-property builder shared by `<JsonView>` (read-only, in\n * `@bendyline/squisq-react`) and `<JsonEditor>` (editable, in\n * `@bendyline/squisq-editor-react`).\n *\n * Both components derive an identical token bag from a Theme + resolved\n * Surface; they differ only in the CSS-var prefix (`--squisq-json` vs\n * `--squisq-jsonform`) so the viewer and editor can coexist on one page.\n * This function is the single source of that derivation. It is pure and\n * framework-free: it performs no `window` / `matchMedia` access — the\n * surface must already be resolved to a concrete `SurfaceScheme` (or left\n * `undefined` to skip surface application) by the calling React hook.\n */\n\nimport { applySurface, DEFAULT_THEME, resolveFontFamily } from '../schemas/index.js';\nimport type { SurfaceScheme, Theme } from '../schemas/index.js';\n\nexport interface BuildJsonFormTokensOptions {\n /**\n * CSS-var prefix WITHOUT the trailing dash, e.g. `--squisq-json` or\n * `--squisq-jsonform`. Each token key is `${prefix}-${name}`.\n */\n prefix: string;\n}\n\n/**\n * Build the CSS custom-property map for a JSON form/view surface.\n *\n * @param theme The theme to render with (falls back to `DEFAULT_THEME`).\n * @param surface Already-resolved concrete surface, or `undefined` to leave\n * the theme un-surfaced. (The `'auto'` → concrete resolution\n * is the React hook's job, so this stays pure.)\n * @param options `{ prefix }` — the CSS-var namespace.\n */\nexport function buildJsonFormTokens(\n theme: Theme | undefined,\n surface: SurfaceScheme | undefined,\n options: BuildJsonFormTokensOptions,\n): Record<string, string> {\n const { prefix } = options;\n const baseTheme = theme ?? DEFAULT_THEME;\n const finalTheme = surface ? applySurface(baseTheme, surface) : baseTheme;\n\n const titleFont = resolveFontFamily(finalTheme.typography.titleFont, 'system-ui, sans-serif');\n const bodyFont = resolveFontFamily(finalTheme.typography.bodyFont, 'system-ui, sans-serif');\n const monoFont = resolveFontFamily(\n finalTheme.typography.monoFont,\n 'ui-monospace, Consolas, monospace',\n );\n\n return {\n [`${prefix}-bg`]: finalTheme.colors.background,\n [`${prefix}-text`]: finalTheme.colors.text,\n [`${prefix}-muted`]: finalTheme.colors.textMuted,\n [`${prefix}-primary`]: finalTheme.colors.primary,\n [`${prefix}-accent`]: finalTheme.colors.secondary,\n [`${prefix}-warning`]: finalTheme.colors.warning,\n [`${prefix}-border`]: `color-mix(in srgb, ${finalTheme.colors.textMuted} 35%, transparent)`,\n [`${prefix}-input-bg`]: finalTheme.colors.backgroundLight,\n [`${prefix}-title-font`]: titleFont,\n [`${prefix}-body-font`]: bodyFont,\n [`${prefix}-mono-font`]: monoFont,\n [`${prefix}-radius`]: `${finalTheme.style.borderRadius ?? 8}px`,\n };\n}\n\n/**\n * Resolve the effective theme after surface application — the companion to\n * `buildJsonFormTokens` for callers that also need the `Theme` object (both\n * hooks return it alongside the style bag).\n */\nexport function resolveJsonFormTheme(\n theme: Theme | undefined,\n surface: SurfaceScheme | undefined,\n): Theme {\n const baseTheme = theme ?? DEFAULT_THEME;\n return surface ? applySurface(baseTheme, surface) : baseTheme;\n}\n"],"mappings":"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","names":[]}
@@ -18,11 +18,52 @@ var QUOTE_PATTERNS = [
18
18
  /\u201c([^\u201d]{15,200})\u201d/g,
19
19
  /(\w+(?:\s+\w+)?)\s+(?:said|stated|wrote|noted|observed|commented),?\s*"([^"]+)"/gi
20
20
  ];
21
+ var COMPARISON_SIDE = "[^.!?;\\r\\n]{2,30}";
21
22
  var COMPARISON_PATTERNS = [
22
- /from\s+(.{2,30})\s+to\s+(.{2,30})/gi,
23
- /(.{2,30})\s+(?:vs\.?|versus)\s+(.{2,30})/gi,
24
- /(.{2,30})\s+compared\s+to\s+(.{2,30})/gi
23
+ new RegExp(`from\\s+(${COMPARISON_SIDE})\\s+to\\s+(${COMPARISON_SIDE})`, "gi"),
24
+ new RegExp(`(${COMPARISON_SIDE})\\s+(?:vs\\.?|versus)\\s+(${COMPARISON_SIDE})`, "gi"),
25
+ new RegExp(`(${COMPARISON_SIDE})\\s+compared\\s+to\\s+(${COMPARISON_SIDE})`, "gi")
25
26
  ];
27
+ var SENTENCE_ABBREVIATIONS = /* @__PURE__ */ new Set([
28
+ "mr",
29
+ "mrs",
30
+ "ms",
31
+ "dr",
32
+ "prof",
33
+ "st",
34
+ "mt",
35
+ "jr",
36
+ "sr",
37
+ "gen",
38
+ "col",
39
+ "capt",
40
+ "lt",
41
+ "sgt",
42
+ "rev",
43
+ "hon",
44
+ "sen",
45
+ "rep",
46
+ "gov",
47
+ "pres",
48
+ "vs",
49
+ "etc",
50
+ "inc",
51
+ "co",
52
+ "ltd",
53
+ "corp",
54
+ "al",
55
+ "fig",
56
+ "eg",
57
+ "ie",
58
+ "approx",
59
+ "dept",
60
+ "univ",
61
+ "assn",
62
+ "bros",
63
+ "ave",
64
+ "blvd",
65
+ "rd"
66
+ ]);
26
67
  var FACT_INDICATORS = [
27
68
  "largest",
28
69
  "smallest",
@@ -261,10 +302,17 @@ function extractComparisons(text) {
261
302
  const position = match.index;
262
303
  if (seen.has(position)) continue;
263
304
  seen.add(position);
264
- const left = match[1].trim();
265
- const right = match[2].trim();
305
+ const trimClause = (s) => s.replace(/,\s+.*$/, "").trim();
306
+ const left = trimClause(match[1].trim());
307
+ const right = trimClause(match[2].trim());
266
308
  if (left.length < 2 || left.length > 30) continue;
267
309
  if (right.length < 2 || right.length > 30) continue;
310
+ const wordsOf = (s) => s.toLowerCase().split(/\s+/).map((w) => w.replace(/[^a-z0-9]+/g, "")).filter(Boolean);
311
+ const leftWords = wordsOf(left);
312
+ const rightWords = wordsOf(right);
313
+ if (leftWords.length > 0 && rightWords.length > 0 && leftWords[leftWords.length - 1] === rightWords[0]) {
314
+ continue;
315
+ }
268
316
  const context = getSentenceContext(text, position, match[0].length);
269
317
  elements.push({
270
318
  type: "comparison",
@@ -278,28 +326,46 @@ function extractComparisons(text) {
278
326
  }
279
327
  return elements;
280
328
  }
329
+ function isSentenceBoundary(text, index) {
330
+ const ch = text[index];
331
+ if (ch === "!" || ch === "?") return true;
332
+ if (ch !== ".") return false;
333
+ if (text[index + 1] === ".") return false;
334
+ if (/[0-9]/.test(text[index - 1] ?? "") && /[0-9]/.test(text[index + 1] ?? "")) return false;
335
+ let w = index - 1;
336
+ while (w >= 0 && /[A-Za-z]/.test(text[w])) w--;
337
+ const word = text.slice(w + 1, index);
338
+ if (word.length === 1) return false;
339
+ if (word && SENTENCE_ABBREVIATIONS.has(word.toLowerCase())) return false;
340
+ let j = index + 1;
341
+ while (j < text.length && /\s/.test(text[j])) j++;
342
+ if (j >= text.length) return true;
343
+ return !/[a-z]/.test(text[j]);
344
+ }
281
345
  function forEachSentenceSpan(text, callback) {
282
- const sentenceRegex = /[^.!?]+[.!?]*/g;
283
- let match;
284
- while ((match = sentenceRegex.exec(text)) !== null) {
285
- const raw = match[0];
286
- const rawStart = match.index;
287
- const rawEnd = rawStart + raw.length;
288
- let leading = 0;
289
- while (leading < raw.length && /\s/.test(raw[leading])) {
290
- leading++;
291
- }
292
- let trailing = 0;
293
- while (trailing < raw.length - leading && /[\s.!?]/.test(raw[raw.length - 1 - trailing])) {
294
- trailing++;
295
- }
296
- const start = rawStart + leading;
297
- const end = rawEnd - trailing;
298
- if (end <= start) continue;
346
+ const emit = (rawStart, rawEnd) => {
347
+ let start = rawStart;
348
+ while (start < rawEnd && /\s/.test(text[start])) start++;
349
+ let end = rawEnd;
350
+ while (end > start && /[\s.!?]/.test(text[end - 1])) end--;
351
+ if (end <= start) return;
299
352
  const trimmed = text.slice(start, end);
300
- if (!trimmed) continue;
301
- callback(trimmed, start, end);
353
+ if (trimmed) callback(trimmed, start, end);
354
+ };
355
+ let spanStart = 0;
356
+ let i = 0;
357
+ while (i < text.length) {
358
+ if (isSentenceBoundary(text, i)) {
359
+ let end = i + 1;
360
+ while (end < text.length && /[.!?]/.test(text[end])) end++;
361
+ emit(spanStart, end);
362
+ spanStart = end;
363
+ i = end;
364
+ } else {
365
+ i++;
366
+ }
302
367
  }
368
+ if (spanStart < text.length) emit(spanStart, text.length);
303
369
  }
304
370
  function extractFacts(text) {
305
371
  const elements = [];
@@ -401,10 +467,10 @@ function extractDefinitions(text) {
401
467
  }
402
468
  function getSentenceContext(text, matchStart, matchLength) {
403
469
  let start = matchStart;
404
- while (start > 0 && !/[.!?]/.test(text[start - 1])) start--;
470
+ while (start > 0 && !isSentenceBoundary(text, start - 1)) start--;
405
471
  while (start < matchStart && /\s/.test(text[start])) start++;
406
472
  let end = matchStart + matchLength;
407
- while (end < text.length && !/[.!?]/.test(text[end])) end++;
473
+ while (end < text.length && !isSentenceBoundary(text, end)) end++;
408
474
  if (end < text.length) end++;
409
475
  return { sentence: text.slice(start, end).trim(), start, end };
410
476
  }
@@ -652,4 +718,4 @@ export {
652
718
  stripMarkdown,
653
719
  mapElementToBlock
654
720
  };
655
- //# sourceMappingURL=chunk-FR2RBTKO.js.map
721
+ //# sourceMappingURL=chunk-TQWLI6S2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generate/contentExtractor.ts","../src/generate/templateMapper.ts"],"sourcesContent":["/**\n * Content Extractor\n *\n * Pattern-based extraction of compelling content from plain text.\n * Identifies statistics, dates, quotes, comparisons, facts, impact lines,\n * lists, and definitions that can be turned into slideshow slides.\n *\n * Extraction is purely regex-based — no AI calls, no runtime costs,\n * deterministic and reproducible.\n */\n\n// ── Types ──────────────────────────────────────────────────────────\n\n/** Kinds of content the extractor recognises. */\nexport type ExtractionType =\n | 'stat'\n | 'date'\n | 'quote'\n | 'comparison'\n | 'fact'\n | 'impactLine'\n | 'list'\n | 'definition';\n\n/** An extracted content element with source position. */\nexport interface ExtractedElement {\n type: ExtractionType;\n /** Full extracted text (usually the surrounding sentence). */\n text: string;\n /** Confidence score 0–1. */\n confidence: number;\n /** Character offset in the source text. */\n sourcePosition: number;\n /** Character offset where element ends. */\n endPosition: number;\n /** Type-specific structured payload. */\n data:\n | StatData\n | DateData\n | QuoteData\n | ComparisonData\n | FactData\n | ImpactLineData\n | ListData\n | DefinitionData;\n}\n\nexport interface StatData {\n type: 'stat';\n value: string;\n unit?: string;\n description: string;\n}\n\nexport interface DateData {\n type: 'date';\n date: string;\n description: string;\n mood?: 'neutral' | 'somber' | 'celebratory';\n}\n\nexport interface QuoteData {\n type: 'quote';\n quote: string;\n attribution?: string;\n}\n\nexport interface ComparisonData {\n type: 'comparison';\n left: { label: string; sublabel?: string };\n right: { label: string; sublabel?: string };\n header?: string;\n}\n\nexport interface FactData {\n type: 'fact';\n fact: string;\n explanation?: string;\n}\n\nexport interface ImpactLineData {\n type: 'impactLine';\n text: string;\n}\n\nexport interface ListData {\n type: 'list';\n items: string[];\n title?: string;\n}\n\nexport interface DefinitionData {\n type: 'definition';\n term: string;\n definition: string;\n origin?: string;\n}\n\n/** Options for `extractContent`. */\nexport interface ExtractionOptions {\n /** Minimum confidence threshold (default: 0.3). */\n minConfidence?: number;\n /** Which types to extract (default: all). */\n types?: ExtractionType[];\n /** Maximum extractions per type (default: unlimited). */\n maxPerType?: number;\n}\n\n/** Result of `extractContent`. */\nexport interface ExtractionResult {\n elements: ExtractedElement[];\n sourceLength: number;\n stats: {\n statCount: number;\n dateCount: number;\n quoteCount: number;\n comparisonCount: number;\n factCount: number;\n impactLineCount: number;\n listCount: number;\n definitionCount: number;\n totalCount: number;\n };\n}\n\n// ── Patterns ───────────────────────────────────────────────────────\n\nconst STAT_PATTERNS = [\n /\\b(roughly |about |approximately |nearly |over |under |more than |less than )?([\\d,]+(?:\\.\\d+)?)\\s*%/gi,\n /\\b([\\d,]+(?:\\.\\d+)?)\\s*(million|billion|trillion|thousand|hundred)\\b/gi,\n /\\b([\\d,]+(?:\\.\\d+)?)\\s*(miles?|feet|ft|meters?|m|kilometers?|km|acres?|square miles?|sq mi|hectares?)\\b/gi,\n /\\b([\\d,]+(?:\\.\\d+)?)\\s*(x|times)\\b/gi,\n /\\b([$€£¥])\\s*([\\d,]+(?:\\.\\d+)?)\\s*(million|billion|thousand)?\\b/gi,\n /\\b(\\d+)(st|nd|rd|th)\\s+(largest|smallest|oldest|newest|most|least)/gi,\n];\n\nconst DATE_PATTERNS = [\n /\\b(January|February|March|April|May|June|July|August|September|October|November|December)\\s+(\\d{1,2},?\\s+)?(\\d{4})\\b/gi,\n /\\b(\\d{4})s\\b|\\b(\\d{1,2})(st|nd|rd|th)\\s+century\\b/gi,\n /\\b(from\\s+)?(\\d{4})\\s*(to|–|-|through)\\s*(\\d{4})\\b/gi,\n /\\b(in|by|since|after|before|during)\\s+(\\d{4})\\b/gi,\n];\n\nconst QUOTE_PATTERNS = [\n /\"([^\"]{15,200})\"/g,\n /\\u201c([^\\u201d]{15,200})\\u201d/g,\n /(\\w+(?:\\s+\\w+)?)\\s+(?:said|stated|wrote|noted|observed|commented),?\\s*\"([^\"]+)\"/gi,\n];\n\n// A single side of a comparison (\"X\" in \"from X to Y\"). Deliberately excludes\n// sentence/clause terminators (. ! ? ;) and newlines so a capture group can\n// never swallow text from an adjacent sentence. Without this, greedy `.{2,30}`\n// matches across a period — e.g. \"…contradicted from witness to witness.\n// Robertson said…\" yields right=\"witness. Robertson\", producing a nonsensical\n// \"witness → witness. Robertson\" slide. Commas and colons are allowed (ranges\n// like \"9:00 to 17:00\", ratios); downstream label truncation trims commas.\nconst COMPARISON_SIDE = '[^.!?;\\\\r\\\\n]{2,30}';\n\nconst COMPARISON_PATTERNS = [\n new RegExp(`from\\\\s+(${COMPARISON_SIDE})\\\\s+to\\\\s+(${COMPARISON_SIDE})`, 'gi'),\n new RegExp(`(${COMPARISON_SIDE})\\\\s+(?:vs\\\\.?|versus)\\\\s+(${COMPARISON_SIDE})`, 'gi'),\n new RegExp(`(${COMPARISON_SIDE})\\\\s+compared\\\\s+to\\\\s+(${COMPARISON_SIDE})`, 'gi'),\n];\n\n// Words whose trailing period is an abbreviation, not a sentence end. Used by\n// isSentenceBoundary() so names like \"George W. Tibbetts\" or \"Quong Chong & Co.\"\n// don't get split into fragments. Matched only when the entire preceding word\n// equals one of these (case-insensitive), so \"canal.\" / \"taco.\" are unaffected.\nconst SENTENCE_ABBREVIATIONS = new Set([\n 'mr',\n 'mrs',\n 'ms',\n 'dr',\n 'prof',\n 'st',\n 'mt',\n 'jr',\n 'sr',\n 'gen',\n 'col',\n 'capt',\n 'lt',\n 'sgt',\n 'rev',\n 'hon',\n 'sen',\n 'rep',\n 'gov',\n 'pres',\n 'vs',\n 'etc',\n 'inc',\n 'co',\n 'ltd',\n 'corp',\n 'al',\n 'fig',\n 'eg',\n 'ie',\n 'approx',\n 'dept',\n 'univ',\n 'assn',\n 'bros',\n 'ave',\n 'blvd',\n 'rd',\n]);\n\nconst FACT_INDICATORS = [\n 'largest',\n 'smallest',\n 'oldest',\n 'newest',\n 'first',\n 'last',\n 'most',\n 'least',\n 'only',\n 'unique',\n 'rare',\n 'famous',\n 'important',\n 'significant',\n 'notable',\n 'remarkable',\n 'extraordinary',\n 'crucial',\n 'essential',\n 'critical',\n 'major',\n 'key',\n 'discovered',\n 'founded',\n 'established',\n 'created',\n 'built',\n 'invented',\n 'developed',\n 'introduced',\n 'pioneered',\n 'transformed',\n 'revolutionized',\n 'changed',\n 'shaped',\n];\n\nconst SOMBER_INDICATORS = [\n 'disaster',\n 'tragedy',\n 'death',\n 'died',\n 'killed',\n 'destroyed',\n 'devastated',\n 'collapsed',\n 'lost',\n 'abandoned',\n 'declined',\n 'war',\n 'battle',\n 'conflict',\n 'earthquake',\n 'fire',\n 'flood',\n];\n\nconst CELEBRATORY_INDICATORS = [\n 'celebration',\n 'victory',\n 'success',\n 'achievement',\n 'triumph',\n 'opened',\n 'completed',\n 'inaugurated',\n 'launched',\n 'won',\n 'record',\n 'breakthrough',\n 'milestone',\n 'golden',\n 'grand',\n];\n\nconst IMPACT_VERBS = [\n 'check',\n 'watch',\n 'stop',\n 'prepare',\n 'imagine',\n 'consider',\n 'look',\n 'listen',\n 'notice',\n 'remember',\n 'discover',\n 'explore',\n 'visit',\n 'try',\n 'experience',\n 'avoid',\n 'beware',\n 'enjoy',\n];\n\nconst LIST_PATTERNS = [\n /(?:including|such as|namely|like|features?|offers?)\\s+([^.]{10,150})/gi,\n /(?:three|four|five|six|several|many|various)\\s+(?:main|key|primary|notable|popular|important)?\\s*\\w+(?:\\s+\\w+)?:\\s*([^.]{10,150})/gi,\n];\n\nconst DEFINITION_PATTERNS = [\n /\\b([A-Z][a-z]+(?:\\s+[A-Z]?[a-z]+){0,3})\\s+is\\s+(?:a|an|the)\\s+([^.]{10,120})/g,\n /known\\s+as\\s+([A-Z][a-z]+(?:\\s+[A-Z]?[a-z]+){0,3}),\\s+([^.]{10,120})/gi,\n /\\b([A-Z][a-z]+(?:\\s+[A-Z]?[a-z]+){0,3})\\s+refers?\\s+to\\s+([^.]{10,120})/g,\n /called\\s+([A-Z][a-z]+(?:\\s+[A-Z]?[a-z]+){0,3}),?\\s+(?:which|meaning|a)\\s+([^.]{10,120})/gi,\n];\n\n// ── Main ───────────────────────────────────────────────────────────\n\n/**\n * Extract compelling content from plain text.\n *\n * Call `stripMarkdown(text)` first if the input contains markdown.\n */\nexport function extractContent(text: string, options: ExtractionOptions = {}): ExtractionResult {\n const {\n minConfidence = 0.3,\n types = ['stat', 'date', 'quote', 'comparison', 'fact', 'impactLine', 'list', 'definition'],\n maxPerType,\n } = options;\n\n const elements: ExtractedElement[] = [];\n\n if (types.includes('stat')) elements.push(...extractStats(text));\n if (types.includes('date')) elements.push(...extractDates(text));\n if (types.includes('quote')) elements.push(...extractQuotes(text));\n if (types.includes('comparison')) elements.push(...extractComparisons(text));\n if (types.includes('fact')) elements.push(...extractFacts(text));\n if (types.includes('impactLine')) elements.push(...extractImpactLines(text));\n if (types.includes('list')) elements.push(...extractLists(text));\n if (types.includes('definition')) elements.push(...extractDefinitions(text));\n\n let filtered = elements.filter((e) => e.confidence >= minConfidence);\n\n if (maxPerType !== undefined) {\n const byType = new Map<ExtractionType, ExtractedElement[]>();\n for (const elem of filtered) {\n const list = byType.get(elem.type) || [];\n list.push(elem);\n byType.set(elem.type, list);\n }\n filtered = [];\n for (const [, list] of byType) {\n list.sort((a, b) => b.confidence - a.confidence);\n filtered.push(...list.slice(0, maxPerType));\n }\n }\n\n filtered.sort((a, b) => a.sourcePosition - b.sourcePosition);\n filtered = deduplicateOverlapping(filtered);\n\n return {\n elements: filtered,\n sourceLength: text.length,\n stats: {\n statCount: filtered.filter((e) => e.type === 'stat').length,\n dateCount: filtered.filter((e) => e.type === 'date').length,\n quoteCount: filtered.filter((e) => e.type === 'quote').length,\n comparisonCount: filtered.filter((e) => e.type === 'comparison').length,\n factCount: filtered.filter((e) => e.type === 'fact').length,\n impactLineCount: filtered.filter((e) => e.type === 'impactLine').length,\n listCount: filtered.filter((e) => e.type === 'list').length,\n definitionCount: filtered.filter((e) => e.type === 'definition').length,\n totalCount: filtered.length,\n },\n };\n}\n\n// ── Per-type extractors ────────────────────────────────────────────\n\nfunction extractStats(text: string): ExtractedElement[] {\n const elements: ExtractedElement[] = [];\n const seen = new Set<number>();\n\n for (const pattern of STAT_PATTERNS) {\n pattern.lastIndex = 0;\n let match;\n while ((match = pattern.exec(text)) !== null) {\n const position = match.index;\n if (seen.has(position)) continue;\n seen.add(position);\n\n const context = getSentenceContext(text, position, match[0].length);\n const value = match[0].trim();\n\n elements.push({\n type: 'stat',\n text: context.sentence,\n confidence: calculateStatConfidence(value, context.sentence),\n sourcePosition: context.start,\n endPosition: context.end,\n data: { type: 'stat', value, description: cleanDescription(context.sentence, value) },\n });\n }\n }\n return elements;\n}\n\nfunction extractDates(text: string): ExtractedElement[] {\n const elements: ExtractedElement[] = [];\n const seen = new Set<number>();\n\n for (const pattern of DATE_PATTERNS) {\n pattern.lastIndex = 0;\n let match;\n while ((match = pattern.exec(text)) !== null) {\n const position = match.index;\n if (seen.has(position)) continue;\n seen.add(position);\n\n const context = getSentenceContext(text, position, match[0].length);\n const dateStr = match[0].trim();\n\n elements.push({\n type: 'date',\n text: context.sentence,\n confidence: calculateDateConfidence(dateStr, context.sentence),\n sourcePosition: context.start,\n endPosition: context.end,\n data: {\n type: 'date',\n date: dateStr,\n description: cleanDescription(context.sentence, dateStr),\n mood: detectMood(context.sentence),\n },\n });\n }\n }\n return elements;\n}\n\nfunction extractQuotes(text: string): ExtractedElement[] {\n const elements: ExtractedElement[] = [];\n const seen = new Set<number>();\n\n for (const pattern of QUOTE_PATTERNS) {\n pattern.lastIndex = 0;\n let match;\n while ((match = pattern.exec(text)) !== null) {\n const position = match.index;\n if (seen.has(position)) continue;\n seen.add(position);\n\n const quote = match.length > 2 ? match[2] : match[1];\n const attribution = match.length > 2 ? match[1] : undefined;\n if (quote.length < 15 || quote.length > 200) continue;\n\n elements.push({\n type: 'quote',\n text: match[0],\n confidence: calculateQuoteConfidence(quote),\n sourcePosition: position,\n endPosition: position + match[0].length,\n data: { type: 'quote', quote: quote.trim(), attribution: attribution?.trim() },\n });\n }\n }\n return elements;\n}\n\nfunction extractComparisons(text: string): ExtractedElement[] {\n const elements: ExtractedElement[] = [];\n const seen = new Set<number>();\n\n for (const pattern of COMPARISON_PATTERNS) {\n pattern.lastIndex = 0;\n let match;\n while ((match = pattern.exec(text)) !== null) {\n const position = match.index;\n if (seen.has(position)) continue;\n seen.add(position);\n\n // Trim a trailing clause introduced by a comma + space (\"retired\n // airliners, arranged in rows\" → \"retired airliners\"). A greedy side\n // match often swallows the words after the compared thing; the comma is a\n // clause boundary, not part of the value. Numeric grouping commas\n // (\"4,000\") survive because those are comma+digit, not comma+space.\n const trimClause = (s: string) => s.replace(/,\\s+.*$/, '').trim();\n const left = trimClause(match[1].trim());\n const right = trimClause(match[2].trim());\n if (left.length < 2 || left.length > 30) continue;\n if (right.length < 2 || right.length > 30) continue;\n // Reject \"from X to X\"-style idioms — \"from witness to witness\",\n // \"from day to day\", \"from generation to generation\". These mean \"it\n // varied\", not \"A versus B\", and render as a pointless \"X → X\" card.\n // The signature is a word repeated across the connector: the last word\n // of the left side equals the first word of the right side. (Greedy\n // matching may append trailing context to the right side, so a whole-\n // string equality check is not enough.)\n const wordsOf = (s: string) =>\n s\n .toLowerCase()\n .split(/\\s+/)\n .map((w) => w.replace(/[^a-z0-9]+/g, ''))\n .filter(Boolean);\n const leftWords = wordsOf(left);\n const rightWords = wordsOf(right);\n if (\n leftWords.length > 0 &&\n rightWords.length > 0 &&\n leftWords[leftWords.length - 1] === rightWords[0]\n ) {\n continue;\n }\n\n const context = getSentenceContext(text, position, match[0].length);\n\n elements.push({\n type: 'comparison',\n text: context.sentence,\n confidence: 0.6,\n sourcePosition: context.start,\n endPosition: context.end,\n data: { type: 'comparison', left: { label: left }, right: { label: right } },\n });\n }\n }\n return elements;\n}\n\n/**\n * Whether the `. ! ?` at `index` is a real sentence terminator rather than a\n * period inside an abbreviation (\"George W. Tibbetts\", \"M. DeWitt\", \"Mr.\",\n * \"U.S.\"), a single-letter initial, a decimal (\"3.5\"), or an ellipsis. Naive\n * splitting on every period produces broken fragments — e.g. the standalone\n * slide \"They were met at George W\" from \"…at George W. Tibbetts' store…\".\n * Returns false for any non-terminator character so callers can scan freely.\n */\nfunction isSentenceBoundary(text: string, index: number): boolean {\n const ch = text[index];\n if (ch === '!' || ch === '?') return true;\n if (ch !== '.') return false;\n\n // Ellipsis: defer the boundary to the final dot in a run of dots.\n if (text[index + 1] === '.') return false;\n // Decimal number: digit \".\" digit → \"3.5\".\n if (/[0-9]/.test(text[index - 1] ?? '') && /[0-9]/.test(text[index + 1] ?? '')) return false;\n\n // The run of letters immediately preceding the period.\n let w = index - 1;\n while (w >= 0 && /[A-Za-z]/.test(text[w])) w--;\n const word = text.slice(w + 1, index);\n if (word.length === 1) return false; // single-letter initial: \"George W.\"\n if (word && SENTENCE_ABBREVIATIONS.has(word.toLowerCase())) return false;\n\n // A genuine sentence starts with an uppercase letter, digit, or opening\n // quote/bracket. A lowercase continuation means the period was mid-sentence\n // (an abbreviation we didn't enumerate).\n let j = index + 1;\n while (j < text.length && /\\s/.test(text[j])) j++;\n if (j >= text.length) return true; // end of text\n return !/[a-z]/.test(text[j]);\n}\n\n/**\n * Iterate over sentence-like spans in the text, yielding trimmed text and\n * accurate start/end indices within the original string.\n *\n * Boundaries are detected by isSentenceBoundary() so abbreviations, initials,\n * decimals, and ellipses do not split a sentence. Leading whitespace and\n * trailing whitespace/terminators are excluded from each span.\n */\nfunction forEachSentenceSpan(\n text: string,\n callback: (trimmed: string, start: number, end: number) => void,\n): void {\n const emit = (rawStart: number, rawEnd: number) => {\n let start = rawStart;\n while (start < rawEnd && /\\s/.test(text[start])) start++;\n let end = rawEnd;\n while (end > start && /[\\s.!?]/.test(text[end - 1])) end--;\n if (end <= start) return;\n const trimmed = text.slice(start, end);\n if (trimmed) callback(trimmed, start, end);\n };\n\n let spanStart = 0;\n let i = 0;\n while (i < text.length) {\n if (isSentenceBoundary(text, i)) {\n // Consume any consecutive terminators (\"?!\", \"…\") as one boundary.\n let end = i + 1;\n while (end < text.length && /[.!?]/.test(text[end])) end++;\n emit(spanStart, end);\n spanStart = end;\n i = end;\n } else {\n i++;\n }\n }\n if (spanStart < text.length) emit(spanStart, text.length);\n}\n\nfunction extractFacts(text: string): ExtractedElement[] {\n const elements: ExtractedElement[] = [];\n\n forEachSentenceSpan(text, (trimmed, start, end) => {\n if (trimmed.length <= 20) return;\n\n const score = scoreSentenceAsFact(trimmed);\n if (score < 0.4) return;\n\n elements.push({\n type: 'fact',\n text: trimmed,\n confidence: score,\n sourcePosition: start,\n endPosition: end,\n data: { type: 'fact', fact: trimmed },\n });\n });\n\n return elements;\n}\n\nfunction extractImpactLines(text: string): ExtractedElement[] {\n const elements: ExtractedElement[] = [];\n\n forEachSentenceSpan(text, (trimmed, start, end) => {\n if (trimmed.length < 8 || trimmed.length > 60) return;\n\n let score = 0.3;\n const lower = trimmed.toLowerCase();\n\n for (const verb of IMPACT_VERBS) {\n if (lower.startsWith(verb)) {\n score += 0.25;\n break;\n }\n }\n\n if (end < text.length && text[end] === '!') score += 0.15;\n if (trimmed.length < 30) score += 0.1;\n if (trimmed.includes(',')) score -= 0.1;\n if (/^(and|but|or|the|a|an)\\b/i.test(trimmed)) score -= 0.15;\n if (score < 0.5) return;\n\n elements.push({\n type: 'impactLine',\n text: trimmed,\n confidence: Math.min(1, score),\n sourcePosition: start,\n endPosition: end,\n data: { type: 'impactLine', text: trimmed },\n });\n });\n\n return elements;\n}\n\nfunction extractLists(text: string): ExtractedElement[] {\n const elements: ExtractedElement[] = [];\n const seen = new Set<number>();\n\n for (const pattern of LIST_PATTERNS) {\n pattern.lastIndex = 0;\n let match;\n while ((match = pattern.exec(text)) !== null) {\n const position = match.index;\n if (seen.has(position)) continue;\n seen.add(position);\n\n const listContent = match[1].trim();\n const items = listContent\n .split(/,\\s*(?:and|or)\\s+|,\\s+|\\s+and\\s+|\\s+or\\s+/)\n .map((item) => item.trim())\n .filter((item) => item.length >= 2 && item.length <= 60);\n\n if (items.length < 3 || items.length > 5) continue;\n\n const context = getSentenceContext(text, position, match[0].length);\n\n elements.push({\n type: 'list',\n text: context.sentence,\n confidence: 0.65,\n sourcePosition: context.start,\n endPosition: context.end,\n data: { type: 'list', items },\n });\n }\n }\n return elements;\n}\n\nfunction extractDefinitions(text: string): ExtractedElement[] {\n const elements: ExtractedElement[] = [];\n const seen = new Set<number>();\n\n for (const pattern of DEFINITION_PATTERNS) {\n pattern.lastIndex = 0;\n let match;\n while ((match = pattern.exec(text)) !== null) {\n const position = match.index;\n if (seen.has(position)) continue;\n seen.add(position);\n\n const term = match[1].trim();\n const definition = match[2].trim();\n if (term.length < 3 || term.length > 30) continue;\n if (definition.length < 10 || definition.length > 120) continue;\n\n const context = getSentenceContext(text, position, match[0].length);\n\n elements.push({\n type: 'definition',\n text: context.sentence,\n confidence: 0.6,\n sourcePosition: context.start,\n endPosition: context.end,\n data: { type: 'definition', term, definition },\n });\n }\n }\n return elements;\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\nfunction getSentenceContext(\n text: string,\n matchStart: number,\n matchLength: number,\n): { sentence: string; start: number; end: number } {\n let start = matchStart;\n while (start > 0 && !isSentenceBoundary(text, start - 1)) start--;\n while (start < matchStart && /\\s/.test(text[start])) start++;\n\n let end = matchStart + matchLength;\n while (end < text.length && !isSentenceBoundary(text, end)) end++;\n if (end < text.length) end++;\n\n return { sentence: text.slice(start, end).trim(), start, end };\n}\n\nfunction calculateStatConfidence(value: string, context: string): number {\n let score = 0.5;\n if (value.includes('%')) score += 0.2;\n if (/million|billion|trillion/i.test(value)) score += 0.15;\n if (/increased|decreased|grew|dropped|rose|fell/i.test(context)) score += 0.15;\n if (context.length < 30) score -= 0.2;\n return Math.min(1, Math.max(0, score));\n}\n\nfunction calculateDateConfidence(date: string, context: string): number {\n let score = 0.5;\n if (\n /January|February|March|April|May|June|July|August|September|October|November|December/i.test(\n date,\n )\n )\n score += 0.2;\n if (/founded|established|built|opened|discovered|created/i.test(context)) score += 0.2;\n if (/happened|occurred|took place|began|ended/i.test(context)) score += 0.15;\n return Math.min(1, Math.max(0, score));\n}\n\nfunction calculateQuoteConfidence(quote: string): number {\n let score = 0.5;\n if (quote.length >= 30 && quote.length <= 150) score += 0.2;\n if (/\\b(I|we|our|my)\\b/i.test(quote)) score += 0.1;\n if (!/[.!?]$/.test(quote)) score -= 0.1;\n return Math.min(1, Math.max(0, score));\n}\n\nfunction scoreSentenceAsFact(sentence: string): number {\n let score = 0.3;\n const lower = sentence.toLowerCase();\n for (const indicator of FACT_INDICATORS) {\n if (lower.includes(indicator)) {\n score += 0.1;\n break;\n }\n }\n const properNouns = sentence.match(/\\b[A-Z][a-z]+\\b/g);\n if (properNouns && properNouns.length >= 2) score += 0.15;\n if (/\\d/.test(sentence)) score += 0.1;\n if (sentence.endsWith('?')) score -= 0.3;\n if (sentence.length > 200) score -= 0.15;\n return Math.min(1, Math.max(0, score));\n}\n\nfunction detectMood(sentence: string): 'neutral' | 'somber' | 'celebratory' {\n const lower = sentence.toLowerCase();\n for (const word of SOMBER_INDICATORS) {\n if (lower.includes(word)) return 'somber';\n }\n for (const word of CELEBRATORY_INDICATORS) {\n if (lower.includes(word)) return 'celebratory';\n }\n return 'neutral';\n}\n\nfunction cleanDescription(sentence: string, value: string): string {\n const prepositionPatterns = [\n new RegExp(`\\\\b[Ii]n\\\\s+${escapeRegex(value)}[,.]?\\\\s*`, 'g'),\n new RegExp(`\\\\b[Oo]n\\\\s+${escapeRegex(value)}[,.]?\\\\s*`, 'g'),\n new RegExp(`\\\\b[Dd]uring\\\\s+${escapeRegex(value)}[,.]?\\\\s*`, 'g'),\n new RegExp(`\\\\b[Bb]y\\\\s+${escapeRegex(value)}[,.]?\\\\s*`, 'g'),\n new RegExp(`\\\\b[Ss]ince\\\\s+${escapeRegex(value)}[,.]?\\\\s*`, 'g'),\n new RegExp(`\\\\b[Ff]rom\\\\s+${escapeRegex(value)}[,.]?\\\\s*`, 'g'),\n ];\n\n let desc = sentence;\n for (const pattern of prepositionPatterns) {\n desc = desc.replace(pattern, '');\n }\n if (desc === sentence) {\n desc = sentence.replace(value, '').trim();\n }\n\n desc = desc.replace(/^[,:\\-–—]+\\s*/, '').replace(/\\s*[,:\\-–—]+$/, '');\n desc = desc.replace(\n /\\s+(?:of|on|in|at|by|to|for|from|with|about|over|under|between|than|into|onto|upon)\\s*([,.])/gi,\n '$1',\n );\n desc = desc.replace(\n /\\s+(?:for|of|to|in|at|by|with|from|about|over|under|between|than|into|onto|upon)\\s*[.!?]?\\s*$/i,\n '.',\n );\n desc = desc.replace(/\\s{2,}/g, ' ').replace(/\\s+([,.])/g, '$1');\n desc = desc.charAt(0).toUpperCase() + desc.slice(1);\n return desc.trim();\n}\n\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nconst TYPE_SPECIFICITY: Record<ExtractionType, number> = {\n stat: 5,\n date: 4,\n quote: 3,\n definition: 3,\n comparison: 2,\n list: 2,\n impactLine: 2,\n fact: 1,\n};\n\nfunction deduplicateOverlapping(elements: ExtractedElement[]): ExtractedElement[] {\n if (elements.length <= 1) return elements;\n\n const toRemove = new Set<number>();\n\n for (let i = 0; i < elements.length; i++) {\n if (toRemove.has(i)) continue;\n const a = elements[i];\n\n for (let j = i + 1; j < elements.length; j++) {\n if (toRemove.has(j)) continue;\n const b = elements[j];\n\n if (elementsOverlap(a, b)) {\n const aSpec = TYPE_SPECIFICITY[a.type];\n const bSpec = TYPE_SPECIFICITY[b.type];\n if (aSpec >= bSpec) {\n toRemove.add(j);\n } else {\n toRemove.add(i);\n break;\n }\n }\n }\n }\n\n return elements.filter((_, index) => !toRemove.has(index));\n}\n\nfunction elementsOverlap(a: ExtractedElement, b: ExtractedElement): boolean {\n if (a.sourcePosition === b.sourcePosition) return true;\n if (a.sourcePosition <= b.sourcePosition && a.endPosition >= b.endPosition) return true;\n if (b.sourcePosition <= a.sourcePosition && b.endPosition >= a.endPosition) return true;\n\n const overlapStart = Math.max(a.sourcePosition, b.sourcePosition);\n const overlapEnd = Math.min(a.endPosition, b.endPosition);\n if (overlapEnd > overlapStart) {\n const overlapLength = overlapEnd - overlapStart;\n const minLength = Math.min(a.endPosition - a.sourcePosition, b.endPosition - b.sourcePosition);\n if (overlapLength > minLength * 0.5) return true;\n }\n\n return false;\n}\n\n/**\n * Strip markdown formatting from text, returning plain prose.\n * Call this before `extractContent` if the input is markdown.\n */\nexport function stripMarkdown(markdown: string): string {\n return (\n markdown\n // Remove code blocks (before inline code)\n .replace(/```[\\s\\S]*?```/g, '')\n // Remove images (before links — `![` prefix distinguishes them)\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '')\n // Remove links, keeping text\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1')\n .replace(/^#{1,6}\\s+/gm, '')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '$1')\n .replace(/\\*([^*]+)\\*/g, '$1')\n .replace(/__([^_]+)__/g, '$1')\n .replace(/_([^_]+)_/g, '$1')\n .replace(/`([^`]+)`/g, '$1')\n .replace(/^>\\s+/gm, '')\n .replace(/^[-*_]{3,}$/gm, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n );\n}\n","/**\n * Template Mapper\n *\n * Converts extracted content elements into squisq TemplateBlock inputs.\n * Each ExtractionType maps to a specific template from BlockTemplates.\n */\n\nimport type {\n ExtractedElement,\n StatData,\n DateData,\n QuoteData,\n ComparisonData,\n FactData,\n ImpactLineData,\n ListData,\n DefinitionData,\n} from './contentExtractor.js';\n\nimport type {\n StatHighlightInput,\n QuoteBlockInput,\n FactCardInput,\n TwoColumnInput,\n DateEventInput,\n FullBleedQuoteInput,\n ListBlockInput,\n DefinitionCardInput,\n TemplateBlock,\n ColorScheme,\n AccentImage,\n} from '../schemas/BlockTemplates.js';\n\n/** Options for mapping an element to a template block. */\nexport interface MapOptions {\n id: string;\n duration: number;\n audioSegment: number;\n colorScheme?: ColorScheme;\n accentImage?: AccentImage;\n sourceStartTime?: number;\n sourceDuration?: number;\n}\n\n/**\n * Convert an extracted element into the matching squisq TemplateBlock.\n */\nexport function mapElementToBlock(element: ExtractedElement, options: MapOptions): TemplateBlock {\n const base = {\n id: options.id,\n duration: options.duration,\n audioSegment: options.audioSegment,\n sourceStartTime: options.sourceStartTime,\n sourceDuration: options.sourceDuration,\n };\n\n switch (element.data.type) {\n case 'stat':\n return mapStat(element.data, base, options);\n case 'date':\n return mapDate(element.data, base, options);\n case 'quote':\n return mapQuote(element.data, base, options);\n case 'comparison':\n return mapComparison(element.data, base, options);\n case 'fact':\n return mapFact(element.data, base, options);\n case 'impactLine':\n return mapImpactLine(element.data, base, options);\n case 'list':\n return mapList(element.data, base, options);\n case 'definition':\n return mapDefinition(element.data, base, options);\n }\n}\n\n// ── Per-type mappers ───────────────────────────────────────────────\n\ntype Base = {\n id: string;\n duration: number;\n audioSegment: number;\n sourceStartTime?: number;\n sourceDuration?: number;\n};\n\nfunction mapStat(data: StatData, base: Base, opts: MapOptions): StatHighlightInput {\n return {\n ...base,\n template: 'statHighlight',\n stat: data.value,\n description: data.description,\n colorScheme: opts.colorScheme,\n accentImage: opts.accentImage,\n };\n}\n\nfunction mapDate(data: DateData, base: Base, opts: MapOptions): DateEventInput {\n return {\n ...base,\n template: 'dateEvent',\n date: data.date,\n description: data.description,\n mood: data.mood,\n accentImage: opts.accentImage,\n };\n}\n\nfunction mapQuote(data: QuoteData, base: Base, opts: MapOptions): QuoteBlockInput {\n return {\n ...base,\n template: 'quote',\n quote: data.quote,\n attribution: data.attribution,\n accentImage: opts.accentImage,\n };\n}\n\nfunction mapComparison(data: ComparisonData, base: Base, opts: MapOptions): TwoColumnInput {\n return {\n ...base,\n template: 'twoColumn',\n left: data.left,\n right: data.right,\n header: data.header,\n leftColor: opts.colorScheme,\n };\n}\n\nfunction mapFact(data: FactData, base: Base, opts: MapOptions): FactCardInput {\n return {\n ...base,\n template: 'factCard',\n fact: data.fact,\n explanation: data.explanation ?? '',\n accentImage: opts.accentImage,\n };\n}\n\nfunction mapImpactLine(data: ImpactLineData, base: Base, opts: MapOptions): FullBleedQuoteInput {\n return {\n ...base,\n template: 'fullBleedQuote',\n text: data.text,\n colorScheme: opts.colorScheme,\n };\n}\n\nfunction mapList(data: ListData, base: Base, opts: MapOptions): ListBlockInput {\n return {\n ...base,\n template: 'list',\n items: data.items,\n title: data.title,\n colorScheme: opts.colorScheme,\n accentImage: opts.accentImage,\n };\n}\n\nfunction mapDefinition(data: DefinitionData, base: Base, opts: MapOptions): DefinitionCardInput {\n return {\n ...base,\n template: 'definitionCard',\n term: data.term,\n definition: data.definition,\n origin: data.origin,\n colorScheme: opts.colorScheme,\n accentImage: opts.accentImage,\n };\n}\n"],"mappings":";AA+HA,IAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AACF;AASA,IAAM,kBAAkB;AAExB,IAAM,sBAAsB;AAAA,EAC1B,IAAI,OAAO,YAAY,eAAe,eAAe,eAAe,KAAK,IAAI;AAAA,EAC7E,IAAI,OAAO,IAAI,eAAe,8BAA8B,eAAe,KAAK,IAAI;AAAA,EACpF,IAAI,OAAO,IAAI,eAAe,2BAA2B,eAAe,KAAK,IAAI;AACnF;AAMA,IAAM,yBAAyB,oBAAI,IAAI;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,eAAe;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AACF;AAEA,IAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AASO,SAAS,eAAe,MAAc,UAA6B,CAAC,GAAqB;AAC9F,QAAM;AAAA,IACJ,gBAAgB;AAAA,IAChB,QAAQ,CAAC,QAAQ,QAAQ,SAAS,cAAc,QAAQ,cAAc,QAAQ,YAAY;AAAA,IAC1F;AAAA,EACF,IAAI;AAEJ,QAAM,WAA+B,CAAC;AAEtC,MAAI,MAAM,SAAS,MAAM,EAAG,UAAS,KAAK,GAAG,aAAa,IAAI,CAAC;AAC/D,MAAI,MAAM,SAAS,MAAM,EAAG,UAAS,KAAK,GAAG,aAAa,IAAI,CAAC;AAC/D,MAAI,MAAM,SAAS,OAAO,EAAG,UAAS,KAAK,GAAG,cAAc,IAAI,CAAC;AACjE,MAAI,MAAM,SAAS,YAAY,EAAG,UAAS,KAAK,GAAG,mBAAmB,IAAI,CAAC;AAC3E,MAAI,MAAM,SAAS,MAAM,EAAG,UAAS,KAAK,GAAG,aAAa,IAAI,CAAC;AAC/D,MAAI,MAAM,SAAS,YAAY,EAAG,UAAS,KAAK,GAAG,mBAAmB,IAAI,CAAC;AAC3E,MAAI,MAAM,SAAS,MAAM,EAAG,UAAS,KAAK,GAAG,aAAa,IAAI,CAAC;AAC/D,MAAI,MAAM,SAAS,YAAY,EAAG,UAAS,KAAK,GAAG,mBAAmB,IAAI,CAAC;AAE3E,MAAI,WAAW,SAAS,OAAO,CAAC,MAAM,EAAE,cAAc,aAAa;AAEnE,MAAI,eAAe,QAAW;AAC5B,UAAM,SAAS,oBAAI,IAAwC;AAC3D,eAAW,QAAQ,UAAU;AAC3B,YAAM,OAAO,OAAO,IAAI,KAAK,IAAI,KAAK,CAAC;AACvC,WAAK,KAAK,IAAI;AACd,aAAO,IAAI,KAAK,MAAM,IAAI;AAAA,IAC5B;AACA,eAAW,CAAC;AACZ,eAAW,CAAC,EAAE,IAAI,KAAK,QAAQ;AAC7B,WAAK,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAC/C,eAAS,KAAK,GAAG,KAAK,MAAM,GAAG,UAAU,CAAC;AAAA,IAC5C;AAAA,EACF;AAEA,WAAS,KAAK,CAAC,GAAG,MAAM,EAAE,iBAAiB,EAAE,cAAc;AAC3D,aAAW,uBAAuB,QAAQ;AAE1C,SAAO;AAAA,IACL,UAAU;AAAA,IACV,cAAc,KAAK;AAAA,IACnB,OAAO;AAAA,MACL,WAAW,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE;AAAA,MACrD,WAAW,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE;AAAA,MACrD,YAAY,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE;AAAA,MACvD,iBAAiB,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE;AAAA,MACjE,WAAW,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE;AAAA,MACrD,iBAAiB,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE;AAAA,MACjE,WAAW,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE;AAAA,MACrD,iBAAiB,SAAS,OAAO,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE;AAAA,MACjE,YAAY,SAAS;AAAA,IACvB;AAAA,EACF;AACF;AAIA,SAAS,aAAa,MAAkC;AACtD,QAAM,WAA+B,CAAC;AACtC,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,WAAW,eAAe;AACnC,YAAQ,YAAY;AACpB,QAAI;AACJ,YAAQ,QAAQ,QAAQ,KAAK,IAAI,OAAO,MAAM;AAC5C,YAAM,WAAW,MAAM;AACvB,UAAI,KAAK,IAAI,QAAQ,EAAG;AACxB,WAAK,IAAI,QAAQ;AAEjB,YAAM,UAAU,mBAAmB,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM;AAClE,YAAM,QAAQ,MAAM,CAAC,EAAE,KAAK;AAE5B,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM,QAAQ;AAAA,QACd,YAAY,wBAAwB,OAAO,QAAQ,QAAQ;AAAA,QAC3D,gBAAgB,QAAQ;AAAA,QACxB,aAAa,QAAQ;AAAA,QACrB,MAAM,EAAE,MAAM,QAAQ,OAAO,aAAa,iBAAiB,QAAQ,UAAU,KAAK,EAAE;AAAA,MACtF,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,MAAkC;AACtD,QAAM,WAA+B,CAAC;AACtC,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,WAAW,eAAe;AACnC,YAAQ,YAAY;AACpB,QAAI;AACJ,YAAQ,QAAQ,QAAQ,KAAK,IAAI,OAAO,MAAM;AAC5C,YAAM,WAAW,MAAM;AACvB,UAAI,KAAK,IAAI,QAAQ,EAAG;AACxB,WAAK,IAAI,QAAQ;AAEjB,YAAM,UAAU,mBAAmB,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM;AAClE,YAAM,UAAU,MAAM,CAAC,EAAE,KAAK;AAE9B,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM,QAAQ;AAAA,QACd,YAAY,wBAAwB,SAAS,QAAQ,QAAQ;AAAA,QAC7D,gBAAgB,QAAQ;AAAA,QACxB,aAAa,QAAQ;AAAA,QACrB,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,MAAM;AAAA,UACN,aAAa,iBAAiB,QAAQ,UAAU,OAAO;AAAA,UACvD,MAAM,WAAW,QAAQ,QAAQ;AAAA,QACnC;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,cAAc,MAAkC;AACvD,QAAM,WAA+B,CAAC;AACtC,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,WAAW,gBAAgB;AACpC,YAAQ,YAAY;AACpB,QAAI;AACJ,YAAQ,QAAQ,QAAQ,KAAK,IAAI,OAAO,MAAM;AAC5C,YAAM,WAAW,MAAM;AACvB,UAAI,KAAK,IAAI,QAAQ,EAAG;AACxB,WAAK,IAAI,QAAQ;AAEjB,YAAM,QAAQ,MAAM,SAAS,IAAI,MAAM,CAAC,IAAI,MAAM,CAAC;AACnD,YAAM,cAAc,MAAM,SAAS,IAAI,MAAM,CAAC,IAAI;AAClD,UAAI,MAAM,SAAS,MAAM,MAAM,SAAS,IAAK;AAE7C,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM,MAAM,CAAC;AAAA,QACb,YAAY,yBAAyB,KAAK;AAAA,QAC1C,gBAAgB;AAAA,QAChB,aAAa,WAAW,MAAM,CAAC,EAAE;AAAA,QACjC,MAAM,EAAE,MAAM,SAAS,OAAO,MAAM,KAAK,GAAG,aAAa,aAAa,KAAK,EAAE;AAAA,MAC/E,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,MAAkC;AAC5D,QAAM,WAA+B,CAAC;AACtC,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,WAAW,qBAAqB;AACzC,YAAQ,YAAY;AACpB,QAAI;AACJ,YAAQ,QAAQ,QAAQ,KAAK,IAAI,OAAO,MAAM;AAC5C,YAAM,WAAW,MAAM;AACvB,UAAI,KAAK,IAAI,QAAQ,EAAG;AACxB,WAAK,IAAI,QAAQ;AAOjB,YAAM,aAAa,CAAC,MAAc,EAAE,QAAQ,WAAW,EAAE,EAAE,KAAK;AAChE,YAAM,OAAO,WAAW,MAAM,CAAC,EAAE,KAAK,CAAC;AACvC,YAAM,QAAQ,WAAW,MAAM,CAAC,EAAE,KAAK,CAAC;AACxC,UAAI,KAAK,SAAS,KAAK,KAAK,SAAS,GAAI;AACzC,UAAI,MAAM,SAAS,KAAK,MAAM,SAAS,GAAI;AAQ3C,YAAM,UAAU,CAAC,MACf,EACG,YAAY,EACZ,MAAM,KAAK,EACX,IAAI,CAAC,MAAM,EAAE,QAAQ,eAAe,EAAE,CAAC,EACvC,OAAO,OAAO;AACnB,YAAM,YAAY,QAAQ,IAAI;AAC9B,YAAM,aAAa,QAAQ,KAAK;AAChC,UACE,UAAU,SAAS,KACnB,WAAW,SAAS,KACpB,UAAU,UAAU,SAAS,CAAC,MAAM,WAAW,CAAC,GAChD;AACA;AAAA,MACF;AAEA,YAAM,UAAU,mBAAmB,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM;AAElE,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM,QAAQ;AAAA,QACd,YAAY;AAAA,QACZ,gBAAgB,QAAQ;AAAA,QACxB,aAAa,QAAQ;AAAA,QACrB,MAAM,EAAE,MAAM,cAAc,MAAM,EAAE,OAAO,KAAK,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE;AAAA,MAC7E,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAUA,SAAS,mBAAmB,MAAc,OAAwB;AAChE,QAAM,KAAK,KAAK,KAAK;AACrB,MAAI,OAAO,OAAO,OAAO,IAAK,QAAO;AACrC,MAAI,OAAO,IAAK,QAAO;AAGvB,MAAI,KAAK,QAAQ,CAAC,MAAM,IAAK,QAAO;AAEpC,MAAI,QAAQ,KAAK,KAAK,QAAQ,CAAC,KAAK,EAAE,KAAK,QAAQ,KAAK,KAAK,QAAQ,CAAC,KAAK,EAAE,EAAG,QAAO;AAGvF,MAAI,IAAI,QAAQ;AAChB,SAAO,KAAK,KAAK,WAAW,KAAK,KAAK,CAAC,CAAC,EAAG;AAC3C,QAAM,OAAO,KAAK,MAAM,IAAI,GAAG,KAAK;AACpC,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI,QAAQ,uBAAuB,IAAI,KAAK,YAAY,CAAC,EAAG,QAAO;AAKnE,MAAI,IAAI,QAAQ;AAChB,SAAO,IAAI,KAAK,UAAU,KAAK,KAAK,KAAK,CAAC,CAAC,EAAG;AAC9C,MAAI,KAAK,KAAK,OAAQ,QAAO;AAC7B,SAAO,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC;AAC9B;AAUA,SAAS,oBACP,MACA,UACM;AACN,QAAM,OAAO,CAAC,UAAkB,WAAmB;AACjD,QAAI,QAAQ;AACZ,WAAO,QAAQ,UAAU,KAAK,KAAK,KAAK,KAAK,CAAC,EAAG;AACjD,QAAI,MAAM;AACV,WAAO,MAAM,SAAS,UAAU,KAAK,KAAK,MAAM,CAAC,CAAC,EAAG;AACrD,QAAI,OAAO,MAAO;AAClB,UAAM,UAAU,KAAK,MAAM,OAAO,GAAG;AACrC,QAAI,QAAS,UAAS,SAAS,OAAO,GAAG;AAAA,EAC3C;AAEA,MAAI,YAAY;AAChB,MAAI,IAAI;AACR,SAAO,IAAI,KAAK,QAAQ;AACtB,QAAI,mBAAmB,MAAM,CAAC,GAAG;AAE/B,UAAI,MAAM,IAAI;AACd,aAAO,MAAM,KAAK,UAAU,QAAQ,KAAK,KAAK,GAAG,CAAC,EAAG;AACrD,WAAK,WAAW,GAAG;AACnB,kBAAY;AACZ,UAAI;AAAA,IACN,OAAO;AACL;AAAA,IACF;AAAA,EACF;AACA,MAAI,YAAY,KAAK,OAAQ,MAAK,WAAW,KAAK,MAAM;AAC1D;AAEA,SAAS,aAAa,MAAkC;AACtD,QAAM,WAA+B,CAAC;AAEtC,sBAAoB,MAAM,CAAC,SAAS,OAAO,QAAQ;AACjD,QAAI,QAAQ,UAAU,GAAI;AAE1B,UAAM,QAAQ,oBAAoB,OAAO;AACzC,QAAI,QAAQ,IAAK;AAEjB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,MAAM,EAAE,MAAM,QAAQ,MAAM,QAAQ;AAAA,IACtC,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AACT;AAEA,SAAS,mBAAmB,MAAkC;AAC5D,QAAM,WAA+B,CAAC;AAEtC,sBAAoB,MAAM,CAAC,SAAS,OAAO,QAAQ;AACjD,QAAI,QAAQ,SAAS,KAAK,QAAQ,SAAS,GAAI;AAE/C,QAAI,QAAQ;AACZ,UAAM,QAAQ,QAAQ,YAAY;AAElC,eAAW,QAAQ,cAAc;AAC/B,UAAI,MAAM,WAAW,IAAI,GAAG;AAC1B,iBAAS;AACT;AAAA,MACF;AAAA,IACF;AAEA,QAAI,MAAM,KAAK,UAAU,KAAK,GAAG,MAAM,IAAK,UAAS;AACrD,QAAI,QAAQ,SAAS,GAAI,UAAS;AAClC,QAAI,QAAQ,SAAS,GAAG,EAAG,UAAS;AACpC,QAAI,4BAA4B,KAAK,OAAO,EAAG,UAAS;AACxD,QAAI,QAAQ,IAAK;AAEjB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,YAAY,KAAK,IAAI,GAAG,KAAK;AAAA,MAC7B,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,MAAM,EAAE,MAAM,cAAc,MAAM,QAAQ;AAAA,IAC5C,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AACT;AAEA,SAAS,aAAa,MAAkC;AACtD,QAAM,WAA+B,CAAC;AACtC,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,WAAW,eAAe;AACnC,YAAQ,YAAY;AACpB,QAAI;AACJ,YAAQ,QAAQ,QAAQ,KAAK,IAAI,OAAO,MAAM;AAC5C,YAAM,WAAW,MAAM;AACvB,UAAI,KAAK,IAAI,QAAQ,EAAG;AACxB,WAAK,IAAI,QAAQ;AAEjB,YAAM,cAAc,MAAM,CAAC,EAAE,KAAK;AAClC,YAAM,QAAQ,YACX,MAAM,2CAA2C,EACjD,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,OAAO,CAAC,SAAS,KAAK,UAAU,KAAK,KAAK,UAAU,EAAE;AAEzD,UAAI,MAAM,SAAS,KAAK,MAAM,SAAS,EAAG;AAE1C,YAAM,UAAU,mBAAmB,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM;AAElE,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM,QAAQ;AAAA,QACd,YAAY;AAAA,QACZ,gBAAgB,QAAQ;AAAA,QACxB,aAAa,QAAQ;AAAA,QACrB,MAAM,EAAE,MAAM,QAAQ,MAAM;AAAA,MAC9B,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,MAAkC;AAC5D,QAAM,WAA+B,CAAC;AACtC,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,WAAW,qBAAqB;AACzC,YAAQ,YAAY;AACpB,QAAI;AACJ,YAAQ,QAAQ,QAAQ,KAAK,IAAI,OAAO,MAAM;AAC5C,YAAM,WAAW,MAAM;AACvB,UAAI,KAAK,IAAI,QAAQ,EAAG;AACxB,WAAK,IAAI,QAAQ;AAEjB,YAAM,OAAO,MAAM,CAAC,EAAE,KAAK;AAC3B,YAAM,aAAa,MAAM,CAAC,EAAE,KAAK;AACjC,UAAI,KAAK,SAAS,KAAK,KAAK,SAAS,GAAI;AACzC,UAAI,WAAW,SAAS,MAAM,WAAW,SAAS,IAAK;AAEvD,YAAM,UAAU,mBAAmB,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM;AAElE,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM,QAAQ;AAAA,QACd,YAAY;AAAA,QACZ,gBAAgB,QAAQ;AAAA,QACxB,aAAa,QAAQ;AAAA,QACrB,MAAM,EAAE,MAAM,cAAc,MAAM,WAAW;AAAA,MAC/C,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAIA,SAAS,mBACP,MACA,YACA,aACkD;AAClD,MAAI,QAAQ;AACZ,SAAO,QAAQ,KAAK,CAAC,mBAAmB,MAAM,QAAQ,CAAC,EAAG;AAC1D,SAAO,QAAQ,cAAc,KAAK,KAAK,KAAK,KAAK,CAAC,EAAG;AAErD,MAAI,MAAM,aAAa;AACvB,SAAO,MAAM,KAAK,UAAU,CAAC,mBAAmB,MAAM,GAAG,EAAG;AAC5D,MAAI,MAAM,KAAK,OAAQ;AAEvB,SAAO,EAAE,UAAU,KAAK,MAAM,OAAO,GAAG,EAAE,KAAK,GAAG,OAAO,IAAI;AAC/D;AAEA,SAAS,wBAAwB,OAAe,SAAyB;AACvE,MAAI,QAAQ;AACZ,MAAI,MAAM,SAAS,GAAG,EAAG,UAAS;AAClC,MAAI,4BAA4B,KAAK,KAAK,EAAG,UAAS;AACtD,MAAI,8CAA8C,KAAK,OAAO,EAAG,UAAS;AAC1E,MAAI,QAAQ,SAAS,GAAI,UAAS;AAClC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACvC;AAEA,SAAS,wBAAwB,MAAc,SAAyB;AACtE,MAAI,QAAQ;AACZ,MACE,yFAAyF;AAAA,IACvF;AAAA,EACF;AAEA,aAAS;AACX,MAAI,uDAAuD,KAAK,OAAO,EAAG,UAAS;AACnF,MAAI,4CAA4C,KAAK,OAAO,EAAG,UAAS;AACxE,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACvC;AAEA,SAAS,yBAAyB,OAAuB;AACvD,MAAI,QAAQ;AACZ,MAAI,MAAM,UAAU,MAAM,MAAM,UAAU,IAAK,UAAS;AACxD,MAAI,qBAAqB,KAAK,KAAK,EAAG,UAAS;AAC/C,MAAI,CAAC,SAAS,KAAK,KAAK,EAAG,UAAS;AACpC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACvC;AAEA,SAAS,oBAAoB,UAA0B;AACrD,MAAI,QAAQ;AACZ,QAAM,QAAQ,SAAS,YAAY;AACnC,aAAW,aAAa,iBAAiB;AACvC,QAAI,MAAM,SAAS,SAAS,GAAG;AAC7B,eAAS;AACT;AAAA,IACF;AAAA,EACF;AACA,QAAM,cAAc,SAAS,MAAM,kBAAkB;AACrD,MAAI,eAAe,YAAY,UAAU,EAAG,UAAS;AACrD,MAAI,KAAK,KAAK,QAAQ,EAAG,UAAS;AAClC,MAAI,SAAS,SAAS,GAAG,EAAG,UAAS;AACrC,MAAI,SAAS,SAAS,IAAK,UAAS;AACpC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACvC;AAEA,SAAS,WAAW,UAAwD;AAC1E,QAAM,QAAQ,SAAS,YAAY;AACnC,aAAW,QAAQ,mBAAmB;AACpC,QAAI,MAAM,SAAS,IAAI,EAAG,QAAO;AAAA,EACnC;AACA,aAAW,QAAQ,wBAAwB;AACzC,QAAI,MAAM,SAAS,IAAI,EAAG,QAAO;AAAA,EACnC;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,UAAkB,OAAuB;AACjE,QAAM,sBAAsB;AAAA,IAC1B,IAAI,OAAO,eAAe,YAAY,KAAK,CAAC,aAAa,GAAG;AAAA,IAC5D,IAAI,OAAO,eAAe,YAAY,KAAK,CAAC,aAAa,GAAG;AAAA,IAC5D,IAAI,OAAO,mBAAmB,YAAY,KAAK,CAAC,aAAa,GAAG;AAAA,IAChE,IAAI,OAAO,eAAe,YAAY,KAAK,CAAC,aAAa,GAAG;AAAA,IAC5D,IAAI,OAAO,kBAAkB,YAAY,KAAK,CAAC,aAAa,GAAG;AAAA,IAC/D,IAAI,OAAO,iBAAiB,YAAY,KAAK,CAAC,aAAa,GAAG;AAAA,EAChE;AAEA,MAAI,OAAO;AACX,aAAW,WAAW,qBAAqB;AACzC,WAAO,KAAK,QAAQ,SAAS,EAAE;AAAA,EACjC;AACA,MAAI,SAAS,UAAU;AACrB,WAAO,SAAS,QAAQ,OAAO,EAAE,EAAE,KAAK;AAAA,EAC1C;AAEA,SAAO,KAAK,QAAQ,iBAAiB,EAAE,EAAE,QAAQ,iBAAiB,EAAE;AACpE,SAAO,KAAK;AAAA,IACV;AAAA,IACA;AAAA,EACF;AACA,SAAO,KAAK;AAAA,IACV;AAAA,IACA;AAAA,EACF;AACA,SAAO,KAAK,QAAQ,WAAW,GAAG,EAAE,QAAQ,cAAc,IAAI;AAC9D,SAAO,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC;AAClD,SAAO,KAAK,KAAK;AACnB;AAEA,SAAS,YAAY,KAAqB;AACxC,SAAO,IAAI,QAAQ,uBAAuB,MAAM;AAClD;AAEA,IAAM,mBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,MAAM;AACR;AAEA,SAAS,uBAAuB,UAAkD;AAChF,MAAI,SAAS,UAAU,EAAG,QAAO;AAEjC,QAAM,WAAW,oBAAI,IAAY;AAEjC,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,QAAI,SAAS,IAAI,CAAC,EAAG;AACrB,UAAM,IAAI,SAAS,CAAC;AAEpB,aAAS,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AAC5C,UAAI,SAAS,IAAI,CAAC,EAAG;AACrB,YAAM,IAAI,SAAS,CAAC;AAEpB,UAAI,gBAAgB,GAAG,CAAC,GAAG;AACzB,cAAM,QAAQ,iBAAiB,EAAE,IAAI;AACrC,cAAM,QAAQ,iBAAiB,EAAE,IAAI;AACrC,YAAI,SAAS,OAAO;AAClB,mBAAS,IAAI,CAAC;AAAA,QAChB,OAAO;AACL,mBAAS,IAAI,CAAC;AACd;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,SAAS,OAAO,CAAC,GAAG,UAAU,CAAC,SAAS,IAAI,KAAK,CAAC;AAC3D;AAEA,SAAS,gBAAgB,GAAqB,GAA8B;AAC1E,MAAI,EAAE,mBAAmB,EAAE,eAAgB,QAAO;AAClD,MAAI,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,eAAe,EAAE,YAAa,QAAO;AACnF,MAAI,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,eAAe,EAAE,YAAa,QAAO;AAEnF,QAAM,eAAe,KAAK,IAAI,EAAE,gBAAgB,EAAE,cAAc;AAChE,QAAM,aAAa,KAAK,IAAI,EAAE,aAAa,EAAE,WAAW;AACxD,MAAI,aAAa,cAAc;AAC7B,UAAM,gBAAgB,aAAa;AACnC,UAAM,YAAY,KAAK,IAAI,EAAE,cAAc,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc;AAC7F,QAAI,gBAAgB,YAAY,IAAK,QAAO;AAAA,EAC9C;AAEA,SAAO;AACT;AAMO,SAAS,cAAc,UAA0B;AACtD,SACE,SAEG,QAAQ,mBAAmB,EAAE,EAE7B,QAAQ,2BAA2B,EAAE,EAErC,QAAQ,0BAA0B,IAAI,EACtC,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,oBAAoB,IAAI,EAChC,QAAQ,gBAAgB,IAAI,EAC5B,QAAQ,gBAAgB,IAAI,EAC5B,QAAQ,cAAc,IAAI,EAC1B,QAAQ,cAAc,IAAI,EAC1B,QAAQ,WAAW,EAAE,EACrB,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,WAAW,MAAM,EACzB,KAAK;AAEZ;;;ACz2BO,SAAS,kBAAkB,SAA2B,SAAoC;AAC/F,QAAM,OAAO;AAAA,IACX,IAAI,QAAQ;AAAA,IACZ,UAAU,QAAQ;AAAA,IAClB,cAAc,QAAQ;AAAA,IACtB,iBAAiB,QAAQ;AAAA,IACzB,gBAAgB,QAAQ;AAAA,EAC1B;AAEA,UAAQ,QAAQ,KAAK,MAAM;AAAA,IACzB,KAAK;AACH,aAAO,QAAQ,QAAQ,MAAM,MAAM,OAAO;AAAA,IAC5C,KAAK;AACH,aAAO,QAAQ,QAAQ,MAAM,MAAM,OAAO;AAAA,IAC5C,KAAK;AACH,aAAO,SAAS,QAAQ,MAAM,MAAM,OAAO;AAAA,IAC7C,KAAK;AACH,aAAO,cAAc,QAAQ,MAAM,MAAM,OAAO;AAAA,IAClD,KAAK;AACH,aAAO,QAAQ,QAAQ,MAAM,MAAM,OAAO;AAAA,IAC5C,KAAK;AACH,aAAO,cAAc,QAAQ,MAAM,MAAM,OAAO;AAAA,IAClD,KAAK;AACH,aAAO,QAAQ,QAAQ,MAAM,MAAM,OAAO;AAAA,IAC5C,KAAK;AACH,aAAO,cAAc,QAAQ,MAAM,MAAM,OAAO;AAAA,EACpD;AACF;AAYA,SAAS,QAAQ,MAAgB,MAAY,MAAsC;AACjF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,aAAa,KAAK;AAAA,IAClB,aAAa,KAAK;AAAA,EACpB;AACF;AAEA,SAAS,QAAQ,MAAgB,MAAY,MAAkC;AAC7E,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,EACpB;AACF;AAEA,SAAS,SAAS,MAAiB,MAAY,MAAmC;AAChF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK;AAAA,IAClB,aAAa,KAAK;AAAA,EACpB;AACF;AAEA,SAAS,cAAc,MAAsB,MAAY,MAAkC;AACzF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,WAAW,KAAK;AAAA,EAClB;AACF;AAEA,SAAS,QAAQ,MAAgB,MAAY,MAAiC;AAC5E,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV,MAAM,KAAK;AAAA,IACX,aAAa,KAAK,eAAe;AAAA,IACjC,aAAa,KAAK;AAAA,EACpB;AACF;AAEA,SAAS,cAAc,MAAsB,MAAY,MAAuC;AAC9F,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,EACpB;AACF;AAEA,SAAS,QAAQ,MAAgB,MAAY,MAAkC;AAC7E,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV,OAAO,KAAK;AAAA,IACZ,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK;AAAA,IAClB,aAAa,KAAK;AAAA,EACpB;AACF;AAEA,SAAS,cAAc,MAAsB,MAAY,MAAuC;AAC9F,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV,MAAM,KAAK;AAAA,IACX,YAAY,KAAK;AAAA,IACjB,QAAQ,KAAK;AAAA,IACb,aAAa,KAAK;AAAA,IAClB,aAAa,KAAK;AAAA,EACpB;AACF;","names":[]}
@@ -2,7 +2,7 @@ import {
2
2
  extractPlainText,
3
3
  findNodesByType,
4
4
  walkMarkdownTree
5
- } from "./chunk-OKJOQXK4.js";
5
+ } from "./chunk-HWVFJAAH.js";
6
6
 
7
7
  // src/recommend/templates.ts
8
8
  var EMPTY_PROFILE = Object.freeze({
@@ -189,4 +189,4 @@ export {
189
189
  pickAutoTemplate,
190
190
  recommendTemplatesForBlock
191
191
  };
192
- //# sourceMappingURL=chunk-LRCV7N7F.js.map
192
+ //# sourceMappingURL=chunk-UY7KGQ5R.js.map
@@ -118,12 +118,39 @@ function estimateNarrationDuration(text, wordsPerSecond = DEFAULT_WORDS_PER_SECO
118
118
  return spokenWords / wordsPerSecond;
119
119
  }
120
120
 
121
+ // src/timing/readingTime.ts
122
+ var DEFAULT_READING_WPM = 200;
123
+ function estimateReadingTime(text, options) {
124
+ const configuredWpm = options?.wordsPerMinute;
125
+ const wpm = typeof configuredWpm === "number" && configuredWpm > 0 ? configuredWpm : DEFAULT_READING_WPM;
126
+ const words = text.split(/\s+/).filter((w) => w.length > 0).length;
127
+ const minutes = words / wpm;
128
+ return {
129
+ words,
130
+ minutes,
131
+ seconds: Math.round(minutes * 60)
132
+ };
133
+ }
134
+ function estimateNarrationTime(text, options) {
135
+ const providedWps = options?.wordsPerSecond;
136
+ const wps = typeof providedWps === "number" && providedWps > 0 ? providedWps : DEFAULT_WORDS_PER_SECOND;
137
+ const spokenWords = countSpokenWords(text);
138
+ const totalSeconds = spokenWords / wps;
139
+ return {
140
+ spokenWords,
141
+ minutes: totalSeconds / 60,
142
+ seconds: Math.round(totalSeconds)
143
+ };
144
+ }
145
+
121
146
  export {
122
147
  DEFAULT_WORDS_PER_SECOND,
123
148
  estimateSpokenWordCount,
124
149
  countSpokenWords,
125
150
  estimateTimeFromText,
126
151
  calculatePrefixDuration,
127
- estimateNarrationDuration
152
+ estimateNarrationDuration,
153
+ estimateReadingTime,
154
+ estimateNarrationTime
128
155
  };
129
- //# sourceMappingURL=chunk-3E5F2XMR.js.map
156
+ //# sourceMappingURL=chunk-VWUFZ6ZG.js.map