reallysimpledocs 1.0.0-beta.1 → 1.0.0-beta.3
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/README.md +12 -4
- package/package.json +4 -3
- package/src/astro/index.js +81 -2
- package/src/components/index.js +9 -0
- package/src/css/custom.css +17 -12
- package/src/runtime/components/CommandDialog.astro +6 -6
- package/src/runtime/components/mdx/Callout.astro +41 -0
- package/src/runtime/components/mdx/Card.astro +14 -0
- package/src/runtime/components/mdx/CardGrid.astro +7 -0
- package/src/runtime/components/mdx/Code.astro +39 -0
- package/src/runtime/components/mdx/CodeGroup.astro +80 -0
- package/src/runtime/components/mdx/Preview.astro +67 -0
- package/src/runtime/components/mdx/Step.astro +8 -0
- package/src/runtime/components/mdx/Steps.astro +11 -0
- package/src/runtime/components/mdx/Tabs.astro +52 -0
- package/src/runtime/lib/docs.js +55 -27
- package/src/runtime/lib/highlight.js +27 -0
- package/src/runtime/lib/paths.js +8 -2
- package/src/runtime/pages/doc.astro +8 -3
- package/src/runtime/scripts/copy-code.js +59 -4
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A simple documentation system for Astro, built with **Basecoat** and **Tailwind CSS**.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
reallysimpledocs keeps docs portable: content lives in `docs/`, navigation lives in `docs/docs.json`, and the Astro integration owns the docs UI.
|
|
6
6
|
|
|
7
7
|
## Add to an Astro site
|
|
8
8
|
|
|
@@ -59,11 +59,19 @@ docs/
|
|
|
59
59
|
Welcome to the docs.
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
reallysimpledocs injects docs pages, Lunr search, per-page Markdown exports, `llms.txt`, and `llms-full.txt`.
|
|
63
|
+
|
|
64
|
+
Use `.mdx` when a page needs reallysimpledocs components. Common reallysimpledocs components are available by default, so you do not need to import them in each page:
|
|
65
|
+
|
|
66
|
+
```mdx
|
|
67
|
+
# Button
|
|
68
|
+
|
|
69
|
+
<Callout title="MDX works">MDX is optional. Plain Markdown stays the default.</Callout>
|
|
70
|
+
```
|
|
63
71
|
|
|
64
72
|
## Use Basecoat on the rest of the site
|
|
65
73
|
|
|
66
|
-
|
|
74
|
+
reallysimpledocs styles and scripts its generated docs pages. For custom Astro pages outside the docs route, use Basecoat directly in your own layout.
|
|
67
75
|
|
|
68
76
|
Install stays the same:
|
|
69
77
|
|
|
@@ -93,7 +101,7 @@ import "basecoat-css/all.min";
|
|
|
93
101
|
<slot />
|
|
94
102
|
```
|
|
95
103
|
|
|
96
|
-
Use the same Basecoat `style` in your
|
|
104
|
+
Use the same Basecoat `style` in your reallysimpledocs config and your site stylesheet if you want the docs and custom pages to match.
|
|
97
105
|
|
|
98
106
|
## Local development (this repo)
|
|
99
107
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reallysimpledocs",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"check": "npm run build && npm pack --dry-run && npm run smoke:consumer"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@astrojs/mdx": "^6.0.3",
|
|
23
24
|
"@tailwindcss/vite": "4.1.17",
|
|
24
25
|
"astro": "^6.4.6",
|
|
25
26
|
"github-slugger": "^2.0.0",
|
|
@@ -29,11 +30,11 @@
|
|
|
29
30
|
"shiki": "^4.1.0"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
|
-
"basecoat-css": "^1.0.0-beta.
|
|
33
|
+
"basecoat-css": "^1.0.0-beta.2",
|
|
33
34
|
"tailwindcss": ">=4.1.17 <5"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"basecoat-css": "
|
|
37
|
+
"basecoat-css": "1.0.0-beta.2",
|
|
37
38
|
"tailwindcss": "4.1.17"
|
|
38
39
|
},
|
|
39
40
|
"overrides": {
|
package/src/astro/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import mdx from "@astrojs/mdx";
|
|
4
5
|
import tailwindcss from "@tailwindcss/vite";
|
|
5
6
|
|
|
6
7
|
const basecoatStyles = new Set(["vega", "nova", "maia", "lyra", "mira", "luma", "sera", "rhea"]);
|
|
@@ -23,6 +24,41 @@ const cssPath = (relativePath) => fileURLToPath(new URL(`../css/${relativePath}`
|
|
|
23
24
|
|
|
24
25
|
const normalizeArray = (value) => (Array.isArray(value) ? value : value ? [value] : []);
|
|
25
26
|
|
|
27
|
+
const normalizeImportPath = (filePath) => filePath.replaceAll(path.sep, "/");
|
|
28
|
+
|
|
29
|
+
const isInsideDir = (dir, file) => {
|
|
30
|
+
const relative = path.relative(dir, file);
|
|
31
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const shikiConfig = {
|
|
35
|
+
themes: {
|
|
36
|
+
light: "github-light",
|
|
37
|
+
dark: "github-dark",
|
|
38
|
+
},
|
|
39
|
+
defaultColor: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function collectMdxModules(docsDir) {
|
|
43
|
+
if (!fs.existsSync(docsDir)) return {};
|
|
44
|
+
const modules = {};
|
|
45
|
+
const walk = (dir) => {
|
|
46
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
47
|
+
const fullPath = path.join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
walk(fullPath);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!entry.isFile() || !entry.name.endsWith(".mdx")) continue;
|
|
53
|
+
const relative = path.relative(docsDir, fullPath).replaceAll(path.sep, "/");
|
|
54
|
+
const slug = relative.replace(/\.mdx$/, "");
|
|
55
|
+
modules[slug] = normalizeImportPath(fullPath);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
walk(docsDir);
|
|
59
|
+
return modules;
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
export default function reallySimpleDocs(options = {}) {
|
|
27
63
|
const style = options.style || "vega";
|
|
28
64
|
if (!basecoatStyles.has(style)) {
|
|
@@ -47,10 +83,11 @@ export default function reallySimpleDocs(options = {}) {
|
|
|
47
83
|
const root = fileURLToPath(config.root);
|
|
48
84
|
const generatedDir = path.join(root, ".astro", "reallysimpledocs");
|
|
49
85
|
const generatedStyles = path.join(generatedDir, "styles.css");
|
|
86
|
+
const docsDir = path.resolve(root, normalizedOptions.docsDir);
|
|
50
87
|
const virtualConfig = {
|
|
51
88
|
...normalizedOptions,
|
|
52
89
|
root,
|
|
53
|
-
docsDir
|
|
90
|
+
docsDir,
|
|
54
91
|
siteFile: normalizedOptions.siteFile ? path.resolve(root, normalizedOptions.siteFile) : null,
|
|
55
92
|
routeBase: normalizeRouteBase(normalizedOptions.routeBase),
|
|
56
93
|
};
|
|
@@ -71,7 +108,7 @@ export default function reallySimpleDocs(options = {}) {
|
|
|
71
108
|
`@import ${JSON.stringify(cssPath("overrides.css"))};`,
|
|
72
109
|
...normalizedOptions.customCss.map((css) => `@import ${JSON.stringify(resolveUserId(css))};`),
|
|
73
110
|
`@source ${JSON.stringify(runtimePath("**/*.{astro,js,ts}"))};`,
|
|
74
|
-
`@source ${JSON.stringify(path.join(virtualConfig.docsDir, "**/*.md"))};`,
|
|
111
|
+
`@source ${JSON.stringify(path.join(virtualConfig.docsDir, "**/*.{md,mdx}"))};`,
|
|
75
112
|
"",
|
|
76
113
|
].join("\n"),
|
|
77
114
|
);
|
|
@@ -98,6 +135,15 @@ export default function reallySimpleDocs(options = {}) {
|
|
|
98
135
|
});
|
|
99
136
|
|
|
100
137
|
updateConfig({
|
|
138
|
+
integrations: [
|
|
139
|
+
mdx({
|
|
140
|
+
syntaxHighlight: "shiki",
|
|
141
|
+
shikiConfig,
|
|
142
|
+
}),
|
|
143
|
+
],
|
|
144
|
+
markdown: {
|
|
145
|
+
shikiConfig,
|
|
146
|
+
},
|
|
101
147
|
vite: {
|
|
102
148
|
build: {
|
|
103
149
|
cssMinify: false,
|
|
@@ -106,8 +152,24 @@ export default function reallySimpleDocs(options = {}) {
|
|
|
106
152
|
tailwindcss(),
|
|
107
153
|
{
|
|
108
154
|
name: "reallysimpledocs-virtual-modules",
|
|
155
|
+
configureServer(server) {
|
|
156
|
+
const invalidateMdxPages = () => {
|
|
157
|
+
const module = server.moduleGraph.getModuleById("\0virtual:reallysimpledocs/mdx-pages");
|
|
158
|
+
if (module) server.moduleGraph.invalidateModule(module);
|
|
159
|
+
server.ws.send({ type: "full-reload" });
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
server.watcher.add(path.join(docsDir, "**/*.mdx"));
|
|
163
|
+
server.watcher.on("add", (file) => {
|
|
164
|
+
if (file.endsWith(".mdx") && isInsideDir(docsDir, file)) invalidateMdxPages();
|
|
165
|
+
});
|
|
166
|
+
server.watcher.on("unlink", (file) => {
|
|
167
|
+
if (file.endsWith(".mdx") && isInsideDir(docsDir, file)) invalidateMdxPages();
|
|
168
|
+
});
|
|
169
|
+
},
|
|
109
170
|
resolveId(id) {
|
|
110
171
|
if (id === "virtual:reallysimpledocs/config") return "\0virtual:reallysimpledocs/config";
|
|
172
|
+
if (id === "virtual:reallysimpledocs/mdx-pages") return "\0virtual:reallysimpledocs/mdx-pages";
|
|
111
173
|
if (id === "virtual:reallysimpledocs/styles.css") return generatedStyles;
|
|
112
174
|
if (id === "virtual:reallysimpledocs/components/SidebarHeader") {
|
|
113
175
|
return "\0virtual:reallysimpledocs/components/SidebarHeader";
|
|
@@ -116,6 +178,23 @@ export default function reallySimpleDocs(options = {}) {
|
|
|
116
178
|
},
|
|
117
179
|
load(id) {
|
|
118
180
|
if (id === "\0virtual:reallysimpledocs/config") return `export default ${JSON.stringify(virtualConfig)};`;
|
|
181
|
+
if (id === "\0virtual:reallysimpledocs/mdx-pages") {
|
|
182
|
+
const mdxModules = collectMdxModules(docsDir);
|
|
183
|
+
const imports = Object.entries(mdxModules)
|
|
184
|
+
.map(([slug, filePath], index) => `import * as mdx${index} from ${JSON.stringify(filePath)};`)
|
|
185
|
+
.join("\n");
|
|
186
|
+
const moduleMap = Object.fromEntries(Object.keys(mdxModules).map((slug, index) => [slug, `mdx${index}`]));
|
|
187
|
+
return `${imports}
|
|
188
|
+
const modules = {
|
|
189
|
+
${Object.entries(moduleMap)
|
|
190
|
+
.map(([slug, name]) => ` ${JSON.stringify(slug)}: ${name},`)
|
|
191
|
+
.join("\n")}
|
|
192
|
+
};
|
|
193
|
+
export async function getMdxPage(slug) {
|
|
194
|
+
return modules[slug || "index"] || null;
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
119
198
|
if (id === "\0virtual:reallysimpledocs/components/SidebarHeader") {
|
|
120
199
|
return `export { default } from ${JSON.stringify(resolveUserId(components.SidebarHeader))};`;
|
|
121
200
|
}
|
package/src/components/index.js
CHANGED
|
@@ -2,3 +2,12 @@ export { default as DocsLayout } from "../runtime/components/DocsLayout.astro";
|
|
|
2
2
|
export { default as CommandDialog } from "../runtime/components/CommandDialog.astro";
|
|
3
3
|
export { default as DropdownMenu } from "../runtime/components/DropdownMenu.astro";
|
|
4
4
|
export { default as Sidebar } from "../runtime/components/Sidebar.astro";
|
|
5
|
+
export { default as Callout } from "../runtime/components/mdx/Callout.astro";
|
|
6
|
+
export { default as Card } from "../runtime/components/mdx/Card.astro";
|
|
7
|
+
export { default as CardGrid } from "../runtime/components/mdx/CardGrid.astro";
|
|
8
|
+
export { default as Code } from "../runtime/components/mdx/Code.astro";
|
|
9
|
+
export { default as CodeGroup } from "../runtime/components/mdx/CodeGroup.astro";
|
|
10
|
+
export { default as Preview } from "../runtime/components/mdx/Preview.astro";
|
|
11
|
+
export { default as Step } from "../runtime/components/mdx/Step.astro";
|
|
12
|
+
export { default as Steps } from "../runtime/components/mdx/Steps.astro";
|
|
13
|
+
export { default as Tabs } from "../runtime/components/mdx/Tabs.astro";
|
package/src/css/custom.css
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
> h3 {
|
|
21
21
|
@apply text-lg sm:text-xl lg:mt-8;
|
|
22
22
|
}
|
|
23
|
+
> h1:first-child {
|
|
24
|
+
@apply hidden;
|
|
25
|
+
}
|
|
23
26
|
p > a,
|
|
24
27
|
li > a,
|
|
25
28
|
dt > a,
|
|
@@ -38,10 +41,7 @@
|
|
|
38
41
|
@apply block min-w-full w-max px-4 py-3 text-[13px];
|
|
39
42
|
}
|
|
40
43
|
pre:has(> code) {
|
|
41
|
-
@apply
|
|
42
|
-
}
|
|
43
|
-
pre:has(> code) > button {
|
|
44
|
-
@apply flex items-center justify-center size-6 absolute right-2.5 top-2.5 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] rounded-md hover:bg-accent dark:hover:bg-accent/50 text-muted-foreground hover:text-foreground [&_svg]:size-4;
|
|
44
|
+
@apply max-w-full overflow-x-auto;
|
|
45
45
|
}
|
|
46
46
|
blockquote {
|
|
47
47
|
@apply border-l-4 border-border pl-4;
|
|
@@ -119,20 +119,25 @@
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
.shiki
|
|
123
|
-
|
|
122
|
+
.shiki,
|
|
123
|
+
.astro-code {
|
|
124
|
+
@apply bg-muted/50;
|
|
125
|
+
background-color: color-mix(in oklab, var(--color-muted) 50%, transparent) !important;
|
|
124
126
|
color: var(--shiki-light);
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
.shiki span
|
|
129
|
+
.shiki span,
|
|
130
|
+
.astro-code span {
|
|
128
131
|
color: var(--shiki-light);
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
html.dark .shiki
|
|
134
|
+
html.dark .shiki,
|
|
135
|
+
html.dark .astro-code {
|
|
132
136
|
color: var(--shiki-dark);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
html.dark .shiki span
|
|
139
|
+
html.dark .shiki span,
|
|
140
|
+
html.dark .astro-code span {
|
|
136
141
|
color: var(--shiki-dark);
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -140,15 +145,15 @@ html.dark .shiki span {
|
|
|
140
145
|
@apply sm:max-w-2xl;
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
|
|
148
|
+
[data-search] .command a[role='menuitem'] {
|
|
144
149
|
@apply flex-col items-start gap-1;
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
[data-search] .command a[role='menuitem'] > strong {
|
|
148
153
|
@apply font-medium;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
|
|
156
|
+
[data-search] .command a[role='menuitem'] > span {
|
|
152
157
|
@apply text-muted-foreground line-clamp-2 text-xs font-normal leading-snug;
|
|
153
158
|
}
|
|
154
159
|
|
|
@@ -4,7 +4,7 @@ import { renderCommandDialog } from "../lib/macros.js";
|
|
|
4
4
|
const { indexUrl } = Astro.props;
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<div data-
|
|
7
|
+
<div data-search data-index-url={indexUrl}>
|
|
8
8
|
<Fragment
|
|
9
9
|
set:html={renderCommandDialog({
|
|
10
10
|
id: "command-search",
|
|
@@ -18,7 +18,7 @@ const { indexUrl } = Astro.props;
|
|
|
18
18
|
<script>
|
|
19
19
|
import lunr from "lunr";
|
|
20
20
|
|
|
21
|
-
const root = document.querySelector("[data-
|
|
21
|
+
const root = document.querySelector("[data-search]");
|
|
22
22
|
const indexUrl = root?.getAttribute("data-index-url");
|
|
23
23
|
const dialog = document.getElementById("command-search");
|
|
24
24
|
const command = dialog?.querySelector(".command");
|
|
@@ -178,9 +178,9 @@ const { indexUrl } = Astro.props;
|
|
|
178
178
|
${items
|
|
179
179
|
.map((item, index) => {
|
|
180
180
|
const view = resultView(item, query);
|
|
181
|
-
return `<a id="command-search-result-${index}" role="menuitem" data-force
|
|
182
|
-
<
|
|
183
|
-
${view.snippet ? `<span
|
|
181
|
+
return `<a id="command-search-result-${index}" role="menuitem" data-force href="${escapeHtml(view.path)}">
|
|
182
|
+
<strong>${view.title}</strong>
|
|
183
|
+
${view.snippet ? `<span>${view.snippet}</span>` : ""}
|
|
184
184
|
</a>`;
|
|
185
185
|
})
|
|
186
186
|
.join("")}
|
|
@@ -199,7 +199,7 @@ const { indexUrl } = Astro.props;
|
|
|
199
199
|
|
|
200
200
|
async function loadIndex() {
|
|
201
201
|
if (state.index) return state;
|
|
202
|
-
if (!indexUrl) throw new Error("Missing
|
|
202
|
+
if (!indexUrl) throw new Error("Missing reallysimpledocs search index URL.");
|
|
203
203
|
if (!state.loadPromise) {
|
|
204
204
|
renderLoading();
|
|
205
205
|
state.loadPromise = fetch(indexUrl)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
import * as lucideIcons from "lucide-static";
|
|
3
|
+
import { addSvgClass } from "../../lib/html.js";
|
|
4
|
+
|
|
5
|
+
const { type, title = "Note", icon: iconName, class: className = "" } = Astro.props;
|
|
6
|
+
const normalizedType = String(type || "").toLowerCase();
|
|
7
|
+
const calloutType = normalizedType === "warning" || normalizedType === "danger" ? normalizedType : "";
|
|
8
|
+
const danger = calloutType === "danger";
|
|
9
|
+
|
|
10
|
+
function resolveNamedIcon(name) {
|
|
11
|
+
if (!name || typeof name !== "string") return "";
|
|
12
|
+
const trimmed = name.trim();
|
|
13
|
+
if (!trimmed) return "";
|
|
14
|
+
if (trimmed.startsWith("<svg")) return trimmed;
|
|
15
|
+
const pascalName = trimmed.includes("-")
|
|
16
|
+
? trimmed
|
|
17
|
+
.split("-")
|
|
18
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
19
|
+
.join("")
|
|
20
|
+
: trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
|
21
|
+
return lucideIcons[pascalName] || "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const iconSource = typeof iconName === "string" ? resolveNamedIcon(iconName) : "";
|
|
25
|
+
const icon = addSvgClass(iconSource, "size-4");
|
|
26
|
+
const heading = title;
|
|
27
|
+
const variantClass =
|
|
28
|
+
calloutType === "warning"
|
|
29
|
+
? "[&>svg]:text-amber-700 [&>h3]:text-amber-700 [&>section]:text-amber-700 dark:[&>svg]:text-amber-300 dark:[&>h3]:text-amber-300 dark:[&>section]:text-amber-300"
|
|
30
|
+
: danger
|
|
31
|
+
? "[&>svg]:text-destructive [&>h3]:text-destructive [&>section]:text-destructive"
|
|
32
|
+
: "";
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<div class:list={[danger ? "alert-destructive" : "alert", "my-6", variantClass, className]}>
|
|
36
|
+
{icon && <Fragment set:html={icon} />}
|
|
37
|
+
<h3>{heading}</h3>
|
|
38
|
+
<section>
|
|
39
|
+
<slot />
|
|
40
|
+
</section>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { title, description, href, class: className = "" } = Astro.props;
|
|
3
|
+
const Tag = href ? "a" : "article";
|
|
4
|
+
const classes = [
|
|
5
|
+
"my-6 block w-full rounded-lg border bg-card p-3 text-card-foreground no-underline",
|
|
6
|
+
href && "transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
7
|
+
className,
|
|
8
|
+
];
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<Tag href={href} class:list={classes}>
|
|
12
|
+
{title && <h3 class="font-medium">{title}</h3>}
|
|
13
|
+
{description && <p class="mt-1 text-sm text-muted-foreground">{description}</p>}
|
|
14
|
+
</Tag>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { highlightCode } from "../../lib/highlight.js";
|
|
3
|
+
|
|
4
|
+
const { lang = "text", title, class: className = "" } = Astro.props;
|
|
5
|
+
const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
|
|
6
|
+
const rawCode = decodeSlotText(slotHtml);
|
|
7
|
+
const html = await highlightCode(rawCode, lang);
|
|
8
|
+
const classes = [
|
|
9
|
+
"relative my-6 w-full max-w-full overflow-hidden rounded-lg",
|
|
10
|
+
className,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function decodeSlotText(value) {
|
|
14
|
+
return String(value || "")
|
|
15
|
+
.replace(/^<p>/, "")
|
|
16
|
+
.replace(/<\/p>$/, "")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">")
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, "'")
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
data-code-block
|
|
28
|
+
data-code-title={title || undefined}
|
|
29
|
+
class:list={classes}
|
|
30
|
+
>
|
|
31
|
+
{
|
|
32
|
+
title && (
|
|
33
|
+
<header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
|
|
34
|
+
<span class="mr-auto">{title}</span>
|
|
35
|
+
</header>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
<Fragment set:html={html} />
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { defaultTab, class: className = "" } = Astro.props;
|
|
3
|
+
const id = `code-group-${Math.random().toString(36).slice(2)}`;
|
|
4
|
+
const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
|
|
5
|
+
const normalizedTabs = parseCodeTabs(slotHtml);
|
|
6
|
+
const defaultValue = defaultTab || normalizedTabs[0]?.value;
|
|
7
|
+
const classes = [
|
|
8
|
+
"relative my-6 w-full max-w-full overflow-hidden rounded-lg [&>[role=tabpanel]]:border-t-0 [&_[data-code-block]]:my-0 [&_[data-code-block]]:min-w-0 [&_[data-code-block]]:rounded-none [&_[data-code-block]>header]:hidden",
|
|
9
|
+
className,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function parseCodeTabs(html) {
|
|
13
|
+
const blocks = [];
|
|
14
|
+
const pattern =
|
|
15
|
+
/<div\b(?=[^>]*\bdata-code-block\b)(?=[^>]*\bdata-code-title="([^"]*)")[^>]*>([\s\S]*?)<\/div>/g;
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = pattern.exec(html))) {
|
|
18
|
+
const label = decodeHtml(match[1]).trim();
|
|
19
|
+
if (!label) continue;
|
|
20
|
+
blocks.push({
|
|
21
|
+
label,
|
|
22
|
+
value: label,
|
|
23
|
+
html: match[0],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return blocks;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function decodeHtml(value) {
|
|
30
|
+
return String(value || "")
|
|
31
|
+
.replace(/</g, "<")
|
|
32
|
+
.replace(/>/g, ">")
|
|
33
|
+
.replace(/&/g, "&")
|
|
34
|
+
.replace(/"/g, '"')
|
|
35
|
+
.replace(/'/g, "'");
|
|
36
|
+
}
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<div class:list={classes} data-code-group>
|
|
40
|
+
<header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
|
|
41
|
+
<div class="tabs my-0 min-w-0 max-w-full gap-0">
|
|
42
|
+
<nav class="h-full justify-start rounded-none bg-transparent p-0" role="tablist" aria-orientation="horizontal" data-variant="line">
|
|
43
|
+
{
|
|
44
|
+
normalizedTabs.map((tab, index) => {
|
|
45
|
+
const selected = tab.value === defaultValue;
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
role="tab"
|
|
50
|
+
id={`${id}-tab-${index + 1}`}
|
|
51
|
+
aria-controls={`${id}-panel-${index + 1}`}
|
|
52
|
+
aria-selected={selected ? "true" : "false"}
|
|
53
|
+
tabindex={selected ? "0" : "-1"}
|
|
54
|
+
>
|
|
55
|
+
{tab.label}
|
|
56
|
+
</button>
|
|
57
|
+
);
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
</nav>
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
{
|
|
64
|
+
normalizedTabs.map((tab, index) => {
|
|
65
|
+
const selected = tab.value === defaultValue;
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
role="tabpanel"
|
|
69
|
+
id={`${id}-panel-${index + 1}`}
|
|
70
|
+
aria-labelledby={`${id}-tab-${index + 1}`}
|
|
71
|
+
tabindex="-1"
|
|
72
|
+
aria-selected={selected ? "true" : "false"}
|
|
73
|
+
hidden={!selected}
|
|
74
|
+
>
|
|
75
|
+
<Fragment set:html={tab.html} />
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { highlightCode } from "../../lib/highlight.js";
|
|
3
|
+
|
|
4
|
+
const { code = "", lang = "html", title = "Preview", class: className = "" } = Astro.props;
|
|
5
|
+
const id = `preview-${Math.random().toString(36).slice(2)}`;
|
|
6
|
+
const html = await highlightCode(code, lang);
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<section
|
|
10
|
+
class="relative my-6 max-w-full overflow-hidden rounded-lg border [&>[role=tabpanel]]:border-t-0 [&>[data-code-panel]_[data-code-block]]:my-0 [&>[data-code-panel]_[data-code-block]]:min-w-0 [&>[data-code-panel]_[data-code-block]]:rounded-none [&>[data-preview-panel]>div]:flex [&>[data-preview-panel]>div]:min-h-64 [&>[data-preview-panel]>div]:w-full [&>[data-preview-panel]>div]:items-center [&>[data-preview-panel]>div]:justify-center [&>[data-preview-panel]>div]:p-6"
|
|
11
|
+
data-preview
|
|
12
|
+
>
|
|
13
|
+
<header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
|
|
14
|
+
<div class="tabs my-0 min-w-0 max-w-full gap-0">
|
|
15
|
+
<nav class="h-full justify-start rounded-none bg-transparent p-0" role="tablist" aria-orientation="horizontal" data-variant="line">
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
role="tab"
|
|
19
|
+
id={`${id}-preview-tab`}
|
|
20
|
+
aria-controls={`${id}-preview-panel`}
|
|
21
|
+
aria-selected="true"
|
|
22
|
+
tabindex="0"
|
|
23
|
+
>
|
|
24
|
+
{title}
|
|
25
|
+
</button>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
role="tab"
|
|
29
|
+
id={`${id}-code-tab`}
|
|
30
|
+
aria-controls={`${id}-code-panel`}
|
|
31
|
+
aria-selected="false"
|
|
32
|
+
tabindex="-1"
|
|
33
|
+
>
|
|
34
|
+
Code
|
|
35
|
+
</button>
|
|
36
|
+
</nav>
|
|
37
|
+
</div>
|
|
38
|
+
</header>
|
|
39
|
+
<section
|
|
40
|
+
role="tabpanel"
|
|
41
|
+
data-preview-panel
|
|
42
|
+
id={`${id}-preview-panel`}
|
|
43
|
+
aria-labelledby={`${id}-preview-tab`}
|
|
44
|
+
tabindex="-1"
|
|
45
|
+
aria-selected="true"
|
|
46
|
+
>
|
|
47
|
+
<div class={className}>
|
|
48
|
+
<slot />
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
<section
|
|
52
|
+
role="tabpanel"
|
|
53
|
+
data-code-panel
|
|
54
|
+
id={`${id}-code-panel`}
|
|
55
|
+
aria-labelledby={`${id}-code-tab`}
|
|
56
|
+
tabindex="-1"
|
|
57
|
+
aria-selected="false"
|
|
58
|
+
hidden
|
|
59
|
+
>
|
|
60
|
+
<div
|
|
61
|
+
class="relative my-6 w-full max-w-full overflow-hidden rounded-lg"
|
|
62
|
+
data-code-block
|
|
63
|
+
>
|
|
64
|
+
<Fragment set:html={html} />
|
|
65
|
+
</div>
|
|
66
|
+
</section>
|
|
67
|
+
</section>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { class: className = "" } = Astro.props;
|
|
3
|
+
const classes = [
|
|
4
|
+
"mb-12 mt-6 [counter-reset:step] md:ml-4 md:border-l md:pl-8 [&>section]:mb-10 [&>section]:[counter-increment:step] [&>section:last-child]:mb-0 [&>section>*+*]:mt-6 [&>section>*]:leading-relaxed [&>section>*:first-child]:mt-0 [&>section>*:last-child]:mb-0 [&>section>h3+*]:mt-4 [&>section>h3]:relative [&>section>h3]:scroll-m-28 [&>section>h3]:text-lg [&>section>h3]:font-medium [&>section>h3]:tracking-tight [&>section>h3]:before:mr-4 [&>section>h3]:before:inline-flex [&>section>h3]:before:size-7 [&>section>h3]:before:items-center [&>section>h3]:before:justify-center [&>section>h3]:before:rounded-full [&>section>h3]:before:bg-muted [&>section>h3]:before:text-sm [&>section>h3]:before:font-medium [&>section>h3]:before:text-muted-foreground [&>section>h3]:before:content-[counter(step)] md:[&>section>h3]:before:absolute md:[&>section>h3]:before:left-0 md:[&>section>h3]:before:top-0 md:[&>section>h3]:before:mr-0 md:[&>section>h3]:before:-translate-x-[2.875rem]",
|
|
5
|
+
className,
|
|
6
|
+
];
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div role="list" class:list={classes}>
|
|
10
|
+
<slot />
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { tabs = [], defaultTab, orientation = "horizontal", variant, class: className = "" } = Astro.props;
|
|
3
|
+
const id = `tabs-${Math.random().toString(36).slice(2)}`;
|
|
4
|
+
const normalizedTabs = tabs.map((tab, index) => ({
|
|
5
|
+
...tab,
|
|
6
|
+
slot: tab.slot || String(index),
|
|
7
|
+
value: tab.value || tab.slot || String(index),
|
|
8
|
+
}));
|
|
9
|
+
const defaultValue = defaultTab || normalizedTabs.find((tab) => !tab.disabled)?.value || normalizedTabs[0]?.value;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class:list={["tabs my-6", className]}>
|
|
13
|
+
<nav role="tablist" aria-orientation={orientation} data-variant={variant}>
|
|
14
|
+
{
|
|
15
|
+
normalizedTabs.map((tab, index) => {
|
|
16
|
+
const selected = tab.value === defaultValue;
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
role="tab"
|
|
21
|
+
id={`${id}-tab-${index + 1}`}
|
|
22
|
+
aria-controls={`${id}-panel-${index + 1}`}
|
|
23
|
+
aria-selected={selected ? "true" : "false"}
|
|
24
|
+
tabindex={selected && !tab.disabled ? "0" : "-1"}
|
|
25
|
+
disabled={tab.disabled}
|
|
26
|
+
>
|
|
27
|
+
{tab.label}
|
|
28
|
+
</button>
|
|
29
|
+
);
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
</nav>
|
|
33
|
+
{
|
|
34
|
+
await Promise.all(
|
|
35
|
+
normalizedTabs.map(async (tab, index) => {
|
|
36
|
+
const selected = tab.value === defaultValue;
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
role="tabpanel"
|
|
40
|
+
id={`${id}-panel-${index + 1}`}
|
|
41
|
+
aria-labelledby={`${id}-tab-${index + 1}`}
|
|
42
|
+
tabindex="-1"
|
|
43
|
+
aria-selected={selected ? "true" : "false"}
|
|
44
|
+
hidden={!selected}
|
|
45
|
+
>
|
|
46
|
+
<Fragment set:html={await Astro.slots.render(tab.slot)} />
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
</div>
|
package/src/runtime/lib/docs.js
CHANGED
|
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { marked } from "marked";
|
|
4
4
|
import GithubSlugger from "github-slugger";
|
|
5
|
-
import { codeToHtml } from "shiki";
|
|
6
5
|
import * as lucideIcons from "lucide-static";
|
|
6
|
+
import { highlightCode as renderHighlightedCode } from "./highlight.js";
|
|
7
7
|
import { markdownPath, routePath } from "./paths.js";
|
|
8
8
|
|
|
9
9
|
export function getManifest(config) {
|
|
@@ -19,7 +19,16 @@ export const fallbackLabelFromSlug = (slug) => {
|
|
|
19
19
|
return base.replace(/-/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
export const slugToFile = (slug) => path.join(...String(slug).split("/")) +
|
|
22
|
+
export const slugToFile = (slug, extension = ".md") => path.join(...String(slug).split("/")) + extension;
|
|
23
|
+
|
|
24
|
+
export function resolveDocFile(config, slug) {
|
|
25
|
+
for (const extension of [".md", ".mdx"]) {
|
|
26
|
+
const file = slugToFile(slug, extension);
|
|
27
|
+
const filePath = path.join(config.docsDir, file);
|
|
28
|
+
if (fs.existsSync(filePath)) return { file, filePath, extension };
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Missing docs page for slug "${slug}". Expected .md or .mdx in ${config.docsDir}.`);
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
export function flattenMenuSlugs(menu) {
|
|
25
34
|
const out = [];
|
|
@@ -44,13 +53,13 @@ export function flattenMenuSlugs(menu) {
|
|
|
44
53
|
export function getPages(config) {
|
|
45
54
|
const manifest = getManifest(config);
|
|
46
55
|
return flattenMenuSlugs(manifest.menu || []).map((slug) => {
|
|
47
|
-
const file =
|
|
48
|
-
const filePath = path.join(config.docsDir, file);
|
|
56
|
+
const { file, filePath, extension } = resolveDocFile(config, slug);
|
|
49
57
|
const source = fs.readFileSync(filePath, "utf8");
|
|
50
58
|
const parsed = parseDocMarkdown(source, fallbackLabelFromSlug(slug));
|
|
51
59
|
return {
|
|
52
60
|
slug,
|
|
53
61
|
file,
|
|
62
|
+
extension,
|
|
54
63
|
path: routePath(config.routeBase, slug),
|
|
55
64
|
markdownPath: markdownPath(config.routeBase, slug),
|
|
56
65
|
title: parsed.title,
|
|
@@ -108,11 +117,25 @@ export async function renderDoc(config, page) {
|
|
|
108
117
|
};
|
|
109
118
|
|
|
110
119
|
const tokens = marked.lexer(content);
|
|
111
|
-
await
|
|
120
|
+
await highlightMarkdownCode(tokens);
|
|
112
121
|
const html = marked.parser(tokens, { renderer });
|
|
113
122
|
return { page, html, headings };
|
|
114
123
|
}
|
|
115
124
|
|
|
125
|
+
export function getDocHeadings(config, page) {
|
|
126
|
+
const parsed = parseDocMarkdown(getDocMarkdown(config, page), page.title);
|
|
127
|
+
const headings = [];
|
|
128
|
+
const slugger = new GithubSlugger();
|
|
129
|
+
const tokens = marked.lexer(parsed.body);
|
|
130
|
+
tokens.forEach((token) => {
|
|
131
|
+
if (token.type !== "heading" || token.depth <= 1 || token.depth >= 4) return;
|
|
132
|
+
const text = plainText(token.tokens || []).trim();
|
|
133
|
+
if (!text) return;
|
|
134
|
+
headings.push({ depth: token.depth, id: slugger.slug(text), text });
|
|
135
|
+
});
|
|
136
|
+
return headings;
|
|
137
|
+
}
|
|
138
|
+
|
|
116
139
|
export function renderNunjucksCompat(content) {
|
|
117
140
|
return String(content).replace(
|
|
118
141
|
/\{%\s*lucide\s+["']([^"']+)["'](?:\s*,\s*[^%]+)?\s*%\}/g,
|
|
@@ -141,7 +164,9 @@ export function resolveIcon(icon) {
|
|
|
141
164
|
.join("")
|
|
142
165
|
: trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
|
143
166
|
const iconClass = trimmed.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
144
|
-
|
|
167
|
+
const svg = lucideIcons[pascalName] || "";
|
|
168
|
+
if (!svg || svg.includes(`lucide-${iconClass}`)) return svg;
|
|
169
|
+
return svg.replace('class="lucide', `class="lucide lucide-${iconClass}`);
|
|
145
170
|
}
|
|
146
171
|
|
|
147
172
|
export function buildMenus(config, currentSlug) {
|
|
@@ -156,7 +181,7 @@ export function buildMenus(config, currentSlug) {
|
|
|
156
181
|
type: "item",
|
|
157
182
|
icon,
|
|
158
183
|
url: page.path,
|
|
159
|
-
label: page.title,
|
|
184
|
+
label: item.label || page.title,
|
|
160
185
|
current: page.slug === currentSlug,
|
|
161
186
|
keywords: page.title,
|
|
162
187
|
};
|
|
@@ -231,7 +256,7 @@ export function getPageItemSlug(item) {
|
|
|
231
256
|
}
|
|
232
257
|
|
|
233
258
|
function parseDocMarkdown(source, fallbackTitle) {
|
|
234
|
-
const content =
|
|
259
|
+
const content = normalizeDocSource(source);
|
|
235
260
|
const tokens = marked.lexer(content);
|
|
236
261
|
const first = tokens[0];
|
|
237
262
|
|
|
@@ -249,10 +274,21 @@ function parseDocMarkdown(source, fallbackTitle) {
|
|
|
249
274
|
}
|
|
250
275
|
|
|
251
276
|
function markdownPlainText(markdown) {
|
|
252
|
-
return tokensText(marked.lexer(renderNunjucksCompat(markdown)));
|
|
277
|
+
return tokensText(marked.lexer(renderNunjucksCompat(normalizeDocSource(markdown))));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeDocSource(source) {
|
|
281
|
+
const withoutFrontmatter = String(source || "")
|
|
282
|
+
.trimStart()
|
|
283
|
+
.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
|
|
284
|
+
const firstHeading = withoutFrontmatter.search(/^#\s+/m);
|
|
285
|
+
if (firstHeading > 0) return withoutFrontmatter.slice(firstHeading).trimStart();
|
|
286
|
+
return withoutFrontmatter
|
|
287
|
+
.replace(/^(?:[ \t]*(?:import|export)\s+[^\n]*\n)+/, "")
|
|
288
|
+
.trimStart();
|
|
253
289
|
}
|
|
254
290
|
|
|
255
|
-
async function
|
|
291
|
+
async function highlightMarkdownCode(tokens) {
|
|
256
292
|
for (const token of tokens) {
|
|
257
293
|
if (token.type === "code") {
|
|
258
294
|
const html = await highlightToken(token);
|
|
@@ -266,36 +302,28 @@ async function highlightCode(tokens) {
|
|
|
266
302
|
}
|
|
267
303
|
|
|
268
304
|
for (const childTokens of nestedTokenLists(token)) {
|
|
269
|
-
await
|
|
305
|
+
await highlightMarkdownCode(childTokens);
|
|
270
306
|
}
|
|
271
307
|
}
|
|
272
308
|
}
|
|
273
309
|
|
|
274
310
|
async function highlightToken(token) {
|
|
311
|
+
const lang = normalizeLanguage(token.lang);
|
|
275
312
|
try {
|
|
276
|
-
return await
|
|
277
|
-
lang: normalizeLanguage(token.lang),
|
|
278
|
-
themes: {
|
|
279
|
-
light: "github-light",
|
|
280
|
-
dark: "github-dark",
|
|
281
|
-
},
|
|
282
|
-
defaultColor: false,
|
|
283
|
-
});
|
|
313
|
+
return wrapCodeBlock(await renderHighlightedCode(token.text, lang));
|
|
284
314
|
} catch {
|
|
285
|
-
return
|
|
286
|
-
lang: "text",
|
|
287
|
-
themes: {
|
|
288
|
-
light: "github-light",
|
|
289
|
-
dark: "github-dark",
|
|
290
|
-
},
|
|
291
|
-
defaultColor: false,
|
|
292
|
-
});
|
|
315
|
+
return wrapCodeBlock(await renderHighlightedCode(token.text, "text"));
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
|
|
319
|
+
function wrapCodeBlock(html) {
|
|
320
|
+
return `<div class="relative my-6 w-full max-w-full overflow-hidden rounded-lg" data-code-block>${html}</div>`;
|
|
321
|
+
}
|
|
322
|
+
|
|
296
323
|
function normalizeLanguage(lang) {
|
|
297
324
|
const value = lang?.trim().split(/\s+/)[0].toLowerCase() || "text";
|
|
298
325
|
if (value === "njk") return "html";
|
|
326
|
+
if (value === "mdx") return "tsx";
|
|
299
327
|
if (value === "sh" || value === "shell") return "bash";
|
|
300
328
|
return value;
|
|
301
329
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { codeToHtml } from "shiki";
|
|
2
|
+
|
|
3
|
+
export async function highlightCode(code, lang = "text") {
|
|
4
|
+
const value = String(code || "");
|
|
5
|
+
const language = String(lang || "text").toLowerCase() === "mdx" ? "tsx" : String(lang || "text");
|
|
6
|
+
const html = await codeToHtml(value, {
|
|
7
|
+
lang: language,
|
|
8
|
+
themes: {
|
|
9
|
+
light: "github-light",
|
|
10
|
+
dark: "github-dark",
|
|
11
|
+
},
|
|
12
|
+
defaultColor: false,
|
|
13
|
+
});
|
|
14
|
+
return addCodeLayoutClasses(html);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function addCodeLayoutClasses(html) {
|
|
18
|
+
return String(html)
|
|
19
|
+
.replace(
|
|
20
|
+
/<pre class="/,
|
|
21
|
+
'<pre class="!my-0 max-w-full overflow-x-auto !rounded-none !border-0 ',
|
|
22
|
+
)
|
|
23
|
+
.replace(
|
|
24
|
+
/<code>/,
|
|
25
|
+
'<code class="block min-w-full w-max px-4 py-3 text-[13px]">',
|
|
26
|
+
);
|
|
27
|
+
}
|
package/src/runtime/lib/paths.js
CHANGED
|
@@ -2,12 +2,18 @@ export const stripSlashes = (value) => String(value || "").replace(/^\/+|\/+$/g,
|
|
|
2
2
|
|
|
3
3
|
export const routePath = (routeBase, slug) => {
|
|
4
4
|
const base = stripSlashes(routeBase);
|
|
5
|
-
const
|
|
5
|
+
const strippedSlug = stripSlashes(slug);
|
|
6
|
+
const normalizedSlug =
|
|
7
|
+
strippedSlug === "index" ? "" : strippedSlug.endsWith("/index") ? strippedSlug.slice(0, -"/index".length) : strippedSlug;
|
|
6
8
|
const parts = [base, normalizedSlug].filter(Boolean);
|
|
7
9
|
return parts.length ? `/${parts.join("/")}/` : "/";
|
|
8
10
|
};
|
|
9
11
|
|
|
10
|
-
export const routeSlug = (slug) =>
|
|
12
|
+
export const routeSlug = (slug) => {
|
|
13
|
+
const strippedSlug = stripSlashes(slug);
|
|
14
|
+
if (strippedSlug === "index") return undefined;
|
|
15
|
+
return strippedSlug.endsWith("/index") ? strippedSlug.slice(0, -"/index".length) : strippedSlug;
|
|
16
|
+
};
|
|
11
17
|
|
|
12
18
|
export const markdownPath = (routeBase, slug) => {
|
|
13
19
|
const base = stripSlashes(routeBase);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
import config from "virtual:reallysimpledocs/config";
|
|
3
|
+
import { getMdxPage } from "virtual:reallysimpledocs/mdx-pages";
|
|
4
|
+
import * as RsdComponents from "../../components/index.js";
|
|
3
5
|
import DocsLayout from "../components/DocsLayout.astro";
|
|
4
|
-
import { getPages, renderDoc } from "../lib/docs.js";
|
|
6
|
+
import { getDocHeadings, getPages, renderDoc } from "../lib/docs.js";
|
|
5
7
|
import { routeSlug } from "../lib/paths.js";
|
|
6
8
|
|
|
7
9
|
export function getStaticPaths() {
|
|
@@ -12,9 +14,12 @@ export function getStaticPaths() {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const { page } = Astro.props;
|
|
15
|
-
const
|
|
17
|
+
const isMdx = page.extension === ".mdx";
|
|
18
|
+
const mdxModule = isMdx ? await getMdxPage(page.slug) : null;
|
|
19
|
+
const MdxContent = mdxModule?.Content || mdxModule?.default;
|
|
20
|
+
const doc = isMdx ? { page, headings: getDocHeadings(config, page), html: "" } : await renderDoc(config, page);
|
|
16
21
|
---
|
|
17
22
|
|
|
18
23
|
<DocsLayout page={page} headings={doc.headings}>
|
|
19
|
-
<Fragment set:html={doc.html} />
|
|
24
|
+
{MdxContent ? <MdxContent components={RsdComponents} /> : <Fragment set:html={doc.html} />}
|
|
20
25
|
</DocsLayout>
|
|
@@ -19,18 +19,63 @@ const copyText = async (text) => {
|
|
|
19
19
|
if (!ok) throw new Error("Copy failed");
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
const activePanelCode = (root) => root.querySelector(':scope > [role="tabpanel"]:not([hidden]) code');
|
|
23
|
+
const codeFrameClass = "relative my-6 w-full max-w-full overflow-hidden rounded-lg";
|
|
24
|
+
const codePreClasses = ["!my-0", "max-w-full", "overflow-x-auto", "!rounded-none", "!border-0"];
|
|
25
|
+
const codeContentClasses = ["block", "min-w-full", "w-max", "px-4", "py-3", "text-[13px]"];
|
|
26
|
+
const copyButtonClass =
|
|
27
|
+
"flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:hover:bg-accent/50 [&_svg]:size-4";
|
|
28
|
+
|
|
22
29
|
const initCopyCode = (codeBlock) => {
|
|
23
30
|
const codeEl = codeBlock.querySelector(":scope > code");
|
|
24
31
|
if (!codeEl) return;
|
|
25
32
|
|
|
33
|
+
codeBlock.classList.add(...codePreClasses);
|
|
34
|
+
codeEl.classList.add(...codeContentClasses);
|
|
35
|
+
|
|
36
|
+
let target = null;
|
|
37
|
+
let frame = codeBlock.closest("[data-code-group]");
|
|
38
|
+
let getCode = frame ? () => activePanelCode(frame) || codeEl : () => codeEl;
|
|
39
|
+
if (frame) target = frame.querySelector(":scope > header");
|
|
40
|
+
|
|
41
|
+
const preview = codeBlock.closest("[data-preview]");
|
|
42
|
+
const previewCodePanel = preview?.querySelector(":scope > [data-code-panel]");
|
|
43
|
+
if (!frame && previewCodePanel?.contains(codeBlock)) {
|
|
44
|
+
frame = preview;
|
|
45
|
+
target = frame.querySelector(":scope > header");
|
|
46
|
+
getCode = () => codeEl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!frame) {
|
|
50
|
+
frame = codeBlock.parentElement?.hasAttribute("data-code-block") ? codeBlock.parentElement : null;
|
|
51
|
+
if (!frame) {
|
|
52
|
+
frame = document.createElement("div");
|
|
53
|
+
frame.dataset.codeBlock = "";
|
|
54
|
+
frame.className = codeFrameClass;
|
|
55
|
+
codeBlock.parentNode.insertBefore(frame, codeBlock);
|
|
56
|
+
frame.appendChild(codeBlock);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
target ||= frame.querySelector(":scope > header") || frame;
|
|
61
|
+
|
|
62
|
+
if (target.querySelector(":scope > button[aria-label='Copy code']")) {
|
|
63
|
+
codeBlock.dataset.copyCodeInitialized = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
const button = document.createElement("button");
|
|
27
68
|
button.type = "button";
|
|
28
|
-
button.className = "code-block-copy";
|
|
29
69
|
button.setAttribute("aria-label", "Copy code");
|
|
70
|
+
button.className = `${copyButtonClass} ${
|
|
71
|
+
target === frame
|
|
72
|
+
? "absolute right-2.5 top-2.5 bg-muted/50 shadow-sm backdrop-blur-sm"
|
|
73
|
+
: "ml-auto"
|
|
74
|
+
}`;
|
|
30
75
|
button.innerHTML = Copy;
|
|
31
76
|
|
|
32
77
|
button.addEventListener("click", async () => {
|
|
33
|
-
const text = (
|
|
78
|
+
const text = (getCode()?.textContent || "").replace(/\n$/, "");
|
|
34
79
|
if (!text) return;
|
|
35
80
|
|
|
36
81
|
button.disabled = true;
|
|
@@ -47,8 +92,18 @@ const initCopyCode = (codeBlock) => {
|
|
|
47
92
|
}, 1200);
|
|
48
93
|
});
|
|
49
94
|
|
|
50
|
-
|
|
95
|
+
target.appendChild(button);
|
|
96
|
+
if (previewCodePanel?.contains(codeBlock)) {
|
|
97
|
+
const syncPreviewCopyButton = () => {
|
|
98
|
+
button.hidden = previewCodePanel.hidden;
|
|
99
|
+
};
|
|
100
|
+
syncPreviewCopyButton();
|
|
101
|
+
new MutationObserver(syncPreviewCopyButton).observe(previewCodePanel, {
|
|
102
|
+
attributes: true,
|
|
103
|
+
attributeFilter: ["hidden"],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
51
106
|
codeBlock.dataset.copyCodeInitialized = true;
|
|
52
107
|
};
|
|
53
108
|
|
|
54
|
-
window.basecoat?.register("copy-code", "pre:not([data-copy-code-initialized])", initCopyCode);
|
|
109
|
+
window.basecoat?.register("copy-code", "pre:has(> code):not([data-copy-code-initialized])", initCopyCode);
|