@writedocs/generator 0.4.9 → 0.4.10
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/astro.config.mjs +39 -2
- package/bin/writedocs.js +23 -0
- package/package.json +1 -1
- package/src/cli/convert.js +82 -0
- package/src/cli/generate-api-pages.js +56 -3
- package/src/components/Accordion.astro +2 -1
- package/src/components/AccordionGroup.astro +4 -1
- package/src/components/ApiPlayground.astro +6 -2
- package/src/components/ApiReferencePanel.astro +6 -2
- package/src/components/Badge.astro +2 -0
- package/src/components/Callout.astro +2 -1
- package/src/components/Card.astro +2 -1
- package/src/components/CardGroup.astro +2 -1
- package/src/components/Check.astro +1 -1
- package/src/components/CodeBlock.astro +94 -0
- package/src/components/CodeGroup.astro +2 -1
- package/src/components/Color.astro +2 -1
- package/src/components/ColorItem.astro +2 -1
- package/src/components/ColorRow.astro +2 -1
- package/src/components/Column.astro +19 -0
- package/src/components/Columns.astro +1 -1
- package/src/components/Danger.astro +1 -1
- package/src/components/Expandable.astro +2 -1
- package/src/components/Frame.astro +2 -1
- package/src/components/GitHubRepo.astro +2 -1
- package/src/components/Hint.astro +2 -1
- package/src/components/Icon.astro +3 -2
- package/src/components/Image.astro +2 -1
- package/src/components/Info.astro +1 -1
- package/src/components/Note.astro +1 -1
- package/src/components/Panel.astro +2 -1
- package/src/components/Parameter.astro +2 -1
- package/src/components/Prompt.astro +2 -1
- package/src/components/RequestExample.astro +2 -1
- package/src/components/ResponseExample.astro +2 -1
- package/src/components/Searchbar.astro +2 -1
- package/src/components/Step.astro +2 -1
- package/src/components/Steps.astro +4 -1
- package/src/components/Tab.astro +2 -1
- package/src/components/Tabs.astro +2 -1
- package/src/components/Tile.astro +2 -1
- package/src/components/Tip.astro +1 -1
- package/src/components/TreeFile.astro +2 -1
- package/src/components/TreeFolder.astro +2 -1
- package/src/components/Update.astro +2 -1
- package/src/components/Video.astro +2 -1
- package/src/components/View.astro +2 -1
- package/src/components/Warning.astro +1 -1
- package/src/components/class-names.ts +8 -0
- package/src/components/index.ts +2 -0
- package/src/content.config.ts +23 -2
- package/src/lib/content-check.js +36 -6
- package/src/lib/mdx-auto-hydrate.js +12 -0
- package/src/lib/mdx-inject-builtins.js +15 -0
- package/src/lib/mdx-inline-react.js +202 -0
- package/src/lib/mdx-mintlify.js +65 -0
- package/src/lib/mdx-substitute-variables.js +17 -0
- package/src/lib/mdx-unknown-components.js +56 -3
- package/src/lib/mintlify-convert.js +599 -0
- package/src/lib/openapi-ref.js +44 -0
- package/src/lib/openapi-render.ts +10 -1
- package/src/lib/pages.js +89 -17
- package/src/pages/[...slug].astro +8 -2
package/astro.config.mjs
CHANGED
|
@@ -26,8 +26,14 @@ import { remarkAutoHydrateSnippets } from './src/lib/mdx-auto-hydrate.js';
|
|
|
26
26
|
import { remarkInjectBuiltinComponents } from './src/lib/mdx-inject-builtins.js';
|
|
27
27
|
import { remarkTitleAnchorIds } from './src/lib/mdx-title-anchor-ids.js';
|
|
28
28
|
import { remarkSubstituteVariables } from './src/lib/mdx-substitute-variables.js';
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
remarkMintlifyTreeLists,
|
|
31
|
+
remarkMintlifyPromptText,
|
|
32
|
+
remarkMintlifyReactHooks,
|
|
33
|
+
mintlifyReactHooksPlugin,
|
|
34
|
+
} from './src/lib/mdx-mintlify.js';
|
|
30
35
|
import { remarkUnknownComponentFallback } from './src/lib/mdx-unknown-components.js';
|
|
36
|
+
import { remarkExtractInlineReactComponents } from './src/lib/mdx-inline-react.js';
|
|
31
37
|
import { writedocsTempDir, writedocsBuildStagingDir } from './src/lib/writedocs-temp-dir.js';
|
|
32
38
|
import { stylesAssetFallback } from './src/lib/styles-asset-integration.js';
|
|
33
39
|
import {
|
|
@@ -47,6 +53,13 @@ const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
|
47
53
|
// does the actual cross-filesystem-safe copy into contentDir/dist as an
|
|
48
54
|
// explicit final step once the build itself is done.
|
|
49
55
|
const packageRoot = process.env.WRITEDOCS_PACKAGE_ROOT || path.dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
// Where this package's dependencies are: the node_modules folder that
|
|
57
|
+
// contains an installed package (<project>/node_modules/@writedocs/
|
|
58
|
+
// generator -> <project>/node_modules), or packageRoot itself in a checkout,
|
|
59
|
+
// whose own node_modules is inside it. Used for the dev server's file-serving
|
|
60
|
+
// allow list below.
|
|
61
|
+
const nodeModulesAt = packageRoot.lastIndexOf(`${path.sep}node_modules${path.sep}`);
|
|
62
|
+
const dependencyRoot = nodeModulesAt === -1 ? packageRoot : packageRoot.slice(0, nodeModulesAt + `${path.sep}node_modules`.length);
|
|
50
63
|
|
|
51
64
|
// Read here (rather than deferred to page-render time, where
|
|
52
65
|
// loadDocsConfig() is also called from [...slug].astro) specifically so
|
|
@@ -371,6 +384,14 @@ export default defineConfig({
|
|
|
371
384
|
// the tree when it looks for components to import.
|
|
372
385
|
remarkMintlifyTreeLists,
|
|
373
386
|
remarkMintlifyPromptText,
|
|
387
|
+
// An inline component that calls React hooks moves into a generated
|
|
388
|
+
// .jsx file so it runs as real React (and hydrates) - see
|
|
389
|
+
// lib/mdx-inline-react.js. Before remarkMintlifyReactHooks, which
|
|
390
|
+
// imports any hooks the page itself still calls (Mintlify
|
|
391
|
+
// pre-injects them), and before remarkAutoHydrateSnippets, which
|
|
392
|
+
// hydrates the component through its new .jsx import.
|
|
393
|
+
remarkExtractInlineReactComponents,
|
|
394
|
+
remarkMintlifyReactHooks,
|
|
374
395
|
// An unknown component becomes a fragment (its children still
|
|
375
396
|
// render) with a warning, instead of failing the whole build - see
|
|
376
397
|
// lib/mdx-unknown-components.js. After remarkMintlifyTreeLists, so
|
|
@@ -441,10 +462,26 @@ export default defineConfig({
|
|
|
441
462
|
// working. See src/styles/global.css for the one @import that wires
|
|
442
463
|
// Tailwind's utilities in.
|
|
443
464
|
vite: {
|
|
465
|
+
// The dev server only serves files under its workspace root by default.
|
|
466
|
+
// A site's own snippets live in its content directory, and inline React
|
|
467
|
+
// components moved out of pages (lib/mdx-inline-react.js) in writedocs'
|
|
468
|
+
// temp directory - both need to reach the browser to hydrate. Setting
|
|
469
|
+
// `allow` replaces Vite's default rather than adding to it, so the
|
|
470
|
+
// package's dependencies must stay reachable too: when writedocs is
|
|
471
|
+
// installed, they sit next to it in the node_modules folder that
|
|
472
|
+
// contains it (React's own hydration client included), not inside
|
|
473
|
+
// packageRoot - see dependencyRoot above.
|
|
474
|
+
server: {
|
|
475
|
+
fs: {
|
|
476
|
+
allow: [packageRoot, dependencyRoot, contentDir, writedocsTempDir(contentDir)],
|
|
477
|
+
},
|
|
478
|
+
},
|
|
444
479
|
// contentTailwindSource() must come before tailwindcss(): both are
|
|
445
480
|
// enforce: 'pre' transforms, which Vite runs in array order, and
|
|
446
481
|
// Tailwind has to see the added @source when it compiles global.css.
|
|
447
|
-
|
|
482
|
+
// mintlifyReactHooksPlugin() does for a site's .jsx/.tsx snippets what
|
|
483
|
+
// remarkMintlifyReactHooks does for its MDX pages.
|
|
484
|
+
plugins: [contentTailwindSource(), tailwindcss(), mintlifyReactHooksPlugin(contentDir)],
|
|
448
485
|
resolve: {
|
|
449
486
|
alias: [
|
|
450
487
|
// Lets a page's MDX write `import Foo from '/snippets/foo.mdx'`
|
package/bin/writedocs.js
CHANGED
|
@@ -153,6 +153,29 @@ program
|
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
program
|
|
157
|
+
.command('convert')
|
|
158
|
+
.description("Convert another docs tool's config into writedocs.json")
|
|
159
|
+
.argument('[dir]', 'project directory (contains docs.json)', '.')
|
|
160
|
+
.option('--mintlify', "convert a Mintlify project's docs.json")
|
|
161
|
+
.option('--docs.json', 'same as --mintlify')
|
|
162
|
+
.option('--force', 'overwrite an existing writedocs.json')
|
|
163
|
+
.option('--dry-run', 'print the converted writedocs.json instead of writing it')
|
|
164
|
+
.action(async (dir, options) => {
|
|
165
|
+
// --mintlify is the only source today; the flag is still required so
|
|
166
|
+
// the command reads the same once other tools are added.
|
|
167
|
+
if (!options.mintlify && !options.docsJson) {
|
|
168
|
+
console.error('[writedocs] Say what to convert from: writedocs convert --mintlify [dir]');
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const { runConvert } = await import('../src/cli/convert.js');
|
|
172
|
+
await runConvert({
|
|
173
|
+
contentDir: path.resolve(process.cwd(), dir),
|
|
174
|
+
force: Boolean(options.force),
|
|
175
|
+
dryRun: Boolean(options.dryRun),
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
156
179
|
program
|
|
157
180
|
.command('init')
|
|
158
181
|
.description('Scaffold a writedocs.json and starter docs/ folder')
|
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// `writedocs convert --mintlify [dir]` - turns a Mintlify project's
|
|
2
|
+
// docs.json into writedocs.json, in the same folder, then checks the pages
|
|
3
|
+
// the same way `writedocs validate` does. The conversion itself is
|
|
4
|
+
// lib/mintlify-convert.js; this file is the CLI around it.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { loadMintlifyConfig, convertMintlifyConfig, formatNotes } from '../lib/mintlify-convert.js';
|
|
8
|
+
import { validateDocsConfig, formatValidationIssuesDetailed } from '../lib/config-schema.js';
|
|
9
|
+
import { checkContent, formatContentIssues } from '../lib/content-check.js';
|
|
10
|
+
|
|
11
|
+
export async function runConvert({ contentDir, force = false, dryRun = false }) {
|
|
12
|
+
const docsJsonPath = path.join(contentDir, 'docs.json');
|
|
13
|
+
if (!fs.existsSync(docsJsonPath)) {
|
|
14
|
+
const legacy = path.join(contentDir, 'mint.json');
|
|
15
|
+
if (fs.existsSync(legacy)) {
|
|
16
|
+
console.error(
|
|
17
|
+
`[writedocs] Found mint.json, Mintlify's older config format. Run \`npx mint upgrade\` in ${contentDir} to turn it into docs.json, then run this again.`
|
|
18
|
+
);
|
|
19
|
+
} else {
|
|
20
|
+
console.error(`[writedocs] No docs.json found in ${contentDir}.`);
|
|
21
|
+
}
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const outPath = path.join(contentDir, 'writedocs.json');
|
|
25
|
+
if (!dryRun && !force && fs.existsSync(outPath)) {
|
|
26
|
+
console.error(`[writedocs] ${outPath} already exists. Pass --force to overwrite it, or --dry-run to only see the result.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let docs;
|
|
31
|
+
try {
|
|
32
|
+
docs = loadMintlifyConfig(docsJsonPath);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`[writedocs] Couldn't read ${docsJsonPath}: ${err.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { config, notes } = convertMintlifyConfig(docs);
|
|
39
|
+
const text = `${JSON.stringify(config, null, 2)}\n`;
|
|
40
|
+
|
|
41
|
+
// The result must pass writedocs' own schema - if it doesn't, that's a
|
|
42
|
+
// converter bug, and writing it would only hand the author a broken file.
|
|
43
|
+
const result = validateDocsConfig(text);
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
console.error('[writedocs] The converted writedocs.json is not valid - this is a bug in the converter, please report it:\n');
|
|
46
|
+
console.error(formatValidationIssuesDetailed(result.issues));
|
|
47
|
+
console.error(`\n${text}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (dryRun) {
|
|
52
|
+
console.log(text);
|
|
53
|
+
} else {
|
|
54
|
+
fs.writeFileSync(outPath, text);
|
|
55
|
+
console.log(`[writedocs] Converted ${path.basename(docsJsonPath)} -> ${outPath}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (notes.length) {
|
|
59
|
+
console.log(`\n[writedocs] ${notes.length} thing${notes.length === 1 ? '' : 's'} couldn't be carried over as-is:\n`);
|
|
60
|
+
console.log(formatNotes(notes));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The pages, checked against the converted config - what's left to fix
|
|
64
|
+
// before the first build.
|
|
65
|
+
const content = await checkContent(contentDir, text);
|
|
66
|
+
if (content.errors.length) {
|
|
67
|
+
console.log(`\n[writedocs] ${content.errors.length} error${content.errors.length === 1 ? '' : 's'} in the pages - the build would fail on these:\n`);
|
|
68
|
+
console.log(formatContentIssues(content.errors));
|
|
69
|
+
}
|
|
70
|
+
if (content.warnings.length) {
|
|
71
|
+
console.log(`\n[writedocs] ${content.warnings.length} warning${content.warnings.length === 1 ? '' : 's'} in the pages:\n`);
|
|
72
|
+
console.log(formatContentIssues(content.warnings));
|
|
73
|
+
}
|
|
74
|
+
console.log(
|
|
75
|
+
`\n[writedocs] Checked ${content.pages} page${content.pages === 1 ? '' : 's'}: ${content.errors.length} error${content.errors.length === 1 ? '' : 's'}, ${content.warnings.length} warning${content.warnings.length === 1 ? '' : 's'}.`
|
|
76
|
+
);
|
|
77
|
+
if (!config.domain) {
|
|
78
|
+
console.log(
|
|
79
|
+
'[writedocs] Next: set "domain" in writedocs.json to your site\'s URL - it turns on sitemap.xml and absolute links for social previews. (Mintlify sets this in its dashboard, not docs.json.)'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -3,6 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import { writedocsTempDir } from '../lib/writedocs-temp-dir.js';
|
|
6
|
+
import { parseOpenApiRef, openApiOperationKey, specDirName } from '../lib/openapi-ref.js';
|
|
7
|
+
import { findAllPages } from '../lib/pages.js';
|
|
6
8
|
|
|
7
9
|
const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
8
10
|
|
|
@@ -99,10 +101,17 @@ function scanDirForOverrides(dir, relBase, overrides) {
|
|
|
99
101
|
}
|
|
100
102
|
if (!/\.mdx?$/i.test(entry.name)) continue;
|
|
101
103
|
const raw = fs.readFileSync(full, 'utf-8');
|
|
102
|
-
|
|
104
|
+
let data;
|
|
105
|
+
try {
|
|
106
|
+
({ data } = matter(raw));
|
|
107
|
+
} catch {
|
|
108
|
+
continue; // invalid frontmatter - `writedocs validate` and the build report it
|
|
109
|
+
}
|
|
103
110
|
if (typeof data.openapi !== 'string') continue;
|
|
104
111
|
const fileId = rel.replace(/\.mdx?$/i, '').replace(/\/index$/, '');
|
|
105
|
-
|
|
112
|
+
// Keyed by "METHOD /path" whichever form the page wrote - Mintlify's
|
|
113
|
+
// "spec.json METHOD /path" included (see lib/openapi-ref.js).
|
|
114
|
+
overrides.set(openApiOperationKey(data.openapi) ?? data.openapi.trim().replace(/\s+/g, ' '), fileId);
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
|
|
@@ -337,7 +346,6 @@ export async function generateApiPages({ contentDir }) {
|
|
|
337
346
|
rmrf(openapiOutDir);
|
|
338
347
|
|
|
339
348
|
const groups = collectOpenApiGroups(config.navigation);
|
|
340
|
-
if (groups.length === 0) return;
|
|
341
349
|
|
|
342
350
|
const seenPaths = new Map(); // normalized path -> owning group's label
|
|
343
351
|
for (const group of groups) {
|
|
@@ -356,4 +364,49 @@ export async function generateApiPages({ contentDir }) {
|
|
|
356
364
|
for (const group of groups) {
|
|
357
365
|
await generateApiPagesForGroup({ contentDir, generatedDocsDir, group, overrides });
|
|
358
366
|
}
|
|
367
|
+
|
|
368
|
+
await parsePageSpecs({ contentDir, openapiOutDir, groupSpecs: groups.map((g) => path.resolve(contentDir, g.openapi.src)) });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Mintlify's page-level form, `openapi: "/spec.json METHOD /path"`: the
|
|
372
|
+
* page names its spec itself instead of belonging to an openapi group in
|
|
373
|
+
* writedocs.json. Every such spec (that no group already parsed) is parsed
|
|
374
|
+
* here and its operations written under openapi/_pages/<spec>/operations/,
|
|
375
|
+
* where findOperationFile() (lib/openapi-render.ts) looks for them. No
|
|
376
|
+
* pages are generated - the pages that name the spec are the pages. A
|
|
377
|
+
* spec that's missing or doesn't parse is a warning, not a build failure:
|
|
378
|
+
* its pages show the playground's own "no operation found" notice. */
|
|
379
|
+
async function parsePageSpecs({ contentDir, openapiOutDir, groupSpecs }) {
|
|
380
|
+
const specs = new Set();
|
|
381
|
+
for (const rel of findAllPages(contentDir)) {
|
|
382
|
+
let data;
|
|
383
|
+
try {
|
|
384
|
+
({ data } = matter(fs.readFileSync(path.join(contentDir, rel), 'utf-8')));
|
|
385
|
+
} catch {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const ref = parseOpenApiRef(data?.openapi);
|
|
389
|
+
if (ref?.spec) specs.add(ref.spec);
|
|
390
|
+
}
|
|
391
|
+
for (const spec of specs) {
|
|
392
|
+
const specPath = path.resolve(contentDir, spec);
|
|
393
|
+
if (groupSpecs.includes(specPath)) continue;
|
|
394
|
+
if (!fs.existsSync(specPath)) {
|
|
395
|
+
console.warn(`[writedocs] Pages name the OpenAPI spec "${spec}", which doesn't exist - their API playground shows a notice instead.`);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
let operations;
|
|
399
|
+
try {
|
|
400
|
+
operations = buildOperations(await SwaggerParser.dereference(specPath));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.warn(`[writedocs] Couldn't parse the OpenAPI spec "${spec}" that pages name: ${err.message}`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const outDir = path.join(openapiOutDir, '_pages', specDirName(spec), 'operations');
|
|
406
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
407
|
+
for (const operation of operations) {
|
|
408
|
+
fs.writeFileSync(path.join(outDir, operationFileName(operation.method, operation.path)), JSON.stringify(operation, null, 2));
|
|
409
|
+
}
|
|
410
|
+
console.log(`[writedocs] Parsed OpenAPI spec "${spec}" for the pages that name it (${operations.length} operations)`);
|
|
411
|
+
}
|
|
359
412
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
import AppIcon from "./AppIcon.astro";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
@@ -32,7 +33,7 @@ function slugify(value: string): string {
|
|
|
32
33
|
const titleId = _titleId ?? slugify(title);
|
|
33
34
|
---
|
|
34
35
|
|
|
35
|
-
<details class="wd-accordion" open={defaultOpen}>
|
|
36
|
+
<details class:list={["wd-accordion", extraClasses(Astro.props)]} open={defaultOpen}>
|
|
36
37
|
<summary>
|
|
37
38
|
<span class="wd-accordion-heading">
|
|
38
39
|
{icon && <AppIcon icon={icon} class="wd-accordion-icon" />}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
findOperationFile,
|
|
13
13
|
type OpenApiOperation,
|
|
14
14
|
} from '../lib/openapi-render';
|
|
15
|
+
import { parseOpenApiRef } from '../lib/openapi-ref.js';
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
17
18
|
operation: string; // "METHOD /path", matching a page's `openapi` frontmatter
|
|
@@ -19,8 +20,11 @@ interface Props {
|
|
|
19
20
|
}
|
|
20
21
|
const { operation, contentDir } = Astro.props as Props;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
const
|
|
23
|
+
// "METHOD /path", or Mintlify's "spec.json METHOD /path" - see lib/openapi-ref.js.
|
|
24
|
+
const ref = parseOpenApiRef(operation);
|
|
25
|
+
const method = ref?.method ?? '';
|
|
26
|
+
const urlPath = ref?.path ?? '';
|
|
27
|
+
const opFile = ref ? findOperationFile(contentDir, method, urlPath, ref.spec) : null;
|
|
24
28
|
const op: OpenApiOperation | null = opFile ? JSON.parse(fs.readFileSync(opFile, 'utf-8')) : null;
|
|
25
29
|
|
|
26
30
|
const pathParams = op?.parameters.filter((p) => p.in === 'path') ?? [];
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
findOperationFile,
|
|
16
16
|
type OpenApiOperation,
|
|
17
17
|
} from '../lib/openapi-render';
|
|
18
|
+
import { parseOpenApiRef } from '../lib/openapi-ref.js';
|
|
18
19
|
import { loadDocsConfig, resolveCodeblockTheme } from '../lib/config';
|
|
19
20
|
import ApiLangSelect from './ApiLangSelect.astro';
|
|
20
21
|
|
|
@@ -64,8 +65,11 @@ const SNIPPET_THEMES = resolveCodeblockTheme(docsConfig);
|
|
|
64
65
|
// writedocs' CORS proxy - see PROXY_BASE_URL in the <script> below.
|
|
65
66
|
const proxyEnabled = docsConfig.api.proxy;
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
const
|
|
68
|
+
// "METHOD /path", or Mintlify's "spec.json METHOD /path" - see lib/openapi-ref.js.
|
|
69
|
+
const ref = parseOpenApiRef(operation);
|
|
70
|
+
const method = ref?.method ?? '';
|
|
71
|
+
const urlPath = ref?.path ?? '';
|
|
72
|
+
const opFile = ref ? findOperationFile(contentDir, method, urlPath, ref.spec) : null;
|
|
69
73
|
const op: OpenApiOperation | null = opFile ? JSON.parse(fs.readFileSync(opFile, 'utf-8')) : null;
|
|
70
74
|
|
|
71
75
|
// ApiPlayground.astro (rendered in the article column) already shows a
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
// A small inline label - status indicators, version tags, "Beta"/"New"
|
|
3
4
|
// markers - modeled on Mintlify's own Badge
|
|
4
5
|
// (https://www.mintlify.com/docs/components/badge). Renders as an
|
|
@@ -42,6 +43,7 @@ const shapeClass = shape === 'pill' ? 'pill' : 'rounded';
|
|
|
42
43
|
`wd-badge-${shapeClass}`,
|
|
43
44
|
stroke && 'wd-badge-stroke',
|
|
44
45
|
disabled && 'wd-badge-disabled',
|
|
46
|
+
extraClasses(Astro.props),
|
|
45
47
|
]}
|
|
46
48
|
>
|
|
47
49
|
{icon && <AppIcon icon={icon} class="wd-badge-icon" />}<slot />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
// See src/components/{Note,Tip,Warning,Danger,Info,Check}.astro - one thin
|
|
3
4
|
// wrapper per `type` below, each just `<Callout type="x">` under the
|
|
4
5
|
// hood, so a page can write either `<Callout type="info">` or the
|
|
@@ -51,7 +52,7 @@ function slugify(value: string): string {
|
|
|
51
52
|
const titleId = title ? (_titleId ?? slugify(title)) : undefined;
|
|
52
53
|
---
|
|
53
54
|
|
|
54
|
-
<div class:list={["wd-callout", `wd-callout-${type}`, { "wd-callout-titled": Boolean(title) }]} style={accentStyle}>
|
|
55
|
+
<div class:list={["wd-callout", `wd-callout-${type}`, { "wd-callout-titled": Boolean(title) }, extraClasses(Astro.props)]} style={accentStyle}>
|
|
55
56
|
<AppIcon icon={icon ?? icons[type]} class="wd-callout-icon" />
|
|
56
57
|
<div class="wd-callout-content">
|
|
57
58
|
{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
import AppIcon from './AppIcon.astro';
|
|
3
4
|
|
|
4
5
|
// Three visual variants, chosen by which optional prop is set - never more
|
|
@@ -42,7 +43,7 @@ const KNOWN_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
|
|
|
42
43
|
const methodLower = method?.toLowerCase();
|
|
43
44
|
const methodClass = methodLower && KNOWN_METHODS.includes(methodLower) ? methodLower : 'other';
|
|
44
45
|
---
|
|
45
|
-
<Tag class:list={['wd-card', { 'wd-card-horizontal': horizontal, 'wd-card-has-arrow': arrow && href }]} href={href}>
|
|
46
|
+
<Tag class:list={['wd-card', { 'wd-card-horizontal': horizontal, 'wd-card-has-arrow': arrow && href }, extraClasses(Astro.props)]} href={href}>
|
|
46
47
|
{img && <img src={img} alt="" class="wd-card-image" />}
|
|
47
48
|
{!img && <AppIcon icon={icon} class="wd-card-icon" style={iconStyle} />}
|
|
48
49
|
<div class="wd-card-text">
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
interface Props {
|
|
3
4
|
cols?: number;
|
|
4
5
|
}
|
|
5
6
|
const { cols = 2 } = Astro.props as Props;
|
|
6
7
|
---
|
|
7
|
-
<div class="wd-card-group" style={`--wd-cols: ${cols}`}>
|
|
8
|
+
<div class:list={["wd-card-group", extraClasses(Astro.props)]} style={`--wd-cols: ${cols}`}>
|
|
8
9
|
<slot />
|
|
9
10
|
</div>
|
|
10
11
|
<style>
|
|
@@ -10,4 +10,4 @@ interface Props {
|
|
|
10
10
|
}
|
|
11
11
|
const { title, _titleId } = Astro.props as Props;
|
|
12
12
|
---
|
|
13
|
-
<Callout type="check" title={title} _titleId={_titleId}><slot /></Callout>
|
|
13
|
+
<Callout type="check" title={title} _titleId={_titleId} class={Astro.props.class} className={Astro.props.className}><slot /></Callout>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Mintlify's <CodeBlock> - a code block from props instead of a ``` fence,
|
|
3
|
+
// for code a page builds up in a component. Renders through the same Shiki
|
|
4
|
+
// setup and the same writedocs:code-block transformer as a fenced block
|
|
5
|
+
// (see astro.config.mjs's markdown.shikiConfig), so title, icon, line
|
|
6
|
+
// numbers, wrap, copy button, expandable, highlight and focus all look and
|
|
7
|
+
// behave the same.
|
|
8
|
+
//
|
|
9
|
+
// The code is the component's children (or a `code` prop). Mintlify's
|
|
10
|
+
// `highlight`/`focus` are stringified arrays ("[1,3,4]"); ranges like
|
|
11
|
+
// "1-3" work too.
|
|
12
|
+
import { Code } from 'astro:components';
|
|
13
|
+
import { extraClasses } from './class-names';
|
|
14
|
+
import {
|
|
15
|
+
transformerMetaHighlight,
|
|
16
|
+
transformerMetaWordHighlight,
|
|
17
|
+
transformerNotationHighlight,
|
|
18
|
+
transformerNotationWordHighlight,
|
|
19
|
+
transformerNotationFocus,
|
|
20
|
+
transformerNotationDiff,
|
|
21
|
+
transformerNotationErrorLevel,
|
|
22
|
+
} from '@shikijs/transformers';
|
|
23
|
+
import { codeBlockTransformer } from '../lib/shiki-code-block.js';
|
|
24
|
+
import { loadDocsConfig, resolveCodeblockTheme } from '../lib/config';
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
language?: string;
|
|
28
|
+
filename?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
lines?: boolean;
|
|
31
|
+
wrap?: boolean;
|
|
32
|
+
nocopy?: boolean;
|
|
33
|
+
expandable?: boolean;
|
|
34
|
+
highlight?: string | number[];
|
|
35
|
+
focus?: string | number[];
|
|
36
|
+
code?: string;
|
|
37
|
+
}
|
|
38
|
+
const { language = 'text', filename, icon, lines, wrap, nocopy, expandable, highlight, focus, code: codeProp } = Astro.props as Props;
|
|
39
|
+
|
|
40
|
+
// Slot content arrives as rendered HTML text - undo its entity escaping.
|
|
41
|
+
function unescape(html: string): string {
|
|
42
|
+
return html
|
|
43
|
+
.replace(/<[^>]+>/g, '')
|
|
44
|
+
.replace(/</g, '<')
|
|
45
|
+
.replace(/>/g, '>')
|
|
46
|
+
.replace(/"/g, '"')
|
|
47
|
+
.replace(/'/g, "'")
|
|
48
|
+
.replace(/'/g, "'")
|
|
49
|
+
.replace(/&/g, '&');
|
|
50
|
+
}
|
|
51
|
+
const code = (codeProp ?? unescape(await Astro.slots.render('default'))).replace(/^\n+|\s+$/g, '');
|
|
52
|
+
|
|
53
|
+
const ranges = (value: string | number[] | undefined) =>
|
|
54
|
+
value === undefined ? '' : (Array.isArray(value) ? value.join(',') : String(value).replace(/[\[\]\s]/g, ''));
|
|
55
|
+
const quoted = (value: string) => `"${value.replace(/"/g, "'")}"`;
|
|
56
|
+
const meta = [
|
|
57
|
+
filename ? `title=${quoted(filename)}` : '',
|
|
58
|
+
icon ? `icon=${quoted(icon)}` : '',
|
|
59
|
+
lines ? 'lines' : '',
|
|
60
|
+
wrap ? 'wrap' : '',
|
|
61
|
+
nocopy ? 'nocopy' : '',
|
|
62
|
+
expandable ? 'expandable' : '',
|
|
63
|
+
ranges(highlight) ? `highlight={${ranges(highlight)}}` : '',
|
|
64
|
+
ranges(focus) ? `focus={${ranges(focus)}}` : '',
|
|
65
|
+
]
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join(' ');
|
|
68
|
+
|
|
69
|
+
const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
70
|
+
const themes = resolveCodeblockTheme(loadDocsConfig(contentDir));
|
|
71
|
+
const transformers = [
|
|
72
|
+
transformerMetaHighlight(),
|
|
73
|
+
transformerMetaWordHighlight(),
|
|
74
|
+
transformerNotationHighlight(),
|
|
75
|
+
transformerNotationWordHighlight(),
|
|
76
|
+
transformerNotationFocus(),
|
|
77
|
+
transformerNotationDiff(),
|
|
78
|
+
transformerNotationErrorLevel(),
|
|
79
|
+
codeBlockTransformer(),
|
|
80
|
+
];
|
|
81
|
+
const extras = extraClasses(Astro.props);
|
|
82
|
+
---
|
|
83
|
+
{
|
|
84
|
+
// The block's own root (.wd-code-block) is built by the Shiki transformer,
|
|
85
|
+
// so `className`/`class` goes on a wrapper around it - only when given, so
|
|
86
|
+
// a plain CodeBlock renders exactly like a fenced block.
|
|
87
|
+
extras.length > 0 ? (
|
|
88
|
+
<div class:list={extras}>
|
|
89
|
+
<Code code={code} lang={language as any} meta={meta} themes={themes} transformers={transformers} />
|
|
90
|
+
</div>
|
|
91
|
+
) : (
|
|
92
|
+
<Code code={code} lang={language as any} meta={meta} themes={themes} transformers={transformers} />
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
// `dropdown` is only ever passed down from RequestExample/
|
|
3
4
|
// ResponseExample.astro today (CodeGroup itself is never used with it
|
|
4
5
|
// directly in any fixture/doc) - kept as a real prop rather than a
|
|
@@ -9,7 +10,7 @@ interface Props {
|
|
|
9
10
|
}
|
|
10
11
|
const { dropdown = false } = Astro.props as Props;
|
|
11
12
|
---
|
|
12
|
-
<div class="wd-codegroup" data-dropdown={dropdown ? "true" : undefined}>
|
|
13
|
+
<div class:list={["wd-codegroup", extraClasses(Astro.props)]} data-dropdown={dropdown ? "true" : undefined}>
|
|
13
14
|
<slot />
|
|
14
15
|
</div>
|
|
15
16
|
<script>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
// Mintlify's <Color> - a palette of <Color.Item> swatches. `variant`:
|
|
3
4
|
// compact (default) - one grid of swatches.
|
|
4
5
|
// table - <Color.Row title="..."> rows, each a titled line of
|
|
@@ -10,7 +11,7 @@ interface Props {
|
|
|
10
11
|
}
|
|
11
12
|
const { variant = 'compact' } = Astro.props as Props;
|
|
12
13
|
---
|
|
13
|
-
<div class:list={['wd-color', `wd-color-${variant}
|
|
14
|
+
<div class:list={['wd-color', `wd-color-${variant}`, extraClasses(Astro.props)]}><slot /></div>
|
|
14
15
|
<script>
|
|
15
16
|
// Click (or Enter/Space) on a swatch copies the value currently shown -
|
|
16
17
|
// for a light/dark pair, the one matching the site's theme right now.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
// Mintlify's <Color.Item name="..." value="..."> - one swatch. `value` is
|
|
3
4
|
// any CSS color, or { light, dark } for a theme-aware pair: the swatch and
|
|
4
5
|
// the value text switch with the site's theme toggle ([data-theme] on
|
|
@@ -15,7 +16,7 @@ const themed = typeof value === 'object' && value !== null && light !== dark;
|
|
|
15
16
|
const style = light ? `--wd-swatch-light: ${light}; --wd-swatch-dark: ${dark ?? light}` : undefined;
|
|
16
17
|
---
|
|
17
18
|
<div
|
|
18
|
-
class=
|
|
19
|
+
class:list={['wd-color-item', extraClasses(Astro.props)]}
|
|
19
20
|
data-light={light}
|
|
20
21
|
data-dark={dark}
|
|
21
22
|
role={light ? 'button' : undefined}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
// Mintlify's <Color.Row title="..."> - one titled row inside
|
|
3
4
|
// <Color variant="table">. `title` takes inline Markdown.
|
|
4
5
|
import { inlineMarkdown } from '../lib/inline-markdown.js';
|
|
@@ -7,7 +8,7 @@ interface Props {
|
|
|
7
8
|
}
|
|
8
9
|
const { title } = Astro.props as Props;
|
|
9
10
|
---
|
|
10
|
-
<div class=
|
|
11
|
+
<div class:list={['wd-color-row', extraClasses(Astro.props)]}>
|
|
11
12
|
<div class="wd-color-row-title" set:html={inlineMarkdown(title ?? '')} />
|
|
12
13
|
<div class="wd-color-row-items"><slot /></div>
|
|
13
14
|
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
3
|
+
// Mintlify's <Column> - one cell of a <Columns> grid, for arbitrary content
|
|
4
|
+
// (text, a code block) side by side rather than Cards. Just a grid item:
|
|
5
|
+
// <Columns>/CardGroup.astro lays its children out, this only keeps the
|
|
6
|
+
// cell's content together and trims the outer margins of what's inside.
|
|
7
|
+
---
|
|
8
|
+
<div class:list={['wd-column', extraClasses(Astro.props)]}><slot /></div>
|
|
9
|
+
<style>
|
|
10
|
+
.wd-column {
|
|
11
|
+
min-width: 0;
|
|
12
|
+
}
|
|
13
|
+
.wd-column > :global(:first-child) {
|
|
14
|
+
margin-top: 0;
|
|
15
|
+
}
|
|
16
|
+
.wd-column > :global(:last-child) {
|
|
17
|
+
margin-bottom: 0;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
@@ -9,4 +9,4 @@ interface Props {
|
|
|
9
9
|
}
|
|
10
10
|
const { title, _titleId } = Astro.props as Props;
|
|
11
11
|
---
|
|
12
|
-
<Callout type="danger" title={title} _titleId={_titleId}><slot /></Callout>
|
|
12
|
+
<Callout type="danger" title={title} _titleId={_titleId} class={Astro.props.class} className={Astro.props.className}><slot /></Callout>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
// The collapsible "Show/Hide properties" wrapper that pairs with
|
|
3
4
|
// Parameter for nested object properties - modeled directly on
|
|
4
5
|
// Mintlify's own Expandable (https://www.mintlify.com/docs/components/
|
|
@@ -25,7 +26,7 @@ interface Props {
|
|
|
25
26
|
const { title = "properties", defaultOpen = true } = Astro.props as Props;
|
|
26
27
|
---
|
|
27
28
|
|
|
28
|
-
<details class="wd-expandable" open={defaultOpen}>
|
|
29
|
+
<details class:list={["wd-expandable", extraClasses(Astro.props)]} open={defaultOpen}>
|
|
29
30
|
<summary>
|
|
30
31
|
<svg class="wd-expandable-chevron" width="11" height="11" viewBox="0 0 10 10" aria-hidden="true">
|
|
31
32
|
<path
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { extraClasses } from './class-names';
|
|
2
3
|
// A generic "put a rounded, bordered card around this" wrapper -
|
|
3
4
|
// deliberately different from Image (a specific <img> with its own
|
|
4
5
|
// src/srcDark/size handling): Frame wraps *arbitrary* slot content - a
|
|
@@ -21,7 +22,7 @@ interface Props {
|
|
|
21
22
|
const { caption, hint } = Astro.props as Props;
|
|
22
23
|
---
|
|
23
24
|
|
|
24
|
-
<figure class="wd-frame">
|
|
25
|
+
<figure class:list={["wd-frame", extraClasses(Astro.props)]}>
|
|
25
26
|
{hint && <p class="wd-frame-hint">{hint}</p>}
|
|
26
27
|
<div class="wd-frame-content">
|
|
27
28
|
<slot />
|