markform-cli 1.1.1 → 1.2.1

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,4 @@
1
- ![Markdown Banner](https://github.com/aryan-madan/Markform/blob/main/images/Markform%20|%20Devlog%202%20.png)
1
+ ![Markdown Banner](images/Markform%20|%20Banner.png)
2
2
 
3
3
  Turn any folder of Markdown files into a beautiful, searchable static site — in one command.
4
4
  ```bash
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.1",
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,7 @@
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
+ "marked": "^17.0.3",
32
+ "open": "^11.0.0"
32
33
  }
33
34
  }
package/src/build.js CHANGED
@@ -2,9 +2,19 @@ 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
+
10
+ function openInBrowser(url) {
11
+ const cmd = process.platform === 'win32'
12
+ ? `start "" "${url}"`
13
+ : process.platform === 'darwin'
14
+ ? `open "${url}"`
15
+ : `xdg-open "${url}"`;
16
+ exec(cmd);
17
+ }
8
18
 
9
19
  async function buildSite(input, options) {
10
20
  const inputDir = path.resolve(input);
@@ -16,7 +26,7 @@ async function buildSite(input, options) {
16
26
  }
17
27
 
18
28
  console.log(`⛏ Building site from ${inputDir} to ${outputDir}...`);
19
- await compile(inputDir, outputDir, options.watch || false);
29
+ await compile(inputDir, outputDir, false);
20
30
  console.log(`(ˆᗜˆ ) Site built to ${outputDir}`);
21
31
 
22
32
  if (options.watch) {
@@ -30,19 +40,22 @@ async function buildSite(input, options) {
30
40
  });
31
41
 
32
42
  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`);
43
+ console.log(`(°ㅁ° ) Serving at http://localhost:3000/index.html`);
44
+ openInBrowser('http://localhost:3000/index.html');
36
45
  });
37
46
 
38
47
  chokidar.watch(inputDir).on('change', async (filePath) => {
39
48
  console.log(`↺ Changed: ${filePath}, rebuilding...`);
40
49
  await compile(inputDir, outputDir, true);
41
50
  shouldReload = true;
42
- console.log(`(ˆᗜˆ ) Site rebuilt to ${outputDir}`);
51
+ console.log(`(ˆᗜˆ ) Site rebuilt.`);
43
52
  });
44
53
 
45
54
  console.log(`(≖_≖ ) Watching for changes...`);
55
+ } else {
56
+ const indexPath = path.join(outputDir, 'index.html');
57
+ console.log(`(°ㅁ° ) Opening ${indexPath}`);
58
+ openInBrowser(indexPath);
46
59
  }
47
60
  }
48
61
 
