vimd 0.5.6 → 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/parser/pandoc-parser.d.ts +5 -0
- package/dist/core/parser/pandoc-parser.d.ts.map +1 -1
- package/dist/core/parser/pandoc-parser.js +31 -2
- 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/filters/latex-metadata.lua +135 -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/default.html +7 -3
- package/dist/templates/folder-mode.html +7 -3
- package/dist/templates/single-file.html +259 -0
- package/dist/themes/styles/academic.css +99 -0
- package/dist/themes/styles/dark.css +94 -0
- package/dist/themes/styles/github.css +94 -0
- package/dist/themes/styles/minimal.css +96 -0
- package/dist/themes/styles/technical.css +96 -0
- package/dist/utils/session-manager.d.ts.map +1 -1
- package/dist/utils/session-manager.js +4 -3
- package/filters/latex-metadata.lua +135 -0
- package/package.json +2 -1
- package/templates/default.html +7 -3
- package/templates/folder-mode.html +7 -3
- 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 -57
- package/templates/standalone.html +0 -57
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="generator" content="vimd">
|
|
7
|
+
<title>{{title}} - vimd</title>
|
|
8
|
+
<style>
|
|
9
|
+
{{theme_css}}
|
|
10
|
+
</style>
|
|
11
|
+
<style>
|
|
12
|
+
/* Single file mode styles */
|
|
13
|
+
* {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
html, body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.vimd-single-container {
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100vh;
|
|
31
|
+
overflow-y: auto;
|
|
32
|
+
overflow-x: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.vimd-single-content {
|
|
36
|
+
padding: 32px;
|
|
37
|
+
max-width: 720px;
|
|
38
|
+
margin: 0 auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.vimd-single-content pre,
|
|
42
|
+
.vimd-single-content table {
|
|
43
|
+
overflow-x: auto;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Connection status indicator */
|
|
47
|
+
.vimd-connection-status {
|
|
48
|
+
position: fixed;
|
|
49
|
+
bottom: 16px;
|
|
50
|
+
right: 16px;
|
|
51
|
+
padding: 8px 12px;
|
|
52
|
+
background: rgba(0, 0, 0, 0.7);
|
|
53
|
+
color: #fff;
|
|
54
|
+
font-size: 12px;
|
|
55
|
+
border-radius: 4px;
|
|
56
|
+
opacity: 0;
|
|
57
|
+
transition: opacity 0.3s;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
z-index: 1000;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.vimd-connection-status.visible {
|
|
63
|
+
opacity: 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.vimd-connection-status.error {
|
|
67
|
+
background: rgba(211, 47, 47, 0.9);
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
70
|
+
{{#if math_enabled}}
|
|
71
|
+
<style>
|
|
72
|
+
/* Block math centering */
|
|
73
|
+
.math-block {
|
|
74
|
+
display: block;
|
|
75
|
+
text-align: center;
|
|
76
|
+
margin: 1em 0;
|
|
77
|
+
}
|
|
78
|
+
mjx-container[display="true"] {
|
|
79
|
+
display: block !important;
|
|
80
|
+
text-align: center !important;
|
|
81
|
+
margin: 1em 0 !important;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
84
|
+
<script>
|
|
85
|
+
MathJax = {
|
|
86
|
+
loader: {load: ['[tex]/bussproofs', '[tex]/ams', '[tex]/physics', '[tex]/boldsymbol']},
|
|
87
|
+
tex: {
|
|
88
|
+
packages: {'[+]': ['bussproofs', 'ams', 'physics', 'boldsymbol']},
|
|
89
|
+
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
|
90
|
+
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
|
91
|
+
tags: 'ams',
|
|
92
|
+
macros: {
|
|
93
|
+
bm: ['\\boldsymbol{#1}', 1]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
</script>
|
|
98
|
+
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
|
99
|
+
{{/if}}
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<div class="vimd-single-container" id="container">
|
|
103
|
+
<article class="vimd-single-content markdown-body" id="content">
|
|
104
|
+
<!-- Content will be loaded via WebSocket -->
|
|
105
|
+
</article>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="vimd-connection-status" id="status"></div>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
(function() {
|
|
112
|
+
'use strict';
|
|
113
|
+
|
|
114
|
+
var container = document.getElementById('container');
|
|
115
|
+
var content = document.getElementById('content');
|
|
116
|
+
var status = document.getElementById('status');
|
|
117
|
+
var ws = null;
|
|
118
|
+
var reconnectAttempts = 0;
|
|
119
|
+
var maxReconnectAttempts = 10;
|
|
120
|
+
var reconnectDelay = 1000;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Show connection status message
|
|
124
|
+
*/
|
|
125
|
+
function showStatus(message, isError) {
|
|
126
|
+
status.textContent = message;
|
|
127
|
+
status.classList.toggle('error', isError);
|
|
128
|
+
status.classList.add('visible');
|
|
129
|
+
|
|
130
|
+
// Hide after 3 seconds for non-error messages
|
|
131
|
+
if (!isError) {
|
|
132
|
+
setTimeout(function() {
|
|
133
|
+
status.classList.remove('visible');
|
|
134
|
+
}, 3000);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Hide connection status
|
|
140
|
+
*/
|
|
141
|
+
function hideStatus() {
|
|
142
|
+
status.classList.remove('visible');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Connect to WebSocket server
|
|
147
|
+
*/
|
|
148
|
+
function connect() {
|
|
149
|
+
var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
150
|
+
ws = new WebSocket(protocol + '//' + location.host);
|
|
151
|
+
|
|
152
|
+
ws.onopen = function() {
|
|
153
|
+
console.log('[vimd] WebSocket connected');
|
|
154
|
+
reconnectAttempts = 0;
|
|
155
|
+
hideStatus();
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
ws.onmessage = function(event) {
|
|
159
|
+
try {
|
|
160
|
+
var msg = JSON.parse(event.data);
|
|
161
|
+
handleMessage(msg);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error('[vimd] Failed to parse message:', e);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
ws.onclose = function() {
|
|
168
|
+
console.log('[vimd] WebSocket disconnected');
|
|
169
|
+
|
|
170
|
+
if (reconnectAttempts < maxReconnectAttempts) {
|
|
171
|
+
reconnectAttempts++;
|
|
172
|
+
showStatus('Reconnecting... (' + reconnectAttempts + '/' + maxReconnectAttempts + ')', false);
|
|
173
|
+
setTimeout(connect, reconnectDelay);
|
|
174
|
+
} else {
|
|
175
|
+
showStatus('Connection lost. Please refresh the page.', true);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
ws.onerror = function(error) {
|
|
180
|
+
console.error('[vimd] WebSocket error:', error);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle incoming WebSocket message
|
|
186
|
+
*/
|
|
187
|
+
function handleMessage(msg) {
|
|
188
|
+
switch (msg.type) {
|
|
189
|
+
case 'content':
|
|
190
|
+
updateContent(msg.data.html);
|
|
191
|
+
if (msg.data.title) {
|
|
192
|
+
document.title = msg.data.title + ' - vimd';
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case 'error':
|
|
197
|
+
showError(msg.data.message);
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
console.warn('[vimd] Unknown message type:', msg.type);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Update content with scroll position preservation
|
|
207
|
+
*/
|
|
208
|
+
function updateContent(html) {
|
|
209
|
+
// Save scroll position
|
|
210
|
+
var scrollTop = container.scrollTop;
|
|
211
|
+
|
|
212
|
+
// Update content
|
|
213
|
+
content.innerHTML = html;
|
|
214
|
+
|
|
215
|
+
// Re-render MathJax if available
|
|
216
|
+
if (window.MathJax && window.MathJax.typeset) {
|
|
217
|
+
// MathJax.typeset returns a promise in v3
|
|
218
|
+
var promise = window.MathJax.typeset([content]);
|
|
219
|
+
|
|
220
|
+
// Restore scroll position after MathJax completes
|
|
221
|
+
if (promise && promise.then) {
|
|
222
|
+
promise.then(function() {
|
|
223
|
+
container.scrollTop = scrollTop;
|
|
224
|
+
}).catch(function() {
|
|
225
|
+
// Restore scroll even if MathJax fails
|
|
226
|
+
container.scrollTop = scrollTop;
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
// Fallback: restore scroll immediately
|
|
230
|
+
container.scrollTop = scrollTop;
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
// No MathJax: restore scroll immediately
|
|
234
|
+
container.scrollTop = scrollTop;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Show error message
|
|
240
|
+
*/
|
|
241
|
+
function showError(message) {
|
|
242
|
+
content.innerHTML = '<div class="vimd-error"><h2>Error</h2><p>' + escapeHtml(message) + '</p></div>';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Escape HTML special characters
|
|
247
|
+
*/
|
|
248
|
+
function escapeHtml(text) {
|
|
249
|
+
var div = document.createElement('div');
|
|
250
|
+
div.textContent = text;
|
|
251
|
+
return div.innerHTML;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Initialize connection
|
|
255
|
+
connect();
|
|
256
|
+
})();
|
|
257
|
+
</script>
|
|
258
|
+
</body>
|
|
259
|
+
</html>
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocketServer options
|
|
3
|
-
* Note: 'open' property is not included (handled by dev.ts)
|
|
4
|
-
*/
|
|
5
|
-
export interface WebSocketServerOptions {
|
|
6
|
-
port: number;
|
|
7
|
-
root: string;
|
|
8
|
-
host?: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Result of server start operation
|
|
12
|
-
* Same interface as previous LiveServer for compatibility
|
|
13
|
-
*/
|
|
14
|
-
export interface ServerStartResult {
|
|
15
|
-
actualPort: number;
|
|
16
|
-
requestedPort: number;
|
|
17
|
-
portChanged: boolean;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* WebSocket server for live reload functionality
|
|
21
|
-
* Replaces live-server with direct WebSocket control
|
|
22
|
-
*/
|
|
23
|
-
export declare class WebSocketServer {
|
|
24
|
-
private httpServer;
|
|
25
|
-
private wsServer;
|
|
26
|
-
private clients;
|
|
27
|
-
private options;
|
|
28
|
-
private _port;
|
|
29
|
-
constructor(options: WebSocketServerOptions);
|
|
30
|
-
/**
|
|
31
|
-
* Get the actual port the server is running on
|
|
32
|
-
*/
|
|
33
|
-
get port(): number;
|
|
34
|
-
/**
|
|
35
|
-
* Start the HTTP and WebSocket servers
|
|
36
|
-
*/
|
|
37
|
-
start(): Promise<ServerStartResult>;
|
|
38
|
-
/**
|
|
39
|
-
* Stop the server
|
|
40
|
-
* Uses force termination to ensure immediate shutdown
|
|
41
|
-
*/
|
|
42
|
-
stop(): Promise<void>;
|
|
43
|
-
/**
|
|
44
|
-
* Broadcast a message to all connected clients
|
|
45
|
-
*/
|
|
46
|
-
broadcast(type: string, data?: unknown): void;
|
|
47
|
-
/**
|
|
48
|
-
* Inject reload script into HTML content
|
|
49
|
-
*/
|
|
50
|
-
private injectReloadScript;
|
|
51
|
-
}
|
|
52
|
-
//# sourceMappingURL=websocket-server.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-server.d.ts","sourceRoot":"","sources":["../../src/core/websocket-server.ts"],"names":[],"mappings":"AAUA;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AA0CD;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,KAAK,CAAS;gBAEV,OAAO,EAAE,sBAAsB;IAQ3C;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC;IA4GzC;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiC3B;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI;IAU7C;;OAEG;IACH,OAAO,CAAC,kBAAkB;CAc3B"}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
// src/core/websocket-server.ts
|
|
2
|
-
import http from 'http';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import polka from 'polka';
|
|
6
|
-
import sirv from 'sirv';
|
|
7
|
-
import { WebSocketServer as WSServer, WebSocket } from 'ws';
|
|
8
|
-
import { Logger } from '../utils/logger.js';
|
|
9
|
-
import { SessionManager } from '../utils/session-manager.js';
|
|
10
|
-
/**
|
|
11
|
-
* WebSocket reload script injected into HTML
|
|
12
|
-
* Features:
|
|
13
|
-
* - Reconnection with max 5 retries (prevents infinite loop)
|
|
14
|
-
* - Protocol detection for future HTTPS support
|
|
15
|
-
*/
|
|
16
|
-
const RELOAD_SCRIPT = `
|
|
17
|
-
<script>
|
|
18
|
-
(function() {
|
|
19
|
-
var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
20
|
-
var ws = new WebSocket(protocol + '//' + location.host);
|
|
21
|
-
|
|
22
|
-
ws.onmessage = function(event) {
|
|
23
|
-
var data = JSON.parse(event.data);
|
|
24
|
-
if (data.type === 'reload') {
|
|
25
|
-
location.reload();
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
ws.onclose = function() {
|
|
30
|
-
var retries = 0;
|
|
31
|
-
var maxRetries = 5;
|
|
32
|
-
|
|
33
|
-
function reconnect() {
|
|
34
|
-
if (retries++ >= maxRetries) {
|
|
35
|
-
console.log('[vimd] Server disconnected. Please refresh manually.');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
setTimeout(function() {
|
|
39
|
-
var newWs = new WebSocket(protocol + '//' + location.host);
|
|
40
|
-
newWs.onopen = function() { location.reload(); };
|
|
41
|
-
newWs.onerror = reconnect;
|
|
42
|
-
}, 1000);
|
|
43
|
-
}
|
|
44
|
-
reconnect();
|
|
45
|
-
};
|
|
46
|
-
})();
|
|
47
|
-
</script>
|
|
48
|
-
`;
|
|
49
|
-
/**
|
|
50
|
-
* WebSocket server for live reload functionality
|
|
51
|
-
* Replaces live-server with direct WebSocket control
|
|
52
|
-
*/
|
|
53
|
-
export class WebSocketServer {
|
|
54
|
-
constructor(options) {
|
|
55
|
-
this.httpServer = null;
|
|
56
|
-
this.wsServer = null;
|
|
57
|
-
this.clients = new Set();
|
|
58
|
-
this.options = {
|
|
59
|
-
host: 'localhost',
|
|
60
|
-
...options,
|
|
61
|
-
};
|
|
62
|
-
this._port = options.port;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Get the actual port the server is running on
|
|
66
|
-
*/
|
|
67
|
-
get port() {
|
|
68
|
-
return this._port;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Start the HTTP and WebSocket servers
|
|
72
|
-
*/
|
|
73
|
-
async start() {
|
|
74
|
-
const requestedPort = this.options.port;
|
|
75
|
-
let actualPort = requestedPort;
|
|
76
|
-
// Check if port is available, find alternative if not
|
|
77
|
-
if (!(await SessionManager.isPortAvailable(requestedPort))) {
|
|
78
|
-
actualPort = await SessionManager.findAvailablePort(requestedPort + 1);
|
|
79
|
-
Logger.warn(`Port ${requestedPort} was unavailable, using port ${actualPort}`);
|
|
80
|
-
}
|
|
81
|
-
this._port = actualPort;
|
|
82
|
-
// Create static file server with sirv
|
|
83
|
-
const serve = sirv(this.options.root, {
|
|
84
|
-
dev: true, // Enable dev mode for no caching
|
|
85
|
-
});
|
|
86
|
-
// Create polka app with HTML injection middleware
|
|
87
|
-
const app = polka();
|
|
88
|
-
// Middleware to inject reload script into HTML
|
|
89
|
-
// For HTML files, read directly to avoid sirv streaming issues
|
|
90
|
-
app.use((req, res, next) => {
|
|
91
|
-
// Only process GET requests for HTML files
|
|
92
|
-
if (req.method !== 'GET' || !req.url?.endsWith('.html')) {
|
|
93
|
-
return serve(req, res, next);
|
|
94
|
-
}
|
|
95
|
-
// Parse the URL and resolve the file path
|
|
96
|
-
const urlPath = req.url || '/index.html';
|
|
97
|
-
const filePath = path.join(this.options.root, urlPath);
|
|
98
|
-
// Check if file exists
|
|
99
|
-
if (!fs.existsSync(filePath)) {
|
|
100
|
-
// Let sirv handle 404
|
|
101
|
-
return serve(req, res, next);
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
// Read HTML file directly
|
|
105
|
-
const html = fs.readFileSync(filePath, 'utf8');
|
|
106
|
-
// Inject reload script
|
|
107
|
-
const injectedHtml = this.injectReloadScript(html);
|
|
108
|
-
// Send response with proper headers
|
|
109
|
-
res.writeHead(200, {
|
|
110
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
111
|
-
'Content-Length': Buffer.byteLength(injectedHtml, 'utf8'),
|
|
112
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
113
|
-
});
|
|
114
|
-
res.end(injectedHtml);
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
// On error, fall back to sirv
|
|
118
|
-
Logger.warn(`Failed to read HTML file: ${filePath}`);
|
|
119
|
-
serve(req, res, next);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
// Create HTTP server
|
|
123
|
-
this.httpServer = http.createServer(app.handler);
|
|
124
|
-
// Create WebSocket server attached to HTTP server
|
|
125
|
-
this.wsServer = new WSServer({ server: this.httpServer });
|
|
126
|
-
// Handle WebSocket connections
|
|
127
|
-
this.wsServer.on('connection', (ws) => {
|
|
128
|
-
this.clients.add(ws);
|
|
129
|
-
Logger.info(`WebSocket client connected (${this.clients.size} total)`);
|
|
130
|
-
ws.on('close', () => {
|
|
131
|
-
this.clients.delete(ws);
|
|
132
|
-
Logger.info(`WebSocket client disconnected (${this.clients.size} remaining)`);
|
|
133
|
-
});
|
|
134
|
-
ws.on('error', (error) => {
|
|
135
|
-
Logger.warn(`WebSocket error: ${error.message}`);
|
|
136
|
-
this.clients.delete(ws);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
// Start listening
|
|
140
|
-
await new Promise((resolve, reject) => {
|
|
141
|
-
const timeout = setTimeout(() => {
|
|
142
|
-
reject(new Error('Server start timeout'));
|
|
143
|
-
}, 10000);
|
|
144
|
-
this.httpServer.listen(actualPort, this.options.host, () => {
|
|
145
|
-
clearTimeout(timeout);
|
|
146
|
-
resolve();
|
|
147
|
-
});
|
|
148
|
-
this.httpServer.on('error', (err) => {
|
|
149
|
-
clearTimeout(timeout);
|
|
150
|
-
reject(err);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
const url = `http://${this.options.host}:${actualPort}`;
|
|
154
|
-
Logger.success(`Server started at ${url}`);
|
|
155
|
-
return {
|
|
156
|
-
actualPort,
|
|
157
|
-
requestedPort,
|
|
158
|
-
portChanged: actualPort !== requestedPort,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Stop the server
|
|
163
|
-
* Uses force termination to ensure immediate shutdown
|
|
164
|
-
*/
|
|
165
|
-
async stop() {
|
|
166
|
-
// Force terminate all WebSocket clients (immediate disconnect)
|
|
167
|
-
// Use terminate() instead of close() to avoid waiting for graceful handshake
|
|
168
|
-
for (const client of this.clients) {
|
|
169
|
-
try {
|
|
170
|
-
client.terminate();
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
// Ignore termination errors
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
this.clients.clear();
|
|
177
|
-
// Close WebSocket server (should be instant now that clients are terminated)
|
|
178
|
-
if (this.wsServer) {
|
|
179
|
-
await new Promise((resolve) => {
|
|
180
|
-
this.wsServer.close(() => resolve());
|
|
181
|
-
});
|
|
182
|
-
this.wsServer = null;
|
|
183
|
-
}
|
|
184
|
-
// Force close all HTTP connections before closing server
|
|
185
|
-
// closeAllConnections() requires Node.js >= 18.2.0
|
|
186
|
-
if (this.httpServer) {
|
|
187
|
-
this.httpServer.closeAllConnections();
|
|
188
|
-
await new Promise((resolve) => {
|
|
189
|
-
this.httpServer.close(() => resolve());
|
|
190
|
-
});
|
|
191
|
-
this.httpServer = null;
|
|
192
|
-
}
|
|
193
|
-
Logger.info('Server stopped');
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Broadcast a message to all connected clients
|
|
197
|
-
*/
|
|
198
|
-
broadcast(type, data) {
|
|
199
|
-
const message = JSON.stringify({ type, data });
|
|
200
|
-
for (const client of this.clients) {
|
|
201
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
202
|
-
client.send(message);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Inject reload script into HTML content
|
|
208
|
-
*/
|
|
209
|
-
injectReloadScript(html) {
|
|
210
|
-
// Try to inject before </body>
|
|
211
|
-
if (html.includes('</body>')) {
|
|
212
|
-
return html.replace('</body>', `${RELOAD_SCRIPT}</body>`);
|
|
213
|
-
}
|
|
214
|
-
// Fallback: inject before </html>
|
|
215
|
-
if (html.includes('</html>')) {
|
|
216
|
-
return html.replace('</html>', `${RELOAD_SCRIPT}</html>`);
|
|
217
|
-
}
|
|
218
|
-
// Last resort: append to the end
|
|
219
|
-
return html + RELOAD_SCRIPT;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="ja">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<meta name="generator" content="vimd">
|
|
7
|
-
<title>{{title}}</title>
|
|
8
|
-
<style>
|
|
9
|
-
{{theme_css}}
|
|
10
|
-
</style>
|
|
11
|
-
{{#if custom_css}}
|
|
12
|
-
<style>
|
|
13
|
-
{{custom_css}}
|
|
14
|
-
</style>
|
|
15
|
-
{{/if}}
|
|
16
|
-
{{#if math_enabled}}
|
|
17
|
-
<style>
|
|
18
|
-
/* Block math centering */
|
|
19
|
-
.math-block {
|
|
20
|
-
display: block;
|
|
21
|
-
text-align: center;
|
|
22
|
-
margin: 1em 0;
|
|
23
|
-
}
|
|
24
|
-
mjx-container[display="true"] {
|
|
25
|
-
display: block !important;
|
|
26
|
-
text-align: center !important;
|
|
27
|
-
margin: 1em 0 !important;
|
|
28
|
-
}
|
|
29
|
-
</style>
|
|
30
|
-
<script>
|
|
31
|
-
MathJax = {
|
|
32
|
-
loader: {load: ['[tex]/bussproofs', '[tex]/ams', '[tex]/physics']},
|
|
33
|
-
tex: {
|
|
34
|
-
packages: {'[+]': ['bussproofs', 'ams', 'physics']},
|
|
35
|
-
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
|
36
|
-
displayMath: [['$$', '$$'], ['\\[', '\\]']]
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
</script>
|
|
40
|
-
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
|
41
|
-
{{/if}}
|
|
42
|
-
</head>
|
|
43
|
-
<body>
|
|
44
|
-
<div class="markdown-body">
|
|
45
|
-
{{content}}
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<!-- vimd metadata -->
|
|
49
|
-
<script type="application/json" id="vimd-meta">
|
|
50
|
-
{
|
|
51
|
-
"generated": "{{timestamp}}",
|
|
52
|
-
"theme": "{{theme}}",
|
|
53
|
-
"version": "{{version}}"
|
|
54
|
-
}
|
|
55
|
-
</script>
|
|
56
|
-
</body>
|
|
57
|
-
</html>
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="ja">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<meta name="generator" content="vimd">
|
|
7
|
-
<title>{{title}}</title>
|
|
8
|
-
<style>
|
|
9
|
-
{{theme_css}}
|
|
10
|
-
</style>
|
|
11
|
-
{{#if custom_css}}
|
|
12
|
-
<style>
|
|
13
|
-
{{custom_css}}
|
|
14
|
-
</style>
|
|
15
|
-
{{/if}}
|
|
16
|
-
{{#if math_enabled}}
|
|
17
|
-
<style>
|
|
18
|
-
/* Block math centering */
|
|
19
|
-
.math-block {
|
|
20
|
-
display: block;
|
|
21
|
-
text-align: center;
|
|
22
|
-
margin: 1em 0;
|
|
23
|
-
}
|
|
24
|
-
mjx-container[display="true"] {
|
|
25
|
-
display: block !important;
|
|
26
|
-
text-align: center !important;
|
|
27
|
-
margin: 1em 0 !important;
|
|
28
|
-
}
|
|
29
|
-
</style>
|
|
30
|
-
<script>
|
|
31
|
-
MathJax = {
|
|
32
|
-
loader: {load: ['[tex]/bussproofs', '[tex]/ams', '[tex]/physics']},
|
|
33
|
-
tex: {
|
|
34
|
-
packages: {'[+]': ['bussproofs', 'ams', 'physics']},
|
|
35
|
-
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
|
36
|
-
displayMath: [['$$', '$$'], ['\\[', '\\]']]
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
</script>
|
|
40
|
-
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
|
41
|
-
{{/if}}
|
|
42
|
-
</head>
|
|
43
|
-
<body>
|
|
44
|
-
<div class="markdown-body">
|
|
45
|
-
{{content}}
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<!-- vimd metadata -->
|
|
49
|
-
<script type="application/json" id="vimd-meta">
|
|
50
|
-
{
|
|
51
|
-
"generated": "{{timestamp}}",
|
|
52
|
-
"theme": "{{theme}}",
|
|
53
|
-
"version": "{{version}}"
|
|
54
|
-
}
|
|
55
|
-
</script>
|
|
56
|
-
</body>
|
|
57
|
-
</html>
|