@astryxdesign/cli 0.6.3-canary.7ada42e → 0.6.3-canary.7ec7be0

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 (221) hide show
  1. package/README.md +21 -19
  2. package/api/build/_adapter.d.mts +50 -0
  3. package/api/build/_adapter.mjs +60 -0
  4. package/api/build/build.doc.mjs +13 -7
  5. package/api/build/build.test.mjs +184 -6
  6. package/api/build/build.type.d.mts +57 -2
  7. package/api/build/build.type.mjs +30 -7
  8. package/api/build/help/help.d.mts +4 -0
  9. package/api/build/help/help.mjs +24 -11
  10. package/api/build/kit/kit.d.mts +4 -1
  11. package/api/build/kit/kit.mjs +165 -49
  12. package/api/build/kit/rank.d.mts +44 -0
  13. package/api/build/kit/rank.mjs +432 -0
  14. package/api/build/kit/rank.test.mjs +196 -0
  15. package/api/component/_adapter.d.mts +6 -12
  16. package/api/component/_adapter.mjs +20 -10
  17. package/api/component/component.mjs +60 -12
  18. package/api/component/list/list.mjs +3 -2
  19. package/api/docs/_adapter.d.mts +251 -28
  20. package/api/docs/_adapter.mjs +918 -35
  21. package/api/docs/detail/detail.mjs +11 -3
  22. package/api/docs/detail/section/section.mjs +24 -13
  23. package/api/docs/detail/section/section.test.mjs +15 -6
  24. package/api/docs/docs.d.mts +5 -3
  25. package/api/docs/docs.doc.mjs +38 -17
  26. package/api/docs/docs.mjs +44 -8
  27. package/api/docs/docs.test.mjs +158 -4
  28. package/api/docs/docs.type.d.mts +181 -2
  29. package/api/docs/docs.type.mjs +117 -3
  30. package/api/docs/index/index.mjs +11 -3
  31. package/api/docs/index/index.test.mjs +1 -1
  32. package/api/docs/integration-tree.test.mjs +555 -0
  33. package/api/docs/integrationDocs.test.mjs +14 -14
  34. package/api/docs/list/list.mjs +25 -5
  35. package/api/docs/node/node.d.mts +43 -0
  36. package/api/docs/node/node.mjs +192 -0
  37. package/api/docs/reference-blocks.test.mjs +406 -0
  38. package/api/doctor/doctor.d.mts +30 -2
  39. package/api/doctor/doctor.mjs +151 -8
  40. package/api/doctor/doctor.test.mjs +95 -7
  41. package/api/index.d.mts +1 -1
  42. package/api/index.mjs +1 -0
  43. package/api/init/init.doc.mjs +1 -1
  44. package/api/integration/add-contribution.d.mts +2 -1
  45. package/api/integration/add-contribution.mjs +107 -5
  46. package/api/integration/add-contribution.test.mjs +137 -0
  47. package/api/integration/authoring-checks.mjs +138 -28
  48. package/api/integration/authoring-checks.test.mjs +171 -3
  49. package/api/integration/authoring-checks.type.mjs +6 -1
  50. package/api/integration/integration-authoring.type.d.mts +2 -0
  51. package/api/integration/integration-authoring.type.mjs +2 -0
  52. package/api/integration/integrationAdd.doc.mjs +6 -0
  53. package/api/integration/integrationAddDoc.doc.mjs +7 -1
  54. package/api/integration/integrationDocConflicts.doc.mjs +1 -1
  55. package/api/integration/integrationPackCheck.doc.mjs +1 -1
  56. package/api/integration/integrationTemplateConflicts.doc.d.mts +3 -0
  57. package/api/integration/integrationTemplateConflicts.doc.mjs +3 -0
  58. package/api/integration/pack-check.mjs +34 -0
  59. package/api/integration/pack-check.test.mjs +121 -0
  60. package/api/integration/pack-check.type.d.mts +26 -2
  61. package/api/integration/pack-check.type.mjs +14 -1
  62. package/api/integration/template-conflict-compatibility.test.mjs +73 -0
  63. package/api/integration/validate-integration.test.mjs +3 -3
  64. package/api/integration/validateIntegration.doc.mjs +1 -1
  65. package/api/json/index.ts +2 -0
  66. package/api/layout/_adapter.mjs +20 -5
  67. package/api/search/search.d.mts +59 -1
  68. package/api/search/search.doc.mjs +4 -3
  69. package/api/search/search.mjs +442 -71
  70. package/api/search/search.test.mjs +91 -2
  71. package/api/search/search.type.d.mts +13 -1
  72. package/api/search/search.type.mjs +4 -1
  73. package/api/template/list/list.mjs +1 -0
  74. package/api/template/table-floating-bulk-actions.test.mjs +66 -0
  75. package/api/template/template-integration.test.mjs +1072 -3
  76. package/api/template/template.doc.mjs +27 -7
  77. package/api/template/template.mjs +45 -8
  78. package/api/template/template.type.d.mts +6 -8
  79. package/api/template/template.type.mjs +3 -2
  80. package/api/theme/template/template.test.mjs +5 -0
  81. package/api/theme/themeTemplate.doc.mjs +1 -1
  82. package/assets/docs/getting-started.doc.mjs +2 -2
  83. package/assets/docs/layout.doc.dense.mjs +2 -2
  84. package/assets/docs/layout.doc.mjs +1 -1
  85. package/assets/docs/principles.doc.mjs +6 -6
  86. package/assets/docs/styling-libraries.doc.mjs +3 -3
  87. package/assets/docs/styling.doc.mjs +4 -4
  88. package/assets/docs/theme.doc.mjs +4 -4
  89. package/assets/docs/tokens.doc.mjs +1 -1
  90. package/assets/docs/tree/api.doc.mjs +30 -0
  91. package/assets/docs/tree/cli.doc.mjs +23 -0
  92. package/assets/docs/tree/commands.doc.mjs +25 -0
  93. package/assets/docs/{cli-integrations.doc.mjs → tree/integrations.doc.mjs} +37 -9
  94. package/assets/docs/tree/integrations.test.mjs +62 -0
  95. package/assets/docs/tree/writing-docs.doc.mjs +286 -0
  96. package/assets/docs/working-with-ai.doc.mjs +3 -3
  97. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.doc.mjs +1 -1
  98. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.tsx +1 -3
  99. package/assets/templates/blocks/components/Table/TableBulkActionsTable.doc.mjs +14 -0
  100. package/assets/templates/blocks/components/Table/TableBulkActionsTable.tsx +71 -0
  101. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.doc.mjs +19 -0
  102. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.tsx +139 -0
  103. package/authoring/debug/parse.d.mts +3 -3
  104. package/authoring/doctypes/_schema.d.mts +119 -117
  105. package/authoring/doctypes/_schema.mjs +54 -3
  106. package/authoring/doctypes/base/graph-fields.doc.mjs +7 -5
  107. package/authoring/doctypes/base/type.ts +6 -5
  108. package/authoring/doctypes/command/command.doc.mjs +1 -1
  109. package/authoring/doctypes/command/type.ts +2 -2
  110. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  111. package/authoring/doctypes/enum/type.ts +1 -1
  112. package/authoring/doctypes/function/function.doc.mjs +3 -2
  113. package/authoring/doctypes/function/type.ts +3 -2
  114. package/authoring/doctypes/load-contract.test.mjs +26 -1
  115. package/authoring/doctypes/namespace/namespace.doc.mjs +6 -10
  116. package/authoring/doctypes/namespace/parse.test.mjs +23 -25
  117. package/authoring/doctypes/namespace/type.ts +5 -2
  118. package/authoring/doctypes/parse.mjs +3 -2
  119. package/authoring/doctypes/reference/reference.doc.mjs +34 -5
  120. package/authoring/doctypes/reference/type.ts +28 -11
  121. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  122. package/authoring/doctypes/schema/type.ts +1 -1
  123. package/authoring/doctypes/template/parse.d.mts +2 -0
  124. package/authoring/doctypes/template/parse.mjs +4 -0
  125. package/authoring/doctypes/template/parse.test.mjs +18 -0
  126. package/authoring/doctypes/template/template.doc.mjs +6 -0
  127. package/authoring/doctypes/template/type.ts +8 -0
  128. package/authoring/identity/identity.doc.mjs +2 -2
  129. package/authoring/index.d.ts +1 -0
  130. package/authoring/integration/type.ts +1 -1
  131. package/clients/cli/commands/build-theme.adaptations.test.mjs +100 -1
  132. package/clients/cli/commands/build-theme.mjs +1 -1
  133. package/clients/cli/commands/build.doc.mjs +11 -6
  134. package/clients/cli/commands/build.mjs +105 -67
  135. package/clients/cli/commands/build.text-fields.test.mjs +41 -0
  136. package/clients/cli/commands/component/index.mjs +1 -1
  137. package/clients/cli/commands/component-ownership.test.mjs +3 -3
  138. package/clients/cli/commands/discover.broken-integration.test.mjs +7 -8
  139. package/clients/cli/commands/discover.mjs +1 -1
  140. package/clients/cli/commands/docs.doc.mjs +20 -8
  141. package/clients/cli/commands/docs.mjs +183 -69
  142. package/clients/cli/commands/docs.test.mjs +120 -16
  143. package/clients/cli/commands/doctor-integration-docs.doc.mjs +2 -2
  144. package/clients/cli/commands/doctor-integration-templates.doc.mjs +14 -7
  145. package/clients/cli/commands/doctor-integration.test.mjs +75 -1
  146. package/clients/cli/commands/doctor.mjs +53 -16
  147. package/clients/cli/commands/integration-add.doc.mjs +10 -0
  148. package/clients/cli/commands/integration.mjs +1 -0
  149. package/clients/cli/commands/search.mjs +11 -2
  150. package/clients/cli/commands/setup-nudge.test.mjs +6 -0
  151. package/clients/cli/commands/template.doc.mjs +23 -6
  152. package/clients/cli/commands/template.mjs +4 -91
  153. package/clients/cli/index.mjs +8 -0
  154. package/clients/cli/lib/manifest.mjs +9 -2
  155. package/foundation/agent-docs/agent-docs.d.mts +3 -2
  156. package/foundation/agent-docs/agent-docs.mjs +13 -9
  157. package/foundation/agent-docs/agent-docs.test.mjs +19 -1
  158. package/foundation/config/project.d.mts +12 -11
  159. package/foundation/config/project.mjs +73 -59
  160. package/foundation/config/project.test.mjs +141 -5
  161. package/foundation/discovery/authoring-self-docs.d.mts +12 -0
  162. package/foundation/discovery/authoring-self-docs.mjs +20 -8
  163. package/foundation/discovery/authoring-self-docs.test.mjs +25 -9
  164. package/foundation/discovery/cli-self-docs.d.mts +40 -34
  165. package/foundation/discovery/cli-self-docs.mjs +91 -115
  166. package/foundation/discovery/cli-self-docs.test.mjs +104 -166
  167. package/foundation/discovery/component-discovery.d.mts +38 -0
  168. package/foundation/discovery/component-discovery.mjs +48 -0
  169. package/foundation/discovery/docs-discovery.d.mts +109 -9
  170. package/foundation/discovery/docs-discovery.mjs +223 -24
  171. package/foundation/discovery/docs-discovery.test.mjs +103 -27
  172. package/foundation/discovery/docs-output-budget.d.mts +2 -2
  173. package/foundation/discovery/docs-output-budget.mjs +1 -1
  174. package/foundation/discovery/docs-section-key.d.mts +28 -10
  175. package/foundation/discovery/docs-section-key.mjs +134 -33
  176. package/foundation/discovery/docs-section-key.test.mjs +41 -19
  177. package/foundation/discovery/template-adapter.d.mts +92 -6
  178. package/foundation/discovery/template-adapter.mjs +453 -17
  179. package/foundation/discovery/template-adapter.test.mjs +42 -0
  180. package/foundation/discovery/template-conflict-release.d.mts +13 -0
  181. package/foundation/discovery/template-conflict-release.mjs +40 -0
  182. package/foundation/discovery/template-conflict-release.test.mjs +40 -0
  183. package/foundation/doc-compiler/bundle.mjs +61 -1
  184. package/foundation/doc-compiler/bundle.test.mjs +21 -10
  185. package/foundation/doc-compiler/compile.d.mts +113 -132
  186. package/foundation/doc-compiler/compile.mjs +57 -10
  187. package/foundation/doc-compiler/diagnostics.mjs +28 -0
  188. package/foundation/doc-compiler/doc-loads.test.mjs +2 -2
  189. package/foundation/doc-compiler/inputs.d.mts +2 -2
  190. package/foundation/doc-compiler/inputs.mjs +6 -1
  191. package/foundation/doc-compiler/ir.mjs +7 -0
  192. package/foundation/doc-compiler/lenses.d.mts +8 -5
  193. package/foundation/doc-compiler/lenses.mjs +50 -4
  194. package/foundation/doc-compiler/links.d.mts +162 -0
  195. package/foundation/doc-compiler/links.mjs +294 -0
  196. package/foundation/doc-compiler/links.test.mjs +192 -0
  197. package/foundation/doc-compiler/lower-doc.test.mjs +5 -2
  198. package/foundation/doc-compiler/read.d.mts +3 -2
  199. package/foundation/doc-compiler/read.mjs +6 -1
  200. package/foundation/doc-compiler/tree.d.mts +288 -0
  201. package/foundation/doc-compiler/tree.mjs +876 -0
  202. package/foundation/doc-compiler/tree.test.mjs +598 -0
  203. package/foundation/integrations/autolink.test.mjs +1 -1
  204. package/foundation/integrations/cli-requirement.d.mts +45 -0
  205. package/foundation/integrations/cli-requirement.mjs +154 -0
  206. package/foundation/integrations/cli-requirement.test.mjs +84 -0
  207. package/foundation/integrations/contribution-inventory.d.mts +2 -1
  208. package/foundation/integrations/contribution-inventory.mjs +10 -4
  209. package/foundation/integrations/contribution-inventory.test.mjs +19 -0
  210. package/foundation/integrations/integration-warnings.d.mts +9 -2
  211. package/foundation/integrations/integration-warnings.mjs +51 -26
  212. package/foundation/integrations/integration-warnings.test.mjs +74 -1
  213. package/foundation/integrations/validate-contributions.mjs +25 -27
  214. package/foundation/response/response-types.doc.d.mts +5 -1
  215. package/foundation/response/response-types.doc.mjs +19 -10
  216. package/foundation/text/string-utils.d.mts +8 -0
  217. package/foundation/text/string-utils.mjs +18 -0
  218. package/foundation/xle/expand.mjs +1 -1
  219. package/foundation/xle/xle.test.mjs +13 -0
  220. package/package.json +9 -9
  221. package/assets/docs/cli.doc.mjs +0 -15
