@bendyline/squisq 1.5.2 → 2.0.1

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 (268) 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-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
  6. package/dist/chunk-2PWOO4ZE.js.map +1 -0
  7. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  8. package/dist/chunk-3E5F2XMR.js.map +1 -0
  9. package/dist/chunk-4X3JQXNM.js +35 -0
  10. package/dist/chunk-4X3JQXNM.js.map +1 -0
  11. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  12. package/dist/chunk-5DKBFXPY.js.map +1 -0
  13. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  14. package/dist/chunk-5J4HKRJF.js.map +1 -0
  15. package/dist/chunk-5UYI3RJB.js +2503 -0
  16. package/dist/chunk-5UYI3RJB.js.map +1 -0
  17. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  18. package/dist/chunk-66QO7XFB.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-CY2R2VPR.js +884 -0
  22. package/dist/chunk-CY2R2VPR.js.map +1 -0
  23. package/dist/chunk-FVJUUTEM.js +74 -0
  24. package/dist/chunk-FVJUUTEM.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-MMPBOUK7.js} +10 -73
  30. package/dist/chunk-MMPBOUK7.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-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  36. package/dist/chunk-R2GC6RLD.js.map +1 -0
  37. package/dist/chunk-R2YZTCLO.js +292 -0
  38. package/dist/chunk-R2YZTCLO.js.map +1 -0
  39. package/dist/chunk-S7A5D7ZJ.js +174 -0
  40. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  41. package/dist/chunk-TFJVXUHH.js +2358 -0
  42. package/dist/chunk-TFJVXUHH.js.map +1 -0
  43. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  44. package/dist/chunk-TUWMK62B.js.map +1 -0
  45. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  46. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  47. package/dist/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
  48. package/dist/chunk-W4O2TX5I.js.map +1 -0
  49. package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
  50. package/dist/chunk-X6DUONGT.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__/customThemesFrontmatter.test.ts +1 -0
  98. package/src/__tests__/dataTable.test.ts +25 -15
  99. package/src/__tests__/diagramBlock.test.ts +3 -3
  100. package/src/__tests__/diagramText.test.ts +26 -0
  101. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  102. package/src/__tests__/drawingBlock.test.ts +3 -3
  103. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  104. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  105. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  106. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  107. package/src/__tests__/getLayers.test.ts +32 -18
  108. package/src/__tests__/getLayersFallback.test.ts +8 -8
  109. package/src/__tests__/imageEditVersions.test.ts +2 -0
  110. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  111. package/src/__tests__/layoutBlock.test.ts +3 -3
  112. package/src/__tests__/majorApiTypes.test.ts +18 -0
  113. package/src/__tests__/markdown.test.ts +54 -4
  114. package/src/__tests__/markdownSanitize.test.ts +7 -5
  115. package/src/__tests__/matchFontFamily.test.ts +114 -0
  116. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  117. package/src/__tests__/materializeTestUtils.ts +17 -0
  118. package/src/__tests__/narrationAlign.test.ts +150 -0
  119. package/src/__tests__/narrationFeatures.test.ts +71 -0
  120. package/src/__tests__/narrationNuclei.test.ts +45 -0
  121. package/src/__tests__/narrationPacing.test.ts +189 -0
  122. package/src/__tests__/narrationScript.test.ts +149 -0
  123. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  124. package/src/__tests__/narrationSidecar.test.ts +88 -0
  125. package/src/__tests__/narrationSyllables.test.ts +65 -0
  126. package/src/__tests__/narrationTestSignals.ts +167 -0
  127. package/src/__tests__/narrationVad.test.ts +66 -0
  128. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  129. package/src/__tests__/rootExports.test.ts +18 -0
  130. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  131. package/src/__tests__/shapeGeometry.test.ts +12 -0
  132. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  133. package/src/__tests__/templateInputs.test.ts +69 -0
  134. package/src/__tests__/templateMetadata.test.ts +2 -1
  135. package/src/__tests__/templates.test.ts +143 -17
  136. package/src/__tests__/themeCompile.test.ts +49 -22
  137. package/src/__tests__/transformProvenance.test.ts +160 -0
  138. package/src/__tests__/transformV2.test.ts +40 -14
  139. package/src/__tests__/treeviewBattle.test.ts +76 -0
  140. package/src/__tests__/treeviewDetect.test.ts +168 -0
  141. package/src/__tests__/treeviewParse.test.ts +118 -0
  142. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  143. package/src/__tests__/treeviewRender.test.ts +91 -0
  144. package/src/__tests__/versions.test.ts +44 -0
  145. package/src/doc/applyNarrationTiming.ts +244 -0
  146. package/src/doc/asciiDiagram/chars.ts +210 -0
  147. package/src/doc/asciiDiagram/detect.ts +175 -0
  148. package/src/doc/asciiDiagram/index.ts +38 -0
  149. package/src/doc/asciiDiagram/mapping.ts +225 -0
  150. package/src/doc/asciiDiagram/parse.ts +1095 -0
  151. package/src/doc/asciiDiagram/render.ts +963 -0
  152. package/src/doc/asciiDiagram/repair.ts +400 -0
  153. package/src/doc/asciiDiagram/types.ts +65 -0
  154. package/src/doc/asciiTimeline/detect.ts +138 -0
  155. package/src/doc/asciiTimeline/index.ts +26 -0
  156. package/src/doc/asciiTimeline/mapping.ts +43 -0
  157. package/src/doc/asciiTimeline/parse.ts +870 -0
  158. package/src/doc/asciiTimeline/render.ts +427 -0
  159. package/src/doc/asciiTimeline/types.ts +73 -0
  160. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  161. package/src/doc/audioMapping.ts +60 -46
  162. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  163. package/src/doc/docToMarkdown.ts +70 -2
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +138 -2
  166. package/src/doc/materializeBlockLayers.ts +304 -0
  167. package/src/doc/mediaAnnotations.ts +20 -0
  168. package/src/doc/resolveDocTheme.ts +8 -7
  169. package/src/doc/templateInputs.ts +185 -2
  170. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  171. package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/fallbackBlock.ts +1 -1
  174. package/src/doc/templates/index.ts +134 -279
  175. package/src/doc/templates/inputDescriptors.ts +20 -1
  176. package/src/doc/templates/listBlock.ts +88 -30
  177. package/src/doc/templates/metadata.ts +10 -0
  178. package/src/doc/templates/persistentLayers.ts +12 -126
  179. package/src/doc/templates/registry.ts +88 -0
  180. package/src/doc/templates/templateNames.ts +22 -0
  181. package/src/doc/templates/timelineBlock.ts +613 -0
  182. package/src/doc/templates/treeBlock.ts +131 -0
  183. package/src/doc/treeview/chars.ts +72 -0
  184. package/src/doc/treeview/detect.ts +145 -0
  185. package/src/doc/treeview/index.ts +30 -0
  186. package/src/doc/treeview/mapping.ts +95 -0
  187. package/src/doc/treeview/parse.ts +201 -0
  188. package/src/doc/treeview/render.ts +41 -0
  189. package/src/doc/treeview/types.ts +43 -0
  190. package/src/doc/utils/applyRenderStyle.ts +2 -9
  191. package/src/doc/utils/diagramText.ts +78 -0
  192. package/src/doc/utils/shapeGeometry.ts +67 -5
  193. package/src/doc/validate.ts +1 -1
  194. package/src/generate/templateMapper.ts +7 -0
  195. package/src/imageEdit/export.ts +6 -5
  196. package/src/imageEdit/versionPaths.ts +13 -26
  197. package/src/imageEdit/versions.ts +21 -100
  198. package/src/index.ts +1 -0
  199. package/src/internal/immutable.ts +58 -0
  200. package/src/jsonForm/index.ts +9 -1
  201. package/src/jsonForm/pointer.ts +68 -23
  202. package/src/markdown/attrTokens.ts +7 -5
  203. package/src/markdown/index.ts +1 -0
  204. package/src/markdown/stringify.ts +4 -9
  205. package/src/markdown/utils.ts +36 -14
  206. package/src/narration/align.ts +514 -0
  207. package/src/narration/features.ts +231 -0
  208. package/src/narration/index.ts +86 -0
  209. package/src/narration/nuclei.ts +118 -0
  210. package/src/narration/pacing.ts +211 -0
  211. package/src/narration/script.ts +239 -0
  212. package/src/narration/session.ts +100 -0
  213. package/src/narration/sidecar.ts +187 -0
  214. package/src/narration/syllables.ts +85 -0
  215. package/src/narration/trace.ts +50 -0
  216. package/src/narration/types.ts +299 -0
  217. package/src/narration/vad.ts +131 -0
  218. package/src/recommend/numberHighlight.ts +25 -0
  219. package/src/recommend/templates.ts +108 -12
  220. package/src/schemas/BlockTemplates.ts +156 -7
  221. package/src/schemas/Doc.ts +72 -9
  222. package/src/schemas/Media.ts +12 -2
  223. package/src/schemas/Theme.ts +45 -25
  224. package/src/schemas/Viewport.ts +2 -2
  225. package/src/schemas/fontStacks.ts +79 -0
  226. package/src/schemas/themeCompile.ts +15 -0
  227. package/src/schemas/themeConstants.ts +2 -0
  228. package/src/schemas/themeLibrary.ts +25 -19
  229. package/src/schemas/themeValidator.ts +1 -1
  230. package/src/schemas/themes/gezellig.json +1 -1
  231. package/src/spatial/Geohash.ts +0 -3
  232. package/src/storage/ContentContainer.ts +17 -9
  233. package/src/storage/ScopedContentContainer.ts +45 -15
  234. package/src/transform/applyTransform.ts +5 -5
  235. package/src/transform/index.ts +3 -1
  236. package/src/transform/registry.ts +169 -49
  237. package/src/transform/templateSelector.ts +21 -2
  238. package/src/transform/timingAllocator.ts +152 -21
  239. package/src/transform/types.ts +14 -1
  240. package/src/versions/gitignore.ts +34 -0
  241. package/src/versions/index.ts +2 -0
  242. package/src/versions/operations.ts +18 -104
  243. package/src/versions/paths.ts +13 -28
  244. package/src/versions/snapshotStore.ts +202 -0
  245. package/src/versions/types.ts +3 -1
  246. package/dist/chunk-5TYS5V3G.js.map +0 -1
  247. package/dist/chunk-A4O7GIWE.js.map +0 -1
  248. package/dist/chunk-BEQODJWV.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-GPLTCSXO.js.map +0 -1
  251. package/dist/chunk-HWVFJAAH.js.map +0 -1
  252. package/dist/chunk-IMSCRLLV.js.map +0 -1
  253. package/dist/chunk-JELIRVDP.js +0 -24
  254. package/dist/chunk-JELIRVDP.js.map +0 -1
  255. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  256. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  257. package/dist/chunk-LTN6I7QW.js.map +0 -1
  258. package/dist/chunk-P3K7NLHF.js.map +0 -1
  259. package/dist/chunk-T5UK6YOB.js.map +0 -1
  260. package/dist/chunk-UY7KGQ5R.js +0 -192
  261. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  262. package/dist/chunk-VLJJHDAC.js +0 -218
  263. package/dist/chunk-VLJJHDAC.js.map +0 -1
  264. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  265. package/dist/story/index.d.ts +0 -1801
  266. package/dist/story/index.js +0 -279
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -6,27 +6,67 @@
6
6
 
