@metaobjectsdev/codegen-ts 0.9.0 → 0.10.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 (323) hide show
  1. package/README.md +1 -1
  2. package/dist/column-mapper.d.ts.map +1 -1
  3. package/dist/column-mapper.js +24 -8
  4. package/dist/column-mapper.js.map +1 -1
  5. package/dist/constants.d.ts +8 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +16 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/docs-paths.d.ts +58 -0
  10. package/dist/docs-paths.d.ts.map +1 -0
  11. package/dist/docs-paths.js +89 -0
  12. package/dist/docs-paths.js.map +1 -0
  13. package/dist/enum-import.d.ts +14 -0
  14. package/dist/enum-import.d.ts.map +1 -0
  15. package/dist/enum-import.js +35 -0
  16. package/dist/enum-import.js.map +1 -0
  17. package/dist/enum-shared.d.ts +32 -0
  18. package/dist/enum-shared.d.ts.map +1 -0
  19. package/dist/enum-shared.js +83 -0
  20. package/dist/enum-shared.js.map +1 -0
  21. package/dist/generator-registry.d.ts +22 -0
  22. package/dist/generator-registry.d.ts.map +1 -0
  23. package/dist/generator-registry.js +161 -0
  24. package/dist/generator-registry.js.map +1 -0
  25. package/dist/generator.d.ts +6 -0
  26. package/dist/generator.d.ts.map +1 -1
  27. package/dist/generator.js.map +1 -1
  28. package/dist/generators/api-doc-render.d.ts +17 -0
  29. package/dist/generators/api-doc-render.d.ts.map +1 -0
  30. package/dist/generators/api-doc-render.js +431 -0
  31. package/dist/generators/api-doc-render.js.map +1 -0
  32. package/dist/generators/api-docs-file.d.ts +21 -0
  33. package/dist/generators/api-docs-file.d.ts.map +1 -0
  34. package/dist/generators/api-docs-file.js +112 -0
  35. package/dist/generators/api-docs-file.js.map +1 -0
  36. package/dist/generators/api-field-shape.d.ts +39 -0
  37. package/dist/generators/api-field-shape.d.ts.map +1 -0
  38. package/dist/generators/api-field-shape.js +92 -0
  39. package/dist/generators/api-field-shape.js.map +1 -0
  40. package/dist/generators/api-label.d.ts +3 -0
  41. package/dist/generators/api-label.d.ts.map +1 -0
  42. package/dist/generators/api-label.js +8 -0
  43. package/dist/generators/api-label.js.map +1 -0
  44. package/dist/generators/api-model.d.ts +122 -0
  45. package/dist/generators/api-model.d.ts.map +1 -0
  46. package/dist/generators/api-model.js +809 -0
  47. package/dist/generators/api-model.js.map +1 -0
  48. package/dist/generators/docs-data-builder.d.ts +26 -4
  49. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  50. package/dist/generators/docs-data-builder.js +436 -164
  51. package/dist/generators/docs-data-builder.js.map +1 -1
  52. package/dist/generators/docs-data.d.ts +136 -27
  53. package/dist/generators/docs-data.d.ts.map +1 -1
  54. package/dist/generators/docs-data.js +1 -1
  55. package/dist/generators/docs-data.js.map +1 -1
  56. package/dist/generators/docs-file.d.ts +19 -0
  57. package/dist/generators/docs-file.d.ts.map +1 -1
  58. package/dist/generators/docs-file.js +154 -27
  59. package/dist/generators/docs-file.js.map +1 -1
  60. package/dist/generators/entity-file.d.ts.map +1 -1
  61. package/dist/generators/entity-file.js +29 -14
  62. package/dist/generators/entity-file.js.map +1 -1
  63. package/dist/generators/extractor-file.d.ts.map +1 -1
  64. package/dist/generators/extractor-file.js +2 -1
  65. package/dist/generators/extractor-file.js.map +1 -1
  66. package/dist/generators/field-anchor.d.ts +7 -0
  67. package/dist/generators/field-anchor.d.ts.map +1 -0
  68. package/dist/generators/field-anchor.js +23 -0
  69. package/dist/generators/field-anchor.js.map +1 -0
  70. package/dist/generators/index.d.ts +8 -1
  71. package/dist/generators/index.d.ts.map +1 -1
  72. package/dist/generators/index.js +6 -0
  73. package/dist/generators/index.js.map +1 -1
  74. package/dist/generators/mermaid-er.d.ts +14 -0
  75. package/dist/generators/mermaid-er.d.ts.map +1 -1
  76. package/dist/generators/mermaid-er.js +14 -0
  77. package/dist/generators/mermaid-er.js.map +1 -1
  78. package/dist/generators/output-parser-file.d.ts.map +1 -1
  79. package/dist/generators/output-parser-file.js +3 -4
  80. package/dist/generators/output-parser-file.js.map +1 -1
  81. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  82. package/dist/generators/output-prompt-file.js +2 -2
  83. package/dist/generators/output-prompt-file.js.map +1 -1
  84. package/dist/generators/prompt-render-file.d.ts.map +1 -1
  85. package/dist/generators/prompt-render-file.js +3 -4
  86. package/dist/generators/prompt-render-file.js.map +1 -1
  87. package/dist/generators/queries-file.d.ts.map +1 -1
  88. package/dist/generators/queries-file.js +8 -3
  89. package/dist/generators/queries-file.js.map +1 -1
  90. package/dist/generators/render-helper-file.d.ts.map +1 -1
  91. package/dist/generators/render-helper-file.js +2 -2
  92. package/dist/generators/render-helper-file.js.map +1 -1
  93. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  94. package/dist/generators/routes-file-hono.js +5 -1
  95. package/dist/generators/routes-file-hono.js.map +1 -1
  96. package/dist/generators/routes-file.d.ts +3 -0
  97. package/dist/generators/routes-file.d.ts.map +1 -1
  98. package/dist/generators/routes-file.js +6 -1
  99. package/dist/generators/routes-file.js.map +1 -1
  100. package/dist/generators/template-doc-builder.d.ts +19 -0
  101. package/dist/generators/template-doc-builder.d.ts.map +1 -0
  102. package/dist/generators/template-doc-builder.js +220 -0
  103. package/dist/generators/template-doc-builder.js.map +1 -0
  104. package/dist/generators/template-doc-data.d.ts +62 -0
  105. package/dist/generators/template-doc-data.d.ts.map +1 -0
  106. package/dist/generators/template-doc-data.js +16 -0
  107. package/dist/generators/template-doc-data.js.map +1 -0
  108. package/dist/generators/template-payload-tree.d.ts +15 -0
  109. package/dist/generators/template-payload-tree.d.ts.map +1 -0
  110. package/dist/generators/template-payload-tree.js +61 -0
  111. package/dist/generators/template-payload-tree.js.map +1 -0
  112. package/dist/generators/template-source-annotate.d.ts +74 -0
  113. package/dist/generators/template-source-annotate.d.ts.map +1 -0
  114. package/dist/generators/template-source-annotate.js +184 -0
  115. package/dist/generators/template-source-annotate.js.map +1 -0
  116. package/dist/generators/template-source-render.d.ts +24 -0
  117. package/dist/generators/template-source-render.d.ts.map +1 -0
  118. package/dist/generators/template-source-render.js +175 -0
  119. package/dist/generators/template-source-render.js.map +1 -0
  120. package/dist/generators/trace-helper-file.d.ts +9 -0
  121. package/dist/generators/trace-helper-file.d.ts.map +1 -0
  122. package/dist/generators/trace-helper-file.js +196 -0
  123. package/dist/generators/trace-helper-file.js.map +1 -0
  124. package/dist/index.d.ts +29 -4
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +28 -2
  127. package/dist/index.js.map +1 -1
  128. package/dist/metaobjects-config.d.ts +75 -2
  129. package/dist/metaobjects-config.d.ts.map +1 -1
  130. package/dist/metaobjects-config.js +43 -0
  131. package/dist/metaobjects-config.js.map +1 -1
  132. package/dist/naming.d.ts +19 -0
  133. package/dist/naming.d.ts.map +1 -1
  134. package/dist/naming.js +41 -0
  135. package/dist/naming.js.map +1 -1
  136. package/dist/payload-codegen.d.ts.map +1 -1
  137. package/dist/payload-codegen.js +12 -4
  138. package/dist/payload-codegen.js.map +1 -1
  139. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  140. package/dist/projection/extract-view-spec.js +51 -25
  141. package/dist/projection/extract-view-spec.js.map +1 -1
  142. package/dist/relation-resolver.d.ts +16 -0
  143. package/dist/relation-resolver.d.ts.map +1 -1
  144. package/dist/relation-resolver.js +82 -1
  145. package/dist/relation-resolver.js.map +1 -1
  146. package/dist/render-context.d.ts +4 -0
  147. package/dist/render-context.d.ts.map +1 -1
  148. package/dist/render-context.js.map +1 -1
  149. package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
  150. package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
  151. package/dist/render-engine/embedded-templates.generated.js +15 -0
  152. package/dist/render-engine/embedded-templates.generated.js.map +1 -0
  153. package/dist/render-engine/framework-provider.d.ts.map +1 -1
  154. package/dist/render-engine/framework-provider.js +26 -13
  155. package/dist/render-engine/framework-provider.js.map +1 -1
  156. package/dist/runner.d.ts.map +1 -1
  157. package/dist/runner.js +17 -0
  158. package/dist/runner.js.map +1 -1
  159. package/dist/templates/docs-file.d.ts +2 -6
  160. package/dist/templates/docs-file.d.ts.map +1 -1
  161. package/dist/templates/docs-file.js +2 -5
  162. package/dist/templates/docs-file.js.map +1 -1
  163. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  164. package/dist/templates/drizzle-schema.js +30 -2
  165. package/dist/templates/drizzle-schema.js.map +1 -1
  166. package/dist/templates/entity-constants.d.ts +7 -0
  167. package/dist/templates/entity-constants.d.ts.map +1 -1
  168. package/dist/templates/entity-constants.js +1 -1
  169. package/dist/templates/entity-constants.js.map +1 -1
  170. package/dist/templates/entity-file.d.ts.map +1 -1
  171. package/dist/templates/entity-file.js +16 -5
  172. package/dist/templates/entity-file.js.map +1 -1
  173. package/dist/templates/enums-file.d.ts +11 -0
  174. package/dist/templates/enums-file.d.ts.map +1 -0
  175. package/dist/templates/enums-file.js +44 -0
  176. package/dist/templates/enums-file.js.map +1 -0
  177. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  178. package/dist/templates/extract-delegate-emitter.js +5 -7
  179. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  180. package/dist/templates/extract-schema-emitter.d.ts.map +1 -1
  181. package/dist/templates/extract-schema-emitter.js +5 -1
  182. package/dist/templates/extract-schema-emitter.js.map +1 -1
  183. package/dist/templates/extractor.d.ts.map +1 -1
  184. package/dist/templates/extractor.js +56 -39
  185. package/dist/templates/extractor.js.map +1 -1
  186. package/dist/templates/field-meta.d.ts.map +1 -1
  187. package/dist/templates/field-meta.js +1 -5
  188. package/dist/templates/field-meta.js.map +1 -1
  189. package/dist/templates/filter-allowlist.d.ts +7 -2
  190. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  191. package/dist/templates/filter-allowlist.js +17 -9
  192. package/dist/templates/filter-allowlist.js.map +1 -1
  193. package/dist/templates/filter-type.d.ts +7 -1
  194. package/dist/templates/filter-type.d.ts.map +1 -1
  195. package/dist/templates/filter-type.js +9 -5
  196. package/dist/templates/filter-type.js.map +1 -1
  197. package/dist/templates/find-templates.d.ts +4 -0
  198. package/dist/templates/find-templates.d.ts.map +1 -0
  199. package/dist/templates/find-templates.js +15 -0
  200. package/dist/templates/find-templates.js.map +1 -0
  201. package/dist/templates/fr010-field-mapping.d.ts +2 -0
  202. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  203. package/dist/templates/fr010-field-mapping.js +10 -6
  204. package/dist/templates/fr010-field-mapping.js.map +1 -1
  205. package/dist/templates/inferred-types.d.ts +44 -7
  206. package/dist/templates/inferred-types.d.ts.map +1 -1
  207. package/dist/templates/inferred-types.js +107 -16
  208. package/dist/templates/inferred-types.js.map +1 -1
  209. package/dist/templates/mermaid-er.d.ts +35 -2
  210. package/dist/templates/mermaid-er.d.ts.map +1 -1
  211. package/dist/templates/mermaid-er.js +174 -7
  212. package/dist/templates/mermaid-er.js.map +1 -1
  213. package/dist/templates/output-parser.d.ts.map +1 -1
  214. package/dist/templates/output-parser.js +30 -79
  215. package/dist/templates/output-parser.js.map +1 -1
  216. package/dist/templates/output-prompt.d.ts.map +1 -1
  217. package/dist/templates/output-prompt.js +2 -2
  218. package/dist/templates/output-prompt.js.map +1 -1
  219. package/dist/templates/queries-file.d.ts.map +1 -1
  220. package/dist/templates/queries-file.js +112 -4
  221. package/dist/templates/queries-file.js.map +1 -1
  222. package/dist/templates/queries.d.ts +5 -0
  223. package/dist/templates/queries.d.ts.map +1 -1
  224. package/dist/templates/queries.js +7 -7
  225. package/dist/templates/queries.js.map +1 -1
  226. package/dist/templates/recover-schema-emitter.d.ts +8 -0
  227. package/dist/templates/recover-schema-emitter.d.ts.map +1 -0
  228. package/dist/templates/recover-schema-emitter.js +64 -0
  229. package/dist/templates/recover-schema-emitter.js.map +1 -0
  230. package/dist/templates/relations-block.js +10 -0
  231. package/dist/templates/relations-block.js.map +1 -1
  232. package/dist/templates/render-helper.d.ts.map +1 -1
  233. package/dist/templates/render-helper.js +4 -4
  234. package/dist/templates/render-helper.js.map +1 -1
  235. package/dist/templates/routes-file.d.ts.map +1 -1
  236. package/dist/templates/routes-file.js +183 -6
  237. package/dist/templates/routes-file.js.map +1 -1
  238. package/dist/templates/tph-discriminator.d.ts +56 -0
  239. package/dist/templates/tph-discriminator.d.ts.map +1 -0
  240. package/dist/templates/tph-discriminator.js +180 -0
  241. package/dist/templates/tph-discriminator.js.map +1 -0
  242. package/dist/templates/value-object-file.d.ts +2 -1
  243. package/dist/templates/value-object-file.d.ts.map +1 -1
  244. package/dist/templates/value-object-file.js +32 -4
  245. package/dist/templates/value-object-file.js.map +1 -1
  246. package/dist/templates/zod-validators.d.ts +64 -1
  247. package/dist/templates/zod-validators.d.ts.map +1 -1
  248. package/dist/templates/zod-validators.js +181 -8
  249. package/dist/templates/zod-validators.js.map +1 -1
  250. package/package.json +103 -34
  251. package/src/column-mapper.ts +25 -8
  252. package/src/constants.ts +18 -0
  253. package/src/docs-paths.ts +128 -0
  254. package/src/enum-import.ts +43 -0
  255. package/src/enum-shared.ts +95 -0
  256. package/src/generator-registry.ts +204 -0
  257. package/src/generator.ts +6 -0
  258. package/src/generators/api-doc-render.ts +572 -0
  259. package/src/generators/api-docs-file.ts +146 -0
  260. package/src/generators/api-field-shape.ts +114 -0
  261. package/src/generators/api-label.ts +7 -0
  262. package/src/generators/api-model.ts +1067 -0
  263. package/src/generators/docs-data-builder.ts +479 -185
  264. package/src/generators/docs-data.ts +139 -28
  265. package/src/generators/docs-file.ts +205 -39
  266. package/src/generators/entity-file.ts +31 -15
  267. package/src/generators/extractor-file.ts +2 -1
  268. package/src/generators/field-anchor.ts +24 -0
  269. package/src/generators/index.ts +8 -1
  270. package/src/generators/mermaid-er.ts +14 -0
  271. package/src/generators/output-parser-file.ts +3 -4
  272. package/src/generators/output-prompt-file.ts +2 -1
  273. package/src/generators/prompt-render-file.ts +3 -4
  274. package/src/generators/queries-file.ts +9 -3
  275. package/src/generators/render-helper-file.ts +2 -1
  276. package/src/generators/routes-file-hono.ts +5 -1
  277. package/src/generators/routes-file.ts +7 -1
  278. package/src/generators/template-doc-builder.ts +306 -0
  279. package/src/generators/template-doc-data.ts +85 -0
  280. package/src/generators/template-payload-tree.ts +71 -0
  281. package/src/generators/template-source-annotate.ts +290 -0
  282. package/src/generators/template-source-render.ts +203 -0
  283. package/src/generators/trace-helper-file.ts +301 -0
  284. package/src/index.ts +55 -4
  285. package/src/metaobjects-config.ts +117 -2
  286. package/src/naming.ts +48 -0
  287. package/src/payload-codegen.ts +14 -3
  288. package/src/projection/extract-view-spec.ts +49 -30
  289. package/src/relation-resolver.ts +103 -1
  290. package/src/render-context.ts +4 -0
  291. package/src/render-engine/embedded-templates.generated.ts +14 -0
  292. package/src/render-engine/framework-provider.ts +25 -11
  293. package/src/runner.ts +21 -0
  294. package/src/templates/docs-file.ts +2 -9
  295. package/src/templates/drizzle-schema.ts +31 -1
  296. package/src/templates/entity-constants.ts +1 -1
  297. package/src/templates/entity-file.ts +16 -5
  298. package/src/templates/enums-file.ts +50 -0
  299. package/src/templates/extract-delegate-emitter.ts +5 -6
  300. package/src/templates/extractor.ts +68 -38
  301. package/src/templates/field-meta.ts +0 -6
  302. package/src/templates/filter-allowlist.ts +17 -10
  303. package/src/templates/filter-type.ts +8 -6
  304. package/src/templates/find-templates.ts +15 -0
  305. package/src/templates/fr010-field-mapping.ts +10 -8
  306. package/src/templates/inferred-types.ts +108 -18
  307. package/src/templates/mermaid-er.ts +176 -8
  308. package/src/templates/output-parser.ts +30 -79
  309. package/src/templates/output-prompt.ts +2 -1
  310. package/src/templates/queries-file.ts +132 -3
  311. package/src/templates/queries.ts +15 -7
  312. package/src/templates/relations-block.ts +17 -0
  313. package/src/templates/render-helper.ts +4 -3
  314. package/src/templates/routes-file.ts +233 -6
  315. package/src/templates/tph-discriminator.ts +232 -0
  316. package/src/templates/value-object-file.ts +38 -4
  317. package/src/templates/zod-validators.ts +204 -7
  318. package/templates/api/agent-api.md.mustache +30 -0
  319. package/templates/api/entity-api.md.mustache +69 -0
  320. package/templates/api/index.md.mustache +21 -0
  321. package/templates/docs/entity-page.md.mustache +33 -21
  322. package/templates/docs/template-page.md.mustache +56 -0
  323. package/src/templates/extract-schema-emitter.ts +0 -111
