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.
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +17 -75
- package/dist/core/folder-mode/assets/folder-mode.js +35 -5
- package/dist/core/single-file-server.d.ts +87 -0
- package/dist/core/single-file-server.d.ts.map +1 -0
- package/dist/core/single-file-server.js +295 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/templates/single-file.html +259 -0
- package/dist/utils/session-manager.d.ts.map +1 -1
- package/dist/utils/session-manager.js +4 -3
- package/package.json +1 -1
- package/templates/single-file.html +259 -0
- package/dist/core/websocket-server.d.ts +0 -52
- package/dist/core/websocket-server.d.ts.map +0 -1
- package/dist/core/websocket-server.js +0 -221
- package/dist/templates/standalone.html +0 -61
- package/templates/standalone.html +0 -61
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// src/cli/commands/dev.ts
|
|
2
2
|
import { ConfigLoader } from '../../config/loader.js';
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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, '&')
|
|
290
|
+
.replace(/</g, '<')
|
|
291
|
+
.replace(/>/g, '>')
|
|
292
|
+
.replace(/"/g, '"')
|
|
293
|
+
.replace(/'/g, ''');
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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';
|