vimd 0.5.7 → 0.5.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAwBA,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CA2Lf"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAoBA,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAyHf"}
@@ -1,11 +1,8 @@
1
1
  // src/cli/commands/dev.ts
2
2
  import { ConfigLoader } from '../../config/loader.js';
3
- import { FileWatcher } from '../../core/watcher.js';
4
- import { MarkdownConverter } from '../../core/converter.js';
5
- import { WebSocketServer } from '../../core/websocket-server.js';
3
+ import { SingleFileServer } from '../../core/single-file-server.js';
6
4
  import { FolderModeServer } from '../../core/folder-mode/index.js';
7
5
  import { PandocDetector } from '../../core/pandoc-detector.js';
8
- import { ParserFactory } from '../../core/parser/index.js';
9
6
  import { Logger } from '../../utils/logger.js';
10
7
  import { ProcessManager } from '../../utils/process-manager.js';
11
8
  import { SessionManager } from '../../utils/session-manager.js';
@@ -70,55 +67,29 @@ export async function devCommand(targetPath, options) {
70
67
  return;
71
68
  }
72
69
  // Continue with single file mode
73
- const filePath = targetPath;
74
- // 7. Detect file type and determine parser/format
75
- const isLatex = isLatexFile(filePath);
76
- const fromFormat = isLatex ? 'latex' : 'markdown';
77
- // 7. Determine parser type (LaTeX requires pandoc)
78
- const parserType = isLatex ? 'pandoc' : (options.pandoc ? 'pandoc' : config.devParser);
79
- Logger.info(`Parser: ${parserType}`);
70
+ const isLatex = isLatexFile(targetPath);
80
71
  if (isLatex) {
81
72
  Logger.info('Mode: LaTeX');
73
+ // Check pandoc installation for LaTeX files
74
+ PandocDetector.ensureInstalled(true);
82
75
  }
