mdv-live 0.3.1 → 0.3.3
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 +26 -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/utils/fileTypes.js
CHANGED
|
@@ -2,62 +2,70 @@
|
|
|
2
2
|
* File type detection and classification
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Helper functions to reduce repetition
|
|
6
|
+
function code(icon, lang) {
|
|
7
|
+
return { type: 'code', icon, lang, binary: false };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function binary(type, icon = type) {
|
|
11
|
+
return { type, icon, lang: null, binary: true };
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
const FILE_TYPES = {
|
|
7
15
|
// Markdown
|
|
8
16
|
md: { type: 'markdown', icon: 'markdown', lang: null, binary: false },
|
|
9
17
|
markdown: { type: 'markdown', icon: 'markdown', lang: null, binary: false },
|
|
10
18
|
|
|
11
19
|
// Code - Python
|
|
12
|
-
py:
|
|
13
|
-
pyw:
|
|
20
|
+
py: code('python', 'python'),
|
|
21
|
+
pyw: code('python', 'python'),
|
|
14
22
|
|
|
15
23
|
// Code - JavaScript/TypeScript
|
|
16
|
-
js:
|
|
17
|
-
mjs:
|
|
18
|
-
cjs:
|
|
19
|
-
ts:
|
|
20
|
-
tsx:
|
|
21
|
-
jsx:
|
|
24
|
+
js: code('javascript', 'javascript'),
|
|
25
|
+
mjs: code('javascript', 'javascript'),
|
|
26
|
+
cjs: code('javascript', 'javascript'),
|
|
27
|
+
ts: code('typescript', 'typescript'),
|
|
28
|
+
tsx: code('react', 'tsx'),
|
|
29
|
+
jsx: code('react', 'jsx'),
|
|
22
30
|
|
|
23
31
|
// Code - Web
|
|
24
|
-
html:
|
|
25
|
-
htm:
|
|
26
|
-
css:
|
|
27
|
-
scss:
|
|
28
|
-
less:
|
|
29
|
-
vue:
|
|
30
|
-
svelte:
|
|
32
|
+
html: code('html', 'html'),
|
|
33
|
+
htm: code('html', 'html'),
|
|
34
|
+
css: code('css', 'css'),
|
|
35
|
+
scss: code('css', 'scss'),
|
|
36
|
+
less: code('css', 'less'),
|
|
37
|
+
vue: code('vue', 'vue'),
|
|
38
|
+
svelte: code('default', 'svelte'),
|
|
31
39
|
|
|
32
40
|
// Data formats
|
|
33
|
-
json:
|
|
34
|
-
yaml:
|
|
35
|
-
yml:
|
|
36
|
-
toml:
|
|
37
|
-
xml:
|
|
41
|
+
json: code('json', 'json'),
|
|
42
|
+
yaml: code('yaml', 'yaml'),
|
|
43
|
+
yml: code('yaml', 'yaml'),
|
|
44
|
+
toml: code('config', 'toml'),
|
|
45
|
+
xml: code('default', 'xml'),
|
|
38
46
|
|
|
39
47
|
// Shell/Config
|
|
40
|
-
sh:
|
|
41
|
-
bash:
|
|
42
|
-
zsh:
|
|
43
|
-
fish:
|
|
44
|
-
env:
|
|
45
|
-
ini:
|
|
46
|
-
conf:
|
|
48
|
+
sh: code('shell', 'bash'),
|
|
49
|
+
bash: code('shell', 'bash'),
|
|
50
|
+
zsh: code('shell', 'bash'),
|
|
51
|
+
fish: code('shell', 'bash'),
|
|
52
|
+
env: code('config', 'bash'),
|
|
53
|
+
ini: code('config', 'ini'),
|
|
54
|
+
conf: code('config', 'ini'),
|
|
47
55
|
|
|
48
56
|
// Other languages
|
|
49
|
-
go:
|
|
50
|
-
rs:
|
|
51
|
-
rb:
|
|
52
|
-
php:
|
|
53
|
-
java:
|
|
54
|
-
kt:
|
|
55
|
-
swift:
|
|
56
|
-
c:
|
|
57
|
-
cpp:
|
|
58
|
-
h:
|
|
59
|
-
cs:
|
|
60
|
-
sql:
|
|
57
|
+
go: code('default', 'go'),
|
|
58
|
+
rs: code('default', 'rust'),
|
|
59
|
+
rb: code('default', 'ruby'),
|
|
60
|
+
php: code('default', 'php'),
|
|
61
|
+
java: code('default', 'java'),
|
|
62
|
+
kt: code('default', 'kotlin'),
|
|
63
|
+
swift: code('default', 'swift'),
|
|
64
|
+
c: code('default', 'c'),
|
|
65
|
+
cpp: code('default', 'cpp'),
|
|
66
|
+
h: code('default', 'c'),
|
|
67
|
+
cs: code('default', 'csharp'),
|
|
68
|
+
sql: code('database', 'sql'),
|
|
61
69
|
|
|
62
70
|
// Text files
|
|
63
71
|
txt: { type: 'text', icon: 'text', lang: null, binary: false },
|
|
@@ -65,84 +73,85 @@ const FILE_TYPES = {
|
|
|
65
73
|
csv: { type: 'text', icon: 'text', lang: null, binary: false },
|
|
66
74
|
|
|
67
75
|
// Images
|
|
68
|
-
png:
|
|
69
|
-
jpg:
|
|
70
|
-
jpeg:
|
|
71
|
-
gif:
|
|
72
|
-
svg:
|
|
73
|
-
webp:
|
|
74
|
-
ico:
|
|
76
|
+
png: binary('image'),
|
|
77
|
+
jpg: binary('image'),
|
|
78
|
+
jpeg: binary('image'),
|
|
79
|
+
gif: binary('image'),
|
|
80
|
+
svg: binary('image'),
|
|
81
|
+
webp: binary('image'),
|
|
82
|
+
ico: binary('image'),
|
|
75
83
|
|
|
76
84
|
// PDF
|
|
77
|
-
pdf:
|
|
85
|
+
pdf: binary('pdf'),
|
|
78
86
|
|
|
79
87
|
// Video
|
|
80
|
-
mp4:
|
|
81
|
-
webm:
|
|
82
|
-
mov:
|
|
83
|
-
avi:
|
|
84
|
-
mkv:
|
|
88
|
+
mp4: binary('video'),
|
|
89
|
+
webm: binary('video'),
|
|
90
|
+
mov: binary('video'),
|
|
91
|
+
avi: binary('video'),
|
|
92
|
+
mkv: binary('video'),
|
|
85
93
|
|
|
86
94
|
// Audio
|
|
87
|
-
mp3:
|
|
88
|
-
wav:
|
|
89
|
-
ogg:
|
|
90
|
-
m4a:
|
|
91
|
-
flac:
|
|
95
|
+
mp3: binary('audio'),
|
|
96
|
+
wav: binary('audio'),
|
|
97
|
+
ogg: binary('audio'),
|
|
98
|
+
m4a: binary('audio'),
|
|
99
|
+
flac: binary('audio'),
|
|
92
100
|
|
|
93
101
|
// Archives
|
|
94
|
-
zip:
|
|
95
|
-
tar:
|
|
96
|
-
gz:
|
|
97
|
-
rar:
|
|
98
|
-
'7z':
|
|
102
|
+
zip: binary('archive'),
|
|
103
|
+
tar: binary('archive'),
|
|
104
|
+
gz: binary('archive'),
|
|
105
|
+
rar: binary('archive'),
|
|
106
|
+
'7z': binary('archive'),
|
|
99
107
|
|
|
100
108
|
// Office
|
|
101
|
-
doc:
|
|
102
|
-
docx:
|
|
103
|
-
xls:
|
|
104
|
-
xlsx:
|
|
105
|
-
ppt:
|
|
106
|
-
pptx:
|
|
109
|
+
doc: binary('office'),
|
|
110
|
+
docx: binary('office'),
|
|
111
|
+
xls: binary('office'),
|
|
112
|
+
xlsx: binary('office'),
|
|
113
|
+
ppt: binary('office'),
|
|
114
|
+
pptx: binary('office'),
|
|
107
115
|
|
|
108
116
|
// Executables
|
|
109
|
-
exe:
|
|
110
|
-
dmg:
|
|
111
|
-
app:
|
|
117
|
+
exe: binary('executable'),
|
|
118
|
+
dmg: binary('executable'),
|
|
119
|
+
app: binary('executable'),
|
|
112
120
|
};
|
|
113
121
|
|
|
114
|
-
// Special filenames
|
|
122
|
+
// Special filenames that don't follow extension-based detection
|
|
115
123
|
const SPECIAL_FILES = {
|
|
116
|
-
'Dockerfile':
|
|
117
|
-
'Makefile':
|
|
118
|
-
'.gitignore':
|
|
119
|
-
'.env':
|
|
120
|
-
'.env.local':
|
|
121
|
-
'.env.example':
|
|
124
|
+
'Dockerfile': code('config', 'dockerfile'),
|
|
125
|
+
'Makefile': code('config', 'makefile'),
|
|
126
|
+
'.gitignore': code('config', 'gitignore'),
|
|
127
|
+
'.env': code('config', 'bash'),
|
|
128
|
+
'.env.local': code('config', 'bash'),
|
|
129
|
+
'.env.example': code('config', 'bash'),
|
|
122
130
|
};
|
|
123
131
|
|
|
132
|
+
const DEFAULT_FILE_TYPE = { type: 'text', icon: 'text', lang: null, binary: false };
|
|
133
|
+
|
|
124
134
|
/**
|
|
125
|
-
* Get file type information
|
|
135
|
+
* Get file type information from filename or path
|
|
126
136
|
* @param {string} filename - File name or path
|
|
127
|
-
* @returns {
|
|
137
|
+
* @returns {{ type: string, icon: string, lang: string|null, binary: boolean }}
|
|
128
138
|
*/
|
|
129
139
|
export function getFileType(filename) {
|
|
130
|
-
const basename = filename.split('/').pop();
|
|
140
|
+
const basename = filename.split('/').pop() || '';
|
|
131
141
|
|
|
132
142
|
// Check special filenames first
|
|
133
143
|
if (SPECIAL_FILES[basename]) {
|
|
134
144
|
return SPECIAL_FILES[basename];
|
|
135
145
|
}
|
|
136
146
|
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return FILE_TYPES[ext];
|
|
147
|
+
// Extract extension (last part after dot)
|
|
148
|
+
const lastDotIndex = basename.lastIndexOf('.');
|
|
149
|
+
if (lastDotIndex === -1 || lastDotIndex === basename.length - 1) {
|
|
150
|
+
return DEFAULT_FILE_TYPE;
|
|
142
151
|
}
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
return
|
|
153
|
+
const ext = basename.slice(lastDotIndex + 1).toLowerCase();
|
|
154
|
+
return FILE_TYPES[ext] || DEFAULT_FILE_TYPE;
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
export
|
|
157
|
+
export { FILE_TYPES };
|
package/src/utils/path.js
CHANGED
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Validate that a path is within the allowed root directory
|
|
9
|
-
* Prevents path traversal attacks
|
|
10
|
-
* @param {string} targetPath -
|
|
8
|
+
* Validate that a path is within the allowed root directory.
|
|
9
|
+
* Prevents path traversal attacks, null byte injection, and absolute path access.
|
|
10
|
+
* @param {string} targetPath - Relative path to validate
|
|
11
11
|
* @param {string} rootDir - Allowed root directory
|
|
12
|
-
* @returns {boolean} True if path is
|
|
12
|
+
* @returns {boolean} True if path is safe and within rootDir
|
|
13
13
|
*/
|
|
14
14
|
export function validatePath(targetPath, rootDir) {
|
|
15
|
-
// Reject null bytes (
|
|
15
|
+
// Reject null bytes (injection attack vector)
|
|
16
16
|
if (targetPath.includes('\0') || targetPath.includes('%00')) {
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// Reject absolute paths
|
|
20
|
+
// Reject absolute paths
|
|
21
21
|
if (path.isAbsolute(targetPath)) {
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
@@ -27,22 +27,19 @@ export function validatePath(targetPath, rootDir) {
|
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
const resolved = path.resolve(
|
|
30
|
+
// Verify resolved path stays within root directory
|
|
31
|
+
const resolved = path.resolve(rootDir, targetPath);
|
|
32
32
|
const resolvedRoot = path.resolve(rootDir);
|
|
33
33
|
|
|
34
|
-
// Check if the resolved path starts with the root directory
|
|
35
34
|
return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
/**
|
|
39
|
-
*
|
|
40
|
-
* @param {string} fullPath -
|
|
38
|
+
* Convert absolute path to relative path with forward slashes
|
|
39
|
+
* @param {string} fullPath - Absolute file path
|
|
41
40
|
* @param {string} rootDir - Root directory
|
|
42
|
-
* @returns {string} Relative path
|
|
41
|
+
* @returns {string} Relative path using forward slashes
|
|
43
42
|
*/
|
|
44
43
|
export function getRelativePath(fullPath, rootDir) {
|
|
45
44
|
return path.relative(rootDir, fullPath).split(path.sep).join('/');
|
|
46
45
|
}
|
|
47
|
-
|
|
48
|
-
export default { validatePath, getRelativePath };
|
package/src/watcher.js
CHANGED
|
@@ -6,6 +6,30 @@ import chokidar from 'chokidar';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { renderFile } from './rendering/index.js';
|
|
8
8
|
|
|
9
|
+
const IGNORED_PATTERNS = [
|
|
10
|
+
/(^|[\/\\])\../, // Dotfiles
|
|
11
|
+
/node_modules/,
|
|
12
|
+
/\.git/,
|
|
13
|
+
/__pycache__/,
|
|
14
|
+
/\.pyc$/,
|
|
15
|
+
/\.cache/,
|
|
16
|
+
/\.pytest_cache/,
|
|
17
|
+
/\.mypy_cache/,
|
|
18
|
+
/\.ruff_cache/,
|
|
19
|
+
/venv/,
|
|
20
|
+
/\.venv/,
|
|
21
|
+
/dist/,
|
|
22
|
+
/build/,
|
|
23
|
+
/\.next/,
|
|
24
|
+
/\.nuxt/,
|
|
25
|
+
/coverage/,
|
|
26
|
+
/\.DS_Store/,
|
|
27
|
+
/Thumbs\.db/,
|
|
28
|
+
/desktop\.ini/,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const TREE_CHANGE_EVENTS = ['add', 'unlink', 'addDir', 'unlinkDir'];
|
|
32
|
+
|
|
9
33
|
/**
|
|
10
34
|
* Setup file watcher
|
|
11
35
|
* @param {string} rootDir - Root directory to watch
|
|
@@ -14,32 +38,7 @@ import { renderFile } from './rendering/index.js';
|
|
|
14
38
|
*/
|
|
15
39
|
export function setupWatcher(rootDir, wss) {
|
|
16
40
|
const watcher = chokidar.watch(rootDir, {
|
|
17
|
-
ignored:
|
|
18
|
-
/(^|[\/\\])\../, // Ignore dotfiles
|
|
19
|
-
/node_modules/,
|
|
20
|
-
/\.git/,
|
|
21
|
-
/__pycache__/,
|
|
22
|
-
/\.pyc$/,
|
|
23
|
-
// Python cache/build
|
|
24
|
-
/\.cache/,
|
|
25
|
-
/\.pytest_cache/,
|
|
26
|
-
/\.mypy_cache/,
|
|
27
|
-
/\.ruff_cache/,
|
|
28
|
-
/venv/,
|
|
29
|
-
/\.venv/,
|
|
30
|
-
// Build outputs
|
|
31
|
-
/dist/,
|
|
32
|
-
/build/,
|
|
33
|
-
// Framework specific
|
|
34
|
-
/\.next/,
|
|
35
|
-
/\.nuxt/,
|
|
36
|
-
// Test coverage
|
|
37
|
-
/coverage/,
|
|
38
|
-
// OS generated files
|
|
39
|
-
/\.DS_Store/,
|
|
40
|
-
/Thumbs\.db/,
|
|
41
|
-
/desktop\.ini/,
|
|
42
|
-
],
|
|
41
|
+
ignored: IGNORED_PATTERNS,
|
|
43
42
|
persistent: true,
|
|
44
43
|
ignoreInitial: true,
|
|
45
44
|
awaitWriteFinish: {
|
|
@@ -48,20 +47,22 @@ export function setupWatcher(rootDir, wss) {
|
|
|
48
47
|
}
|
|
49
48
|
});
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
const getRelativePath = (filePath) => {
|
|
50
|
+
function toRelativePath(filePath) {
|
|
53
51
|
return path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
54
|
-
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function broadcastTreeUpdate() {
|
|
55
|
+
wss.broadcast({
|
|
56
|
+
type: 'tree_update',
|
|
57
|
+
tree: null
|
|
58
|
+
});
|
|
59
|
+
}
|
|
55
60
|
|
|
56
|
-
// File change handler
|
|
57
61
|
watcher.on('change', async (filePath) => {
|
|
58
|
-
const relativePath =
|
|
62
|
+
const relativePath = toRelativePath(filePath);
|
|
59
63
|
|
|
60
64
|
try {
|
|
61
|
-
// Render the file content
|
|
62
65
|
const rendered = await renderFile(filePath);
|
|
63
|
-
|
|
64
|
-
// Broadcast to clients watching this file
|
|
65
66
|
wss.broadcastFileUpdate(relativePath, {
|
|
66
67
|
type: 'file_update',
|
|
67
68
|
path: relativePath,
|
|
@@ -72,20 +73,9 @@ export function setupWatcher(rootDir, wss) {
|
|
|
72
73
|
}
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// For now, just notify clients to refresh
|
|
79
|
-
wss.broadcast({
|
|
80
|
-
type: 'tree_update',
|
|
81
|
-
tree: null // Client will fetch via API
|
|
82
|
-
});
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
watcher.on('add', broadcastTreeUpdate);
|
|
86
|
-
watcher.on('unlink', broadcastTreeUpdate);
|
|
87
|
-
watcher.on('addDir', broadcastTreeUpdate);
|
|
88
|
-
watcher.on('unlinkDir', broadcastTreeUpdate);
|
|
76
|
+
for (const event of TREE_CHANGE_EVENTS) {
|
|
77
|
+
watcher.on(event, broadcastTreeUpdate);
|
|
78
|
+
}
|
|
89
79
|
|
|
90
80
|
watcher.on('error', (err) => {
|
|
91
81
|
console.error('Watcher error:', err);
|
package/src/websocket.js
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
* WebSocket management for MDV
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { WebSocketServer } from 'ws';
|
|
5
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if a WebSocket client is ready to receive messages
|
|
9
|
+
* @param {WebSocket} client - WebSocket client
|
|
10
|
+
* @returns {boolean} True if client is open and ready
|
|
11
|
+
*/
|
|
12
|
+
function isClientReady(client) {
|
|
13
|
+
return client.readyState === WebSocket.OPEN;
|
|
14
|
+
}
|
|
6
15
|
|
|
7
16
|
/**
|
|
8
17
|
* Setup WebSocket server
|
|
@@ -11,8 +20,6 @@ import { WebSocketServer } from 'ws';
|
|
|
11
20
|
*/
|
|
12
21
|
export function setupWebSocket(server) {
|
|
13
22
|
const wss = new WebSocketServer({ server });
|
|
14
|
-
|
|
15
|
-
// Track watched files per client
|
|
16
23
|
const clientWatches = new Map();
|
|
17
24
|
|
|
18
25
|
wss.on('connection', (ws) => {
|
|
@@ -23,7 +30,6 @@ export function setupWebSocket(server) {
|
|
|
23
30
|
const message = JSON.parse(data.toString());
|
|
24
31
|
|
|
25
32
|
if (message.type === 'watch') {
|
|
26
|
-
// Client wants to watch a file
|
|
27
33
|
const watches = clientWatches.get(ws);
|
|
28
34
|
watches.clear();
|
|
29
35
|
watches.add(message.path);
|
|
@@ -43,30 +49,28 @@ export function setupWebSocket(server) {
|
|
|
43
49
|
});
|
|
44
50
|
});
|
|
45
51
|
|
|
46
|
-
// Add broadcast helper
|
|
47
52
|
wss.broadcast = (data) => {
|
|
48
53
|
const message = JSON.stringify(data);
|
|
49
54
|
wss.clients.forEach((client) => {
|
|
50
|
-
if (client
|
|
55
|
+
if (isClientReady(client)) {
|
|
51
56
|
client.send(message);
|
|
52
57
|
}
|
|
53
58
|
});
|
|
54
59
|
};
|
|
55
60
|
|
|
56
|
-
// Add targeted broadcast for file updates
|
|
57
61
|
wss.broadcastFileUpdate = (filePath, data) => {
|
|
58
62
|
const message = JSON.stringify(data);
|
|
59
63
|
wss.clients.forEach((client) => {
|
|
60
|
-
if (client
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
if (!isClientReady(client)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const watches = clientWatches.get(client);
|
|
68
|
+
if (watches && watches.has(filePath)) {
|
|
69
|
+
client.send(message);
|
|
65
70
|
}
|
|
66
71
|
});
|
|
67
72
|
};
|
|
68
73
|
|
|
69
|
-
// Store clientWatches for external access
|
|
70
74
|
wss.clientWatches = clientWatches;
|
|
71
75
|
|
|
72
76
|
return wss;
|
package/README.pdf
DELETED
|
Binary file
|