serve-my-md 1.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/README.md +52 -0
- package/bin/index.js +487 -0
- package/index.html +70 -0
- package/package.json +111 -0
- package/shared/constants.json +3 -0
- package/shared/index.d.ts +34 -0
- package/web/.cta.json +12 -0
- package/web/.cursorrules +7 -0
- package/web/.prettierignore +3 -0
- package/web/.vscode/settings.json +11 -0
- package/web/README.md +489 -0
- package/web/components.json +21 -0
- package/web/eslint.config.js +5 -0
- package/web/index.html +66 -0
- package/web/prettier.config.js +10 -0
- package/web/public/og-image.png +0 -0
- package/web/src/.generated/output.json +1 -0
- package/web/src/.generated/paths.json +1 -0
- package/web/src/App.tsx +15 -0
- package/web/src/article.css +199 -0
- package/web/src/components/Bettercrumb.tsx +86 -0
- package/web/src/components/Fonts.tsx +13 -0
- package/web/src/components/Header.tsx +10 -0
- package/web/src/components/IntentLink.tsx +20 -0
- package/web/src/components/Rendrer.tsx +140 -0
- package/web/src/components/Search.tsx +275 -0
- package/web/src/components/Sidebar.tsx +89 -0
- package/web/src/components/ThemeSwitcher.tsx +46 -0
- package/web/src/components/ui/breadcrumb.tsx +122 -0
- package/web/src/components/ui/button.tsx +60 -0
- package/web/src/components/ui/collapsible.tsx +33 -0
- package/web/src/components/ui/dropdown-menu.tsx +255 -0
- package/web/src/components/ui/input.tsx +21 -0
- package/web/src/components/ui/kbd.tsx +28 -0
- package/web/src/components/ui/separator.tsx +26 -0
- package/web/src/components/ui/sheet.tsx +139 -0
- package/web/src/components/ui/sidebar.tsx +727 -0
- package/web/src/components/ui/skeleton.tsx +13 -0
- package/web/src/components/ui/tooltip.tsx +59 -0
- package/web/src/contexts.ts +10 -0
- package/web/src/hooks/useMobile.ts +19 -0
- package/web/src/lib/utils.tsx +89 -0
- package/web/src/main.tsx +100 -0
- package/web/src/reportWebVitals.ts +13 -0
- package/web/src/styles.css +196 -0
- package/web/src/types/index.ts +3 -0
- package/web/tsconfig.json +35 -0
- package/web/vite.config.ts +31 -0
- package/web/vitest.config.ts +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# serve-my-md
|
|
2
|
+
|
|
3
|
+
A tiny CLI to generate a static docs website from markdown files.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
Full docs are at [https://ashishantil.dev/serve-my-md](https://ashishantil.dev/serve-my-md).
|
|
8
|
+
|
|
9
|
+
## Basic usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
serve-my-md --directory .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run this inside (or pointing to) the folder that contains your markdown docs.
|
|
16
|
+
|
|
17
|
+
## Commands and options
|
|
18
|
+
|
|
19
|
+
- `serve-my-md`: scans markdown files, builds the static site, and outputs it in the target directory.
|
|
20
|
+
- `-d, --directory <path>`: sets the docs root directory (default: current directory).
|
|
21
|
+
- `-i, --interactive`: asks for directory input interactively.
|
|
22
|
+
|
|
23
|
+
## Optional customization
|
|
24
|
+
|
|
25
|
+
In your target docs directory, you can optionally create files like `smm.config.json` and `.smmignore` to customize behavior (routing, sorting, ignored paths, etc.).
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Static site generation** — every page is a standalone `.html` file with pre-rendered content
|
|
30
|
+
- **Groupers** — directory-based sidebar grouping with `(Name)` syntax, excluded from URLs
|
|
31
|
+
- **Full-text search** — site-wide search with `Ctrl+Shift+F`
|
|
32
|
+
- **Keyboard shortcuts** — quick page navigation
|
|
33
|
+
- **Light/dark themes** — automatic or user-toggleable
|
|
34
|
+
- **Responsive** — works on desktop and mobile
|
|
35
|
+
- **Accessibility** — keyboard navigation, ARIA labels, semantic landmarks
|
|
36
|
+
- **Breadcrumbs** — clear navigation context
|
|
37
|
+
- **Custom ordering** — numeric filename prefixes with `trimIndexFromPath` config
|
|
38
|
+
- **Rich markdown** — footnotes, task lists, syntax highlighting
|
|
39
|
+
- **SEO** — per-page OG tags, meta descriptions, pre-rendered content
|
|
40
|
+
- **Font customization** — configurable title/body/mono fonts
|
|
41
|
+
- **Public assets** — copy static files via `publicPath` config
|
|
42
|
+
|
|
43
|
+
## Future goals
|
|
44
|
+
|
|
45
|
+
- **Search Indexing** — structured search index for smarter results
|
|
46
|
+
- **Config validation** — Zod-based schema validation of `smm.config.json` with JSON Schema export
|
|
47
|
+
- **Sitemap** — automatic `sitemap.xml` generation
|
|
48
|
+
- **Per-page Open Graph** — page-level og tags from frontmatter
|
|
49
|
+
- **Doctor command** — health checks: config validation, route discovery, broken link detection, and more
|
|
50
|
+
- **Link validation** — flag invalid internal links at build-time
|
|
51
|
+
- **Optional RSS** — config-enableable RSS feed for blog/changelog content
|
|
52
|
+
- **SchemaStore upload** — publish JSON Schema to SchemaStore once stable
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli/src/lib/logger.ts
|
|
4
|
+
var Logger = class {
|
|
5
|
+
static log(message, type) {
|
|
6
|
+
console.log(`${type ? `[${type.toUpperCase()}] ` : ""}${message}`);
|
|
7
|
+
}
|
|
8
|
+
static error(message) {
|
|
9
|
+
console.error(`[ERROR] ${message}`);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// cli/src/shared.ts
|
|
14
|
+
import MarkdownIt from "markdown-it";
|
|
15
|
+
import path2 from "path";
|
|
16
|
+
import Prism from "prismjs";
|
|
17
|
+
|
|
18
|
+
// cli/src/core/index.ts
|
|
19
|
+
import fs2 from "fs/promises";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import { minimatch } from "minimatch";
|
|
22
|
+
|
|
23
|
+
// cli/src/utils/index.ts
|
|
24
|
+
import fs from "fs/promises";
|
|
25
|
+
var indexTokens = "1234567890.";
|
|
26
|
+
function trimIndexFromPath(filePath) {
|
|
27
|
+
return filePath.split("/").map((segment) => {
|
|
28
|
+
let offset = 0;
|
|
29
|
+
let encountered = false;
|
|
30
|
+
while (offset < segment.length && (indexTokens.includes(segment[offset]) || segment[offset] === " " && !encountered))
|
|
31
|
+
if (segment[offset++] !== " ") encountered = true;
|
|
32
|
+
return segment.slice(offset).trim();
|
|
33
|
+
}).join("/");
|
|
34
|
+
}
|
|
35
|
+
function cleanName(filename) {
|
|
36
|
+
return filename === "index.md" ? "" : filename.replace(/\.md$/, "");
|
|
37
|
+
}
|
|
38
|
+
function optional(prop, val) {
|
|
39
|
+
return val ? { [prop]: val } : {};
|
|
40
|
+
}
|
|
41
|
+
function slugify(filepath) {
|
|
42
|
+
return filepath.toLowerCase().split("").map((c) => {
|
|
43
|
+
if (".,;\"'\\:<>`?!".includes(c)) return "";
|
|
44
|
+
if (c === " " || c === "_") return "-";
|
|
45
|
+
return c;
|
|
46
|
+
}).join("");
|
|
47
|
+
}
|
|
48
|
+
async function FileOrDirectoryExists(filepath) {
|
|
49
|
+
try {
|
|
50
|
+
await fs.access(filepath);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function makeRoutesOfNestedPaths(nestedPaths, prefix = "/") {
|
|
57
|
+
return nestedPaths.reduce((acc, { pathSegment, children, isGrouper }) => {
|
|
58
|
+
return [
|
|
59
|
+
...acc,
|
|
60
|
+
...isGrouper || !children ? [] : [prefix + pathSegment],
|
|
61
|
+
...children ? makeRoutesOfNestedPaths(
|
|
62
|
+
children,
|
|
63
|
+
prefix + (!isGrouper ? pathSegment + "/" : "")
|
|
64
|
+
) : isGrouper ? [] : [prefix + pathSegment]
|
|
65
|
+
];
|
|
66
|
+
}, []);
|
|
67
|
+
}
|
|
68
|
+
function makeRoutesOfNestedPathsRaw(nestedPaths, prefix = "/") {
|
|
69
|
+
return nestedPaths.reduce((acc, { pathSegment, children }) => {
|
|
70
|
+
return [
|
|
71
|
+
...acc,
|
|
72
|
+
...children ? makeRoutesOfNestedPathsRaw(
|
|
73
|
+
children,
|
|
74
|
+
prefix + pathSegment + "/"
|
|
75
|
+
) : [prefix + pathSegment]
|
|
76
|
+
];
|
|
77
|
+
}, []);
|
|
78
|
+
}
|
|
79
|
+
function ogToHtml(og) {
|
|
80
|
+
const tags = [];
|
|
81
|
+
for (const [key, value] of Object.entries(og)) {
|
|
82
|
+
if (value == null) continue;
|
|
83
|
+
if (["images", "videos", "audios"].includes(key)) continue;
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
for (const v of value) {
|
|
86
|
+
tags.push(`<meta property="og:${key}" content="${v}">`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
tags.push(`<meta property="og:${key}" content="${value}">`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
[...og.images ?? [], ...og.videos ?? [], ...og.audios ?? []].forEach(
|
|
93
|
+
(img) => {
|
|
94
|
+
Object.entries(img).forEach(([k, v]) => {
|
|
95
|
+
if (v == null) return;
|
|
96
|
+
tags.push(`<meta property="og:${k}" content="${v}">`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
return tags.join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// cli/src/lib/commander.ts
|
|
104
|
+
import * as inquirer from "@inquirer/prompts";
|
|
105
|
+
|
|
106
|
+
// cli/src/config.json
|
|
107
|
+
var config_default = {
|
|
108
|
+
name: "serve-my-md",
|
|
109
|
+
version: "1.0.0",
|
|
110
|
+
description: "A CLI tool to create a ready-to-serve static website from markdown files",
|
|
111
|
+
defaultConfigPath: "./smm.config.json",
|
|
112
|
+
defaultIgnorePath: "./.smmignore"
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// cli/src/lib/commander.ts
|
|
116
|
+
import { Command } from "commander";
|
|
117
|
+
var program = new Command();
|
|
118
|
+
program.name(config_default.name).description(config_default.description).version(config_default.version);
|
|
119
|
+
program.option("-d, --directory <path>", "Directory to scan for markdown files", ".");
|
|
120
|
+
program.option("-i, --interactive", "Enable interactive mode");
|
|
121
|
+
if (process.env.VITEST) {
|
|
122
|
+
program.option("--skip-build", "Skip the build step");
|
|
123
|
+
}
|
|
124
|
+
program.parse(process.argv);
|
|
125
|
+
var options = program.opts();
|
|
126
|
+
if (options.interactive || options.directory === void 0) {
|
|
127
|
+
const res = await inquirer.input({
|
|
128
|
+
message: `Enter root directory: `,
|
|
129
|
+
default: options.directory || "./"
|
|
130
|
+
});
|
|
131
|
+
options.directory = res.trim();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// cli/src/core/index.ts
|
|
135
|
+
import { readdirSync } from "fs";
|
|
136
|
+
|
|
137
|
+
// shared/constants.json
|
|
138
|
+
var constants_default = {
|
|
139
|
+
STATIC_TEMP_CONTENT_PREFIX: "__smm_static_temp_content__"
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// cli/src/core/index.ts
|
|
143
|
+
var STATIC_TEMP_CONTENT_PREFIX = constants_default.STATIC_TEMP_CONTENT_PREFIX;
|
|
144
|
+
async function readConfig(filepath) {
|
|
145
|
+
try {
|
|
146
|
+
const data = JSON.parse(await fs2.readFile(filepath, "utf-8"));
|
|
147
|
+
return data;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
Logger.log(
|
|
150
|
+
`No config file found at ${filepath}, proceeding with defaults.`,
|
|
151
|
+
"info"
|
|
152
|
+
);
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function parseSmmIgnore(filePath) {
|
|
157
|
+
try {
|
|
158
|
+
let shouldIgnore3 = function(targetPath) {
|
|
159
|
+
const p = targetPath.replace(/\\/g, "/");
|
|
160
|
+
let ignored = false;
|
|
161
|
+
for (const rule of rules) {
|
|
162
|
+
if (minimatch(p, rule.pattern, { dot: true })) {
|
|
163
|
+
ignored = !rule.negated;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return ignored;
|
|
167
|
+
};
|
|
168
|
+
var shouldIgnore2 = shouldIgnore3;
|
|
169
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
170
|
+
const rules = raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#")).map((line) => {
|
|
171
|
+
const negated = line.startsWith("!");
|
|
172
|
+
const pattern = negated ? line.slice(1) : line;
|
|
173
|
+
return { pattern, negated };
|
|
174
|
+
});
|
|
175
|
+
return { rules, shouldIgnore: shouldIgnore3 };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
Logger.log(
|
|
178
|
+
`No .smmignore file found at ${filePath}, proceeding without ignore rules.`,
|
|
179
|
+
"info"
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
rules: [],
|
|
183
|
+
shouldIgnore: (_) => false
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function getMarkdownFiles(baseUrl, pairChildren) {
|
|
188
|
+
const files = await fs2.readdir(baseUrl, { withFileTypes: true });
|
|
189
|
+
const routeTree = pairChildren || [];
|
|
190
|
+
const promises = [];
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
const filePath = path.join(baseUrl, file.name);
|
|
193
|
+
if (shouldIgnore(filePath.slice(options.directory.length)) || filePath.slice(options.directory.length) === finalConfig.publicPath)
|
|
194
|
+
continue;
|
|
195
|
+
if (file.isDirectory()) {
|
|
196
|
+
const isGrouper = file.name.startsWith("(") && file.name.endsWith(")");
|
|
197
|
+
const dirPair = {
|
|
198
|
+
label: isGrouper ? file.name.slice(1, -1) : file.name,
|
|
199
|
+
children: [],
|
|
200
|
+
pathSegment: file.name,
|
|
201
|
+
isGrouper
|
|
202
|
+
};
|
|
203
|
+
routeTree.push(dirPair);
|
|
204
|
+
promises.push(
|
|
205
|
+
getMarkdownFiles(filePath, dirPair.children)
|
|
206
|
+
);
|
|
207
|
+
} else if (file.name.endsWith(".md")) {
|
|
208
|
+
routeTree.push({
|
|
209
|
+
label: file.name,
|
|
210
|
+
children: null,
|
|
211
|
+
pathSegment: file.name
|
|
212
|
+
});
|
|
213
|
+
promises.push(Promise.resolve([filePath]));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (finalConfig.sortRoutes)
|
|
217
|
+
routeTree.sort((a, b) => {
|
|
218
|
+
if (a.label === "index.md") return -1;
|
|
219
|
+
if (b.label === "index.md") return 1;
|
|
220
|
+
if (a.isGrouper && !b.isGrouper) return 1;
|
|
221
|
+
if (b.isGrouper && !a.isGrouper) return -1;
|
|
222
|
+
return a.label.localeCompare(b.label);
|
|
223
|
+
});
|
|
224
|
+
const filess = finalConfig.trimIndexFromPath ? (await Promise.all(promises)).flat().map((val) => trimIndexFromPath(val)) : (await Promise.all(promises)).flat();
|
|
225
|
+
return pairChildren ? filess : { routeTree, files: filess };
|
|
226
|
+
}
|
|
227
|
+
function cleanNestedPaths(routeTree) {
|
|
228
|
+
for (const pair of routeTree) {
|
|
229
|
+
if (finalConfig.trimIndexFromPath) {
|
|
230
|
+
pair.label = trimIndexFromPath(pair.label);
|
|
231
|
+
}
|
|
232
|
+
pair.label = cleanName(pair.label);
|
|
233
|
+
pair.pathSegment = getPath(cleanName(pair.pathSegment)).replaceAll("/", "");
|
|
234
|
+
if (pair.children) {
|
|
235
|
+
cleanNestedPaths(pair.children);
|
|
236
|
+
if (pair.children?.length === 1 && ["", "index.md"].includes(pair.children?.[0]?.label)) {
|
|
237
|
+
pair.children = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function getPath(filepath) {
|
|
243
|
+
let transformedPath = filepath.replace(options.directory, "").replace(/\\/g, "/").replace(/\/index.md$/, "").replace(/\.md$/, "");
|
|
244
|
+
if (finalConfig.trimIndexFromPath) {
|
|
245
|
+
transformedPath = trimIndexFromPath(transformedPath);
|
|
246
|
+
}
|
|
247
|
+
return slugify(transformedPath).split("/").filter((s) => !(s.startsWith("(") && s.endsWith(")"))).join("/") || "/";
|
|
248
|
+
}
|
|
249
|
+
async function parseMD(filepath) {
|
|
250
|
+
const path4 = getPath(filepath);
|
|
251
|
+
return {
|
|
252
|
+
path: path4,
|
|
253
|
+
content: mdParser.render(await fs2.readFile(filepath, "utf-8"))
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async function generateHtml(distDir, routeContent) {
|
|
257
|
+
try {
|
|
258
|
+
let htmlTemplate = await fs2.readFile(
|
|
259
|
+
path.join(import.meta.dirname, "..", "index.html"),
|
|
260
|
+
"utf-8"
|
|
261
|
+
);
|
|
262
|
+
const commentStart = htmlTemplate.indexOf("<!--");
|
|
263
|
+
htmlTemplate = htmlTemplate.replace(
|
|
264
|
+
htmlTemplate.slice(
|
|
265
|
+
commentStart,
|
|
266
|
+
htmlTemplate.indexOf("-->", commentStart) + 3
|
|
267
|
+
),
|
|
268
|
+
""
|
|
269
|
+
);
|
|
270
|
+
if (distDir) {
|
|
271
|
+
const files = readdirSync(path.join(distDir, "assets"));
|
|
272
|
+
const cssFile = files.find((file) => file.endsWith(".css"));
|
|
273
|
+
const jsFile = files.find((file) => file.endsWith(".js"));
|
|
274
|
+
const prefix = distDir.slice(path.join(import.meta.dirname, options.directory).length);
|
|
275
|
+
htmlTemplate = htmlTemplate.replace(`<script type="module" src="/src/main.tsx"></script>`, "");
|
|
276
|
+
if (cssFile && jsFile) {
|
|
277
|
+
htmlTemplate = htmlTemplate.replace(
|
|
278
|
+
"{{distAssets}}",
|
|
279
|
+
`<link rel="stylesheet" href="${path.join(
|
|
280
|
+
prefix,
|
|
281
|
+
"assets",
|
|
282
|
+
cssFile
|
|
283
|
+
)}" />
|
|
284
|
+
<script type="module" src="${path.join(
|
|
285
|
+
prefix,
|
|
286
|
+
"assets",
|
|
287
|
+
jsFile
|
|
288
|
+
)}"></script>`
|
|
289
|
+
);
|
|
290
|
+
} else {
|
|
291
|
+
Logger.error(`Could not find CSS and JS files in dist assets.`);
|
|
292
|
+
htmlTemplate = htmlTemplate.replace("{{distAssets}}", "");
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
htmlTemplate = htmlTemplate.replace("{{distAssets}}", "");
|
|
296
|
+
}
|
|
297
|
+
return htmlTemplate.replace("{{og}}", ogToHtml(finalConfig.og ?? {})).replace("{{title}}", finalConfig.rootTitle ?? "Serve My MD").replace("{{description}}", finalConfig.description ?? "").replace(
|
|
298
|
+
"{{favicon}}",
|
|
299
|
+
finalConfig.favicon ? `<link rel="icon" href="${finalConfig.favicon}" />` : ""
|
|
300
|
+
).replace(
|
|
301
|
+
"{{fonts}}",
|
|
302
|
+
finalConfig.fonts ? (finalConfig.fonts.title && finalConfig.fonts.title.url ? `<link rel="stylesheet" href="${finalConfig.fonts.title.url}" />` : "") + (finalConfig.fonts.body && finalConfig.fonts.body.url ? `<link rel="stylesheet" href="${finalConfig.fonts.body.url}" />` : "") + (finalConfig.fonts.mono && finalConfig.fonts.mono.url ? `<link rel="stylesheet" href="${finalConfig.fonts.mono.url}" />` : "") : ""
|
|
303
|
+
).replace("{{content}}", STATIC_TEMP_CONTENT_PREFIX + (routeContent ?? "")).trim();
|
|
304
|
+
} catch (err) {
|
|
305
|
+
throw new Error(`Failed to generate HTML: ${err}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function buildDistRoutesFromRouteTree(routeTree, groupedRoutes, distPath, prefix = "/") {
|
|
309
|
+
for (const node of routeTree) {
|
|
310
|
+
if (node.children && node.isGrouper) {
|
|
311
|
+
await buildDistRoutesFromRouteTree(
|
|
312
|
+
node.children,
|
|
313
|
+
groupedRoutes,
|
|
314
|
+
distPath,
|
|
315
|
+
prefix
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
const distRoutePath = path.join(distPath, prefix, node.pathSegment.replace("/", "")) + (node.pathSegment === "" ? "/index.html" : ".html");
|
|
319
|
+
await fs2.mkdir(path.dirname(distRoutePath), { recursive: true });
|
|
320
|
+
const html = await generateHtml(
|
|
321
|
+
distPath,
|
|
322
|
+
groupedRoutes[path.posix.join(prefix, node.pathSegment)]?.[0]?.content
|
|
323
|
+
);
|
|
324
|
+
await fs2.writeFile(distRoutePath, html, "utf-8");
|
|
325
|
+
if (node.children) {
|
|
326
|
+
await buildDistRoutesFromRouteTree(
|
|
327
|
+
node.children,
|
|
328
|
+
groupedRoutes,
|
|
329
|
+
distPath,
|
|
330
|
+
path.join(prefix, node.pathSegment)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// cli/src/smm.config.json
|
|
338
|
+
var smm_config_default = {
|
|
339
|
+
rootTitle: "Serve My MD",
|
|
340
|
+
description: "A simple markdown to static site builder.",
|
|
341
|
+
baseRoute: "/",
|
|
342
|
+
defaultTheme: "dark",
|
|
343
|
+
markdownItOptions: {
|
|
344
|
+
html: true,
|
|
345
|
+
xhtmlOut: true,
|
|
346
|
+
breaks: true,
|
|
347
|
+
langPrefix: "language-",
|
|
348
|
+
linkify: true,
|
|
349
|
+
typographer: false
|
|
350
|
+
},
|
|
351
|
+
outDir: "dist",
|
|
352
|
+
favicon: "",
|
|
353
|
+
logo: "",
|
|
354
|
+
name: "Serve My MD",
|
|
355
|
+
showNameWithLogo: true,
|
|
356
|
+
sortRoutes: true,
|
|
357
|
+
trimIndexFromPath: false
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// cli/src/shared.ts
|
|
361
|
+
import MarkdownItFootNote from "markdown-it-footnote";
|
|
362
|
+
import MarkdownItTasks from "markdown-it-task-lists";
|
|
363
|
+
import loadLanguages from "prismjs/components/index.js";
|
|
364
|
+
var { shouldIgnore } = await parseSmmIgnore(
|
|
365
|
+
path2.join(options.directory, config_default.defaultIgnorePath)
|
|
366
|
+
);
|
|
367
|
+
var finalConfig = {
|
|
368
|
+
...smm_config_default,
|
|
369
|
+
...await readConfig(path2.join(options.directory, config_default.defaultConfigPath))
|
|
370
|
+
};
|
|
371
|
+
var md = new MarkdownIt({
|
|
372
|
+
...finalConfig.markdownItOptions,
|
|
373
|
+
highlight: function(str, lang) {
|
|
374
|
+
if (!Object.hasOwn(Prism.languages, lang)) {
|
|
375
|
+
loadLanguages([lang]);
|
|
376
|
+
}
|
|
377
|
+
const highlighted = Prism.highlight(str, Prism.languages[lang], lang);
|
|
378
|
+
return `<pre class="language-${lang}"><code class="language-${lang}">${highlighted}</code></pre>`;
|
|
379
|
+
}
|
|
380
|
+
}).use(MarkdownItFootNote).use(MarkdownItTasks);
|
|
381
|
+
md.linkify.set({ fuzzyEmail: false });
|
|
382
|
+
var mdParser = md;
|
|
383
|
+
|
|
384
|
+
// cli/src/core/build.ts
|
|
385
|
+
import { cp, rm, writeFile } from "fs/promises";
|
|
386
|
+
import path3, { resolve } from "path";
|
|
387
|
+
import { fileURLToPath } from "url";
|
|
388
|
+
import { build as viteBuild } from "vite";
|
|
389
|
+
import { mkdirSync } from "fs";
|
|
390
|
+
var DIST_DIRNAME = finalConfig.outDir || "dist";
|
|
391
|
+
var WEB_DIRNAME = "web";
|
|
392
|
+
var PUBLIC_DIRNAME = "public";
|
|
393
|
+
async function build(options2) {
|
|
394
|
+
const skipBuild = ("skipBuild" in options2 && options2.skipBuild) ?? false;
|
|
395
|
+
const { routeTree, files: markdownFiles } = await getMarkdownFiles(
|
|
396
|
+
options2.directory
|
|
397
|
+
);
|
|
398
|
+
const parsePromises = [];
|
|
399
|
+
Logger.log("Processing routes...");
|
|
400
|
+
for (const file of makeRoutesOfNestedPathsRaw(routeTree)) {
|
|
401
|
+
parsePromises.push(parseMD(path3.join(options2.directory, file)));
|
|
402
|
+
}
|
|
403
|
+
cleanNestedPaths(routeTree);
|
|
404
|
+
const groupedRoutes = Object.groupBy(
|
|
405
|
+
await Promise.all(parsePromises),
|
|
406
|
+
(route) => route.path
|
|
407
|
+
);
|
|
408
|
+
const routes = makeRoutesOfNestedPaths(routeTree).reduce(
|
|
409
|
+
(acc, pth) => [...acc, ...(groupedRoutes[pth] ?? []).map((r) => ({ ...r, path: path3.join(finalConfig.baseRoute || "/", r.path) }))],
|
|
410
|
+
[]
|
|
411
|
+
);
|
|
412
|
+
const out = {
|
|
413
|
+
rootTitle: finalConfig.rootTitle ?? "Documentation",
|
|
414
|
+
description: finalConfig.description ?? "Documentation",
|
|
415
|
+
baseRoute: finalConfig.baseRoute ?? "/",
|
|
416
|
+
defaultTheme: finalConfig.defaultTheme ?? "dark",
|
|
417
|
+
name: finalConfig.name ?? "Serve My MD",
|
|
418
|
+
showNameWithLogo: finalConfig.showNameWithLogo ?? false,
|
|
419
|
+
routes,
|
|
420
|
+
outDir: DIST_DIRNAME,
|
|
421
|
+
fonts: {
|
|
422
|
+
title: finalConfig.fonts?.title?.name || "serif",
|
|
423
|
+
body: finalConfig.fonts?.body?.name || "sans-serif",
|
|
424
|
+
mono: finalConfig.fonts?.mono?.name || "monospace"
|
|
425
|
+
},
|
|
426
|
+
...optional("favicon", finalConfig.favicon),
|
|
427
|
+
...optional("version", finalConfig.version)
|
|
428
|
+
};
|
|
429
|
+
routes.forEach((o) => {
|
|
430
|
+
Logger.log(o.path);
|
|
431
|
+
});
|
|
432
|
+
const __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
433
|
+
const webDir = path3.join(__dirname, "..", WEB_DIRNAME);
|
|
434
|
+
const distDir = path3.join(webDir, DIST_DIRNAME);
|
|
435
|
+
mkdirSync(path3.join(webDir, "src", ".generated"), { recursive: true });
|
|
436
|
+
await writeFile(
|
|
437
|
+
path3.join(webDir, "src", ".generated", "output.json"),
|
|
438
|
+
JSON.stringify(out)
|
|
439
|
+
);
|
|
440
|
+
await writeFile(
|
|
441
|
+
path3.join(webDir, "src", ".generated", "paths.json"),
|
|
442
|
+
JSON.stringify(routeTree)
|
|
443
|
+
);
|
|
444
|
+
Logger.log("\nParsed MDs");
|
|
445
|
+
await writeFile(path3.join(webDir, "index.html"), await generateHtml());
|
|
446
|
+
Logger.log("Generated HTML from template");
|
|
447
|
+
if (!skipBuild) {
|
|
448
|
+
if (finalConfig.publicPath) {
|
|
449
|
+
if (await FileOrDirectoryExists(
|
|
450
|
+
path3.join(options2.directory, finalConfig.publicPath)
|
|
451
|
+
)) {
|
|
452
|
+
Logger.log(`Copying public assets from ${finalConfig.publicPath}...`);
|
|
453
|
+
await cp(
|
|
454
|
+
path3.join(options2.directory, finalConfig.publicPath),
|
|
455
|
+
path3.join(webDir, PUBLIC_DIRNAME),
|
|
456
|
+
{ recursive: true }
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
Logger.error(`Public path "${finalConfig.publicPath}" does not exist!`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
Logger.log("Building the app...");
|
|
463
|
+
await viteBuild({
|
|
464
|
+
configFile: resolve(webDir, "vite.config.ts")
|
|
465
|
+
});
|
|
466
|
+
await buildDistRoutesFromRouteTree(routeTree, groupedRoutes, distDir);
|
|
467
|
+
const targetDist = path3.join(options2.directory, DIST_DIRNAME);
|
|
468
|
+
await rm(targetDist, { recursive: true }).catch(() => {
|
|
469
|
+
});
|
|
470
|
+
Logger.log("Built the app, copying results...");
|
|
471
|
+
return cp(distDir, targetDist, { recursive: true }).then(() => {
|
|
472
|
+
Logger.log("Done successfully!");
|
|
473
|
+
return true;
|
|
474
|
+
}).catch((err) => {
|
|
475
|
+
Logger.error("Error copying files: " + err);
|
|
476
|
+
return false;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return Promise.resolve(true);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// cli/src/index.ts
|
|
483
|
+
if (await build(options)) {
|
|
484
|
+
Logger.log("Completed successfully.");
|
|
485
|
+
} else {
|
|
486
|
+
Logger.error("Failed.");
|
|
487
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!-- ? this html file is a template for that final site
|
|
2
|
+
|
|
3
|
+
? The `content` field is only for SEO purpose, and is obviously overriden on mount by react
|
|
4
|
+
? `distAssets` is for the css and js files generated by vite. Only used for the sub-routes, not for the home page.
|
|
5
|
+
? The `loading` class on body is for the loading screen, and is removed on mount by react.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
<!doctype html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8" />
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
13
|
+
{{favicon}}
|
|
14
|
+
<meta name="theme-color" content="#000000" />
|
|
15
|
+
<meta
|
|
16
|
+
name="description"
|
|
17
|
+
content="{{description}}"
|
|
18
|
+
/>
|
|
19
|
+
{{fonts}}
|
|
20
|
+
{{og}}
|
|
21
|
+
<title>{{title}}</title>
|
|
22
|
+
{{distAssets}}
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
@keyframes keep-rotating {
|
|
26
|
+
from {
|
|
27
|
+
transform: translate(-50%, -50%) rotate(0deg);
|
|
28
|
+
}
|
|
29
|
+
to {
|
|
30
|
+
transform: translate(-50%, -50%) rotate(360deg);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
body.loading {
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
|
|
37
|
+
&::before {
|
|
38
|
+
content: '';
|
|
39
|
+
position: fixed;
|
|
40
|
+
inset: 0;
|
|
41
|
+
z-index: 50;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
background-color: rgba(255, 255, 255, 0.8);
|
|
46
|
+
backdrop-filter: blur(4px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&::after {
|
|
50
|
+
content: '';
|
|
51
|
+
position: fixed;
|
|
52
|
+
width: 4rem;
|
|
53
|
+
height: 4rem;
|
|
54
|
+
top: 50%;
|
|
55
|
+
left: 50%;
|
|
56
|
+
transform: translate(-50%, -50%);
|
|
57
|
+
z-index: 51;
|
|
58
|
+
border: 4px solid #3498db;
|
|
59
|
+
border-top-color: transparent;
|
|
60
|
+
border-radius: 50%;
|
|
61
|
+
animation: keep-rotating 1s linear infinite;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body class='loading'>
|
|
67
|
+
<div id="app">{{content}}</div>
|
|
68
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|