@masumdev/markforge 0.3.0 → 0.4.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 +167 -140
- package/dist/App-TPDCT2VL.mjs +393 -0
- package/dist/{chunk-IYGPJIK5.mjs → chunk-BPHDY6VB.mjs} +6 -0
- package/dist/{App-AM2CWXHJ.mjs → chunk-R6DT335C.mjs} +2140 -924
- package/dist/{chunk-HKB4CSPZ.mjs → chunk-UNWAEVDU.mjs} +4 -3
- package/dist/cli.mjs +38 -8
- package/dist/index.d.mts +554 -136
- package/dist/index.d.ts +554 -136
- package/dist/index.js +2652 -227
- package/dist/index.mjs +2643 -228
- package/dist/{loadConfig-PD6ENMAM.mjs → loadConfig-PGJKPE6G.mjs} +1 -1
- package/dist/previewServer-4EELOUAV.mjs +893 -0
- package/package.json +5 -1
- package/schema.json +142 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try {
|
|
3
|
+
if (typeof globalThis !== "undefined" && (!globalThis.localStorage || typeof globalThis.localStorage.getItem !== "function")) {
|
|
4
|
+
Object.defineProperty(globalThis, "localStorage", {
|
|
5
|
+
value: { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {}, key: () => null, length: 0 },
|
|
6
|
+
configurable: true, writable: true,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
} catch {}
|
|
10
|
+
import {
|
|
11
|
+
buildDocxDocument,
|
|
12
|
+
buildHtmlDocument,
|
|
13
|
+
buildPdfDocument,
|
|
14
|
+
parseMarkdownDocument
|
|
15
|
+
} from "./chunk-R6DT335C.mjs";
|
|
16
|
+
import {
|
|
17
|
+
loadConfig
|
|
18
|
+
} from "./chunk-BPHDY6VB.mjs";
|
|
19
|
+
import "./chunk-YZHGBG4N.mjs";
|
|
20
|
+
|
|
21
|
+
// src/server/previewServer.ts
|
|
22
|
+
import * as http from "http";
|
|
23
|
+
import * as fs from "fs";
|
|
24
|
+
import * as path from "path";
|
|
25
|
+
async function startPreviewServer(options) {
|
|
26
|
+
const absoluteFilePath = path.resolve(process.cwd(), options.filePath);
|
|
27
|
+
if (!fs.existsSync(absoluteFilePath)) {
|
|
28
|
+
throw new Error(`MarkForge preview error: File not found at "${absoluteFilePath}"`);
|
|
29
|
+
}
|
|
30
|
+
const baseDir = path.dirname(absoluteFilePath);
|
|
31
|
+
const { config: fileConfig } = await loadConfig(void 0, baseDir);
|
|
32
|
+
const baseConfig = options.config || fileConfig;
|
|
33
|
+
const port = options.port || 3e3;
|
|
34
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
35
|
+
const broadcastReload = () => {
|
|
36
|
+
sseClients.forEach((client) => {
|
|
37
|
+
try {
|
|
38
|
+
client.write(`event: reload
|
|
39
|
+
data: ${Date.now()}
|
|
40
|
+
|
|
41
|
+
`);
|
|
42
|
+
} catch {
|
|
43
|
+
sseClients.delete(client);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
let debounceTimer = null;
|
|
48
|
+
const watcher = fs.watch(baseDir, { recursive: false }, (_event, filename) => {
|
|
49
|
+
if (!filename) return;
|
|
50
|
+
const changedPath = path.resolve(baseDir, filename);
|
|
51
|
+
if (changedPath === absoluteFilePath || filename.includes("markforge") || filename.endsWith(".css")) {
|
|
52
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
53
|
+
debounceTimer = setTimeout(() => {
|
|
54
|
+
broadcastReload();
|
|
55
|
+
}, 150);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const server = http.createServer(async (req, res) => {
|
|
59
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
60
|
+
if (url.pathname === "/events") {
|
|
61
|
+
res.writeHead(200, {
|
|
62
|
+
"Content-Type": "text/event-stream",
|
|
63
|
+
"Cache-Control": "no-cache, no-transform",
|
|
64
|
+
Connection: "keep-alive"
|
|
65
|
+
});
|
|
66
|
+
res.write(`data: connected
|
|
67
|
+
|
|
68
|
+
`);
|
|
69
|
+
sseClients.add(res);
|
|
70
|
+
req.on("close", () => {
|
|
71
|
+
sseClients.delete(res);
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (url.pathname === "/api/file-content" && req.method === "GET") {
|
|
76
|
+
try {
|
|
77
|
+
const content = fs.readFileSync(absoluteFilePath, "utf-8");
|
|
78
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
79
|
+
res.end(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
content,
|
|
82
|
+
fileName: path.basename(absoluteFilePath),
|
|
83
|
+
filePath: absoluteFilePath
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
89
|
+
res.end(JSON.stringify({ error: msg }));
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (url.pathname === "/api/save-content" && req.method === "POST") {
|
|
94
|
+
let body = "";
|
|
95
|
+
req.on("data", (chunk) => {
|
|
96
|
+
body += chunk;
|
|
97
|
+
});
|
|
98
|
+
req.on("end", () => {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(body);
|
|
101
|
+
if (typeof parsed.content === "string") {
|
|
102
|
+
fs.writeFileSync(absoluteFilePath, parsed.content, "utf-8");
|
|
103
|
+
broadcastReload();
|
|
104
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
105
|
+
res.end(JSON.stringify({ success: true, savedAt: Date.now() }));
|
|
106
|
+
} else {
|
|
107
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
108
|
+
res.end(JSON.stringify({ error: "Missing content field in request body" }));
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
112
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
113
|
+
res.end(JSON.stringify({ error: msg }));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (url.pathname === "/api/export" && (req.method === "GET" || req.method === "POST")) {
|
|
119
|
+
const format = url.searchParams.get("format") || "docx";
|
|
120
|
+
try {
|
|
121
|
+
const mdContent = fs.readFileSync(absoluteFilePath, "utf-8");
|
|
122
|
+
const doc = parseMarkdownDocument(mdContent);
|
|
123
|
+
const { config: resolvedConfig } = await loadConfig(void 0, baseDir);
|
|
124
|
+
const mergedConfig = { ...baseConfig, ...resolvedConfig };
|
|
125
|
+
const fileBase = path.basename(absoluteFilePath, path.extname(absoluteFilePath));
|
|
126
|
+
if (format === "docx") {
|
|
127
|
+
const buffer = await buildDocxDocument(doc, mergedConfig, baseDir);
|
|
128
|
+
res.writeHead(200, {
|
|
129
|
+
"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
130
|
+
"Content-Disposition": `attachment; filename="${fileBase}.docx"`
|
|
131
|
+
});
|
|
132
|
+
res.end(buffer);
|
|
133
|
+
return;
|
|
134
|
+
} else if (format === "pdf") {
|
|
135
|
+
const buffer = await buildPdfDocument(doc, mergedConfig, baseDir);
|
|
136
|
+
res.writeHead(200, {
|
|
137
|
+
"Content-Type": "application/pdf",
|
|
138
|
+
"Content-Disposition": `attachment; filename="${fileBase}.pdf"`
|
|
139
|
+
});
|
|
140
|
+
res.end(buffer);
|
|
141
|
+
return;
|
|
142
|
+
} else {
|
|
143
|
+
const html = await buildHtmlDocument(doc, mergedConfig, baseDir);
|
|
144
|
+
res.writeHead(200, {
|
|
145
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
146
|
+
"Content-Disposition": `attachment; filename="${fileBase}.html"`
|
|
147
|
+
});
|
|
148
|
+
res.end(html);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
154
|
+
res.end(`Export failed: ${msg}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (url.pathname === "/document-content") {
|
|
159
|
+
try {
|
|
160
|
+
const mdContent = fs.readFileSync(absoluteFilePath, "utf-8");
|
|
161
|
+
const doc = parseMarkdownDocument(mdContent);
|
|
162
|
+
const { config: resolvedConfig } = await loadConfig(void 0, baseDir);
|
|
163
|
+
const html = await buildHtmlDocument(doc, { ...baseConfig, ...resolvedConfig }, baseDir);
|
|
164
|
+
const injectedScript = `
|
|
165
|
+
<script>
|
|
166
|
+
(function() {
|
|
167
|
+
var evtSource = new EventSource('/events');
|
|
168
|
+
evtSource.addEventListener('reload', function() {
|
|
169
|
+
var scrollPos = window.scrollY;
|
|
170
|
+
sessionStorage.setItem('markforge_scroll', scrollPos);
|
|
171
|
+
window.location.reload();
|
|
172
|
+
});
|
|
173
|
+
window.addEventListener('load', function() {
|
|
174
|
+
var saved = sessionStorage.getItem('markforge_scroll');
|
|
175
|
+
if (saved) {
|
|
176
|
+
window.scrollTo(0, parseInt(saved, 10));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
})();
|
|
180
|
+
</script>
|
|
181
|
+
`;
|
|
182
|
+
const finalHtml = html.replace("</body>", `${injectedScript}</body>`);
|
|
183
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
184
|
+
res.end(finalHtml);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
187
|
+
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
188
|
+
res.end(`<div style="padding:2rem;font-family:sans-serif;color:#ef4444;background:#fef2f2;border:1px solid #f87171;border-radius:8px;"><h3>MarkForge Compilation Error</h3><pre>${escapeHtml(msg)}</pre></div>`);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
193
|
+
const fileName = path.basename(absoluteFilePath);
|
|
194
|
+
const initialContent = fs.readFileSync(absoluteFilePath, "utf-8");
|
|
195
|
+
const appHtml = `<!DOCTYPE html>
|
|
196
|
+
<html lang="en">
|
|
197
|
+
<head>
|
|
198
|
+
<meta charset="UTF-8">
|
|
199
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
200
|
+
<title>MarkForge Live Studio - ${escapeHtml(fileName)}</title>
|
|
201
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
202
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
203
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
204
|
+
<style>
|
|
205
|
+
:root {
|
|
206
|
+
--mf-primary: #0D998D;
|
|
207
|
+
--mf-primary-dark: #008277;
|
|
208
|
+
--mf-primary-light: #ECFDFD;
|
|
209
|
+
--mf-primary-border: #33CDCF;
|
|
210
|
+
--mf-dark: #0F172A;
|
|
211
|
+
--mf-slate: #1E293B;
|
|
212
|
+
--mf-editor-bg: #0F172A;
|
|
213
|
+
--mf-editor-gutter: #1E293B;
|
|
214
|
+
--mf-editor-text: #F8FAFC;
|
|
215
|
+
--mf-muted: #64748B;
|
|
216
|
+
--mf-light-border: #E2E8F0;
|
|
217
|
+
--mf-bg: #F1F5F9;
|
|
218
|
+
}
|
|
219
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
220
|
+
body {
|
|
221
|
+
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
222
|
+
background: var(--mf-bg);
|
|
223
|
+
color: var(--mf-dark);
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
height: 100vh;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
}
|
|
229
|
+
header {
|
|
230
|
+
background: #FFFFFF;
|
|
231
|
+
border-bottom: 1px solid var(--mf-light-border);
|
|
232
|
+
min-height: 56px;
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-wrap: wrap;
|
|
235
|
+
align-items: center;
|
|
236
|
+
justify-content: space-between;
|
|
237
|
+
padding: 0.4rem 1.2rem;
|
|
238
|
+
z-index: 10;
|
|
239
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
|
240
|
+
gap: 0.75rem;
|
|
241
|
+
}
|
|
242
|
+
.brand-section {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
gap: 0.75rem;
|
|
246
|
+
}
|
|
247
|
+
.brand-badge {
|
|
248
|
+
font-size: 0.72rem;
|
|
249
|
+
font-weight: 800;
|
|
250
|
+
letter-spacing: 0.08em;
|
|
251
|
+
background: var(--mf-dark);
|
|
252
|
+
color: #FFFFFF;
|
|
253
|
+
padding: 0.25rem 0.55rem;
|
|
254
|
+
border-radius: 4px;
|
|
255
|
+
text-transform: uppercase;
|
|
256
|
+
}
|
|
257
|
+
.file-name {
|
|
258
|
+
font-size: 0.9rem;
|
|
259
|
+
font-weight: 700;
|
|
260
|
+
color: var(--mf-dark);
|
|
261
|
+
}
|
|
262
|
+
.sync-status {
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
gap: 0.35rem;
|
|
266
|
+
font-size: 0.75rem;
|
|
267
|
+
font-weight: 600;
|
|
268
|
+
color: var(--mf-primary-dark);
|
|
269
|
+
background: var(--mf-primary-light);
|
|
270
|
+
padding: 0.2rem 0.55rem;
|
|
271
|
+
border-radius: 9999px;
|
|
272
|
+
border: 1px solid var(--mf-primary-border);
|
|
273
|
+
}
|
|
274
|
+
.sync-dot {
|
|
275
|
+
width: 7px;
|
|
276
|
+
height: 7px;
|
|
277
|
+
background-color: var(--mf-primary);
|
|
278
|
+
border-radius: 50%;
|
|
279
|
+
box-shadow: 0 0 0 2px rgba(13, 153, 141, 0.2);
|
|
280
|
+
}
|
|
281
|
+
.toolbar-section {
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 0.3rem;
|
|
285
|
+
background: #F8FAFC;
|
|
286
|
+
padding: 0.25rem 0.4rem;
|
|
287
|
+
border-radius: 6px;
|
|
288
|
+
border: 1px solid var(--mf-light-border);
|
|
289
|
+
}
|
|
290
|
+
.tool-btn {
|
|
291
|
+
font-family: 'JetBrains Mono', monospace;
|
|
292
|
+
font-size: 0.75rem;
|
|
293
|
+
font-weight: 600;
|
|
294
|
+
padding: 0.25rem 0.45rem;
|
|
295
|
+
background: transparent;
|
|
296
|
+
border: 1px solid transparent;
|
|
297
|
+
border-radius: 4px;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
color: var(--mf-slate);
|
|
300
|
+
transition: all 0.1s ease;
|
|
301
|
+
}
|
|
302
|
+
.tool-btn:hover {
|
|
303
|
+
background: #FFFFFF;
|
|
304
|
+
border-color: var(--mf-light-border);
|
|
305
|
+
color: var(--mf-primary-dark);
|
|
306
|
+
}
|
|
307
|
+
.tool-divider {
|
|
308
|
+
width: 1px;
|
|
309
|
+
height: 16px;
|
|
310
|
+
background: var(--mf-light-border);
|
|
311
|
+
margin: 0 0.15rem;
|
|
312
|
+
}
|
|
313
|
+
.controls {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 0.5rem;
|
|
317
|
+
}
|
|
318
|
+
.view-toggles {
|
|
319
|
+
display: flex;
|
|
320
|
+
background: #F1F5F9;
|
|
321
|
+
padding: 2px;
|
|
322
|
+
border-radius: 6px;
|
|
323
|
+
border: 1px solid var(--mf-light-border);
|
|
324
|
+
}
|
|
325
|
+
.toggle-btn {
|
|
326
|
+
font-family: inherit;
|
|
327
|
+
font-size: 0.74rem;
|
|
328
|
+
font-weight: 600;
|
|
329
|
+
padding: 0.25rem 0.55rem;
|
|
330
|
+
border: none;
|
|
331
|
+
background: transparent;
|
|
332
|
+
border-radius: 4px;
|
|
333
|
+
cursor: pointer;
|
|
334
|
+
color: var(--mf-muted);
|
|
335
|
+
transition: all 0.15s ease;
|
|
336
|
+
}
|
|
337
|
+
.toggle-btn.active {
|
|
338
|
+
background: #FFFFFF;
|
|
339
|
+
color: var(--mf-dark);
|
|
340
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
|
|
341
|
+
}
|
|
342
|
+
.btn {
|
|
343
|
+
font-family: inherit;
|
|
344
|
+
font-size: 0.78rem;
|
|
345
|
+
font-weight: 600;
|
|
346
|
+
padding: 0.35rem 0.75rem;
|
|
347
|
+
border-radius: 6px;
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
text-decoration: none;
|
|
350
|
+
transition: all 0.15s ease;
|
|
351
|
+
display: inline-flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
gap: 0.3rem;
|
|
354
|
+
border: 1px solid var(--mf-light-border);
|
|
355
|
+
background: #FFFFFF;
|
|
356
|
+
color: var(--mf-dark);
|
|
357
|
+
}
|
|
358
|
+
.btn:hover {
|
|
359
|
+
background: #F8FAFC;
|
|
360
|
+
border-color: #CBD5E1;
|
|
361
|
+
}
|
|
362
|
+
.btn-primary {
|
|
363
|
+
background: var(--mf-primary);
|
|
364
|
+
color: #FFFFFF;
|
|
365
|
+
border-color: var(--mf-primary);
|
|
366
|
+
}
|
|
367
|
+
.btn-primary:hover {
|
|
368
|
+
background: var(--mf-primary-dark);
|
|
369
|
+
border-color: var(--mf-primary-dark);
|
|
370
|
+
}
|
|
371
|
+
.save-indicator {
|
|
372
|
+
font-size: 0.75rem;
|
|
373
|
+
font-weight: 600;
|
|
374
|
+
color: var(--mf-muted);
|
|
375
|
+
min-width: 65px;
|
|
376
|
+
text-align: right;
|
|
377
|
+
}
|
|
378
|
+
.save-indicator.saved {
|
|
379
|
+
color: var(--mf-primary-dark);
|
|
380
|
+
}
|
|
381
|
+
.save-indicator.saving {
|
|
382
|
+
color: #D97706;
|
|
383
|
+
}
|
|
384
|
+
.save-indicator.unsaved {
|
|
385
|
+
color: #E11D48;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* Main Workspace Splitter Layout */
|
|
389
|
+
main.workspace {
|
|
390
|
+
flex: 1;
|
|
391
|
+
display: flex;
|
|
392
|
+
height: calc(100vh - 56px);
|
|
393
|
+
overflow: hidden;
|
|
394
|
+
background: var(--mf-bg);
|
|
395
|
+
position: relative;
|
|
396
|
+
}
|
|
397
|
+
.editor-pane {
|
|
398
|
+
width: 50%;
|
|
399
|
+
height: 100%;
|
|
400
|
+
display: flex;
|
|
401
|
+
flex-direction: column;
|
|
402
|
+
background: var(--mf-editor-bg);
|
|
403
|
+
border-right: 1px solid #334155;
|
|
404
|
+
overflow: hidden;
|
|
405
|
+
}
|
|
406
|
+
.editor-header {
|
|
407
|
+
background: #090D16;
|
|
408
|
+
border-bottom: 1px solid #1E293B;
|
|
409
|
+
padding: 0.4rem 0.8rem;
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
justify-content: space-between;
|
|
413
|
+
color: #94A3B8;
|
|
414
|
+
font-size: 0.74rem;
|
|
415
|
+
font-weight: 500;
|
|
416
|
+
}
|
|
417
|
+
.editor-container {
|
|
418
|
+
flex: 1;
|
|
419
|
+
display: flex;
|
|
420
|
+
position: relative;
|
|
421
|
+
overflow: hidden;
|
|
422
|
+
background: var(--mf-editor-bg);
|
|
423
|
+
}
|
|
424
|
+
.line-numbers {
|
|
425
|
+
width: 44px;
|
|
426
|
+
padding: 0.8rem 0.4rem;
|
|
427
|
+
font-family: 'JetBrains Mono', monospace;
|
|
428
|
+
font-size: 13px;
|
|
429
|
+
line-height: 1.55;
|
|
430
|
+
color: #475569;
|
|
431
|
+
text-align: right;
|
|
432
|
+
user-select: none;
|
|
433
|
+
background: var(--mf-editor-gutter);
|
|
434
|
+
overflow: hidden;
|
|
435
|
+
border-right: 1px solid #1E293B;
|
|
436
|
+
}
|
|
437
|
+
.code-editor {
|
|
438
|
+
flex: 1;
|
|
439
|
+
padding: 0.8rem 1rem;
|
|
440
|
+
font-family: 'JetBrains Mono', monospace;
|
|
441
|
+
font-size: 13px;
|
|
442
|
+
line-height: 1.55;
|
|
443
|
+
color: var(--mf-editor-text);
|
|
444
|
+
background: transparent;
|
|
445
|
+
border: none;
|
|
446
|
+
outline: none;
|
|
447
|
+
resize: none;
|
|
448
|
+
white-space: pre;
|
|
449
|
+
overflow-wrap: normal;
|
|
450
|
+
overflow: auto;
|
|
451
|
+
tab-size: 2;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* Draggable Splitter Handle */
|
|
455
|
+
.splitter {
|
|
456
|
+
width: 8px;
|
|
457
|
+
cursor: col-resize;
|
|
458
|
+
background: #E2E8F0;
|
|
459
|
+
transition: background 0.15s ease;
|
|
460
|
+
position: relative;
|
|
461
|
+
z-index: 5;
|
|
462
|
+
}
|
|
463
|
+
.splitter:hover, .splitter.active {
|
|
464
|
+
background: var(--mf-primary);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* Right Preview Pane */
|
|
468
|
+
.preview-pane {
|
|
469
|
+
width: 50%;
|
|
470
|
+
height: 100%;
|
|
471
|
+
display: flex;
|
|
472
|
+
flex-direction: column;
|
|
473
|
+
background: #FFFFFF;
|
|
474
|
+
overflow: hidden;
|
|
475
|
+
}
|
|
476
|
+
.preview-header {
|
|
477
|
+
background: #FFFFFF;
|
|
478
|
+
border-bottom: 1px solid var(--mf-light-border);
|
|
479
|
+
padding: 0.35rem 0.8rem;
|
|
480
|
+
display: flex;
|
|
481
|
+
align-items: center;
|
|
482
|
+
justify-content: space-between;
|
|
483
|
+
color: var(--mf-muted);
|
|
484
|
+
font-size: 0.74rem;
|
|
485
|
+
font-weight: 600;
|
|
486
|
+
}
|
|
487
|
+
.viewport-selector {
|
|
488
|
+
display: flex;
|
|
489
|
+
gap: 0.25rem;
|
|
490
|
+
}
|
|
491
|
+
.vp-btn {
|
|
492
|
+
font-size: 0.72rem;
|
|
493
|
+
padding: 0.15rem 0.4rem;
|
|
494
|
+
border: 1px solid var(--mf-light-border);
|
|
495
|
+
background: #F8FAFC;
|
|
496
|
+
border-radius: 4px;
|
|
497
|
+
cursor: pointer;
|
|
498
|
+
color: var(--mf-muted);
|
|
499
|
+
}
|
|
500
|
+
.vp-btn.active {
|
|
501
|
+
background: var(--mf-primary-light);
|
|
502
|
+
color: var(--mf-primary-dark);
|
|
503
|
+
border-color: var(--mf-primary-border);
|
|
504
|
+
}
|
|
505
|
+
.preview-wrapper {
|
|
506
|
+
flex: 1;
|
|
507
|
+
display: flex;
|
|
508
|
+
justify-content: center;
|
|
509
|
+
align-items: stretch;
|
|
510
|
+
background: #F1F5F9;
|
|
511
|
+
overflow: hidden;
|
|
512
|
+
}
|
|
513
|
+
iframe {
|
|
514
|
+
width: 100%;
|
|
515
|
+
height: 100%;
|
|
516
|
+
border: none;
|
|
517
|
+
background: #FFFFFF;
|
|
518
|
+
transition: max-width 0.2s ease;
|
|
519
|
+
}
|
|
520
|
+
.author-footer {
|
|
521
|
+
font-size: 0.72rem;
|
|
522
|
+
color: var(--mf-muted);
|
|
523
|
+
padding-right: 0.5rem;
|
|
524
|
+
}
|
|
525
|
+
.author-footer a {
|
|
526
|
+
color: var(--mf-primary-dark);
|
|
527
|
+
text-decoration: none;
|
|
528
|
+
font-weight: 600;
|
|
529
|
+
}
|
|
530
|
+
</style>
|
|
531
|
+
</head>
|
|
532
|
+
<body>
|
|
533
|
+
<header>
|
|
534
|
+
<div class="brand-section">
|
|
535
|
+
<span class="brand-badge">MARKFORGE STUDIO</span>
|
|
536
|
+
<span class="file-name" title="${escapeHtml(absoluteFilePath)}">${escapeHtml(fileName)}</span>
|
|
537
|
+
<div class="sync-status">
|
|
538
|
+
<div class="sync-dot"></div>
|
|
539
|
+
<span>Live Sync Active</span>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<!-- Quick Formatting Toolbar -->
|
|
544
|
+
<div class="toolbar-section">
|
|
545
|
+
<button class="tool-btn" onclick="insertFormat('h1')" title="Heading 1">H1</button>
|
|
546
|
+
<button class="tool-btn" onclick="insertFormat('h2')" title="Heading 2">H2</button>
|
|
547
|
+
<button class="tool-btn" onclick="insertFormat('h3')" title="Heading 3">H3</button>
|
|
548
|
+
<div class="tool-divider"></div>
|
|
549
|
+
<button class="tool-btn" onclick="insertFormat('bold')" title="Bold">B</button>
|
|
550
|
+
<button class="tool-btn" onclick="insertFormat('italic')" title="Italic">I</button>
|
|
551
|
+
<button class="tool-btn" onclick="insertFormat('code')" title="Inline Code"><></button>
|
|
552
|
+
<button class="tool-btn" onclick="insertFormat('quote')" title="Blockquote">></button>
|
|
553
|
+
<div class="tool-divider"></div>
|
|
554
|
+
<button class="tool-btn" onclick="insertFormat('table')" title="GFM Table">Table</button>
|
|
555
|
+
<button class="tool-btn" onclick="insertFormat('list')" title="List">List</button>
|
|
556
|
+
<button class="tool-btn" onclick="insertFormat('task')" title="Task Checklist">Task</button>
|
|
557
|
+
<div class="tool-divider"></div>
|
|
558
|
+
<button class="tool-btn" onclick="insertFormat('callout')" title="Callout Box">Callout</button>
|
|
559
|
+
<button class="tool-btn" onclick="insertFormat('math')" title="LaTeX Math">Math</button>
|
|
560
|
+
<button class="tool-btn" onclick="insertFormat('columns')" title="Multi-Columns">Columns</button>
|
|
561
|
+
<button class="tool-btn" onclick="insertFormat('footnote')" title="Footnote">Footnote</button>
|
|
562
|
+
<button class="tool-btn" onclick="insertFormat('mermaid')" title="Mermaid Diagram">Mermaid</button>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<!-- Controls & View Mode -->
|
|
566
|
+
<div class="controls">
|
|
567
|
+
<div class="view-toggles">
|
|
568
|
+
<button class="toggle-btn active" id="btn-split" onclick="setViewMode('split')">Split</button>
|
|
569
|
+
<button class="toggle-btn" id="btn-edit" onclick="setViewMode('edit')">Editor</button>
|
|
570
|
+
<button class="toggle-btn" id="btn-prev" onclick="setViewMode('prev')">Preview</button>
|
|
571
|
+
</div>
|
|
572
|
+
<span class="save-indicator saved" id="save-status">Saved</span>
|
|
573
|
+
<button class="btn btn-primary" onclick="saveContentManual()" title="Save (Ctrl+S)">Save</button>
|
|
574
|
+
<button class="btn" onclick="exportDoc('docx')" title="Download Word Document">DOCX</button>
|
|
575
|
+
<button class="btn" onclick="exportDoc('pdf')" title="Download PDF Document">PDF</button>
|
|
576
|
+
<button class="btn" onclick="printDoc()" title="Print / PDF dialog">Print</button>
|
|
577
|
+
</div>
|
|
578
|
+
</header>
|
|
579
|
+
|
|
580
|
+
<main class="workspace" id="workspace">
|
|
581
|
+
<!-- Left: Code Editor Pane -->
|
|
582
|
+
<div class="editor-pane" id="editor-pane">
|
|
583
|
+
<div class="editor-header">
|
|
584
|
+
<span>MARKDOWN SOURCE</span>
|
|
585
|
+
<span id="editor-stats">Lines: 1 | Words: 0 | UTF-8</span>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="editor-container">
|
|
588
|
+
<div class="line-numbers" id="line-numbers">1</div>
|
|
589
|
+
<textarea class="code-editor" id="code-editor" spellcheck="false" placeholder="Write markdown here...">${escapeHtml(initialContent)}</textarea>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<!-- Middle: Draggable Splitter Handle -->
|
|
594
|
+
<div class="splitter" id="splitter"></div>
|
|
595
|
+
|
|
596
|
+
<!-- Right: Rendered Preview Pane -->
|
|
597
|
+
<div class="preview-pane" id="preview-pane">
|
|
598
|
+
<div class="preview-header">
|
|
599
|
+
<span>RENDERED PREVIEW</span>
|
|
600
|
+
<div class="viewport-selector">
|
|
601
|
+
<button class="vp-btn active" onclick="setViewport('100%')" id="vp-full">100% Full</button>
|
|
602
|
+
<button class="vp-btn" onclick="setViewport('820px')" id="vp-a4">A4 (820px)</button>
|
|
603
|
+
<button class="vp-btn" onclick="setViewport('440px')" id="vp-mob">Mobile</button>
|
|
604
|
+
</div>
|
|
605
|
+
<span class="author-footer">Created by <a href="https://github.com/masumrpg" target="_blank">Ma'sum (@masumrpg)</a></span>
|
|
606
|
+
</div>
|
|
607
|
+
<div class="preview-wrapper">
|
|
608
|
+
<iframe id="preview-frame" src="/document-content"></iframe>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
</main>
|
|
612
|
+
|
|
613
|
+
<script>
|
|
614
|
+
var editor = document.getElementById('code-editor');
|
|
615
|
+
var lineNumbers = document.getElementById('line-numbers');
|
|
616
|
+
var stats = document.getElementById('editor-stats');
|
|
617
|
+
var saveStatus = document.getElementById('save-status');
|
|
618
|
+
var previewFrame = document.getElementById('preview-frame');
|
|
619
|
+
var editorPane = document.getElementById('editor-pane');
|
|
620
|
+
var previewPane = document.getElementById('preview-pane');
|
|
621
|
+
var splitter = document.getElementById('splitter');
|
|
622
|
+
var isDirty = false;
|
|
623
|
+
var autoSaveTimeout = null;
|
|
624
|
+
|
|
625
|
+
// Update Line Numbers & Stats
|
|
626
|
+
function updateStatsAndLines() {
|
|
627
|
+
var lines = editor.value.split('\\n');
|
|
628
|
+
var lineCount = lines.length;
|
|
629
|
+
var numHtml = '';
|
|
630
|
+
for (var i = 1; i <= lineCount; i++) {
|
|
631
|
+
numHtml += i + '<br>';
|
|
632
|
+
}
|
|
633
|
+
lineNumbers.innerHTML = numHtml;
|
|
634
|
+
|
|
635
|
+
var words = editor.value.trim().length > 0 ? editor.value.trim().split(/\\s+/).length : 0;
|
|
636
|
+
var chars = editor.value.length;
|
|
637
|
+
stats.textContent = 'Lines: ' + lineCount + ' | Words: ' + words + ' | Chars: ' + chars + ' | UTF-8';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Synchronize vertical scroll between Line Numbers and Textarea
|
|
641
|
+
editor.addEventListener('scroll', function() {
|
|
642
|
+
lineNumbers.scrollTop = editor.scrollTop;
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Handle Input & Debounced Auto-Save
|
|
646
|
+
editor.addEventListener('input', function() {
|
|
647
|
+
updateStatsAndLines();
|
|
648
|
+
setSaveState('unsaved');
|
|
649
|
+
if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
|
|
650
|
+
autoSaveTimeout = setTimeout(function() {
|
|
651
|
+
saveContent();
|
|
652
|
+
}, 600);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
function setSaveState(state) {
|
|
656
|
+
if (state === 'saved') {
|
|
657
|
+
saveStatus.textContent = 'Saved';
|
|
658
|
+
saveStatus.className = 'save-indicator saved';
|
|
659
|
+
isDirty = false;
|
|
660
|
+
} else if (state === 'saving') {
|
|
661
|
+
saveStatus.textContent = 'Saving...';
|
|
662
|
+
saveStatus.className = 'save-indicator saving';
|
|
663
|
+
} else {
|
|
664
|
+
saveStatus.textContent = 'Changes...';
|
|
665
|
+
saveStatus.className = 'save-indicator unsaved';
|
|
666
|
+
isDirty = true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Save Content via API
|
|
671
|
+
function saveContent(callback) {
|
|
672
|
+
setSaveState('saving');
|
|
673
|
+
fetch('/api/save-content', {
|
|
674
|
+
method: 'POST',
|
|
675
|
+
headers: { 'Content-Type': 'application/json' },
|
|
676
|
+
body: JSON.stringify({ content: editor.value }),
|
|
677
|
+
})
|
|
678
|
+
.then(function(res) { return res.json(); })
|
|
679
|
+
.then(function(data) {
|
|
680
|
+
if (data.success) {
|
|
681
|
+
setSaveState('saved');
|
|
682
|
+
if (callback) callback();
|
|
683
|
+
} else {
|
|
684
|
+
saveStatus.textContent = 'Save Error';
|
|
685
|
+
}
|
|
686
|
+
})
|
|
687
|
+
.catch(function() {
|
|
688
|
+
saveStatus.textContent = 'Save Error';
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function saveContentManual() {
|
|
693
|
+
saveContent();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Keyboard Shortcuts: Tab (2 spaces), Shift+Tab, Ctrl+S
|
|
697
|
+
editor.addEventListener('keydown', function(e) {
|
|
698
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
699
|
+
e.preventDefault();
|
|
700
|
+
saveContent();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (e.key === 'Tab') {
|
|
705
|
+
e.preventDefault();
|
|
706
|
+
var start = this.selectionStart;
|
|
707
|
+
var end = this.selectionEnd;
|
|
708
|
+
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
|
709
|
+
this.selectionStart = this.selectionEnd = start + 2;
|
|
710
|
+
updateStatsAndLines();
|
|
711
|
+
setSaveState('unsaved');
|
|
712
|
+
if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
|
|
713
|
+
autoSaveTimeout = setTimeout(saveContent, 600);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Formatting Snippet Injector
|
|
718
|
+
function insertFormat(type) {
|
|
719
|
+
var start = editor.selectionStart;
|
|
720
|
+
var end = editor.selectionEnd;
|
|
721
|
+
var selected = editor.value.substring(start, end);
|
|
722
|
+
var replacement = '';
|
|
723
|
+
|
|
724
|
+
switch (type) {
|
|
725
|
+
case 'h1': replacement = '# ' + (selected || 'Heading 1'); break;
|
|
726
|
+
case 'h2': replacement = '## ' + (selected || 'Heading 2'); break;
|
|
727
|
+
case 'h3': replacement = '### ' + (selected || 'Heading 3'); break;
|
|
728
|
+
case 'bold': replacement = '**' + (selected || 'bold text') + '**'; break;
|
|
729
|
+
case 'italic': replacement = '*' + (selected || 'italic text') + '*'; break;
|
|
730
|
+
case 'code': replacement = '\`' + (selected || 'inline code') + '\`'; break;
|
|
731
|
+
case 'quote': replacement = '> ' + (selected || 'Quote text'); break;
|
|
732
|
+
case 'table':
|
|
733
|
+
replacement = '\\n| Column 1 | Column 2 | Column 3 |\\n| :--- | :---: | ---: |\\n| Data A | Data B | Data C |\\n| Data D | Data E | Data F |\\n';
|
|
734
|
+
break;
|
|
735
|
+
case 'list': replacement = '- ' + (selected || 'List item'); break;
|
|
736
|
+
case 'task': replacement = '- [ ] ' + (selected || 'Task item'); break;
|
|
737
|
+
case 'callout':
|
|
738
|
+
replacement = '> [!NOTE]\\n> ' + (selected || 'This is an important callout note.');
|
|
739
|
+
break;
|
|
740
|
+
case 'math':
|
|
741
|
+
replacement = '$$\\n' + (selected || '\\\\int_{-\\\\infty}^{\\\\infty} e^{-x^2} dx = \\\\sqrt{\\\\pi}') + '\\n$$';
|
|
742
|
+
break;
|
|
743
|
+
case 'columns':
|
|
744
|
+
replacement = ':::columns 2\\n:::col\\n### Left Column\\n' + (selected || 'Content on the left.') + '\\n:::\\n:::col\\n### Right Column\\nContent on the right.\\n:::\\n:::';
|
|
745
|
+
break;
|
|
746
|
+
case 'footnote':
|
|
747
|
+
replacement = (selected || 'Statement with footnote') + '[^1]\\n\\n[^1]: Note description text.';
|
|
748
|
+
break;
|
|
749
|
+
case 'mermaid':
|
|
750
|
+
replacement = '\\n\`\`\`mermaid\\ngraph TD\\n A[Start] --> B(Process)\\n B --> C{Decision}\\n C -->|Yes| D[Done]\\n C -->|No| B\\n\`\`\`\\n';
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end);
|
|
755
|
+
editor.selectionStart = editor.selectionEnd = start + replacement.length;
|
|
756
|
+
editor.focus();
|
|
757
|
+
updateStatsAndLines();
|
|
758
|
+
setSaveState('unsaved');
|
|
759
|
+
if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
|
|
760
|
+
autoSaveTimeout = setTimeout(saveContent, 600);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// View Mode Toggle (Split / Editor Only / Preview Only)
|
|
764
|
+
function setViewMode(mode) {
|
|
765
|
+
document.getElementById('btn-split').classList.remove('active');
|
|
766
|
+
document.getElementById('btn-edit').classList.remove('active');
|
|
767
|
+
document.getElementById('btn-prev').classList.remove('active');
|
|
768
|
+
|
|
769
|
+
if (mode === 'split') {
|
|
770
|
+
document.getElementById('btn-split').classList.add('active');
|
|
771
|
+
editorPane.style.display = 'flex';
|
|
772
|
+
editorPane.style.width = '50%';
|
|
773
|
+
previewPane.style.display = 'flex';
|
|
774
|
+
previewPane.style.width = '50%';
|
|
775
|
+
splitter.style.display = 'block';
|
|
776
|
+
} else if (mode === 'edit') {
|
|
777
|
+
document.getElementById('btn-edit').classList.add('active');
|
|
778
|
+
editorPane.style.display = 'flex';
|
|
779
|
+
editorPane.style.width = '100%';
|
|
780
|
+
previewPane.style.display = 'none';
|
|
781
|
+
splitter.style.display = 'none';
|
|
782
|
+
} else if (mode === 'prev') {
|
|
783
|
+
document.getElementById('btn-prev').classList.add('active');
|
|
784
|
+
editorPane.style.display = 'none';
|
|
785
|
+
previewPane.style.display = 'flex';
|
|
786
|
+
previewPane.style.width = '100%';
|
|
787
|
+
splitter.style.display = 'none';
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Viewport Width Resizer
|
|
792
|
+
function setViewport(width) {
|
|
793
|
+
document.getElementById('vp-full').classList.remove('active');
|
|
794
|
+
document.getElementById('vp-a4').classList.remove('active');
|
|
795
|
+
document.getElementById('vp-mob').classList.remove('active');
|
|
796
|
+
|
|
797
|
+
if (width === '100%') {
|
|
798
|
+
document.getElementById('vp-full').classList.add('active');
|
|
799
|
+
previewFrame.style.maxWidth = '100%';
|
|
800
|
+
} else if (width === '820px') {
|
|
801
|
+
document.getElementById('vp-a4').classList.add('active');
|
|
802
|
+
previewFrame.style.maxWidth = '820px';
|
|
803
|
+
} else if (width === '440px') {
|
|
804
|
+
document.getElementById('vp-mob').classList.add('active');
|
|
805
|
+
previewFrame.style.maxWidth = '440px';
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Draggable Splitter Handle Logic
|
|
810
|
+
var isDragging = false;
|
|
811
|
+
splitter.addEventListener('mousedown', function(e) {
|
|
812
|
+
isDragging = true;
|
|
813
|
+
splitter.classList.add('active');
|
|
814
|
+
document.body.style.cursor = 'col-resize';
|
|
815
|
+
document.body.style.userSelect = 'none';
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
window.addEventListener('mousemove', function(e) {
|
|
819
|
+
if (!isDragging) return;
|
|
820
|
+
var totalWidth = document.getElementById('workspace').clientWidth;
|
|
821
|
+
var newEditorWidth = (e.clientX / totalWidth) * 100;
|
|
822
|
+
if (newEditorWidth > 15 && newEditorWidth < 85) {
|
|
823
|
+
editorPane.style.width = newEditorWidth + '%';
|
|
824
|
+
previewPane.style.width = (100 - newEditorWidth) + '%';
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
window.addEventListener('mouseup', function() {
|
|
829
|
+
if (isDragging) {
|
|
830
|
+
isDragging = false;
|
|
831
|
+
splitter.classList.remove('active');
|
|
832
|
+
document.body.style.cursor = '';
|
|
833
|
+
document.body.style.userSelect = '';
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// Document Print Action
|
|
838
|
+
function printDoc() {
|
|
839
|
+
previewFrame.contentWindow.print();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Document Export Action
|
|
843
|
+
function exportDoc(format) {
|
|
844
|
+
saveContent(function() {
|
|
845
|
+
window.location.href = '/api/export?format=' + format;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Initialize line stats
|
|
850
|
+
updateStatsAndLines();
|
|
851
|
+
</script>
|
|
852
|
+
</body>
|
|
853
|
+
</html>`;
|
|
854
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
855
|
+
res.end(appHtml);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
859
|
+
res.end("Not Found");
|
|
860
|
+
});
|
|
861
|
+
return new Promise((resolve2, reject) => {
|
|
862
|
+
server.listen(port, () => {
|
|
863
|
+
const url = `http://localhost:${port}`;
|
|
864
|
+
resolve2({
|
|
865
|
+
server,
|
|
866
|
+
port,
|
|
867
|
+
url,
|
|
868
|
+
close: async () => {
|
|
869
|
+
watcher.close();
|
|
870
|
+
sseClients.forEach((client) => {
|
|
871
|
+
try {
|
|
872
|
+
client.end();
|
|
873
|
+
} catch {
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
sseClients.clear();
|
|
877
|
+
return new Promise((res) => {
|
|
878
|
+
server.close(() => res());
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
server.on("error", (err) => {
|
|
884
|
+
reject(err);
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function escapeHtml(str) {
|
|
889
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
890
|
+
}
|
|
891
|
+
export {
|
|
892
|
+
startPreviewServer
|
|
893
|
+
};
|