@writedocs/generator 0.4.9 → 0.4.11
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
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { writedocsTempDir } from './writedocs-temp-dir.js';
|
|
14
|
+
import { specDirName } from './openapi-ref.js';
|
|
14
15
|
|
|
15
16
|
export interface OpenApiSchema {
|
|
16
17
|
type?: string;
|
|
@@ -58,10 +59,18 @@ export function operationFileName(method: string, urlPath: string): string {
|
|
|
58
59
|
* every "operations" directory it finds for the target filename. Returns
|
|
59
60
|
* null if no group's spec defines this operation (a stale/typo'd
|
|
60
61
|
* `openapi:` frontmatter value). */
|
|
61
|
-
export function findOperationFile(contentDir: string, method: string, urlPath: string): string | null {
|
|
62
|
+
export function findOperationFile(contentDir: string, method: string, urlPath: string, spec?: string | null): string | null {
|
|
62
63
|
const openapiRoot = path.join(writedocsTempDir(contentDir), 'openapi');
|
|
63
64
|
const filename = operationFileName(method, urlPath);
|
|
64
65
|
|
|
66
|
+
// A page that names its spec (Mintlify's "spec.json METHOD /path" -
|
|
67
|
+
// see lib/openapi-ref.js) looks in that spec's own operations first, so
|
|
68
|
+
// the same METHOD /path in two specs resolves to the right one.
|
|
69
|
+
if (spec) {
|
|
70
|
+
const own = path.join(openapiRoot, '_pages', specDirName(spec), 'operations', filename);
|
|
71
|
+
if (fs.existsSync(own)) return own;
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
function walk(dir: string): string | null {
|
|
66
75
|
if (!fs.existsSync(dir)) return null;
|
|
67
76
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
package/src/lib/pages.js
CHANGED
|
@@ -7,28 +7,97 @@ import path from 'node:path';
|
|
|
7
7
|
import matter from 'gray-matter';
|
|
8
8
|
|
|
9
9
|
// Top-level folders of a content directory that are never scanned for
|
|
10
|
-
// pages - build output, dependencies, and static assets.
|
|
10
|
+
// pages - build output, dependencies, and static assets. Any folder whose
|
|
11
|
+
// name starts with a dot (.git, .github, .claude, .mintlify, ...) is
|
|
12
|
+
// skipped too, at any depth - see findAllPages().
|
|
11
13
|
export const EXCLUDED_TOP_LEVEL_DIRS = new Set(['node_modules', 'dist', '.astro', '.writedocs', 'public', '.git']);
|
|
12
14
|
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
/** A page's title when its frontmatter has none - Mintlify's rule, so a
|
|
16
|
+
* migrated page that relied on it keeps the same title: the file name
|
|
17
|
+
* without its extension, dashes and underscores as spaces, first letter
|
|
18
|
+
* capitalized. `guides/getting-started.mdx` -> "Getting started". */
|
|
19
|
+
export function titleFromPath(filePath) {
|
|
20
|
+
const base = String(filePath).split(/[\\/]/).pop().replace(/\.mdx?$/i, '');
|
|
21
|
+
const words = base.replace(/[-_]+/g, ' ').trim();
|
|
22
|
+
return words ? words.charAt(0).toUpperCase() + words.slice(1) : 'Untitled';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Mintlify's .mintignore, if the content directory has one: gitignore-
|
|
26
|
+
* style patterns for files and folders that aren't published. Supports
|
|
27
|
+
* comments, blank lines, `*`, `**`, `?`, a leading `/` (anchored to the
|
|
28
|
+
* content directory) and a trailing `/` (folders only); negation (`!`)
|
|
29
|
+
* isn't supported and such lines are skipped. Returns (relPath, isDir) =>
|
|
30
|
+
* boolean. */
|
|
31
|
+
function readIgnoreFile(contentDir) {
|
|
32
|
+
let text;
|
|
33
|
+
try {
|
|
34
|
+
text = fs.readFileSync(path.join(contentDir, '.mintignore'), 'utf-8');
|
|
35
|
+
} catch {
|
|
36
|
+
return () => false;
|
|
37
|
+
}
|
|
38
|
+
const rules = text
|
|
39
|
+
.split(/\r?\n/)
|
|
40
|
+
.map((l) => l.trim())
|
|
41
|
+
.filter((l) => l && !l.startsWith('#') && !l.startsWith('!'))
|
|
42
|
+
.map((pattern) => {
|
|
43
|
+
const dirOnly = pattern.endsWith('/');
|
|
44
|
+
let p = pattern.replace(/\/+$/, '');
|
|
45
|
+
const anchored = p.startsWith('/') || p.includes('/');
|
|
46
|
+
p = p.replace(/^\/+/, '');
|
|
47
|
+
const source = p
|
|
48
|
+
.split(/(\*\*\/?|\*|\?)/)
|
|
49
|
+
.map((part) =>
|
|
50
|
+
part === '**/' || part === '**' ? '(?:.*/)?' : part === '*' ? '[^/]*' : part === '?' ? '[^/]' : part.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
51
|
+
)
|
|
52
|
+
.join('');
|
|
53
|
+
const re = new RegExp(anchored ? `^${source}$` : `(?:^|/)${source}$`);
|
|
54
|
+
return { re, dirOnly };
|
|
55
|
+
});
|
|
56
|
+
return (rel, isDir) => rules.some((r) => (!r.dirOnly || isDir) && r.re.test(rel));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Page ids writedocs.json's navigation lists (strings in arrays, and
|
|
60
|
+
* groups' `page`) - read straight from the file, since this runs before
|
|
61
|
+
* (and independently of) config validation. Empty when there's no
|
|
62
|
+
* readable writedocs.json. */
|
|
63
|
+
function navigationPageIds(contentDir) {
|
|
64
|
+
let config;
|
|
65
|
+
try {
|
|
66
|
+
config = JSON.parse(fs.readFileSync(path.join(contentDir, 'writedocs.json'), 'utf-8'));
|
|
67
|
+
} catch {
|
|
68
|
+
return new Set();
|
|
69
|
+
}
|
|
70
|
+
const ids = new Set();
|
|
71
|
+
(function walk(node) {
|
|
72
|
+
if (Array.isArray(node)) node.forEach((item) => (typeof item === 'string' ? ids.add(item) : walk(item)));
|
|
73
|
+
else if (node && typeof node === 'object') {
|
|
74
|
+
for (const [key, value] of Object.entries(node)) {
|
|
75
|
+
if (key === 'page' && typeof value === 'string') ids.add(value);
|
|
76
|
+
else if (key !== 'openapi' && key !== 'href') walk(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})(config.navigation);
|
|
80
|
+
return ids;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Recursively finds every .md/.mdx file under `contentDir` that is a page:
|
|
84
|
+
* one with a frontmatter block, or one writedocs.json's navigation lists
|
|
85
|
+
* (a Mintlify page needs no frontmatter at all - see titleFromPath()).
|
|
86
|
+
* Skips EXCLUDED_TOP_LEVEL_DIRS, every dot-folder at any depth, and
|
|
87
|
+
* anything a .mintignore file lists. docs/ is scanned exactly like any
|
|
17
88
|
* other folder, no special-casing. Returns POSIX-relative paths (from
|
|
18
|
-
* `contentDir`) suitable to hand straight to Astro's `glob()` loader as
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* (see content.config.ts's own comment on why an empty collection still
|
|
23
|
-
* needs to exist, just backed by a no-op loader, to avoid Astro's "does
|
|
24
|
-
* not exist or is empty" warning). Also reused directly by
|
|
89
|
+
* `contentDir`) suitable to hand straight to Astro's `glob()` loader as a
|
|
90
|
+
* literal `pattern` array - see content.config.ts's `pages` collection,
|
|
91
|
+
* and [...slug].astro, which needs the identical list to decide whether
|
|
92
|
+
* calling `getCollection('pages')` is worth doing at all. Also reused by
|
|
25
93
|
* astro.config.mjs's noindex/sitemap scan and by `writedocs validate`, so
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* site's own file count) not to matter. */
|
|
94
|
+
* all of them see exactly the same file set. Synchronous and re-run from
|
|
95
|
+
* scratch wherever it's called - cheap enough for a docs site's file
|
|
96
|
+
* count. */
|
|
30
97
|
export function findAllPages(contentDir) {
|
|
31
98
|
const results = [];
|
|
99
|
+
const ignored = readIgnoreFile(contentDir);
|
|
100
|
+
const listed = navigationPageIds(contentDir);
|
|
32
101
|
function walk(dir, relBase) {
|
|
33
102
|
let entries;
|
|
34
103
|
try {
|
|
@@ -41,10 +110,13 @@ export function findAllPages(contentDir) {
|
|
|
41
110
|
const abs = path.join(dir, entry.name);
|
|
42
111
|
if (entry.isDirectory()) {
|
|
43
112
|
if (relBase === '' && EXCLUDED_TOP_LEVEL_DIRS.has(entry.name)) continue;
|
|
113
|
+
if (entry.name.startsWith('.')) continue;
|
|
114
|
+
if (ignored(rel, true)) continue;
|
|
44
115
|
walk(abs, rel);
|
|
45
116
|
continue;
|
|
46
117
|
}
|
|
47
118
|
if (!entry.isFile() || !/\.mdx?$/i.test(entry.name)) continue;
|
|
119
|
+
if (ignored(rel, false)) continue;
|
|
48
120
|
let raw;
|
|
49
121
|
try {
|
|
50
122
|
raw = fs.readFileSync(abs, 'utf-8');
|
|
@@ -62,7 +134,7 @@ export function findAllPages(contentDir) {
|
|
|
62
134
|
if (/^---\r?\n/.test(raw)) results.push(rel);
|
|
63
135
|
continue;
|
|
64
136
|
}
|
|
65
|
-
if (Object.keys(data).length === 0) continue; //
|
|
137
|
+
if (Object.keys(data).length === 0 && !listed.has(fileIdForPath(rel))) continue; // not a page
|
|
66
138
|
results.push(rel);
|
|
67
139
|
}
|
|
68
140
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
findAllPages,
|
|
21
21
|
} from "../lib/config";
|
|
22
22
|
import { writedocsTempDir } from "../lib/writedocs-temp-dir.js";
|
|
23
|
+
import { parseOpenApiRef } from "../lib/openapi-ref.js";
|
|
23
24
|
import BaseLayout from "../layout/BaseLayout.astro";
|
|
24
25
|
import Sidebar from "../layout/components/Sidebar.astro";
|
|
25
26
|
import TableOfContents from "../layout/components/TableOfContents.astro";
|
|
@@ -55,6 +56,8 @@ import Check from "../components/Check.astro";
|
|
|
55
56
|
import ParamField from "../components/ParamField.astro";
|
|
56
57
|
import ResponseField from "../components/ResponseField.astro";
|
|
57
58
|
import Columns from "../components/Columns.astro";
|
|
59
|
+
import Column from "../components/Column.astro";
|
|
60
|
+
import CodeBlock from "../components/CodeBlock.astro";
|
|
58
61
|
import Tooltip from "../components/Tooltip.astro";
|
|
59
62
|
import Update from "../components/Update.astro";
|
|
60
63
|
import Tile from "../components/Tile.astro";
|
|
@@ -321,8 +324,9 @@ for (const e of entries) {
|
|
|
321
324
|
navTitleByFileId.set(fileId, e.data.sidebarTitle ?? e.data.title);
|
|
322
325
|
entryByFileId.set(fileId, e);
|
|
323
326
|
// `hideApiMarker` (Mintlify's) drops the badge for this one page.
|
|
324
|
-
|
|
325
|
-
|
|
327
|
+
const apiRef = parseOpenApiRef(e.data.openapi);
|
|
328
|
+
if (apiRef && !e.data.hideApiMarker) {
|
|
329
|
+
methodByFileId.set(fileId, apiRef.method);
|
|
326
330
|
}
|
|
327
331
|
}
|
|
328
332
|
const titleForSlug = (slug: string) => titleByFileId.get(slug) ?? slug;
|
|
@@ -444,6 +448,8 @@ const components = {
|
|
|
444
448
|
ParamField,
|
|
445
449
|
ResponseField,
|
|
446
450
|
Columns,
|
|
451
|
+
Column,
|
|
452
|
+
CodeBlock,
|
|
447
453
|
Tooltip,
|
|
448
454
|
Update,
|
|
449
455
|
Tile,
|