7
7
  import type { SquisqAnnotatedSchema } from './types.js';
8
8
 
9
+ /** Escape one object-member name for use as an RFC 6901 pointer segment. */
10
+ export function escapePointerSegment(segment: string): string {
11
+ return segment.replace(/~/g, '~0').replace(/\//g, '~1');
12
+ }
13
+
9
14
  /** Normalize a path to a JSON Pointer string. Accepts both forms. */
10
15
  export function toPointer(path: string): string {
11
- if (path === '' || path === '/') return '';
16
+ if (path === '') return '';
12
17
  if (path.startsWith('/')) return path;
13
18
  // Dotted form: split on `.` and escape per RFC 6901.
14
- return (
15
- '/' +
16
- path
17
- .split('.')
18
- .map((seg) => seg.replace(/~/g, '~0').replace(/\//g, '~1'))
19
- .join('/')
20
- );
19
+ return '/' + path.split('.').map(escapePointerSegment).join('/');
20
+ }
21
+
22
+ /** Append one member name to a dotted path or validated JSON Pointer. */
23
+ export function appendPointer(base: string, segment: string): string {
24
+ const pointer = toPointer(base);
25
+ pointerSegments(pointer); // validate explicit pointer syntax before composing
26
+ return `${pointer}/${escapePointerSegment(segment)}`;
21
27
  }
22
28
 
23
29
  /** Split a JSON Pointer into decoded segments. Empty string → []. */
24
30
  export function pointerSegments(pointer: string): string[] {
25
31
  if (pointer === '') return [];
32
+ if (!pointer.startsWith('/')) {
33
+ throw new TypeError(`Invalid JSON Pointer ${JSON.stringify(pointer)}: expected "" or "/..."`);
34
+ }
26
35
  return pointer
27
36
  .slice(1)
28
37
  .split('/')
29
- .map((seg) => seg.replace(/~1/g, '/').replace(/~0/g, '~'));
38
+ .map((seg) => {
39
+ if (/~(?:[^01]|$)/.test(seg)) {
40
+ throw new TypeError(
41
+ `Invalid JSON Pointer ${JSON.stringify(pointer)}: "~" must be escaped as "~0"`,
42
+ );
43
+ }
44
+ return seg.replace(/~1/g, '/').replace(/~0/g, '~');
45
+ });
46
+ }
47
+
48
+ /** Parse the canonical array-index spelling used by JSON Pointer consumers. */
49
+ function arrayIndex(segment: string): number | undefined {
50
+ if (!/^(?:0|[1-9]\d*)$/.test(segment)) return undefined;
51
+ const index = Number(segment);
52
+ // 2^32 - 1 is the Array length sentinel, not an element index.
53
+ return Number.isSafeInteger(index) && index <= 0xffff_fffe ? index : undefined;
54
+ }
55
+
56
+ function ownValue(object: object, key: string): unknown {
57
+ return Object.prototype.hasOwnProperty.call(object, key)
58
+ ? (object as Record<string, unknown>)[key]
59
+ : undefined;
60
+ }
61
+
62
+ /** Define a data member without invoking Object.prototype.__proto__'s setter. */
63
+ function setOwn(object: Record<string, unknown>, key: string, value: unknown): void {
64
+ Object.defineProperty(object, key, {
65
+ value,
66
+ enumerable: true,
67
+ configurable: true,
68
+ writable: true,
69
+ });
30
70
  }
31
71
 
32
72
  /** Read a value from data by path (dotted or pointer). Returns undefined if missing. */
@@ -36,11 +76,11 @@ export function getByPointer(data: unknown, path: string): unknown {
36
76
  for (const seg of segments) {
37
77
  if (cur === null || cur === undefined) return undefined;
38
78
  if (Array.isArray(cur)) {
39
- const idx = Number(seg);
40
- if (!Number.isInteger(idx) || idx < 0 || idx >= cur.length) return undefined;
79
+ const idx = arrayIndex(seg);
80
+ if (idx === undefined || idx >= cur.length) return undefined;
41
81
  cur = cur[idx];
42
82
  } else if (typeof cur === 'object') {
43
- cur = (cur as Record<string, unknown>)[seg];
83
+ cur = ownValue(cur, seg);
44
84
  } else {
45
85
  return undefined;
46
86
  }
@@ -65,10 +105,13 @@ function setRec(node: unknown, segments: string[], i: number, value: unknown): u
65
105
 
66
106
  // Decide whether the slot we're about to write into is an array index.
67
107
  const nextSeg = isLast ? undefined : segments[i + 1];
68
- const writingArrayChild = nextSeg !== undefined && /^\d+$/.test(nextSeg);
108
+ const writingArrayChild = nextSeg !== undefined && arrayIndex(nextSeg) !== undefined;
69
109
 
70
110
  if (Array.isArray(node)) {
71
- const idx = Number(seg);
111
+ const idx = arrayIndex(seg);
112
+ if (idx === undefined) {
113
+ throw new TypeError(`Invalid array index ${JSON.stringify(seg)} in JSON Pointer`);
114
+ }
72
115
  const next = node.slice();
73
116
  next[idx] = isLast
74
117
  ? value
@@ -80,9 +123,12 @@ function setRec(node: unknown, segments: string[], i: number, value: unknown): u
80
123
  string,
81
124
  unknown
82
125
  >;
83
- obj[seg] = isLast
84
- ? value
85
- : setRec(obj[seg] ?? (writingArrayChild ? [] : {}), segments, i + 1, value);
126
+ const previous = ownValue(obj, seg);
127
+ setOwn(
128
+ obj,
129
+ seg,
130
+ isLast ? value : setRec(previous ?? (writingArrayChild ? [] : {}), segments, i + 1, value),
131
+ );
86
132
  return obj;
87
133
  }
88
134
 
@@ -98,12 +144,11 @@ export function resolveRef(
98
144
  ): SquisqAnnotatedSchema | undefined {
99
145
  if (!schema.$ref) return schema;
100
146
  const ref = schema.$ref;
147
+ if (ref === '#') return root;
101
148
  if (!ref.startsWith('#/')) return undefined;
102
- const segments = pointerSegments(ref.slice(1));
103
- let cur: unknown = root;
104
- for (const seg of segments) {
105
- if (cur === null || cur === undefined || typeof cur !== 'object') return undefined;
106
- cur = (cur as Record<string, unknown>)[seg];
149
+ try {
150
+ return getByPointer(root, ref.slice(1)) as SquisqAnnotatedSchema | undefined;
151
+ } catch {
152
+ return undefined;
107
153
  }
108
- return (cur as SquisqAnnotatedSchema | undefined) ?? undefined;
109
154
  }
@@ -38,19 +38,21 @@ const SQ_RUN = `'(?:[^'\\\\]|\\\\.)*'`;
38
38
  /**
39
39
  * Quote-aware match for a trailing `{[…]}` annotation: the interior is any
40
40
  * mix of quoted runs and characters other than `]` and quotes, so quoted
41
- * values may contain `]`. The trailing `[\s\]}]*` tolerates an accidental
42
- * doubled `]}` that users sometimes type when learning the syntax.
41
+ * values may contain `]`. An empty interior is valid so `{[]}` can remain
42
+ * lossless metadata instead of visible heading text. The trailing
43
+ * `[\s\]}]*` tolerates an accidental doubled `]}` that users sometimes type
44
+ * when learning the syntax.
43
45
  */
44
46
  const TEMPLATE_STRICT_RE = new RegExp(
45
- `\\s*\\{\\[((?:[^\\]"']|${DQ_RUN}|${SQ_RUN})+)\\]\\}[\\s\\]}]*$`,
47
+ `\\s*\\{\\[((?:[^\\]"']|${DQ_RUN}|${SQ_RUN})*)\\]\\}[\\s\\]}]*$`,
46
48
  );
47
49
 
48
50
  /**
49
- * Legacy fallback matching the pre-quote-aware grammar (`[^\]]+` interior).
51
+ * Legacy fallback matching the pre-quote-aware grammar (`[^\]]*` interior).
50
52
  * Tried after the strict form so inputs with unbalanced quotes keep parsing
51
53
  * exactly as they did before quote support landed.
52
54
  */
53
- const TEMPLATE_LEGACY_RE = /\s*\{\[([^\]]+)\]\}[\s\]}]*$/;
55
+ const TEMPLATE_LEGACY_RE = /\s*\{\[([^\]]*)\]\}[\s\]}]*$/;
54
56
 
