@shoalrun/cli 0.1.0-alpha.18 → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +63 -28
- 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/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/branding-security.md +66 -0
- package/agent-docs/changelog/pdf-custom-languages.md +48 -0
- package/agent-docs/changelog.md +17 -1
- package/agent-docs/chat/host.md +20 -2
- package/agent-docs/chat/settings.md +1 -1
- package/agent-docs/chat/settings.sections/configure-chat.md +1 -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/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 +623 -270
- package/agent-docs/features.md +4 -1
- package/agent-docs/index.md +2 -2
- package/agent-docs/provenance.json +6 -6
- 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 +7 -5
- package/agent-docs/what-is-shoalrun.md +2 -2
- package/assets/ask-widget.js +1 -1
- 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 +1 -0
- package/dist/artifact-bundle.js +2 -0
- package/dist/ask-function.js +5 -2
- 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/chat-config.d.ts +2 -0
- package/dist/chat-config.js +10 -2
- package/dist/cli.js +2 -2
- package/dist/compiler.d.ts +4 -2
- package/dist/compiler.js +29 -4
- 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 +1 -0
- 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-renderer.js +6 -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/representation-tree.js +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +35 -8
- package/dist/turnstile-policy.d.ts +2 -0
- package/dist/turnstile-policy.js +23 -0
- package/dist/turnstile.d.ts +7 -0
- package/dist/turnstile.js +29 -2
- package/dist/vercel-function.js +1 -1
- package/dist/vercel.js +6 -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 +4 -2
- package/skills/shoalrun-author/SKILL.md +2 -0
- package/skills/shoalrun-author/references/pdf-export.md +15 -0
- package/skills/shoalrun-brand/SKILL.md +54 -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 +3 -1
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
package/dist/frontmatter.js
CHANGED
|
@@ -4,7 +4,7 @@ import { validateCardMetadata } from './cards.js';
|
|
|
4
4
|
import { resolveTaxonomy, validateTaxonomy, taxonomySchema } from './taxonomy.js';
|
|
5
5
|
import { validateStandaloneSectionDeclarations } from './section-identity.js';
|
|
6
6
|
export const nativePublicFields = new Set(['id', 'title', 'type', 'summary', 'description', 'language', 'locale', 'date', 'updated', 'verified', 'releaseVersion', 'availability', 'product', 'area', 'subarea', 'operations', 'relatedApiReferences', 'audiences', 'readerFilters', 'tags', 'subjectLifecycle', 'subjectDeprecation', 'change', 'lifecycle']);
|
|
7
|
-
export const protectedMetadataRoots = new Set(['shared', 'requires', 'standaloneSections', 'access', 'seo', 'markdownMetadata', 'variables', 'tocDepth', 'layout', 'showNavigationTabs', 'showVerified', 'image', 'private', 'internal', 'card', 'displayText', 'publications']);
|
|
7
|
+
export const protectedMetadataRoots = new Set(['shared', 'requires', 'standaloneSections', 'access', 'seo', 'markdownMetadata', 'variables', 'tocDepth', 'layout', 'showNavigationTabs', 'showVerified', 'pdf', 'image', 'private', 'internal', 'card', 'displayText', 'publications']);
|
|
8
8
|
const fieldTypes = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array']);
|
|
9
9
|
const safePath = (value) => /^[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*)*$/.test(value);
|
|
10
10
|
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -118,6 +118,8 @@ export function validateFrontmatter(frontmatter, config, file, source) {
|
|
|
118
118
|
throw new Error(`${file}: shared must be true; use requires for restricted content`);
|
|
119
119
|
if (frontmatter.shared === true && frontmatter.requires !== undefined)
|
|
120
120
|
throw new Error(`${file}: declare shared: true or requires, not both`);
|
|
121
|
+
if (frontmatter.pdf !== undefined && typeof frontmatter.pdf !== 'boolean')
|
|
122
|
+
throw new Error(`${file}: pdf must be true or false`);
|
|
121
123
|
validateCardMetadata(frontmatter.card, file);
|
|
122
124
|
validateSubjectDeprecation(frontmatter.subjectDeprecation, file);
|
|
123
125
|
validateAuthoredChange(frontmatter, file);
|
|
@@ -259,6 +261,7 @@ export function frontmatterSchema(config) {
|
|
|
259
261
|
lifecycle: { type: 'string', minLength: 1, description: 'Editorial lifecycle of the document; validated by an opted-in external vocabulary.' },
|
|
260
262
|
standaloneSections: { type: 'array', items: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'object', additionalProperties: false, required: ['id', 'context'], properties: { id: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,511}$' }, context: { const: 'section' } } }] }, uniqueItems: true, description: 'Page-specific standalone approval: a heading title approves contextual discovery with the page introduction; {id, context: section} approves the root level-two section slice for content export. IDs may be derived or explicitly anchored. Omitted means full-page reads.' },
|
|
261
263
|
tocDepth: { type: 'integer', enum: [0, 2, 3, 4, 5, 6], description: 'Deepest heading shown in the page table of contents; 0 hides it. Defaults to 3.' },
|
|
264
|
+
pdf: { type: 'boolean', description: 'Controls the optional page print/PDF action. False suppresses its control and endpoint inputs; true follows the site setting. Site builds do not generate per-page PDFs.' },
|
|
262
265
|
showVerified: { type: 'boolean', description: 'Show an authored verified date in the HTML page header. Defaults to true. False hides it only in HTML; Markdown and manifest dates remain unchanged. Does not assign a verification date.' },
|
|
263
266
|
showNavigationTabs: { type: 'boolean', description: 'Show the HTML navigation section bar when tabs are configured. Defaults to true for docs and wide layouts, false for landing and splash. Does not change agent discovery or section membership.' },
|
|
264
267
|
image: { type: 'string', pattern: '\\.(?:png|jpe?g)$', description: 'Local .png or .jpg shown in link previews for this page, relative to the page file; replaces the generated share image. Best at 1200×630.' },
|
|
@@ -332,7 +335,7 @@ export function frontmatterReference(config, sectionsOnly = false) {
|
|
|
332
335
|
'Language is the content language and selects a publication. Locale describes formatting conventions in the content and does not select a publication. `frontmatter.locales` can set a project locale for each language; pages may override it.', '',
|
|
333
336
|
'## Categorize, connect, and present the page', '',
|
|
334
337
|
'By default, `product`, `area`, and `subarea` are optional, nonempty string categories. They can come from project defaults and appear in the document manifest and search index. A matching configured reader facet makes each category a page filter; without that facet it remains taxonomy only. `operations` links a guide or reference to stable API operation IDs. On a guide or how-to page, `relatedApiReferences` names API reference page IDs to add cross links in both directions within the same publication. Tags help document collections find the page. Card settings change its preview when another page links to it; they do not change the destination. `subjectLifecycle` displays the status of the documented API or feature; `lifecycle` is available for the document\'s editorial state. `tocDepth` limits the HTML page table of contents; use 0 to hide it. `layout` picks the HTML page layout: `docs`, `wide`, `landing`, or `splash`. `showNavigationTabs` overrides the navigation tab bar for this page; landing and splash pages hide it by default. `image` replaces the generated link-preview image. `date` and `releaseVersion` can identify a changelog entry under the project\'s `frontmatter.changelogIdentifier` rule.', '',
|
|
335
|
-
...table, ...nativeRows(['availability', 'product', 'area', 'subarea', 'operations', 'relatedApiReferences', 'tags', 'date', 'releaseVersion', 'subjectLifecycle', 'subjectDeprecation', 'change', 'lifecycle', 'card', 'standaloneSections', 'tocDepth', 'layout', 'showNavigationTabs', 'showVerified', 'image']), '',
|
|
338
|
+
...table, ...nativeRows(['availability', 'product', 'area', 'subarea', 'operations', 'relatedApiReferences', 'tags', 'date', 'releaseVersion', 'subjectLifecycle', 'subjectDeprecation', 'change', 'lifecycle', 'card', 'standaloneSections', 'tocDepth', 'layout', 'showNavigationTabs', 'showVerified', 'pdf', 'image']), '',
|
|
336
339
|
'Projects can opt into built-in category enforcement with `frontmatter.taxonomy`. It declares areas and their allowed subareas, optional required fields, and optional explicit-field requirements for authored pages. Its `multiple` list permits arrays for selected category fields; otherwise each field allows one value. Defaults and enforced values still apply; generated API references use resolved categories. Definitions appear in the document manifest and constrain the generated frontmatter schema. Omit taxonomy or set it to false to preserve optional category behavior.', '',
|
|
337
340
|
'With `profiles.requireClassification: true`, every public source page in the shared enabled publications needs an explicit `shared: true` or `requires` declaration before customer selection. Defaults do not count. Shared pages may contain feature-specific sections wrapped in `<ForFeature>`; unwrapped text remains shared.', '',
|
|
338
341
|
'## Control output details', '',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LanguageRegistration } from 'shiki';
|
|
2
|
+
export type LanguageFiles = {
|
|
3
|
+
grammarOrigin?: (index: number) => string;
|
|
4
|
+
onDependency?: (file: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function customHighlightLanguages(value: unknown, declaringFile: string | undefined, reserved: ReadonlySet<string>, options: LanguageFiles, reservedScopes?: ReadonlySet<string>): Promise<{
|
|
7
|
+
file: string;
|
|
8
|
+
grammar: LanguageRegistration;
|
|
9
|
+
}[]>;
|