reallysimpledocs 1.0.0-beta.5 → 1.0.1
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 +52 -32
- package/package.json +8 -4
- package/src/astro/index.js +18 -0
- package/src/astro/preview-scripts.js +140 -0
- package/src/astro/preview-source.js +68 -8
- package/src/astro/rehype-basecoat-tables.js +62 -0
- package/src/css/custom.css +49 -129
- package/src/css/styles/luma.css +4 -6
- package/src/css/styles/lyra.css +4 -6
- package/src/css/styles/maia.css +4 -6
- package/src/css/styles/mira.css +4 -6
- package/src/css/styles/nova.css +4 -6
- package/src/css/styles/rhea.css +4 -6
- package/src/css/styles/sera.css +4 -6
- package/src/css/styles/vega.css +4 -6
- package/src/runtime/components/BaseLayout.astro +20 -19
- package/src/runtime/components/CommandDialog.astro +24 -5
- package/src/runtime/components/DefaultHead.astro +20 -30
- package/src/runtime/components/DocsLayout.astro +83 -39
- package/src/runtime/components/ThemeToggle.astro +1 -1
- package/src/runtime/components/mdx/Code.astro +22 -14
- package/src/runtime/components/mdx/CodeGroup.astro +109 -15
- package/src/runtime/components/mdx/Preview.astro +1 -1
- package/src/runtime/lib/code-meta.js +8 -0
- package/src/runtime/lib/docs.js +11 -17
- package/src/runtime/lib/html.js +9 -0
- package/src/runtime/lib/markdown-export.js +111 -3
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# ReallySimpleDocs
|
|
2
2
|
|
|
3
|
-
A simple documentation system for Astro, built with
|
|
3
|
+
A simple documentation system for Astro, built with Basecoat and Tailwind CSS.
|
|
4
4
|
|
|
5
|
-
ReallySimpleDocs keeps docs portable: content lives in `docs/`, navigation lives in `docs/docs.json`, and the Astro integration owns the docs UI.
|
|
5
|
+
ReallySimpleDocs keeps docs portable: content lives in `docs/`, navigation lives in `docs/docs.json`, and the Astro integration owns the docs UI, search, Markdown exports, and LLM files.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Add ReallySimpleDocs to an Astro site:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install reallysimpledocs basecoat-css tailwindcss
|
|
12
|
+
npm install reallysimpledocs basecoat-css@beta tailwindcss
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Configure the integration:
|
|
16
16
|
|
|
17
17
|
```js
|
|
18
18
|
// astro.config.mjs
|
|
@@ -25,17 +25,16 @@ export default defineConfig({
|
|
|
25
25
|
docsDir: "./docs",
|
|
26
26
|
routeBase: "/docs",
|
|
27
27
|
style: "vega",
|
|
28
|
-
customCss: ["./src/docs.css"],
|
|
29
28
|
site: {
|
|
30
|
-
title: "
|
|
31
|
-
description: "
|
|
29
|
+
title: "Acme Docs",
|
|
30
|
+
description: "Documentation for Acme.",
|
|
32
31
|
},
|
|
33
32
|
}),
|
|
34
33
|
],
|
|
35
34
|
});
|
|
36
35
|
```
|
|
37
36
|
|
|
38
|
-
Create docs:
|
|
37
|
+
Create your first docs page:
|
|
39
38
|
|
|
40
39
|
```text
|
|
41
40
|
docs/
|
|
@@ -61,9 +60,19 @@ docs/
|
|
|
61
60
|
Welcome to the docs.
|
|
62
61
|
```
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
## What it provides
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
- Static docs routes from Markdown and MDX files.
|
|
66
|
+
- Sidebar navigation from `docs/docs.json`.
|
|
67
|
+
- Lunr search with a built-in command dialog.
|
|
68
|
+
- Syntax-highlighted code blocks with copy buttons.
|
|
69
|
+
- `llms.txt`, `llms-full.txt`, and per-page Markdown exports.
|
|
70
|
+
- Basecoat styling with style-specific RSD CSS for docs surfaces.
|
|
71
|
+
- Component slots for additive head content, sidebar chrome, and header controls.
|
|
72
|
+
|
|
73
|
+
## Customization
|
|
74
|
+
|
|
75
|
+
Use `components.ContentHeader` for controls between search and the built-in theme toggle:
|
|
67
76
|
|
|
68
77
|
```js
|
|
69
78
|
reallySimpleDocs({
|
|
@@ -73,38 +82,48 @@ reallySimpleDocs({
|
|
|
73
82
|
});
|
|
74
83
|
```
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
Use `components.Head` to add tags to `<head>`. RSD's built-in metadata, theme setup, Basecoat JavaScript, copy-code behavior, and search behavior still render.
|
|
77
86
|
|
|
78
|
-
Use
|
|
87
|
+
Use `customCss` for project CSS imported after Basecoat and RSD styles:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
reallySimpleDocs({
|
|
91
|
+
style: "nova",
|
|
92
|
+
customCss: ["./src/docs.css"],
|
|
93
|
+
});
|
|
94
|
+
```
|
|
79
95
|
|
|
80
|
-
|
|
81
|
-
# Button
|
|
96
|
+
Use `css: false` only when you provide the full Tailwind/Basecoat/RSD stylesheet pipeline yourself. RSD exports its own CSS for that case:
|
|
82
97
|
|
|
83
|
-
|
|
98
|
+
```css
|
|
99
|
+
@import "reallysimpledocs/css";
|
|
100
|
+
@import "reallysimpledocs/css/styles/vega";
|
|
84
101
|
```
|
|
85
102
|
|
|
86
|
-
##
|
|
103
|
+
## Search outside docs
|
|
87
104
|
|
|
88
|
-
|
|
105
|
+
Render the exported `CommandDialog` on any Astro page where you want docs search:
|
|
89
106
|
|
|
90
|
-
|
|
107
|
+
```astro
|
|
108
|
+
---
|
|
109
|
+
import { CommandDialog } from "reallysimpledocs/components";
|
|
110
|
+
---
|
|
91
111
|
|
|
92
|
-
|
|
93
|
-
reallySimpleDocs({
|
|
94
|
-
css: false,
|
|
95
|
-
});
|
|
96
|
-
```
|
|
112
|
+
<CommandDialog />
|
|
97
113
|
|
|
98
|
-
|
|
114
|
+
<button type="button" class="btn" onclick="window.rsdOpenCommandSearch?.()">
|
|
115
|
+
Search docs
|
|
116
|
+
</button>
|
|
117
|
+
```
|
|
99
118
|
|
|
100
|
-
## Local development
|
|
119
|
+
## Local development
|
|
101
120
|
|
|
102
121
|
```bash
|
|
103
122
|
npm install
|
|
104
123
|
npm run dev
|
|
105
124
|
```
|
|
106
125
|
|
|
107
|
-
Useful checks before publishing
|
|
126
|
+
Useful checks before publishing:
|
|
108
127
|
|
|
109
128
|
```bash
|
|
110
129
|
npm run build
|
|
@@ -112,17 +131,18 @@ npm pack --dry-run
|
|
|
112
131
|
npm run smoke:consumer
|
|
113
132
|
```
|
|
114
133
|
|
|
134
|
+
`npm run check` runs all three.
|
|
135
|
+
|
|
115
136
|
## Documentation
|
|
116
137
|
|
|
117
|
-
Go to [
|
|
138
|
+
Go to [reallysimpledocs.com](https://reallysimpledocs.com).
|
|
118
139
|
|
|
119
|
-
## Support
|
|
140
|
+
## Support
|
|
120
141
|
|
|
121
|
-
- [Contribute code](/CONTRIBUTING.md)
|
|
122
142
|
- [Report issues](https://github.com/hunvreus/reallysimpledocs/issues)
|
|
123
143
|
- [Sponsor me](https://github.com/sponsors/hunvreus)
|
|
124
144
|
- [Star the project on GitHub](https://github.com/hunvreus/reallysimpledocs)
|
|
125
145
|
|
|
126
146
|
## License
|
|
127
147
|
|
|
128
|
-
[MIT](
|
|
148
|
+
[MIT](LICENSE.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reallysimpledocs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.js",
|
|
@@ -30,16 +30,20 @@
|
|
|
30
30
|
"lucide-static": "^0.552.0",
|
|
31
31
|
"lunr": "^2.3.9",
|
|
32
32
|
"marked": "^16.4.1",
|
|
33
|
-
"
|
|
33
|
+
"rehype-parse": "^9.0.1",
|
|
34
|
+
"rehype-stringify": "^10.0.1",
|
|
35
|
+
"shiki": "^4.1.0",
|
|
36
|
+
"unified": "^11.0.5"
|
|
34
37
|
},
|
|
35
38
|
"peerDependencies": {
|
|
36
39
|
"astro": "^6.4.6",
|
|
37
|
-
"basecoat-css": "^1.0.0-beta.
|
|
40
|
+
"basecoat-css": "^1.0.0-beta.6",
|
|
38
41
|
"tailwindcss": ">=4.1.17 <5"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"astro": "^6.4.6",
|
|
42
|
-
"basecoat-css": "^1.0.0-beta.
|
|
45
|
+
"basecoat-css": "^1.0.0-beta.6",
|
|
46
|
+
"htmx.org": "^2.0.10",
|
|
43
47
|
"tailwindcss": "4.1.17"
|
|
44
48
|
},
|
|
45
49
|
"overrides": {
|
package/src/astro/index.js
CHANGED
|
@@ -4,7 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import mdx from "@astrojs/mdx";
|
|
5
5
|
import { unified } from "@astrojs/markdown-remark";
|
|
6
6
|
import tailwindcss from "@tailwindcss/vite";
|
|
7
|
+
import { rehypeBasecoatTables } from "./rehype-basecoat-tables.js";
|
|
7
8
|
import { remarkPreviewSource } from "./preview-source.js";
|
|
9
|
+
import { transformPreviewScripts } from "./preview-scripts.js";
|
|
8
10
|
|
|
9
11
|
const basecoatStyles = new Set(["vega", "nova", "maia", "lyra", "mira", "luma", "sera", "rhea"]);
|
|
10
12
|
|
|
@@ -71,6 +73,7 @@ export default function reallySimpleDocs(options = {}) {
|
|
|
71
73
|
style,
|
|
72
74
|
customCss: normalizeArray(options.customCss),
|
|
73
75
|
css: normalizeBoolean(options.css),
|
|
76
|
+
bodyAttrs: options.bodyAttrs || {},
|
|
74
77
|
components: options.components || {},
|
|
75
78
|
routeBase: options.routeBase ?? "/docs",
|
|
76
79
|
assetsBase: options.assetsBase || "/assets",
|
|
@@ -156,6 +159,7 @@ export default function reallySimpleDocs(options = {}) {
|
|
|
156
159
|
markdown: {
|
|
157
160
|
processor: unified({
|
|
158
161
|
remarkPlugins: [[remarkPreviewSource, { docsDir }]],
|
|
162
|
+
rehypePlugins: [rehypeBasecoatTables],
|
|
159
163
|
}),
|
|
160
164
|
shikiConfig,
|
|
161
165
|
},
|
|
@@ -165,6 +169,15 @@ export default function reallySimpleDocs(options = {}) {
|
|
|
165
169
|
},
|
|
166
170
|
plugins: [
|
|
167
171
|
tailwindcss(),
|
|
172
|
+
{
|
|
173
|
+
name: "reallysimpledocs-preview-scripts",
|
|
174
|
+
enforce: "pre",
|
|
175
|
+
transform(code, id) {
|
|
176
|
+
if (!id.endsWith(".mdx") || !isInsideDir(id, docsDir)) return null;
|
|
177
|
+
const transformed = transformPreviewScripts(code);
|
|
178
|
+
return transformed === code ? null : { code: transformed, map: null };
|
|
179
|
+
},
|
|
180
|
+
},
|
|
168
181
|
{
|
|
169
182
|
name: "reallysimpledocs-virtual-modules",
|
|
170
183
|
resolveId(id) {
|
|
@@ -211,3 +224,8 @@ export async function getMdxPage(slug) {
|
|
|
211
224
|
},
|
|
212
225
|
};
|
|
213
226
|
}
|
|
227
|
+
|
|
228
|
+
function isInsideDir(filePath, dir) {
|
|
229
|
+
const relative = path.relative(dir, filePath);
|
|
230
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
231
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const previewTag = "Preview";
|
|
2
|
+
const scriptTagPattern = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script>/g;
|
|
3
|
+
|
|
4
|
+
export function transformPreviewScripts(source) {
|
|
5
|
+
let output = "";
|
|
6
|
+
let cursor = 0;
|
|
7
|
+
|
|
8
|
+
while (cursor < source.length) {
|
|
9
|
+
const open = findNextTag(source, previewTag, cursor);
|
|
10
|
+
if (!open) {
|
|
11
|
+
output += source.slice(cursor);
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const close = findMatchingCloseTag(source, open);
|
|
16
|
+
if (!close) {
|
|
17
|
+
output += source.slice(cursor);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const body = source.slice(open.end, close.start);
|
|
22
|
+
const rewrittenBody = transformPreviewScriptBody(body);
|
|
23
|
+
|
|
24
|
+
output += source.slice(cursor, open.start);
|
|
25
|
+
if (rewrittenBody === body) {
|
|
26
|
+
output += source.slice(open.start, close.end);
|
|
27
|
+
} else {
|
|
28
|
+
output += addPreviewCodeAttribute(source.slice(open.start, open.end), body);
|
|
29
|
+
output += rewrittenBody;
|
|
30
|
+
output += source.slice(close.start, close.end);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
cursor = close.end;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function transformPreviewScriptBody(body) {
|
|
40
|
+
return body.replace(scriptTagPattern, (match, attrs = "", content = "") => {
|
|
41
|
+
if (/\ssrc\s*=/.test(attrs) || /(?:^|\s)is:inline(?:\s|=|$)/.test(attrs)) return match;
|
|
42
|
+
return `<script${attrs} is:inline>{${JSON.stringify(content)}}</script>`;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function addPreviewCodeAttribute(openTag, body) {
|
|
47
|
+
if (/(?:^|\s)code(?:\s|=|>)/.test(openTag)) return openTag;
|
|
48
|
+
return `${openTag.slice(0, -1)} code={${JSON.stringify(body.trim())}}>`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findMatchingCloseTag(source, open) {
|
|
52
|
+
if (open.selfClosing) return null;
|
|
53
|
+
let depth = 1;
|
|
54
|
+
let cursor = open.end;
|
|
55
|
+
|
|
56
|
+
while (cursor < source.length) {
|
|
57
|
+
const tag = findNextTag(source, previewTag, cursor);
|
|
58
|
+
if (!tag) return null;
|
|
59
|
+
if (tag.closing) depth -= 1;
|
|
60
|
+
else if (!tag.selfClosing) depth += 1;
|
|
61
|
+
if (depth === 0) return tag;
|
|
62
|
+
cursor = tag.end;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findNextTag(source, name, fromIndex) {
|
|
69
|
+
let cursor = fromIndex;
|
|
70
|
+
while (cursor < source.length) {
|
|
71
|
+
const start = source.indexOf("<", cursor);
|
|
72
|
+
if (start === -1) return null;
|
|
73
|
+
if (source.startsWith("<!--", start)) {
|
|
74
|
+
const commentEnd = source.indexOf("-->", start + 4);
|
|
75
|
+
cursor = commentEnd === -1 ? source.length : commentEnd + 3;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const tag = readTag(source, start);
|
|
80
|
+
if (!tag) {
|
|
81
|
+
cursor = start + 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (tag.name === name) return tag;
|
|
85
|
+
if (!tag.closing && !tag.selfClosing && tag.name.toLowerCase() === "script") {
|
|
86
|
+
const scriptEnd = source.toLowerCase().indexOf("</script>", tag.end);
|
|
87
|
+
cursor = scriptEnd === -1 ? source.length : scriptEnd + "</script>".length;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
cursor = tag.end;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readTag(source, start) {
|
|
96
|
+
let cursor = start + 1;
|
|
97
|
+
let closing = false;
|
|
98
|
+
|
|
99
|
+
if (source[cursor] === "/") {
|
|
100
|
+
closing = true;
|
|
101
|
+
cursor += 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const nameStart = cursor;
|
|
105
|
+
while (/[$A-Z_a-z0-9-]/.test(source[cursor] || "")) cursor += 1;
|
|
106
|
+
const name = source.slice(nameStart, cursor);
|
|
107
|
+
if (!name) return null;
|
|
108
|
+
|
|
109
|
+
let quote = "";
|
|
110
|
+
while (cursor < source.length) {
|
|
111
|
+
const char = source[cursor];
|
|
112
|
+
if (quote) {
|
|
113
|
+
if (char === "\\" && source[cursor + 1]) {
|
|
114
|
+
cursor += 2;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === quote) quote = "";
|
|
118
|
+
cursor += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
122
|
+
quote = char;
|
|
123
|
+
cursor += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === ">") {
|
|
127
|
+
const beforeEnd = source.slice(start, cursor).trimEnd();
|
|
128
|
+
return {
|
|
129
|
+
start,
|
|
130
|
+
end: cursor + 1,
|
|
131
|
+
name,
|
|
132
|
+
closing,
|
|
133
|
+
selfClosing: beforeEnd.endsWith("/"),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
cursor += 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toMarkdown } from "mdast-util-to-markdown";
|
|
2
2
|
import { mdxToMarkdown } from "mdast-util-mdx";
|
|
3
|
+
import { parseCodeMeta } from "../runtime/lib/code-meta.js";
|
|
3
4
|
|
|
4
5
|
export function remarkPreviewSource({ docsDir } = {}) {
|
|
5
6
|
const normalizedDocsDir = docsDir ? normalizePath(docsDir).replace(/\/$/, "") : "";
|
|
@@ -7,10 +8,15 @@ export function remarkPreviewSource({ docsDir } = {}) {
|
|
|
7
8
|
return (tree, file) => {
|
|
8
9
|
if (normalizedDocsDir && !isDocsFile(file?.path, normalizedDocsDir)) return;
|
|
9
10
|
|
|
10
|
-
visit(tree, (node) => {
|
|
11
|
-
if (node?.type
|
|
12
|
-
if (node
|
|
11
|
+
visit(tree, (node, parent, index) => {
|
|
12
|
+
if (node?.type === "code") return codeToMdx(node, parent, index);
|
|
13
|
+
if (node?.type !== "mdxJsxFlowElement") return null;
|
|
14
|
+
if (node.name === "Preview") {
|
|
15
|
+
addPreviewSource(node, file);
|
|
16
|
+
unwrapPreviewMarkdown(node);
|
|
17
|
+
}
|
|
13
18
|
if (node.name === "Tabs") addTabsMetadata(node);
|
|
19
|
+
return null;
|
|
14
20
|
});
|
|
15
21
|
};
|
|
16
22
|
}
|
|
@@ -24,14 +30,62 @@ function normalizePath(value) {
|
|
|
24
30
|
return String(value || "").replaceAll("\\", "/");
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
function addPreviewSource(node) {
|
|
33
|
+
function addPreviewSource(node, file) {
|
|
28
34
|
node.attributes ||= [];
|
|
29
35
|
if (hasAttr(node, "code")) return;
|
|
30
36
|
|
|
31
|
-
const source = toMarkdown({ type: "root", children: node.children || [] }, { extensions: [mdxToMarkdown()] })
|
|
37
|
+
const source = formatPreviewSource(getPreviewSource(node, file) ?? toMarkdown({ type: "root", children: node.children || [] }, { extensions: [mdxToMarkdown()] }));
|
|
32
38
|
node.attributes.push({ type: "mdxJsxAttribute", name: "code", value: source });
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
function getPreviewSource(node, file) {
|
|
42
|
+
const value = String(file?.value || "");
|
|
43
|
+
const start = node?.position?.start?.offset;
|
|
44
|
+
const end = node?.position?.end?.offset;
|
|
45
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || end <= start) return null;
|
|
46
|
+
|
|
47
|
+
const raw = value.slice(start, end);
|
|
48
|
+
const openEnd = raw.indexOf(">");
|
|
49
|
+
const closeStart = raw.lastIndexOf("</Preview>");
|
|
50
|
+
if (openEnd === -1 || closeStart === -1 || closeStart <= openEnd) return null;
|
|
51
|
+
|
|
52
|
+
return raw.slice(openEnd + 1, closeStart);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatPreviewSource(source) {
|
|
56
|
+
return String(source || "").trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function codeToMdx(node, parent, index) {
|
|
60
|
+
if (!parent || !Number.isInteger(index)) return null;
|
|
61
|
+
const title = parseCodeMeta(node.meta).title;
|
|
62
|
+
if (!title) return null;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
type: "mdxJsxFlowElement",
|
|
66
|
+
name: "Code",
|
|
67
|
+
attributes: [
|
|
68
|
+
{ type: "mdxJsxAttribute", name: "lang", value: node.lang || "text" },
|
|
69
|
+
{ type: "mdxJsxAttribute", name: "title", value: title },
|
|
70
|
+
{ type: "mdxJsxAttribute", name: "code", value: String(node.value || "") },
|
|
71
|
+
],
|
|
72
|
+
children: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function unwrapPreviewMarkdown(node) {
|
|
77
|
+
if (!Array.isArray(node?.children)) return;
|
|
78
|
+
|
|
79
|
+
const preserveDirectParagraphs =
|
|
80
|
+
(node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && node.name === "Step";
|
|
81
|
+
if (!preserveDirectParagraphs) {
|
|
82
|
+
node.children = node.children.flatMap((child) => (child?.type === "paragraph" ? child.children || [] : [child]));
|
|
83
|
+
}
|
|
84
|
+
for (const child of node.children) {
|
|
85
|
+
if (child?.type === "mdxJsxFlowElement" || child?.type === "mdxJsxTextElement") unwrapPreviewMarkdown(child);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
35
89
|
function addTabsMetadata(node) {
|
|
36
90
|
node.attributes ||= [];
|
|
37
91
|
if (hasAttr(node, "tabs")) return;
|
|
@@ -60,10 +114,16 @@ function addTabsMetadata(node) {
|
|
|
60
114
|
});
|
|
61
115
|
}
|
|
62
116
|
|
|
63
|
-
function visit(node, visitor) {
|
|
64
|
-
visitor(node);
|
|
117
|
+
function visit(node, visitor, parent = null, index = -1) {
|
|
118
|
+
const replacement = visitor(node, parent, index);
|
|
119
|
+
if (replacement && parent && Number.isInteger(index)) {
|
|
120
|
+
parent.children[index] = replacement;
|
|
121
|
+
node = replacement;
|
|
122
|
+
}
|
|
65
123
|
if (!Array.isArray(node?.children)) return;
|
|
66
|
-
for (
|
|
124
|
+
for (let childIndex = 0; childIndex < node.children.length; childIndex += 1) {
|
|
125
|
+
visit(node.children[childIndex], visitor, node, childIndex);
|
|
126
|
+
}
|
|
67
127
|
}
|
|
68
128
|
|
|
69
129
|
function getAttr(node, name) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import rehypeParse from "rehype-parse";
|
|
2
|
+
import rehypeStringify from "rehype-stringify";
|
|
3
|
+
import { unified } from "unified";
|
|
4
|
+
|
|
5
|
+
const containerClass = ["table-container", "scrollbar", "my-6"];
|
|
6
|
+
const tableClass = ["table"];
|
|
7
|
+
|
|
8
|
+
export function rehypeBasecoatTables() {
|
|
9
|
+
return (tree) => {
|
|
10
|
+
transformTables(tree);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function transformBasecoatTablesHtml(html) {
|
|
15
|
+
const file = await unified()
|
|
16
|
+
.use(rehypeParse, { fragment: true })
|
|
17
|
+
.use(rehypeBasecoatTables)
|
|
18
|
+
.use(rehypeStringify)
|
|
19
|
+
.process(html);
|
|
20
|
+
return String(file);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function transformTables(parent) {
|
|
24
|
+
if (!Array.isArray(parent?.children)) return;
|
|
25
|
+
|
|
26
|
+
for (let index = 0; index < parent.children.length; index += 1) {
|
|
27
|
+
const child = parent.children[index];
|
|
28
|
+
if (isElement(child, "table")) {
|
|
29
|
+
addClass(child, tableClass);
|
|
30
|
+
if (isTableContainer(parent)) continue;
|
|
31
|
+
parent.children[index] = {
|
|
32
|
+
type: "element",
|
|
33
|
+
tagName: "div",
|
|
34
|
+
properties: { className: [...containerClass] },
|
|
35
|
+
children: [child],
|
|
36
|
+
};
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
transformTables(child);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isElement(node, tagName) {
|
|
44
|
+
return node?.type === "element" && node.tagName === tagName;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isTableContainer(node) {
|
|
48
|
+
return isElement(node, "div") && classList(node).includes("table-container");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function addClass(node, classes) {
|
|
52
|
+
const merged = new Set([...classList(node), ...classes]);
|
|
53
|
+
node.properties ||= {};
|
|
54
|
+
node.properties.className = [...merged];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function classList(node) {
|
|
58
|
+
const value = node?.properties?.className;
|
|
59
|
+
if (Array.isArray(value)) return value.map(String);
|
|
60
|
+
if (typeof value === "string") return value.split(/\s+/).filter(Boolean);
|
|
61
|
+
return [];
|
|
62
|
+
}
|