reallysimpledocs 1.0.0-beta.5 → 1.0.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/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # ReallySimpleDocs
2
2
 
3
- A simple documentation system for Astro, built with **Basecoat** and **Tailwind CSS**.
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
- ## Add to an Astro site
7
+ ## Install
8
8
 
9
- ReallySimpleDocs is an Astro integration. Add it to an existing Astro site, or create one first with `npm create astro@latest`.
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
- Add the integration:
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: "ReallySimpleDocs",
31
- description: "A simple documentation system for Astro.",
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
- ReallySimpleDocs injects docs pages, Lunr search, per-page Markdown exports, `llms.txt`, and `llms-full.txt`.
63
+ ## What it provides
65
64
 
66
- Use component overrides for layout chrome such as sidebar branding, extra head tags, or header controls between search and the built-in theme toggle:
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
- For example, put only the GitHub button in `ContentHeader`; the theme toggle stays built in.
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 `.mdx` when a page needs ReallySimpleDocs components. Common components are available by default, so you do not need to import them in each page:
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
- ```mdx
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
- <Callout title="MDX works">MDX is optional. Plain Markdown stays the default.</Callout>
98
+ ```css
99
+ @import "reallysimpledocs/css";
100
+ @import "reallysimpledocs/css/styles/vega";
84
101
  ```
85
102
 
86
- ## Use Basecoat outside the docs
103
+ ## Search outside docs
87
104
 
88
- By default, ReallySimpleDocs manages one Tailwind/Basecoat stylesheet for the app. It scans `src/`, `docs/`, and the ReallySimpleDocs runtime, so Basecoat and Tailwind classes used by custom Astro pages outside the docs route are included in the same CSS build.
105
+ Render the exported `CommandDialog` on any Astro page where you want docs search:
89
106
 
90
- Disable managed CSS only when you want to provide the full stylesheet pipeline yourself:
107
+ ```astro
108
+ ---
109
+ import { CommandDialog } from "reallysimpledocs/components";
110
+ ---
91
111
 
92
- ```js
93
- reallySimpleDocs({
94
- css: false,
95
- });
96
- ```
112
+ <CommandDialog />
97
113
 
98
- Use `css: false` when you bring your own Tailwind/Basecoat/RSD stylesheet. ReallySimpleDocs still provides its managed JavaScript.
114
+ <button type="button" class="btn" onclick="window.rsdOpenCommandSearch?.()">
115
+ Search docs
116
+ </button>
117
+ ```
99
118
 
100
- ## Local development (this repo)
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 changes:
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 [ReallySimpleDocs.com](https://reallysimpledocs.com).
138
+ Go to [reallysimpledocs.com](https://reallysimpledocs.com).
118
139
 
119
- ## Support the project
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](/LICENSE.md)
148
+ [MIT](LICENSE.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reallysimpledocs",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.js",
@@ -30,16 +30,19 @@
30
30
  "lucide-static": "^0.552.0",
31
31
  "lunr": "^2.3.9",
32
32
  "marked": "^16.4.1",
33
- "shiki": "^4.1.0"
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.5",
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.5",
45
+ "basecoat-css": "^1.0.0-beta.6",
43
46
  "tailwindcss": "4.1.17"
44
47
  },
45
48
  "overrides": {
@@ -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 !== "mdxJsxFlowElement") return;
12
- if (node.name === "Preview") addPreviewSource(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()] }).trim();
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 (const child of node.children) visit(child, visitor);
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
+ }