83
- // 8. Check pandoc installation (required for pandoc parser or LaTeX files)
84
- if (parserType === 'pandoc') {
85
- PandocDetector.ensureInstalled(isLatex);
86
- }
87
- // 9. Prepare output HTML in source directory
88
- const sourceDir = path.dirname(absolutePath);
89
- const basename = path.basename(filePath, path.extname(filePath));
90
- const htmlFileName = `vimd-preview-${basename}.html`;
91
- const htmlPath = path.join(sourceDir, htmlFileName);
92
- // 10. Prepare converter with selected parser
93
- const parser = ParserFactory.create(parserType, config.pandoc, config.math, fromFormat);
94
- const converter = new MarkdownConverter({
76
+ // 7. Create and start SingleFileServer
77
+ // Server handles conversion, watching, and WebSocket internally
78
+ const server = new SingleFileServer({
79
+ port: port,
80
+ host: config.host,
81
+ filePath: absolutePath,
95
82
  theme: config.theme,
83
+ mathEnabled: config.math?.enabled ?? true,
96
84
  pandocOptions: config.pandoc,
97
85
  customCSS: config.css,
98
- template: config.template,
99
- mathEnabled: config.math?.enabled ?? true,
100
- });
101
- converter.setParser(parser);
102
- // 11. Initial conversion
103
- Logger.info(`Converting ${isLatex ? 'LaTeX' : 'markdown'}...`);
104
- const html = await converter.convertWithTemplate(absolutePath);
105
- await converter.writeHTML(html, htmlPath);
106
- Logger.success('Conversion complete');
107
- // 11. Start WebSocket server from source directory
108
- const server = new WebSocketServer({
109
- port: port,
110
- host: config.host,
111
- root: sourceDir,
86
+ watchOptions: config.watch,
112
87
  });
113
88
  const startResult = await server.start();
114
- // Update port if server used a different one
115
89
  const actualPort = startResult.actualPort;
116
- if (startResult.portChanged) {
117
- port = actualPort;
118
- }
119
- // 11.5. Open browser if configured
90
+ // 8. Open browser if configured
120
91
  if (config.open) {
121
- const url = `http://${config.host}:${actualPort}/${htmlFileName}`;
92
+ const url = `http://${config.host}:${actualPort}/`;
122
93
  try {
123
94
  await open(url);
124
95
  Logger.info('Browser opened');
@@ -127,48 +98,19 @@ export async function devCommand(targetPath, options) {
127
98
  Logger.warn('Failed to open browser automatically');
128
99
  }
129
100
  }
130
- // 12. Save session with actual port
101
+ // 9. Save session (no HTML file in single file mode)
131
102
  await SessionManager.saveSession({
132
103
  pid: process.pid,
133
104
  port: actualPort,
134
- htmlPath: htmlPath,
105
+ htmlPath: '', // No HTML file generated
135
106
  sourcePath: absolutePath,
136
107
  startedAt: new Date().toISOString(),
137
108
  });
138
- Logger.info(`Watching: ${filePath}`);
139
109
  Logger.info('Press Ctrl+C to stop');
140
- // 13. Start file watching
141
- const watcher = new FileWatcher(absolutePath, config.watch);
142
- watcher.onChange(async (changedPath) => {
143
- Logger.info('File changed, reconverting...');
144
- try {
145
- const newHtml = await converter.convertWithTemplate(changedPath);
146
- await converter.writeHTML(newHtml, htmlPath);
147
- server.broadcast('reload');
148
- Logger.success('Reconversion complete');
149
- }
150
- catch (error) {
151
- Logger.error('Reconversion failed');
152
- if (error instanceof Error) {
153
- Logger.error(error.message);
154
- }
155
- }
156
- });
157
- watcher.start();
158
- // 14. Register cleanup - remove generated HTML file and session
110
+ // 10. Register cleanup
159
111
  ProcessManager.onExit(async () => {
160
112
  Logger.info('Shutting down...');
161
- await watcher.stop();
162
113
  await server.stop();
163
- // Remove the generated preview HTML file
164
- try {
165
- await fs.remove(htmlPath);
166
- Logger.info(`Removed: ${htmlFileName}`);
167
- }
168
- catch {
169
- // Ignore errors when removing file
170
- }
171
- // Remove session from registry (use actual port)
172
114
  await SessionManager.removeSession(actualPort);
173
115
  Logger.info('Cleanup complete');
174
116
  });
@@ -613,13 +613,21 @@
613
613
  state.panels[panelIndex].file = path;
614
614
  }
615
615
 
616
+ // Get scroll container (panel-body) and save scroll position
617
+ var scrollContainer = null;
618
+ var scrollTop = 0;
619
+
616
620
  if (panelIndex === 0) {
621
+ scrollContainer = content.parentElement;
622
+ scrollTop = scrollContainer ? scrollContainer.scrollTop : 0;
617
623
  welcome.classList.add('hidden');
618
624
  content.classList.add('visible');
619
625
  content.innerHTML = html;
620
626
  panel1Filename.textContent = getFileName(path);
621
627
  panel1Header.classList.add('visible');
622
628
  } else if (panelIndex === 1 && panel2Content) {
629
+ scrollContainer = panel2Content.parentElement;
630
+ scrollTop = scrollContainer ? scrollContainer.scrollTop : 0;
623
631
  panel2Content.innerHTML = html;
624
632
  panel2Content.classList.add('visible');
625
633
  panel2Filename.textContent = getFileName(path);
@@ -628,12 +636,34 @@
628
636
 
629
637
  updatePanelUI();
630
638
 
631
- // Re-render MathJax if available
639
+ // Re-render MathJax if available, then restore scroll position
632
640
  if (window.MathJax && window.MathJax.typeset) {
633
- if (panelIndex === 0) {
634
- window.MathJax.typeset([content]);
635
- } else if (panel2Content) {
636
- window.MathJax.typeset([panel2Content]);
641
+ var targetElement = panelIndex === 0 ? content : panel2Content;
642
+ if (targetElement) {
643
+ var promise = window.MathJax.typeset([targetElement]);
644
+ // MathJax v3 returns a promise
645
+ if (promise && promise.then) {
646
+ promise.then(function() {
647
+ if (scrollContainer) {
648
+ scrollContainer.scrollTop = scrollTop;
649
+ }
650
+ }).catch(function() {
651
+ // Restore scroll even if MathJax fails
652
+ if (scrollContainer) {
653
+ scrollContainer.scrollTop = scrollTop;
654
+ }
655
+ });
656
+ } else {
657
+ // Fallback for older MathJax or if promise not returned
658
+ if (scrollContainer) {
659
+ scrollContainer.scrollTop = scrollTop;
660
+ }
661
+ }
662
+ }
663
+ } else {
664
+ // No MathJax: restore scroll immediately
665
+ if (scrollContainer) {
666
+ scrollContainer.scrollTop = scrollTop;
637
667
  }
638
668
  }
639
669
  }
@@ -0,0 +1,87 @@
1
+ import type { WatchConfig, PandocConfig } from '../config/types.js';
2
+ /**
3
+ * Result of server start operation
4
+ */
5
+ export interface ServerStartResult {
6
+ actualPort: number;
7
+ requestedPort: number;
8
+ portChanged: boolean;
9
+ }
10
+ /**
11
+ * SingleFileServer options
12
+ */
13
+ export interface SingleFileServerOptions {
14
+ port: number;
15
+ host?: string;
16
+ filePath: string;
17
+ theme: string;
18
+ mathEnabled?: boolean;
19
+ pandocOptions?: Partial<PandocConfig>;
20
+ customCSS?: string;
21
+ watchOptions?: Partial<WatchConfig>;
22
+ }
23
+ /**
24
+ * Single file mode server for preview
25
+ * Serves HTML template in memory, sends content via WebSocket
26
+ */
27
+ export declare class SingleFileServer {
28
+ private httpServer;
29
+ private wsServer;
30
+ private clients;
31
+ private watcher;
32
+ private options;
33
+ private _port;
34
+ private renderedHtml;
35
+ private currentContent;
36
+ private fileTitle;
37
+ constructor(options: SingleFileServerOptions);
38
+ /**
39
+ * Get the actual port the server is running on
40
+ */
41
+ get port(): number;
42
+ /**
43
+ * Start the HTTP and WebSocket servers
44
+ */
45
+ start(): Promise<ServerStartResult>;
46
+ /**
47
+ * Stop the server
48
+ */
49
+ stop(): Promise<void>;
50
+ /**
51
+ * Handle new WebSocket connection
52
+ */
53
+ private handleConnection;
54
+ /**
55
+ * Start watching the file for changes
56
+ */
57
+ private startWatcher;
58
+ /**
59
+ * Convert file and cache the result
60
+ */
61
+ private convertAndCacheFile;
62
+ /**
63
+ * Broadcast current content to all clients
64
+ */
65
+ private broadcastContent;
66
+ /**
67
+ * Send message to a specific client
68
+ */
69
+ private sendMessage;
70
+ /**
71
+ * Broadcast message to all clients
72
+ */
73
+ private broadcast;
74
+ /**
75
+ * Serve the HTML template
76
+ */
77
+ private serveHtml;
78
+ /**
79
+ * Render the single file template
80
+ */
81
+ private renderTemplate;
82
+ /**
83
+ * Escape HTML special characters
84
+ */
85
+ private escapeHtml;
86
+ }
87
+ //# sourceMappingURL=single-file-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"single-file-server.d.ts","sourceRoot":"","sources":["../../src/core/single-file-server.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKpE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CACrC;AASD;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,SAAS,CAAc;gBAEnB,OAAO,EAAE,uBAAuB;IAU5C;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IA0FzC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqC3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAuBxB;;OAEG;IACH,OAAO,CAAC,YAAY;IA2BpB;;OAEG;YACW,mBAAmB;IAuBjC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;OAEG;IACH,OAAO,CAAC,WAAW;IAMnB;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB;;OAEG;IACH,OAAO,CAAC,SAAS;IAWjB;;OAEG;YACW,cAAc;IA6B5B;;OAEG;IACH,OAAO,CAAC,UAAU;CAQnB"}
@@ -0,0 +1,295 @@
1
+ // src/core/single-file-server.ts
2
+ import http from 'http';
3
+ import path from 'path';
4
+ import fs from 'fs-extra';
5
+ import { fileURLToPath } from 'url';
6
+ import polka from 'polka';
7
+ import sirv from 'sirv';
8
+ import { WebSocketServer as WSServer, WebSocket } from 'ws';
9
+ import { Logger } from '../utils/logger.js';
10
+ import { SessionManager } from '../utils/session-manager.js';
11
+ import { ThemeManager } from '../themes/index.js';
12
+ import { ParserFactory } from './parser/index.js';
13
+ import { PandocDetector } from './pandoc-detector.js';
14
+ import { FileWatcher } from './watcher.js';
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ /**
18
+ * Single file mode server for preview
19
+ * Serves HTML template in memory, sends content via WebSocket
20
+ */
21
+ export class SingleFileServer {
22
+ constructor(options) {
23
+ this.httpServer = null;
24
+ this.wsServer = null;
25
+ this.clients = new Set();
26
+ this.watcher = null;
27
+ this.renderedHtml = null;
28
+ this.currentContent = null;
29
+ this.fileTitle = '';
30
+ this.options = {
31
+ host: 'localhost',
32
+ mathEnabled: true,
33
+ ...options,
34
+ };
35
+ this._port = options.port;
36
+ this.fileTitle = path.basename(options.filePath, path.extname(options.filePath));
37
+ }
38
+ /**
39
+ * Get the actual port the server is running on
40
+ */
41
+ get port() {
42
+ return this._port;
43
+ }
44
+ /**
45
+ * Start the HTTP and WebSocket servers
46
+ */
47
+ async start() {
48
+ const requestedPort = this.options.port;
49
+ let actualPort = requestedPort;
50
+ // Check if port is available
51
+ if (!(await SessionManager.isPortAvailable(requestedPort))) {
52
+ actualPort = await SessionManager.findAvailablePort(requestedPort + 1);
53
+ Logger.warn(`Port ${requestedPort} was unavailable, using port ${actualPort}`);
54
+ }
55
+ this._port = actualPort;
56
+ // Render template
57
+ this.renderedHtml = await this.renderTemplate();
58
+ // Initial conversion
59
+ await this.convertAndCacheFile();
60
+ // Create polka app
61
+ const app = polka();
62
+ // Static file server for images, CSS, JS from source directory
63
+ const sourceDir = path.dirname(this.options.filePath);
64
+ const serve = sirv(sourceDir, {
65
+ dev: true, // Disable caching for development
66
+ });
67
+ // Serve static files (images, etc.) but not the source markdown/latex
68
+ app.use((req, res, next) => {
69
+ const url = req.url || '/';
70
+ // Root path serves our template
71
+ if (url === '/' || url === '/index.html') {
72
+ return next();
73
+ }
74
+ // Serve static files
75
+ serve(req, res, next);
76
+ });
77
+ // Serve single file mode HTML for root
78
+ app.get('/', (req, res) => {
79
+ this.serveHtml(req, res);
80
+ });
81
+ app.get('/index.html', (req, res) => {
82
+ this.serveHtml(req, res);
83
+ });
84
+ // Create HTTP server
85
+ this.httpServer = http.createServer(app.handler);
86
+ // Create WebSocket server
87
+ this.wsServer = new WSServer({ server: this.httpServer });
88
+ // Handle WebSocket connections
89
+ this.wsServer.on('connection', (ws) => {
90
+ this.handleConnection(ws);
91
+ });
92
+ // Start listening
93
+ await new Promise((resolve, reject) => {
94
+ const timeout = setTimeout(() => {
95
+ reject(new Error('Server start timeout'));
96
+ }, 10000);
97
+ this.httpServer.listen(actualPort, this.options.host, () => {
98
+ clearTimeout(timeout);
99
+ resolve();
100
+ });
101
+ this.httpServer.on('error', (err) => {
102
+ clearTimeout(timeout);
103
+ reject(err);
104
+ });
105
+ });
106
+ // Start file watcher
107
+ this.startWatcher();
108
+ const url = `http://${this.options.host}:${actualPort}`;
109
+ Logger.success(`Server started at ${url}`);
110
+ return {
111
+ actualPort,
112
+ requestedPort,
113
+ portChanged: actualPort !== requestedPort,
114
+ };
115
+ }
116
+ /**
117
+ * Stop the server
118
+ */
119
+ async stop() {
120
+ // Stop file watcher
121
+ if (this.watcher) {
122
+ await this.watcher.stop();
123
+ this.watcher = null;
124
+ }
125
+ // Terminate all WebSocket clients
126
+ for (const client of this.clients) {
127
+ try {
128
+ client.terminate();
129
+ }
130
+ catch {
131
+ // Ignore termination errors
132
+ }
133
+ }
134
+ this.clients.clear();
135
+ // Close WebSocket server
136
+ if (this.wsServer) {
137
+ await new Promise((resolve) => {
138
+ this.wsServer.close(() => resolve());
139
+ });
140
+ this.wsServer = null;
141
+ }
142
+ // Close HTTP server
143
+ if (this.httpServer) {
144
+ this.httpServer.closeAllConnections();
145
+ await new Promise((resolve) => {
146
+ this.httpServer.close(() => resolve());
147
+ });
148
+ this.httpServer = null;
149
+ }
150
+ Logger.info('Server stopped');
151
+ }
152
+ /**
153
+ * Handle new WebSocket connection
154
+ */
155
+ handleConnection(ws) {
156
+ this.clients.add(ws);
157
+ Logger.info(`WebSocket client connected (${this.clients.size} total)`);
158
+ // Send current content on connection
159
+ if (this.currentContent) {
160
+ this.sendMessage(ws, {
161
+ type: 'content',
162
+ data: { html: this.currentContent, title: this.fileTitle },
163
+ });
164
+ }
165
+ ws.on('close', () => {
166
+ this.clients.delete(ws);
167
+ Logger.info(`WebSocket client disconnected (${this.clients.size} remaining)`);
168
+ });
169
+ ws.on('error', (error) => {
170
+ Logger.warn(`WebSocket error: ${error.message}`);
171
+ this.clients.delete(ws);
172
+ });
173
+ }
174
+ /**
175
+ * Start watching the file for changes
176
+ */
177
+ startWatcher() {
178
+ const watchOptions = {
179
+ debounce: this.options.watchOptions?.debounce ?? 500,
180
+ ignored: this.options.watchOptions?.ignored ?? [],
181
+ };
182
+ this.watcher = new FileWatcher(this.options.filePath, watchOptions);
183
+ this.watcher.onChange(async () => {
184
+ Logger.info('File changed, reconverting...');
185
+ try {
186
+ await this.convertAndCacheFile();
187
+ this.broadcastContent();
188
+ Logger.success('Reconversion complete');
189
+ }
190
+ catch (error) {
191
+ const message = error instanceof Error ? error.message : 'Unknown error';
192
+ Logger.error(`Reconversion failed: ${message}`);
193
+ this.broadcast({
194
+ type: 'error',
195
+ data: { message: `Conversion failed: ${message}` },
196
+ });
197
+ }
198
+ });
199
+ this.watcher.start();
200
+ Logger.info(`Watching: ${this.options.filePath}`);
201
+ }
202
+ /**
203
+ * Convert file and cache the result
204
+ */
205
+ async convertAndCacheFile() {
206
+ const filePath = this.options.filePath;
207
+ const ext = path.extname(filePath).toLowerCase();
208
+ const isLatex = ext === '.tex' || ext === '.latex';
209
+ const fromFormat = isLatex ? 'latex' : 'markdown';
210
+ // Check pandoc for LaTeX files
211
+ if (isLatex && !PandocDetector.check()) {
212
+ throw new Error('LaTeX files require pandoc. Please install pandoc.');
213
+ }
214
+ const content = await fs.readFile(filePath, 'utf-8');
215
+ const parserType = isLatex ? 'pandoc' : 'markdown-it';
216
+ const parser = ParserFactory.create(parserType, this.options.pandocOptions || {}, undefined, fromFormat);
217
+ this.currentContent = await parser.parse(content);
218
+ }
219
+ /**
220
+ * Broadcast current content to all clients
221
+ */
222
+ broadcastContent() {
223
+ if (this.currentContent) {
224
+ this.broadcast({
225
+ type: 'content',
226
+ data: { html: this.currentContent, title: this.fileTitle },
227
+ });
228
+ }
229
+ }
230
+ /**
231
+ * Send message to a specific client
232
+ */
233
+ sendMessage(ws, message) {
234
+ if (ws.readyState === WebSocket.OPEN) {
235
+ ws.send(JSON.stringify(message));
236
+ }
237
+ }
238
+ /**
239
+ * Broadcast message to all clients
240
+ */
241
+ broadcast(message) {
242
+ const data = JSON.stringify(message);
243
+ for (const client of this.clients) {
244
+ if (client.readyState === WebSocket.OPEN) {
245
+ client.send(data);
246
+ }
247
+ }
248
+ }
249
+ /**
250
+ * Serve the HTML template
251
+ */
252
+ serveHtml(_req, res) {
253
+ res.writeHead(200, {
254
+ 'Content-Type': 'text/html; charset=utf-8',
255
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
256
+ });
257
+ res.end(this.renderedHtml);
258
+ }
259
+ /**
260
+ * Render the single file template
261
+ */
262
+ async renderTemplate() {
263
+ // Load template
264
+ const templatePath = path.join(__dirname, '../../templates/single-file.html');
265
+ let template = await fs.readFile(templatePath, 'utf-8');
266
+ // Load theme CSS
267
+ const themeCss = await ThemeManager.getCSS(this.options.theme);
268
+ // Replace placeholders
269
+ template = template
270
+ .replace('{{title}}', this.escapeHtml(this.fileTitle))
271
+ .replace('{{theme_css}}', themeCss);
272
+ // Handle math support
273
+ if (this.options.mathEnabled) {
274
+ template = template
275
+ .replace(/\{\{#if math_enabled\}\}/g, '')
276
+ .replace(/\{\{\/if\}\}/g, '');
277
+ }
278
+ else {
279
+ // Remove math-related content
280
+ template = template.replace(/\{\{#if math_enabled\}\}[\s\S]*?\{\{\/if\}\}/g, '');
281
+ }
282
+ return template;
283
+ }
284
+ /**
285
+ * Escape HTML special characters
286
+ */
287
+ escapeHtml(text) {
288
+ return text
289
+ .replace(/&/g, '&amp;')
290
+ .replace(/</g, '&lt;')
291
+ .replace(/>/g, '&gt;')
292
+ .replace(/"/g, '&quot;')
293
+ .replace(/'/g, '&#039;');
294
+ }
295
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,6 @@ export type { VimdConfig, ThemeInfo } from './config/types.js';
2
2
  export { defineConfig } from './config/types.js';
3
3
  export { MarkdownConverter } from './core/converter.js';
4
4
  export { FileWatcher } from './core/watcher.js';
5
- export { WebSocketServer } from './core/websocket-server.js';
6
5
  export { PandocDetector } from './core/pandoc-detector.js';
7
6
  export { ThemeManager } from './themes/index.js';
8
7
  export { ConfigLoader } from './config/loader.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/index.js CHANGED
@@ -4,7 +4,6 @@ export { defineConfig } from './config/types.js';
4
4
  // ==================== コア機能エクスポート ====================
5
5
  export { MarkdownConverter } from './core/converter.js';
6
6
  export { FileWatcher } from './core/watcher.js';
7
- export { WebSocketServer } from './core/websocket-server.js';
8
7
  export { PandocDetector } from './core/pandoc-detector.js';
9
8
  // ==================== テーマ管理エクスポート ====================
10
9
  export { ThemeManager } from './themes/index.js';