@@ -0,0 +1,290 @@
1
+ // Annotated-template IR (linked-template-source-docs, Task 1).
2
+ //
3
+ // annotateTemplate parses a Mustache template into an ordered TplToken[] where
4
+ // every {{variable}} / {{#section}} is resolved to the payload field it
5
+ // references plus that field's doc link. It tokenizes with the SAME parser
6
+ // verify walks (`parseTemplate` from @metaobjectsdev/render) and resolves each
7
+ // path with verify's EXPORTED `resolveTemplateVariable` — so the annotator and
8
+ // the build-time drift gate share ONE resolution and can never disagree (a
9
+ // later conformance gate asserts exactly that).
10
+ //
11
+ // Reuse, not reimplementation:
12
+ // • parseTemplate — the render verify engine's Mustache.parse.
13
+ // • resolveTemplateVariable — verify's context-stack walk (sections push the
14
+ // nested subtree; dotted paths descend). Generic
15
+ // over the node, so an ENRICHED tree (carrying
16
+ // owner/type/required) resolves identically.
17
+ //
18
+ // The annotator is a pure function over an enriched payload tree + the source —
19
+ // no metadata import, no I/O — so it is unit-testable and golden-pinnable.
20
+
21
+ import {
22
+ parseTemplate,
23
+ resolveTemplateVariable,
24
+ type PayloadField,
25
+ type ResolveStack,
26
+ } from "@metaobjectsdev/render";
27
+ import { fieldAnchorSlug } from "./field-anchor.js";
28
+
29
+ /**
30
+ * An enriched payload-field node. Structurally a verify `PayloadField` (so the
31
+ * shared resolver walks it), plus the per-field doc metadata the annotator needs
32
+ * to emit a `ResolvedField` + link: `owner` (the VO that declares the field),
33
+ * `type`, and `required`. Container fields (object / array-of-object) carry
34
+ * `fields` whose nodes are owned by the nested VO.
35
+ */
36
+ export interface AnnotatePayloadField extends PayloadField {
37
+ owner: string;
38
+ type: string;
39
+ required: boolean;
40
+ fields?: AnnotatePayloadField[];
41
+ }
42
+
43
+ /** The field a `{{variable}}` / `{{#section}}` resolves to. */
44
+ export interface ResolvedField {
45
+ owner: string;
46
+ name: string;
47
+ type: string;
48
+ required: boolean;
49
+ }
50
+
51
+ /** One ordered token of the annotated template. `raw` is the verbatim source
52
+ * span of the tag (text tokens carry `text`), so the source round-trips. */
53
+ export type TplToken =
54
+ | { kind: "text"; text: string }
55
+ | {
56
+ kind: "var" | "unescaped";
57
+ raw: string;
58
+ path: string;
59
+ field?: ResolvedField;
60
+ href?: string;
61
+ valid: boolean;
62
+ }
63
+ | {
64
+ kind: "section" | "inverted" | "close";
65
+ raw: string;
66
+ path: string;
67
+ field?: ResolvedField;
68
+ href?: string;
69
+ }
70
+ | { kind: "partial"; raw: string; ref: string; href?: string }
71
+ | { kind: "comment"; raw: string };
72
+
73
+ export interface AnnotateOptions {
74
+ /** The root payload VO's short name (owner of the root-context fields). Used
75
+ * only for diagnostics / callers; per-field owner comes off the tree node. */
76
+ ownerVoName: string;
77
+ /**
78
+ * Resolve a partial `{{>ref}}` to a doc-page href, if the ref names a
79
+ * documented template. Returns the href or undefined (highlight-only).
80
+ * Optional — when absent, partials are captured ref-only (no href).
81
+ */
82
+ resolvePartialHref?: (ref: string) => string | undefined;
83
+ /**
84
+ * Override how a resolved field's doc-page href is built (owner page + the
85
+ * shared `#field-<name>` fragment). Optional — when absent, the flat default
86
+ * `./<owner>.md#field-<name>` is used (byte-identical to today). A caller with
87
+ * the output layout + page placement in scope injects a layout-aware resolver
88
+ * (the SAME `docPageHref(layout, …)` the Payload cross-link uses) so the link
89
+ * resolves under package layout too.
90
+ */
91
+ fieldHref?: (owner: string, name: string) => string;
92
+ }
93
+
94
+ // A Mustache parse token: [type, value, start, end, subTokens?, closeStart?, ...].
95
+ type Token = readonly unknown[];
96
+
97
+ /** The doc-page href for a resolved field: `./<OwnerVO>.md#field-<name>`. The
98
+ * anchor slug comes from the SHARED `fieldAnchorSlug()` (prefixed — avoids
99
+ * colliding with other page anchors), the SAME helper the entity page uses to
100
+ * emit its per-field `<a id="field-<name>">` anchor, so the link and the anchor
101
+ * can never drift. */
102
+ function fieldHref(owner: string, name: string): string {
103
+ return `./${owner}.md#${fieldAnchorSlug(name)}`;
104
+ }
105
+
106
+ function toResolvedField(f: AnnotatePayloadField): ResolvedField {
107
+ return { owner: f.owner, name: f.name, type: f.type, required: f.required };
108
+ }
109
+
110
+ /**
111
+ * Parse `source` into an annotated IR, resolving each variable/section against
112
+ * the enriched `payload` tree using verify's shared resolution.
113
+ */
114
+ export function annotateTemplate(
115
+ source: string,
116
+ payload: AnnotatePayloadField[],
117
+ opts: AnnotateOptions,
118
+ ): TplToken[] {
119
+ const root = payload;
120
+ const out: TplToken[] = [];
121
+ let cursor = 0;
122
+
123
+ // How a resolved field's href is built: the injected layout-aware resolver
124
+ // when provided, else the flat default (byte-identical to today's output).
125
+ const buildFieldHref = opts.fieldHref ?? fieldHref;
126
+
127
+ // Emit verbatim source between `cursor` and `to` as a text token, advancing
128
+ // the cursor. This recovers literal text AND any span Mustache trimmed as
129
+ // standalone (e.g. the newline after a standalone partial/section), so the
130
+ // concatenated tokens reproduce the source byte-for-byte.
131
+ function emitTextUpTo(to: number): void {
132
+ if (to > cursor) {
133
+ out.push({ kind: "text", text: source.slice(cursor, to) });
134
+ cursor = to;
135
+ }
136
+ }
137
+
138
+ function resolveAt(
139
+ stack: ResolveStack<AnnotatePayloadField>,
140
+ path: string,
141
+ ): { field: ResolvedField; href: string } | undefined {
142
+ const hit = resolveTemplateVariable(stack, path);
143
+ if (!hit) return undefined;
144
+ const field = toResolvedField(hit);
145
+ return { field, href: buildFieldHref(field.owner, field.name) };
146
+ }
147
+
148
+ // Build a variable token ({{x}} or {{&x}}/{{{x}}}). The implicit iterator
149
+ // `.` is always valid against the current context; any other path resolves
150
+ // against the enriched tree (carrying field + href when found).
151
+ function makeVarToken(
152
+ kind: "var" | "unescaped",
153
+ raw: string,
154
+ value: string,
155
+ stack: ResolveStack<AnnotatePayloadField>,
156
+ ): TplToken {
157
+ if (value === ".") return { kind, raw, path: value, valid: true };
158
+ const r = resolveAt(stack, value);
159
+ return r
160
+ ? { kind, raw, path: value, field: r.field, href: r.href, valid: true }
161
+ : { kind, raw, path: value, valid: false };
162
+ }
163
+
164
+ function walk(
165
+ tokens: Token[],
166
+ stack: ResolveStack<AnnotatePayloadField>,
167
+ ): void {
168
+ for (const tok of tokens) {
169
+ const type = tok[0] as string;
170
+ const value = tok[1] as string;
171
+ const start = tok[2] as number;
172
+ const end = tok[3] as number;
173
+
174
+ // Recover any source between the previous token and this tag (literal
175
+ // text, or trimmed standalone whitespace).
176
+ emitTextUpTo(start);
177
+
178
+ switch (type) {
179
+ case "text": {
180
+ out.push({ kind: "text", text: source.slice(start, end) });
181
+ cursor = end;
182
+ break;
183
+ }
184
+ case "name": {
185
+ // {{x}} — escaped variable.
186
+ const raw = source.slice(start, end);
187
+ cursor = end;
188
+ out.push(makeVarToken("var", raw, value, stack));
189
+ break;
190
+ }
191
+ case "&":
192
+ case "{": {
193
+ // {{&x}} / {{{x}}} — unescaped variable (mustache.js emits "&" for both).
194
+ const raw = source.slice(start, end);
195
+ cursor = end;
196
+ out.push(makeVarToken("unescaped", raw, value, stack));
197
+ break;
198
+ }
199
+ case "#":
200
+ case "^": {
201
+ // {{#x}}…{{/x}} (section) / {{^x}}…{{/x}} (inverted). `end` is the end
202
+ // of the OPENING tag; subTokens at [4]; [5] is where the closing tag
203
+ // begins. Emit the open tag, recurse the body in the pushed context,
204
+ // then emit the close tag.
205
+ const sub = Array.isArray(tok[4]) ? (tok[4] as Token[]) : [];
206
+ const closeStart = tok[5] as number;
207
+ const openRaw = source.slice(start, end);
208
+ cursor = end;
209
+
210
+ const isImplicit = value === ".";
211
+ const r = isImplicit ? undefined : resolveAt(stack, value);
212
+ const kind = type === "#" ? "section" : "inverted";
213
+ out.push(
214
+ r
215
+ ? {
216
+ kind,
217
+ raw: openRaw,
218
+ path: value,
219
+ field: r.field,
220
+ href: r.href,
221
+ }
222
+ : { kind, raw: openRaw, path: value },
223
+ );
224
+
225
+ // `#` over a container pushes its element fields; `^` (and `#` over a
226
+ // scalar conditional) keep the current context — EXACTLY verify's rule.
227
+ const hit = isImplicit
228
+ ? undefined
229
+ : resolveTemplateVariable(stack, value);
230
+ const nested = type === "#" ? hit?.fields : undefined;
231
+ const childStack: ResolveStack<AnnotatePayloadField> =
232
+ nested !== undefined ? [...stack, nested] : stack;
233
+ walk(sub, childStack);
234
+
235
+ // The closing tag `{{/value}}` runs from closeStart to the end of the
236
+ // section. Recover any body remainder Mustache trimmed, then emit close.
237
+ emitTextUpTo(closeStart);
238
+ // The close tag's exact end isn't in the token; derive it from the
239
+ // literal `{{/…}}` form starting at closeStart so the raw is verbatim.
240
+ const closeRaw = readCloseTag(source, closeStart, value);
241
+ out.push({ kind: "close", raw: closeRaw, path: value });
242
+ cursor = closeStart + closeRaw.length;
243
+ break;
244
+ }
245
+ case ">": {
246
+ // {{>ref}} — partial.
247
+ const raw = source.slice(start, end);
248
+ cursor = end;
249
+ const href = opts.resolvePartialHref?.(value);
250
+ out.push(
251
+ href !== undefined
252
+ ? { kind: "partial", raw, ref: value, href }
253
+ : { kind: "partial", raw, ref: value },
254
+ );
255
+ break;
256
+ }
257
+ case "!": {
258
+ // {{! comment }}.
259
+ out.push({ kind: "comment", raw: source.slice(start, end) });
260
+ cursor = end;
261
+ break;
262
+ }
263
+ default: {
264
+ // set-delimiter (=) or any other: preserve verbatim as text.
265
+ if (Number.isFinite(end) && end > start) {
266
+ out.push({ kind: "text", text: source.slice(start, end) });
267
+ cursor = end;
268
+ }
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ walk(parseTemplate(source) as Token[], [root]);
276
+ // Any trailing source after the last token (e.g. a standalone-trimmed final
277
+ // newline) is recovered as text.
278
+ emitTextUpTo(source.length);
279
+
280
+ return out;
281
+ }
282
+
283
+ // Read the verbatim closing tag `{{/name}}` (with any inner whitespace the
284
+ // author wrote) starting at `from`. Mustache only gives the close-tag start, so
285
+ // we locate the terminating `}}` from there to recover the exact source span.
286
+ function readCloseTag(source: string, from: number, _name: string): string {
287
+ const close = source.indexOf("}}", from);
288
+ if (close === -1) return source.slice(from);
289
+ return source.slice(from, close + 2);
290
+ }
@@ -0,0 +1,203 @@
1
+ // Template-source renderers (linked-template-source-docs, Task 3).
2
+ //
3
+ // ONE annotated IR (the TplToken[] from annotateTemplate), THREE doc forms — no
4
+ // re-derivation, no second parse. All three are pure functions over the tokens:
5
+ //
6
+ // • renderSourceBlock — the verbatim template inside a ```mustache fence.
7
+ // The annotator round-trips the source via text/raw,
8
+ // so the fenced body equals the original byte-for-byte
9
+ // and any viewer/site highlighter colors it.
10
+ // • renderVariablesTable — a Markdown table of the UNIQUE referenced variables
11
+ // (vars + sections that resolve to a field), each a
12
+ // Markdown-native `[owner.name](href)` link (or flagged
13
+ // "not on payload" when unresolved). Agent-clean.
14
+ // • renderRichLinkedHtml — a collapsed <details> whose <pre> reproduces the
15
+ // template with each token in a SELF-CONTAINED
16
+ // inline-styled <span> (color per kind) and each
17
+ // resolved variable/section wrapped in a clickable
18
+ // <a href>. Inline styles only (no external CSS) so it
19
+ // renders identically on GitHub and a static site.
20
+ //
21
+ // Color (block + rich view), links (table + rich view), agent-cleanliness (block
22
+ // + table) — all from the same source of truth.
23
+
24
+ import type { TplToken } from "./template-source-annotate.js";
25
+
26
+ // ── 1. Source block ────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Reconstruct the verbatim template source from the tokens and wrap it in a
30
+ * ```mustache fenced block. Concatenating `text` (text tokens) / `raw` (tag
31
+ * tokens) round-trips the original source byte-for-byte (the annotator pins
32
+ * this), so the fence body is the clean, highlightable source.
33
+ */
34
+ export function renderSourceBlock(tokens: TplToken[]): string {
35
+ const source = reconstructSource(tokens);
36
+ return "```mustache\n" + source + "\n```";
37
+ }
38
+
39
+ /** Concatenate the verbatim span of every token (text or tag raw). */
40
+ function reconstructSource(tokens: TplToken[]): string {
41
+ let out = "";
42
+ for (const t of tokens) out += t.kind === "text" ? t.text : t.raw;
43
+ return out;
44
+ }
45
+
46
+ // ── 2. Variables table ─────────────────────────────────────────────────────
47
+
48
+ interface VarRow {
49
+ path: string;
50
+ owner: string | undefined;
51
+ name: string | undefined;
52
+ type: string | undefined;
53
+ required: boolean | undefined;
54
+ href: string | undefined;
55
+ }
56
+
57
+ /** Collect the unique referenced variables (vars/unescaped + sections/inverted
58
+ * that carry a path), deduped by path, in first-seen order. Implicit-iterator
59
+ * `.` tokens and close tokens are not "variables" and are skipped. */
60
+ function collectVarRows(tokens: TplToken[]): VarRow[] {
61
+ const seen = new Set<string>();
62
+ const rows: VarRow[] = [];
63
+ for (const t of tokens) {
64
+ const isRef =
65
+ t.kind === "var" ||
66
+ t.kind === "unescaped" ||
67
+ t.kind === "section" ||
68
+ t.kind === "inverted";
69
+ if (!isRef) continue;
70
+ if (t.path === "." || seen.has(t.path)) continue;
71
+ seen.add(t.path);
72
+ rows.push({
73
+ path: t.path,
74
+ owner: t.field?.owner,
75
+ name: t.field?.name,
76
+ type: t.field?.type,
77
+ required: t.field?.required,
78
+ href: t.href,
79
+ });
80
+ }
81
+ return rows;
82
+ }
83
+
84
+ /** Markdown-escape a table cell whose text may contain a `|`. */
85
+ function mdCell(text: string): string {
86
+ return text.replace(/\|/g, "\\|");
87
+ }
88
+
89
+ /**
90
+ * A Markdown table of the unique referenced variables. Resolved variables link
91
+ * to their field doc (`[owner.name](href)`); unresolved ones are em-dashed and
92
+ * carry a "not on payload" note in the Type cell. Returns "" when there are no
93
+ * variables (keeps the page clean).
94
+ */
95
+ export function renderVariablesTable(tokens: TplToken[]): string {
96
+ const rows = collectVarRows(tokens);
97
+ if (rows.length === 0) return "";
98
+
99
+ const lines: string[] = [
100
+ "| Variable | Field | Type | Required |",
101
+ "| --- | --- | --- | --- |",
102
+ ];
103
+ for (const r of rows) {
104
+ const variable = `\`{{${mdCell(r.path)}}}\``;
105
+ if (r.href && r.owner && r.name) {
106
+ const field = `[${r.owner}.${r.name}](${r.href})`;
107
+ const type = r.type ? mdCell(r.type) : "—";
108
+ const required = r.required ? "yes" : "no";
109
+ lines.push(`| ${variable} | ${field} | ${type} | ${required} |`);
110
+ } else {
111
+ // Unresolved: no field/type/required. Keep the 4-column shape; flag it
112
+ // "not on payload" in the Field cell, em-dash the rest.
113
+ lines.push(`| ${variable} | — (not on payload) | — | — |`);
114
+ }
115
+ }
116
+ return lines.join("\n");
117
+ }
118
+
119
+ // ── 3. Rich linked HTML ────────────────────────────────────────────────────
120
+
121
+ // Per-kind inline color (self-contained — no external CSS, no classes). Chosen
122
+ // for legibility on both GitHub's light/dark Markdown and a generic docs site.
123
+ const STYLE_VAR = "color:#0969da"; // blue — escaped/unescaped variable
124
+ const STYLE_SECTION = "color:#8250df"; // purple — section / inverted / close
125
+ const STYLE_PARTIAL = "color:#1a7f7a"; // teal — partial reference
126
+ const STYLE_COMMENT = "color:#6e7781;font-style:italic"; // gray italic — comment
127
+
128
+ /** HTML-escape literal text so `<`, `>`, `&`, `"` in the template don't break
129
+ * the surrounding HTML. Quotes are escaped too so token raw is safe in any
130
+ * attribute-adjacent position. */
131
+ function escapeHtml(s: string): string {
132
+ return s
133
+ .replace(/&/g, "&amp;")
134
+ .replace(/</g, "&lt;")
135
+ .replace(/>/g, "&gt;")
136
+ .replace(/"/g, "&quot;");
137
+ }
138
+
139
+ /** An inline-styled span for a token kind, escaping the inner verbatim text. */
140
+ function styledSpan(style: string, inner: string): string {
141
+ return `<span style="${style}">${inner}</span>`;
142
+ }
143
+
144
+ /** A styled span for a token kind, wrapped in a clickable `<a href>` when the
145
+ * token resolved to a link (else the bare span). The raw text is HTML-escaped. */
146
+ function linkSpan(
147
+ style: string,
148
+ raw: string,
149
+ href: string | undefined,
150
+ ): string {
151
+ const span = styledSpan(style, escapeHtml(raw));
152
+ return href ? `<a href="${escapeHtml(href)}">${span}</a>` : span;
153
+ }
154
+
155
+ /**
156
+ * The collapsed <details> "Linked view": a <pre> reproducing the template where
157
+ * each token is an inline-styled <span> (color by kind) and each resolved
158
+ * variable/section is additionally wrapped in a clickable <a href>. Whitespace
159
+ * and newlines are preserved inside the <pre>. Collapsed by default (no `open`)
160
+ * so the plain/agent view stays clean; a human expands it for the clickable vars.
161
+ */
162
+ export function renderRichLinkedHtml(tokens: TplToken[]): string {
163
+ let pre = "";
164
+ for (const t of tokens) {
165
+ switch (t.kind) {
166
+ case "text": {
167
+ // Default color; escape literal text verbatim (preserves newlines).
168
+ pre += escapeHtml(t.text);
169
+ break;
170
+ }
171
+ case "var":
172
+ case "unescaped": {
173
+ pre += linkSpan(STYLE_VAR, t.raw, t.href);
174
+ break;
175
+ }
176
+ case "section":
177
+ case "inverted":
178
+ case "close": {
179
+ // Only open tags (section/inverted) carry an href; close tags don't.
180
+ const href = "href" in t ? t.href : undefined;
181
+ pre += linkSpan(STYLE_SECTION, t.raw, href);
182
+ break;
183
+ }
184
+ case "partial": {
185
+ pre += linkSpan(STYLE_PARTIAL, t.raw, t.href);
186
+ break;
187
+ }
188
+ case "comment": {
189
+ pre += styledSpan(STYLE_COMMENT, escapeHtml(t.raw));
190
+ break;
191
+ }
192
+ }
193
+ }
194
+
195
+ return (
196
+ "<details>\n" +
197
+ "<summary>Linked view</summary>\n\n" +
198
+ '<pre style="white-space:pre-wrap;word-break:break-word">' +
199
+ pre +
200
+ "</pre>\n\n" +
201
+ "</details>"
202
+ );
203
+ }