55
57
  /**
56
58
  * Quote-aware match for a trailing Pandoc `{…}` block. The negative
@@ -144,6 +144,7 @@ export {
144
144
  createDocument,
145
145
  parseFrontmatter,
146
146
  formatBlockScalar,
147
+ formatFrontmatterValue,
147
148
  setFrontmatterValues,
148
149
  inferDocumentTitle,
149
150
  readFrontmatterThemeId,
@@ -12,7 +12,7 @@ import remarkMath from 'remark-math';
12
12
  import remarkDirective from 'remark-directive';
13
13
  import type { MarkdownDocument, StringifyOptions } from './types.js';
14
14
  import { toMdast } from './convert.js';
15
- import { formatBlockScalar } from './utils.js';
15
+ import { formatFrontmatterValue } from './utils.js';
16
16
 
17
17
  // Cache the default processor (all extensions, default formatting) to avoid rebuilding on every call.
18
18
  let defaultProcessor: any;
@@ -33,7 +33,7 @@ const SQ_RUN = `'(?:[^'\\\\]|\\\\.)*'`;
33
33
  * and quoted runs (which may themselves contain `]`).
34
34
  */
35
35
  const ESCAPED_TEMPLATE_SPAN_RE = new RegExp(
36
- `\\{\\\\\\[((?:${DQ_RUN}|${SQ_RUN}|\\\\.|[^\\]\\\\])+)\\]\\}`,
36
+ `\\{\\\\\\[((?:${DQ_RUN}|${SQ_RUN}|\\\\.|[^\\]\\\\])*)\\]\\}`,
37
37
  'g',
38
38
  );