@@ -51,12 +64,13 @@ async function compile(inputDir, outputDir, watch = false) {
51
64
 
52
65
  const mdFiles = getMdFiles(inputDir);
53
66
  const nav = buildNav(mdFiles, inputDir);
67
+ const searchIndex = buildSearchIndex(mdFiles, inputDir);
68
+ const searchIndexJson = JSON.stringify(searchIndex);
69
+
54
70
  const template = fs.readFileSync(
55
71
  path.join(__dirname, '../themes/default.html'), 'utf-8'
56
72
  );
57
73
 
58
- await buildSearchIndex(mdFiles, inputDir, outputDir);
59
-
60
74
  for (const filePath of mdFiles) {
61
75
  const content = fs.readFileSync(filePath, 'utf-8');
62
76
  const htmlContent = marked(content);
@@ -66,7 +80,8 @@ async function compile(inputDir, outputDir, watch = false) {
66
80
  let html = template
67
81
  .replace('{{content}}', htmlContent)
68
82
  .replace('{{nav}}', nav)
69
- .replace('{{title}}', path.basename(filePath, '.md'));
83
+ .replace('{{title}}', path.basename(filePath, '.md'))
84
+ .replace('{{search_index}}', searchIndexJson);
70
85
 
71
86
  if (watch) {
72
87
  html = html.replace('</body>', `
@@ -80,12 +95,12 @@ async function compile(inputDir, outputDir, watch = false) {
80
95
  </body>`);
81
96
  }
82
97
 
83
- const indexHtml = buildIndexPage(mdFiles, inputDir, template);
84
- await fs.writeFile(path.join(outputDir, 'index.html'), indexHtml);
85
-
86
98
  await fs.ensureDir(path.dirname(outputPath));
87
99
  await fs.writeFile(outputPath, html);
88
100
  }
101
+
102
+ const indexHtml = buildIndexPage(mdFiles, inputDir, template, searchIndexJson);
103
+ await fs.writeFile(path.join(outputDir, 'index.html'), indexHtml);
89
104
  }
90
105
 
91
106
  function getMdFiles(dir) {
@@ -102,27 +117,23 @@ function getMdFiles(dir) {
102
117
  return results;
103
118
  }
104
119
 
105
- async function buildSearchIndex(mdFiles, inputDir, outputDir) {
106
- const index = mdFiles.map((filePath) => {
120
+ function buildSearchIndex(mdFiles, inputDir) {
121
+ return mdFiles.map((filePath) => {
107
122
  const raw = fs.readFileSync(filePath, 'utf-8');
108
123
  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
124
+ .replace(/#{1,6}\s/g, '')
125
+ .replace(/\*\*|__|\*|_/g, '')
126
+ .replace(/`{1,3}[^`]*`{1,3}/g, '')
127
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
128
+ .replace(/^\s*[-*+]\s/gm, '')
129
+ .replace(/\n+/g, ' ')
115
130
  .trim();
131
+
116
132
  const relativePath = path.relative(inputDir, filePath);
117
133
  const href = relativePath.replace(/\.md$/, '.html');
118
134
  const title = path.basename(filePath, '.md').replace(/-/g, ' ');
119
135
  return { title, href, content };
120
136
  });
121
-
122
- await fs.writeFile(
123
- path.join(outputDir, 'search-index.json'),
124
- JSON.stringify(index, null, 2)
125
- );
126
137
  }
127
138
 
128
139
  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,25 @@ 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);
24
30
  }
25
31
 
26
32
  module.exports = { buildIndexPage };
@@ -1,283 +1,328 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
-
4
3
  <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>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{title}}</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ :root {
11
+ --bg: #0e0e0e;
12
+ --surface: #161616;
13
+ --border: #222222;
14
+ --text: #e2e2e2;
15
+ --muted: #444;
16
+ --muted-light: #777;
17
+ --nav-width: 240px;
18
+ }
19
+
20
+ body {
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
22
+ display: flex;
23
+ min-height: 100vh;
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ line-height: 1.7;
27
+ }
28
+
29
+ /* ── NAV ── */
30
+ nav {
31
+ width: var(--nav-width);
32
+ min-height: 100vh;
33
+ background: var(--surface);
34
+ border-right: 1px solid var(--border);
35
+ padding: 1.75rem 1rem;
36
+ position: sticky;
37
+ top: 0;
38
+ height: 100vh;
39
+ overflow-y: auto;
40
+ flex-shrink: 0;
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 0.25rem;
44
+ }
45
+
46
+ nav.hidden { display: none; }
47
+
48
+ .nav-logo {
49
+ font-size: 0.65rem;
50
+ font-weight: 800;
51
+ letter-spacing: 0.2em;
52
+ text-transform: uppercase;
53
+ color: var(--muted-light);
54
+ text-decoration: none;
55
+ padding: 0 0.5rem;
56
+ margin-bottom: 0.75rem;
57
+ display: block;
58
+ }
59
+
60
+ .nav-logo:hover { color: var(--text); }
61
+
62
+ .nav-divider {
63
+ height: 1px;
64
+ background: var(--border);
65
+ margin: 0.5rem 0;
66
+ }
67
+
68
+ #search {
69
+ width: 100%;
70
+ padding: 0.5rem 0.6rem;
71
+ border: 1px solid var(--border);
72
+ background: var(--bg);
73
+ color: var(--text);
74
+ font-size: 0.8rem;
75
+ outline: none;
76
+ font-family: inherit;
77
+ margin-bottom: 0.5rem;
78
+ }
79
+
80
+ #search:focus { border-color: #444; }
81
+ #search::placeholder { color: var(--muted); }
82
+
83
+ .nav-section-label {
84
+ font-size: 0.6rem;
85
+ font-weight: 700;
86
+ letter-spacing: 0.15em;
87
+ text-transform: uppercase;
88
+ color: var(--muted);
89
+ padding: 0 0.5rem;
90
+ margin: 0.75rem 0 0.35rem;
91
+ }
92
+
93
+ #nav-list { list-style: none; }
94
+ #nav-list li a {
95
+ color: var(--muted-light);
96
+ text-decoration: none;
97
+ font-size: 0.82rem;
98
+ display: block;
99
+ padding: 0.3rem 0.5rem;
100
+ text-transform: capitalize;
101
+ border-left: 2px solid transparent;
102
+ }
103
+
104
+ #nav-list li a:hover {
105
+ color: var(--text);
106
+ border-left-color: var(--muted);
107
+ }
108
+
109
+ #search-results { list-style: none; }
110
+ #search-results li { padding: 0.6rem 0.5rem; border-bottom: 1px solid var(--border); }
111
+ #search-results a {
112
+ font-size: 0.82rem;
113
+ font-weight: 600;
114
+ color: var(--text);
115
+ text-decoration: none;
116
+ text-transform: capitalize;
117
+ display: block;
118
+ margin-bottom: 0.15rem;
119
+ }
120
+ #search-results a:hover { color: #fff; }
121
+ #search-results p {
122
+ color: var(--muted-light);
123
+ font-size: 0.72rem;
124
+ line-height: 1.5;
125
+ margin: 0;
126
+ }
127
+
128
+ /* ── MAIN ── */
129
+ main {
130
+ flex: 1;
131
+ max-width: 700px;
132
+ margin: 0 auto;
133
+ padding: 4rem 2.5rem;
134
+ }
135
+
136
+ /* index page gets full width */
137
+ body.index-page main {
138
+ max-width: 900px;
139
+ }
140
+
141
+ /* ── TYPOGRAPHY ── */
142
+ h1 { font-size: 1.75rem; font-weight: 700; margin-bottom: 0.75rem; line-height: 1.2; }
143
+ h2 { font-size: 1.2rem; font-weight: 600; margin: 2.25rem 0 0.5rem; }
144
+ h3 { font-size: 1rem; font-weight: 600; margin: 1.5rem 0 0.4rem; }
145
+
146
+ p { margin-bottom: 1rem; color: #aaa; }
147
+
148
+ a { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
149
+ a:hover { color: #fff; }
150
+
151
+ code {
152
+ font-family: 'SF Mono', 'Fira Code', monospace;
153
+ font-size: 0.82em;
154
+ background: var(--surface);
155
+ color: #ccc;
156
+ padding: 0.15em 0.4em;
157
+ border: 1px solid var(--border);
158
+ }
159
+
160
+ pre {
161
+ background: var(--surface);
162
+ border: 1px solid var(--border);
163
+ padding: 1.25rem 1.5rem;
164
+ overflow-x: auto;
165
+ margin-bottom: 1.25rem;
166
+ font-size: 0.82rem;
167
+ line-height: 1.7;
168
+ }
169
+
170
+ pre code { background: none; color: #ccc; padding: 0; border: none; font-size: inherit; }
171
+
172
+ blockquote {
173
+ border-left: 2px solid var(--border);
174
+ padding: 0.4rem 0 0.4rem 1rem;
175
+ color: var(--muted-light);
176
+ margin: 1.25rem 0;
177
+ font-style: italic;
178
+ }
179
+
180
+ hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
181
+ img { max-width: 100%; margin: 1rem 0; }
182
+
183
+ table { width: 100%; border-collapse: collapse; margin-bottom: 1.25rem; font-size: 0.875rem; }
184
+ th, td { padding: 0.6rem 0.875rem; border: 1px solid var(--border); text-align: left; }
185
+ th { background: var(--surface); font-weight: 600; color: var(--text); }
186
+ td { color: #aaa; }
187
+
188
+ ul, ol { padding-left: 1.5rem; margin-bottom: 1rem; color: #aaa; }
189
+ li { margin-bottom: 0.25rem; }
190
+
191
+ /* ── INDEX PAGE ── */
192
+ .index-header {
193
+ margin-bottom: 3rem;
194
+ border-bottom: 1px solid var(--border);
195
+ padding-bottom: 2rem;
196
+ }
197
+
198
+ .index-header h1 {
199
+ font-size: 2rem;
200
+ margin-bottom: 0.4rem;
201
+ }
202
+
203
+ .index-header p {
204
+ color: var(--muted-light);
205
+ font-size: 0.9rem;
206
+ margin: 0;
207
+ }
208
+
209
+ .card-grid {
210
+ display: grid;
211
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
212
+ gap: 1px;
213
+ background: var(--border);
214
+ border: 1px solid var(--border);
215
+ }
216
+
217
+ .card {
218
+ display: flex;
219
+ flex-direction: column;
220
+ padding: 1.25rem;
221
+ background: var(--bg);
222
+ text-decoration: none;
223
+ transition: background 0.1s;
224
+ }
225
+
226
+ .card:hover { background: var(--surface); }
227
+
228
+ .card-folder {
229
+ font-size: 0.6rem;
230
+ color: var(--muted);
231
+ text-transform: uppercase;
232
+ letter-spacing: 0.12em;
233
+ margin-bottom: 0.4rem;
234
+ }
235
+
236
+ .card-title {
237
+ font-size: 0.875rem;
238
+ font-weight: 500;
239
+ color: var(--text);
240
+ text-transform: capitalize;
241
+ }
242
+
243
+ /* ── SEARCH HIGHLIGHT ── */
244
+ mark { background: #2a2a2a; color: #fff; padding: 0 2px; }
245
+ </style>
208
246
  </head>
209
-
210
247
  <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>
248
+ <nav id="sidebar">
249
+ <a class="nav-logo" href="index.html">markform</a>
250
+ <div class="nav-divider"></div>
251
+ <input id="search" type="text" placeholder="Search..." />
252
+ <span class="nav-section-label">Pages</span>
253
+ <ul id="nav-list">{{nav}}</ul>
254
+ <ul id="search-results" style="display:none;"></ul>
255
+ </nav>
256
+ <main>
257
+ {{content}}
258
+ </main>
259
+
260
+ <script src="https://cdn.jsdelivr.net/npm/fuse.js@7/dist/fuse.min.js"></script>
261
+ <script>
262
+ const searchData = {{search_index}};
263
+
264
+ // Hide sidebar on index page
265
+ if (document.title === 'Home') {
266
+ document.getElementById('sidebar').classList.add('hidden');
267
+ document.body.classList.add('index-page');
268
+ }
269
+
270
+ const fuse = new Fuse(searchData, {
271
+ keys: ['title', 'content'],
272
+ threshold: 0.1,
273
+ includeScore: true,
274
+ ignoreLocation: true,
275
+ minMatchCharLength: 3,
276
+ });
277
+
278
+ function highlight(text, query) {
279
+ if (!query) return text;
280
+ const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
281
+ return text.replace(regex, '<mark>$1</mark>');
282
+ }
283
+
284
+ function getSnippet(content, query) {
285
+ const idx = content.toLowerCase().indexOf(query.toLowerCase());
286
+ if (idx === -1) return content.slice(0, 120) + '...';
287
+ const start = Math.max(0, idx - 40);
288
+ const end = Math.min(content.length, idx + 100);
289
+ return (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : '');
290
+ }
291
+
292
+ function resolveHref(href) {
293
+ const isFile = window.location.protocol === 'file:';
294
+ if (!isFile) return '/' + href;
295
+ const parts = window.location.pathname.split('/');
296
+ parts.pop();
297
+ return 'file://' + parts.join('/') + '/' + href;
298
+ }
299
+
300
+ const searchEl = document.getElementById('search');
301
+ if (searchEl) {
302
+ searchEl.addEventListener('input', function () {
303
+ const query = this.value.trim();
304
+ const navList = document.getElementById('nav-list');
305
+ const results = document.getElementById('search-results');
306
+
307
+ if (!query || query.length < 2) {
308
+ navList.style.display = '';
309
+ results.style.display = 'none';
310
+ results.innerHTML = '';
311
+ return;
312
+ }
313
+
314
+ const matches = fuse.search(query).slice(0, 8);
315
+ navList.style.display = 'none';
316
+ results.style.display = '';
317
+ results.innerHTML = matches.length
318
+ ? matches.map(m => `
319
+ <li>
320
+ <a href="${resolveHref(m.item.href)}">${highlight(m.item.title, query)}</a>
321
+ <p>${highlight(getSnippet(m.item.content, query), query)}</p>
322
+ </li>`).join('')
323
+ : `<li><p>No results found.</p></li>`;
324
+ });
325
+ }
326
+ </script>
281
327
  </body>
282
-
283
328
  </html>