create-markdown2site 0.0.0-stage → 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/index.js +193 -0
- package/package.json +33 -4
- package/template/_gitignore +6 -0
- package/template/index.html +12 -0
- package/template/package.json +20 -0
- package/template/public/markdown/index.md +20 -0
- package/template/public/markdown/posts/getting-started.md +33 -0
- package/template/public/markdown/posts/writing-pages.md +34 -0
- package/template/src/App.jsx +10 -0
- package/template/src/main.jsx +9 -0
- package/template/src/site.config.js +59 -0
- package/template/vite.config.js +7 -0
- package/README.md +0 -3
package/index.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { stdin, stdout, argv, exit, cwd, env } from 'node:process';
|
|
7
|
+
|
|
8
|
+
const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'template');
|
|
9
|
+
const PRESETS = ['default', 'ocean', 'forest', 'sunset', 'rose', 'midnight', 'mono', 'paper'];
|
|
10
|
+
const RENAME = { _gitignore: '.gitignore' };
|
|
11
|
+
|
|
12
|
+
const color = stdout.isTTY && !env.NO_COLOR;
|
|
13
|
+
const paint = (code) => (s) => (color ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
14
|
+
const bold = paint('1');
|
|
15
|
+
const dim = paint('2');
|
|
16
|
+
const green = paint('32');
|
|
17
|
+
const cyan = paint('36');
|
|
18
|
+
const red = paint('31');
|
|
19
|
+
|
|
20
|
+
const HELP = `
|
|
21
|
+
${bold('create-markdown2site')} — scaffold a markdown2site website
|
|
22
|
+
|
|
23
|
+
${bold('Usage')}
|
|
24
|
+
npm create markdown2site@latest [project-name] [-- options]
|
|
25
|
+
npx create-markdown2site [project-name] [options]
|
|
26
|
+
|
|
27
|
+
${bold('Options')}
|
|
28
|
+
-p, --preset <name> Color preset: ${PRESETS.join(', ')}
|
|
29
|
+
-t, --title <title> Site title (defaults to the project name)
|
|
30
|
+
-f, --force Write into a non-empty folder
|
|
31
|
+
-y, --yes Skip questions and use defaults
|
|
32
|
+
-h, --help Show this help
|
|
33
|
+
-v, --version Show version
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
function parseArgs(args) {
|
|
37
|
+
const opts = { dir: undefined, preset: undefined, title: undefined, force: false, yes: false };
|
|
38
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
39
|
+
const arg = args[i];
|
|
40
|
+
const next = () => {
|
|
41
|
+
const value = args[i + 1];
|
|
42
|
+
if (!value || value.startsWith('-')) fail(`Missing value for ${arg}`);
|
|
43
|
+
i += 1;
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
if (arg === '-h' || arg === '--help') opts.help = true;
|
|
47
|
+
else if (arg === '-v' || arg === '--version') opts.version = true;
|
|
48
|
+
else if (arg === '-f' || arg === '--force') opts.force = true;
|
|
49
|
+
else if (arg === '-y' || arg === '--yes') opts.yes = true;
|
|
50
|
+
else if (arg === '-p' || arg === '--preset') opts.preset = next();
|
|
51
|
+
else if (arg.startsWith('--preset=')) opts.preset = arg.slice(9);
|
|
52
|
+
else if (arg === '-t' || arg === '--title') opts.title = next();
|
|
53
|
+
else if (arg.startsWith('--title=')) opts.title = arg.slice(8);
|
|
54
|
+
else if (arg.startsWith('-')) fail(`Unknown option: ${arg}`);
|
|
55
|
+
else if (!opts.dir) opts.dir = arg;
|
|
56
|
+
else fail(`Unexpected argument: ${arg}`);
|
|
57
|
+
}
|
|
58
|
+
return opts;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fail(message) {
|
|
62
|
+
console.error(`\n${red('✖')} ${message}\n`);
|
|
63
|
+
exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toPackageName(name) {
|
|
67
|
+
return name
|
|
68
|
+
.trim()
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/\s+/g, '-')
|
|
71
|
+
.replace(/^[._]/, '')
|
|
72
|
+
.replace(/[^a-z0-9-~]+/g, '-')
|
|
73
|
+
.replace(/-+/g, '-')
|
|
74
|
+
.replace(/^-|-$/g, '') || 'markdown2site-app';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toTitle(name) {
|
|
78
|
+
return name
|
|
79
|
+
.replace(/[-_]+/g, ' ')
|
|
80
|
+
.replace(/\s+/g, ' ')
|
|
81
|
+
.trim()
|
|
82
|
+
.split(' ')
|
|
83
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
84
|
+
.join(' ') || 'My Site';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isEmptyDir(dir) {
|
|
88
|
+
if (!fs.existsSync(dir)) return true;
|
|
89
|
+
const files = fs.readdirSync(dir).filter((f) => f !== '.git' && f !== '.DS_Store');
|
|
90
|
+
return files.length === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Escape the title for the kind of file it's inserted into.
|
|
94
|
+
const escapers = {
|
|
95
|
+
'.js': (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/`/g, '\\`').replace(/\$\{/g, '\\${'),
|
|
96
|
+
'.jsx': (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/`/g, '\\`').replace(/\$\{/g, '\\${'),
|
|
97
|
+
'.html': (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function copyTemplate(src, dest, vars) {
|
|
101
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
102
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
103
|
+
const from = path.join(src, entry.name);
|
|
104
|
+
const to = path.join(dest, RENAME[entry.name] || entry.name);
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
copyTemplate(from, to, vars);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const ext = path.extname(entry.name);
|
|
110
|
+
if (['.js', '.jsx', '.html', '.md', '.json'].includes(ext) || entry.name === '_gitignore') {
|
|
111
|
+
const escape = escapers[ext] || ((s) => s);
|
|
112
|
+
let text = fs.readFileSync(from, 'utf8');
|
|
113
|
+
for (const [key, value] of Object.entries(vars)) text = text.split(`%${key}%`).join(escape(value));
|
|
114
|
+
fs.writeFileSync(to, text);
|
|
115
|
+
} else {
|
|
116
|
+
fs.copyFileSync(from, to);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function packageManager() {
|
|
122
|
+
const agent = env.npm_config_user_agent || '';
|
|
123
|
+
if (agent.startsWith('pnpm')) return 'pnpm';
|
|
124
|
+
if (agent.startsWith('yarn')) return 'yarn';
|
|
125
|
+
if (agent.startsWith('bun')) return 'bun';
|
|
126
|
+
return 'npm';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function main() {
|
|
130
|
+
const opts = parseArgs(argv.slice(2));
|
|
131
|
+
if (opts.help) { console.log(HELP); return; }
|
|
132
|
+
if (opts.version) {
|
|
133
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
134
|
+
console.log(pkg.version);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const interactive = stdin.isTTY && stdout.isTTY && !opts.yes;
|
|
139
|
+
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
140
|
+
const ask = async (question, fallback) => {
|
|
141
|
+
if (!rl) return fallback;
|
|
142
|
+
const answer = (await rl.question(`${cyan('?')} ${bold(question)} ${dim(`(${fallback})`)} `)).trim();
|
|
143
|
+
return answer || fallback;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
console.log(`\n${bold('markdown2site')} ${dim('— build websites from markdown')}\n`);
|
|
148
|
+
|
|
149
|
+
const dirName = opts.dir || (await ask('Project name:', 'my-site'));
|
|
150
|
+
const targetDir = path.resolve(cwd(), dirName);
|
|
151
|
+
const baseName = path.basename(targetDir);
|
|
152
|
+
|
|
153
|
+
if (!isEmptyDir(targetDir) && !opts.force) {
|
|
154
|
+
const answer = rl
|
|
155
|
+
? (await ask(`"${dirName}" is not empty. Write into it anyway? (y/N)`, 'n')).toLowerCase()
|
|
156
|
+
: 'n';
|
|
157
|
+
if (answer !== 'y' && answer !== 'yes') fail(`Folder "${dirName}" is not empty. Use --force to write into it.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let preset = opts.preset;
|
|
161
|
+
if (!preset && rl) {
|
|
162
|
+
console.log(dim(` Presets: ${PRESETS.map((p, i) => `${i + 1}) ${p}`).join(' ')}`));
|
|
163
|
+
const picked = await ask('Color preset:', 'default');
|
|
164
|
+
preset = PRESETS[Number(picked) - 1] || picked;
|
|
165
|
+
}
|
|
166
|
+
preset = preset || 'default';
|
|
167
|
+
if (!PRESETS.includes(preset)) fail(`Unknown preset "${preset}". Choose one of: ${PRESETS.join(', ')}`);
|
|
168
|
+
|
|
169
|
+
const title = opts.title || toTitle(baseName);
|
|
170
|
+
|
|
171
|
+
copyTemplate(TEMPLATE_DIR, targetDir, { SITE_TITLE: title, PRESET: preset });
|
|
172
|
+
|
|
173
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
174
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
175
|
+
pkg.name = toPackageName(baseName);
|
|
176
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
177
|
+
|
|
178
|
+
const pm = packageManager();
|
|
179
|
+
const run = pm === 'npm' ? 'npm run' : pm;
|
|
180
|
+
const relative = path.relative(cwd(), targetDir);
|
|
181
|
+
|
|
182
|
+
console.log(`\n${green('✔')} Created ${bold(title)} in ${cyan(relative || '.')}\n`);
|
|
183
|
+
console.log(' Next steps:\n');
|
|
184
|
+
if (relative) console.log(` cd ${relative.includes(' ') ? `"${relative}"` : relative}`);
|
|
185
|
+
console.log(` ${pm} install`);
|
|
186
|
+
console.log(` ${run} dev\n`);
|
|
187
|
+
console.log(dim(' Edit src/site.config.js to configure your site, and add pages in public/markdown/.\n'));
|
|
188
|
+
} finally {
|
|
189
|
+
if (rl) rl.close();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main().catch((err) => fail(err && err.message ? err.message : String(err)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-markdown2site",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a ready-to-run markdown2site website: npm create markdown2site my-site",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-markdown2site": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"markdown2site",
|
|
18
|
+
"create",
|
|
19
|
+
"scaffold",
|
|
20
|
+
"markdown",
|
|
21
|
+
"react",
|
|
22
|
+
"vite",
|
|
23
|
+
"website"
|
|
24
|
+
],
|
|
25
|
+
"author": "imaketech <imaketechofficial@gmail.com>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/imaketech1/markdown2site.git",
|
|
30
|
+
"directory": "create-markdown2site"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/imaketech1/markdown2site/issues"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>%SITE_TITLE%</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "markdown2site-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"markdown2site": "^1.1.0",
|
|
13
|
+
"react": "^19.0.0",
|
|
14
|
+
"react-dom": "^19.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
18
|
+
"vite": "^8.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Welcome to %SITE_TITLE%
|
|
2
|
+
|
|
3
|
+
This website is generated from markdown files with **markdown2site**. Edit
|
|
4
|
+
`public/markdown/index.md` to change this page.
|
|
5
|
+
|
|
6
|
+
## What's here
|
|
7
|
+
|
|
8
|
+
- **Home** — this page, from `public/markdown/index.md`
|
|
9
|
+
- **Posts** — every file in `public/markdown/posts/` listed in `src/site.config.js`
|
|
10
|
+
- **Themes** — light and dark mode, color presets, fonts and layouts
|
|
11
|
+
|
|
12
|
+
## Make it yours
|
|
13
|
+
|
|
14
|
+
Click the palette button in the bottom-left corner to try colors, fonts and
|
|
15
|
+
layouts live. When you like it, press **Copy config** and paste the result into
|
|
16
|
+
`src/site.config.js`.
|
|
17
|
+
|
|
18
|
+
## Next
|
|
19
|
+
|
|
20
|
+
Read [Getting Started](/getting-started) to add your first page.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## Add a page
|
|
2
|
+
|
|
3
|
+
1. Create a file in `public/markdown/posts/`, for example `my-page.md`.
|
|
4
|
+
2. Add it to `postsList` in `src/site.config.js`:
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
postsList: [
|
|
8
|
+
{ title: 'My Page', slug: 'my-page' },
|
|
9
|
+
],
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
3. Open `/my-page` in the browser.
|
|
13
|
+
|
|
14
|
+
## Change the look
|
|
15
|
+
|
|
16
|
+
Everything lives in `src/site.config.js`:
|
|
17
|
+
|
|
18
|
+
| Option | Values |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `preset` | `default`, `ocean`, `forest`, `sunset`, `rose`, `midnight`, `mono`, `paper` |
|
|
21
|
+
| `mode` | `light`, `dark`, `system` |
|
|
22
|
+
| `layout.style` | `classic`, `card`, `minimal`, `wide` |
|
|
23
|
+
| `sidebar.align` | `left`, `right`, `hidden` |
|
|
24
|
+
| `header.show` | `true`, `false` |
|
|
25
|
+
|
|
26
|
+
## Deploy
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Upload the `dist/` folder to any static host. For GitHub Pages, set
|
|
33
|
+
`router: 'hash'` in `src/site.config.js`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## Headings
|
|
2
|
+
|
|
3
|
+
Every `##` and `###` title shows up in the **On this page** sidebar.
|
|
4
|
+
|
|
5
|
+
### Smaller sections
|
|
6
|
+
|
|
7
|
+
Like this one.
|
|
8
|
+
|
|
9
|
+
## Text and links
|
|
10
|
+
|
|
11
|
+
Write **bold**, *italic*, `inline code` and [links](/getting-started) as usual.
|
|
12
|
+
|
|
13
|
+
> Blockquotes are great for tips and notes.
|
|
14
|
+
|
|
15
|
+
## Code
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
function greet(name) {
|
|
19
|
+
return `Hello, ${name}!`;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Lists and tasks
|
|
24
|
+
|
|
25
|
+
- Plain lists
|
|
26
|
+
- [x] Task lists
|
|
27
|
+
- [ ] Still to do
|
|
28
|
+
|
|
29
|
+
## Tables
|
|
30
|
+
|
|
31
|
+
| Feature | Supported |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Tables | Yes |
|
|
34
|
+
| Images | Yes |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your site's configuration. Every option is optional.
|
|
3
|
+
* Turn on `features.customizer`, tweak the look live in the browser, then click "Copy config".
|
|
4
|
+
*/
|
|
5
|
+
export const siteConfig = {
|
|
6
|
+
siteTitle: '%SITE_TITLE%',
|
|
7
|
+
|
|
8
|
+
// 'default' | 'ocean' | 'forest' | 'sunset' | 'rose' | 'midnight' | 'mono' | 'paper'
|
|
9
|
+
preset: '%PRESET%',
|
|
10
|
+
mode: 'system', // 'light' | 'dark' | 'system'
|
|
11
|
+
|
|
12
|
+
fonts: {
|
|
13
|
+
body: 'inter',
|
|
14
|
+
heading: 'plus-jakarta',
|
|
15
|
+
code: 'jetbrains-mono',
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
typography: {
|
|
19
|
+
headingStyle: 'accent', // 'plain' | 'underline' | 'accent' | 'hash'
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
layout: {
|
|
23
|
+
style: 'classic', // 'classic' | 'card' | 'minimal' | 'wide'
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
header: {
|
|
27
|
+
logo: '📝 %SITE_TITLE%',
|
|
28
|
+
variant: 'glass', // 'gradient' | 'solid' | 'glass' | 'minimal'
|
|
29
|
+
show: true,
|
|
30
|
+
showNav: true,
|
|
31
|
+
showThemeToggle: true,
|
|
32
|
+
sticky: true,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// "On this page" navigation built from your ## titles
|
|
36
|
+
sidebar: {
|
|
37
|
+
align: 'right', // 'left' | 'right' | 'hidden'
|
|
38
|
+
variant: 'line', // 'line' | 'card' | 'minimal'
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
footer: {
|
|
42
|
+
text: `© ${new Date().getFullYear()} %SITE_TITLE%`,
|
|
43
|
+
links: [{ label: 'GitHub', url: 'https://github.com', external: true }],
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
features: {
|
|
47
|
+
readingProgress: true,
|
|
48
|
+
backToTop: true,
|
|
49
|
+
customizer: true, // set to false before you deploy
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
router: 'browser', // use 'hash' for GitHub Pages
|
|
53
|
+
|
|
54
|
+
// Each slug matches a file in public/markdown/posts/<slug>.md
|
|
55
|
+
postsList: [
|
|
56
|
+
{ title: 'Getting Started', slug: 'getting-started' },
|
|
57
|
+
{ title: 'Writing Pages', slug: 'writing-pages' },
|
|
58
|
+
],
|
|
59
|
+
};
|
package/README.md
DELETED