39
39
 
@@ -195,13 +195,8 @@ export function stringifyMarkdown(doc: MarkdownDocument, options?: StringifyOpti
195
195
  // Prepend YAML frontmatter if present
196
196
  if (doc.frontmatter && Object.keys(doc.frontmatter).length > 0) {
197
197
  const yamlLines = Object.entries(doc.frontmatter).map(([k, v]) => {
198
- if (typeof v === 'string') {
199
- // Multi-line strings emit as a YAML literal block scalar, matching
200
- // `formatFrontmatterValue` / `setFrontmatterValues` so both paths
201
- // agree; single-line strings stay verbatim as before.
202
- return /[\r\n]/.test(v)
203
- ? `${k}: ${formatBlockScalar(v.replace(/\r\n?/g, '\n'))}`
204
- : `${k}: ${v}`;
198
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
199
+ return `${k}: ${formatFrontmatterValue(v)}`;
205
200
  }
206
201
  return `${k}: ${JSON.stringify(v)}`;
207
202
  });
@@ -126,6 +126,17 @@ export function parseFrontmatter(yaml: string): Record<string, unknown> | null {
126
126
  const result: Record<string, unknown> = {};
127
127
  const lines = yaml.split('\n');
128
128
 
129
+ // Define keys as data properties so a frontmatter key named `__proto__`
130
+ // cannot invoke Object.prototype's legacy setter.
131
+ const assign = (key: string, value: unknown): void => {
132
+ Object.defineProperty(result, key, {
133
+ value,
134
+ enumerable: true,
135
+ configurable: true,
136
+ writable: true,
137
+ });
138
+ };
139
+
129
140
  for (let i = 0; i < lines.length; i++) {
130
141
  const line = lines[i];
131
142
  const trimmed = line.trim();
@@ -135,7 +146,7 @@ export function parseFrontmatter(yaml: string): Record<string, unknown> | null {
135
146
  if (colonIdx < 1) continue;
136
147
 
137
148
  const key = trimmed.slice(0, colonIdx).trim();
138
- let value: string | boolean | number = trimmed.slice(colonIdx + 1).trim();
149
+ const value = trimmed.slice(colonIdx + 1).trim();
139
150
 
140
151
  // YAML literal block scalar: `key: |` or `key: |-`. Subsequent lines
141
152
  // that are more-indented than the key belong to the scalar; they are
@@ -172,27 +183,37 @@ export function parseFrontmatter(yaml: string): Record<string, unknown> | null {
172
183
  commonIndent = Math.min(commonIndent, bl.length - bl.trimStart().length);
173
184
  }
174
185
  if (!isFinite(commonIndent)) commonIndent = 0;
175
- result[key] = blockLines.map((bl) => (bl === '' ? '' : bl.slice(commonIndent))).join('\n');
186
+ assign(key, blockLines.map((bl) => (bl === '' ? '' : bl.slice(commonIndent))).join('\n'));
176
187
  continue;
177
188
  }
178
189
 
179
- // Remove surrounding quotes
180
- if (
181
- (value.startsWith('"') && value.endsWith('"')) ||
182
- (value.startsWith("'") && value.endsWith("'"))
183
- ) {
184
- value = value.slice(1, -1);
190
+ // Quoted scalars are always strings, even when their contents look like a
191
+ // number or boolean. Double-quoted values emitted by
192
+ // formatFrontmatterValue use JSON-compatible escaping; single-quoted YAML
193
+ // represents a literal quote as two consecutive quotes.
194
+ if (value.startsWith('"') && value.endsWith('"')) {
195
+ try {
196
+ assign(key, JSON.parse(value));
197
+ } catch {
198
+ // Be permissive for hand-authored YAML outside our formatter's subset.
199
+ assign(key, value.slice(1, -1));
200
+ }
201
+ continue;
202
+ }
203
+ if (value.startsWith("'") && value.endsWith("'")) {
204
+ assign(key, value.slice(1, -1).replace(/''/g, "'"));
205
+ continue;
185
206
  }
186
207
 
187
208
  // Parse booleans and numbers
188
209
  if (value === 'true') {
189
- result[key] = true;
210
+ assign(key, true);
190
211
  } else if (value === 'false') {
191
- result[key] = false;
212
+ assign(key, false);
192
213
  } else if (value !== '' && !isNaN(Number(value))) {
193
- result[key] = Number(value);
214
+ assign(key, Number(value));
194
215
  } else {
195
- result[key] = value;
216
+ assign(key, value);
196
217
  }
197
218
  }
198
219
 
@@ -217,7 +238,7 @@ export function formatBlockScalar(value: string): string {
217
238
  const FRONTMATTER_BLOCK_RE = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n)?/;
218
239
 
219
240
  /** Quote a frontmatter scalar so it round-trips cleanly through `parseFrontmatter`. */
220
- function formatFrontmatterValue(value: string | number | boolean): string {
241
+ export function formatFrontmatterValue(value: string | number | boolean): string {
221
242
  if (typeof value === 'boolean' || typeof value === 'number') return String(value);
222
243
  // Multi-line strings can't live on one line — emit a YAML literal block
223
244
  // scalar (`|-`) that `parseFrontmatter` reads back. This carries pretty-
@@ -239,7 +260,8 @@ function formatFrontmatterValue(value: string | number | boolean): string {
239
260
  value === '' ||
240
261
  value === 'true' ||
241
262
  value === 'false' ||
242
- /^-?\d+(\.\d+)?$/.test(value);
263
+ (value !== '' && !Number.isNaN(Number(value))) ||
264
+ (/^["']/.test(value) && /["']$/.test(value));
243
265
  if (!needsQuote) return value;
244
266
  // Prefer double quotes; escape any embedded double-quote / backslash.
245
267
  const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');