@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/fonts.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
export const fontMediaType = (file) => ({ '.woff2': 'font/woff2', '.woff': 'font/woff', '.ttf': 'font/ttf', '.otf': 'font/otf' }[path.extname(file).toLowerCase()]);
|
|
2
5
|
// Latin variable fonts from Fontsource, all under the SIL Open Font License. Readers download only the faces a page uses.
|
|
3
6
|
export const bundledFonts = [
|
|
4
7
|
{ family: 'Inter', file: 'inter.woff2', style: 'normal' },
|
|
@@ -22,7 +25,7 @@ const fallbackFaces = [
|
|
|
22
25
|
{ family: 'Jost', src: localSans, size: '95.84%', ascent: '111.65%', descent: '39.13%' }
|
|
23
26
|
];
|
|
24
27
|
export const bundledFontLicenses = ['inter', 'figtree', 'source-serif-4', 'jetbrains-mono', 'jost'].map(name => `${name}-LICENSE.txt`);
|
|
25
|
-
//
|
|
28
|
+
// Begin fetching the preset's body font before the browser discovers its CSS rule.
|
|
26
29
|
const presetBodyFont = { shoal: 'inter.woff2', reef: 'figtree.woff2', ledger: 'source-serif-4.woff2', vice: 'figtree.woff2' };
|
|
27
30
|
const fallbacks = {
|
|
28
31
|
body: 'ui-sans-serif, system-ui, sans-serif',
|
|
@@ -45,6 +48,8 @@ export function resolveFonts(value, preset, declaringFile) {
|
|
|
45
48
|
if (unknown.length)
|
|
46
49
|
throw new Error(`theme.fonts accepts only body, headings, and code; remove ${unknown.join(', ')}`);
|
|
47
50
|
const faces = [];
|
|
51
|
+
const preloadFaces = [];
|
|
52
|
+
const families = {};
|
|
48
53
|
const tokens = {};
|
|
49
54
|
for (const role of roles) {
|
|
50
55
|
const entry = value[role];
|
|
@@ -56,6 +61,7 @@ export function resolveFonts(value, preset, declaringFile) {
|
|
|
56
61
|
if (typeof spec.family !== 'string' || !familyName.test(spec.family))
|
|
57
62
|
throw new Error(`theme.fonts.${role}.family must be a font name using letters, numbers, spaces, dots, dashes, or underscores`);
|
|
58
63
|
const files = spec.files ?? [];
|
|
64
|
+
families[role] = spec.family;
|
|
59
65
|
if (!Array.isArray(files))
|
|
60
66
|
throw new Error(`theme.fonts.${role}.files must be a list`);
|
|
61
67
|
for (const [index, item] of files.entries()) {
|
|
@@ -74,11 +80,60 @@ export function resolveFonts(value, preset, declaringFile) {
|
|
|
74
80
|
const file = path.resolve(path.dirname(declaringFile), face.file);
|
|
75
81
|
if (!/^[\w.-]+$/.test(path.basename(file)))
|
|
76
82
|
throw new Error(`${where} file name may use only letters, numbers, dots, dashes, and underscores`);
|
|
77
|
-
|
|
83
|
+
const registration = { family: spec.family, file, route: `/assets/fonts/project/${path.basename(file)}`, style, weight };
|
|
84
|
+
faces.push(registration);
|
|
78
85
|
}
|
|
79
86
|
tokens[role] = `"${spec.family}", ${fallbacks[role]}`;
|
|
80
87
|
}
|
|
81
|
-
|
|
88
|
+
// CSS resolves style before weight and prefers the last equivalent registration.
|
|
89
|
+
const registrations = [...bundledFonts.map(font => ({ family: font.family, file: font.file, route: `/assets/fonts/${font.file}`, style: font.style, weight: '100 900' })), ...faces];
|
|
90
|
+
for (const role of ['body', 'headings']) {
|
|
91
|
+
if (!families[role])
|
|
92
|
+
continue;
|
|
93
|
+
const target = role === 'body' ? 400 : preset === 'vice' ? 800 : 700;
|
|
94
|
+
const style = role === 'headings' && preset === 'vice' ? 'italic' : 'normal';
|
|
95
|
+
const family = registrations.filter(face => face.family === families[role]);
|
|
96
|
+
const exactStyle = family.filter(face => face.style === style);
|
|
97
|
+
const rank = (face) => {
|
|
98
|
+
const [min, max = min] = face.weight.split(' ').map(Number);
|
|
99
|
+
if (min <= target && max >= target)
|
|
100
|
+
return [0, 0];
|
|
101
|
+
if (target >= 400 && target <= 500) {
|
|
102
|
+
if (min > target && min <= 500)
|
|
103
|
+
return [1, min - target];
|
|
104
|
+
return max < target ? [2, target - max] : [3, min - target];
|
|
105
|
+
}
|
|
106
|
+
if (target < 400)
|
|
107
|
+
return max < target ? [1, target - max] : [2, min - target];
|
|
108
|
+
return min > target ? [1, min - target] : [2, target - max];
|
|
109
|
+
};
|
|
110
|
+
const candidates = (exactStyle.length ? exactStyle : family).map((face, index) => ({ face, index, rank: rank(face) }));
|
|
111
|
+
candidates.sort((a, b) => a.rank[0] - b.rank[0] || a.rank[1] - b.rank[1] || b.index - a.index);
|
|
112
|
+
if (candidates[0])
|
|
113
|
+
preloadFaces.push(candidates[0].face);
|
|
114
|
+
}
|
|
115
|
+
return { bundled: true, faces, tokens, preload: tokens.body ? undefined : presetBodyFont[preset], preloadFaces };
|
|
116
|
+
}
|
|
117
|
+
/** Pin project font URLs to the same bytes that will be emitted by this build. */
|
|
118
|
+
export async function prepareFontAssets(fonts) {
|
|
119
|
+
const files = new Map();
|
|
120
|
+
const assets = new Map();
|
|
121
|
+
for (const face of fonts.faces) {
|
|
122
|
+
let bytes = files.get(face.file);
|
|
123
|
+
if (!bytes) {
|
|
124
|
+
bytes = await readFile(face.file);
|
|
125
|
+
files.set(face.file, bytes);
|
|
126
|
+
}
|
|
127
|
+
const extension = path.extname(face.file).toLowerCase();
|
|
128
|
+
const name = path.basename(face.file, path.extname(face.file));
|
|
129
|
+
const hash = createHash('sha256').update(bytes).digest('hex');
|
|
130
|
+
face.route = `/assets/fonts/project/${name}-${hash}${extension}`;
|
|
131
|
+
assets.set(face.route, bytes);
|
|
132
|
+
}
|
|
133
|
+
return assets;
|
|
134
|
+
}
|
|
135
|
+
export function customFontPreloads(fonts) {
|
|
136
|
+
return [...new Map((fonts.preloadFaces ?? []).map(face => [face.route, { href: face.route, type: fontMediaType(face.file) }])).values()];
|
|
82
137
|
}
|
|
83
138
|
const face = (family, url, style, weight, format) => `@font-face{font-family:"${family}";src:url("${url}") format("${format}");font-style:${style};font-weight:${weight};font-display:swap}`;
|
|
84
139
|
export function fontsCss(fonts) {
|
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
|
+
}[]>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const identifier = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
4
|
+
const mapping = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
export async function customHighlightLanguages(value, declaringFile, reserved, options, reservedScopes = new Set()) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return [];
|
|
8
|
+
if (!Array.isArray(value))
|
|
9
|
+
throw new Error('outputs.html.highlighting.languages must be a list');
|
|
10
|
+
const names = new Set(reserved);
|
|
11
|
+
const scopes = new Set(reservedScopes);
|
|
12
|
+
const result = [];
|
|
13
|
+
for (const [index, entry] of value.entries()) {
|
|
14
|
+
const setting = `outputs.html.highlighting.languages[${index}]`;
|
|
15
|
+
if (!mapping(entry) || Object.keys(entry).some(key => !['id', 'grammar', 'aliases'].includes(key)))
|
|
16
|
+
throw new Error(`${setting} accepts id, grammar, and aliases`);
|
|
17
|
+
if (typeof entry.id !== 'string' || !identifier.test(entry.id))
|
|
18
|
+
throw new Error(`${setting}.id must be a lowercase language ID, using letters, digits, underscores, or hyphens`);
|
|
19
|
+
const aliases = entry.aliases ?? [];
|
|
20
|
+
if (!Array.isArray(aliases) || aliases.some(alias => typeof alias !== 'string' || !identifier.test(alias)))
|
|
21
|
+
throw new Error(`${setting}.aliases must be a list of lowercase language IDs`);
|
|
22
|
+
for (const name of [entry.id, ...aliases]) {
|
|
23
|
+
if (names.has(name))
|
|
24
|
+
throw new Error(`${setting}: language ID or alias ${name} is already registered`);
|
|
25
|
+
names.add(name);
|
|
26
|
+
}
|
|
27
|
+
if (typeof entry.grammar !== 'string' || !entry.grammar || path.isAbsolute(entry.grammar) || /^[a-z][a-z0-9+.-]*:/i.test(entry.grammar) || path.extname(entry.grammar).toLowerCase() !== '.json')
|
|
28
|
+
throw new Error(`${setting}.grammar must be a relative path to a local TextMate JSON grammar`);
|
|
29
|
+
const origin = options.grammarOrigin?.(index) ?? declaringFile;
|
|
30
|
+
if (!origin)
|
|
31
|
+
throw new Error(`${setting}.grammar needs the declaring configuration file`);
|
|
32
|
+
const file = path.resolve(path.dirname(origin), entry.grammar);
|
|
33
|
+
options.onDependency?.(file);
|
|
34
|
+
const info = await stat(file).catch(() => { throw new Error(`${setting}: cannot read grammar ${file}`); });
|
|
35
|
+
if (!info.isFile() || info.size > 4 * 1024 * 1024)
|
|
36
|
+
throw new Error(`${setting}: grammar must be a regular JSON file no larger than 4 MiB: ${file}`);
|
|
37
|
+
let grammar;
|
|
38
|
+
try {
|
|
39
|
+
grammar = JSON.parse(await readFile(file, 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new Error(`${setting}: invalid TextMate JSON grammar ${file}: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
if (!mapping(grammar) || typeof grammar.scopeName !== 'string' || !/^\S+$/.test(grammar.scopeName) || !Array.isArray(grammar.patterns))
|
|
45
|
+
throw new Error(`${setting}: grammar needs a scopeName and a patterns array: ${file}`);
|
|
46
|
+
if (scopes.has(grammar.scopeName))
|
|
47
|
+
throw new Error(`${setting}: grammar scopeName ${grammar.scopeName} is already registered`);
|
|
48
|
+
scopes.add(grammar.scopeName);
|
|
49
|
+
result.push({ file, grammar: { ...grammar, name: entry.id, aliases } });
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
package/dist/highlight.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LanguageFiles } from './highlight-languages.js';
|
|
1
2
|
export type HighlightToken = {
|
|
2
3
|
text: string;
|
|
3
4
|
color?: string;
|
|
@@ -8,7 +9,9 @@ export type CodeHighlighter = {
|
|
|
8
9
|
foreground?: string;
|
|
9
10
|
dark?: boolean;
|
|
10
11
|
dependency?: string;
|
|
12
|
+
dependencies?: string[];
|
|
13
|
+
dispose?: () => void;
|
|
11
14
|
};
|
|
12
|
-
export declare function createCodeHighlighter(config?: any, declaringFile?: string): Promise<CodeHighlighter>;
|
|
15
|
+
export declare function createCodeHighlighter(config?: any, declaringFile?: string, options?: LanguageFiles): Promise<CodeHighlighter>;
|
|
13
16
|
export declare function highlightCode(highlighter?: CodeHighlighter): (tree: any) => void;
|
|
14
17
|
export declare function highlightedExample(value: string, language: string, highlighter?: CodeHighlighter): string;
|
package/dist/highlight.js
CHANGED
|
@@ -2,16 +2,18 @@ import path from 'node:path';
|
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { unified } from 'unified';
|
|
4
4
|
import rehypeStringify from 'rehype-stringify';
|
|
5
|
+
import { customHighlightLanguages } from './highlight-languages.js';
|
|
5
6
|
const plain = { highlight: text => [{ text }] };
|
|
6
7
|
const stringify = unified().use(rehypeStringify);
|
|
7
8
|
const hex = /^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i;
|
|
8
9
|
// The site theme colors tokens with CSS variables that each palette sets.
|
|
9
10
|
const siteColor = /^var\(--code-(?:foreground|token-[a-z-]+)\)$/;
|
|
10
|
-
|
|
11
|
+
const defaultLanguages = ['json', 'yaml', 'bash', 'javascript', 'typescript', 'python', 'html', 'css', 'mdx', 'markdown', 'http', 'xml', 'sql', 'go', 'java', 'ruby', 'csharp', 'php', 'rust'];
|
|
12
|
+
export async function createCodeHighlighter(config = {}, declaringFile, options = {}) {
|
|
11
13
|
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
12
14
|
throw new Error('outputs.html.highlighting must be a mapping');
|
|
13
15
|
const provider = config.provider ?? 'shiki';
|
|
14
|
-
const allowed = provider === 'shiki' ? ['provider', 'theme'] : provider === 'custom' ? ['provider', 'module'] : ['provider'];
|
|
16
|
+
const allowed = provider === 'shiki' ? ['provider', 'theme', 'languages'] : provider === 'custom' ? ['provider', 'module'] : ['provider'];
|
|
15
17
|
for (const key of Object.keys(config))
|
|
16
18
|
if (!allowed.includes(key))
|
|
17
19
|
throw new Error(`Unsupported highlighting setting ${key} for ${provider}`);
|
|
@@ -29,17 +31,30 @@ export async function createCodeHighlighter(config = {}, declaringFile) {
|
|
|
29
31
|
if (provider !== 'shiki')
|
|
30
32
|
throw new Error(`Unknown highlighting provider ${provider}`);
|
|
31
33
|
// Load only when selected. Shiki caches its build-time instance and grammars.
|
|
32
|
-
const { getSingletonHighlighter, bundledThemes, createCssVariablesTheme } = await import('shiki');
|
|
34
|
+
const { getSingletonHighlighter, createHighlighter, bundledLanguages, bundledThemes, createCssVariablesTheme } = await import('shiki');
|
|
33
35
|
const theme = config.theme ?? 'site';
|
|
34
36
|
if (typeof theme !== 'string' || (theme !== 'site' && !Object.hasOwn(bundledThemes, theme)))
|
|
35
37
|
throw new Error(`Unknown Shiki theme ${theme}; use site or a bundled Shiki theme name`);
|
|
36
|
-
const
|
|
38
|
+
const builtinGrammars = Array.isArray(config.languages) && config.languages.length ? (await Promise.all(defaultLanguages.map(async (language) => (await bundledLanguages[language]()).default))).flat() : [];
|
|
39
|
+
const custom = await customHighlightLanguages(config.languages, declaringFile, new Set([...Object.keys(bundledLanguages), 'text', 'txt', 'plain', 'plaintext', 'ansi']), options, new Set(builtinGrammars.map(grammar => grammar.scopeName)));
|
|
40
|
+
// Custom grammars belong to this compilation, never Shiki's process-wide registry.
|
|
41
|
+
const create = custom.length ? createHighlighter : getSingletonHighlighter;
|
|
42
|
+
const highlighter = await create({
|
|
37
43
|
themes: [theme === 'site' ? createCssVariablesTheme({ name: 'site', variablePrefix: '--code-' }) : theme],
|
|
38
|
-
langs: [
|
|
44
|
+
langs: [...defaultLanguages]
|
|
39
45
|
});
|
|
46
|
+
try {
|
|
47
|
+
if (custom.length)
|
|
48
|
+
await highlighter.loadLanguage(...custom.map(item => item.grammar));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
highlighter.dispose();
|
|
52
|
+
throw new Error(`Cannot load TextMate grammars ${custom.map(item => item.file).join(', ')}: ${error.message}`);
|
|
53
|
+
}
|
|
40
54
|
const languages = new Set(highlighter.getLoadedLanguages());
|
|
41
55
|
const colors = highlighter.getTheme(theme);
|
|
42
56
|
return {
|
|
57
|
+
...(custom.length ? { dependencies: custom.map(item => item.file), dispose: () => highlighter.dispose() } : {}),
|
|
43
58
|
...(theme === 'site' ? {} : { background: colors.bg, foreground: colors.fg, dark: colors.type === 'dark' }),
|
|
44
59
|
highlight(value, language) {
|
|
45
60
|
const lang = language?.toLowerCase();
|
package/dist/output-commit.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function commitArtifacts(target: string, artifacts: Map<string, string | Buffer>, prepare?: () => Promise<void>, kind?: 'site' | 'export'): Promise<void>;
|
|
1
|
+
export declare function commitArtifacts(target: string, artifacts: Map<string, string | Buffer>, prepare?: () => Promise<void>, kind?: 'site' | 'export' | 'pdf'): Promise<void>;
|
package/dist/output-commit.js
CHANGED
|
@@ -65,9 +65,11 @@ async function checkReplaceable(target, kind) {
|
|
|
65
65
|
return;
|
|
66
66
|
if (kind === 'site' && entries.includes('documents.json') && entries.includes('resource-headers.json'))
|
|
67
67
|
return;
|
|
68
|
-
if (kind === 'export' && entries.includes('manifest.json')) {
|
|
68
|
+
if ((kind === 'export' || kind === 'pdf') && entries.includes('manifest.json')) {
|
|
69
69
|
const manifest = JSON.parse(await readFile(path.join(target, 'manifest.json'), 'utf8'));
|
|
70
|
-
if (manifest.kind === 'shoalrun-content-export' && manifest.schemaVersion
|
|
70
|
+
if (kind === 'export' && manifest.kind === 'shoalrun-content-export' && [1, 2].includes(manifest.schemaVersion))
|
|
71
|
+
return;
|
|
72
|
+
if (kind === 'pdf' && manifest.kind === 'shoalrun-pdf-export' && manifest.schemaVersion === 1)
|
|
71
73
|
return;
|
|
72
74
|
}
|
|
73
75
|
throw new Error(`Output directory ${target} already exists and is not an earlier Shoalrun ${kind === 'site' ? 'build (it has no documents.json and resource-headers.json)' : 'content export'}. The build replaces its output directory, so it stopped instead of deleting it. Choose a new or empty directory`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderCodeReference } from './code-references.js';
|
|
1
2
|
import { documentKey, publicationKey } from './publication-identity.js';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -28,8 +29,8 @@ export async function materializePages({ variables, frontmatterConfig, publicati
|
|
|
28
29
|
for (const other of linkDocuments.values())
|
|
29
30
|
if (keyFor(other.frontmatter.id, doc.variant) === keyFor(other.frontmatter.id, other.variant))
|
|
30
31
|
local.set(other.frontmatter.id, other);
|
|
31
|
-
const parsed = parse.parse(doc.body);
|
|
32
|
-
const tree = await resolveApiEmbedTree(transformMdx(parsed, doc, local, snippets, variables, snippetUses, calloutTypes, base, readerFilters, frontmatterConfig, publications, badgeVariants, componentPlugins, linkDocuments), api, operationId => sourceForOperation(operationId, doc.variant), apiId => {
|
|
32
|
+
const parsed = doc.codeReference ? null : parse.parse(doc.body);
|
|
33
|
+
const tree = doc.codeReference ? renderCodeReference(doc.codeReference, doc, linkDocuments) : await resolveApiEmbedTree(transformMdx(parsed, doc, local, snippets, variables, snippetUses, calloutTypes, base, readerFilters, frontmatterConfig, publications, badgeVariants, componentPlugins, linkDocuments), api, operationId => sourceForOperation(operationId, doc.variant), apiId => {
|
|
33
34
|
if (doc.featureContext?.grants)
|
|
34
35
|
throw new Error('Standalone API schema embeds require an explicitly scoped source and are not supported in profiled builds; select an operation instead');
|
|
35
36
|
const definition = apiPlan.apiDefinitions[apiId];
|
package/dist/page-renderer.d.ts
CHANGED
package/dist/page-renderer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pdfPageAction } from './pdf-button.js';
|
|
1
2
|
import { publicationKey } from './publication-identity.js';
|
|
2
3
|
import { resolveAgentDiscovery } from './agent-discovery.js';
|
|
3
4
|
import { navigationCounts, navigationHeading } from './compiler-config.js';
|
|
@@ -37,6 +38,8 @@ export function shareCardText(doc, config) {
|
|
|
37
38
|
return { title, summary: String(summary), type: typeLabel(doc.frontmatter.type) };
|
|
38
39
|
}
|
|
39
40
|
export function typeLabel(type) {
|
|
41
|
+
if (type === 'code-reference')
|
|
42
|
+
return 'Code API reference';
|
|
40
43
|
const words = String(type).split('-').filter(Boolean).map(word => word === 'api' ? 'API' : word);
|
|
41
44
|
const label = words.join(' ');
|
|
42
45
|
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
@@ -60,6 +63,10 @@ export function buildHtmlPage(doc, tree, markdownUrl, siteTitle, navigation, exa
|
|
|
60
63
|
const sampleBlock = (list, fallbackCurl, idHint) => list?.length ? codeSamplesBlock(list, highlighter, esc, idHint) : fallbackCurl ? highlightedExample(fallbackCurl, 'bash', highlighter) : '';
|
|
61
64
|
const subjectLifecycleLabel = (subjectLifecycle) => ({ deprecated: 'Deprecated', 'in-development': 'In development', beta: 'Beta', experimental: 'Experimental' })[subjectLifecycle];
|
|
62
65
|
const badge = (subjectLifecycle) => subjectLifecycle ? `<span class="subject-lifecycle-badge subject-lifecycle-${subjectLifecycle}">${subjectLifecycleLabel(subjectLifecycle)}</span>` : '';
|
|
66
|
+
const pdfAction = pdfPageAction(doc, config.outputs, normalizeBasePath(config.site?.basePath));
|
|
67
|
+
const pdfLink = pdfAction ? pdfAction.mode === 'print'
|
|
68
|
+
? `<button type="button" class="formats pdf-print" data-print-page data-print-settings="${esc(sitePath(normalizeBasePath(config.site?.basePath), '/assets/print/settings.json'))}" hidden>${esc(pdfAction.label)}</button><span role="status" data-print-status></span><script type="application/json" data-print-page-meta>${JSON.stringify({ title: doc.frontmatter.title, summary: doc.frontmatter.summary, publication: doc.variant }).replaceAll('<', '\\u003c')}</script>`
|
|
69
|
+
: `<a class="formats pdf-download" href="${esc(pdfAction.href)}">${esc(pdfAction.label)}</a>` : '';
|
|
63
70
|
const readerConfig = config.readerFilters;
|
|
64
71
|
const basePath = normalizeBasePath(config.site?.basePath);
|
|
65
72
|
const agent = resolveAgentDiscovery(config.site?.agentDiscovery);
|
|
@@ -187,6 +194,6 @@ export function buildHtmlPage(doc, tree, markdownUrl, siteTitle, navigation, exa
|
|
|
187
194
|
const menu = side || sectionNavigation ? `<a class="mobile-menu" href="#${side ? 'docs-navigation' : 'docs-sections'}">Menu</a>` : '';
|
|
188
195
|
const rail = toc || panel ? `<aside class="page-rail">${doc.endpoint ? `${panel}${toc}` : `${toc}${panel}`}</aside>` : '';
|
|
189
196
|
const breadcrumbs = (shell.breadcrumbs?.length ?? 0) > 1 && !hero ? `<nav class="doc-breadcrumbs" aria-label="Breadcrumb"><ol>${shell.breadcrumbs.map((item, index) => `<li>${index === shell.breadcrumbs.length - 1 ? `<span aria-current="page">${esc(item.label)}</span>` : item.url ? `<a href="${esc(item.url)}">${esc(item.label)}</a>` : `<span>${esc(item.label)}</span>`}</li>`).join('')}</ol></nav>` : '';
|
|
190
|
-
const markup = `<!doctype html><html lang="${esc(doc.variant.language)}"${shell.themeCss || shell.paletteCss ? ` data-theme="${esc(shell.preset ?? 'shoal')}" data-palette="${esc(shell.palette ?? '')}"` : ''}${shell.masthead ? ` data-masthead="${esc(shell.masthead)}"` : ''}${layout === 'docs' ? '' : ` data-layout="${layout}"`}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="${shell.mode === 'system' ? 'light dark' : shell.mode ?? 'light'}">${shell.indexing === false || doc.frontmatter.seo?.noindex === true ? '<meta name="robots" content="noindex">' : ''}${shell.origin ? `<link rel="canonical" href="${esc(absolute(doc.route))}">` : ''}${shell.toggle ? '<script src="/assets/mode.js"></script>' : ''}<title>${esc(String(title))}</title>${description ? `<meta name="description" content="${esc(String(description))}">` : ''}${socialDescription ? `<meta property="og:description" content="${esc(socialDescription)}">` : ''}${social}<link rel="alternate" type="text/markdown" href="${esc(markdownUrl)}"><link rel="describedby" href="/llms.min.txt" title="Documentation index">${icons}${shell.preloadFont ? `<link rel="preload" href="/assets/fonts/${esc(shell.preloadFont)}" as="font" type="font/woff2" crossorigin>` : ''}<link rel="stylesheet" href="/assets/site.css">${article.includes('data-theme-preview') ? '<link rel="stylesheet" href="/assets/theme-preview.css">' : ''}${shell.themeCss ? '<link rel="stylesheet" href="/assets/theme.css" data-theme-link="theme">' : ''}${shell.paletteCss ? '<link rel="stylesheet" href="/assets/palette.css" data-theme-link="palette">' : ''}${shell.badgesCss ? '<link rel="stylesheet" href="/assets/badges.css">' : ''}${shell.themeSwitcher ? `<script src="/assets/theme-switcher.js" data-themes="${esc(shell.themeSwitcher)}"></script>` : ''}${shell.fontsCss ? '<link rel="stylesheet" href="/assets/fonts.css">' : ''}${customCss ? '<link rel="stylesheet" href="/assets/project.css">' : ''}</head><body><a class="skip" href="#main">Skip to content</a>${agent.mode === 'disclosure' ? '' : agentBlock}<header class="mast"><div class="mast-inner">${shell.home ? `<a class="${brandClass}" href="/">${brand}</a>` : `<span class="${brandClass}">${brand}</span>`}${publicationPicker(shell, esc)}${editionLabel && !shell.publicationChoices?.length && (shell.publicationAlternates?.length ?? 0) < 2 ? `<span class="edition">${esc(editionLabel)}</span>` : ''}${menu}${headerTools(shell, doc.variant, esc, readerConfig)}${agent.mode === 'disclosure' ? agentBlock : ''}</div></header>${sectionNavigation}${shortcuts}<div class="layout${layout === 'docs' ? '' : ` layout-${layout}`}${rail ? ' has-rail' : ''}${panel ? ' has-examples' : ''}${doc.endpoint ? ' api-reference' : ''}"><main id="main" class="content" tabindex="-1" data-document-type="${esc(doc.frontmatter.type)}"${readerConfig ? ` data-reader-page="${esc(readerFilterAttribute(doc.readerFilters ?? {}))}"` : ''}>${breadcrumbs}${hero ? '' : `<header class="page-head"><span class="eyebrow">${esc(typeLabel(doc.frontmatter.type))}</span>${doc.endpoint ? '' : badge(doc.frontmatter.subjectLifecycle)}<h1>${esc(doc.frontmatter.title)}${doc.endpoint ? badge(doc.frontmatter.subjectLifecycle) : ''}</h1>${pageHeader && !routeSummary ? `<p class="summary">${esc(pageHeader)}</p>` : ''}${pageDates}${endpoint}${audienceBadges}</header>`}${hero ? '' : `<div class="page-formats"
|
|
197
|
+
const markup = `<!doctype html><html lang="${esc(doc.variant.language)}"${shell.themeCss || shell.paletteCss ? ` data-theme="${esc(shell.preset ?? 'shoal')}" data-palette="${esc(shell.palette ?? '')}"` : ''}${shell.masthead ? ` data-masthead="${esc(shell.masthead)}"` : ''}${layout === 'docs' ? '' : ` data-layout="${layout}"`}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="${shell.mode === 'system' ? 'light dark' : shell.mode ?? 'light'}">${shell.indexing === false || doc.frontmatter.seo?.noindex === true ? '<meta name="robots" content="noindex">' : ''}${shell.origin ? `<link rel="canonical" href="${esc(absolute(doc.route))}">` : ''}${shell.toggle ? '<script src="/assets/mode.js"></script>' : ''}<title>${esc(String(title))}</title>${description ? `<meta name="description" content="${esc(String(description))}">` : ''}${socialDescription ? `<meta property="og:description" content="${esc(socialDescription)}">` : ''}${social}<link rel="alternate" type="text/markdown" href="${esc(markdownUrl)}"><link rel="describedby" href="/llms.min.txt" title="Documentation index">${icons}${shell.preloadFont ? `<link rel="preload" href="/assets/fonts/${esc(shell.preloadFont)}" as="font" type="font/woff2" crossorigin>` : ''}${(shell.preloadFonts ?? []).map(font => `<link rel="preload" href="${esc(font.href)}" as="font" type="${esc(font.type)}" crossorigin>`).join('')}<link rel="stylesheet" href="/assets/site.css">${article.includes('data-theme-preview') ? '<link rel="stylesheet" href="/assets/theme-preview.css">' : ''}${shell.themeCss ? '<link rel="stylesheet" href="/assets/theme.css" data-theme-link="theme">' : ''}${shell.paletteCss ? '<link rel="stylesheet" href="/assets/palette.css" data-theme-link="palette">' : ''}${shell.badgesCss ? '<link rel="stylesheet" href="/assets/badges.css">' : ''}${shell.themeSwitcher ? `<script src="/assets/theme-switcher.js" data-themes="${esc(shell.themeSwitcher)}"></script>` : ''}${shell.fontsCss ? '<link rel="stylesheet" href="/assets/fonts.css">' : ''}${customCss ? '<link rel="stylesheet" href="/assets/project.css">' : ''}</head><body><a class="skip" href="#main">Skip to content</a>${agent.mode === 'disclosure' ? '' : agentBlock}<header class="mast"><div class="mast-inner">${shell.home ? `<a class="${brandClass}" href="/">${brand}</a>` : `<span class="${brandClass}">${brand}</span>`}${publicationPicker(shell, esc)}${editionLabel && !shell.publicationChoices?.length && (shell.publicationAlternates?.length ?? 0) < 2 ? `<span class="edition">${esc(editionLabel)}</span>` : ''}${menu}${headerTools(shell, doc.variant, esc, readerConfig)}${agent.mode === 'disclosure' ? agentBlock : ''}</div></header>${sectionNavigation}${shortcuts}<div class="layout${layout === 'docs' ? '' : ` layout-${layout}`}${rail ? ' has-rail' : ''}${panel ? ' has-examples' : ''}${doc.endpoint ? ' api-reference' : ''}"><main id="main" class="content" tabindex="-1" data-document-type="${esc(doc.frontmatter.type)}"${readerConfig ? ` data-reader-page="${esc(readerFilterAttribute(doc.readerFilters ?? {}))}"` : ''}>${breadcrumbs}${hero ? '' : `<header class="page-head"><span class="eyebrow">${esc(typeLabel(doc.frontmatter.type))}</span>${doc.endpoint ? '' : badge(doc.frontmatter.subjectLifecycle)}<h1>${esc(doc.frontmatter.title)}${doc.endpoint ? badge(doc.frontmatter.subjectLifecycle) : ''}</h1>${pageHeader && !routeSummary ? `<p class="summary">${esc(pageHeader)}</p>` : ''}${pageDates}${endpoint}${audienceBadges}</header>`}${hero ? (pdfLink ? `<div class="page-formats">${pdfLink}</div>` : '') : `<div class="page-formats">${pdfAction?.placement === 'before' ? pdfLink : ''}<a class="formats" data-markdown-link href="${esc(markdownUrl)}">Read as Markdown</a><button type="button" class="formats" data-copy-markdown hidden>Copy as Markdown</button><span role="status" data-copy-markdown-status></span>${pdfAction?.placement === 'after' ? pdfLink : ''}</div>`}${toc ? '<div class="mobile-outline-slot"></div>' : ''}${jumps}${readerBanner}<article>${article}</article></main>${rail}${side}</div>${footer}${analyticsBootstrap}${readerBootstrap}${pdfAction?.mode === 'print' ? '<script type="module" src="/assets/print.js"></script>' : ''}<script type="module" src="/assets/copy-markdown.js"></script><script type="module" src="/assets/navigation.js"></script><script type="module" src="/assets/reader.js"></script><script type="module" src="/assets/tabs.js"></script>${shell.search ? '<script type="module" src="/assets/search.js"></script>' : ''}${article.includes('data-tooltip') ? '<script type="module" src="/assets/tooltips.js"></script>' : ''}${article.includes('data-component-module') ? '<script type="module" src="/assets/components.js"></script>' : ''}${article.includes('data-file-tree') ? '<script type="module" src="/assets/file-tree.js"></script>' : ''}${article.includes('data-image-expand') ? '<script type="module" src="/assets/image-viewer.js"></script>' : ''}${article.includes('data-table-filter') || article.includes('data-definition-filter') ? '<script type="module" src="/assets/table-filter.js"></script>' : ''}${article.includes('data-walkthrough') ? '<script type="module" src="/assets/walkthrough.js"></script>' : ''}${article.includes('data-cookbook-chooser') ? '<script type="module" src="/assets/cookbook-chooser.js"></script>' : ''}${article.includes('data-theme-preview') ? '<script type="module" src="/assets/theme-preview.js"></script>' : ''}${panel ? '<script type="module" src="/assets/example-panel.js"></script>' : ''}${panel.includes('data-code-samples') ? '<script type="module" src="/assets/code-samples.js"></script>' : ''}</body></html>`;
|
|
191
198
|
return basePath ? markup.replace(/((?:href|src)=")\/(assets\/|llms(?:\.min)?\.txt|apple-touch-icon\.png)/g, `$1${basePath}/$2`).replace('href="/"', `href="${basePath}/"`) : markup;
|
|
192
199
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Document, Variant } from './content-source.js';
|
|
2
|
+
export type PdfButtonConfig = {
|
|
3
|
+
mode: 'print' | 'endpoint';
|
|
4
|
+
label: string;
|
|
5
|
+
endpoint: string;
|
|
6
|
+
placement: 'before' | 'after';
|
|
7
|
+
style?: string;
|
|
8
|
+
include?: string[];
|
|
9
|
+
exclude?: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare const pdfRuntimePath = "/pdf-runtime.json";
|
|
12
|
+
export declare const pdfDataPath = "/__shoalrun/pdf-data";
|
|
13
|
+
export declare function resolvePdfButton(outputs: unknown): PdfButtonConfig | undefined;
|
|
14
|
+
export declare function validatePdfButtonStyle(button: PdfButtonConfig | undefined, exports: unknown): void;
|
|
15
|
+
export declare function pdfPageKey(id: string, publication: Variant): string;
|
|
16
|
+
export declare function pdfButtonIncludes(doc: Document, button: PdfButtonConfig): boolean;
|
|
17
|
+
export declare function pdfPageAction(doc: Document, outputs: unknown, basePath?: string): {
|
|
18
|
+
mode: 'print' | 'endpoint';
|
|
19
|
+
href?: string;
|
|
20
|
+
label: string;
|
|
21
|
+
placement: 'before' | 'after';
|
|
22
|
+
} | undefined;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { configField, isConfigMapping } from './compiler-config.js';
|
|
3
|
+
import { documentKey } from './publication-identity.js';
|
|
4
|
+
import { normalizeBasePath, sitePath } from './site-path.js';
|
|
5
|
+
export const pdfRuntimePath = '/pdf-runtime.json';
|
|
6
|
+
export const pdfDataPath = '/__shoalrun/pdf-data';
|
|
7
|
+
export function resolvePdfButton(outputs) {
|
|
8
|
+
const value = configField(configField(outputs, 'html'), 'pdf');
|
|
9
|
+
if (value === undefined || value === false)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (!isConfigMapping(value) || Object.keys(value).some(key => !['mode', 'label', 'endpoint', 'placement', 'style', 'include', 'exclude'].includes(key)))
|
|
12
|
+
throw new Error('outputs.html.pdf must be false or a mapping containing mode, label, endpoint, placement, style, include, and exclude');
|
|
13
|
+
const mode = value.mode ?? 'print';
|
|
14
|
+
if (mode !== 'print' && mode !== 'endpoint')
|
|
15
|
+
throw new Error('outputs.html.pdf.mode must be print or endpoint; site builds do not generate per-page PDFs');
|
|
16
|
+
const label = value.label ?? (mode === 'print' ? 'Print PDF' : 'Download PDF');
|
|
17
|
+
if (typeof label !== 'string' || !label.trim() || label.length > 128)
|
|
18
|
+
throw new Error('outputs.html.pdf.label must be 1–128 characters');
|
|
19
|
+
const endpoint = value.endpoint ?? '/__shoalrun/pdf';
|
|
20
|
+
if (typeof endpoint !== 'string' || !/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(endpoint))
|
|
21
|
+
throw new Error('outputs.html.pdf.endpoint must be a same-origin absolute path without query, fragment, or encoded separators');
|
|
22
|
+
if (mode === 'print' && value.endpoint !== undefined)
|
|
23
|
+
throw new Error('outputs.html.pdf.endpoint requires mode: endpoint');
|
|
24
|
+
const normalizedEndpoint = endpoint.toLowerCase();
|
|
25
|
+
if (['/mcp-runtime.json', '/__shoalrun/mcp-data', pdfRuntimePath, pdfDataPath, '/__shoalrun/ask', '/__shoalrun/search', '/__shoalrun/reload'].some(reserved => normalizedEndpoint === reserved || normalizedEndpoint.startsWith(`${reserved}/`) || reserved.startsWith(`${normalizedEndpoint}/`)))
|
|
26
|
+
throw new Error('outputs.html.pdf.endpoint overlaps a reserved runtime path');
|
|
27
|
+
if (value.style !== undefined && (typeof value.style !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value.style)))
|
|
28
|
+
throw new Error('outputs.html.pdf.style must name an export target containing PDF styling');
|
|
29
|
+
const placement = value.placement ?? 'after';
|
|
30
|
+
if (placement !== 'before' && placement !== 'after')
|
|
31
|
+
throw new Error('outputs.html.pdf.placement must be before or after');
|
|
32
|
+
for (const key of ['include', 'exclude'])
|
|
33
|
+
if (value[key] !== undefined && (!Array.isArray(value[key]) || value[key].some(id => typeof id !== 'string' || !id.trim()) || new Set(value[key]).size !== value[key].length))
|
|
34
|
+
throw new Error(`outputs.html.pdf.${key} must contain unique document IDs`);
|
|
35
|
+
return { mode, label: label.trim(), endpoint, placement, ...(value.style === undefined ? {} : { style: value.style }), ...(value.include === undefined ? {} : { include: value.include }), ...(value.exclude === undefined ? {} : { exclude: value.exclude }) };
|
|
36
|
+
}
|
|
37
|
+
export function validatePdfButtonStyle(button, exports) {
|
|
38
|
+
if (!button?.style)
|
|
39
|
+
return;
|
|
40
|
+
const target = isConfigMapping(exports) && Object.hasOwn(exports, button.style) ? exports[button.style] : undefined;
|
|
41
|
+
if (!isConfigMapping(target) || !isConfigMapping(target.pdf))
|
|
42
|
+
throw new Error(`Unknown PDF button style ${button.style}; define exports.${button.style}.pdf`);
|
|
43
|
+
}
|
|
44
|
+
export function pdfPageKey(id, publication) {
|
|
45
|
+
return createHash('sha256').update(documentKey(id, publication)).digest('hex');
|
|
46
|
+
}
|
|
47
|
+
export function pdfButtonIncludes(doc, button) {
|
|
48
|
+
return doc.frontmatter.pdf !== false && (!button.include || button.include.includes(doc.frontmatter.id)) && !button.exclude?.includes(doc.frontmatter.id);
|
|
49
|
+
}
|
|
50
|
+
export function pdfPageAction(doc, outputs, basePath = '') {
|
|
51
|
+
const button = resolvePdfButton(outputs);
|
|
52
|
+
if (!button || !pdfButtonIncludes(doc, button))
|
|
53
|
+
return undefined;
|
|
54
|
+
return { mode: button.mode, label: button.label, placement: button.placement, ...(button.mode === 'endpoint' ? { href: `${sitePath(normalizeBasePath(basePath), button.endpoint)}?key=${pdfPageKey(doc.frontmatter.id, doc.variant)}` } : {}) };
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type PdfFont = {
|
|
2
|
+
family: string;
|
|
3
|
+
source: string;
|
|
4
|
+
weight?: string;
|
|
5
|
+
style?: 'normal' | 'italic';
|
|
6
|
+
};
|
|
7
|
+
export type PdfConfig = {
|
|
8
|
+
stylesheet?: string;
|
|
9
|
+
logo?: string;
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
fonts?: PdfFont[];
|
|
12
|
+
size?: 'A4' | 'Letter' | 'Legal' | 'A3' | 'A5';
|
|
13
|
+
margins?: {
|
|
14
|
+
top?: string;
|
|
15
|
+
right?: string;
|
|
16
|
+
bottom?: string;
|
|
17
|
+
left?: string;
|
|
18
|
+
};
|
|
19
|
+
header?: string;
|
|
20
|
+
footer?: string;
|
|
21
|
+
cover?: boolean;
|
|
22
|
+
toc?: boolean;
|
|
23
|
+
bookmarks?: boolean;
|
|
24
|
+
executablePath?: string;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
};
|
|
27
|
+
export declare function validatePdfConfig(value: unknown): PdfConfig;
|
|
28
|
+
export declare function mergePdfConfig(base: unknown, override: unknown): PdfConfig;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isConfigMapping } from './compiler-config.js';
|
|
2
|
+
const localFile = (value) => typeof value === 'string' && value.trim() && !/^[a-z][a-z0-9+.-]*:|^\/\//i.test(value) && !value.includes('\0');
|
|
3
|
+
export function validatePdfConfig(value) {
|
|
4
|
+
if (value === undefined)
|
|
5
|
+
return {};
|
|
6
|
+
const keys = ['stylesheet', 'logo', 'subtitle', 'fonts', 'size', 'margins', 'header', 'footer', 'cover', 'toc', 'bookmarks', 'executablePath', 'timeout'];
|
|
7
|
+
if (!isConfigMapping(value) || Object.keys(value).some(key => !keys.includes(key)))
|
|
8
|
+
throw new Error(`PDF settings accept ${keys.join(', ')}`);
|
|
9
|
+
for (const key of ['stylesheet', 'logo', 'executablePath'])
|
|
10
|
+
if (value[key] !== undefined && !localFile(value[key]))
|
|
11
|
+
throw new Error(`PDF ${key} must be a local file path`);
|
|
12
|
+
if (value.subtitle !== undefined && (typeof value.subtitle !== 'string' || value.subtitle.length > 1000))
|
|
13
|
+
throw new Error('PDF subtitle must be text of at most 1000 characters');
|
|
14
|
+
if (value.size !== undefined && !['A4', 'Letter', 'Legal', 'A3', 'A5'].includes(value.size))
|
|
15
|
+
throw new Error('PDF size must be A4, Letter, Legal, A3, or A5');
|
|
16
|
+
if (value.margins !== undefined) {
|
|
17
|
+
if (!isConfigMapping(value.margins) || Object.keys(value.margins).some(key => !['top', 'right', 'bottom', 'left'].includes(key)))
|
|
18
|
+
throw new Error('PDF margins accept top, right, bottom, left');
|
|
19
|
+
for (const margin of Object.values(value.margins))
|
|
20
|
+
if (typeof margin !== 'string' || !/^(?:\d+(?:\.\d+)?|\.\d+)(?:mm|cm|in|px)$/.test(margin) || parseFloat(margin) > 1000)
|
|
21
|
+
throw new Error('PDF margins need nonnegative dimensions with mm, cm, in, or px units');
|
|
22
|
+
}
|
|
23
|
+
for (const key of ['cover', 'toc', 'bookmarks'])
|
|
24
|
+
if (value[key] !== undefined && typeof value[key] !== 'boolean')
|
|
25
|
+
throw new Error(`PDF ${key} must be boolean`);
|
|
26
|
+
for (const key of ['header', 'footer'])
|
|
27
|
+
if (value[key] !== undefined && (typeof value[key] !== 'string' || value[key].length > 2000 || /\{(?!title\}|page\}|pages\})[^}]*\}/.test(value[key])))
|
|
28
|
+
throw new Error(`PDF ${key} accepts text and {title}, {page}, {pages} placeholders`);
|
|
29
|
+
if (value.timeout !== undefined && (!Number.isInteger(value.timeout) || Number(value.timeout) < 1000 || Number(value.timeout) > 120000))
|
|
30
|
+
throw new Error('PDF timeout must be 1000–120000 milliseconds');
|
|
31
|
+
if (value.fonts !== undefined) {
|
|
32
|
+
if (!Array.isArray(value.fonts) || value.fonts.length > 32)
|
|
33
|
+
throw new Error('PDF fonts must be an array of at most 32 local fonts');
|
|
34
|
+
for (const font of value.fonts)
|
|
35
|
+
if (!isConfigMapping(font) || Object.keys(font).some(key => !['family', 'source', 'weight', 'style'].includes(key)) || typeof font.family !== 'string' || !font.family.trim() || font.family.length > 100 || !localFile(font.source) || (font.weight !== undefined && (typeof font.weight !== 'string' || !/^(?:normal|bold|[1-9]00)$/.test(font.weight))) || (font.style !== undefined && !['normal', 'italic'].includes(font.style)))
|
|
36
|
+
throw new Error('Each PDF font needs family and local source, with optional weight and normal/italic style');
|
|
37
|
+
}
|
|
38
|
+
return structuredClone(value);
|
|
39
|
+
}
|
|
40
|
+
export function mergePdfConfig(base, override) {
|
|
41
|
+
const first = validatePdfConfig(base), second = validatePdfConfig(override);
|
|
42
|
+
return { ...first, ...second, ...(first.margins || second.margins ? { margins: { ...first.margins, ...second.margins } } : {}) };
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type PreparedPdf } from './pdf-renderer.js';
|
|
2
|
+
import { type PdfConfig } from './pdf-config.js';
|
|
3
|
+
type Page = {
|
|
4
|
+
documentId: string;
|
|
5
|
+
publication: {
|
|
6
|
+
product?: string;
|
|
7
|
+
channel: string;
|
|
8
|
+
version: string;
|
|
9
|
+
language: string;
|
|
10
|
+
};
|
|
11
|
+
title: string;
|
|
12
|
+
file: string;
|
|
13
|
+
digest: string;
|
|
14
|
+
};
|
|
15
|
+
export type PdfRuntime = {
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
endpoint: string;
|
|
18
|
+
config: PdfConfig;
|
|
19
|
+
pages: Record<string, Page>;
|
|
20
|
+
inventory: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
export type LoadedPdfRuntime = {
|
|
23
|
+
runtime: PdfRuntime;
|
|
24
|
+
directory: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function loadPdfRuntime(directory: string): Promise<LoadedPdfRuntime | undefined>;
|
|
27
|
+
/** Mount behind the same authentication as the built site. This handler does not grant access to other profiles. */
|
|
28
|
+
export declare function createPdfHttpHandler(input: LoadedPdfRuntime, options?: {
|
|
29
|
+
authorize?: (request: Request) => boolean | Promise<boolean>;
|
|
30
|
+
render?: (prepared: PreparedPdf, config: PdfConfig) => Promise<{
|
|
31
|
+
bytes: Buffer | Uint8Array;
|
|
32
|
+
}>;
|
|
33
|
+
now?: () => number;
|
|
34
|
+
}): (request: Request) => Promise<Response>;
|
|
35
|
+
export declare function hydratePreparedPdf(value: unknown): PreparedPdf;
|
|
36
|
+
export {};
|