@bendyline/squisq 1.5.2 → 2.0.0

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 (263) hide show
  1. package/README.md +63 -10
  2. package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
  3. package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
  4. package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
  5. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  6. package/dist/chunk-3E5F2XMR.js.map +1 -0
  7. package/dist/chunk-4X3JQXNM.js +35 -0
  8. package/dist/chunk-4X3JQXNM.js.map +1 -0
  9. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  10. package/dist/chunk-5DKBFXPY.js.map +1 -0
  11. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  12. package/dist/chunk-5J4HKRJF.js.map +1 -0
  13. package/dist/chunk-5UYI3RJB.js +2503 -0
  14. package/dist/chunk-5UYI3RJB.js.map +1 -0
  15. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  16. package/dist/chunk-66QO7XFB.js.map +1 -0
  17. package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
  18. package/dist/chunk-6PJRB2CF.js.map +1 -0
  19. package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
  20. package/dist/chunk-AMIR72JP.js.map +1 -0
  21. package/dist/chunk-FVJUUTEM.js +74 -0
  22. package/dist/chunk-FVJUUTEM.js.map +1 -0
  23. package/dist/{chunk-5TYS5V3G.js → chunk-GF3BVNLU.js} +4127 -3538
  24. package/dist/chunk-GF3BVNLU.js.map +1 -0
  25. package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
  26. package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
  27. package/dist/chunk-JOQYPRDZ.js +127 -0
  28. package/dist/chunk-JOQYPRDZ.js.map +1 -0
  29. package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
  30. package/dist/chunk-LYNJEOAC.js.map +1 -0
  31. package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
  32. package/dist/chunk-PXLVXTLW.js.map +1 -0
  33. package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
  34. package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
  35. package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
  36. package/dist/chunk-QTSJOY54.js.map +1 -0
  37. package/dist/chunk-QVANWDPH.js +2358 -0
  38. package/dist/chunk-QVANWDPH.js.map +1 -0
  39. package/dist/{chunk-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  40. package/dist/chunk-R2GC6RLD.js.map +1 -0
  41. package/dist/chunk-R2YZTCLO.js +292 -0
  42. package/dist/chunk-R2YZTCLO.js.map +1 -0
  43. package/dist/chunk-S7A5D7ZJ.js +174 -0
  44. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  45. package/dist/chunk-T3TI7GSZ.js +884 -0
  46. package/dist/chunk-T3TI7GSZ.js.map +1 -0
  47. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  48. package/dist/chunk-TUWMK62B.js.map +1 -0
  49. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  50. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +127 -33
  53. package/dist/generate/index.d.ts +7 -2
  54. package/dist/generate/index.js +1 -1
  55. package/dist/imageEdit/index.d.ts +9 -6
  56. package/dist/imageEdit/index.js +3 -3
  57. package/dist/index.d.ts +12 -10
  58. package/dist/index.js +246 -80
  59. package/dist/jsonForm/index.d.ts +7 -3
  60. package/dist/jsonForm/index.js +9 -4
  61. package/dist/markdown/index.d.ts +6 -70
  62. package/dist/markdown/index.js +13 -9
  63. package/dist/narration/index.d.ts +618 -0
  64. package/dist/narration/index.js +82 -0
  65. package/dist/recommend/index.d.ts +15 -1
  66. package/dist/recommend/index.js +2 -2
  67. package/dist/schemas/index.d.ts +28 -6
  68. package/dist/schemas/index.js +15 -13
  69. package/dist/spatial/index.d.ts +0 -1
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.js +1 -1
  72. package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
  73. package/dist/timing/index.js +5 -3
  74. package/dist/transform/index.d.ts +35 -24
  75. package/dist/transform/index.js +9 -10
  76. package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
  77. package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
  78. package/dist/versions/index.d.ts +2 -1
  79. package/dist/versions/index.js +2 -2
  80. package/package.json +6 -6
  81. package/src/__tests__/applyNarrationTiming.test.ts +190 -0
  82. package/src/__tests__/applyRenderStyle.test.ts +3 -3
  83. package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
  84. package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
  85. package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
  86. package/src/__tests__/asciiDiagramParse.test.ts +233 -0
  87. package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
  88. package/src/__tests__/asciiDiagramRender.test.ts +162 -0
  89. package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
  90. package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
  91. package/src/__tests__/asciiTimelineParse.test.ts +205 -0
  92. package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
  93. package/src/__tests__/asciiTimelineRender.test.ts +376 -0
  94. package/src/__tests__/attrTokens.test.ts +5 -0
  95. package/src/__tests__/contentContainer.test.ts +43 -0
  96. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
  97. package/src/__tests__/dataTable.test.ts +25 -15
  98. package/src/__tests__/diagramBlock.test.ts +3 -3
  99. package/src/__tests__/diagramText.test.ts +26 -0
  100. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  101. package/src/__tests__/drawingBlock.test.ts +3 -3
  102. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  103. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  104. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  105. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  106. package/src/__tests__/getLayers.test.ts +32 -18
  107. package/src/__tests__/getLayersFallback.test.ts +8 -8
  108. package/src/__tests__/imageEditVersions.test.ts +2 -0
  109. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  110. package/src/__tests__/layoutBlock.test.ts +3 -3
  111. package/src/__tests__/majorApiTypes.test.ts +18 -0
  112. package/src/__tests__/markdown.test.ts +41 -0
  113. package/src/__tests__/matchFontFamily.test.ts +114 -0
  114. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  115. package/src/__tests__/materializeTestUtils.ts +17 -0
  116. package/src/__tests__/narrationAlign.test.ts +150 -0
  117. package/src/__tests__/narrationFeatures.test.ts +71 -0
  118. package/src/__tests__/narrationNuclei.test.ts +45 -0
  119. package/src/__tests__/narrationPacing.test.ts +189 -0
  120. package/src/__tests__/narrationScript.test.ts +149 -0
  121. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  122. package/src/__tests__/narrationSidecar.test.ts +88 -0
  123. package/src/__tests__/narrationSyllables.test.ts +65 -0
  124. package/src/__tests__/narrationTestSignals.ts +167 -0
  125. package/src/__tests__/narrationVad.test.ts +66 -0
  126. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  127. package/src/__tests__/rootExports.test.ts +18 -0
  128. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  129. package/src/__tests__/shapeGeometry.test.ts +12 -0
  130. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  131. package/src/__tests__/templateInputs.test.ts +69 -0
  132. package/src/__tests__/templateMetadata.test.ts +2 -1
  133. package/src/__tests__/templates.test.ts +143 -17
  134. package/src/__tests__/themeCompile.test.ts +49 -22
  135. package/src/__tests__/transformProvenance.test.ts +160 -0
  136. package/src/__tests__/transformV2.test.ts +40 -14
  137. package/src/__tests__/treeviewBattle.test.ts +76 -0
  138. package/src/__tests__/treeviewDetect.test.ts +168 -0
  139. package/src/__tests__/treeviewParse.test.ts +118 -0
  140. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  141. package/src/__tests__/treeviewRender.test.ts +91 -0
  142. package/src/__tests__/versions.test.ts +44 -0
  143. package/src/doc/applyNarrationTiming.ts +244 -0
  144. package/src/doc/asciiDiagram/chars.ts +210 -0
  145. package/src/doc/asciiDiagram/detect.ts +175 -0
  146. package/src/doc/asciiDiagram/index.ts +38 -0
  147. package/src/doc/asciiDiagram/mapping.ts +225 -0
  148. package/src/doc/asciiDiagram/parse.ts +1095 -0
  149. package/src/doc/asciiDiagram/render.ts +963 -0
  150. package/src/doc/asciiDiagram/repair.ts +400 -0
  151. package/src/doc/asciiDiagram/types.ts +65 -0
  152. package/src/doc/asciiTimeline/detect.ts +138 -0
  153. package/src/doc/asciiTimeline/index.ts +26 -0
  154. package/src/doc/asciiTimeline/mapping.ts +43 -0
  155. package/src/doc/asciiTimeline/parse.ts +870 -0
  156. package/src/doc/asciiTimeline/render.ts +427 -0
  157. package/src/doc/asciiTimeline/types.ts +73 -0
  158. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  159. package/src/doc/audioMapping.ts +60 -46
  160. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  161. package/src/doc/docToMarkdown.ts +70 -2
  162. package/src/doc/index.ts +76 -4
  163. package/src/doc/markdownToDoc.ts +135 -1
  164. package/src/doc/materializeBlockLayers.ts +304 -0
  165. package/src/doc/mediaAnnotations.ts +20 -0
  166. package/src/doc/resolveDocTheme.ts +8 -7
  167. package/src/doc/templateInputs.ts +185 -2
  168. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  169. package/src/doc/templates/diagramBlock.ts +237 -38
  170. package/src/doc/templates/fallbackBlock.ts +1 -1
  171. package/src/doc/templates/index.ts +134 -279
  172. package/src/doc/templates/inputDescriptors.ts +20 -1
  173. package/src/doc/templates/metadata.ts +10 -0
  174. package/src/doc/templates/persistentLayers.ts +12 -126
  175. package/src/doc/templates/registry.ts +88 -0
  176. package/src/doc/templates/templateNames.ts +22 -0
  177. package/src/doc/templates/timelineBlock.ts +613 -0
  178. package/src/doc/templates/treeBlock.ts +131 -0
  179. package/src/doc/treeview/chars.ts +72 -0
  180. package/src/doc/treeview/detect.ts +145 -0
  181. package/src/doc/treeview/index.ts +30 -0
  182. package/src/doc/treeview/mapping.ts +95 -0
  183. package/src/doc/treeview/parse.ts +201 -0
  184. package/src/doc/treeview/render.ts +41 -0
  185. package/src/doc/treeview/types.ts +43 -0
  186. package/src/doc/utils/applyRenderStyle.ts +2 -9
  187. package/src/doc/utils/diagramText.ts +78 -0
  188. package/src/doc/utils/shapeGeometry.ts +67 -5
  189. package/src/doc/validate.ts +1 -1
  190. package/src/generate/templateMapper.ts +7 -0
  191. package/src/imageEdit/export.ts +6 -5
  192. package/src/imageEdit/versionPaths.ts +13 -26
  193. package/src/imageEdit/versions.ts +21 -100
  194. package/src/index.ts +1 -0
  195. package/src/internal/immutable.ts +58 -0
  196. package/src/jsonForm/index.ts +9 -1
  197. package/src/jsonForm/pointer.ts +68 -23
  198. package/src/markdown/attrTokens.ts +7 -5
  199. package/src/markdown/index.ts +1 -0
  200. package/src/markdown/stringify.ts +4 -9
  201. package/src/markdown/utils.ts +36 -14
  202. package/src/narration/align.ts +514 -0
  203. package/src/narration/features.ts +231 -0
  204. package/src/narration/index.ts +86 -0
  205. package/src/narration/nuclei.ts +118 -0
  206. package/src/narration/pacing.ts +211 -0
  207. package/src/narration/script.ts +239 -0
  208. package/src/narration/session.ts +100 -0
  209. package/src/narration/sidecar.ts +187 -0
  210. package/src/narration/syllables.ts +85 -0
  211. package/src/narration/trace.ts +50 -0
  212. package/src/narration/types.ts +299 -0
  213. package/src/narration/vad.ts +131 -0
  214. package/src/recommend/numberHighlight.ts +25 -0
  215. package/src/recommend/templates.ts +108 -12
  216. package/src/schemas/BlockTemplates.ts +156 -7
  217. package/src/schemas/Doc.ts +72 -9
  218. package/src/schemas/Media.ts +12 -2
  219. package/src/schemas/Theme.ts +45 -25
  220. package/src/schemas/Viewport.ts +2 -2
  221. package/src/schemas/fontStacks.ts +79 -0
  222. package/src/schemas/themeCompile.ts +15 -0
  223. package/src/schemas/themeConstants.ts +2 -0
  224. package/src/schemas/themeLibrary.ts +25 -19
  225. package/src/schemas/themeValidator.ts +1 -1
  226. package/src/spatial/Geohash.ts +0 -3
  227. package/src/storage/ContentContainer.ts +17 -9
  228. package/src/storage/ScopedContentContainer.ts +45 -15
  229. package/src/transform/applyTransform.ts +5 -5
  230. package/src/transform/index.ts +3 -1
  231. package/src/transform/registry.ts +169 -49
  232. package/src/transform/templateSelector.ts +21 -2
  233. package/src/transform/timingAllocator.ts +152 -21
  234. package/src/transform/types.ts +14 -1
  235. package/src/versions/gitignore.ts +34 -0
  236. package/src/versions/index.ts +2 -0
  237. package/src/versions/operations.ts +18 -104
  238. package/src/versions/paths.ts +13 -28
  239. package/src/versions/snapshotStore.ts +202 -0
  240. package/src/versions/types.ts +3 -1
  241. package/dist/chunk-5TYS5V3G.js.map +0 -1
  242. package/dist/chunk-A4O7GIWE.js.map +0 -1
  243. package/dist/chunk-BEQODJWV.js.map +0 -1
  244. package/dist/chunk-DLXZMS5K.js.map +0 -1
  245. package/dist/chunk-GPLTCSXO.js.map +0 -1
  246. package/dist/chunk-HWVFJAAH.js.map +0 -1
  247. package/dist/chunk-IMSCRLLV.js.map +0 -1
  248. package/dist/chunk-JELIRVDP.js +0 -24
  249. package/dist/chunk-JELIRVDP.js.map +0 -1
  250. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  251. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  252. package/dist/chunk-LTN6I7QW.js.map +0 -1
  253. package/dist/chunk-P3K7NLHF.js.map +0 -1
  254. package/dist/chunk-T5UK6YOB.js.map +0 -1
  255. package/dist/chunk-UY7KGQ5R.js +0 -192
  256. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  257. package/dist/chunk-VLJJHDAC.js +0 -218
  258. package/dist/chunk-VLJJHDAC.js.map +0 -1
  259. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  260. package/dist/story/index.d.ts +0 -1801
  261. package/dist/story/index.js +0 -279
  262. package/src/doc/getLayers.ts +0 -195
  263. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * ASCII tree renderer: nested nodes → clean tree art.
3
+ *
4
+ * A deterministic depth-first walk emitting rail/branch prefixes. The
5
+ * fixpoint is structural: `parseTree(renderTree(t))` preserves the node
6
+ * hierarchy + labels + comments, and `renderTree(parseTree(renderTree(t)))`
7
+ * is byte-identical (idempotent after one normalization cycle). Unlike the
8
+ * diagram renderer there is no spatial layout, so jitter is impossible.
9
+ */
10
+
11
+ import { ASCII_TREE_VOCAB, UNICODE_TREE_VOCAB } from './chars.js';
12
+ import type { Tree, TreeNode } from './types.js';
13
+
14
+ export interface RenderTreeOptions {
15
+ /** Character vocabulary; defaults to the tree's own detected style. */
16
+ style?: 'unicode' | 'ascii';
17
+ }
18
+
19
+ export function renderTree(tree: Tree, options: RenderTreeOptions = {}): string {
20
+ const vocab = (options.style ?? tree.style) === 'ascii' ? ASCII_TREE_VOCAB : UNICODE_TREE_VOCAB;
21
+ const lines: string[] = [];
22
+
23
+ const label = (node: TreeNode): string =>
24
+ node.comment ? `${node.label} # ${node.comment}` : node.label;
25
+
26
+ const walk = (nodes: readonly TreeNode[], prefix: string): void => {
27
+ nodes.forEach((node, i) => {
28
+ const last = i === nodes.length - 1;
29
+ lines.push(prefix + (last ? vocab.elbow : vocab.tee) + label(node));
30
+ walk(node.children, prefix + (last ? vocab.gap : vocab.rail));
31
+ });
32
+ };
33
+
34
+ // Roots sit at column 0 with no connector; their children get branches.
35
+ for (const root of tree.roots) {
36
+ lines.push(label(root));
37
+ walk(root.children, '');
38
+ }
39
+
40
+ return lines.join('\n');
41
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Public types for the ASCII treeview codec.
3
+ *
4
+ * A tree is a hierarchy of labelled nodes (file trees, dependency trees,
5
+ * category outlines). Unlike the diagram codec's spatial grid model, the
6
+ * tree model is a plain nested structure — rendering is a deterministic
7
+ * tree-walk, so the parse↔render fixpoint is structural and trivially
8
+ * byte-stable.
9
+ */
10
+
11
+ export interface TreeNode {
12
+ /**
13
+ * Stable id: slug of the label, deduplicated with `-2`/`-3` suffixes in
14
+ * document (depth-first) order. Identical art re-parses to identical ids
15
+ * (the outline widget's selection resolves by id).
16
+ */
17
+ id: string;
18
+ /** Display label, verbatim (may keep a trailing `/`). */
19
+ label: string;
20
+ /** Child nodes (empty for leaves). */
21
+ children: TreeNode[];
22
+ /** True when this node reads as a container (trailing `/` or has children). */
23
+ isDir?: boolean;
24
+ /** Trailing `# …` / `<-- …` comment, preserved across round-trips. */
25
+ comment?: string;
26
+ }
27
+
28
+ export interface Tree {
29
+ /** Top-level nodes (a forest — usually one root for a file tree). */
30
+ roots: TreeNode[];
31
+ /** Character vocabulary detected in the source; render emits the same. */
32
+ style: 'unicode' | 'ascii';
33
+ /** Non-fatal parse notes (ragged dedents, dropped lines, …). */
34
+ warnings: string[];
35
+ }
36
+
37
+ export interface TreeDetection {
38
+ isTree: boolean;
39
+ /** Present when `isTree` — avoids a second parse in callers. */
40
+ tree?: Tree;
41
+ /** Machine-greppable accept/reject reasons. */
42
+ reasons: string[];
43
+ }
@@ -50,16 +50,9 @@ function isFullBleedCoverMedia(layer: Layer): boolean {
50
50
  return fit === undefined || fit === 'cover';
51
51
  }
52
52
 
53
- /**
54
- * Whether the theme asks for ambient Ken Burns on otherwise-static imagery.
55
- * `defaultImageAnimation: 'slowZoom'` is treated as an alias for the boolean
56
- * (that's what themes authoring it were reaching for).
57
- */
53
+ /** Whether the theme asks for ambient Ken Burns on otherwise-static imagery. */
58
54
  export function themeWantsAmbientMotion(theme: Theme): boolean {
59
- return (
60
- theme.renderStyle.ambientMotion === true ||
61
- theme.renderStyle.defaultImageAnimation === 'slowZoom'
62
- );
55
+ return theme.renderStyle.ambientMotion === true;
63
56
  }
64
57
 
65
58
  /**
@@ -0,0 +1,78 @@
1
+ /** Text fitting shared by the live diagram canvas and template renderer. */
2
+
3
+ export const DIAGRAM_LABEL_LINE_HEIGHT = 1.25;
4
+ export const DIAGRAM_LABEL_HORIZONTAL_PADDING = 24;
5
+ export const DIAGRAM_LABEL_VERTICAL_PADDING = 16;
6
+ export const DIAGRAM_LABEL_MIN_FONT_SIZE = 10;
7
+
8
+ export interface DiagramLabelFit {
9
+ fontSize: number;
10
+ lineCount: number;
11
+ /** Upward shift that centers the full line group around the node midpoint. */
12
+ firstLineOffset: number;
13
+ textWidth: number;
14
+ }
15
+
16
+ /**
17
+ * Fit a plain diagram label inside a node card using the same character-width
18
+ * estimate and word wrapping as React's `TextLayer`.
19
+ */
20
+ export function fitDiagramLabel(
21
+ text: string,
22
+ boxWidth: number,
23
+ boxHeight: number,
24
+ preferredFontSize: number,
25
+ ): DiagramLabelFit {
26
+ const textWidth = Math.max(1, boxWidth - DIAGRAM_LABEL_HORIZONTAL_PADDING);
27
+ const textHeight = Math.max(1, boxHeight - DIAGRAM_LABEL_VERTICAL_PADDING);
28
+ const preferred = Math.max(DIAGRAM_LABEL_MIN_FONT_SIZE, Math.floor(preferredFontSize));
29
+
30
+ let fontSize = preferred;
31
+ let lineCount = countWrappedLines(text, fontSize, textWidth);
32
+ while (
33
+ fontSize > DIAGRAM_LABEL_MIN_FONT_SIZE &&
34
+ lineCount * fontSize * DIAGRAM_LABEL_LINE_HEIGHT > textHeight
35
+ ) {
36
+ fontSize--;
37
+ lineCount = countWrappedLines(text, fontSize, textWidth);
38
+ }
39
+
40
+ return {
41
+ fontSize,
42
+ lineCount,
43
+ firstLineOffset:
44
+ lineCount === 1 ? 0 : -((lineCount - 1) * fontSize * DIAGRAM_LABEL_LINE_HEIGHT) / 2,
45
+ textWidth,
46
+ };
47
+ }
48
+
49
+ function countWrappedLines(text: string, fontSize: number, maxWidth: number): number {
50
+ return text
51
+ .split('\n')
52
+ .reduce((count, line) => count + wrappedLineCount(line, fontSize, maxWidth), 0);
53
+ }
54
+
55
+ function wrappedLineCount(text: string, fontSize: number, maxWidth: number): number {
56
+ if (!text.trim()) return 1;
57
+ const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
58
+ if (charsPerLine <= 0) return 1;
59
+
60
+ let lines = 0;
61
+ let currentLength = 0;
62
+ for (const word of text.split(/\s+/)) {
63
+ const testLength = currentLength > 0 ? currentLength + 1 + word.length : word.length;
64
+ if (testLength <= charsPerLine) {
65
+ currentLength = testLength;
66
+ continue;
67
+ }
68
+ if (currentLength > 0) lines++;
69
+ if (word.length > charsPerLine) {
70
+ lines += Math.floor((word.length - 1) / charsPerLine);
71
+ currentLength = word.length % charsPerLine || charsPerLine;
72
+ } else {
73
+ currentLength = word.length;
74
+ }
75
+ }
76
+ if (currentLength > 0) lines++;
77
+ return Math.max(1, lines);
78
+ }
@@ -429,6 +429,11 @@ export interface ConnectorSnapPoint {
429
429
  y: number;
430
430
  }
431
431
 
432
+ export interface ConnectorAnchor {
433
+ side: 'top' | 'right' | 'bottom' | 'left';
434
+ offset: number;
435
+ }
436
+
432
437
  /** Intersection of the center-to-center line with `from`'s bounding box. */
433
438
  export function clipPoint(from: ClipBox, to: ClipBox): { x: number; y: number } {
434
439
  const dx = to.cx - from.cx;
@@ -466,6 +471,25 @@ export function snapPoints(box: ClipBox): ConnectorSnapPoint[] {
466
471
  ];
467
472
  }
468
473
 
474
+ /** Resolve a normalized authored anchor to a concrete point on a box. */
475
+ export function anchorPoint(box: ClipBox, anchor: ConnectorAnchor): ConnectorSnapPoint {
476
+ const offset = Math.max(0, Math.min(1, anchor.offset));
477
+ const left = box.cx - box.rx;
478
+ const right = box.cx + box.rx;
479
+ const top = box.cy - box.ry;
480
+ const bottom = box.cy + box.ry;
481
+ switch (anchor.side) {
482
+ case 'top':
483
+ return { port: 'top', x: left + (right - left) * offset, y: top };
484
+ case 'right':
485
+ return { port: 'right', x: right, y: top + (bottom - top) * offset };
486
+ case 'bottom':
487
+ return { port: 'bottom', x: left + (right - left) * offset, y: bottom };
488
+ case 'left':
489
+ return { port: 'left', x: left, y: top + (bottom - top) * offset };
490
+ }
491
+ }
492
+
469
493
  /** Nearest stable connector port on `box` to an arbitrary point. */
470
494
  export function nearestSnapPoint(
471
495
  box: ClipBox,
@@ -521,20 +545,58 @@ function portTieBreak(port: ConnectorPort): number {
521
545
  /** Build the SVG `d` for a connector between two clipped endpoints. */
522
546
  export function connectorPath(
523
547
  routing: ConnectorRouting,
524
- start: { x: number; y: number },
525
- end: { x: number; y: number },
548
+ start: { x: number; y: number; port?: ConnectorPort },
549
+ end: { x: number; y: number; port?: ConnectorPort },
526
550
  ): string {
527
551
  if (routing === 'straight') {
528
552
  return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
529
553
  }
530
554
  if (routing === 'orthogonal') {
555
+ const startAxis = portAxis(start.port);
556
+ const endAxis = portAxis(end.port);
557
+ if (startAxis === 'vertical' && endAxis === 'vertical') {
558
+ const midY = (start.y + end.y) / 2;
559
+ return `M ${r(start.x)} ${r(start.y)} L ${r(start.x)} ${r(midY)} L ${r(end.x)} ${r(midY)} L ${r(end.x)} ${r(end.y)}`;
560
+ }
561
+ if (startAxis === 'vertical' && endAxis === 'horizontal') {
562
+ return `M ${r(start.x)} ${r(start.y)} L ${r(start.x)} ${r(end.y)} L ${r(end.x)} ${r(end.y)}`;
563
+ }
564
+ if (startAxis === 'horizontal' && endAxis === 'vertical') {
565
+ return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
566
+ }
531
567
  const midX = (start.x + end.x) / 2;
532
568
  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)}`;
533
569
  }
534
570
  // curved
535
- const dx = Math.abs(end.x - start.x);
536
- const cp = Math.max(40, dx / 2);
537
- 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)}`;
571
+ const distance = Math.hypot(end.x - start.x, end.y - start.y);
572
+ const cp = Math.max(40, distance / 2);
573
+ const startVector = portVector(start.port, { x: 1, y: 0 });
574
+ const endVector = portVector(end.port, { x: -1, y: 0 });
575
+ return `M ${r(start.x)} ${r(start.y)} C ${r(start.x + startVector.x * cp)} ${r(start.y + startVector.y * cp)}, ${r(end.x + endVector.x * cp)} ${r(end.y + endVector.y * cp)}, ${r(end.x)} ${r(end.y)}`;
576
+ }
577
+
578
+ function portAxis(port: ConnectorPort | undefined): 'horizontal' | 'vertical' | null {
579
+ if (port === 'top' || port === 'bottom') return 'vertical';
580
+ if (port === 'left' || port === 'right') return 'horizontal';
581
+ return null;
582
+ }
583
+
584
+ function portVector(
585
+ port: ConnectorPort | undefined,
586
+ fallback: { x: number; y: number },
587
+ ): { x: number; y: number } {
588
+ switch (port) {
589
+ case 'top':
590
+ return { x: 0, y: -1 };
591
+ case 'right':
592
+ return { x: 1, y: 0 };
593
+ case 'bottom':
594
+ return { x: 0, y: 1 };
595
+ case 'left':
596
+ return { x: -1, y: 0 };
597
+ default:
598
+ return fallback;
599
+ }
538
600
  }
539
601
 
540
602
  /** Map a `lineStyle` (solid|dashed|dotted) to an SVG `stroke-dasharray`. */
@@ -36,12 +36,12 @@ import { deriveTemplateInputs } from './templateInputs.js';
36
36
  import { lintTemplateParams } from './templates/inputDescriptors.js';
37
37
  import {
38
38
  templateRegistry,
39
- TEMPLATE_ALIASES,
40
39
  resolveTemplateName,
41
40
  isShapeName,
42
41
  normalizeShapeKind,
43
42
  SHAPE_NAMES,
44
43
  } from './templates/index.js';
44
+ import { TEMPLATE_ALIASES } from './templates/templateNames.js';
45
45
  import { nearestName } from './utils/nearest.js';
46
46
 
47
47
  // ============================================
@@ -38,8 +38,13 @@ export interface MapOptions {
38
38
  audioSegment: number;
39
39
  colorScheme?: ColorScheme;
40
40
  accentImage?: AccentImage;
41
+ /** Doc-timeline SECONDS when the extracted content is spoken. */
41
42
  sourceStartTime?: number;
42
43
  sourceDuration?: number;
44
+ /** Provenance: the source block the element was extracted from. */
45
+ sourceBlockId?: string;
46
+ /** Provenance: char offset of the extraction in the source plain text. */
47
+ sourceCharOffset?: number;
43
48
  }
44
49
 
45
50
  /**
@@ -52,6 +57,8 @@ export function mapElementToBlock(element: ExtractedElement, options: MapOptions
52
57
  audioSegment: options.audioSegment,
53
58
  sourceStartTime: options.sourceStartTime,
54
59
  sourceDuration: options.sourceDuration,
60
+ sourceBlockId: options.sourceBlockId,
61
+ sourceCharOffset: options.sourceCharOffset,
55
62
  };
56
63
 
57
64
  switch (element.data.type) {
@@ -203,9 +203,10 @@ function renderPathToSvg(
203
203
  const fill = c.fill ?? 'none';
204
204
  const dash = c.dasharray ? ` stroke-dasharray="${escapeAttr(c.dasharray)}"` : '';
205
205
 
206
- // Markers: explicit start/end win; otherwise derive from the legacy `arrow` flag.
207
- const startStyle = effectiveExportMarker(c.startMarker, c.arrow, 'start');
208
- const endStyle = effectiveExportMarker(c.endMarker, c.arrow, 'end');
206
+ // Keep old serialized documents readable without retaining `arrow` in the public type.
207
+ const legacyArrow = (c as typeof c & { arrow?: 'none' | 'end' | 'start' | 'both' }).arrow;
208
+ const startStyle = effectiveExportMarker(c.startMarker, legacyArrow, 'start');
209
+ const endStyle = effectiveExportMarker(c.endMarker, legacyArrow, 'end');
209
210
  const start = markerPath(startStyle, 'start');
210
211
  const end = markerPath(endStyle, 'end');
211
212
  const startId = `marker-start-${layer.id}`;
@@ -228,11 +229,11 @@ function renderPathToSvg(
228
229
 
229
230
  function effectiveExportMarker(
230
231
  explicit: MarkerStyle | undefined,
231
- arrow: (ImageEditLayer & { type: 'path' })['content']['arrow'],
232
+ legacyArrow: 'none' | 'end' | 'start' | 'both' | undefined,
232
233
  end: 'start' | 'end',
233
234
  ): MarkerStyle {
234
235
  if (explicit) return explicit;
235
- return arrow === 'both' || arrow === end ? 'arrow' : 'none';
236
+ return legacyArrow === 'both' || legacyArrow === end ? 'arrow' : 'none';
236
237
  }
237
238
 
238
239
  function markerDefSvg(
@@ -10,7 +10,11 @@
10
10
  * `.md` so the two histories live side-by-side without colliding.
11
11
  */
12
12
 
13
- import { formatVersionTimestamp, parseVersionTimestamp } from '../versions/timestamp.js';
13
+ import {
14
+ buildSnapshotPath,
15
+ parseSnapshotPath,
16
+ type SnapshotPathStrategy,
17
+ } from '../versions/snapshotStore.js';
14
18
 
15
19
  /** Subfolder (inside the sidecar) that holds image-editor snapshots. */
16
20
  export const IMAGE_EDIT_VERSIONS_PREFIX = '.versions/';
@@ -18,11 +22,15 @@ export const IMAGE_EDIT_VERSIONS_PREFIX = '.versions/';
18
22
  /** Default basename when none is supplied. Matches `IMAGE_EDIT_STATE_FILENAME` stem. */
19
23
  export const IMAGE_EDIT_DEFAULT_BASENAME = 'state';
20
24
 
25
+ /** Internal strategy shared with the generic snapshot store. */
26
+ export const IMAGE_EDIT_VERSION_PATHS: SnapshotPathStrategy = Object.freeze({
27
+ prefix: IMAGE_EDIT_VERSIONS_PREFIX,
28
+ extension: 'json',
29
+ });
30
+
21
31
  /** Build a snapshot path. Optionally append a collision suffix. */
22
32
  export function buildImageEditVersionPath(basename: string, date: Date, collision = 0): string {
23
- const stamp = formatVersionTimestamp(date);
24
- const suffix = collision > 0 ? `-${collision + 1}` : '';
25
- return `${IMAGE_EDIT_VERSIONS_PREFIX}${basename}.${stamp}${suffix}.json`;
33
+ return buildSnapshotPath(IMAGE_EDIT_VERSION_PATHS, basename, date, collision);
26
34
  }
27
35
 
28
36
  /**
@@ -32,26 +40,5 @@ export function buildImageEditVersionPath(basename: string, date: Date, collisio
32
40
  export function parseImageEditVersionPath(
33
41
  path: string,
34
42
  ): { basename: string; timestamp: Date; collision: number } | null {
35
- if (!path.startsWith(IMAGE_EDIT_VERSIONS_PREFIX)) return null;
36
- const rest = path.slice(IMAGE_EDIT_VERSIONS_PREFIX.length);
37
- if (!rest.endsWith('.json')) return null;
38
- const stem = rest.slice(0, -'.json'.length);
39
- const lastDot = stem.lastIndexOf('.');
40
- if (lastDot <= 0) return null;
41
- const basename = stem.slice(0, lastDot);
42
- const tail = stem.slice(lastDot + 1);
43
- const dash = tail.indexOf('-');
44
- let stamp: string;
45
- let collision = 0;
46
- if (dash >= 0) {
47
- stamp = tail.slice(0, dash);
48
- const n = Number(tail.slice(dash + 1));
49
- if (!Number.isInteger(n) || n < 2) return null;
50
- collision = n - 1;
51
- } else {
52
- stamp = tail;
53
- }
54
- const timestamp = parseVersionTimestamp(stamp);
55
- if (!timestamp) return null;
56
- return { basename, timestamp, collision };
43
+ return parseSnapshotPath(IMAGE_EDIT_VERSION_PATHS, path);
57
44
  }
@@ -5,23 +5,24 @@
5
5
  * Mirrors `core/src/versions/operations.ts` but operates on
6
6
  * {@link ImageEditDoc} JSON state at `.versions/<basename>.<timestamp>.json`
7
7
  * instead of the markdown document. Reuses {@link PrunePolicy} so hosts
8
- * can share their pruning configuration between the two histories.
8
+ * can share their pruning configuration between the two histories. The
9
+ * sidecar root gets a `.gitignore` rule for `.versions/` when snapshots
10
+ * are retained.
9
11
  */
10
12
 
11
13
  import type { ContentContainer } from '../storage/ContentContainer.js';
12
14
  import type { ImageEditDoc } from '../schemas/ImageEditDoc.js';
13
15
  import type { CoalesceOptions, PrunePolicy, Version } from '../versions/types.js';
16
+ import {
17
+ coalesceSnapshots,
18
+ listSnapshots,
19
+ pruneSnapshots,
20
+ readTextSnapshot,
21
+ saveTextSnapshot,
22
+ } from '../versions/snapshotStore.js';
14
23
  import { readImageEditDoc, writeImageEditDoc } from './persistence.js';
15
24
  import { IMAGE_EDIT_STATE_FILENAME } from './state.js';
16
- import {
17
- IMAGE_EDIT_VERSIONS_PREFIX,
18
- IMAGE_EDIT_DEFAULT_BASENAME,
19
- buildImageEditVersionPath,
20
- parseImageEditVersionPath,
21
- } from './versionPaths.js';
22
-
23
- const decoder = new TextDecoder();
24
- const encoder = new TextEncoder();
25
+ import { IMAGE_EDIT_DEFAULT_BASENAME, IMAGE_EDIT_VERSION_PATHS } from './versionPaths.js';
25
26
 
26
27
  /** Options for {@link saveImageEditVersion}. */
27
28
  export interface SaveImageEditVersionOptions {
@@ -60,26 +61,7 @@ export async function listImageEditVersions(
60
61
  container: ContentContainer,
61
62
  basename?: string,
62
63
  ): Promise<Version[]> {
63
- const entries = await container.listFiles(IMAGE_EDIT_VERSIONS_PREFIX);
64
- const versions: Version[] = [];
65
- for (const entry of entries) {
66
- const parsed = parseImageEditVersionPath(entry.path);
67
- if (!parsed) continue;
68
- if (basename !== undefined && parsed.basename !== basename) continue;
69
- versions.push({
70
- path: entry.path,
71
- basename: parsed.basename,
72
- timestamp: parsed.timestamp,
73
- size: entry.size,
74
- collision: parsed.collision,
75
- });
76
- }
77
- versions.sort((a, b) => {
78
- const dt = b.timestamp.getTime() - a.timestamp.getTime();
79
- if (dt !== 0) return dt;
80
- return b.collision - a.collision;
81
- });
82
- return versions;
64
+ return listSnapshots(container, IMAGE_EDIT_VERSION_PATHS, basename);
83
65
  }
84
66
 
85
67
  /** Read a snapshot's JSON text. Returns `null` if the snapshot is missing. */
@@ -87,10 +69,7 @@ export async function readImageEditVersionText(
87
69
  container: ContentContainer,
88
70
  version: Version | string,
89
71
  ): Promise<string | null> {
90
- const path = typeof version === 'string' ? version : version.path;
91
- const data = await container.readFile(path);
92
- if (!data) return null;
93
- return decoder.decode(data);
72
+ return readTextSnapshot(container, version);
94
73
  }
95
74
 
96
75
  /** Read and parse a snapshot. Returns `null` if missing. */
@@ -124,34 +103,13 @@ export async function saveImageEditVersion(
124
103
  const basename = options.basename ?? IMAGE_EDIT_DEFAULT_BASENAME;
125
104
  const serialized = JSON.stringify(doc, null, 2);
126
105
 
127
- const versions = await listImageEditVersions(container, basename);
128
- if (!options.force && versions.length > 0) {
129
- const latest = versions[0]!;
130
- const existing = await readImageEditVersionText(container, latest);
131
- if (existing === serialized) {
132
- return { saved: false, version: null, reason: 'unchanged' };
133
- }
134
- }
135
-
136
- const now = options.now ?? new Date();
137
- let collision = 0;
138
- let path = buildImageEditVersionPath(basename, now, collision);
139
- while (await container.exists(path)) {
140
- collision += 1;
141
- path = buildImageEditVersionPath(basename, now, collision);
142
- }
143
-
144
- const data = encoder.encode(serialized);
145
- await container.writeFile(path, data, 'application/json');
146
-
147
- const version: Version = {
148
- path,
106
+ return saveTextSnapshot(container, IMAGE_EDIT_VERSION_PATHS, {
107
+ content: serialized,
149
108
  basename,
150
- timestamp: now,
151
- size: data.byteLength,
152
- collision,
153
- };
154
- return { saved: true, version, reason: 'saved' };
109
+ mimeType: 'application/json',
110
+ now: options.now,
111
+ force: options.force,
112
+ });
155
113
  }
156
114
 
157
115
  /** Revert `state.json` to a prior snapshot. Snapshots the current state first by default. */
@@ -181,27 +139,7 @@ export async function pruneImageEditVersions(
181
139
  policy: PrunePolicy,
182
140
  basename?: string,
183
141
  ): Promise<Version[]> {
184
- const versions = await listImageEditVersions(container, basename);
185
- const toDelete: Version[] = [];
186
-
187
- if (policy.type === 'keep-last-n') {
188
- const n = Math.max(0, Math.floor(policy.n));
189
- toDelete.push(...versions.slice(n));
190
- } else if (policy.type === 'older-than') {
191
- const cutoff = policy.date.getTime();
192
- for (const v of versions) {
193
- if (v.timestamp.getTime() < cutoff) toDelete.push(v);
194
- }
195
- } else {
196
- for (const v of versions) {
197
- if (!policy.keep(v, versions)) toDelete.push(v);
198
- }
199
- }
200
-
201
- for (const v of toDelete) {
202
- await container.removeFile(v.path);
203
- }
204
- return toDelete;
142
+ return pruneSnapshots(container, IMAGE_EDIT_VERSION_PATHS, policy, basename);
205
143
  }
206
144
 
207
145
  /** Collapse adjacent snapshots within `windowMs`, keeping the newer of each pair. */
@@ -210,22 +148,5 @@ export async function coalesceImageEditVersions(
210
148
  options: CoalesceOptions = {},
211
149
  basename?: string,
212
150
  ): Promise<Version[]> {
213
- const windowMs = options.windowMs ?? 60_000;
214
- const versions = await listImageEditVersions(container, basename);
215
- const toDelete: Version[] = [];
216
- if (versions.length > 0) {
217
- let anchor = versions[0]!;
218
- for (let i = 1; i < versions.length; i++) {
219
- const candidate = versions[i]!;
220
- if (anchor.timestamp.getTime() - candidate.timestamp.getTime() <= windowMs) {
221
- toDelete.push(candidate);
222
- } else {
223
- anchor = candidate;
224
- }
225
- }
226
- }
227
- for (const v of toDelete) {
228
- await container.removeFile(v.path);
229
- }
230
- return toDelete;
151
+ return coalesceSnapshots(container, IMAGE_EDIT_VERSION_PATHS, options, basename);
231
152
  }
package/src/index.ts CHANGED
@@ -12,3 +12,4 @@ export * from './jsonForm/index.js';
12
12
  export * from './imageEdit/index.js';
13
13
  export * from './icons/index.js';
14
14
  export * from './recommend/index.js';
15
+ export * from './narration/index.js';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Clone JSON-shaped runtime configuration without relying on structuredClone
3
+ * (which is unavailable in some of the ES2020/browser runtimes core supports).
4
+ * Data objects are recreated with safe own-property definitions so even a key
5
+ * named `__proto__` remains ordinary data.
6
+ */
7
+ export function cloneData<T>(value: T, seen: WeakMap<object, unknown> = new WeakMap()): T {
8
+ if (value === null || typeof value !== 'object') return value;
9
+
10
+ const existing = seen.get(value);
11
+ if (existing !== undefined) return existing as T;
12
+
13
+ const tag = Object.prototype.toString.call(value);
14
+ if (tag === '[object ArrayBuffer]') {
15
+ const source = new Uint8Array(value as unknown as ArrayBuffer);
16
+ const copy = new Uint8Array(source.byteLength);
17
+ copy.set(source);
18
+ return copy.buffer as T;
19
+ }
20
+ if (tag === '[object Date]') {
21
+ return new Date(Date.prototype.getTime.call(value)) as T;
22
+ }
23
+
24
+ if (Array.isArray(value)) {
25
+ const copy: unknown[] = [];
26
+ seen.set(value, copy);
27
+ for (const item of value) copy.push(cloneData(item, seen));
28
+ return copy as T;
29
+ }
30
+
31
+ const source = value as Record<string, unknown>;
32
+ const copy: Record<string, unknown> = {};
33
+ seen.set(value, copy);
34
+ for (const key of Object.keys(source)) {
35
+ Object.defineProperty(copy, key, {
36
+ value: cloneData(source[key], seen),
37
+ enumerable: true,
38
+ configurable: true,
39
+ writable: true,
40
+ });
41
+ }
42
+ return copy as T;
43
+ }
44
+
45
+ /** Recursively freeze a data graph. Cycles are supported. */
46
+ export function deepFreezeData<T>(value: T, seen: WeakSet<object> = new WeakSet()): T {
47
+ if (value === null || typeof value !== 'object' || seen.has(value)) return value;
48
+ seen.add(value);
49
+ for (const key of Object.keys(value)) {
50
+ deepFreezeData((value as Record<string, unknown>)[key], seen);
51
+ }
52
+ return Object.freeze(value);
53
+ }
54
+
55
+ /** Take an immutable snapshot suitable for storing in a process registry. */
56
+ export function cloneAndFreezeData<T>(value: T): T {
57
+ return deepFreezeData(cloneData(value));
58
+ }
@@ -17,7 +17,15 @@ export type {
17
17
 
18
18
  export { chooseControl, primaryType, arrayItemKind } from './chooseControl.js';
19
19
  export { evaluateWhen, resolveFlag } from './evaluateWhen.js';
20
- export { toPointer, pointerSegments, getByPointer, setByPointer, resolveRef } from './pointer.js';
20
+ export {
21
+ escapePointerSegment,
22
+ appendPointer,
23
+ toPointer,
24
+ pointerSegments,
25
+ getByPointer,
26
+ setByPointer,
27
+ resolveRef,
28
+ } from './pointer.js';
21
29
  export { inferSchema } from './inferSchema.js';
22
30
  export type { InferSchemaOptions } from './inferSchema.js';
23
31
  export { buildJsonFormTokens, resolveJsonFormTheme } from './tokens.js';