@@ -27,6 +27,7 @@ const NODE_FIELDS = new Set([
27
27
  'lang',
28
28
  'provenance',
29
29
  'sourceTitles',
30
+ 'sectionProviders',
30
31
  'doc',
31
32
  ]);
32
33
  const RESOLVED_FIELDS = new Set([
@@ -170,6 +171,12 @@ function structureProblems(node) {
170
171
  'provenance: expected {provider, replaces, extensions} naming packages, not paths',
171
172
  );
172
173
  }
174
+ const providers = isRecord(node.sectionProviders)
175
+ ? node.sectionProviders
176
+ : null;
177
+ if (!providers || !Object.values(providers).every(isText)) {
178
+ problems.push('sectionProviders: expected section key -> provider id');
179
+ }
173
180
  const titles = isRecord(node.sourceTitles) ? node.sourceTitles : null;
174
181
  if (!titles || !Object.values(titles).every(isText)) {
175
182
  problems.push('sourceTitles: expected section key -> authored title');
@@ -18,14 +18,17 @@ export function readerSections(node: import("./compile.mjs").CompiledReferenceNo
18
18
  export function detailView(node: import("./compile.mjs").CompiledReferenceNode): any;
19
19
  /**
20
20
  * `docs.index`: what the topic is, and each section's key, title and summary.
21
+ * The index leaf adds the moves.
21
22
  * @param {import('./compile.mjs').CompiledReferenceNode} node
22
- * @returns {import('../../api/docs/docs.type.mjs').DocsIndex}
23
+ * @returns {Omit<import('../../api/docs/docs.type.mjs').DocsIndex, 'links'>}
23
24
  */
24
- export function indexView(node: import("./compile.mjs").CompiledReferenceNode): import("../../api/docs/docs.type.mjs").DocsIndex;
25
+ export function indexView(node: import("./compile.mjs").CompiledReferenceNode): Omit<import("../../api/docs/docs.type.mjs").DocsIndex, "links">;
25
26
  /**
26
- * `docs.detail.section`: one section with its token references inlined. A
27
- * referenced section's content takes the reference's place; the section takes
28
- * the preview type of the last reference that has one, unless it has its own.
27
+ * `docs.detail.section`: one section with its token references and reference
28
+ * blocks inlined, so a read returns only the stable block kinds. A referenced
29
+ * section's content takes the token reference's place; the section takes the
30
+ * preview type of the last reference that has one, unless it has its own. A
31
+ * reference block becomes the content it includes (see referenceView).
29
32
  * @param {import('./compile.mjs').CompiledReferenceNode} node
30
33
  * @param {any} section a linked section of `node`
31
34
  * @returns {any}
@@ -62,17 +62,20 @@ export function detailView(node) {
62
62
 
63
63
  /**
64
64
  * `docs.index`: what the topic is, and each section's key, title and summary.
65
+ * The index leaf adds the moves.
65
66
  * @param {import('./compile.mjs').CompiledReferenceNode} node
66
- * @returns {import('../../api/docs/docs.type.mjs').DocsIndex}
67
+ * @returns {Omit<import('../../api/docs/docs.type.mjs').DocsIndex, 'links'>}
67
68
  */
68
69
  export function indexView(node) {
69
70
  return buildDocsIndexData(node.doc);
70
71
  }
71
72
 
72
73
  /**
73
- * `docs.detail.section`: one section with its token references inlined. A
74
- * referenced section's content takes the reference's place; the section takes
75
- * the preview type of the last reference that has one, unless it has its own.
74
+ * `docs.detail.section`: one section with its token references and reference
75
+ * blocks inlined, so a read returns only the stable block kinds. A referenced
76
+ * section's content takes the token reference's place; the section takes the
77
+ * preview type of the last reference that has one, unless it has its own. A
78
+ * reference block becomes the content it includes (see referenceView).
76
79
  * @param {import('./compile.mjs').CompiledReferenceNode} node
77
80
  * @param {any} section a linked section of `node`
78
81
  * @returns {any}
@@ -83,6 +86,10 @@ export function sectionView(node, section) {
83
86
  /** @type {string | null} */
84
87
  let previewType = null;
85
88
  for (const block of section.content) {
89
+ if (block?.type === 'reference') {
90
+ content.push(...referenceView(node, block));
91
+ continue;
92
+ }
86
93
  if (block?.type !== 'token-ref') {
87
94
  content.push(structuredClone(block));
88
95
  continue;
@@ -125,3 +132,42 @@ export function sectionView(node, section) {
125
132
  view.content = content;
126
133
  return withSourceTitle(view, authoredTitle(node, section));
127
134
  }
135
+
136
+ /**
137
+ * A reference block as a read shows it (spec:AST-047 FR9): the content it
138
+ * includes, then where that content comes from and the command that opens it.
139
+ * A reference that includes nothing (a summary, or a doc that is not included
140
+ * whole) shows the doc's title and summary; one whose doc is missing says so
141
+ * where its content would be, as a token reference does.
142
+ * @param {import('./compile.mjs').CompiledReferenceNode} node
143
+ * @param {any} block a linked reference block
144
+ * @returns {any[]}
145
+ */
146
+ function referenceView(node, block) {
147
+ if (!('link' in block)) {
148
+ throw new Error(
149
+ `The reference to "${block.target}" in "${node.id}" was read before it was linked.`,
150
+ );
151
+ }
152
+ const link = block.link;
153
+ if (link == null) {
154
+ return [
155
+ {type: 'prose', text: `[reference: "${block.target}" names no doc]`},
156
+ ];
157
+ }
158
+ const included = (block.content ?? []).map((/** @type {any} */ each) =>
159
+ structuredClone(each),
160
+ );
161
+ if (included.length === 0) {
162
+ const summary = String(link.summary ?? '').trim();
163
+ const lead =
164
+ summary === ''
165
+ ? link.title
166
+ : `${link.title}: ${summary}${/[.!?]$/.test(summary) ? '' : '.'}`;
167
+ return [{type: 'prose', text: `${lead} Read it with \`${link.command}\`.`}];
168
+ }
169
+ return [
170
+ ...included,
171
+ {type: 'prose', text: `From ${link.title}: \`${link.command}\``},
172
+ ];
173
+ }
@@ -0,0 +1,162 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * @typedef {object} LinkTarget
6
+ * @property {string | null} provider the provider id it names, or null for the
7
+ * linking doc's own provider
8
+ * @property {string} kind
9
+ * @property {string} name
10
+ */
11
+ /**
12
+ * The doc a link opens, as a docs read returns it.
13
+ * @typedef {object} DocLink
14
+ * @property {string} target the target as written: `[<provider>:]<kind>:<name>`
15
+ * @property {string} id the doc's identity (a DocId)
16
+ * @property {string} route what `astryx docs <route>` opens
17
+ * @property {string} title
18
+ * @property {string} summary
19
+ * @property {string} command the command that opens it, `astryx docs <route>`
20
+ */
21
+ /**
22
+ * Finds the doc a target names, or says why none matches.
23
+ * @typedef {(target: string) => Promise<DocLink | {problem: string}>} LinkResolver
24
+ */
25
+ /**
26
+ * @typedef {object} LinkProblem
27
+ * @property {string} target the target as written
28
+ * @property {string} message
29
+ * @property {string} [section] the key of the section the link sits in
30
+ * @property {true} [include] the problem is in a `reference` block, which
31
+ * includes content rather than linking to it: a reader loses that content,
32
+ * so the authoring check fails on it
33
+ */
34
+ /**
35
+ * What a `reference` block in a topic section includes of the doc it names:
36
+ * stable content blocks, with anything it could not include named in
37
+ * `problems`.
38
+ * @typedef {(block: any) => Promise<{content: any[], problems: string[]}>} DocIncluder
39
+ */
40
+ /**
41
+ * Parse `[<provider>:]<kind>:<name>`. A name may hold spaces
42
+ * (`command:theme build`); no part holds a colon.
43
+ * @param {unknown} target
44
+ * @returns {LinkTarget | {error: string}}
45
+ */
46
+ export function parseLinkTarget(target: unknown): LinkTarget | {
47
+ error: string;
48
+ };
49
+ /**
50
+ * Text with each inline link replaced by its name, for indexes that must not
51
+ * read the link syntax as words.
52
+ * @param {string} text
53
+ * @returns {string}
54
+ */
55
+ export function unlinkText(text: string): string;
56
+ /**
57
+ * Resolve every link in a list of content blocks, in order.
58
+ * @param {any[]} blocks
59
+ * @param {LinkResolver} resolve
60
+ * @param {{section?: string}} [at]
61
+ * @param {DocIncluder} [include] how a `reference` block includes the doc it
62
+ * names; without it, the block only carries its `link`
63
+ * @returns {Promise<{content: any[], problems: LinkProblem[]}>}
64
+ */
65
+ export function linkBlocks(blocks: any[], resolve: LinkResolver, at?: {
66
+ section?: string;
67
+ }, include?: DocIncluder): Promise<{
68
+ content: any[];
69
+ problems: LinkProblem[];
70
+ }>;
71
+ /**
72
+ * Resolve every link in a topic's sections.
73
+ * @param {any[]} sections
74
+ * @param {LinkResolver} resolve
75
+ * @returns {Promise<{sections: any[], problems: LinkProblem[]}>}
76
+ */
77
+ export function linkSections(sections: any[], resolve: LinkResolver): Promise<{
78
+ sections: any[];
79
+ problems: LinkProblem[];
80
+ }>;
81
+ /**
82
+ * @file Links between docs, named by doc identity (spec:AST-047 FR9).
83
+ *
84
+ * @input A link target, `[<provider>:]<kind>:<name>`, written as a `reference`
85
+ * block, in a `workflow` step's `references`, or inline in text as
86
+ * `{@link <target>}`; and a resolver that finds the doc a target names.
87
+ * @output The same content with every link resolved: an inline link becomes
88
+ * the `astryx docs` command that opens its doc, and a block carries a `link`
89
+ * with the doc's identity, route, title, summary, and command. A reference
90
+ * block in a topic section also carries the `content` it includes. A link
91
+ * that names no doc is a problem, never a guess.
92
+ * @position Pure. The docs adapter owns the resolver (the project's docs tree
93
+ * and topic catalog) and applies this to every read and every doctor check.
94
+ */
95
+ /** The kinds a link can name: every authored doc kind. */
96
+ export const LINK_KINDS: Set<string>;
97
+ /** An inline link in text: `{@link <target>}`. */
98
+ export const INLINE_LINK: RegExp;
99
+ export type LinkTarget = {
100
+ /**
101
+ * the provider id it names, or null for the
102
+ * linking doc's own provider
103
+ */
104
+ provider: string | null;
105
+ kind: string;
106
+ name: string;
107
+ };
108
+ /**
109
+ * The doc a link opens, as a docs read returns it.
110
+ */
111
+ export type DocLink = {
112
+ /**
113
+ * the target as written: `[<provider>:]<kind>:<name>`
114
+ */
115
+ target: string;
116
+ /**
117
+ * the doc's identity (a DocId)
118
+ */
119
+ id: string;
120
+ /**
121
+ * what `astryx docs <route>` opens
122
+ */
123
+ route: string;
124
+ title: string;
125
+ summary: string;
126
+ /**
127
+ * the command that opens it, `astryx docs <route>`
128
+ */
129
+ command: string;
130
+ };
131
+ /**
132
+ * Finds the doc a target names, or says why none matches.
133
+ */
134
+ export type LinkResolver = (target: string) => Promise<DocLink | {
135
+ problem: string;
136
+ }>;
137
+ export type LinkProblem = {
138
+ /**
139
+ * the target as written
140
+ */
141
+ target: string;
142
+ message: string;
143
+ /**
144
+ * the key of the section the link sits in
145
+ */
146
+ section?: string | undefined;
147
+ /**
148
+ * the problem is in a `reference` block, which
149
+ * includes content rather than linking to it: a reader loses that content,
150
+ * so the authoring check fails on it
151
+ */
152
+ include?: true | undefined;
153
+ };
154
+ /**
155
+ * What a `reference` block in a topic section includes of the doc it names:
156
+ * stable content blocks, with anything it could not include named in
157
+ * `problems`.
158
+ */
159
+ export type DocIncluder = (block: any) => Promise<{
160
+ content: any[];
161
+ problems: string[];
162
+ }>;
@@ -0,0 +1,294 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Links between docs, named by doc identity (spec:AST-047 FR9).
5
+ *
6
+ * @input A link target, `[<provider>:]<kind>:<name>`, written as a `reference`
7
+ * block, in a `workflow` step's `references`, or inline in text as
8
+ * `{@link <target>}`; and a resolver that finds the doc a target names.
9
+ * @output The same content with every link resolved: an inline link becomes
10
+ * the `astryx docs` command that opens its doc, and a block carries a `link`
11
+ * with the doc's identity, route, title, summary, and command. A reference
12
+ * block in a topic section also carries the `content` it includes. A link
13
+ * that names no doc is a problem, never a guess.
14
+ * @position Pure. The docs adapter owns the resolver (the project's docs tree
15
+ * and topic catalog) and applies this to every read and every doctor check.
16
+ */
17
+
18
+ /** The kinds a link can name: every authored doc kind. */
19
+ export const LINK_KINDS = new Set([
20
+ 'generic',
21
+ 'command',
22
+ 'function',
23
+ 'schema',
24
+ 'enum',
25
+ 'namespace',
26
+ 'component',
27
+ 'page',
28
+ 'block',
29
+ ]);
30
+
31
+ /** An inline link in text: `{@link <target>}`. */
32
+ export const INLINE_LINK = /\{@link\s+([^{}]+?)\s*\}/g;
33
+
34
+ /** Code spans in text, which show link syntax as written. */
35
+ const CODE_SPAN = /(`[^`]*`)/;
36
+
37
+ /**
38
+ * @typedef {object} LinkTarget
39
+ * @property {string | null} provider the provider id it names, or null for the
40
+ * linking doc's own provider
41
+ * @property {string} kind
42
+ * @property {string} name
43
+ */
44
+
45
+ /**
46
+ * The doc a link opens, as a docs read returns it.
47
+ * @typedef {object} DocLink
48
+ * @property {string} target the target as written: `[<provider>:]<kind>:<name>`
49
+ * @property {string} id the doc's identity (a DocId)
50
+ * @property {string} route what `astryx docs <route>` opens
51
+ * @property {string} title
52
+ * @property {string} summary
53
+ * @property {string} command the command that opens it, `astryx docs <route>`
54
+ */
55
+
56
+ /**
57
+ * Finds the doc a target names, or says why none matches.
58
+ * @typedef {(target: string) => Promise<DocLink | {problem: string}>} LinkResolver
59
+ */
60
+
61
+ /**
62
+ * @typedef {object} LinkProblem
63
+ * @property {string} target the target as written
64
+ * @property {string} message
65
+ * @property {string} [section] the key of the section the link sits in
66
+ * @property {true} [include] the problem is in a `reference` block, which
67
+ * includes content rather than linking to it: a reader loses that content,
68
+ * so the authoring check fails on it
69
+ */
70
+
71
+ /**
72
+ * What a `reference` block in a topic section includes of the doc it names:
73
+ * stable content blocks, with anything it could not include named in
74
+ * `problems`.
75
+ * @typedef {(block: any) => Promise<{content: any[], problems: string[]}>} DocIncluder
76
+ */
77
+
78
+ /**
79
+ * Parse `[<provider>:]<kind>:<name>`. A name may hold spaces
80
+ * (`command:theme build`); no part holds a colon.
81
+ * @param {unknown} target
82
+ * @returns {LinkTarget | {error: string}}
83
+ */
84
+ export function parseLinkTarget(target) {
85
+ if (typeof target !== 'string' || target.trim() === '') {
86
+ return {
87
+ error:
88
+ 'a link target is a non-empty string: `[<provider>:]<kind>:<name>`',
89
+ };
90
+ }
91
+ const parts = target.trim().split(':');
92
+ if (parts.length < 2 || parts.length > 3 || parts.some(p => p.trim() === '')) {
93
+ return {error: `"${target}" is not \`[<provider>:]<kind>:<name>\``};
94
+ }
95
+ const provider = parts.length === 3 ? parts[0].trim() : null;
96
+ const kind = parts[parts.length - 2].trim();
97
+ const name = parts[parts.length - 1].trim();
98
+ if (!LINK_KINDS.has(kind)) {
99
+ return {
100
+ error: `"${target}" names the kind "${kind}"; a doc kind is one of ${[
101
+ ...LINK_KINDS,
102
+ ].join(', ')}`,
103
+ };
104
+ }
105
+ return {provider, kind, name};
106
+ }
107
+
108
+ /**
109
+ * Text with each inline link replaced by its name, for indexes that must not
110
+ * read the link syntax as words.
111
+ * @param {string} text
112
+ * @returns {string}
113
+ */
114
+ export function unlinkText(text) {
115
+ return text
116
+ .split(CODE_SPAN)
117
+ .map(part =>
118
+ part.startsWith('`')
119
+ ? part
120
+ : part.replace(INLINE_LINK, (_, target) => {
121
+ const parsed = parseLinkTarget(target);
122
+ return 'error' in parsed ? target : parsed.name;
123
+ }),
124
+ )
125
+ .join('');
126
+ }
127
+
128
+ /**
129
+ * Resolve every link in a list of content blocks, in order.
130
+ * @param {any[]} blocks
131
+ * @param {LinkResolver} resolve
132
+ * @param {{section?: string}} [at]
133
+ * @param {DocIncluder} [include] how a `reference` block includes the doc it
134
+ * names; without it, the block only carries its `link`
135
+ * @returns {Promise<{content: any[], problems: LinkProblem[]}>}
136
+ */
137
+ export async function linkBlocks(blocks, resolve, at = {}, include) {
138
+ /** @type {LinkProblem[]} */
139
+ const problems = [];
140
+ const where = at.section ? {section: at.section} : {};
141
+ /**
142
+ * @param {string} target
143
+ * @returns {Promise<DocLink | null>}
144
+ */
145
+ const find = async target => {
146
+ const found = await resolve(target);
147
+ if ('problem' in found) {
148
+ problems.push({target, message: found.problem, ...where});
149
+ return null;
150
+ }
151
+ return found;
152
+ };
153
+ /**
154
+ * @param {unknown} text
155
+ * @returns {Promise<any>}
156
+ */
157
+ const linkText = async text => {
158
+ if (typeof text !== 'string' || !text.includes('{@link')) return text;
159
+ let out = '';
160
+ // A link written inside code ticks is shown as written: it documents the
161
+ // syntax, it does not link.
162
+ for (const part of text.split(CODE_SPAN)) {
163
+ if (part.startsWith('`')) {
164
+ out += part;
165
+ continue;
166
+ }
167
+ let last = 0;
168
+ for (const match of part.matchAll(INLINE_LINK)) {
169
+ const link = await find(match[1]);
170
+ const at = /** @type {number} */ (match.index);
171
+ out += part.slice(last, at);
172
+ // A link that names no doc prints as written: text that was never
173
+ // meant as a link reads the way it did, and doctor names the problem.
174
+ out += link ? `\`${link.command}\`` : match[0];
175
+ last = at + match[0].length;
176
+ }
177
+ out += part.slice(last);
178
+ }
179
+ return out;
180
+ };
181
+ /**
182
+ * @param {unknown[]} values
183
+ * @returns {Promise<any[]>}
184
+ */
185
+ const linkAll = async values => {
186
+ const out = [];
187
+ for (const value of values) out.push(await linkText(value));
188
+ return out;
189
+ };
190
+
191
+ const content = [];
192
+ for (const block of blocks ?? []) {
193
+ switch (block?.type) {
194
+ case 'prose':
195
+ case 'heading':
196
+ content.push({...block, text: await linkText(block.text)});
197
+ break;
198
+ case 'list':
199
+ content.push({...block, items: await linkAll(block.items ?? [])});
200
+ break;
201
+ case 'table': {
202
+ const rows = [];
203
+ for (const row of block.rows ?? []) rows.push(await linkAll(row));
204
+ content.push({
205
+ ...block,
206
+ headers: await linkAll(block.headers ?? []),
207
+ rows,
208
+ });
209
+ break;
210
+ }
211
+ case 'reference': {
212
+ // A reference includes content, so what it cannot include is a
213
+ // problem the authoring check fails on, not one that prints as written.
214
+ const found = await resolve(block.target);
215
+ if ('problem' in found) {
216
+ problems.push({
217
+ target: block.target,
218
+ message: found.problem,
219
+ include: true,
220
+ ...where,
221
+ });
222
+ content.push({...block, link: null});
223
+ break;
224
+ }
225
+ /** @type {any} */
226
+ const linked = {...block, link: found};
227
+ if (include) {
228
+ const included = await include(block);
229
+ for (const message of included.problems) {
230
+ problems.push({
231
+ target: block.target,
232
+ message,
233
+ include: true,
234
+ ...where,
235
+ });
236
+ }
237
+ linked.content = included.content;
238
+ }
239
+ content.push(linked);
240
+ break;
241
+ }
242
+ case 'workflow': {
243
+ const steps = [];
244
+ for (const step of block.steps ?? []) {
245
+ /** @type {any} */
246
+ const linked = {...step};
247
+ if (step.description != null) {
248
+ linked.description = await linkText(step.description);
249
+ }
250
+ if (Array.isArray(step.references)) {
251
+ const links = [];
252
+ for (const target of step.references) links.push(await find(target));
253
+ linked.links = links;
254
+ }
255
+ steps.push(linked);
256
+ }
257
+ content.push({...block, steps});
258
+ break;
259
+ }
260
+ case 'collection':
261
+ problems.push({
262
+ target: `collection:${block.source?.slot ?? ''}`,
263
+ message:
264
+ 'a collection block is not rendered yet; link each doc with a reference block instead',
265
+ ...where,
266
+ });
267
+ content.push(block);
268
+ break;
269
+ default:
270
+ content.push(block);
271
+ }
272
+ }
273
+ return {content, problems};
274
+ }
275
+
276
+ /**
277
+ * Resolve every link in a topic's sections.
278
+ * @param {any[]} sections
279
+ * @param {LinkResolver} resolve
280
+ * @returns {Promise<{sections: any[], problems: LinkProblem[]}>}
281
+ */
282
+ export async function linkSections(sections, resolve) {
283
+ /** @type {LinkProblem[]} */
284
+ const problems = [];
285
+ const out = [];
286
+ for (const section of sections ?? []) {
287
+ const linked = await linkBlocks(section.content, resolve, {
288
+ section: section.id ?? section.title,
289
+ });
290
+ problems.push(...linked.problems);
291
+ out.push({...section, content: linked.content});
292
+ }
293
+ return {sections: out, problems};
294
+ }