@writedocs/generator 0.1.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/LICENSE +15 -0
- package/README.md +17 -0
- package/astro.config.mjs +419 -0
- package/bin/writedocs.js +73 -0
- package/package.json +79 -0
- package/src/assets/wd_watermark.png +0 -0
- package/src/assets/wd_watermark_dark.png +0 -0
- package/src/cli/build-auth.js +53 -0
- package/src/cli/build.js +40 -0
- package/src/cli/dev.js +12 -0
- package/src/cli/generate-api-pages.js +359 -0
- package/src/cli/init.js +81 -0
- package/src/cli/preflight.js +40 -0
- package/src/cli/run-astro.js +57 -0
- package/src/cli/run-pagefind.js +66 -0
- package/src/cli/write-redirects-file.js +80 -0
- package/src/components/Accordion.astro +164 -0
- package/src/components/AccordionGroup.astro +40 -0
- package/src/components/ApiLangSelect.astro +168 -0
- package/src/components/ApiPlayground.astro +281 -0
- package/src/components/ApiReferencePanel.astro +1754 -0
- package/src/components/ApiSchemaField.astro +54 -0
- package/src/components/AppIcon.astro +32 -0
- package/src/components/Badge.astro +128 -0
- package/src/components/Callout.astro +168 -0
- package/src/components/Card.astro +136 -0
- package/src/components/CardGroup.astro +20 -0
- package/src/components/CodeGroup.astro +184 -0
- package/src/components/CopyPageMenu.astro +246 -0
- package/src/components/Danger.astro +12 -0
- package/src/components/Expandable.astro +126 -0
- package/src/components/Frame.astro +102 -0
- package/src/components/Hint.astro +99 -0
- package/src/components/Icon.astro +70 -0
- package/src/components/Image.astro +147 -0
- package/src/components/Info.astro +12 -0
- package/src/components/Note.astro +12 -0
- package/src/components/Parameter.astro +119 -0
- package/src/components/RequestExample.astro +33 -0
- package/src/components/ResponseExample.astro +19 -0
- package/src/components/Searchbar.astro +117 -0
- package/src/components/Step.astro +10 -0
- package/src/components/Steps.astro +32 -0
- package/src/components/Tab.astro +9 -0
- package/src/components/Tabs.astro +52 -0
- package/src/components/Tip.astro +12 -0
- package/src/components/Video.astro +135 -0
- package/src/components/Warning.astro +12 -0
- package/src/components/index.ts +48 -0
- package/src/content.config.ts +223 -0
- package/src/layout/BaseLayout.astro +750 -0
- package/src/layout/components/AnalyticsScripts.astro +77 -0
- package/src/layout/components/AskAiWidget.astro +37 -0
- package/src/layout/components/Breadcrumbs.astro +97 -0
- package/src/layout/components/ImageZoom.astro +19 -0
- package/src/layout/components/MobileMenu.astro +200 -0
- package/src/layout/components/NavTree.astro +351 -0
- package/src/layout/components/SearchModal.astro +42 -0
- package/src/layout/components/Sidebar.astro +122 -0
- package/src/layout/components/SiteFooter.astro +85 -0
- package/src/layout/components/TableOfContents.astro +117 -0
- package/src/layout/components/TopBar.astro +311 -0
- package/src/layout/styles/banner.css +44 -0
- package/src/layout/styles/base.css +234 -0
- package/src/layout/styles/dropdown.css +133 -0
- package/src/layout/styles/footer.css +108 -0
- package/src/layout/styles/image-zoom.css +50 -0
- package/src/layout/styles/mobile-menu.css +258 -0
- package/src/layout/styles/search-modal.css +122 -0
- package/src/layout/styles/topbar.css +437 -0
- package/src/lib/config.ts +2131 -0
- package/src/lib/mdx-auto-hydrate.js +70 -0
- package/src/lib/mdx-inject-builtins.js +87 -0
- package/src/lib/mdx-substitute-variables.js +66 -0
- package/src/lib/mdx-title-anchor-ids.js +84 -0
- package/src/lib/mermaid-rehype.js +72 -0
- package/src/lib/openapi-render.ts +479 -0
- package/src/lib/shiki-code-block.js +102 -0
- package/src/lib/shiki-copy-button.js +45 -0
- package/src/lib/styles-asset-integration.js +210 -0
- package/src/lib/writedocs-temp-dir.js +93 -0
- package/src/pages/404.astro +62 -0
- package/src/pages/[...slug].astro +1270 -0
- package/src/pages/[...slug].md.ts +78 -0
- package/src/pages/llms-full.txt.ts +71 -0
- package/src/pages/llms.txt.ts +141 -0
- package/src/scripts/banner.ts +20 -0
- package/src/scripts/dropdowns.ts +61 -0
- package/src/scripts/image-zoom.ts +66 -0
- package/src/scripts/mobile-menu.ts +55 -0
- package/src/scripts/search.ts +155 -0
- package/src/scripts/sidebar-scroll.ts +65 -0
- package/src/scripts/theme-toggle.ts +35 -0
- package/src/scripts/topbar-offset.ts +141 -0
- package/src/styles/global.css +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriel Raeder
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Writedocs
|
|
2
|
+
|
|
3
|
+
A static site generator for documentation: a `writedocs.json` config + a folder of MDX/Markdown in, a fully static site out. Built on [Astro](https://astro.build), in the spirit of Docusaurus/Mintlify.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @writedocs/generator
|
|
9
|
+
|
|
10
|
+
cd my-docs # any folder containing writedocs.json + docs/
|
|
11
|
+
writedocs dev
|
|
12
|
+
writedocs build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The npm package is `@writedocs/generator`; the CLI command it installs is still `writedocs` (see the `bin` field in `package.json`).
|
|
16
|
+
|
|
17
|
+
`npx @writedocs/generator init/dev/build` still works too, for a one-off run with no install at all.
|
package/astro.config.mjs
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { defineConfig } from 'astro/config';
|
|
2
|
+
import mdx from '@astrojs/mdx';
|
|
3
|
+
import react from '@astrojs/react';
|
|
4
|
+
import icon from 'astro-icon';
|
|
5
|
+
import sitemap from '@astrojs/sitemap';
|
|
6
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import matter from 'gray-matter';
|
|
11
|
+
import { unified } from '@astrojs/markdown-remark';
|
|
12
|
+
import {
|
|
13
|
+
transformerMetaHighlight,
|
|
14
|
+
transformerMetaWordHighlight,
|
|
15
|
+
transformerNotationHighlight,
|
|
16
|
+
transformerNotationWordHighlight,
|
|
17
|
+
transformerNotationFocus,
|
|
18
|
+
transformerNotationDiff,
|
|
19
|
+
transformerNotationErrorLevel,
|
|
20
|
+
} from '@shikijs/transformers';
|
|
21
|
+
import { codeBlockTransformer } from './src/lib/shiki-code-block.js';
|
|
22
|
+
import { rehypeMermaid } from './src/lib/mermaid-rehype.js';
|
|
23
|
+
import remarkMath from 'remark-math';
|
|
24
|
+
import rehypeKatex from 'rehype-katex';
|
|
25
|
+
import { remarkAutoHydrateSnippets } from './src/lib/mdx-auto-hydrate.js';
|
|
26
|
+
import { remarkInjectBuiltinComponents } from './src/lib/mdx-inject-builtins.js';
|
|
27
|
+
import { remarkTitleAnchorIds } from './src/lib/mdx-title-anchor-ids.js';
|
|
28
|
+
import { remarkSubstituteVariables } from './src/lib/mdx-substitute-variables.js';
|
|
29
|
+
import { writedocsTempDir, writedocsBuildStagingDir } from './src/lib/writedocs-temp-dir.js';
|
|
30
|
+
import { stylesAssetFallback } from './src/lib/styles-asset-integration.js';
|
|
31
|
+
import {
|
|
32
|
+
loadDocsConfig,
|
|
33
|
+
resolveCodeblockTheme,
|
|
34
|
+
resolveCodeblockLangAlias,
|
|
35
|
+
resolveSiteUrl,
|
|
36
|
+
normalizeEntryId,
|
|
37
|
+
findAllPages,
|
|
38
|
+
} from './src/lib/config.ts';
|
|
39
|
+
|
|
40
|
+
const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
41
|
+
// See writedocsBuildStagingDir()'s own comment (writedocs-temp-dir.js) for
|
|
42
|
+
// why `outDir` below points here instead of directly at contentDir/dist -
|
|
43
|
+
// short version: keeps Astro's own build executing inside the package (so
|
|
44
|
+
// its dependencies' own module resolution keeps working), while build.js
|
|
45
|
+
// does the actual cross-filesystem-safe copy into contentDir/dist as an
|
|
46
|
+
// explicit final step once the build itself is done.
|
|
47
|
+
const packageRoot = process.env.WRITEDOCS_PACKAGE_ROOT || path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
|
|
49
|
+
// Read here (rather than deferred to page-render time, where
|
|
50
|
+
// loadDocsConfig() is also called from [...slug].astro) specifically so
|
|
51
|
+
// a site's writedocs.json can pick its own Shiki theme names - astro.config.mjs
|
|
52
|
+
// is where Shiki itself actually gets configured, and dev.js/build.js
|
|
53
|
+
// already guarantee writedocs.json exists and is valid JSON (preflightCheck())
|
|
54
|
+
// before runAstro() ever gets this far, so letting loadDocsConfig() throw
|
|
55
|
+
// its own validation error on a malformed writedocs.json here is consistent
|
|
56
|
+
// with how every other consumer of it in this codebase behaves - no
|
|
57
|
+
// special-casing needed.
|
|
58
|
+
const docsConfig = loadDocsConfig(contentDir);
|
|
59
|
+
// resolveCodeblockTheme() is the single source of truth for the
|
|
60
|
+
// github-light/github-dark fallback - also used by ApiReferencePanel.astro
|
|
61
|
+
// for its own separate <Code/> usages (which don't inherit
|
|
62
|
+
// markdown.shikiConfig at all), so both stay in sync off the same
|
|
63
|
+
// writedocs.json field.
|
|
64
|
+
const codeblockTheme = resolveCodeblockTheme(docsConfig);
|
|
65
|
+
// resolveCodeblockLangAlias() - see its own comment in lib/config.ts for
|
|
66
|
+
// why this exists: some bundled Shiki grammars (mdx, notably) parse
|
|
67
|
+
// without error but produce no real token differentiation under this
|
|
68
|
+
// theme, so a ```lang fence renders as one flat color instead of
|
|
69
|
+
// highlighted. Remaps a fence's language tag to a better-behaved grammar
|
|
70
|
+
// before Shiki ever sees it - built-in mdx -> jsx default, plus whatever
|
|
71
|
+
// a site adds of its own under writedocs.json's styles.codeblocks.langAlias.
|
|
72
|
+
const codeblockLangAlias = resolveCodeblockLangAlias(docsConfig);
|
|
73
|
+
|
|
74
|
+
// publicDir defaults relative to Astro's --root (always the writedocs
|
|
75
|
+
// package itself - see src/cli/run-astro.js), not the content directory
|
|
76
|
+
// being built. Point it at <contentDir>/public instead so a site's own
|
|
77
|
+
// static assets (logos, favicons, ...) actually get copied/served -
|
|
78
|
+
// falling back to Astro's normal default (<packageRoot>/public, which
|
|
79
|
+
// doesn't exist in this package) when a site has no public/ of its own,
|
|
80
|
+
// since passing a nonexistent publicDir is a hard Astro error.
|
|
81
|
+
const contentPublicDir = path.join(contentDir, 'public');
|
|
82
|
+
|
|
83
|
+
// Sitemap generation needs an absolute origin to build absolute <loc>
|
|
84
|
+
// entries against - writedocs.json's `domain` field (see resolveSiteUrl() in
|
|
85
|
+
// lib/config.ts) is the single source of truth for that, shared with the
|
|
86
|
+
// canonical/OG/Twitter URL tags BaseLayout.astro renders per-page. A site
|
|
87
|
+
// with no `domain` set (most fixtures, local-only builds) just doesn't
|
|
88
|
+
// get a sitemap.xml at all - @astrojs/sitemap requires Astro's own `site`
|
|
89
|
+
// config to be set and would otherwise throw, and a sitemap of
|
|
90
|
+
// document-relative URLs isn't meaningful anyway.
|
|
91
|
+
const siteUrl = resolveSiteUrl(docsConfig);
|
|
92
|
+
if (!siteUrl) {
|
|
93
|
+
console.log('[writedocs] No writedocs.json "domain" set - skipping sitemap.xml generation.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Recursively lists every .md/.mdx file under `baseDir` (Node 20's
|
|
97
|
+
* recursive readdir, no extra glob dependency needed - matches the
|
|
98
|
+
* plain-fs-walk style already used by generate-api-pages.js's
|
|
99
|
+
* findHandWrittenOverrides()), or an empty list if the directory doesn't
|
|
100
|
+
* exist (e.g. a site with no OpenAPI groups has no generated-docs/ at
|
|
101
|
+
* all under writedocsTempDir()). */
|
|
102
|
+
function walkMdFiles(baseDir) {
|
|
103
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
104
|
+
return fs
|
|
105
|
+
.readdirSync(baseDir, { recursive: true })
|
|
106
|
+
.filter((f) => /\.mdx?$/i.test(f))
|
|
107
|
+
.map((f) => path.join(baseDir, f));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The set of normalized page ids (see normalizeEntryId()) whose
|
|
111
|
+
* frontmatter sets `seo.noindex: true` - excluded from sitemap.xml below,
|
|
112
|
+
* since listing a page there while also telling crawlers not to index it
|
|
113
|
+
* (BaseLayout.astro's <meta name="robots" content="noindex..."> for the
|
|
114
|
+
* same flag) would be self-contradictory. Reads frontmatter directly via
|
|
115
|
+
* gray-matter rather than astro:content, which doesn't exist yet this
|
|
116
|
+
* early in Astro's own startup - the same reason
|
|
117
|
+
* generate-api-pages.js's own frontmatter scanning does the same thing.
|
|
118
|
+
*
|
|
119
|
+
* Reuses findAllPages() from lib/config.ts (rather than its own
|
|
120
|
+
* hand-rolled walk) for the hand-written-page half of this scan, so it
|
|
121
|
+
* always sees exactly the same file set that actually becomes a page in
|
|
122
|
+
* the `pages` collection - no risk of the two independently drifting
|
|
123
|
+
* apart on which directories get excluded. The fallback id
|
|
124
|
+
* (`relativeId` with its extension and any trailing `/index` segment
|
|
125
|
+
* stripped) mirrors fileIdForEntry()'s own algorithm in lib/config.ts,
|
|
126
|
+
* for a page with no explicit `slug` override. */
|
|
127
|
+
function collectNoindexIds(rootContentDir) {
|
|
128
|
+
const generatedDocsDir = path.join(writedocsTempDir(rootContentDir), 'generated-docs');
|
|
129
|
+
const ids = new Set();
|
|
130
|
+
|
|
131
|
+
for (const relativeId of findAllPages(rootContentDir)) {
|
|
132
|
+
const file = path.join(rootContentDir, relativeId);
|
|
133
|
+
const { data } = matter(fs.readFileSync(file, 'utf-8'));
|
|
134
|
+
if (!data?.seo?.noindex) continue;
|
|
135
|
+
const fallbackId = relativeId.replace(/\.mdx?$/i, '').replace(/\/index$/, '');
|
|
136
|
+
ids.add(normalizeEntryId(data.slug ?? fallbackId));
|
|
137
|
+
}
|
|
138
|
+
// Generated OpenAPI stub pages always set an explicit `slug` (see
|
|
139
|
+
// generate-api-pages.js) - there's no file-path-derived id to fall back
|
|
140
|
+
// to the way there is for a hand-written page.
|
|
141
|
+
for (const file of walkMdFiles(generatedDocsDir)) {
|
|
142
|
+
const { data } = matter(fs.readFileSync(file, 'utf-8'));
|
|
143
|
+
if (data?.seo?.noindex && data.slug) ids.add(normalizeEntryId(data.slug));
|
|
144
|
+
}
|
|
145
|
+
return ids;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const noindexIds = siteUrl ? collectNoindexIds(contentDir) : new Set();
|
|
149
|
+
|
|
150
|
+
// writedocs.json's `redirects` array -> Astro's own `redirects` config option
|
|
151
|
+
// (a plain `{ [source]: destination }` record - see redirectSchema in
|
|
152
|
+
// lib/config.ts for why there's no `permanent`/status-code field: this
|
|
153
|
+
// package always builds `output: 'static'` with no adapter installed, and
|
|
154
|
+
// Astro's own docs say a static redirect is always a client-side
|
|
155
|
+
// `<meta http-equiv="refresh">` page regardless of what status is asked
|
|
156
|
+
// for). Omitted entirely (rather than passed as `{}`) when a site has no
|
|
157
|
+
// redirects configured, matching the `siteUrl ? [...] : []` pattern above
|
|
158
|
+
// of only opting into a feature's config surface when it's actually used.
|
|
159
|
+
const redirectsConfig = Object.fromEntries(docsConfig.redirects.map((r) => [r.source, r.destination]));
|
|
160
|
+
|
|
161
|
+
export default defineConfig({
|
|
162
|
+
...(siteUrl ? { site: siteUrl } : {}),
|
|
163
|
+
...(docsConfig.redirects.length ? { redirects: redirectsConfig } : {}),
|
|
164
|
+
// Astro's own dev-mode toolbar (the floating pill in the bottom center
|
|
165
|
+
// of every page - inspect/audit/settings) is a tool for authoring
|
|
166
|
+
// Astro itself, not something a writedocs site's own reader/author
|
|
167
|
+
// should see while running `writedocs dev` - it's not exposed as a
|
|
168
|
+
// writedocs.json option (nothing site-specific to configure here) since
|
|
169
|
+
// there's no scenario where a writedocs project actually wants it on.
|
|
170
|
+
// Off in every environment (build never shows it anyway, since it's a
|
|
171
|
+
// dev-only overlay Astro strips from production output regardless).
|
|
172
|
+
devToolbar: { enabled: false },
|
|
173
|
+
// Not contentDir/dist directly - see writedocsBuildStagingDir()'s own
|
|
174
|
+
// comment (writedocs-temp-dir.js) for why, and build.js for the copy
|
|
175
|
+
// step that actually gets a finished build from here into the real
|
|
176
|
+
// <contentDir>/dist a reader/host ever sees. Irrelevant to `astro dev`
|
|
177
|
+
// (no build output involved), so this applies unconditionally rather
|
|
178
|
+
// than branching on which CLI command is running.
|
|
179
|
+
outDir: writedocsBuildStagingDir(packageRoot, contentDir),
|
|
180
|
+
cacheDir: path.join(writedocsTempDir(contentDir), 'cache'),
|
|
181
|
+
...(fs.existsSync(contentPublicDir) ? { publicDir: contentPublicDir } : {}),
|
|
182
|
+
// icon() reads from whatever @iconify-json/* collections are installed
|
|
183
|
+
// (see package.json) and inlines only the specific icons a build
|
|
184
|
+
// actually references as <svg> - the installed collections don't
|
|
185
|
+
// affect output size beyond that. No `include` config needed: astro-icon
|
|
186
|
+
// resolves "collection:icon-name" against any installed collection
|
|
187
|
+
// automatically.
|
|
188
|
+
integrations: [
|
|
189
|
+
// markdown.processor (below) carries remarkAutoHydrateSnippets - see
|
|
190
|
+
// its own comment there for why it's attached there and not here.
|
|
191
|
+
// Auto-adds `client:load` to every usage of a component imported from
|
|
192
|
+
// a .jsx/.tsx file (a snippet - see docs/dev/docs/snippets.mdx) that
|
|
193
|
+
// doesn't already carry its own `client:*` attribute, so a snippet
|
|
194
|
+
// just works when dropped into a page - nobody using it needs to know
|
|
195
|
+
// Astro's hydration-directive syntax exists at all, only someone who
|
|
196
|
+
// wants a *different* strategy (client:visible/idle/only) needs to
|
|
197
|
+
// write one explicitly.
|
|
198
|
+
mdx(),
|
|
199
|
+
// Lets a site's own snippets (see docs/dev/docs/snippets.mdx) be real
|
|
200
|
+
// .jsx/.tsx React components - hooks and all - dropped straight into
|
|
201
|
+
// MDX and hydrated client-side as an Astro island (automatically -
|
|
202
|
+
// see remarkAutoHydrateSnippets above). writedocs' own built-in
|
|
203
|
+
// components (Callout, Card, ...) stay plain .astro files - this is
|
|
204
|
+
// purely for snippets a site author writes themselves.
|
|
205
|
+
react(),
|
|
206
|
+
icon(),
|
|
207
|
+
...(siteUrl
|
|
208
|
+
? [
|
|
209
|
+
sitemap({
|
|
210
|
+
filter: (page) => {
|
|
211
|
+
const pathname = new URL(page).pathname;
|
|
212
|
+
const id = normalizeEntryId(pathname);
|
|
213
|
+
return !noindexIds.has(id);
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
]
|
|
217
|
+
: []),
|
|
218
|
+
// Lets styles.favicon/styles.logo/styles.background.images/seo.ogImage/
|
|
219
|
+
// styles.fonts.*.source resolve from anywhere in the project, not just
|
|
220
|
+
// public/ - see collectConfiguredAssetPaths() (lib/config.ts) for the
|
|
221
|
+
// field list and styles-asset-integration.js for how it's actually
|
|
222
|
+
// served/copied.
|
|
223
|
+
stylesAssetFallback(contentDir),
|
|
224
|
+
],
|
|
225
|
+
output: 'static',
|
|
226
|
+
// Dual Shiki themes for fenced code blocks (```) in MDX content, so
|
|
227
|
+
// they switch with the site's own [data-theme] toggle instead of
|
|
228
|
+
// always rendering Astro's single default (github-dark) regardless of
|
|
229
|
+
// light/dark mode. Defaults to github-light/github-dark, overridable
|
|
230
|
+
// per-site via writedocs.json's styles.codeblocks.{light,dark} (any Shiki
|
|
231
|
+
// theme *name* - see https://shiki.style/themes for the built-in
|
|
232
|
+
// list - not a custom theme object/JSON file, at least for now).
|
|
233
|
+
// Astro bakes the "light" theme's colors as the element's base inline
|
|
234
|
+
// style and adds --shiki-dark/--shiki-dark-bg (etc.) custom
|
|
235
|
+
// properties alongside it - see the
|
|
236
|
+
// [data-theme='dark'] :global(.astro-code) override in
|
|
237
|
+
// BaseLayout.astro that actually swaps to them.
|
|
238
|
+
markdown: {
|
|
239
|
+
// Excludes ```mermaid fences from Shiki's own tokenization entirely -
|
|
240
|
+
// they reach rehypeMermaid (below) as plain, un-highlighted text,
|
|
241
|
+
// which is what mermaid.js needs to parse and render them into an
|
|
242
|
+
// actual diagram client-side (see [...slug].astro's initMermaid()).
|
|
243
|
+
// Syntax-highlighting mermaid's own pseudo-language as if it were
|
|
244
|
+
// source code would be actively wrong here - the goal is a rendered
|
|
245
|
+
// diagram, not colored diagram-description text.
|
|
246
|
+
syntaxHighlight: { type: 'shiki', excludeLangs: ['mermaid'] },
|
|
247
|
+
// Astro 7's markdown.rehypePlugins (top-level) is deprecated in favor
|
|
248
|
+
// of passing plugins straight to the `unified` processor itself -
|
|
249
|
+
// functionally identical (Astro's own deprecated-option shim used to
|
|
250
|
+
// just forward the top-level array into this exact processor), just
|
|
251
|
+
// explicit instead of relying on that shim (and the console warning
|
|
252
|
+
// it prints on every build/dev run). Astro still runs this *after*
|
|
253
|
+
// its own internal Shiki step (see @astrojs/markdown-remark's
|
|
254
|
+
// index.js) regardless of which processor plugins are attached to,
|
|
255
|
+
// so rehypeMermaid still always sees the excluded ```mermaid block
|
|
256
|
+
// completely untouched by Shiki - exactly the plain
|
|
257
|
+
// <pre><code class="language-mermaid"> shape it expects to reshape
|
|
258
|
+
// into mermaid.js's <div class="mermaid"> form.
|
|
259
|
+
// remarkInjectBuiltinComponents/remarkAutoHydrateSnippets only ever
|
|
260
|
+
// match JSX/import nodes that exist solely in MDX's extended syntax
|
|
261
|
+
// tree (see mdx-inject-builtins.js/mdx-auto-hydrate.js) - running them
|
|
262
|
+
// here rather than passing them to mdx({ remarkPlugins }) avoids that
|
|
263
|
+
// option's own deprecation warning ("pass it to unified()... MDX will
|
|
264
|
+
// inherit it" - extendMarkdownConfig defaults to true, so mdx() below
|
|
265
|
+
// already picks this processor up automatically). Harmless no-op on
|
|
266
|
+
// plain .md content, which never has an mdxjsEsm/mdxJsxFlowElement
|
|
267
|
+
// node to match in the first place.
|
|
268
|
+
// remarkSubstituteVariables is a parameterized plugin (an "attacher"
|
|
269
|
+
// that takes writedocs.json's `variables` map as its options and returns
|
|
270
|
+
// the actual transformer) - passed as a unified [attacher, options]
|
|
271
|
+
// tuple, *not* pre-called (`remarkSubstituteVariables(docsConfig.variables)`
|
|
272
|
+
// directly in this array). unified's own `use()` always calls
|
|
273
|
+
// whatever it finds in this array as if it were the attacher itself;
|
|
274
|
+
// handing it an already-produced transformer function makes unified
|
|
275
|
+
// invoke *that* with zero arguments during its `freeze()` setup pass,
|
|
276
|
+
// which crashes deep inside unist-util-visit ("Cannot use 'in'
|
|
277
|
+
// operator to search for 'children' in undefined") the first time any
|
|
278
|
+
// MDX page actually builds - a real bug hit and fixed while testing
|
|
279
|
+
// 25-site-config, not a hypothetical. remarkInjectBuiltinComponents/
|
|
280
|
+
// remarkAutoHydrateSnippets don't need this tuple form since neither
|
|
281
|
+
// takes options - they're valid attachers as bare function references.
|
|
282
|
+
// A site with no `variables` set still gets this plugin instance
|
|
283
|
+
// (which then no-ops on every file - see its own early-return) rather
|
|
284
|
+
// than being conditionally omitted, keeping this array's shape static
|
|
285
|
+
// regardless of writedocs.json content.
|
|
286
|
+
processor: unified({
|
|
287
|
+
// remarkMath goes first: it parses $inline$ and standalone $$block$$
|
|
288
|
+
// math into their own `inlineMath`/`math` mdast node types (mdast-
|
|
289
|
+
// util-to-hast has built-in support for both, turning them into a
|
|
290
|
+
// <span>/<div> carrying the raw LaTeX source as a "language-math"
|
|
291
|
+
// code-like payload for rehypeKatex, below, to render). Running it
|
|
292
|
+
// before remarkSubstituteVariables means a $...$ span's raw LaTeX
|
|
293
|
+
// source is no longer a plain `text` node by the time that plugin's
|
|
294
|
+
// visitor runs (it only visits `text` nodes - see its own comment) -
|
|
295
|
+
// not that `[[key]]` syntax inside actual LaTeX is a realistic thing
|
|
296
|
+
// to write, but this keeps the two plugins' node-type boundaries
|
|
297
|
+
// clean rather than leaving it to chance which one sees a math span
|
|
298
|
+
// as raw text first.
|
|
299
|
+
remarkPlugins: [
|
|
300
|
+
remarkMath,
|
|
301
|
+
remarkInjectBuiltinComponents,
|
|
302
|
+
// Runs after remarkInjectBuiltinComponents (order doesn't actually
|
|
303
|
+
// matter between them - that plugin only ever prepends an import
|
|
304
|
+
// statement, never touches an existing JSX node's attributes) but
|
|
305
|
+
// grouped right next to it since both deal with the same set of
|
|
306
|
+
// built-in component tag names. See its own comment for why this
|
|
307
|
+
// has to be a whole-document pass rather than logic living inside
|
|
308
|
+
// Callout.astro/Accordion.astro themselves.
|
|
309
|
+
remarkTitleAnchorIds,
|
|
310
|
+
remarkAutoHydrateSnippets,
|
|
311
|
+
[remarkSubstituteVariables, docsConfig.variables],
|
|
312
|
+
],
|
|
313
|
+
// rehypeKatex renders every math/inlineMath node remarkMath produced
|
|
314
|
+
// into real KaTeX markup (a <span class="katex">...</span> tree of
|
|
315
|
+
// MathML + HTML, not an image or client-rendered widget) - fully at
|
|
316
|
+
// build time, so unlike Mermaid there's no client-side JS or runtime
|
|
317
|
+
// cost at all, just the one katex.min.css import (BaseLayout.astro)
|
|
318
|
+
// for layout/glyph styling. Order relative to rehypeMermaid doesn't
|
|
319
|
+
// matter - they walk disjoint hast node shapes (pre>code.language-
|
|
320
|
+
// mermaid vs. remarkMath's own math/inlineMath-derived nodes) and
|
|
321
|
+
// neither plugin's output is visible to the other.
|
|
322
|
+
rehypePlugins: [rehypeMermaid, rehypeKatex],
|
|
323
|
+
}),
|
|
324
|
+
shikiConfig: {
|
|
325
|
+
themes: codeblockTheme,
|
|
326
|
+
langAlias: codeblockLangAlias,
|
|
327
|
+
// The official @shikijs/transformers annotation transformers -
|
|
328
|
+
// enabled sitewide so any fenced code block can use their meta-
|
|
329
|
+
// string/comment syntax without per-page opt-in:
|
|
330
|
+
// ```js {1,3-5} -> transformerMetaHighlight
|
|
331
|
+
// ```js /someWord/ -> transformerMetaWordHighlight
|
|
332
|
+
// // [!code highlight] -> transformerNotationHighlight
|
|
333
|
+
// // [!code word:someWord] -> transformerNotationWordHighlight
|
|
334
|
+
// // [!code focus] -> transformerNotationFocus
|
|
335
|
+
// // [!code ++] / // [!code --] -> transformerNotationDiff
|
|
336
|
+
// // [!code error] / [!code warning] -> transformerNotationErrorLevel
|
|
337
|
+
// (comment syntax adapts to the language - # for Python/bash, //
|
|
338
|
+
// for JS/etc - see each transformer's own docs at shiki.style).
|
|
339
|
+
// Plus codeBlockTransformer, writedocs' own transformer for the
|
|
340
|
+
// copy button, an optional title bar, and wrap/lines/expandable
|
|
341
|
+
// meta flags - see src/lib/shiki-code-block.js. It runs last so
|
|
342
|
+
// its `root` hook wraps the <pre> only after every class the
|
|
343
|
+
// transformers above added to it is already in place - though in
|
|
344
|
+
// practice this doesn't matter, since all `pre`/`line` hooks
|
|
345
|
+
// across every transformer run to completion before any `root`
|
|
346
|
+
// hook runs at all (two separate passes, not interleaved).
|
|
347
|
+
transformers: [
|
|
348
|
+
transformerMetaHighlight(),
|
|
349
|
+
transformerMetaWordHighlight(),
|
|
350
|
+
transformerNotationHighlight(),
|
|
351
|
+
transformerNotationWordHighlight(),
|
|
352
|
+
transformerNotationFocus(),
|
|
353
|
+
transformerNotationDiff(),
|
|
354
|
+
transformerNotationErrorLevel(),
|
|
355
|
+
codeBlockTransformer(),
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
// Tailwind is available for future/custom content (e.g. hand-authored
|
|
360
|
+
// MDX) but nothing in writedocs' own components has been migrated to
|
|
361
|
+
// it - they still use scoped <style> blocks + the --wd-* CSS variables
|
|
362
|
+
// so per-site writedocs.json theming (colors, and now dark mode) keeps
|
|
363
|
+
// working. See src/styles/global.css for the one @import that wires
|
|
364
|
+
// Tailwind's utilities in.
|
|
365
|
+
vite: {
|
|
366
|
+
plugins: [tailwindcss()],
|
|
367
|
+
resolve: {
|
|
368
|
+
alias: [
|
|
369
|
+
// Lets a page's MDX write `import Foo from '/snippets/foo.mdx'`
|
|
370
|
+
// (Mintlify's own snippet-import convention) instead of a
|
|
371
|
+
// relative path whose "../" depth depends on how deeply nested
|
|
372
|
+
// the importing page happens to be - see docs/dev/docs/snippets.mdx
|
|
373
|
+
// for the full feature writeup. Vite's project root is always
|
|
374
|
+
// packageRoot (this package itself - see run-astro.js), not
|
|
375
|
+
// contentDir, so a real leading-slash import would otherwise
|
|
376
|
+
// resolve (and fail to find anything) inside this package
|
|
377
|
+
// instead of the site's own content directory; this alias
|
|
378
|
+
// redirects just that one prefix to contentDir/snippets/ instead.
|
|
379
|
+
{ find: /^\/snippets\//, replacement: path.join(contentDir, 'snippets') + '/' },
|
|
380
|
+
// remarkInjectBuiltinComponents (mdx-inject-builtins.js) auto-
|
|
381
|
+
// injects `import { Callout, ... } from 'writedocs/components'`
|
|
382
|
+
// into any .mdx file that uses a built-in component tag without
|
|
383
|
+
// importing it itself - a real bare specifier Vite has to resolve
|
|
384
|
+
// as if the .mdx file had written it. package.json's own
|
|
385
|
+
// `exports: { "./components": "./src/components/index.ts" }`
|
|
386
|
+
// makes that specifier valid *in principle* (Node's package
|
|
387
|
+
// self-reference feature - a package importing its own name), but
|
|
388
|
+
// self-reference only resolves when the importing file is a
|
|
389
|
+
// descendant of this package's own package.json - true for every
|
|
390
|
+
// existing fixture (always built from inside this repo/package
|
|
391
|
+
// tree) but never true for a real consumer's content, which lives
|
|
392
|
+
// in a completely separate directory in the actual product
|
|
393
|
+
// architecture (see docs/dev/docs/architecture.mdx) - walking up
|
|
394
|
+
// from such a file never finds a package.json named "writedocs",
|
|
395
|
+
// so self-reference fails and Vite reports the bare specifier as
|
|
396
|
+
// unresolvable ("Rolldown failed to resolve import
|
|
397
|
+
// 'writedocs/components'"). See BUG.md for the full writeup this
|
|
398
|
+
// fix comes from.
|
|
399
|
+
// This alias bypasses self-reference resolution entirely for
|
|
400
|
+
// that one specifier - Vite's alias matching is a plain string
|
|
401
|
+
// match against the specifier itself, independent of which file
|
|
402
|
+
// imported it, so it resolves correctly regardless of where the
|
|
403
|
+
// content directory actually lives. WRITEDOCS_PACKAGE_ROOT is the
|
|
404
|
+
// same packageRoot value already set by run-astro.js for
|
|
405
|
+
// lib/config.ts's fileIdForEntry() to consume - reusing it here
|
|
406
|
+
// instead of recomputing via import.meta.url keeps there being
|
|
407
|
+
// exactly one source of truth for "where this package lives" per
|
|
408
|
+
// astro subprocess. Falls back to this file's own directory
|
|
409
|
+
// (astro.config.mjs always lives at packageRoot) for anything
|
|
410
|
+
// that ever imports this config outside that subprocess, e.g. a
|
|
411
|
+
// future test harness.
|
|
412
|
+
{
|
|
413
|
+
find: 'writedocs/components',
|
|
414
|
+
replacement: path.join(packageRoot, 'src/components/index.ts'),
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
});
|
package/bin/writedocs.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
import { runDev } from '../src/cli/dev.js';
|
|
8
|
+
import { runBuild } from '../src/cli/build.js';
|
|
9
|
+
import { runInit } from '../src/cli/init.js';
|
|
10
|
+
import { requireBuildKey } from '../src/cli/build-auth.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
14
|
+
// Read rather than hardcode - a literal version string here silently drifts
|
|
15
|
+
// from package.json's own "version" the moment either one is bumped without
|
|
16
|
+
// the other (exactly what `npm version <bump>` does: it only touches
|
|
17
|
+
// package.json). `writedocs --version` should always reflect what actually
|
|
18
|
+
// got published, not whatever this string happened to say at the time this
|
|
19
|
+
// line was last hand-edited.
|
|
20
|
+
const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
program
|
|
24
|
+
.name('writedocs')
|
|
25
|
+
.description('Static site generator for writedocs.json + MDX')
|
|
26
|
+
.version(version);
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.command('dev')
|
|
30
|
+
.description('Start a local dev server with hot reload')
|
|
31
|
+
.argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
|
|
32
|
+
.option('-p, --port <port>', 'port to run on')
|
|
33
|
+
.action(async (dir, opts) => {
|
|
34
|
+
await runDev({
|
|
35
|
+
contentDir: path.resolve(process.cwd(), dir),
|
|
36
|
+
packageRoot,
|
|
37
|
+
port: opts.port,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
// { hidden: true } keeps `build` out of `writedocs --help` - it's not a
|
|
43
|
+
// command ordinary users are meant to discover or run themselves. See
|
|
44
|
+
// src/cli/build-auth.js for the second layer: even someone who knows the
|
|
45
|
+
// command exists still can't run it without the right key.
|
|
46
|
+
.command('build', { hidden: true })
|
|
47
|
+
.description('Build a static site into <dir>/dist')
|
|
48
|
+
.argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
|
|
49
|
+
.option('-k, --key <key>', 'build authorization key (or set WRITEDOCS_API_KEY)')
|
|
50
|
+
.action(async (dir, opts) => {
|
|
51
|
+
const contentDir = path.resolve(process.cwd(), dir);
|
|
52
|
+
// Project-scoped, not global: a .env sitting next to this project's own
|
|
53
|
+
// writedocs.json (WRITEDOCS_KEY_SERVER_URL, WRITEDOCS_API_KEY) is picked
|
|
54
|
+
// up automatically, so build doesn't need those exported by hand every
|
|
55
|
+
// session. dotenv never overwrites a var already set in the real
|
|
56
|
+
// environment - an explicit `export`/CI secret still wins over the file.
|
|
57
|
+
dotenv.config({ path: path.join(contentDir, '.env'), quiet: true });
|
|
58
|
+
await requireBuildKey(opts.key || process.env.WRITEDOCS_API_KEY);
|
|
59
|
+
await runBuild({ contentDir, packageRoot });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command('init')
|
|
64
|
+
.description('Scaffold a writedocs.json and starter docs/ folder')
|
|
65
|
+
.argument('[dir]', 'directory to scaffold into', '.')
|
|
66
|
+
.action(async (dir) => {
|
|
67
|
+
await runInit({ targetDir: path.resolve(process.cwd(), dir) });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
71
|
+
console.error(`[writedocs] ${err.message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@writedocs/generator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"writedocs": "./bin/writedocs.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./components": "./src/components/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"astro.config.mjs"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "node bin/writedocs.js dev",
|
|
19
|
+
"build": "node bin/writedocs.js build",
|
|
20
|
+
"init": "node bin/writedocs.js init"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"docs",
|
|
24
|
+
"documentation",
|
|
25
|
+
"static-site-generator",
|
|
26
|
+
"mdx",
|
|
27
|
+
"astro"
|
|
28
|
+
],
|
|
29
|
+
"homepage": "https://github.com/writedocs/writedocs#readme",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/writedocs/writedocs.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/writedocs/writedocs/issues"
|
|
36
|
+
},
|
|
37
|
+
"author": "Gabriel Raeder",
|
|
38
|
+
"license": "ISC",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@apidevtools/swagger-parser": "^12.1.0",
|
|
44
|
+
"@astrojs/markdown-remark": "7.2.2",
|
|
45
|
+
"@astrojs/mdx": "^7.0.5",
|
|
46
|
+
"@astrojs/react": "^6.0.2",
|
|
47
|
+
"@astrojs/sitemap": "^3.7.3",
|
|
48
|
+
"@iconify-json/bi": "^1.2.7",
|
|
49
|
+
"@iconify-json/fa6-brands": "^1.2.6",
|
|
50
|
+
"@iconify-json/fa6-solid": "^1.2.4",
|
|
51
|
+
"@iconify-json/heroicons": "^1.2.3",
|
|
52
|
+
"@iconify-json/ion": "^1.2.7",
|
|
53
|
+
"@iconify-json/lucide": "^1.2.116",
|
|
54
|
+
"@iconify-json/mdi": "^1.2.3",
|
|
55
|
+
"@iconify-json/ri": "^1.2.10",
|
|
56
|
+
"@iconify-json/simple-icons": "^1.2.89",
|
|
57
|
+
"@iconify-json/tabler": "^1.2.35",
|
|
58
|
+
"@shikijs/transformers": "^4.3.1",
|
|
59
|
+
"@tailwindcss/vite": "^4.3.2",
|
|
60
|
+
"astro": "^7.2.2",
|
|
61
|
+
"astro-icon": "^1.1.5",
|
|
62
|
+
"commander": "^15.0.0",
|
|
63
|
+
"dotenv": "^17.4.2",
|
|
64
|
+
"gray-matter": "^4.0.3",
|
|
65
|
+
"katex": "^0.16.47",
|
|
66
|
+
"mermaid": "^11.16.0",
|
|
67
|
+
"pagefind": "^1.5.2",
|
|
68
|
+
"react": "^19.2.8",
|
|
69
|
+
"react-dom": "^19.2.8",
|
|
70
|
+
"rehype-katex": "^7.0.1",
|
|
71
|
+
"remark-math": "^6.0.0",
|
|
72
|
+
"shiki": "^4.3.1",
|
|
73
|
+
"tailwindcss": "^4.3.2",
|
|
74
|
+
"zod": "^4.4.3"
|
|
75
|
+
},
|
|
76
|
+
"engines": {
|
|
77
|
+
"node": ">=20.3.0"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
Binary file
|
|
Binary file
|