@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.
- package/CHANGELOG.md +30 -0
- package/CODE-REFERENCES.md +137 -0
- package/README.md +19 -6
- package/adapters/python/requirements.txt +1 -0
- package/adapters/python/shoalrun_reference.py +234 -0
- package/agent-docs/.well-known/shoalrun/navigation/project-updates-50f26fe22670.txt +7 -3
- package/agent-docs/SNAPSHOT.json +5 -5
- package/agent-docs/about.md +3 -3
- package/agent-docs/api-docs/code-reference-settings.md +160 -0
- package/agent-docs/api-docs/code-reference-settings.sections/catalog-settings.md +50 -0
- package/agent-docs/api-docs/code-reference-settings.sections/complete-reference-page.md +46 -0
- package/agent-docs/api-docs/code-reference-settings.sections/converter-snapshot.md +58 -0
- package/agent-docs/api-docs/code-reference-settings.sections/descriptions-type-links-and-source-links.md +40 -0
- package/agent-docs/api-docs/code-reference-settings.sections/html-markdown-and-agents.md +38 -0
- package/agent-docs/api-docs/code-reference-settings.sections/metadata-and-selection.md +38 -0
- package/agent-docs/api-docs/code-reference-settings.sections/publication-sources.md +54 -0
- package/agent-docs/api-docs/code-reference-settings.sections/python-adapter-limits.md +38 -0
- package/agent-docs/api-docs/code-reference-settings.sections/related-reading.md +32 -0
- package/agent-docs/api-docs/code-reference-settings.sections/symbol-fields.md +56 -0
- package/agent-docs/api-docs/code-references.md +143 -0
- package/agent-docs/author/agent-overview.md +2 -2
- package/agent-docs/author/components/code-blocks.md +46 -1
- package/agent-docs/author/frontmatter-reference.md +1 -0
- package/agent-docs/author/frontmatter-reference.sections/categorize-connect-and-present-the-page.md +1 -0
- package/agent-docs/author/workspace.md +2 -2
- package/agent-docs/author/writing-style.md +9 -3
- package/agent-docs/author/writing-style.sections/adapt-this-example.md +1 -1
- package/agent-docs/author/writing-style.sections/choose-components-by-reader-need.md +2 -2
- package/agent-docs/author/writing-style.sections/evidence-and-uncertainty.md +1 -1
- package/agent-docs/author/writing-style.sections/example-audience-and-scope.md +1 -1
- package/agent-docs/author/writing-style.sections/feature-availability.md +1 -1
- package/agent-docs/author/writing-style.sections/ingredients-recipes-and-cookbooks.md +1 -1
- package/agent-docs/author/writing-style.sections/page-purpose-and-structure.md +2 -2
- package/agent-docs/author/writing-style.sections/procedures-and-examples.md +1 -1
- package/agent-docs/author/writing-style.sections/related-reading.md +1 -1
- package/agent-docs/author/writing-style.sections/release-notes.md +30 -0
- package/agent-docs/author/writing-style.sections/review-a-draft.md +1 -1
- package/agent-docs/author/writing-style.sections/reviewed-writing-examples.md +1 -1
- package/agent-docs/author/writing-style.sections/voice-and-explanations.md +1 -1
- package/agent-docs/changelog/2026-10-16-navigation-layouts.md +1 -1
- package/agent-docs/changelog/code-api-references.md +44 -0
- package/agent-docs/changelog/pdf-custom-languages.md +48 -0
- package/agent-docs/changelog.md +17 -1
- package/agent-docs/cli.md +11 -1
- package/agent-docs/cli.sections/audit-a-project-and-api-sources.md +1 -1
- package/agent-docs/cli.sections/author-pages.md +1 -1
- package/agent-docs/cli.sections/build-and-serve.md +1 -1
- package/agent-docs/cli.sections/inspect-config-and-operations.md +1 -1
- package/agent-docs/cli.sections/npm-scripts-in-this-repository.md +1 -1
- package/agent-docs/cli.sections/related-reading.md +1 -1
- package/agent-docs/cli.sections/search-and-chat.md +1 -1
- package/agent-docs/cli.sections/start-a-project.md +1 -1
- package/agent-docs/cli.sections/validate-code-reference-snapshots.md +41 -0
- package/agent-docs/cli.sections/working-tree-authoring-tools.md +1 -1
- package/agent-docs/config/apis.md +5 -1
- package/agent-docs/config/examples.md +3 -3
- package/agent-docs/config/examples.sections/feature-showcase.md +3 -3
- package/agent-docs/config/examples.sections/minimal-site.md +2 -2
- package/agent-docs/config/examples.sections/multilingual-help-center.md +2 -2
- package/agent-docs/config/examples.sections/parcel-api-documentation.md +2 -2
- package/agent-docs/config/examples.sections/related-reading.md +2 -2
- package/agent-docs/config/examples.sections/shared-company-preset.md +2 -2
- package/agent-docs/config/outputs.md +33 -12
- package/agent-docs/config/outputs.sections/exports.md +3 -3
- package/agent-docs/config/outputs.sections/metadata.md +3 -3
- package/agent-docs/config/outputs.sections/pdf-output.md +51 -0
- package/agent-docs/config/outputs.sections/related-reading.md +3 -3
- package/agent-docs/config.md +2 -2
- package/agent-docs/configuration.md +2 -2
- package/agent-docs/configuration.sections/build-several-sites-from-shared-content.md +1 -1
- package/agent-docs/configuration.sections/change-inherited-lists-deliberately.md +1 -1
- package/agent-docs/configuration.sections/extract-a-section-with-include.md +1 -1
- package/agent-docs/configuration.sections/inspect-and-validate-changes.md +1 -1
- package/agent-docs/configuration.sections/related-reading.md +1 -1
- package/agent-docs/configuration.sections/separate-config-inheritance-from-page-metadata.md +1 -1
- package/agent-docs/configuration.sections/share-defaults-through-extends.md +1 -1
- package/agent-docs/configuration.sections/start-with-one-working-file.md +2 -2
- package/agent-docs/configuration.sections/understand-the-three-stages.md +1 -1
- package/agent-docs/configuration.sections/understand-where-paths-point.md +1 -1
- package/agent-docs/contribute/agent-skills.md +3 -1
- package/agent-docs/documents.json +1009 -356
- package/agent-docs/features.md +6 -2
- package/agent-docs/index.md +2 -2
- package/agent-docs/provenance.json +6 -6
- package/agent-docs/publish/api-references.md +3 -1
- package/agent-docs/publish/api-references.sections/author-the-reference-page.md +3 -1
- package/agent-docs/publish/api-references.sections/build-and-verify-the-output.md +3 -1
- package/agent-docs/publish/api-references.sections/inspect-the-machine-readable-contract.md +3 -1
- package/agent-docs/publish/api-references.sections/next-steps.md +3 -1
- package/agent-docs/publish/api-references.sections/preview-a-small-api-reference.md +3 -1
- package/agent-docs/publish/api-references.sections/put-the-spec-in-your-project.md +3 -1
- package/agent-docs/publish/api-references.sections/register-the-operation.md +3 -1
- package/agent-docs/publish/api-references.sections/related-reading.md +3 -1
- package/agent-docs/publish/api-references.sections/review-imported-content-before-publishing.md +3 -1
- package/agent-docs/publish/api-references.sections/what-readers-get.md +3 -1
- package/agent-docs/publish/content-export.md +2 -0
- package/agent-docs/publish/content-export.sections/assets-downloads-and-notices.md +2 -0
- package/agent-docs/publish/content-export.sections/check-the-result.md +2 -0
- package/agent-docs/publish/content-export.sections/inspect-the-manifest.md +2 -0
- package/agent-docs/publish/content-export.sections/prerequisites.md +2 -0
- package/agent-docs/publish/content-export.sections/related-reading.md +2 -0
- package/agent-docs/publish/content-export.sections/review-exported-api-material.md +2 -0
- package/agent-docs/publish/content-export.sections/try-the-help-drawer-example.md +2 -0
- package/agent-docs/publish/content-export.sections/what-the-representations-retain.md +2 -0
- package/agent-docs/publish/pdf.md +184 -0
- package/agent-docs/publish/pdf.sections/apply-print-styling.md +65 -0
- package/agent-docs/publish/pdf.sections/check-the-result.md +33 -0
- package/agent-docs/publish/pdf.sections/export-a-page-or-publication.md +61 -0
- package/agent-docs/publish/pdf.sections/html-markdown-and-agents.md +31 -0
- package/agent-docs/publish/pdf.sections/offer-browser-page-printing.md +49 -0
- package/agent-docs/publish/pdf.sections/offer-direct-downloads-through-a-node-server.md +46 -0
- package/agent-docs/publish/pdf.sections/prerequisites.md +37 -0
- package/agent-docs/publish/pdf.sections/publish-an-optional-manual-download.md +39 -0
- package/agent-docs/publish.md +2 -0
- package/agent-docs/reference/agent-skills.md +7 -3
- package/agent-docs/search-index.json +1 -1
- package/agent-docs/start.md +4 -4
- package/agent-docs/start.sections/add-the-page-to-navigation.md +2 -2
- package/agent-docs/start.sections/build-and-check-both-outputs.md +2 -2
- package/agent-docs/start.sections/build-this-site.md +2 -2
- package/agent-docs/start.sections/check-writing-style.md +2 -2
- package/agent-docs/start.sections/create-a-page.md +2 -2
- package/agent-docs/start.sections/make-it-yours.md +2 -2
- package/agent-docs/start.sections/migrating-from-github-packages.md +2 -2
- package/agent-docs/start.sections/prerequisites.md +2 -2
- package/agent-docs/start.sections/related-reading.md +2 -2
- package/agent-docs/start.sections/start-your-own-project.md +4 -4
- package/agent-docs/status.md +9 -5
- package/agent-docs/styling/themes.md +7 -3
- package/agent-docs/what-is-shoalrun.md +2 -2
- package/assets/copy-markdown.js +1 -1
- package/assets/print-view.js +165 -0
- package/assets/print.js +63 -0
- package/dist/artifact-bundle.d.ts +2 -0
- package/dist/artifact-bundle.js +13 -3
- package/dist/authoring-snapshot.js +1 -1
- package/dist/browser-print-assets.d.ts +9 -0
- package/dist/browser-print-assets.js +45 -0
- package/dist/cli-discovery.d.ts +1 -1
- package/dist/cli-discovery.js +1 -0
- package/dist/cli.js +7 -0
- package/dist/code-reference-model.d.ts +85 -0
- package/dist/code-reference-model.js +73 -0
- package/dist/code-references.d.ts +23 -0
- package/dist/code-references.js +371 -0
- package/dist/compiler-config.d.ts +1 -0
- package/dist/compiler.d.ts +4 -2
- package/dist/compiler.js +33 -7
- package/dist/config.js +1 -1
- package/dist/content-export-command.d.ts +77 -1
- package/dist/content-export-command.js +5 -2
- package/dist/content-export-config.d.ts +4 -1
- package/dist/content-export-config.js +10 -5
- package/dist/content-export-render.d.ts +9 -3
- package/dist/content-export-render.js +11 -7
- package/dist/content-export.d.ts +126 -0
- package/dist/content-export.js +267 -124
- package/dist/content-source.d.ts +2 -0
- package/dist/content-workspace.d.ts +1 -0
- package/dist/content-workspace.js +2 -0
- package/dist/fonts.d.ts +9 -0
- package/dist/fonts.js +58 -3
- package/dist/frontmatter.js +5 -2
- package/dist/highlight-languages.d.ts +9 -0
- package/dist/highlight-languages.js +52 -0
- package/dist/highlight.d.ts +4 -1
- package/dist/highlight.js +20 -5
- package/dist/output-commit.d.ts +1 -1
- package/dist/output-commit.js +4 -2
- package/dist/page-materialization.js +3 -2
- package/dist/page-renderer.d.ts +1 -0
- package/dist/page-renderer.js +8 -1
- package/dist/pdf-button.d.ts +22 -0
- package/dist/pdf-button.js +55 -0
- package/dist/pdf-config.d.ts +28 -0
- package/dist/pdf-config.js +43 -0
- package/dist/pdf-http.d.ts +36 -0
- package/dist/pdf-http.js +132 -0
- package/dist/pdf-project.d.ts +6 -0
- package/dist/pdf-project.js +31 -0
- package/dist/pdf-renderer.d.ts +45 -0
- package/dist/pdf-renderer.js +208 -0
- package/dist/pdf-site.d.ts +37 -0
- package/dist/pdf-site.js +65 -0
- package/dist/pdf-stylesheet.d.ts +1 -0
- package/dist/pdf-stylesheet.js +57 -0
- package/dist/publication-render.d.ts +2 -1
- package/dist/publication-render.js +2 -2
- package/dist/representation-tree.js +5 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +35 -8
- package/dist/vercel.js +6 -0
- package/examples/code-references/content/start.md +53 -0
- package/examples/code-references/docs.config.yaml +262 -0
- package/examples/code-references/python/orchard/__init__.py +11 -0
- package/examples/code-references/python/orchard/client.py +212 -0
- package/examples/code-references/python/orchard/errors.py +31 -0
- package/examples/code-references/python/orchard/models.py +69 -0
- package/examples/code-references/python/orchard/pagination.py +39 -0
- package/examples/code-references/reference.json +1244 -0
- package/examples/custom-language/README.md +32 -0
- package/examples/custom-language/content/start.mdx +28 -0
- package/examples/custom-language/docs.config.yaml +18 -0
- package/examples/custom-language/grammars/custompy.tmLanguage.json +7 -0
- package/examples/pdf/README.md +27 -0
- package/examples/pdf/content/operations.mdx +26 -0
- package/examples/pdf/content/start.mdx +28 -0
- package/examples/pdf/docs.config.yaml +29 -0
- package/examples/pdf/print.css +9 -0
- package/package.json +11 -2
- package/schemas/code-reference.schema.json +306 -0
- package/skills/shoalrun-author/SKILL.md +6 -0
- package/skills/shoalrun-author/references/code-api-references.md +15 -0
- package/skills/shoalrun-author/references/pdf-export.md +15 -0
- package/skills/shoalrun-brand/SKILL.md +2 -0
- package/skills/shoalrun-pdf/SKILL.md +49 -0
- package/skills/shoalrun-pdf/assets/print.css +49 -0
- package/skills/shoalrun-pdf/references/print-design.md +75 -0
- package/styles/site.css +64 -3
package/dist/content-export.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { documentKey, samePublication } from './publication-identity.js';
|
|
1
|
+
import { documentKey, publicationKey, samePublication } from './publication-identity.js';
|
|
2
2
|
import { pruneFeatureTree, profileSelection } from './deployment-profile.js';
|
|
3
3
|
import { readFile, realpath } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -31,7 +31,18 @@ import { resolveAuthoredChange, authoredChangeNotice } from './authored-change.j
|
|
|
31
31
|
import { resolveSnippetValues } from './snippets.js';
|
|
32
32
|
import { isolateExportTree, markExportSource, exportHeadingInventory, preserveExportHeadingIds } from './content-export-anchors.js';
|
|
33
33
|
import { selectSection } from './section-identity.js';
|
|
34
|
+
import { profileNavigation, resolveProjectNavigation } from './project-navigation.js';
|
|
35
|
+
import { createCodeHighlighter } from './highlight.js';
|
|
36
|
+
import { projectPdfSettings } from './pdf-project.js';
|
|
37
|
+
import { renderPdf } from './pdf-renderer.js';
|
|
34
38
|
import { compareContentManifests } from './content-export-comparison.js';
|
|
39
|
+
/** Prepare passive, scoped print inputs without launching a browser or writing output. */
|
|
40
|
+
export async function preparePdfExport(configFile, options = {}) {
|
|
41
|
+
const result = await exportContent(configFile, '', { ...options, formats: ['pdf'], preparePdfOnly: true });
|
|
42
|
+
if (!('preparedPdf' in result) || !result.preparedPdf)
|
|
43
|
+
throw new Error('PDF preparation failed');
|
|
44
|
+
return result.preparedPdf;
|
|
45
|
+
}
|
|
35
46
|
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
36
47
|
const keyFor = (doc) => documentKey(doc.frontmatter.id, doc.variant);
|
|
37
48
|
const resourceIdentity = (item) => JSON.stringify([item.ref.kind, item.ref.kind === 'snippet' ? item.ref.id : item.ref.documentId, item.ref.kind === 'section' ? item.ref.sectionId : null, item.doc.variant.channel, item.doc.variant.version, item.doc.variant.language, ...(item.doc.variant.product === undefined ? [] : [item.doc.variant.product])]);
|
|
@@ -75,7 +86,22 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
75
86
|
throw new Error(`Unknown export target ${options.target}`);
|
|
76
87
|
raw = config.exports[options.target];
|
|
77
88
|
}
|
|
89
|
+
// Website buttons can reuse a target's print styling without inheriting its
|
|
90
|
+
// content selection, publication, asset roots, or API review approvals.
|
|
91
|
+
let styleTarget = raw;
|
|
92
|
+
const styleTargetName = options.pdfStyleTarget ?? options.target;
|
|
93
|
+
if (options.pdfStyleTarget) {
|
|
94
|
+
if (!isConfigMapping(config.exports) || !Object.hasOwn(config.exports, options.pdfStyleTarget))
|
|
95
|
+
throw new Error(`Unknown PDF style target ${options.pdfStyleTarget}`);
|
|
96
|
+
styleTarget = config.exports[options.pdfStyleTarget];
|
|
97
|
+
if (!isConfigMapping(styleTarget.pdf))
|
|
98
|
+
throw new Error(`PDF style target ${options.pdfStyleTarget} needs a pdf mapping`);
|
|
99
|
+
}
|
|
78
100
|
const target = validateExportTarget({ ...raw, ...(options.formats ? { formats: options.formats } : {}), ...(options.documents ? { documents: options.documents } : {}), ...(options.publication ? { publication: options.publication } : {}), ...(options.publicBase ? { publicBase: options.publicBase } : {}) }, config.readerFilters);
|
|
101
|
+
const pdf = target.formats.includes('pdf');
|
|
102
|
+
if (pdf && (options.previousManifest || options.failOnRemoved))
|
|
103
|
+
throw new Error('PDF exports do not support content-reference manifest comparison; compare their PDF manifests separately');
|
|
104
|
+
const formats = pdf ? ['html'] : target.formats;
|
|
79
105
|
const language = defaultLanguage(config);
|
|
80
106
|
const enabled = config.publications.enabled;
|
|
81
107
|
if (!Array.isArray(enabled) || !enabled.length || enabled.some(value => !isConfigMapping(value)))
|
|
@@ -88,6 +114,8 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
88
114
|
if (!unique.some(variant => sameVariant(variant, publication)))
|
|
89
115
|
throw new Error('The selected export publication is not enabled');
|
|
90
116
|
const readerFilters = validateReaderFilters(config.readerFilters);
|
|
117
|
+
const siteSelection = validateContentFilter(site.contentFilter, readerFilters);
|
|
118
|
+
const selection = target.contentFilter ?? (pdf ? siteSelection : undefined);
|
|
91
119
|
if (config.metadata !== undefined && (!isConfigMapping(config.metadata) || Object.keys(config.metadata).some(key => key !== 'provider')))
|
|
92
120
|
throw new Error('metadata accepts only provider');
|
|
93
121
|
const providerSetting = configField(config.metadata, 'provider');
|
|
@@ -100,10 +128,10 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
100
128
|
const doc = [...eligible.values()].find(doc => doc.frontmatter.id === id);
|
|
101
129
|
if (!doc)
|
|
102
130
|
throw new Error(`Requested export document ${id} is missing, non-public, or unavailable in the selected publication`);
|
|
103
|
-
if (!matchesContentFilter(doc.readerFilters,
|
|
131
|
+
if (!matchesContentFilter(doc.readerFilters, selection))
|
|
104
132
|
throw new Error(`Requested export document ${id} is excluded by the export contentFilter`);
|
|
105
133
|
}
|
|
106
|
-
const docs = new Map([...eligible].filter(([, doc]) => (!(target.documents || target.resources) || ids.has(doc.frontmatter.id)) && matchesContentFilter(doc.readerFilters,
|
|
134
|
+
const docs = new Map([...eligible].filter(([, doc]) => (!(target.documents || target.resources) || ids.has(doc.frontmatter.id)) && matchesContentFilter(doc.readerFilters, selection)).sort(([, a], [, b]) => compare(keyFor(a), keyFor(b))));
|
|
107
135
|
if (!docs.size && !target.resources?.some(item => item.kind === 'snippet'))
|
|
108
136
|
throw new Error('Export selection matched no eligible pages');
|
|
109
137
|
const plugins = await loadComponentPlugins(loaded, { browser: false });
|
|
@@ -196,6 +224,34 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
196
224
|
}
|
|
197
225
|
}
|
|
198
226
|
selected.sort((a, b) => compare(resourceIdentity(a), resourceIdentity(b)));
|
|
227
|
+
if (pdf) {
|
|
228
|
+
const order = [];
|
|
229
|
+
if (target.documents)
|
|
230
|
+
order.push(...target.documents);
|
|
231
|
+
else if (config.navigation !== undefined) {
|
|
232
|
+
// Validate against known identities before pruning this publication/edition.
|
|
233
|
+
const layout = profileNavigation(config.navigation, docs, workspace.knownDocumentIds, apiPlan.knownOperations, new Set(Object.keys(apiPlan.apiDefinitions)));
|
|
234
|
+
const navigation = resolveProjectNavigation(layout, publication, docs, materialized.navigationEntries.get(publicationKey(publication)) ?? [], materialized.navigationTagOrder.get(publicationKey(publication)) ?? new Map());
|
|
235
|
+
const byRoute = new Map([...docs.values()].map(doc => [doc.route, doc.frontmatter.id]));
|
|
236
|
+
const walk = (nodes) => { for (const node of nodes) {
|
|
237
|
+
if (node.route) {
|
|
238
|
+
const id = byRoute.get(node.route);
|
|
239
|
+
if (id && !order.includes(id))
|
|
240
|
+
order.push(id);
|
|
241
|
+
}
|
|
242
|
+
if (node.items)
|
|
243
|
+
walk(node.items);
|
|
244
|
+
} };
|
|
245
|
+
walk(navigation);
|
|
246
|
+
const unlisted = [...docs.values()].map(doc => doc.frontmatter.id).filter(id => !order.includes(id)).sort(compare);
|
|
247
|
+
if (unlisted.length)
|
|
248
|
+
diagnostics.push({ code: 'pdf-unlisted-pages', message: `${unlisted.length} eligible page(s) outside navigation were appended in stable document-ID order.` });
|
|
249
|
+
order.push(...unlisted);
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
order.push(...[...docs.values()].map(doc => doc.frontmatter.id).sort(compare));
|
|
253
|
+
selected.sort((a, b) => order.indexOf(a.doc.frontmatter.id) - order.indexOf(b.doc.frontmatter.id));
|
|
254
|
+
}
|
|
199
255
|
const diagrams = await resolveDiagramTrees(selected.map(item => item.tree));
|
|
200
256
|
const assets = new ExportAssets(target.publicBase);
|
|
201
257
|
const assetRootsOrigin = options.target && target.assetRoots.length ? explainSegments(loaded, ['exports', options.target, 'assetRoots']).source.file : configFile;
|
|
@@ -218,9 +274,13 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
218
274
|
const unreviewed = reviews.filter(review => (target.apiReview[review.apiId] ?? configField(apiPlan.apiDefinitions[review.apiId].publicReview, 'digest')) !== review.digest);
|
|
219
275
|
if (unreviewed.length && !options.review)
|
|
220
276
|
throw new Error(`Export API review required: ${unreviewed.map(row => `${row.apiId}=${row.digest}`).join(', ')}. Run export with --review, inspect the candidate, and pin these digests in exports.<target>.apiReview`);
|
|
221
|
-
const siteSelection = validateContentFilter(site.contentFilter, readerFilters);
|
|
222
277
|
const website = [...eligible.values()].filter(doc => matchesContentFilter(doc.readerFilters, siteSelection));
|
|
223
278
|
const canonicalDestination = (doc) => website.includes(doc) && website.filter(other => other.route.toLowerCase() === doc.route.toLowerCase()).length === 1;
|
|
279
|
+
const publishedAssetRoutes = new Map();
|
|
280
|
+
for (const route of materialized.focusedContracts.keys())
|
|
281
|
+
publishedAssetRoutes.set(route, route);
|
|
282
|
+
for (const asset of diagrams)
|
|
283
|
+
publishedAssetRoutes.set(asset.route, publicPath(asset.route));
|
|
224
284
|
const byId = new Map([...eligible.values()].map(doc => [doc.frontmatter.id, doc]));
|
|
225
285
|
const byRoute = (pathname) => {
|
|
226
286
|
const matches = [...eligible.values()].filter(doc => doc.route === pathname.replace(/\/$/, '') || doc.route === pathname || doc.markdownRoute === pathname);
|
|
@@ -234,13 +294,13 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
234
294
|
const sourceInventories = new Map();
|
|
235
295
|
const sourceAliases = new Map();
|
|
236
296
|
for (const item of selected)
|
|
237
|
-
for (const format of
|
|
297
|
+
for (const format of formats) {
|
|
238
298
|
const sourceKey = `${selectedSourceKey(item)}:${format}`;
|
|
239
299
|
if (!sourceInventories.has(sourceKey))
|
|
240
|
-
sourceInventories.set(sourceKey, exportHeadingInventory(portableTree(structuredClone(item.doc.tree), format, routes, basePath)));
|
|
241
|
-
const tree = portableTree(structuredClone(item.tree), format, routes, basePath);
|
|
300
|
+
sourceInventories.set(sourceKey, exportHeadingInventory(portableTree(structuredClone(item.doc.tree), format, routes, basePath, { print: pdf })));
|
|
301
|
+
const tree = portableTree(structuredClone(item.tree), format, routes, basePath, { print: pdf });
|
|
242
302
|
if (item.ref.kind !== 'page') {
|
|
243
|
-
const meaning = item.meaningTree ? portableTree(structuredClone(item.meaningTree), format, routes, basePath) : tree;
|
|
303
|
+
const meaning = item.meaningTree ? portableTree(structuredClone(item.meaningTree), format, routes, basePath, { print: pdf }) : tree;
|
|
244
304
|
const body = (meaning.children ?? []).filter((node) => node.type !== 'heading' && node.type !== 'definition' && node.type !== 'footnoteDefinition');
|
|
245
305
|
if (!portableMarkdown(root(body)).trim())
|
|
246
306
|
throw new Error(`Resource ${resourceIdentity(item)} has no content in ${format}`);
|
|
@@ -249,7 +309,7 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
249
309
|
sourceAliases.set(`${resourceIdentity(item)}:${format}`, aliases);
|
|
250
310
|
trees.set(`${resourceIdentity(item)}:${format}`, tree);
|
|
251
311
|
// Resolve anchors independently in each representation, before URL rewriting.
|
|
252
|
-
anchors.set(`${resourceIdentity(item)}:${format}`, passiveHtml(tree, `sr-${resourceKey(item)}`, false).anchors);
|
|
312
|
+
anchors.set(`${resourceIdentity(item)}:${format}`, passiveHtml(tree, `sr-${resourceKey(item)}`, false, { print: pdf }).anchors);
|
|
253
313
|
}
|
|
254
314
|
const canonicalAnchors = new Map();
|
|
255
315
|
const verifiedCanonicalAnchors = (destination, format) => {
|
|
@@ -278,130 +338,177 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
278
338
|
};
|
|
279
339
|
const records = [];
|
|
280
340
|
const artifacts = new Map();
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
destination = [...eligible.values()].find(other => path.resolve(other.file) === file);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
341
|
+
const printDocuments = [];
|
|
342
|
+
const highlightDependencies = new Set();
|
|
343
|
+
const highlighting = configField(configField(config.outputs, 'html'), 'highlighting');
|
|
344
|
+
// PDF has a light paper background, independent of the site's screen mode.
|
|
345
|
+
const printHighlighting = isConfigMapping(highlighting) && highlighting.provider !== 'custom' && highlighting.provider !== 'none' ? { ...highlighting, theme: 'github-light' } : highlighting ?? { theme: 'github-light' };
|
|
346
|
+
const highlighter = pdf ? await createCodeHighlighter(printHighlighting, highlighting ? explainSegments(loaded, ['outputs', 'html', 'highlighting']).source.file : configFile, {
|
|
347
|
+
grammarOrigin: index => explainSegments(loaded, ['outputs', 'html', 'highlighting', 'languages', String(index), 'grammar']).source.file,
|
|
348
|
+
onDependency: file => highlightDependencies.add(file)
|
|
349
|
+
}) : undefined;
|
|
350
|
+
if (highlighter?.dependency)
|
|
351
|
+
highlightDependencies.add(highlighter.dependency);
|
|
352
|
+
for (const file of highlighter?.dependencies ?? [])
|
|
353
|
+
highlightDependencies.add(file);
|
|
354
|
+
try {
|
|
355
|
+
for (const item of selected) {
|
|
356
|
+
const doc = item.doc;
|
|
357
|
+
const externalDependencies = new Map();
|
|
358
|
+
const usedAssets = new Set();
|
|
359
|
+
const representations = {};
|
|
360
|
+
const canonicalUrl = item.ref.kind !== 'snippet' && origin && canonicalDestination(doc) ? `${origin}${doc.route}` : undefined;
|
|
361
|
+
for (const format of formats) {
|
|
362
|
+
const links = [];
|
|
363
|
+
const tree = trees.get(`${resourceIdentity(item)}:${format}`);
|
|
364
|
+
const resolve = async (node) => {
|
|
365
|
+
// Native adapters can express ordinary links/images as semantic elements.
|
|
366
|
+
const properties = node.data?.hProperties;
|
|
367
|
+
const attr = node.data?.hName === 'a' ? 'href' : node.data?.hName === 'img' ? 'src' : undefined;
|
|
368
|
+
if (attr && typeof properties?.[attr] === 'string') {
|
|
369
|
+
const virtual = { type: attr === 'src' ? 'image' : 'link', url: properties[attr], children: [] };
|
|
370
|
+
await resolve(virtual);
|
|
371
|
+
properties[attr] = virtual.url;
|
|
372
|
+
if (virtual.data?.hProperties?.['data-export-link'])
|
|
373
|
+
properties['data-export-link'] = virtual.data.hProperties['data-export-link'];
|
|
317
374
|
}
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
375
|
+
if (typeof node.url === 'string' && ['link', 'image', 'definition'].includes(node.type)) {
|
|
376
|
+
const value = node.url;
|
|
377
|
+
const parsed = new URL(value, `https://export.invalid${doc.route === basePath ? `${doc.route}/` : doc.route}`);
|
|
378
|
+
if (!['http:', 'https:', ...(node.type !== 'image' ? ['mailto:', 'tel:'] : [])].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
379
|
+
throw new Error(`Unsafe export URL ${value}`);
|
|
380
|
+
let destination;
|
|
381
|
+
if (node.type !== 'image') {
|
|
382
|
+
destination = node.exportDocumentId ? byId.get(node.exportDocumentId) : value.startsWith('#') || value.startsWith('?') ? doc : undefined;
|
|
383
|
+
if (!destination && parsed.origin === 'https://export.invalid') {
|
|
384
|
+
const pathUrl = outputUrl(value, doc.route, undefined, 'html', routes, basePath);
|
|
385
|
+
destination = byRoute(new URL(pathUrl, 'https://export.invalid').pathname);
|
|
386
|
+
if (!destination && /\.mdx?$/.test(parsed.pathname) && !value.startsWith('/')) {
|
|
387
|
+
const file = path.resolve(path.dirname(doc.file), decodeURIComponent(value.split(/[?#]/)[0]));
|
|
388
|
+
destination = [...eligible.values()].find(other => path.resolve(other.file) === file);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
330
391
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
392
|
+
if (destination) {
|
|
393
|
+
let href;
|
|
394
|
+
let kind;
|
|
395
|
+
const fragment = decodeURIComponent(parsed.hash.slice(1));
|
|
396
|
+
const candidates = selected.filter(candidate => candidate.ref.kind !== 'snippet' && keyFor(candidate.doc) === keyFor(destination));
|
|
397
|
+
const hasAnchor = (candidate) => sourceAliases.get(`${resourceIdentity(candidate)}:${format}`)?.has(fragment);
|
|
398
|
+
let chosen = destination === doc && fragment && hasAnchor(item) ? item : undefined;
|
|
399
|
+
if (!chosen && fragment) {
|
|
400
|
+
const matching = candidates.filter(candidate => candidate.ref.kind === 'section' && hasAnchor(candidate));
|
|
401
|
+
if (matching.length > 1)
|
|
402
|
+
throw new Error(`Ambiguous fragment ${fragment} in ${destination.frontmatter.id}`);
|
|
403
|
+
chosen = matching[0];
|
|
404
|
+
}
|
|
405
|
+
chosen ??= candidates.find(candidate => candidate.ref.kind === 'page');
|
|
406
|
+
// Clean Markdown has no explicit alias anchors. Prefer an actual
|
|
407
|
+
// section resource for those references, otherwise the canonical HTML.
|
|
408
|
+
if (chosen && format === 'markdown' && fragment && chosen.ref.kind === 'page') {
|
|
409
|
+
const targetId = sourceAliases.get(`${resourceIdentity(chosen)}:${format}`)?.get(fragment);
|
|
410
|
+
const target = anchors.get(`${resourceIdentity(chosen)}:${format}`)?.find(entry => entry.id === targetId);
|
|
411
|
+
if (target && !target.title)
|
|
412
|
+
chosen = candidates.find(candidate => candidate.ref.kind === 'section' && candidate.ref.sectionId === fragment);
|
|
413
|
+
}
|
|
414
|
+
if (chosen) {
|
|
415
|
+
const targetAnchors = anchors.get(`${resourceIdentity(chosen)}:${format}`);
|
|
416
|
+
const targetId = sourceAliases.get(`${resourceIdentity(chosen)}:${format}`)?.get(fragment);
|
|
417
|
+
const anchor = fragment ? targetAnchors.find(entry => entry.id === targetId) : undefined;
|
|
418
|
+
if (fragment && !anchor)
|
|
419
|
+
throw new Error(`Missing export anchor ${fragment} in ${destination.frontmatter.id} (${format})`);
|
|
420
|
+
// A section-resource URL is sufficient when addressing its leading
|
|
421
|
+
// section; clean Markdown does not publish explicit HTML anchors.
|
|
422
|
+
const leadingSection = chosen.ref.kind === 'section' && chosen.ref.sectionId === fragment;
|
|
423
|
+
const markdownAnchor = anchor?.id ?? fragment;
|
|
424
|
+
const suffix = parsed.search + (fragment && !(format === 'markdown' && leadingSection) ? `#${format === 'html' ? anchor.exportedId : markdownAnchor}` : '');
|
|
425
|
+
href = chosen === item && !parsed.search && fragment && format === 'html' ? suffix : `${target.publicBase}content/${resourceKey(chosen)}.${format === 'html' ? 'html' : 'md'}${suffix}`;
|
|
426
|
+
if (pdf) {
|
|
427
|
+
if (options.pdfLinks === 'canonical' && chosen !== item) {
|
|
428
|
+
if (!origin || !canonicalDestination(destination))
|
|
429
|
+
throw new Error(`PDF page link to ${destination.frontmatter.id} requires site.origin and an eligible canonical website route`);
|
|
430
|
+
if (fragment && !verifiedCanonicalAnchors(destination, format).has(fragment))
|
|
431
|
+
throw new Error(`Cannot verify canonical anchor ${fragment} in ${destination.frontmatter.id}; use its page URL without a fragment`);
|
|
432
|
+
href = `${origin}${destination.route}${parsed.search}${parsed.hash}`;
|
|
433
|
+
kind = 'canonical';
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
if (parsed.search)
|
|
437
|
+
throw new Error(`PDF internal links cannot retain query parameters: ${value}. Use an explicit canonical HTTPS link instead`);
|
|
438
|
+
href = `#${fragment ? anchor.exportedId : `pdf-document-${options.pdfLinks === 'canonical' ? 1 : selected.indexOf(chosen) + 1}`}`;
|
|
439
|
+
kind = 'resource';
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else
|
|
443
|
+
kind = 'resource';
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
if (!origin || !canonicalDestination(destination))
|
|
447
|
+
throw new Error(`Document ${destination.frontmatter.id} is outside the export and has no eligible canonical website fallback`);
|
|
448
|
+
if (fragment) {
|
|
449
|
+
const inventory = sourceInventories.get(`${sourceKeyFor(destination)}:${format}`);
|
|
450
|
+
if (!inventory)
|
|
451
|
+
throw new Error(`Cannot validate canonical anchor ${fragment} in unselected document ${destination.frontmatter.id}; select that page or section explicitly`);
|
|
452
|
+
if (!inventory.ids.has(fragment))
|
|
453
|
+
throw new Error(`Missing source anchor ${fragment} in ${destination.frontmatter.id} (${format})`);
|
|
454
|
+
if (!verifiedCanonicalAnchors(destination, format).has(fragment))
|
|
455
|
+
throw new Error(`Cannot verify canonical anchor ${fragment} in ${destination.frontmatter.id}; select its target resource explicitly`);
|
|
456
|
+
}
|
|
457
|
+
href = `${origin}${destination.route}${parsed.search}${parsed.hash}`;
|
|
458
|
+
kind = 'canonical';
|
|
459
|
+
}
|
|
460
|
+
const key = `link-${links.length + 1}`;
|
|
461
|
+
links.push({ key, kind, href, ...(chosen ? { ref: chosen.ref } : {}), ...(fragment ? { fragment } : {}), ...(parsed.search ? { query: parsed.search.slice(1) } : {}) });
|
|
462
|
+
node.url = href;
|
|
463
|
+
if (format === 'html' && node.type === 'link')
|
|
464
|
+
node.data = { ...node.data, hProperties: { ...node.data?.hProperties, 'data-export-link': key } };
|
|
339
465
|
}
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const markdownAnchor = anchor?.id ?? fragment;
|
|
350
|
-
const suffix = parsed.search + (fragment && !(format === 'markdown' && leadingSection) ? `#${format === 'html' ? anchor.exportedId : markdownAnchor}` : '');
|
|
351
|
-
href = chosen === item && !parsed.search && fragment && format === 'html' ? suffix : `${target.publicBase}content/${resourceKey(chosen)}.${format === 'html' ? 'html' : 'md'}${suffix}`;
|
|
352
|
-
kind = 'resource';
|
|
466
|
+
else if (parsed.origin !== 'https://export.invalid') {
|
|
467
|
+
node.url = value.startsWith('//') ? `https:${value}` : value;
|
|
468
|
+
if (node.type === 'image') {
|
|
469
|
+
if (pdf)
|
|
470
|
+
throw new Error(`PDF images must be local staged assets: ${node.url}`);
|
|
471
|
+
externalDependencies.set(node.url, { url: node.url, kind: 'image' });
|
|
472
|
+
}
|
|
473
|
+
else
|
|
474
|
+
links.push({ key: `link-${links.length + 1}`, kind: 'external', href: node.url });
|
|
353
475
|
}
|
|
354
476
|
else {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
477
|
+
const asset = await assets.resolve(value, doc.file, selectedSourceKey(item));
|
|
478
|
+
if (!asset)
|
|
479
|
+
throw new Error(`Unresolved local export ${node.type === 'image' ? 'image' : 'link'} ${value} in ${doc.frontmatter.id}`);
|
|
480
|
+
usedAssets.add(asset.path);
|
|
481
|
+
node.url = `${target.publicBase}${asset.path}${parsed.search}${parsed.hash}`;
|
|
482
|
+
if (node.type !== 'image') {
|
|
483
|
+
if (pdf) {
|
|
484
|
+
if (!origin || !canonicalDestination(doc) || !publishedAssetRoutes.has(parsed.pathname))
|
|
485
|
+
throw new Error(`PDF cannot embed local download link ${value}; use an explicit canonical HTTPS download URL instead`);
|
|
486
|
+
node.url = `${origin}${publishedAssetRoutes.get(parsed.pathname)}${parsed.search}${parsed.hash}`;
|
|
487
|
+
}
|
|
488
|
+
links.push({ key: `link-${links.length + 1}`, kind: 'asset', href: node.url, asset: asset.path });
|
|
365
489
|
}
|
|
366
|
-
href = `${origin}${destination.route}${parsed.search}${parsed.hash}`;
|
|
367
|
-
kind = 'canonical';
|
|
368
490
|
}
|
|
369
|
-
const key = `link-${links.length + 1}`;
|
|
370
|
-
links.push({ key, kind, href, ...(chosen ? { ref: chosen.ref } : {}), ...(fragment ? { fragment } : {}), ...(parsed.search ? { query: parsed.search.slice(1) } : {}) });
|
|
371
|
-
node.url = href;
|
|
372
|
-
if (format === 'html' && node.type === 'link')
|
|
373
|
-
node.data = { ...node.data, hProperties: { ...node.data?.hProperties, 'data-export-link': key } };
|
|
374
|
-
}
|
|
375
|
-
else if (parsed.origin !== 'https://export.invalid') {
|
|
376
|
-
node.url = value.startsWith('//') ? `https:${value}` : value;
|
|
377
|
-
if (node.type === 'image')
|
|
378
|
-
externalDependencies.set(node.url, { url: node.url, kind: 'image' });
|
|
379
|
-
else
|
|
380
|
-
links.push({ key: `link-${links.length + 1}`, kind: 'external', href: node.url });
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
const asset = await assets.resolve(value, doc.file, selectedSourceKey(item));
|
|
384
|
-
if (!asset)
|
|
385
|
-
throw new Error(`Unresolved local export ${node.type === 'image' ? 'image' : 'link'} ${value} in ${doc.frontmatter.id}`);
|
|
386
|
-
usedAssets.add(asset.path);
|
|
387
|
-
node.url = `${target.publicBase}${asset.path}${parsed.search}${parsed.hash}`;
|
|
388
|
-
if (node.type !== 'image')
|
|
389
|
-
links.push({ key: `link-${links.length + 1}`, kind: 'asset', href: node.url, asset: asset.path });
|
|
390
491
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
492
|
+
for (const child of node.children ?? [])
|
|
493
|
+
await resolve(child);
|
|
494
|
+
};
|
|
495
|
+
await resolve(tree);
|
|
496
|
+
const filename = `content/${resourceKey(item)}.${format === 'html' ? 'html' : 'md'}`;
|
|
497
|
+
const rendered = format === 'html' ? passiveHtml(tree, `sr-${resourceKey(item)}`, !pdf, { print: pdf, highlighter }) : undefined;
|
|
498
|
+
const content = rendered?.html ?? portableMarkdown(tree);
|
|
499
|
+
if (pdf)
|
|
500
|
+
printDocuments.push({ id: doc.frontmatter.id, title: item.title, html: content });
|
|
501
|
+
else
|
|
502
|
+
artifacts.set(filename, content);
|
|
503
|
+
representations[format] = { path: filename, mediaType: format === 'html' ? 'text/html' : 'text/markdown', bytes: Buffer.byteLength(content), digest: exportDigest(content), anchors: rendered?.anchors ?? [], links };
|
|
504
|
+
}
|
|
505
|
+
if (externalDependencies.size)
|
|
506
|
+
diagnostics.push({ code: 'external-render-dependencies', documentId: doc.frontmatter.id, message: `${externalDependencies.size} remote image(s) remain external; this document is not offline-complete.` });
|
|
507
|
+
records.push({ ref: item.ref, publication, title: item.title, summary: item.summary, type: item.type, ...(item.source ? { source: item.source } : {}), ...(canonicalUrl ? { canonicalUrl } : {}), representations, assets: [...usedAssets].sort(), externalDependencies: [...externalDependencies.values()].sort((a, b) => compare(a.url, b.url)) });
|
|
401
508
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
highlighter?.dispose?.();
|
|
405
512
|
}
|
|
406
513
|
for (const [filename, bytes] of assets.artifacts)
|
|
407
514
|
artifacts.set(filename, bytes);
|
|
@@ -432,6 +539,42 @@ export async function exportContent(configFile, destination, options = {}) {
|
|
|
432
539
|
notices += `\n${path.basename(file)}:\n${await readFile(resolvedNotice, 'utf8')}\n`;
|
|
433
540
|
}
|
|
434
541
|
artifacts.set('NOTICES.txt', notices);
|
|
542
|
+
if (pdf) {
|
|
543
|
+
const singlePage = target.documents?.length === 1;
|
|
544
|
+
const standalone = singlePage || options.pdfLinks === 'canonical';
|
|
545
|
+
const projectPdf = projectPdfSettings(loaded, configFile, styleTargetName);
|
|
546
|
+
const pdfConfig = { cover: !standalone, toc: !standalone, ...projectPdf.config };
|
|
547
|
+
const pdfInputs = projectPdf.files;
|
|
548
|
+
const inputs = [...noticeFiles, ...loaded.dependencies, ...metadataDependencies, ...plugins.dependencies, ...apiPlan.api.dependencies, ...[...eligible.values()].map(doc => doc.file), ...[...snippets.values()].map(snippet => snippet.file), ...highlightDependencies, ...pdfInputs];
|
|
549
|
+
const manifest = {
|
|
550
|
+
schemaVersion: 1, kind: 'shoalrun-pdf-export', publication, title: site.title,
|
|
551
|
+
review: { status: unreviewed.length ? 'required' : 'ok', apis: reviews.map(row => ({ apiId: row.apiId, digest: row.digest })) },
|
|
552
|
+
documents: records.map((record, index) => ({ id: selected[index].doc.frontmatter.id, title: record.title, contentDigest: exportDigest(printDocuments[index].html), anchors: record.representations.html.anchors, links: record.representations.html.links })), diagnostics
|
|
553
|
+
};
|
|
554
|
+
const input = { documents: printDocuments, title: selected.length === 1 ? selected[0].title : site.title, publication, assets: assets.artifacts, publicBase: target.publicBase, config: pdfConfig, configFile, assetRoots: [...roots, ...pdfInputs.map(file => path.dirname(file))] };
|
|
555
|
+
const preparedPdf = { input, manifest, inputs };
|
|
556
|
+
const common = { status: unreviewed.length ? 'needs-review' : 'ok', output: path.resolve(destination || base), publication, formats: target.formats, profileSelection: profileSelection(loaded.deployment), resources: records.length, documents: records.length, documentIds: selected.map(item => item.doc.frontmatter.id), comparison: { status: 'no-baseline', added: [], removed: [], diagnostics: [] }, assets: assets.records.size, apiReview: manifest.review.apis, diagnostics };
|
|
557
|
+
if (options.preparePdfOnly)
|
|
558
|
+
return { ...common, digest: exportDigest(JSON.stringify(manifest)), preparedPdf };
|
|
559
|
+
checkOutputDirectory(destination, base, workspace.contentDir, inputs);
|
|
560
|
+
// Style/font/logo files are protected by the explicit input list. Their
|
|
561
|
+
// parent directory can be the project root and isn't a content directory.
|
|
562
|
+
for (const directory of roots)
|
|
563
|
+
checkOutputDirectory(destination, base, directory, inputs);
|
|
564
|
+
if (unreviewed.length) {
|
|
565
|
+
const prior = await readFile(path.join(destination, 'manifest.json'), 'utf8').then(JSON.parse).catch(() => undefined);
|
|
566
|
+
if (prior?.review?.status === 'ok')
|
|
567
|
+
throw new Error('Review candidate cannot overwrite an approved export; choose a separate review destination');
|
|
568
|
+
}
|
|
569
|
+
const rendered = await renderPdf(input);
|
|
570
|
+
const filename = singlePage ? 'page.pdf' : 'publication.pdf';
|
|
571
|
+
const pdfArtifacts = new Map([[filename, rendered.bytes], ['NOTICES.txt', notices]]);
|
|
572
|
+
const payload = { ...manifest, renderer: rendered.metadata, files: [...pdfArtifacts].map(([file, bytes]) => ({ path: file, bytes: Buffer.byteLength(bytes), digest: exportDigest(bytes) })) };
|
|
573
|
+
const digest = exportDigest(JSON.stringify(payload));
|
|
574
|
+
pdfArtifacts.set('manifest.json', JSON.stringify({ ...payload, digest }, null, 2) + '\n');
|
|
575
|
+
await commitArtifacts(path.resolve(destination), pdfArtifacts, undefined, 'pdf');
|
|
576
|
+
return { ...common, digest, pdf: filename };
|
|
577
|
+
}
|
|
435
578
|
const manifest = { schemaVersion: publication.product === undefined ? 1 : 2, kind: 'shoalrun-content-export', publicBase: target.publicBase, publication,
|
|
436
579
|
review: { status: unreviewed.length ? 'required' : 'ok', apis: reviews.map(row => ({ apiId: row.apiId, digest: row.digest })) },
|
|
437
580
|
resources: records, assets: [...assets.records.values()].sort((a, b) => compare(a.path, b.path)),
|
package/dist/content-source.d.ts
CHANGED
|
@@ -53,9 +53,11 @@ export type Frontmatter = {
|
|
|
53
53
|
image?: string;
|
|
54
54
|
showNavigationTabs?: boolean;
|
|
55
55
|
showVerified?: boolean;
|
|
56
|
+
pdf?: boolean;
|
|
56
57
|
[key: string]: unknown;
|
|
57
58
|
};
|
|
58
59
|
export type Document = {
|
|
60
|
+
codeReference?: import('./code-reference-model.js').CodeReference;
|
|
59
61
|
featureContext?: import('./deployment-profile.js').FeatureContext;
|
|
60
62
|
change?: ResolvedChange;
|
|
61
63
|
subjectDeprecation?: ResolvedSubjectDeprecation;
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { parse } from './content.js';
|
|
6
6
|
import { planAuthoredDocuments } from './publication-plan.js';
|
|
7
|
+
import { materializeCodeReferences } from './code-references.js';
|
|
7
8
|
import { materializeApiReferences } from './api-publication-plan.js';
|
|
8
9
|
import { explainSegments } from './config.js';
|
|
9
10
|
import { validateSnippetConfig } from './snippets.js';
|
|
@@ -13,6 +14,7 @@ export async function prepareContentWorkspace({ base, basePath, config, loaded,
|
|
|
13
14
|
const { docs, knownDocumentIds, knownDocumentTypes, authoredOperationIds, routeOwners, contentDir } = await planAuthoredDocuments({
|
|
14
15
|
base, basePath, config, variants, language, readerFilters, contentFilter, featureContext: loaded.deployment?.context, metadataProvider, metadataDependencies, sourceOverrides, authoringFiles, allowRouteCollisions
|
|
15
16
|
});
|
|
17
|
+
await materializeCodeReferences({ config, loaded, plan: { docs, knownDocumentIds, knownDocumentTypes, authoredOperationIds, routeOwners, contentDir }, base, basePath, variants, language, readerFilters, contentFilter, dependencies: metadataDependencies, allowRouteCollisions });
|
|
16
18
|
if (config.snippets !== undefined && (!config.snippets || typeof config.snippets !== 'object' || Array.isArray(config.snippets)))
|
|
17
19
|
throw new Error('snippets must be a mapping');
|
|
18
20
|
const snippets = new Map();
|
package/dist/fonts.d.ts
CHANGED
|
@@ -6,12 +6,18 @@ export type FontFace = {
|
|
|
6
6
|
style: 'normal' | 'italic';
|
|
7
7
|
weight: string;
|
|
8
8
|
};
|
|
9
|
+
export type FontPreload = {
|
|
10
|
+
href: string;
|
|
11
|
+
type: string;
|
|
12
|
+
};
|
|
9
13
|
export type SiteFonts = {
|
|
10
14
|
bundled: boolean;
|
|
11
15
|
faces: FontFace[];
|
|
12
16
|
tokens: Partial<Record<FontRole, string>>;
|
|
13
17
|
preload?: string;
|
|
18
|
+
preloadFaces?: FontFace[];
|
|
14
19
|
};
|
|
20
|
+
export declare const fontMediaType: (file: string) => string | undefined;
|
|
15
21
|
export declare const bundledFonts: readonly [{
|
|
16
22
|
readonly family: "Inter";
|
|
17
23
|
readonly file: "inter.woff2";
|
|
@@ -51,4 +57,7 @@ export declare const bundledFonts: readonly [{
|
|
|
51
57
|
}];
|
|
52
58
|
export declare const bundledFontLicenses: string[];
|
|
53
59
|
export declare function resolveFonts(value: unknown, preset: string, declaringFile: string): SiteFonts;
|
|
60
|
+
/** Pin project font URLs to the same bytes that will be emitted by this build. */
|
|
61
|
+
export declare function prepareFontAssets(fonts: SiteFonts): Promise<Map<string, Buffer>>;
|
|
62
|
+
export declare function customFontPreloads(fonts: SiteFonts): FontPreload[];
|
|
54
63
|
export declare function fontsCss(fonts: SiteFonts): string | undefined;
|