@shoalrun/cli 0.1.0-alpha.19 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/CODE-REFERENCES.md +137 -0
  3. package/README.md +19 -6
  4. package/adapters/python/requirements.txt +1 -0
  5. package/adapters/python/shoalrun_reference.py +234 -0
  6. package/agent-docs/.well-known/shoalrun/navigation/project-updates-50f26fe22670.txt +7 -3
  7. package/agent-docs/SNAPSHOT.json +5 -5
  8. package/agent-docs/about.md +3 -3
  9. package/agent-docs/api-docs/code-reference-settings.md +160 -0
  10. package/agent-docs/api-docs/code-reference-settings.sections/catalog-settings.md +50 -0
  11. package/agent-docs/api-docs/code-reference-settings.sections/complete-reference-page.md +46 -0
  12. package/agent-docs/api-docs/code-reference-settings.sections/converter-snapshot.md +58 -0
  13. package/agent-docs/api-docs/code-reference-settings.sections/descriptions-type-links-and-source-links.md +40 -0
  14. package/agent-docs/api-docs/code-reference-settings.sections/html-markdown-and-agents.md +38 -0
  15. package/agent-docs/api-docs/code-reference-settings.sections/metadata-and-selection.md +38 -0
  16. package/agent-docs/api-docs/code-reference-settings.sections/publication-sources.md +54 -0
  17. package/agent-docs/api-docs/code-reference-settings.sections/python-adapter-limits.md +38 -0
  18. package/agent-docs/api-docs/code-reference-settings.sections/related-reading.md +32 -0
  19. package/agent-docs/api-docs/code-reference-settings.sections/symbol-fields.md +56 -0
  20. package/agent-docs/api-docs/code-references.md +143 -0
  21. package/agent-docs/author/agent-overview.md +2 -2
  22. package/agent-docs/author/components/code-blocks.md +46 -1
  23. package/agent-docs/author/frontmatter-reference.md +1 -0
  24. package/agent-docs/author/frontmatter-reference.sections/categorize-connect-and-present-the-page.md +1 -0
  25. package/agent-docs/author/workspace.md +2 -2
  26. package/agent-docs/author/writing-style.md +9 -3
  27. package/agent-docs/author/writing-style.sections/adapt-this-example.md +1 -1
  28. package/agent-docs/author/writing-style.sections/choose-components-by-reader-need.md +2 -2
  29. package/agent-docs/author/writing-style.sections/evidence-and-uncertainty.md +1 -1
  30. package/agent-docs/author/writing-style.sections/example-audience-and-scope.md +1 -1
  31. package/agent-docs/author/writing-style.sections/feature-availability.md +1 -1
  32. package/agent-docs/author/writing-style.sections/ingredients-recipes-and-cookbooks.md +1 -1
  33. package/agent-docs/author/writing-style.sections/page-purpose-and-structure.md +2 -2
  34. package/agent-docs/author/writing-style.sections/procedures-and-examples.md +1 -1
  35. package/agent-docs/author/writing-style.sections/related-reading.md +1 -1
  36. package/agent-docs/author/writing-style.sections/release-notes.md +30 -0
  37. package/agent-docs/author/writing-style.sections/review-a-draft.md +1 -1
  38. package/agent-docs/author/writing-style.sections/reviewed-writing-examples.md +1 -1
  39. package/agent-docs/author/writing-style.sections/voice-and-explanations.md +1 -1
  40. package/agent-docs/changelog/2026-10-16-navigation-layouts.md +1 -1
  41. package/agent-docs/changelog/code-api-references.md +44 -0
  42. package/agent-docs/changelog/pdf-custom-languages.md +48 -0
  43. package/agent-docs/changelog.md +17 -1
  44. package/agent-docs/cli.md +11 -1
  45. package/agent-docs/cli.sections/audit-a-project-and-api-sources.md +1 -1
  46. package/agent-docs/cli.sections/author-pages.md +1 -1
  47. package/agent-docs/cli.sections/build-and-serve.md +1 -1
  48. package/agent-docs/cli.sections/inspect-config-and-operations.md +1 -1
  49. package/agent-docs/cli.sections/npm-scripts-in-this-repository.md +1 -1
  50. package/agent-docs/cli.sections/related-reading.md +1 -1
  51. package/agent-docs/cli.sections/search-and-chat.md +1 -1
  52. package/agent-docs/cli.sections/start-a-project.md +1 -1
  53. package/agent-docs/cli.sections/validate-code-reference-snapshots.md +41 -0
  54. package/agent-docs/cli.sections/working-tree-authoring-tools.md +1 -1
  55. package/agent-docs/config/apis.md +5 -1
  56. package/agent-docs/config/examples.md +3 -3
  57. package/agent-docs/config/examples.sections/feature-showcase.md +3 -3
  58. package/agent-docs/config/examples.sections/minimal-site.md +2 -2
  59. package/agent-docs/config/examples.sections/multilingual-help-center.md +2 -2
  60. package/agent-docs/config/examples.sections/parcel-api-documentation.md +2 -2
  61. package/agent-docs/config/examples.sections/related-reading.md +2 -2
  62. package/agent-docs/config/examples.sections/shared-company-preset.md +2 -2
  63. package/agent-docs/config/outputs.md +33 -12
  64. package/agent-docs/config/outputs.sections/exports.md +3 -3
  65. package/agent-docs/config/outputs.sections/metadata.md +3 -3
  66. package/agent-docs/config/outputs.sections/pdf-output.md +51 -0
  67. package/agent-docs/config/outputs.sections/related-reading.md +3 -3
  68. package/agent-docs/config.md +2 -2
  69. package/agent-docs/configuration.md +2 -2
  70. package/agent-docs/configuration.sections/build-several-sites-from-shared-content.md +1 -1
  71. package/agent-docs/configuration.sections/change-inherited-lists-deliberately.md +1 -1
  72. package/agent-docs/configuration.sections/extract-a-section-with-include.md +1 -1
  73. package/agent-docs/configuration.sections/inspect-and-validate-changes.md +1 -1
  74. package/agent-docs/configuration.sections/related-reading.md +1 -1
  75. package/agent-docs/configuration.sections/separate-config-inheritance-from-page-metadata.md +1 -1
  76. package/agent-docs/configuration.sections/share-defaults-through-extends.md +1 -1
  77. package/agent-docs/configuration.sections/start-with-one-working-file.md +2 -2
  78. package/agent-docs/configuration.sections/understand-the-three-stages.md +1 -1
  79. package/agent-docs/configuration.sections/understand-where-paths-point.md +1 -1
  80. package/agent-docs/contribute/agent-skills.md +3 -1
  81. package/agent-docs/documents.json +1009 -356
  82. package/agent-docs/features.md +6 -2
  83. package/agent-docs/index.md +2 -2
  84. package/agent-docs/provenance.json +6 -6
  85. package/agent-docs/publish/api-references.md +3 -1
  86. package/agent-docs/publish/api-references.sections/author-the-reference-page.md +3 -1
  87. package/agent-docs/publish/api-references.sections/build-and-verify-the-output.md +3 -1
  88. package/agent-docs/publish/api-references.sections/inspect-the-machine-readable-contract.md +3 -1
  89. package/agent-docs/publish/api-references.sections/next-steps.md +3 -1
  90. package/agent-docs/publish/api-references.sections/preview-a-small-api-reference.md +3 -1
  91. package/agent-docs/publish/api-references.sections/put-the-spec-in-your-project.md +3 -1
  92. package/agent-docs/publish/api-references.sections/register-the-operation.md +3 -1
  93. package/agent-docs/publish/api-references.sections/related-reading.md +3 -1
  94. package/agent-docs/publish/api-references.sections/review-imported-content-before-publishing.md +3 -1
  95. package/agent-docs/publish/api-references.sections/what-readers-get.md +3 -1
  96. package/agent-docs/publish/content-export.md +2 -0
  97. package/agent-docs/publish/content-export.sections/assets-downloads-and-notices.md +2 -0
  98. package/agent-docs/publish/content-export.sections/check-the-result.md +2 -0
  99. package/agent-docs/publish/content-export.sections/inspect-the-manifest.md +2 -0
  100. package/agent-docs/publish/content-export.sections/prerequisites.md +2 -0
  101. package/agent-docs/publish/content-export.sections/related-reading.md +2 -0
  102. package/agent-docs/publish/content-export.sections/review-exported-api-material.md +2 -0
  103. package/agent-docs/publish/content-export.sections/try-the-help-drawer-example.md +2 -0
  104. package/agent-docs/publish/content-export.sections/what-the-representations-retain.md +2 -0
  105. package/agent-docs/publish/pdf.md +184 -0
  106. package/agent-docs/publish/pdf.sections/apply-print-styling.md +65 -0
  107. package/agent-docs/publish/pdf.sections/check-the-result.md +33 -0
  108. package/agent-docs/publish/pdf.sections/export-a-page-or-publication.md +61 -0
  109. package/agent-docs/publish/pdf.sections/html-markdown-and-agents.md +31 -0
  110. package/agent-docs/publish/pdf.sections/offer-browser-page-printing.md +49 -0
  111. package/agent-docs/publish/pdf.sections/offer-direct-downloads-through-a-node-server.md +46 -0
  112. package/agent-docs/publish/pdf.sections/prerequisites.md +37 -0
  113. package/agent-docs/publish/pdf.sections/publish-an-optional-manual-download.md +39 -0
  114. package/agent-docs/publish.md +2 -0
  115. package/agent-docs/reference/agent-skills.md +7 -3
  116. package/agent-docs/search-index.json +1 -1
  117. package/agent-docs/start.md +4 -4
  118. package/agent-docs/start.sections/add-the-page-to-navigation.md +2 -2
  119. package/agent-docs/start.sections/build-and-check-both-outputs.md +2 -2
  120. package/agent-docs/start.sections/build-this-site.md +2 -2
  121. package/agent-docs/start.sections/check-writing-style.md +2 -2
  122. package/agent-docs/start.sections/create-a-page.md +2 -2
  123. package/agent-docs/start.sections/make-it-yours.md +2 -2
  124. package/agent-docs/start.sections/migrating-from-github-packages.md +2 -2
  125. package/agent-docs/start.sections/prerequisites.md +2 -2
  126. package/agent-docs/start.sections/related-reading.md +2 -2
  127. package/agent-docs/start.sections/start-your-own-project.md +4 -4
  128. package/agent-docs/status.md +9 -5
  129. package/agent-docs/styling/themes.md +7 -3
  130. package/agent-docs/what-is-shoalrun.md +2 -2
  131. package/assets/copy-markdown.js +1 -1
  132. package/assets/print-view.js +165 -0
  133. package/assets/print.js +63 -0
  134. package/dist/artifact-bundle.d.ts +2 -0
  135. package/dist/artifact-bundle.js +13 -3
  136. package/dist/authoring-snapshot.js +1 -1
  137. package/dist/browser-print-assets.d.ts +9 -0
  138. package/dist/browser-print-assets.js +45 -0
  139. package/dist/cli-discovery.d.ts +1 -1
  140. package/dist/cli-discovery.js +1 -0
  141. package/dist/cli.js +7 -0
  142. package/dist/code-reference-model.d.ts +85 -0
  143. package/dist/code-reference-model.js +73 -0
  144. package/dist/code-references.d.ts +23 -0
  145. package/dist/code-references.js +371 -0
  146. package/dist/compiler-config.d.ts +1 -0
  147. package/dist/compiler.d.ts +4 -2
  148. package/dist/compiler.js +33 -7
  149. package/dist/config.js +1 -1
  150. package/dist/content-export-command.d.ts +77 -1
  151. package/dist/content-export-command.js +5 -2
  152. package/dist/content-export-config.d.ts +4 -1
  153. package/dist/content-export-config.js +10 -5
  154. package/dist/content-export-render.d.ts +9 -3
  155. package/dist/content-export-render.js +11 -7
  156. package/dist/content-export.d.ts +126 -0
  157. package/dist/content-export.js +267 -124
  158. package/dist/content-source.d.ts +2 -0
  159. package/dist/content-workspace.d.ts +1 -0
  160. package/dist/content-workspace.js +2 -0
  161. package/dist/fonts.d.ts +9 -0
  162. package/dist/fonts.js +58 -3
  163. package/dist/frontmatter.js +5 -2
  164. package/dist/highlight-languages.d.ts +9 -0
  165. package/dist/highlight-languages.js +52 -0
  166. package/dist/highlight.d.ts +4 -1
  167. package/dist/highlight.js +20 -5
  168. package/dist/output-commit.d.ts +1 -1
  169. package/dist/output-commit.js +4 -2
  170. package/dist/page-materialization.js +3 -2
  171. package/dist/page-renderer.d.ts +1 -0
  172. package/dist/page-renderer.js +8 -1
  173. package/dist/pdf-button.d.ts +22 -0
  174. package/dist/pdf-button.js +55 -0
  175. package/dist/pdf-config.d.ts +28 -0
  176. package/dist/pdf-config.js +43 -0
  177. package/dist/pdf-http.d.ts +36 -0
  178. package/dist/pdf-http.js +132 -0
  179. package/dist/pdf-project.d.ts +6 -0
  180. package/dist/pdf-project.js +31 -0
  181. package/dist/pdf-renderer.d.ts +45 -0
  182. package/dist/pdf-renderer.js +208 -0
  183. package/dist/pdf-site.d.ts +37 -0
  184. package/dist/pdf-site.js +65 -0
  185. package/dist/pdf-stylesheet.d.ts +1 -0
  186. package/dist/pdf-stylesheet.js +57 -0
  187. package/dist/publication-render.d.ts +2 -1
  188. package/dist/publication-render.js +2 -2
  189. package/dist/representation-tree.js +5 -1
  190. package/dist/server.d.ts +2 -0
  191. package/dist/server.js +35 -8
  192. package/dist/vercel.js +6 -0
  193. package/examples/code-references/content/start.md +53 -0
  194. package/examples/code-references/docs.config.yaml +262 -0
  195. package/examples/code-references/python/orchard/__init__.py +11 -0
  196. package/examples/code-references/python/orchard/client.py +212 -0
  197. package/examples/code-references/python/orchard/errors.py +31 -0
  198. package/examples/code-references/python/orchard/models.py +69 -0
  199. package/examples/code-references/python/orchard/pagination.py +39 -0
  200. package/examples/code-references/reference.json +1244 -0
  201. package/examples/custom-language/README.md +32 -0
  202. package/examples/custom-language/content/start.mdx +28 -0
  203. package/examples/custom-language/docs.config.yaml +18 -0
  204. package/examples/custom-language/grammars/custompy.tmLanguage.json +7 -0
  205. package/examples/pdf/README.md +27 -0
  206. package/examples/pdf/content/operations.mdx +26 -0
  207. package/examples/pdf/content/start.mdx +28 -0
  208. package/examples/pdf/docs.config.yaml +29 -0
  209. package/examples/pdf/print.css +9 -0
  210. package/package.json +11 -2
  211. package/schemas/code-reference.schema.json +306 -0
  212. package/skills/shoalrun-author/SKILL.md +6 -0
  213. package/skills/shoalrun-author/references/code-api-references.md +15 -0
  214. package/skills/shoalrun-author/references/pdf-export.md +15 -0
  215. package/skills/shoalrun-brand/SKILL.md +2 -0
  216. package/skills/shoalrun-pdf/SKILL.md +49 -0
  217. package/skills/shoalrun-pdf/assets/print.css +49 -0
  218. package/skills/shoalrun-pdf/references/print-design.md +75 -0
  219. package/styles/site.css +64 -3
