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 CHANGED
@@ -1,4 +1,6 @@
1
- ![Markdown Banner](https://github.com/aryan-madan/Markform/blob/main/images/Markform%20|%20Devlog%202%20.png)
1
+ # markform 📝
2
+
3
+ ![Markdown Banner](images/Markform%20|%20Banner.png)
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 windows), use:
28
+ If it doesn't work (mainly on Windows), use:
27
29
  ```bash
28
- npx markform-cli ./test -o ./output
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
- | `--theme <name>` | `default` | Theme to use |
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.1.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
- "homepage": "https://github.com/yourusername/markform",
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
- "marked": "^17.0.3"
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 { buildIndexPage } = require('./index-page');
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, options.watch || false);
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
- const mdFiles = getMdFiles(inputDir);
34
- const firstHref = path.relative(inputDir, mdFiles[0]).replace(/\.md$/, '.html');
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 to ${outputDir}`);
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 content = fs.readFileSync(filePath, 'utf-8');
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}}', path.basename(filePath, '.md'));
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
- async function buildSearchIndex(mdFiles, inputDir, outputDir) {
106
- const index = mdFiles.map((filePath) => {
134
+ function buildSearchIndex(mdFiles, inputDir) {
135
+ return mdFiles.map((filePath) => {
107
136
  const raw = fs.readFileSync(filePath, 'utf-8');
108
- const content = raw
109
- .replace(/#{1,6}\s/g, '') // headings
110
- .replace(/\*\*|__|\*|_/g, '') // bold/italic
111
- .replace(/`{1,3}[^`]*`{1,3}/g, '') // code
112
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links
113
- .replace(/^\s*[-*+]\s/gm, '') // list items
114
- .replace(/\n+/g, ' ') // newlines
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="/${href}" class="card">
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.0.0')
10
- .argument('<input>', 'folder of Markdown files')
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
- buildSite(input, options);
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
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: Post
3
+ date: 2026-02-27
4
+ description:
5
+ ---
6
+
7
+ # Post
8
+
9
+ Write something here...
@@ -2,282 +2,450 @@
2
2
  <html lang="en">
3
3
 
4
4
  <head>
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
- box-sizing: border-box;
11
- margin: 0;
12
- padding: 0;
13
- }
14
-
15
- body {
16
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
17
- display: flex;
18
- min-height: 100vh;
19
- background: #f9f9f9;
20
- color: #1a1a1a;
21
- }
22
-
23
- nav {
24
- width: 240px;
25
- min-height: 100vh;
26
- background: #1e1e2e;
27
- padding: 2rem 1.5rem;
28
- position: sticky;
29
- top: 0;
30
- }
31
-
32
- nav h2 {
33
- color: #cdd6f4;
34
- font-size: 1rem;
35
- text-transform: uppercase;
36
- letter-spacing: 0.1em;
37
- margin-bottom: 1rem;
38
- }
39
-
40
- #search {
41
- width: 100%;
42
- padding: 0.5rem 0.75rem;
43
- margin-bottom: 1.25rem;
44
- border-radius: 6px;
45
- border: none;
46
- background: #313244;
47
- color: #cdd6f4;
48
- font-size: 0.9rem;
49
- outline: none;
50
- }
51
-
52
- #search::placeholder {
53
- color: #6c7086;
54
- }
55
-
56
- nav ul {
57
- list-style: none;
58
- }
59
-
60
- nav ul li {
61
- margin-bottom: 0.6rem;
62
- }
63
-
64
- nav ul li a {
65
- color: #a6adc8;
66
- text-decoration: none;
67
- font-size: 0.95rem;
68
- transition: color 0.2s;
69
- text-transform: capitalize;
70
- }
71
-
72
- nav ul li a:hover {
73
- color: #cdd6f4;
74
- }
75
-
76
- main {
77
- flex: 1;
78
- max-width: 800px;
79
- margin: 0 auto;
80
- padding: 3rem 2rem;
81
- }
82
-
83
- h1,
84
- h2,
85
- h3 {
86
- margin: 1.5rem 0 0.75rem;
87
- line-height: 1.3;
88
- }
89
-
90
- h1 {
91
- font-size: 2rem;
92
- }
93
-
94
- h2 {
95
- font-size: 1.5rem;
96
- }
97
-
98
- h3 {
99
- font-size: 1.2rem;
100
- }
101
-
102
- p {
103
- line-height: 1.75;
104
- margin-bottom: 1rem;
105
- color: #333;
106
- }
107
-
108
- code {
109
- background: #ececec;
110
- padding: 0.2em 0.4em;
111
- border-radius: 4px;
112
- font-size: 0.9em;
113
- font-family: 'Fira Code', monospace;
114
- }
115
-
116
- pre {
117
- background: #1e1e2e;
118
- color: #cdd6f4;
119
- padding: 1.25rem;
120
- border-radius: 8px;
121
- overflow-x: auto;
122
- margin-bottom: 1.25rem;
123
- }
124
-
125
- pre code {
126
- background: none;
127
- color: inherit;
128
- padding: 0;
129
- }
130
-
131
- blockquote {
132
- border-left: 4px solid #cdd6f4;
133
- padding-left: 1rem;
134
- color: #666;
135
- margin: 1rem 0;
136
- font-style: italic;
137
- }
138
-
139
- a {
140
- color: #7287fd;
141
- }
142
-
143
- a:hover {
144
- text-decoration: underline;
145
- }
146
-
147
- img {
148
- max-width: 100%;
149
- border-radius: 6px;
150
- margin: 1rem 0;
151
- }
152
-
153
- table {
154
- width: 100%;
155
- border-collapse: collapse;
156
- margin-bottom: 1rem;
157
- }
158
-
159
- th,
160
- td {
161
- padding: 0.6rem 1rem;
162
- border: 1px solid #ddd;
163
- text-align: left;
164
- }
165
-
166
- th {
167
- background: #f0f0f0;
168
- }
169
-
170
- .card-grid {
171
- display: grid;
172
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
173
- gap: 1rem;
174
- margin-top: 1.5rem;
175
- }
176
-
177
- .card {
178
- display: flex;
179
- flex-direction: column;
180
- padding: 1.25rem;
181
- background: #fff;
182
- border: 1px solid #e0e0e0;
183
- border-radius: 10px;
184
- text-decoration: none;
185
- transition: box-shadow 0.2s, transform 0.2s;
186
- }
187
-
188
- .card:hover {
189
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
190
- transform: translateY(-2px);
191
- }
192
-
193
- .card-folder {
194
- font-size: 0.75rem;
195
- color: #888;
196
- text-transform: uppercase;
197
- letter-spacing: 0.05em;
198
- margin-bottom: 0.4rem;
199
- }
200
-
201
- .card-title {
202
- font-size: 1rem;
203
- font-weight: 600;
204
- color: #1a1a1a;
205
- text-transform: capitalize;
206
- }
207
- </style>
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
- <nav>
212
- <h2>markform</h2>
213
- <input id="search" type="text" placeholder="Search..." />
214
- <ul id="nav-list">{{nav}}</ul>
215
- <ul id="search-results" style="list-style:none; display:none;"></ul>
216
- </nav>
217
- <main>
218
- {{content}}
219
- </main>
220
-
221
- <script src="https://cdn.jsdelivr.net/npm/fuse.js@7/dist/fuse.min.js"></script>
222
- <script>
223
- let fuse;
224
-
225
- fetch('/search-index.json')
226
- .then(r => r.json())
227
- .then(data => {
228
- fuse = new Fuse(data, {
229
- keys: ['title', 'content'],
230
- threshold: 0.1,
231
- includeScore: true,
232
- ignoreLocation: true,
233
- minMatchCharLength: 3,
234
- });
235
- });
236
-
237
- function highlight(text, query) {
238
- if (!query) return text;
239
- const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
240
- return text.replace(regex, '<mark style="background:#f9e2af;color:#1e1e2e;border-radius:3px;padding:0 2px;">$1</mark>');
241
- }
242
-
243
- function getSnippet(content, query) {
244
- const idx = content.toLowerCase().indexOf(query.toLowerCase());
245
- if (idx === -1) return content.slice(0, 100) + '...';
246
- const start = Math.max(0, idx - 40);
247
- const end = Math.min(content.length, idx + 80);
248
- return (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : '');
249
- }
250
-
251
- document.getElementById('search').addEventListener('input', function () {
252
- const query = this.value.trim();
253
- const navList = document.getElementById('nav-list');
254
- const results = document.getElementById('search-results');
255
-
256
- if (!query || !fuse || query.length < 2) {
257
- navList.style.display = '';
258
- results.style.display = 'none';
259
- results.innerHTML = '';
260
- return;
261
- }
262
-
263
- const matches = fuse.search(query).slice(0, 8);
264
- navList.style.display = 'none';
265
- results.style.display = '';
266
- results.innerHTML = matches.length
267
- ? matches.map(m => {
268
- const snippet = getSnippet(m.item.content, query);
269
- return `<li style="margin-bottom:1rem">
270
- <a href="/${m.item.href}" style="color:#cdd6f4;text-decoration:none;font-size:0.95rem;text-transform:capitalize;font-weight:600;">
271
- ${highlight(m.item.title, query)}
272
- </a>
273
- <p style="color:#6c7086;font-size:0.8rem;margin-top:0.25rem;line-height:1.4;">
274
- ${highlight(snippet, query)}
275
- </p>
276
- </li>`;
277
- }).join('')
278
- : `<li style="color:#6c7086;font-size:0.9rem">No results found.</li>`;
279
- });
280
- </script>
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>