markform-cli 1.1.1 → 1.2.2
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 +24 -4
- package/images/Markform | Devlog 3.png +0 -0
- package/package.json +5 -3
- package/src/build.js +56 -30
- package/src/index-page.js +14 -7
- package/src/index.js +12 -4
- package/src/new.js +35 -0
- package/test/post.md +9 -0
- package/themes/default.html +441 -273
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# markform 📝
|
|
2
|
+
|
|
3
|
+

|
|
2
4
|
|
|
3
5
|
Turn any folder of Markdown files into a beautiful, searchable static site — in one command.
|
|
4
6
|
```bash
|
|
@@ -23,9 +25,9 @@ npm install -g markform-cli
|
|
|
23
25
|
markform ./my-notes -o ./output
|
|
24
26
|
```
|
|
25
27
|
|
|
26
|
-
If it doesn't work (mainly on
|
|
28
|
+
If it doesn't work (mainly on Windows), use:
|
|
27
29
|
```bash
|
|
28
|
-
npx markform-cli ./
|
|
30
|
+
npx markform-cli ./my-notes -o ./output
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
**Watch mode** — auto-rebuilds and live-reloads the browser on file changes:
|
|
@@ -33,6 +35,24 @@ npx markform-cli ./test -o ./output
|
|
|
33
35
|
markform ./my-notes -o ./output --watch
|
|
34
36
|
```
|
|
35
37
|
|
|
38
|
+
**Create a new page:**
|
|
39
|
+
```bash
|
|
40
|
+
markform --new my-page
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This scaffolds a new Markdown file with frontmatter ready to fill in:
|
|
44
|
+
```markdown
|
|
45
|
+
---
|
|
46
|
+
title: My Page
|
|
47
|
+
date: 2026-02-27
|
|
48
|
+
description:
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# My Page
|
|
52
|
+
|
|
53
|
+
Write something here...
|
|
54
|
+
```
|
|
55
|
+
|
|
36
56
|
---
|
|
37
57
|
|
|
38
58
|
## (≖_≖ ) Options
|
|
@@ -41,7 +61,7 @@ markform ./my-notes -o ./output --watch
|
|
|
41
61
|
|------|---------|-------------|
|
|
42
62
|
| `-o, --output <dir>` | `./output` | Where to write the site |
|
|
43
63
|
| `-w, --watch` | off | Watch for changes and live reload |
|
|
44
|
-
|
|
|
64
|
+
| `-n, --new <name>` | — | Scaffold a new Markdown file |
|
|
45
65
|
|
|
46
66
|
---
|
|
47
67
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markform-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Turn any folder of Markdown files into a beautiful, searchable static site.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"author": "Aryan Madan",
|
|
19
19
|
"license": "GPL-3.0-only",
|
|
20
|
-
|
|
20
|
+
"homepage": "https://github.com/yourusername/markform",
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
23
|
"url": "https://github.com/yourusername/markform"
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"express": "^5.2.1",
|
|
29
29
|
"fs-extra": "^11.3.3",
|
|
30
30
|
"fuse.js": "^7.1.0",
|
|
31
|
-
"
|
|
31
|
+
"gray-matter": "^4.0.3",
|
|
32
|
+
"marked": "^17.0.3",
|
|
33
|
+
"open": "^11.0.0"
|
|
32
34
|
}
|
|
33
35
|
}
|
package/src/build.js
CHANGED
|
@@ -2,9 +2,20 @@ const fs = require('fs-extra');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { marked } = require('marked');
|
|
4
4
|
const { buildNav } = require('./nav');
|
|
5
|
+
const { buildIndexPage } = require('./index-page');
|
|
5
6
|
const chokidar = require('chokidar');
|
|
6
7
|
const express = require('express');
|
|
7
|
-
const {
|
|
8
|
+
const { exec } = require('child_process');
|
|
9
|
+
const matter = require('gray-matter');
|
|
10
|
+
|
|
11
|
+
function openInBrowser(url) {
|
|
12
|
+
const cmd = process.platform === 'win32'
|
|
13
|
+
? `start "" "${url}"`
|
|
14
|
+
: process.platform === 'darwin'
|
|
15
|
+
? `open "${url}"`
|
|
16
|
+
: `xdg-open "${url}"`;
|
|
17
|
+
exec(cmd);
|
|
18
|
+
}
|
|
8
19
|
|
|
9
20
|
async function buildSite(input, options) {
|
|
10
21
|
const inputDir = path.resolve(input);
|
|
@@ -16,7 +27,7 @@ async function buildSite(input, options) {
|
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
console.log(`⛏ Building site from ${inputDir} to ${outputDir}...`);
|
|
19
|
-
await compile(inputDir, outputDir,
|
|
30
|
+
await compile(inputDir, outputDir, false);
|
|
20
31
|
console.log(`(ˆᗜˆ ) Site built to ${outputDir}`);
|
|
21
32
|
|
|
22
33
|
if (options.watch) {
|
|
@@ -30,19 +41,22 @@ async function buildSite(input, options) {
|
|
|
30
41
|
});
|
|
31
42
|
|
|
32
43
|
app.listen(3000, () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log(`(°ㅁ°) Serving at http://localhost:3000/index.html`);
|
|
44
|
+
console.log(`(°ㅁ° ) Serving at http://localhost:3000/index.html`);
|
|
45
|
+
openInBrowser('http://localhost:3000/index.html');
|
|
36
46
|
});
|
|
37
47
|
|
|
38
48
|
chokidar.watch(inputDir).on('change', async (filePath) => {
|
|
39
49
|
console.log(`↺ Changed: ${filePath}, rebuilding...`);
|
|
40
50
|
await compile(inputDir, outputDir, true);
|
|
41
51
|
shouldReload = true;
|
|
42
|
-
console.log(`(ˆᗜˆ ) Site rebuilt
|
|
52
|
+
console.log(`(ˆᗜˆ ) Site rebuilt.`);
|
|
43
53
|
});
|
|
44
54
|
|
|
45
55
|
console.log(`(≖_≖ ) Watching for changes...`);
|
|
56
|
+
} else {
|
|
57
|
+
const indexPath = path.join(outputDir, 'index.html');
|
|
58
|
+
console.log(`(°ㅁ° ) Opening ${indexPath}`);
|
|
59
|
+
openInBrowser(indexPath);
|
|
46
60
|
}
|
|
47
61
|
}
|
|
48
62
|
|
|
@@ -51,22 +65,37 @@ async function compile(inputDir, outputDir, watch = false) {
|
|
|
51
65
|
|
|
52
66
|
const mdFiles = getMdFiles(inputDir);
|
|
53
67
|
const nav = buildNav(mdFiles, inputDir);
|
|
68
|
+
const searchIndex = buildSearchIndex(mdFiles, inputDir);
|
|
69
|
+
const searchIndexJson = JSON.stringify(searchIndex);
|
|
70
|
+
|
|
54
71
|
const template = fs.readFileSync(
|
|
55
72
|
path.join(__dirname, '../themes/default.html'), 'utf-8'
|
|
56
73
|
);
|
|
57
74
|
|
|
58
|
-
await buildSearchIndex(mdFiles, inputDir, outputDir);
|
|
59
|
-
|
|
60
75
|
for (const filePath of mdFiles) {
|
|
61
|
-
const
|
|
76
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
77
|
+
const { content, data: frontmatter } = matter(raw);
|
|
62
78
|
const htmlContent = marked(content);
|
|
63
79
|
const relativePath = path.relative(inputDir, filePath);
|
|
64
80
|
const outputPath = path.join(outputDir, relativePath.replace(/\.md$/, '.html'));
|
|
65
81
|
|
|
82
|
+
const title = frontmatter.title || path.basename(filePath, '.md').replace(/-/g, ' ');
|
|
83
|
+
const description = frontmatter.description || '';
|
|
84
|
+
const date = frontmatter.date || '';
|
|
85
|
+
|
|
86
|
+
let metaHtml = '';
|
|
87
|
+
if (date || description) {
|
|
88
|
+
metaHtml = `<div class="page-meta">
|
|
89
|
+
${date ? `<span class="page-date">${date}</span>` : ''}
|
|
90
|
+
${description ? `<p class="page-description">${description}</p>` : ''}
|
|
91
|
+
</div>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
66
94
|
let html = template
|
|
67
|
-
.replace('{{content}}', htmlContent)
|
|
95
|
+
.replace('{{content}}', metaHtml + htmlContent)
|
|
68
96
|
.replace('{{nav}}', nav)
|
|
69
|
-
.replace('{{title}}',
|
|
97
|
+
.replace('{{title}}', title)
|
|
98
|
+
.replace('{{search_index}}', searchIndexJson);
|
|
70
99
|
|
|
71
100
|
if (watch) {
|
|
72
101
|
html = html.replace('</body>', `
|
|
@@ -80,12 +109,12 @@ async function compile(inputDir, outputDir, watch = false) {
|
|
|
80
109
|
</body>`);
|
|
81
110
|
}
|
|
82
111
|
|
|
83
|
-
const indexHtml = buildIndexPage(mdFiles, inputDir, template);
|
|
84
|
-
await fs.writeFile(path.join(outputDir, 'index.html'), indexHtml);
|
|
85
|
-
|
|
86
112
|
await fs.ensureDir(path.dirname(outputPath));
|
|
87
113
|
await fs.writeFile(outputPath, html);
|
|
88
114
|
}
|
|
115
|
+
|
|
116
|
+
const indexHtml = buildIndexPage(mdFiles, inputDir, template, searchIndexJson);
|
|
117
|
+
await fs.writeFile(path.join(outputDir, 'index.html'), indexHtml);
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
function getMdFiles(dir) {
|
|
@@ -102,27 +131,24 @@ function getMdFiles(dir) {
|
|
|
102
131
|
return results;
|
|
103
132
|
}
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
function buildSearchIndex(mdFiles, inputDir) {
|
|
135
|
+
return mdFiles.map((filePath) => {
|
|
107
136
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
108
|
-
const content = raw
|
|
109
|
-
|
|
110
|
-
.replace(
|
|
111
|
-
.replace(
|
|
112
|
-
.replace(
|
|
113
|
-
.replace(
|
|
114
|
-
.replace(
|
|
137
|
+
const { content, data: frontmatter } = matter(raw);
|
|
138
|
+
const cleaned = content
|
|
139
|
+
.replace(/#{1,6}\s/g, '')
|
|
140
|
+
.replace(/\*\*|__|\*|_/g, '')
|
|
141
|
+
.replace(/`{1,3}[^`]*`{1,3}/g, '')
|
|
142
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
143
|
+
.replace(/^\s*[-*+]\s/gm, '')
|
|
144
|
+
.replace(/\n+/g, ' ')
|
|
115
145
|
.trim();
|
|
146
|
+
|
|
116
147
|
const relativePath = path.relative(inputDir, filePath);
|
|
117
148
|
const href = relativePath.replace(/\.md$/, '.html');
|
|
118
|
-
const title = path.basename(filePath, '.md').replace(/-/g, ' ');
|
|
119
|
-
return { title, href, content };
|
|
149
|
+
const title = frontmatter.title || path.basename(filePath, '.md').replace(/-/g, ' ');
|
|
150
|
+
return { title, href, content: cleaned };
|
|
120
151
|
});
|
|
121
|
-
|
|
122
|
-
await fs.writeFile(
|
|
123
|
-
path.join(outputDir, 'search-index.json'),
|
|
124
|
-
JSON.stringify(index, null, 2)
|
|
125
|
-
);
|
|
126
152
|
}
|
|
127
153
|
|
|
128
154
|
module.exports = { buildSite };
|
package/src/index-page.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
|
|
3
|
-
function buildIndexPage(mdFiles, inputDir, template) {
|
|
3
|
+
function buildIndexPage(mdFiles, inputDir, template, searchIndexJson) {
|
|
4
4
|
const cards = mdFiles.map((filePath) => {
|
|
5
5
|
const relativePath = path.relative(inputDir, filePath);
|
|
6
6
|
const href = relativePath.replace(/\.md$/, '.html');
|
|
@@ -8,19 +8,26 @@ function buildIndexPage(mdFiles, inputDir, template) {
|
|
|
8
8
|
const folder = path.dirname(relativePath) === '.' ? 'root' : path.dirname(relativePath);
|
|
9
9
|
|
|
10
10
|
return `
|
|
11
|
-
<a href="
|
|
11
|
+
<a href="${href}" class="card">
|
|
12
12
|
<span class="card-folder">${folder}</span>
|
|
13
13
|
<span class="card-title">${title}</span>
|
|
14
14
|
</a>`;
|
|
15
15
|
}).join('\n');
|
|
16
16
|
|
|
17
|
+
const content = `
|
|
18
|
+
<div class="index-header">
|
|
19
|
+
<h1>markform</h1>
|
|
20
|
+
<p>${mdFiles.length} page${mdFiles.length !== 1 ? 's' : ''}</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="card-grid">${cards}</div>
|
|
23
|
+
`;
|
|
24
|
+
|
|
17
25
|
return template
|
|
18
|
-
.replace('{{content}}',
|
|
19
|
-
<h1>All Pages</h1>
|
|
20
|
-
<div class="card-grid">${cards}</div>
|
|
21
|
-
`)
|
|
26
|
+
.replace('{{content}}', content)
|
|
22
27
|
.replace('{{nav}}', '')
|
|
23
|
-
.replace('{{title}}', 'Home')
|
|
28
|
+
.replace('{{title}}', 'Home')
|
|
29
|
+
.replace('{{search_index}}', searchIndexJson)
|
|
30
|
+
.replace('<body>', '<body data-page="index">');
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
module.exports = { buildIndexPage };
|
package/src/index.js
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const { buildSite } = require('./build');
|
|
5
|
+
const { createNewPage } = require('./new');
|
|
5
6
|
|
|
6
7
|
program
|
|
7
8
|
.name('markform')
|
|
8
|
-
.description('Turn any folder of Markdown files into a beautiful, searchable static site
|
|
9
|
-
.version('1.
|
|
10
|
-
.argument('
|
|
9
|
+
.description('Turn any folder of Markdown files into a beautiful, searchable static site')
|
|
10
|
+
.version('1.2.2')
|
|
11
|
+
.argument('[input]', 'folder of Markdown files')
|
|
11
12
|
.option('-o, --output <dir>', 'output directory', './output')
|
|
12
13
|
.option('-w, --watch', 'watch for changes and rebuild')
|
|
14
|
+
.option('-n, --new <name>', 'create a new markdown file')
|
|
13
15
|
.action((input, options) => {
|
|
14
|
-
|
|
16
|
+
if (options.new) {
|
|
17
|
+
createNewPage(options.new);
|
|
18
|
+
} else if (input) {
|
|
19
|
+
buildSite(input, options);
|
|
20
|
+
} else {
|
|
21
|
+
program.help();
|
|
22
|
+
}
|
|
15
23
|
});
|
|
16
24
|
|
|
17
25
|
program.parse();
|
package/src/new.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function createNewPage(name) {
|
|
5
|
+
const filename = name.endsWith('.md') ? name : `${name}.md`;
|
|
6
|
+
const filepath = path.resolve(filename);
|
|
7
|
+
|
|
8
|
+
if (fs.existsSync(filepath)) {
|
|
9
|
+
console.error(`(˙◠˙ ) File already exists: ${filepath}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const title = name
|
|
14
|
+
.replace(/-/g, ' ')
|
|
15
|
+
.replace(/\.md$/, '')
|
|
16
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
17
|
+
|
|
18
|
+
const date = new Date().toISOString().split('T')[0];
|
|
19
|
+
|
|
20
|
+
const content = `---
|
|
21
|
+
title: ${title}
|
|
22
|
+
date: ${date}
|
|
23
|
+
description:
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# ${title}
|
|
27
|
+
|
|
28
|
+
Write something here...
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
fs.writeFileSync(filepath, content);
|
|
32
|
+
console.log(`(ˆᗜˆ ) Created ${filepath}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { createNewPage };
|
package/test/post.md
ADDED
package/themes/default.html
CHANGED
|
@@ -2,282 +2,450 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
|
|
4
4
|
<head>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{title}}</title>
|
|
8
|
+
<style>
|
|
9
|
+
*,
|
|
10
|
+
*::before,
|
|
11
|
+
*::after {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #0e0e0e;
|
|
19
|
+
--surface: #161616;
|
|
20
|
+
--border: #222222;
|
|
21
|
+
--text: #e2e2e2;
|
|
22
|
+
--muted: #444;
|
|
23
|
+
--muted-light: #777;
|
|
24
|
+
--nav-width: 240px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
29
|
+
display: flex;
|
|
30
|
+
min-height: 100vh;
|
|
31
|
+
background: var(--bg);
|
|
32
|
+
color: var(--text);
|
|
33
|
+
line-height: 1.7;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── NAV ── */
|
|
37
|
+
nav {
|
|
38
|
+
width: var(--nav-width);
|
|
39
|
+
min-height: 100vh;
|
|
40
|
+
background: var(--surface);
|
|
41
|
+
border-right: 1px solid var(--border);
|
|
42
|
+
padding: 1.75rem 1rem;
|
|
43
|
+
position: sticky;
|
|
44
|
+
top: 0;
|
|
45
|
+
height: 100vh;
|
|
46
|
+
overflow-y: auto;
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
gap: 0.25rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
nav.hidden {
|
|
54
|
+
display: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.nav-logo {
|
|
58
|
+
font-size: 0.65rem;
|
|
59
|
+
font-weight: 800;
|
|
60
|
+
letter-spacing: 0.2em;
|
|
61
|
+
text-transform: uppercase;
|
|
62
|
+
color: var(--muted-light);
|
|
63
|
+
text-decoration: none;
|
|
64
|
+
padding: 0 0.5rem;
|
|
65
|
+
margin-bottom: 0.75rem;
|
|
66
|
+
display: block;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.nav-logo:hover {
|
|
70
|
+
color: var(--text);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.nav-divider {
|
|
74
|
+
height: 1px;
|
|
75
|
+
background: var(--border);
|
|
76
|
+
margin: 0.5rem 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#search {
|
|
80
|
+
width: 100%;
|
|
81
|
+
padding: 0.5rem 0.6rem;
|
|
82
|
+
border: 1px solid var(--border);
|
|
83
|
+
background: var(--bg);
|
|
84
|
+
color: var(--text);
|
|
85
|
+
font-size: 0.8rem;
|
|
86
|
+
outline: none;
|
|
87
|
+
font-family: inherit;
|
|
88
|
+
margin-bottom: 0.5rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#search:focus {
|
|
92
|
+
border-color: #444;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#search::placeholder {
|
|
96
|
+
color: var(--muted);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.nav-section-label {
|
|
100
|
+
font-size: 0.6rem;
|
|
101
|
+
font-weight: 700;
|
|
102
|
+
letter-spacing: 0.15em;
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
color: var(--muted);
|
|
105
|
+
padding: 0 0.5rem;
|
|
106
|
+
margin: 0.75rem 0 0.35rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#nav-list {
|
|
110
|
+
list-style: none;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#nav-list li a {
|
|
114
|
+
color: var(--muted-light);
|
|
115
|
+
text-decoration: none;
|
|
116
|
+
font-size: 0.82rem;
|
|
117
|
+
display: block;
|
|
118
|
+
padding: 0.3rem 0.5rem;
|
|
119
|
+
text-transform: capitalize;
|
|
120
|
+
border-left: 2px solid transparent;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#nav-list li a:hover {
|
|
124
|
+
color: var(--text);
|
|
125
|
+
border-left-color: var(--muted);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#search-results {
|
|
129
|
+
list-style: none;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#search-results li {
|
|
133
|
+
padding: 0.6rem 0.5rem;
|
|
134
|
+
border-bottom: 1px solid var(--border);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#search-results a {
|
|
138
|
+
font-size: 0.82rem;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
color: var(--text);
|
|
141
|
+
text-decoration: none;
|
|
142
|
+
text-transform: capitalize;
|
|
143
|
+
display: block;
|
|
144
|
+
margin-bottom: 0.15rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#search-results a:hover {
|
|
148
|
+
color: #fff;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#search-results p {
|
|
152
|
+
color: var(--muted-light);
|
|
153
|
+
font-size: 0.72rem;
|
|
154
|
+
line-height: 1.5;
|
|
155
|
+
margin: 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* ── MAIN ── */
|
|
159
|
+
main {
|
|
160
|
+
flex: 1;
|
|
161
|
+
max-width: 700px;
|
|
162
|
+
margin: 0 auto;
|
|
163
|
+
padding: 4rem 2.5rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* index page gets full width */
|
|
167
|
+
body.index-page main {
|
|
168
|
+
max-width: 900px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ── TYPOGRAPHY ── */
|
|
172
|
+
h1 {
|
|
173
|
+
font-size: 1.75rem;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
margin-bottom: 0.75rem;
|
|
176
|
+
line-height: 1.2;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
h2 {
|
|
180
|
+
font-size: 1.2rem;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
margin: 2.25rem 0 0.5rem;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
h3 {
|
|
186
|
+
font-size: 1rem;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
margin: 1.5rem 0 0.4rem;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
p {
|
|
192
|
+
margin-bottom: 1rem;
|
|
193
|
+
color: #aaa;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
a {
|
|
197
|
+
color: var(--text);
|
|
198
|
+
text-decoration: underline;
|
|
199
|
+
text-underline-offset: 3px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
a:hover {
|
|
203
|
+
color: #fff;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
code {
|
|
207
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
208
|
+
font-size: 0.82em;
|
|
209
|
+
background: var(--surface);
|
|
210
|
+
color: #ccc;
|
|
211
|
+
padding: 0.15em 0.4em;
|
|
212
|
+
border: 1px solid var(--border);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pre {
|
|
216
|
+
background: var(--surface);
|
|
217
|
+
border: 1px solid var(--border);
|
|
218
|
+
padding: 1.25rem 1.5rem;
|
|
219
|
+
overflow-x: auto;
|
|
220
|
+
margin-bottom: 1.25rem;
|
|
221
|
+
font-size: 0.82rem;
|
|
222
|
+
line-height: 1.7;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
pre code {
|
|
226
|
+
background: none;
|
|
227
|
+
color: #ccc;
|
|
228
|
+
padding: 0;
|
|
229
|
+
border: none;
|
|
230
|
+
font-size: inherit;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
blockquote {
|
|
234
|
+
border-left: 2px solid var(--border);
|
|
235
|
+
padding: 0.4rem 0 0.4rem 1rem;
|
|
236
|
+
color: var(--muted-light);
|
|
237
|
+
margin: 1.25rem 0;
|
|
238
|
+
font-style: italic;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
hr {
|
|
242
|
+
border: none;
|
|
243
|
+
border-top: 1px solid var(--border);
|
|
244
|
+
margin: 2rem 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
img {
|
|
248
|
+
max-width: 100%;
|
|
249
|
+
margin: 1rem 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
table {
|
|
253
|
+
width: 100%;
|
|
254
|
+
border-collapse: collapse;
|
|
255
|
+
margin-bottom: 1.25rem;
|
|
256
|
+
font-size: 0.875rem;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
th,
|
|
260
|
+
td {
|
|
261
|
+
padding: 0.6rem 0.875rem;
|
|
262
|
+
border: 1px solid var(--border);
|
|
263
|
+
text-align: left;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
th {
|
|
267
|
+
background: var(--surface);
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
color: var(--text);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
td {
|
|
273
|
+
color: #aaa;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
ul,
|
|
277
|
+
ol {
|
|
278
|
+
padding-left: 1.5rem;
|
|
279
|
+
margin-bottom: 1rem;
|
|
280
|
+
color: #aaa;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
li {
|
|
284
|
+
margin-bottom: 0.25rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ── INDEX PAGE ── */
|
|
288
|
+
.index-header {
|
|
289
|
+
margin-bottom: 3rem;
|
|
290
|
+
border-bottom: 1px solid var(--border);
|
|
291
|
+
padding-bottom: 2rem;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.index-header h1 {
|
|
295
|
+
font-size: 2rem;
|
|
296
|
+
margin-bottom: 0.4rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.index-header p {
|
|
300
|
+
color: var(--muted-light);
|
|
301
|
+
font-size: 0.9rem;
|
|
302
|
+
margin: 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.card-grid {
|
|
306
|
+
display: grid;
|
|
307
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
308
|
+
gap: 1px;
|
|
309
|
+
background: var(--border);
|
|
310
|
+
border: 1px solid var(--border);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.card {
|
|
314
|
+
display: flex;
|
|
315
|
+
flex-direction: column;
|
|
316
|
+
padding: 1.25rem;
|
|
317
|
+
background: var(--bg);
|
|
318
|
+
text-decoration: none;
|
|
319
|
+
transition: background 0.1s;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.card:hover {
|
|
323
|
+
background: var(--surface);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.card-folder {
|
|
327
|
+
font-size: 0.6rem;
|
|
328
|
+
color: var(--muted);
|
|
329
|
+
text-transform: uppercase;
|
|
330
|
+
letter-spacing: 0.12em;
|
|
331
|
+
margin-bottom: 0.4rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.card-title {
|
|
335
|
+
font-size: 0.875rem;
|
|
336
|
+
font-weight: 500;
|
|
337
|
+
color: var(--text);
|
|
338
|
+
text-transform: capitalize;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* ── SEARCH HIGHLIGHT ── */
|
|
342
|
+
mark {
|
|
343
|
+
background: #2a2a2a;
|
|
344
|
+
color: #fff;
|
|
345
|
+
padding: 0 2px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.page-meta {
|
|
349
|
+
margin-bottom: 2rem;
|
|
350
|
+
padding-bottom: 1.25rem;
|
|
351
|
+
border-bottom: 1px solid var(--border);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.page-date {
|
|
355
|
+
font-size: 0.75rem;
|
|
356
|
+
color: var(--muted-light);
|
|
357
|
+
letter-spacing: 0.05em;
|
|
358
|
+
display: block;
|
|
359
|
+
margin-bottom: 0.4rem;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.page-description {
|
|
363
|
+
color: var(--muted-light) !important;
|
|
364
|
+
font-size: 0.9rem;
|
|
365
|
+
margin: 0 !important;
|
|
366
|
+
}
|
|
367
|
+
</style>
|
|
208
368
|
</head>
|
|
209
369
|
|
|
210
370
|
<body>
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
371
|
+
<nav id="sidebar">
|
|
372
|
+
<a class="nav-logo" href="index.html">markform</a>
|
|
373
|
+
<div class="nav-divider"></div>
|
|
374
|
+
<input id="search" type="text" placeholder="Search..." />
|
|
375
|
+
<span class="nav-section-label">Pages</span>
|
|
376
|
+
<ul id="nav-list">{{nav}}</ul>
|
|
377
|
+
<ul id="search-results" style="display:none;"></ul>
|
|
378
|
+
</nav>
|
|
379
|
+
<main>
|
|
380
|
+
{{content}}
|
|
381
|
+
</main>
|
|
382
|
+
|
|
383
|
+
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7/dist/fuse.min.js"></script>
|
|
384
|
+
<script>
|
|
385
|
+
const searchData = {{ search_index }};
|
|
386
|
+
|
|
387
|
+
if (document.body.dataset.page === 'index') {
|
|
388
|
+
document.getElementById('sidebar').classList.add('hidden');
|
|
389
|
+
document.body.classList.add('index-page');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const fuse = new Fuse(searchData, {
|
|
393
|
+
keys: ['title', 'content'],
|
|
394
|
+
threshold: 0.1,
|
|
395
|
+
includeScore: true,
|
|
396
|
+
ignoreLocation: true,
|
|
397
|
+
minMatchCharLength: 3,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
function highlight(text, query) {
|
|
401
|
+
if (!query) return text;
|
|
402
|
+
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
|
403
|
+
return text.replace(regex, '<mark>$1</mark>');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function getSnippet(content, query) {
|
|
407
|
+
const idx = content.toLowerCase().indexOf(query.toLowerCase());
|
|
408
|
+
if (idx === -1) return content.slice(0, 120) + '...';
|
|
409
|
+
const start = Math.max(0, idx - 40);
|
|
410
|
+
const end = Math.min(content.length, idx + 100);
|
|
411
|
+
return (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : '');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function resolveHref(href) {
|
|
415
|
+
const isFile = window.location.protocol === 'file:';
|
|
416
|
+
if (!isFile) return '/' + href;
|
|
417
|
+
const parts = window.location.pathname.split('/');
|
|
418
|
+
parts.pop();
|
|
419
|
+
return 'file://' + parts.join('/') + '/' + href;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const searchEl = document.getElementById('search');
|
|
423
|
+
if (searchEl) {
|
|
424
|
+
searchEl.addEventListener('input', function () {
|
|
425
|
+
const query = this.value.trim();
|
|
426
|
+
const navList = document.getElementById('nav-list');
|
|
427
|
+
const results = document.getElementById('search-results');
|
|
428
|
+
|
|
429
|
+
if (!query || query.length < 2) {
|
|
430
|
+
navList.style.display = '';
|
|
431
|
+
results.style.display = 'none';
|
|
432
|
+
results.innerHTML = '';
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const matches = fuse.search(query).slice(0, 8);
|
|
437
|
+
navList.style.display = 'none';
|
|
438
|
+
results.style.display = '';
|
|
439
|
+
results.innerHTML = matches.length
|
|
440
|
+
? matches.map(m => `
|
|
441
|
+
<li>
|
|
442
|
+
<a href="${resolveHref(m.item.href)}">${highlight(m.item.title, query)}</a>
|
|
443
|
+
<p>${highlight(getSnippet(m.item.content, query), query)}</p>
|
|
444
|
+
</li>`).join('')
|
|
445
|
+
: `<li><p>No results found.</p></li>`;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
</script>
|
|
281
449
|
</body>
|
|
282
450
|
|
|
283
451
|
</html>
|