mdv-live 0.3.1 → 0.3.2
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/CHANGELOG.md +20 -0
- package/bin/mdv.js +134 -89
- package/package.json +1 -1
- package/src/api/file.js +140 -111
- package/src/api/pdf.js +24 -27
- package/src/api/tree.js +35 -28
- package/src/api/upload.js +26 -25
- package/src/rendering/index.js +26 -25
- package/src/rendering/markdown.js +27 -40
- package/src/server.js +53 -66
- package/src/static/app.js +107 -140
- package/src/static/index.html +48 -25
- package/src/static/styles.css +95 -169
- package/src/utils/fileTypes.js +99 -90
- package/src/utils/path.js +11 -14
- package/src/watcher.js +38 -48
- package/src/websocket.js +17 -13
- package/README.pdf +0 -0
package/src/rendering/index.js
CHANGED
|
@@ -52,46 +52,47 @@ export async function renderFile(filePath) {
|
|
|
52
52
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
53
53
|
const fileType = getFileType(filePath);
|
|
54
54
|
|
|
55
|
-
// Markdown files
|
|
56
55
|
if (fileType.type === 'markdown') {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const { html, css } = renderMarp(content);
|
|
60
|
-
return {
|
|
61
|
-
content: html,
|
|
62
|
-
css,
|
|
63
|
-
raw: content,
|
|
64
|
-
fileType: 'markdown',
|
|
65
|
-
isMarp: true
|
|
66
|
-
};
|
|
67
|
-
}
|
|
56
|
+
return renderMarkdownFile(content);
|
|
57
|
+
}
|
|
68
58
|
|
|
69
|
-
|
|
70
|
-
const html = renderMarkdown(content);
|
|
59
|
+
if (fileType.type === 'code') {
|
|
71
60
|
return {
|
|
72
|
-
content:
|
|
61
|
+
content: renderCode(content, fileType.lang),
|
|
73
62
|
raw: content,
|
|
74
|
-
fileType: '
|
|
75
|
-
isMarp: false
|
|
63
|
+
fileType: 'code'
|
|
76
64
|
};
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
return {
|
|
68
|
+
content: renderText(content),
|
|
69
|
+
raw: content,
|
|
70
|
+
fileType: 'text'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render markdown content, detecting Marp presentations
|
|
76
|
+
* @param {string} content - Raw markdown content
|
|
77
|
+
* @returns {Object} Rendered content and metadata
|
|
78
|
+
*/
|
|
79
|
+
function renderMarkdownFile(content) {
|
|
80
|
+
if (isMarp(content)) {
|
|
81
|
+
const { html, css } = renderMarp(content);
|
|
82
82
|
return {
|
|
83
83
|
content: html,
|
|
84
|
+
css,
|
|
84
85
|
raw: content,
|
|
85
|
-
fileType: '
|
|
86
|
+
fileType: 'markdown',
|
|
87
|
+
isMarp: true
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
// Plain text
|
|
90
|
-
const html = renderText(content);
|
|
91
91
|
return {
|
|
92
|
-
content:
|
|
92
|
+
content: renderMarkdown(content),
|
|
93
93
|
raw: content,
|
|
94
|
-
fileType: '
|
|
94
|
+
fileType: 'markdown',
|
|
95
|
+
isMarp: false
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -60,67 +60,54 @@ function convertFrontmatter(content) {
|
|
|
60
60
|
*/
|
|
61
61
|
function protectMermaidBlocks(content) {
|
|
62
62
|
const blocks = [];
|
|
63
|
-
const
|
|
63
|
+
const protectedContent = content.replace(MERMAID_PATTERN, (match, code) => {
|
|
64
64
|
blocks.push(code);
|
|
65
65
|
return `<!--MERMAID_PLACEHOLDER_${blocks.length - 1}-->`;
|
|
66
66
|
});
|
|
67
|
-
return { content:
|
|
67
|
+
return { content: protectedContent, blocks };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
* @param {string}
|
|
73
|
-
* @param {string[]} blocks - Mermaid code blocks
|
|
71
|
+
* Escape HTML entities for safe display
|
|
72
|
+
* @param {string} text - Text to escape
|
|
74
73
|
* @returns {string}
|
|
75
74
|
*/
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.replace(/>/g, '>');
|
|
82
|
-
const mermaidHtml = `<pre><code class="language-mermaid">${escaped}</code></pre>`;
|
|
83
|
-
|
|
84
|
-
// Replace both paragraph-wrapped and bare placeholders
|
|
85
|
-
html = html.replace(`<p><!--MERMAID_PLACEHOLDER_${i}--></p>`, mermaidHtml);
|
|
86
|
-
html = html.replace(`<!--MERMAID_PLACEHOLDER_${i}-->`, mermaidHtml);
|
|
87
|
-
});
|
|
88
|
-
return html;
|
|
75
|
+
function escapeHtmlEntities(text) {
|
|
76
|
+
return text
|
|
77
|
+
.replace(/&/g, '&')
|
|
78
|
+
.replace(/</g, '<')
|
|
79
|
+
.replace(/>/g, '>');
|
|
89
80
|
}
|
|
90
81
|
|
|
91
82
|
/**
|
|
92
|
-
*
|
|
83
|
+
* Restore Mermaid blocks after markdown processing
|
|
93
84
|
* @param {string} html - Rendered HTML
|
|
85
|
+
* @param {string[]} blocks - Mermaid code blocks
|
|
94
86
|
* @returns {string}
|
|
95
87
|
*/
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
function restoreMermaidBlocks(html, blocks) {
|
|
89
|
+
let result = html;
|
|
90
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
91
|
+
const escaped = escapeHtmlEntities(blocks[i]);
|
|
92
|
+
const mermaidHtml = `<pre><code class="language-mermaid">${escaped}</code></pre>`;
|
|
93
|
+
// Replace both paragraph-wrapped and bare placeholders
|
|
94
|
+
result = result
|
|
95
|
+
.replace(`<p><!--MERMAID_PLACEHOLDER_${i}--></p>`, mermaidHtml)
|
|
96
|
+
.replace(`<!--MERMAID_PLACEHOLDER_${i}-->`, mermaidHtml);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
/**
|
|
103
102
|
* Render markdown to HTML
|
|
104
103
|
* @param {string} content - Markdown content
|
|
105
|
-
* @returns {string}
|
|
104
|
+
* @returns {string}
|
|
106
105
|
*/
|
|
107
106
|
export function renderMarkdown(content) {
|
|
108
|
-
|
|
109
|
-
content =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const { content: protected_, blocks } = protectMermaidBlocks(content);
|
|
113
|
-
|
|
114
|
-
// Render markdown
|
|
115
|
-
let html = md.render(protected_);
|
|
116
|
-
|
|
117
|
-
// Restore Mermaid blocks
|
|
118
|
-
html = restoreMermaidBlocks(html, blocks);
|
|
119
|
-
|
|
120
|
-
// Add line numbers
|
|
121
|
-
html = addLineNumbers(html);
|
|
122
|
-
|
|
123
|
-
return html;
|
|
107
|
+
const withFrontmatter = convertFrontmatter(content);
|
|
108
|
+
const { content: protectedContent, blocks } = protectMermaidBlocks(withFrontmatter);
|
|
109
|
+
const html = md.render(protectedContent);
|
|
110
|
+
return restoreMermaidBlocks(html, blocks);
|
|
124
111
|
}
|
|
125
112
|
|
|
126
113
|
export default { renderMarkdown, isMarp };
|
package/src/server.js
CHANGED
|
@@ -7,103 +7,90 @@ import express from 'express';
|
|
|
7
7
|
import { createServer } from 'http';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
-
|
|
11
|
-
import { setupWatcher } from './watcher.js';
|
|
12
|
-
import { setupTreeRoutes } from './api/tree.js';
|
|
10
|
+
|
|
13
11
|
import { setupFileRoutes } from './api/file.js';
|
|
14
|
-
import { setupUploadRoutes } from './api/upload.js';
|
|
15
12
|
import { setupPdfRoutes } from './api/pdf.js';
|
|
13
|
+
import { setupTreeRoutes } from './api/tree.js';
|
|
14
|
+
import { setupUploadRoutes } from './api/upload.js';
|
|
15
|
+
import { setupWatcher } from './watcher.js';
|
|
16
|
+
import { setupWebSocket } from './websocket.js';
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const STATIC_DIR = path.join(__dirname, 'static');
|
|
20
|
+
const VERSION = '0.3.2';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
22
|
-
* @param {
|
|
23
|
-
* @param {string} options.rootDir - Root directory to serve
|
|
24
|
-
* @param {number} options.port - Port to listen on
|
|
25
|
-
* @returns {Object} Server instance and control functions
|
|
23
|
+
* Setup API routes for the Express app
|
|
24
|
+
* @param {express.Application} app - Express application instance
|
|
26
25
|
*/
|
|
27
|
-
|
|
28
|
-
const { rootDir, port = 8080 } = options;
|
|
29
|
-
|
|
30
|
-
const app = express();
|
|
31
|
-
const server = createServer(app);
|
|
32
|
-
|
|
33
|
-
// Store root directory in app locals for access in routes
|
|
34
|
-
app.locals.rootDir = path.resolve(rootDir);
|
|
35
|
-
|
|
36
|
-
// Middleware
|
|
37
|
-
app.use(express.json());
|
|
38
|
-
app.use(express.urlencoded({ extended: true }));
|
|
39
|
-
|
|
40
|
-
// Static files
|
|
41
|
-
const staticDir = path.join(__dirname, 'static');
|
|
42
|
-
app.use('/static', express.static(staticDir));
|
|
43
|
-
|
|
44
|
-
// API routes
|
|
26
|
+
function setupApiRoutes(app) {
|
|
45
27
|
setupTreeRoutes(app);
|
|
46
28
|
setupFileRoutes(app);
|
|
47
29
|
setupUploadRoutes(app);
|
|
48
30
|
setupPdfRoutes(app);
|
|
49
31
|
|
|
50
|
-
// Server info endpoint
|
|
51
32
|
app.get('/api/info', (req, res) => {
|
|
52
33
|
res.json({
|
|
53
34
|
rootPath: app.locals.rootDir,
|
|
54
|
-
version:
|
|
35
|
+
version: VERSION
|
|
55
36
|
});
|
|
56
37
|
});
|
|
57
38
|
|
|
58
|
-
// Shutdown endpoint
|
|
59
39
|
app.post('/api/shutdown', (req, res) => {
|
|
60
40
|
res.json({ success: true });
|
|
61
|
-
setTimeout(() =>
|
|
62
|
-
process.exit(0);
|
|
63
|
-
}, 100);
|
|
41
|
+
setTimeout(() => process.exit(0), 100);
|
|
64
42
|
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create and configure the MDV server
|
|
47
|
+
* @param {Object} options - Server options
|
|
48
|
+
* @param {string} options.rootDir - Root directory to serve
|
|
49
|
+
* @param {number} [options.port=8080] - Port to listen on
|
|
50
|
+
* @returns {{ app: express.Application, server: http.Server, watcher: FSWatcher, wss: WebSocketServer, port: number, start: () => Promise<{port: number}>, stop: () => Promise<void> }}
|
|
51
|
+
*/
|
|
52
|
+
export function createMdvServer(options) {
|
|
53
|
+
const { rootDir, port = 8080 } = options;
|
|
54
|
+
|
|
55
|
+
const app = express();
|
|
56
|
+
const server = createServer(app);
|
|
57
|
+
|
|
58
|
+
app.locals.rootDir = path.resolve(rootDir);
|
|
59
|
+
|
|
60
|
+
app.use(express.json());
|
|
61
|
+
app.use(express.urlencoded({ extended: true }));
|
|
62
|
+
app.use('/static', express.static(STATIC_DIR));
|
|
63
|
+
|
|
64
|
+
setupApiRoutes(app);
|
|
65
65
|
|
|
66
|
-
// Serve index.html for root
|
|
67
66
|
app.get('/', (req, res) => {
|
|
68
|
-
res.sendFile(path.join(
|
|
67
|
+
res.sendFile(path.join(STATIC_DIR, 'index.html'));
|
|
69
68
|
});
|
|
70
69
|
|
|
71
|
-
// Setup WebSocket
|
|
72
70
|
const wss = setupWebSocket(server);
|
|
73
|
-
|
|
74
|
-
// Setup file watcher
|
|
75
71
|
const watcher = setupWatcher(app.locals.rootDir, wss);
|
|
76
72
|
|
|
77
|
-
// Store watcher reference
|
|
78
73
|
app.locals.watcher = watcher;
|
|
79
74
|
app.locals.wss = wss;
|
|
80
75
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
port,
|
|
87
|
-
|
|
88
|
-
start() {
|
|
89
|
-
return new Promise((resolve) => {
|
|
90
|
-
server.listen(port, () => {
|
|
91
|
-
console.log(`MDV server running at http://localhost:${port}`);
|
|
92
|
-
resolve({ port });
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
stop() {
|
|
98
|
-
return new Promise((resolve) => {
|
|
99
|
-
watcher.close();
|
|
100
|
-
wss.close();
|
|
101
|
-
server.close(() => {
|
|
102
|
-
resolve();
|
|
103
|
-
});
|
|
76
|
+
function start() {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
server.listen(port, () => {
|
|
79
|
+
console.log(`MDV server running at http://localhost:${port}`);
|
|
80
|
+
resolve({ port });
|
|
104
81
|
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stop() {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
watcher.close();
|
|
88
|
+
wss.close();
|
|
89
|
+
server.close(resolve);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { app, server, watcher, wss, port, start, stop };
|
|
107
94
|
}
|
|
108
95
|
|
|
109
96
|
export default createMdvServer;
|