@@ -0,0 +1,371 @@
1
+ import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import { renderHtmlWithHeadings } from './html-renderer.js';
5
+ import { readCodeReference } from './code-reference-model.js';
6
+ import { explainSegments } from './config.js';
7
+ import { resolvePublication } from './compiler-config.js';
8
+ import { resolveFrontmatter, validateFrontmatter, applyFormattingLocale } from './frontmatter.js';
9
+ import { matchesFeatures, validateFeatureClassification } from './deployment-profile.js';
10
+ import { pageReaderFilters } from './reader-filters.js';
11
+ import { matchesContentFilter } from './content-filter.js';
12
+ import { documentKey } from './publication-identity.js';
13
+ import { routeFor, markdownRoute } from './content-source.js';
14
+ import { heading, paragraph, text, parsePlainMarkdown, root } from './semantic-markdown.js';
15
+ const mapping = z.record(z.string(), z.unknown());
16
+ const source = z.object({ kind: z.literal('file'), path: z.string().min(1) }).strict();
17
+ const override = z.object({ documentId: z.string().min(1).optional(), slug: z.string().startsWith('/').optional(), metadata: mapping.optional() }).strict();
18
+ const catalogSchema = z.object({
19
+ source: source.optional(),
20
+ sourceLink: z.object({ baseUrl: z.string().url().refine(value => { const url = new URL(value); return url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash; }, 'Source links need an HTTPS base URL without credentials, query or fragment') }).strict().optional(),
21
+ publications: z.array(z.object({ product: z.string().optional(), channel: z.string(), version: z.string(), language: z.string().optional(), source }).strict()).min(1).optional(),
22
+ layout: z.enum(['symbols', 'classes', 'modules']).default('symbols'),
23
+ singlePage: z.union([z.literal(false), z.object({ documentId: z.string().min(1).optional(), slug: z.string().startsWith('/').optional(), title: z.string().min(1).default('Complete API reference'), summary: z.string().min(1).default('Classes, functions, and fields in this publication.'), metadata: mapping.optional() }).strict()]).optional(),
24
+ pathPrefix: z.string().regex(/^\/[a-zA-Z0-9_/-]+$/).default('/reference/code'),
25
+ metadata: mapping.optional(),
26
+ include: z.array(z.string()).optional(), exclude: z.array(z.string()).optional(),
27
+ symbols: z.record(z.string(), override).optional()
28
+ }).strict().refine(value => !!value.source !== !!value.publications, 'Declare source or publications');
29
+ const hash = (value) => createHash('sha256').update(value).digest('hex').slice(0, 16);
30
+ export function codeDocumentId(catalog, symbol) { return `reference.${catalog}.${/^[A-Za-z0-9_.-]+$/.test(symbol) ? symbol : hash(symbol)}`; }
31
+ export async function materializeCodeReferences(input) {
32
+ const { config, loaded, plan, base, basePath, variants, language, readerFilters, contentFilter, dependencies } = input;
33
+ if (config.codeReferences === undefined)
34
+ return;
35
+ const catalogs = z.record(z.string().regex(/^[a-z][a-z0-9-]*$/), catalogSchema).parse(config.codeReferences);
36
+ const owners = new Map([...plan.knownDocumentIds].map(id => [id, 'authored']));
37
+ for (const [catalogId, catalog] of Object.entries(catalogs)) {
38
+ const bindings = catalog.publications ?? [{ ...resolvePublication(config.publications, config.publications.defaults ?? {}, language), source: catalog.source }];
39
+ const seenPublications = new Set();
40
+ for (const [index, binding] of bindings.entries()) {
41
+ const variant = resolvePublication(config.publications, binding, language);
42
+ const identity = documentKey(catalogId, variant);
43
+ if (seenPublications.has(identity))
44
+ throw new Error(`Duplicate code reference publication ${identity}`);
45
+ seenPublications.add(identity);
46
+ const segments = ['codeReferences', catalogId, ...(catalog.publications ? ['publications', String(index)] : []), 'source', 'path'];
47
+ const owner = explainSegments(loaded, segments).source.file;
48
+ if (path.isAbsolute(binding.source.path) || /^[a-z][a-z0-9+.-]*:/i.test(binding.source.path))
49
+ throw new Error('Code reference sources need local relative paths');
50
+ const file = path.resolve(path.dirname(owner), binding.source.path);
51
+ dependencies.add(file);
52
+ const snapshot = await readCodeReference(file, base);
53
+ const symbols = new Map(snapshot.symbols.map(symbol => [symbol.id, symbol]));
54
+ for (const name of [...catalog.include ?? [], ...catalog.exclude ?? [], ...Object.keys(catalog.symbols ?? {})])
55
+ if (!symbols.has(name))
56
+ throw new Error(`Code reference ${catalogId}: unknown selected symbol ${name}`);
57
+ const visibility = new Map();
58
+ for (const symbol of snapshot.symbols) {
59
+ const chain = [];
60
+ let ancestor = symbol;
61
+ while (ancestor && !visibility.has(ancestor.id)) {
62
+ chain.push(ancestor);
63
+ ancestor = ancestor.parent ? symbols.get(ancestor.parent) : undefined;
64
+ }
65
+ let visible = ancestor ? visibility.get(ancestor.id) : true;
66
+ for (const entry of chain.reverse()) {
67
+ visible = visible && entry.visibility === 'public';
68
+ visibility.set(entry.id, visible);
69
+ }
70
+ }
71
+ for (const symbol of snapshot.symbols) {
72
+ const selected = visibility.get(symbol.id) && (!catalog.include || catalog.include.includes(symbol.id)) && !catalog.exclude?.includes(symbol.id);
73
+ const setting = catalog.symbols?.[symbol.id];
74
+ const inherited = { ...catalog.metadata };
75
+ if (setting?.metadata && (Object.hasOwn(setting.metadata, 'shared') || Object.hasOwn(setting.metadata, 'requires'))) {
76
+ delete inherited.shared;
77
+ delete inherited.requires;
78
+ }
79
+ const metadata = { ...inherited, ...setting?.metadata };
80
+ const id = setting?.documentId ?? codeDocumentId(catalogId, symbol.id);
81
+ const ownerId = `${catalogId}:${symbol.id}`;
82
+ if (owners.has(id) && owners.get(id) !== ownerId)
83
+ throw new Error(`Duplicate code reference document ID ${id}`);
84
+ owners.set(id, ownerId);
85
+ plan.knownDocumentIds.add(id);
86
+ const existing = plan.knownDocumentTypes.get(id);
87
+ if (existing && existing !== 'code-reference')
88
+ throw new Error(`Duplicate code reference document ID ${id}`);
89
+ plan.knownDocumentTypes.set(id, 'code-reference');
90
+ const pageSource = {
91
+ ...metadata, id, title: symbol.name, summary: symbol.summary ?? `${symbol.kind}: ${symbol.id}`,
92
+ type: 'code-reference', slug: setting?.slug ?? `${catalog.pathPrefix}/${symbol.id.toLowerCase().replace(/[^a-z0-9_-]+/g, '-')}-${hash(symbol.id)}`,
93
+ ...variant
94
+ };
95
+ const frontmatter = resolveFrontmatter(pageSource, config.frontmatter);
96
+ frontmatter.access ??= { policy: 'public' };
97
+ if (catalog.layout !== 'symbols')
98
+ frontmatter.tocDepth ??= 2;
99
+ for (const [field, expected] of Object.entries({ id, type: 'code-reference', ...variant }))
100
+ if (frontmatter[field] !== expected)
101
+ throw new Error(`Code reference ${id}: enforced ${field} conflicts with its catalog`);
102
+ applyFormattingLocale(frontmatter, config.frontmatter, pageSource);
103
+ validateFrontmatter(frontmatter, config.frontmatter, file, pageSource);
104
+ const classification = loaded.deployment?.context.classificationPublications ?? variants;
105
+ if (selected && frontmatter.access?.policy === 'public' && classification.some(item => documentKey(catalogId, item) === identity))
106
+ validateFeatureClassification(metadata, loaded.deployment?.context, `${file}: ${symbol.id}`);
107
+ if (!selected || !matchesFeatures(frontmatter.requires, loaded.deployment?.context, `${file}: ${symbol.id}`) || !variants.some(item => documentKey(catalogId, item) === identity))
108
+ continue;
109
+ if (frontmatter.access?.policy !== 'public')
110
+ continue;
111
+ const marks = pageReaderFilters(frontmatter, readerFilters, file);
112
+ if (!matchesContentFilter(marks, contentFilter))
113
+ continue;
114
+ const key = documentKey(id, variant);
115
+ if (plan.docs.has(key))
116
+ throw new Error(`Duplicate code reference document ${id}`);
117
+ const route = routeFor(frontmatter, variant, config);
118
+ const collision = plan.routeOwners.get(route.toLowerCase());
119
+ if (collision && !input.allowRouteCollisions)
120
+ throw new Error(`Code reference route collision ${route}`);
121
+ plan.routeOwners.set(route.toLowerCase(), key);
122
+ plan.docs.set(key, { file, frontmatter, variant, route, markdownRoute: markdownRoute(route, basePath), readerFilters: marks, featureContext: loaded.deployment?.context, body: '', tree: null, codeReference: { catalog: catalogId, programmingLanguage: snapshot.programmingLanguage, symbol, layout: catalog.layout, ...(catalog.sourceLink && symbol.source ? { sourceUrl: catalog.sourceLink.baseUrl.replace(/\/$/, '') + '/' + symbol.source.path.split('/').map(encodeURIComponent).join('/') + (symbol.source.line ? `#L${symbol.source.line}` : '') } : {}) } });
123
+ }
124
+ if (catalog.singlePage) {
125
+ const single = catalog.singlePage;
126
+ const id = single.documentId ?? `reference.${catalogId}.complete`;
127
+ const ownerId = `${catalogId}:complete-page`;
128
+ if (owners.has(id) && owners.get(id) !== ownerId)
129
+ throw new Error(`Duplicate code reference document ID ${id}`);
130
+ owners.set(id, ownerId);
131
+ plan.knownDocumentIds.add(id);
132
+ plan.knownDocumentTypes.set(id, 'code-reference');
133
+ const inherited = { ...catalog.metadata };
134
+ if (single.metadata && (Object.hasOwn(single.metadata, 'shared') || Object.hasOwn(single.metadata, 'requires'))) {
135
+ delete inherited.shared;
136
+ delete inherited.requires;
137
+ }
138
+ const metadata = { ...inherited, ...single.metadata };
139
+ const source = { ...metadata, id, title: single.title, summary: single.summary, type: 'code-reference', slug: single.slug ?? `${catalog.pathPrefix}/all`, ...variant };
140
+ const frontmatter = resolveFrontmatter(source, config.frontmatter);
141
+ frontmatter.access ??= { policy: 'public' };
142
+ for (const [field, expected] of Object.entries({ id, type: 'code-reference', ...variant }))
143
+ if (frontmatter[field] !== expected)
144
+ throw new Error(`Code reference ${id}: enforced ${field} conflicts with its catalog`);
145
+ applyFormattingLocale(frontmatter, config.frontmatter, source);
146
+ validateFrontmatter(frontmatter, config.frontmatter, file, source);
147
+ frontmatter.tocDepth ??= 2;
148
+ const active = variants.some(item => documentKey(catalogId, item) === identity);
149
+ const classification = loaded.deployment?.context.classificationPublications ?? variants;
150
+ if (frontmatter.access?.policy === 'public' && classification.some(item => documentKey(catalogId, item) === identity))
151
+ validateFeatureClassification(metadata, loaded.deployment?.context, `${file}: ${id}`);
152
+ const granted = matchesFeatures(frontmatter.requires, loaded.deployment?.context, `${file}: ${id}`);
153
+ const marks = pageReaderFilters(frontmatter, readerFilters, file);
154
+ const hasContent = [...plan.docs.values()].some(doc => doc.codeReference?.catalog === catalogId && documentKey(catalogId, doc.variant) === identity);
155
+ if (active && granted && hasContent && frontmatter.access.policy === 'public' && matchesContentFilter(marks, contentFilter)) {
156
+ const key = documentKey(id, variant);
157
+ if (plan.docs.has(key))
158
+ throw new Error(`Duplicate code reference document ${id}`);
159
+ const route = routeFor(frontmatter, variant, config);
160
+ if (plan.routeOwners.has(route.toLowerCase()) && !input.allowRouteCollisions)
161
+ throw new Error(`Code reference route collision ${route}`);
162
+ plan.routeOwners.set(route.toLowerCase(), key);
163
+ plan.docs.set(key, { file, frontmatter, variant, route, markdownRoute: markdownRoute(route, basePath), readerFilters: marks, featureContext: loaded.deployment?.context, body: '', tree: null,
164
+ codeReference: { catalog: catalogId, programmingLanguage: snapshot.programmingLanguage, complete: true, symbol: { id, name: single.title, kind: 'module', visibility: 'public' } } });
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ /** Imported prose is inert Markdown; it never enters MDX or project directive expansion. */
171
+ export function renderCodeReference(reference, doc, documents) {
172
+ const eligible = new Map();
173
+ for (const candidate of documents.values())
174
+ if (candidate.codeReference?.catalog === reference.catalog && !candidate.codeReference.complete && documentKey('scope', candidate.variant) === documentKey('scope', doc.variant))
175
+ eligible.set(candidate.codeReference.symbol.id, candidate);
176
+ const owner = (page) => {
177
+ let parent = page.codeReference.symbol.parent;
178
+ while (parent) {
179
+ const candidate = eligible.get(parent);
180
+ if (!candidate)
181
+ return;
182
+ const kind = candidate.codeReference.symbol.kind;
183
+ if (['module', 'namespace'].includes(kind) || reference.layout === 'classes' && ['class', 'interface', 'enum', 'type'].includes(kind))
184
+ return parent;
185
+ parent = candidate.codeReference.symbol.parent;
186
+ }
187
+ };
188
+ const embedded = [...eligible.values()].filter(page => reference.complete || reference.layout !== undefined && reference.layout !== 'symbols' && owner(page) === reference.symbol.id)
189
+ .sort((a, b) => a.codeReference.symbol.id < b.codeReference.symbol.id ? -1 : 1);
190
+ const blocks = reference.complete ? [] : renderCodeSymbol(reference, doc, eligible, false).children;
191
+ const headings = new Map();
192
+ for (const page of embedded) {
193
+ const entry = heading(2, page.codeReference.symbol.id);
194
+ headings.set(entry, page);
195
+ const content = renderCodeSymbol(page.codeReference, page, eligible, true).children;
196
+ const deepen = (node) => { if (node.type === 'heading')
197
+ node.depth = Math.min(6, node.depth + 1); for (const child of node.children ?? [])
198
+ deepen(child); };
199
+ content.forEach(deepen);
200
+ blocks.push(entry, ...content);
201
+ }
202
+ const tree = root(blocks);
203
+ if (embedded.length) {
204
+ // Use the renderer's actual heading allocation, including duplicate headings.
205
+ const reserved = ['main', 'docs-navigation', 'docs-sections', 'documentation-menu', 'page-outline', 'nav-filter', 'examples-title', 'api-view-select'];
206
+ const rendered = renderHtmlWithHeadings(tree, undefined, reserved).headings;
207
+ const targets = new Map();
208
+ let index = 0;
209
+ const collect = (node) => {
210
+ if (node.type === 'heading' && node.depth >= 2 && node.depth <= 6) {
211
+ const target = headings.get(node);
212
+ if (target)
213
+ targets.set(target.frontmatter.id, rendered[index].id);
214
+ index++;
215
+ }
216
+ for (const child of node.children ?? [])
217
+ collect(child);
218
+ };
219
+ collect(tree);
220
+ const relink = (node) => {
221
+ const anchor = targets.get(node.exportDocumentId);
222
+ if (node.type === 'link' && anchor) {
223
+ node.url = `${doc.route}#${anchor}`;
224
+ node.exportDocumentId = doc.frontmatter.id;
225
+ }
226
+ for (const child of node.children ?? [])
227
+ relink(child);
228
+ };
229
+ relink(tree);
230
+ }
231
+ return tree;
232
+ }
233
+ function renderCodeSymbol(reference, doc, eligible, embedded) {
234
+ const prose = (value = '') => {
235
+ const parsed = parsePlainMarkdown(value);
236
+ const definitions = new Map();
237
+ const collect = (node) => { if (node.type === 'definition')
238
+ definitions.set(node.identifier.toLowerCase(), node); for (const child of node.children ?? [])
239
+ collect(child); };
240
+ parsed.forEach(collect);
241
+ const clean = (node) => {
242
+ if (node.type === 'definition')
243
+ return [];
244
+ if (node.type === 'linkReference') {
245
+ const definition = definitions.get(node.identifier.toLowerCase());
246
+ if (!definition)
247
+ return (node.children ?? []).flatMap(clean);
248
+ node = { type: 'link', url: definition.url, title: definition.title, children: node.children };
249
+ }
250
+ if (node.type === 'image' || node.type === 'imageReference')
251
+ throw new Error(`${doc.file}: code reference prose images need explicit asset support`);
252
+ if (typeof node.url === 'string') {
253
+ if (node.url.startsWith('symbol:')) {
254
+ const target = eligible.get(node.url.slice(7));
255
+ if (!target)
256
+ return (node.children ?? []).flatMap(clean);
257
+ node = { ...node, url: target.route, exportDocumentId: target.frontmatter.id };
258
+ }
259
+ else {
260
+ const url = new URL(node.url, 'https://code-reference.invalid');
261
+ if (!['https:', 'mailto:'].includes(url.protocol) || url.username || url.password || url.origin === 'https://code-reference.invalid')
262
+ throw new Error(`${doc.file}: unsupported code reference prose URL ${node.url}`);
263
+ }
264
+ }
265
+ return [{ ...node, ...(node.children ? { children: node.children.flatMap(clean) } : {}) }];
266
+ };
267
+ return parsed.flatMap(clean);
268
+ };
269
+ const symbol = reference.symbol;
270
+ const inlineCode = (value) => ({ type: 'inlineCode', value });
271
+ const code = (value, signature = false) => ({ type: 'code', lang: reference.programmingLanguage, value,
272
+ ...(signature ? { data: { hProperties: { className: [`language-${reference.programmingLanguage}`, 'code-reference-signature'] } } } : {}) });
273
+ const pageLink = (page, label = page.frontmatter.title) => ({ type: 'link', url: page.route, exportDocumentId: page.frontmatter.id, children: [inlineCode(label)] });
274
+ // References are explicit converter data; never guess an ambiguous short name.
275
+ const typeNodes = (value, context = symbol) => {
276
+ const tokens = Object.keys(context.typeReferences ?? {}).sort((a, b) => b.length - a.length);
277
+ if (!tokens.length)
278
+ return [inlineCode(value)];
279
+ const output = [];
280
+ let plain = '';
281
+ for (let offset = 0; offset < value.length;) {
282
+ const match = tokens.find(token => value.startsWith(token, offset) && !/[\p{L}\p{N}_.$]/u.test(value[offset - 1] ?? '') && !/[\p{L}\p{N}_.$]/u.test(value[offset + token.length] ?? ''));
283
+ const page = match ? eligible.get(context.typeReferences[match]) : undefined;
284
+ if (match && page) {
285
+ if (plain)
286
+ output.push(inlineCode(plain));
287
+ plain = '';
288
+ output.push(pageLink(page, match));
289
+ offset += match.length;
290
+ }
291
+ else {
292
+ plain += value[offset];
293
+ offset++;
294
+ }
295
+ }
296
+ if (plain)
297
+ output.push(inlineCode(plain));
298
+ return output;
299
+ };
300
+ const table = (headers, rows, label) => ({ type: 'table', align: headers.map(() => null),
301
+ data: { referenceTableLabel: label, hProperties: { className: ['code-reference-table'] } }, children: [headers.map(value => [text(value)]), ...rows].map(cells => ({ type: 'tableRow', children: cells.map(children => ({ type: 'tableCell', children })) })) });
302
+ const summary = (value) => {
303
+ const nodes = prose(value);
304
+ const paragraph = nodes.find(node => node.type === 'paragraph');
305
+ return paragraph?.children ?? [text('—')];
306
+ };
307
+ const description = prose(symbol.description);
308
+ // Hide the repeated summary only in the website shell; exports retain it.
309
+ if (description[0]?.type === 'paragraph') {
310
+ const plain = (node) => node.value ?? (node.children ?? []).map(plain).join('');
311
+ if (!embedded && plain(description[0]).trim() === doc.frontmatter.summary.trim())
312
+ description[0] = { ...description[0], data: { hProperties: { className: ['code-reference-summary'] } } };
313
+ }
314
+ const identity = paragraph({ type: 'strong', children: [text(symbol.kind.charAt(0).toUpperCase() + symbol.kind.slice(1))] }, text(' '), inlineCode(symbol.id), ...(reference.sourceUrl ? [text(' · '), { type: 'link', url: reference.sourceUrl, children: [text('View source')] }] : []));
315
+ identity.data = { hProperties: { className: ['code-reference-identity'] } };
316
+ const blocks = [identity, ...(symbol.signatures?.length ? [code(symbol.signatures.join('\n\n'), true)] : []), ...description];
317
+ if (symbol.constructorSignature)
318
+ blocks.push(heading(2, 'Constructor'), code(symbol.constructorSignature, true));
319
+ if (symbol.bases?.length)
320
+ blocks.push(paragraph({ type: 'strong', children: [text('Bases: ')] }, ...symbol.bases.flatMap((base, i) => [...(i ? [text(', ')] : []), ...typeNodes(base)])));
321
+ if (symbol.annotation)
322
+ blocks.push(paragraph({ type: 'strong', children: [text('Type: ')] }, ...typeNodes(symbol.annotation)));
323
+ if (symbol.required !== undefined || symbol.default !== undefined)
324
+ blocks.push(paragraph(...(symbol.required === undefined ? [] : [text(symbol.required ? 'Required' : 'Optional')]), ...(symbol.default === undefined ? [] : [text(symbol.required === undefined ? 'Default: ' : ' · Default: '), inlineCode(symbol.default)])));
325
+ const details = (title, items) => {
326
+ if (!items.length)
327
+ return;
328
+ blocks.push(heading(2, title));
329
+ blocks.push(table(title === 'Parameters' ? ['Parameter', 'Type / default', 'Description'] : ['Exception', 'Description'], items.map(item => {
330
+ const kind = [...(item.type ? typeNodes(item.type) : [text('—')]), ...(item.default === undefined ? [] : [text(' = '), inlineCode(item.default)])];
331
+ return title === 'Parameters' ? [[inlineCode(item.name)], kind, summary(item.description)] : [typeNodes(item.name), summary(item.description)];
332
+ }), title));
333
+ // Preserve richer Markdown without forcing block content into a table cell.
334
+ for (const item of items) {
335
+ const content = prose(item.description);
336
+ if (content.length > 1 || content.some(node => node.type !== 'paragraph'))
337
+ blocks.push(heading(3, item.name), ...content);
338
+ }
339
+ };
340
+ details('Parameters', symbol.parameters ?? []);
341
+ for (const [title, result] of [['Returns', symbol.returns], ['Yields', symbol.yields]])
342
+ if (result) {
343
+ blocks.push(heading(2, title));
344
+ if (result.type)
345
+ blocks.push(paragraph(...typeNodes(result.type)));
346
+ blocks.push(...prose(result.description));
347
+ }
348
+ details('Raises', symbol.raises ?? []);
349
+ if (symbol.examples?.length) {
350
+ blocks.push(heading(2, 'Examples'));
351
+ for (const example of symbol.examples)
352
+ blocks.push(...(example.title ? [heading(3, example.title)] : []), code(example.code));
353
+ }
354
+ const members = [...eligible.values()].filter(item => item.codeReference?.symbol.parent === symbol.id).sort((a, b) => a.frontmatter.title < b.frontmatter.title ? -1 : a.frontmatter.title > b.frontmatter.title ? 1 : 0);
355
+ const fields = members.filter(page => ['attribute', 'property', 'constant'].includes(page.codeReference.symbol.kind));
356
+ const callables = members.filter(page => ['function', 'method', 'constructor'].includes(page.codeReference.symbol.kind));
357
+ const types = members.filter(page => !fields.includes(page) && !callables.includes(page));
358
+ for (const [title, pages] of [['Fields', fields], [['module', 'namespace'].includes(symbol.kind) ? 'Functions' : 'Methods', callables], ['Members', types]])
359
+ if (pages.length) {
360
+ blocks.push(heading(2, title), table(title === 'Fields' ? ['Name', 'Type', 'Required / default', 'Description'] : ['Name', 'Kind', 'Description'], pages.map(page => {
361
+ const member = page.codeReference.symbol;
362
+ const declaration = member.signatures?.[0];
363
+ const annotation = declaration?.startsWith(member.name + ': ') ? declaration.slice(member.name.length + 2) : undefined;
364
+ return title === 'Fields' ? [[pageLink(page)], typeNodes(member.annotation ?? annotation ?? member.kind, member), [...(member.required === undefined ? [] : [text(member.required ? 'Required' : 'Optional')]), ...(member.default === undefined ? [] : [text(member.required === undefined ? '' : ' · '), inlineCode(member.default)]), ...(member.required === undefined && member.default === undefined ? [text('—')] : [])], summary(member.summary ?? member.description)] : [[pageLink(page)], [inlineCode(member.kind)], summary(member.summary ?? member.description)];
365
+ }), title));
366
+ }
367
+ const related = (symbol.seeAlso ?? []).flatMap(id => eligible.has(id) ? [eligible.get(id)] : []);
368
+ if (related.length)
369
+ blocks.push(heading(2, 'See also'), { type: 'list', ordered: false, children: related.map(page => ({ type: 'listItem', children: [paragraph(pageLink(page))] })) });
370
+ return root(blocks);
371
+ }
@@ -13,6 +13,7 @@ export interface ResolvedCompilerConfig extends ConfigMapping {
13
13
  navigation?: unknown;
14
14
  redirects?: unknown;
15
15
  apis?: unknown;
16
+ codeReferences?: unknown;
16
17
  relationships?: unknown;
17
18
  variables?: unknown;
18
19
  snippets?: unknown;
@@ -6,13 +6,14 @@ export declare function checkThemeContrast(configFile: string): Promise<{
6
6
  modes: Mode[];
7
7
  palette: string;
8
8
  }>;
9
- export declare function compile(configFile: string, destination?: string, provenance?: BuildProvenance, options?: {
9
+ type CompileOptions = {
10
10
  apiReview?: 'report';
11
11
  sourceOverrides?: ReadonlyMap<string, string>;
12
12
  authoringFiles?: ReadonlySet<string>;
13
13
  profile?: string;
14
14
  onDependency?: (file: string) => void;
15
- }): Promise<{
15
+ };
16
+ export declare function compile(configFile: string, destination?: string, provenance?: BuildProvenance, options?: CompileOptions): Promise<{
16
17
  authoringSources?: {
17
18
  id: string;
18
19
  file: string;
@@ -240,3 +241,4 @@ export declare function compile(configFile: string, destination?: string, proven
240
241
  schemaVersion: number;
241
242
  configDigest: string;
242
243
  }>;
244
+ export {};
package/dist/compiler.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { buildPdfSiteArtifacts } from './pdf-site.js';
2
+ import { resolvePdfButton, validatePdfButtonStyle } from './pdf-button.js';
1
3
  import { documentKey, publicationKey, samePublication } from './publication-identity.js';
2
4
  import { validateHeader, validateFooter } from './site-identity.js';
3
5
  import { profileSelection } from './deployment-profile.js';
@@ -16,7 +18,7 @@ import { validateAuthoringGuidance } from './authoring-guidance.js';
16
18
  import { loadFreshness } from './freshness.js';
17
19
  import { resolveAgentDiscovery } from './agent-discovery.js';
18
20
  import { iconLicenseFile } from './icons.js';
19
- import { bundledFontLicenses, bundledFonts, fontsCss, resolveFonts } from './fonts.js';
21
+ import { bundledFontLicenses, bundledFonts, fontsCss, resolveFonts, prepareFontAssets, customFontPreloads } from './fonts.js';
20
22
  import { bundledShareFonts, resolveShareImages, shareRoute } from './share-images.js';
21
23
  import { buildInfo } from './build-info.js';
22
24
  import { validateRelationships } from './cards.js';
@@ -102,6 +104,15 @@ export async function checkThemeContrast(configFile) {
102
104
  return { modes, palette: palette.name };
103
105
  }
104
106
  export async function compile(configFile, destination, provenance = {}, options = {}) {
107
+ let dispose;
108
+ try {
109
+ return await compileProject(configFile, destination, provenance, options, release => { dispose = release; });
110
+ }
111
+ finally {
112
+ dispose?.();
113
+ }
114
+ }
115
+ async function compileProject(configFile, destination, provenance, options, ownHighlighter) {
105
116
  const loaded = await loadConfig(configFile, { profile: options.profile, onDependency: options.onDependency });
106
117
  const componentPlugins = await loadComponentPlugins(loaded);
107
118
  const provenanceRecord = buildInfo(loaded.digest, provenance);
@@ -118,9 +129,15 @@ export async function compile(configFile, destination, provenance = {}, options
118
129
  const calloutTypes = resolveCalloutTypes(config.callouts, configFile);
119
130
  const badgeVariants = resolveBadgeVariants(config.badges, configFile);
120
131
  const htmlOptions = configField(config.outputs, 'html');
132
+ const pdfButton = resolvePdfButton(config.outputs);
133
+ validatePdfButtonStyle(pdfButton, config.exports);
121
134
  const highlighting = configField(htmlOptions, 'highlighting');
122
135
  const highlightingOrigin = configField(highlighting, 'module') === undefined ? configFile : explainSegments(loaded, ['outputs', 'html', 'highlighting', 'module']).source.file;
123
- const highlighter = await createCodeHighlighter(highlighting, highlightingOrigin);
136
+ const highlighter = await createCodeHighlighter(highlighting, highlightingOrigin, {
137
+ grammarOrigin: index => explainSegments(loaded, ['outputs', 'html', 'highlighting', 'languages', String(index), 'grammar']).source.file,
138
+ onDependency: options.onDependency
139
+ });
140
+ ownHighlighter(highlighter.dispose);
124
141
  const base = path.dirname(path.resolve(configFile));
125
142
  if (config.site.customCss !== undefined && (typeof config.site.customCss !== 'string' || !config.site.customCss.endsWith('.css')))
126
143
  throw new Error('site.customCss must be a local .css path');
@@ -158,6 +175,7 @@ export async function compile(configFile, destination, provenance = {}, options
158
175
  for (const face of fonts.faces)
159
176
  if (fonts.faces.some(other => other.route === face.route && other.file !== face.file))
160
177
  throw new Error(`theme.fonts: two font files share the name ${path.basename(face.file)}; rename one`);
178
+ const projectFontAssets = await prepareFontAssets(fonts);
161
179
  const generatedFontsCss = fontsCss(fonts);
162
180
  // A temporary preview aid: a floating shape and palette switcher on every page. Remove it before launch.
163
181
  // `demo` ships it hidden until a page is opened with ?theme-switcher.
@@ -313,7 +331,7 @@ export async function compile(configFile, destination, provenance = {}, options
313
331
  const cardUses = new Map();
314
332
  const publishedRelationships = [];
315
333
  const styleFiles = ['site.css', 'tokens.css', 'header.css'].map(name => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../styles', name));
316
- const scriptFiles = [...(analyticsModule ? ['analytics.js'] : []), 'code-samples.js', 'cookbook-chooser.js', 'example-panel.js', 'mode.js', 'copy-markdown.js', 'components.js', 'navigation.js', 'reader.js', ...(readerFilters ? ['reader-filters.js'] : []), 'search-highlight.js', 'search.js', 'table-filter.js', 'tabs.js', 'tooltips.js', 'file-tree.js', 'image-viewer.js', 'walkthrough.js'].map(name => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../assets', name));
334
+ const scriptFiles = [...(analyticsModule ? ['analytics.js'] : []), 'code-samples.js', 'cookbook-chooser.js', 'example-panel.js', 'mode.js', ...(pdfButton?.mode === 'print' ? ['print.js', 'print-view.js'] : []), 'copy-markdown.js', 'components.js', 'navigation.js', 'reader.js', ...(readerFilters ? ['reader-filters.js'] : []), 'search-highlight.js', 'search.js', 'table-filter.js', 'tabs.js', 'tooltips.js', 'file-tree.js', 'image-viewer.js', 'walkthrough.js'].map(name => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../assets', name));
317
335
  const documentRoutes = new Set([...docs.values()].map(doc => doc.route));
318
336
  const { apiPublications, focusedContracts, navigationEntries, navigationTagOrder, rendered, snippetUses, apiEmbedUses, operations } = await materializePages({
319
337
  variables: isConfigMapping(config.variables) ? config.variables : {}, frontmatterConfig: config.frontmatter,
@@ -379,8 +397,8 @@ export async function compile(configFile, destination, provenance = {}, options
379
397
  const resolvedNavigation = renderPublicationPages({
380
398
  freshness,
381
399
  rendered, docs, relationships, knownOperations, cardUses, publishedRelationships, cookbooks, documentRoutes,
382
- basePath, siteOrigin, colorMode, config, highlighter, editionLabel, customStyleFile, themePreset, palette,
383
- themeStyleFile, generatedPaletteCss, generatedBadgeCss, themeSwitcherData, generatedFontsCss, fontsPreload: fonts.preload,
400
+ basePath, siteOrigin, colorMode, config: options.authoringFiles ? { ...config, outputs: { ...config.outputs, html: { ...htmlOptions, pdf: false } } } : config, highlighter, editionLabel, customStyleFile, themePreset, palette,
401
+ themeStyleFile, generatedPaletteCss, generatedBadgeCss, themeSwitcherData, generatedFontsCss, fontsPreload: fonts.preload, customFontPreloads: customFontPreloads(fonts),
384
402
  shareFor, faviconRoute, logoRoute, modeToggle, header, footer, search, readerFilters, variants, externalDocuments,
385
403
  externalSiteLinks, navigationSpec, navigationEntries, navigationTagOrder
386
404
  });
@@ -442,7 +460,14 @@ export async function compile(configFile, destination, provenance = {}, options
442
460
  ] };
443
461
  }
444
462
  const apiPublicationReview = await reviewApiPublication(configFile, apiPlan, rendered, apiEmbedUses, focusedContracts, apiSources, sourceForOperation, options.apiReview);
445
- const manifestDependencies = [...new Set([...loaded.dependencies, ...componentPlugins.dependencies, ...externalManifestFiles, ...api.dependencies, ...(metadataProvider ? [metadataProvider.module] : []), ...metadataDependencies, ...(highlighter.dependency ? [highlighter.dependency] : []), ...styleFiles, ...(themeStyleFile ? [themeStyleFile] : []), ...palette.files, ...(search.module ? [search.module] : []), ...previewFiles, ...(customStyleFile ? [customStyleFile] : []), ...(customReaderModule ? [customReaderModule] : []), ...(analyticsModule ? [analyticsModule] : []), ...(mcpAnalyticsModule ? [mcpAnalyticsModule] : []), ...(shareImages.template ? [shareImages.template] : []), ...shareImages.fonts, ...(faviconFile ? [faviconFile] : []), ...heroImages.values(), ...walkthroughSources, ...fontFiles, ...fonts.faces.map(face => face.file), ...scriptFiles, iconLicenseFile, ...[...snippets.values()].map(s => s.file), ...[...docs.values()].map(d => d.file)])].sort();
463
+ if (pdfButton?.mode === 'endpoint') {
464
+ const endpoint = publicPath(pdfButton.endpoint);
465
+ const mcpEndpoint = configField(config.mcp, 'path') ?? '/mcp';
466
+ if (rendered.some(({ doc }) => doc.route === endpoint || endpoint.startsWith(`${doc.route}/`)) || redirects.some(rule => rule.from === endpoint) || endpoint === publicPath(String(mcpEndpoint)))
467
+ throw new Error(`PDF endpoint shadows an existing route: ${endpoint}`);
468
+ }
469
+ const sitePdfs = options.authoringFiles ? { artifacts: new Map(), dependencies: new Set() } : await buildPdfSiteArtifacts(configFile, { outputs: config.outputs, exports: config.exports, documents: rendered.map(page => page.doc), knownDocumentIds, basePath, profile: options.profile });
470
+ const manifestDependencies = [...new Set([...sitePdfs.dependencies, ...loaded.dependencies, ...componentPlugins.dependencies, ...externalManifestFiles, ...api.dependencies, ...(metadataProvider ? [metadataProvider.module] : []), ...metadataDependencies, ...(highlighter.dependency ? [highlighter.dependency] : []), ...(highlighter.dependencies ?? []), ...styleFiles, ...(themeStyleFile ? [themeStyleFile] : []), ...palette.files, ...(search.module ? [search.module] : []), ...previewFiles, ...(customStyleFile ? [customStyleFile] : []), ...(customReaderModule ? [customReaderModule] : []), ...(analyticsModule ? [analyticsModule] : []), ...(mcpAnalyticsModule ? [mcpAnalyticsModule] : []), ...(shareImages.template ? [shareImages.template] : []), ...shareImages.fonts, ...(faviconFile ? [faviconFile] : []), ...heroImages.values(), ...walkthroughSources, ...fontFiles, ...fonts.faces.map(face => face.file), ...scriptFiles, iconLicenseFile, ...[...snippets.values()].map(s => s.file), ...[...docs.values()].map(d => d.file)])].sort();
446
471
  const llmsDiscovery = resolveLlmsDiscovery(configField(config.outputs, 'llms'));
447
472
  const { manifest, headers, sitemaps } = buildPublicationManifest({
448
473
  configPublications: config.publications,
@@ -454,12 +479,13 @@ export async function compile(configFile, destination, provenance = {}, options
454
479
  });
455
480
  const artifactConfig = options.authoringFiles ? { ...config, mcp: false } : config;
456
481
  const { artifacts, buildId } = await assembleArtifacts({
482
+ extraArtifacts: sitePdfs.artifacts,
457
483
  base, basePath, publicPath, canonical, siteOrigin, manifest, provenanceRecord, rendered, sitemaps,
458
484
  privateDependencies: new Set([...(loaded.deployment?.file ? [loaded.deployment.file] : []), ...(loaded.deployment?.assetFiles ?? [])]),
459
485
  diagramAssets, heroImages, focusedContracts, styleFiles, themeStyleFile, generatedPaletteCss,
460
486
  generatedBadgeCss, search, synonyms, config: artifactConfig, builtInSearchProvider, customReaderModule, analyticsModule, mcpAnalyticsModule,
461
487
  previewFiles, assetDirectory, themeSwitcher, themeDirectory, palette, colorMode, generatedFontsCss,
462
- fontFiles, fonts, shareImages, shareFor, themePreset, shareFontFiles, shareCache, faviconFile,
488
+ fontFiles, projectFontAssets, fonts, shareImages, shareFor, themePreset, shareFontFiles, shareCache, faviconFile,
463
489
  faviconRoute, customStyleFile, scriptFiles, componentAssets: loaded.deployment?.context.grants ? componentPlugins.assets.filter(asset => usedComponentAssets.has(asset.route)) : componentPlugins.assets, redirects, apiPublications, api, catalog, headers,
464
490
  documentRoutes, readerFilters, apiSources
465
491
  });
package/dist/config.js CHANGED
@@ -9,7 +9,7 @@ import { parseYaml } from './yaml-value.js';
9
9
  import { validateSearch } from './search-config.js';
10
10
  import { resolveTaxonomy } from './taxonomy.js';
11
11
  import { validateCookbooks } from './cookbook-chooser.js';
12
- const allowed = new Set(['profiles', 'exports', 'schemaVersion', 'site', 'navigation', 'redirects', 'apis', 'relationships', 'variables', 'snippets', 'callouts', 'badges', 'publications', 'outputs', 'templates', 'theme', 'frontmatter', 'metadata', 'audit', 'readerFilters', 'retrieval', 'chat', 'authoring', 'search', 'cookbooks', 'analytics', 'mcp', 'components']);
12
+ const allowed = new Set(['profiles', 'exports', 'schemaVersion', 'site', 'navigation', 'redirects', 'apis', 'codeReferences', 'relationships', 'variables', 'snippets', 'callouts', 'badges', 'publications', 'outputs', 'templates', 'theme', 'frontmatter', 'metadata', 'audit', 'readerFilters', 'retrieval', 'chat', 'authoring', 'search', 'cookbooks', 'analytics', 'mcp', 'components']);
13
13
  const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
14
14
  const treeObject = (v) => object(v) && !('kind' in v && 'items' in v);
15
15
  export class ConfigError extends Error {
@@ -1,5 +1,81 @@
1
- export declare const exportHelp = "Usage: shoalrun export [config] --out DIRECTORY [--target NAME]\n [--format markdown|html|both] [--document ID]...\n [--channel C --version V --language L [--product P]] [--public-base /help/] [--review]\n [--profile FILE] [--previous-manifest FILE] [--fail-on-removed]\n\nExport selected pages, sections, and named snippets as Markdown and passive HTML.\nConfigure typed resources in an export target; --document selects whole pages.\nNavigation, facets, and a prior website build are not required.\n--review writes an API-review candidate; status needs-review exits 2 until pinned.\n--previous-manifest compares public references with an earlier reviewed export.\n--fail-on-removed requires that baseline and refuses to replace output if references disappear.\n";
1
+ export declare const exportHelp = "Usage: shoalrun export [config] --out DIRECTORY [--target NAME]\n [--format markdown|html|both|pdf] [--document ID]...\n [--channel C --version V --language L [--product P]] [--public-base /help/] [--review]\n [--profile FILE] [--previous-manifest FILE] [--fail-on-removed]\n\nExport selected pages, sections, and named snippets as Markdown and passive HTML.\nConfigure typed resources in an export target; --document selects whole pages.\nNavigation, facets, and a prior website build are not required.\n--format pdf exports one publication, or the selected whole pages, as a styled PDF.\nPDF uses outputs.pdf defaults and target pdf overrides; it follows navigation order\nand appends eligible unlisted pages. PDF defaults to the site's contentFilter.\n--review writes an API-review candidate; status needs-review exits 2 until pinned.\n--previous-manifest compares public references with an earlier reviewed export.\n--fail-on-removed requires that baseline and refuses to replace output if references disappear.\n";
2
2
  export declare function runExportCommand(args: string[]): Promise<{
3
+ digest: string;
4
+ preparedPdf: import("./content-export.js").PdfExportPreparation;
5
+ status: string;
6
+ output: string;
7
+ publication: import("./content-source.js").Variant;
8
+ formats: import("./content-export-config.js").ExportFormat[];
9
+ profileSelection: {
10
+ schemaVersion: number;
11
+ profileDigest: string;
12
+ features: string[];
13
+ provenance: Record<string, string[]>;
14
+ decisions: {
15
+ kind: "page" | "section";
16
+ id: string;
17
+ included: boolean;
18
+ requires?: import("./deployment-profile.js").FeatureRequirements;
19
+ }[];
20
+ } | undefined;
21
+ resources: number;
22
+ documents: number;
23
+ documentIds: string[];
24
+ comparison: {
25
+ status: "no-baseline";
26
+ added: never[];
27
+ removed: never[];
28
+ diagnostics: never[];
29
+ };
30
+ assets: number;
31
+ apiReview: {
32
+ apiId: string;
33
+ digest: string;
34
+ }[];
35
+ diagnostics: {
36
+ code: string;
37
+ documentId?: string;
38
+ message: string;
39
+ }[];
40
+ } | {
41
+ digest: string;
42
+ pdf: string;
43
+ status: string;
44
+ output: string;
45
+ publication: import("./content-source.js").Variant;
46
+ formats: import("./content-export-config.js").ExportFormat[];
47
+ profileSelection: {
48
+ schemaVersion: number;
49
+ profileDigest: string;
50
+ features: string[];
51
+ provenance: Record<string, string[]>;
52
+ decisions: {
53
+ kind: "page" | "section";
54
+ id: string;
55
+ included: boolean;
56
+ requires?: import("./deployment-profile.js").FeatureRequirements;
57
+ }[];
58
+ } | undefined;
59
+ resources: number;
60
+ documents: number;
61
+ documentIds: string[];
62
+ comparison: {
63
+ status: "no-baseline";
64
+ added: never[];
65
+ removed: never[];
66
+ diagnostics: never[];
67
+ };
68
+ assets: number;
69
+ apiReview: {
70
+ apiId: string;
71
+ digest: string;
72
+ }[];
73
+ diagnostics: {
74
+ code: string;
75
+ documentId?: string;
76
+ message: string;
77
+ }[];
78
+ } | {
3
79
  status: string;
4
80
  output: string;
5
81
  publication: import("./content-source.js").Variant;
@@ -1,12 +1,15 @@
1
1
  import { exportContent } from './content-export.js';
2
2
  export const exportHelp = `Usage: shoalrun export [config] --out DIRECTORY [--target NAME]
3
- [--format markdown|html|both] [--document ID]...
3
+ [--format markdown|html|both|pdf] [--document ID]...
4
4
  [--channel C --version V --language L [--product P]] [--public-base /help/] [--review]
5
5
  [--profile FILE] [--previous-manifest FILE] [--fail-on-removed]
6
6
 
7
7
  Export selected pages, sections, and named snippets as Markdown and passive HTML.
8
8
  Configure typed resources in an export target; --document selects whole pages.
9
9
  Navigation, facets, and a prior website build are not required.
10
+ --format pdf exports one publication, or the selected whole pages, as a styled PDF.
11
+ PDF uses outputs.pdf defaults and target pdf overrides; it follows navigation order
12
+ and appends eligible unlisted pages. PDF defaults to the site's contentFilter.
10
13
  --review writes an API-review candidate; status needs-review exits 2 until pinned.
11
14
  --previous-manifest compares public references with an earlier reviewed export.
12
15
  --fail-on-removed requires that baseline and refuses to replace output if references disappear.
@@ -51,7 +54,7 @@ export async function runExportCommand(args) {
51
54
  if (!output)
52
55
  throw new Error(exportHelp);
53
56
  const format = values.get('--format');
54
- if (format && !['html', 'markdown', 'both'].includes(format))
57
+ if (format && !['html', 'markdown', 'both', 'pdf'].includes(format))
55
58
  throw new Error(exportHelp);
56
59
  if (format)
57
60
  options.formats = format === 'both' ? ['markdown', 'html'] : [format];