mdv-live 0.3.0

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.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * File type detection and classification
3
+ */
4
+
5
+ // File extension to type mapping
6
+ const FILE_TYPES = {
7
+ // Markdown
8
+ md: { type: 'markdown', icon: 'markdown', lang: null, binary: false },
9
+ markdown: { type: 'markdown', icon: 'markdown', lang: null, binary: false },
10
+
11
+ // Code - Python
12
+ py: { type: 'code', icon: 'python', lang: 'python', binary: false },
13
+ pyw: { type: 'code', icon: 'python', lang: 'python', binary: false },
14
+
15
+ // Code - JavaScript/TypeScript
16
+ js: { type: 'code', icon: 'javascript', lang: 'javascript', binary: false },
17
+ mjs: { type: 'code', icon: 'javascript', lang: 'javascript', binary: false },
18
+ cjs: { type: 'code', icon: 'javascript', lang: 'javascript', binary: false },
19
+ ts: { type: 'code', icon: 'typescript', lang: 'typescript', binary: false },
20
+ tsx: { type: 'code', icon: 'react', lang: 'tsx', binary: false },
21
+ jsx: { type: 'code', icon: 'react', lang: 'jsx', binary: false },
22
+
23
+ // Code - Web
24
+ html: { type: 'code', icon: 'html', lang: 'html', binary: false },
25
+ htm: { type: 'code', icon: 'html', lang: 'html', binary: false },
26
+ css: { type: 'code', icon: 'css', lang: 'css', binary: false },
27
+ scss: { type: 'code', icon: 'css', lang: 'scss', binary: false },
28
+ less: { type: 'code', icon: 'css', lang: 'less', binary: false },
29
+ vue: { type: 'code', icon: 'vue', lang: 'vue', binary: false },
30
+ svelte: { type: 'code', icon: 'default', lang: 'svelte', binary: false },
31
+
32
+ // Data formats
33
+ json: { type: 'code', icon: 'json', lang: 'json', binary: false },
34
+ yaml: { type: 'code', icon: 'yaml', lang: 'yaml', binary: false },
35
+ yml: { type: 'code', icon: 'yaml', lang: 'yaml', binary: false },
36
+ toml: { type: 'code', icon: 'config', lang: 'toml', binary: false },
37
+ xml: { type: 'code', icon: 'default', lang: 'xml', binary: false },
38
+
39
+ // Shell/Config
40
+ sh: { type: 'code', icon: 'shell', lang: 'bash', binary: false },
41
+ bash: { type: 'code', icon: 'shell', lang: 'bash', binary: false },
42
+ zsh: { type: 'code', icon: 'shell', lang: 'bash', binary: false },
43
+ fish: { type: 'code', icon: 'shell', lang: 'bash', binary: false },
44
+ env: { type: 'code', icon: 'config', lang: 'bash', binary: false },
45
+ ini: { type: 'code', icon: 'config', lang: 'ini', binary: false },
46
+ conf: { type: 'code', icon: 'config', lang: 'ini', binary: false },
47
+
48
+ // Other languages
49
+ go: { type: 'code', icon: 'default', lang: 'go', binary: false },
50
+ rs: { type: 'code', icon: 'default', lang: 'rust', binary: false },
51
+ rb: { type: 'code', icon: 'default', lang: 'ruby', binary: false },
52
+ php: { type: 'code', icon: 'default', lang: 'php', binary: false },
53
+ java: { type: 'code', icon: 'default', lang: 'java', binary: false },
54
+ kt: { type: 'code', icon: 'default', lang: 'kotlin', binary: false },
55
+ swift: { type: 'code', icon: 'default', lang: 'swift', binary: false },
56
+ c: { type: 'code', icon: 'default', lang: 'c', binary: false },
57
+ cpp: { type: 'code', icon: 'default', lang: 'cpp', binary: false },
58
+ h: { type: 'code', icon: 'default', lang: 'c', binary: false },
59
+ cs: { type: 'code', icon: 'default', lang: 'csharp', binary: false },
60
+ sql: { type: 'code', icon: 'database', lang: 'sql', binary: false },
61
+
62
+ // Text files
63
+ txt: { type: 'text', icon: 'text', lang: null, binary: false },
64
+ log: { type: 'text', icon: 'text', lang: null, binary: false },
65
+ csv: { type: 'text', icon: 'text', lang: null, binary: false },
66
+
67
+ // Images
68
+ png: { type: 'image', icon: 'image', lang: null, binary: true },
69
+ jpg: { type: 'image', icon: 'image', lang: null, binary: true },
70
+ jpeg: { type: 'image', icon: 'image', lang: null, binary: true },
71
+ gif: { type: 'image', icon: 'image', lang: null, binary: true },
72
+ svg: { type: 'image', icon: 'image', lang: null, binary: true },
73
+ webp: { type: 'image', icon: 'image', lang: null, binary: true },
74
+ ico: { type: 'image', icon: 'image', lang: null, binary: true },
75
+
76
+ // PDF
77
+ pdf: { type: 'pdf', icon: 'pdf', lang: null, binary: true },
78
+
79
+ // Video
80
+ mp4: { type: 'video', icon: 'video', lang: null, binary: true },
81
+ webm: { type: 'video', icon: 'video', lang: null, binary: true },
82
+ mov: { type: 'video', icon: 'video', lang: null, binary: true },
83
+ avi: { type: 'video', icon: 'video', lang: null, binary: true },
84
+ mkv: { type: 'video', icon: 'video', lang: null, binary: true },
85
+
86
+ // Audio
87
+ mp3: { type: 'audio', icon: 'audio', lang: null, binary: true },
88
+ wav: { type: 'audio', icon: 'audio', lang: null, binary: true },
89
+ ogg: { type: 'audio', icon: 'audio', lang: null, binary: true },
90
+ m4a: { type: 'audio', icon: 'audio', lang: null, binary: true },
91
+ flac: { type: 'audio', icon: 'audio', lang: null, binary: true },
92
+
93
+ // Archives
94
+ zip: { type: 'archive', icon: 'archive', lang: null, binary: true },
95
+ tar: { type: 'archive', icon: 'archive', lang: null, binary: true },
96
+ gz: { type: 'archive', icon: 'archive', lang: null, binary: true },
97
+ rar: { type: 'archive', icon: 'archive', lang: null, binary: true },
98
+ '7z': { type: 'archive', icon: 'archive', lang: null, binary: true },
99
+
100
+ // Office
101
+ doc: { type: 'office', icon: 'office', lang: null, binary: true },
102
+ docx: { type: 'office', icon: 'office', lang: null, binary: true },
103
+ xls: { type: 'office', icon: 'office', lang: null, binary: true },
104
+ xlsx: { type: 'office', icon: 'office', lang: null, binary: true },
105
+ ppt: { type: 'office', icon: 'office', lang: null, binary: true },
106
+ pptx: { type: 'office', icon: 'office', lang: null, binary: true },
107
+
108
+ // Executables
109
+ exe: { type: 'executable', icon: 'executable', lang: null, binary: true },
110
+ dmg: { type: 'executable', icon: 'executable', lang: null, binary: true },
111
+ app: { type: 'executable', icon: 'executable', lang: null, binary: true },
112
+ };
113
+
114
+ // Special filenames
115
+ const SPECIAL_FILES = {
116
+ 'Dockerfile': { type: 'code', icon: 'config', lang: 'dockerfile', binary: false },
117
+ 'Makefile': { type: 'code', icon: 'config', lang: 'makefile', binary: false },
118
+ '.gitignore': { type: 'code', icon: 'config', lang: 'gitignore', binary: false },
119
+ '.env': { type: 'code', icon: 'config', lang: 'bash', binary: false },
120
+ '.env.local': { type: 'code', icon: 'config', lang: 'bash', binary: false },
121
+ '.env.example': { type: 'code', icon: 'config', lang: 'bash', binary: false },
122
+ };
123
+
124
+ /**
125
+ * Get file type information
126
+ * @param {string} filename - File name or path
127
+ * @returns {Object} File type info { type, icon, lang, binary }
128
+ */
129
+ export function getFileType(filename) {
130
+ const basename = filename.split('/').pop();
131
+
132
+ // Check special filenames first
133
+ if (SPECIAL_FILES[basename]) {
134
+ return SPECIAL_FILES[basename];
135
+ }
136
+
137
+ // Get extension
138
+ const ext = basename.split('.').pop()?.toLowerCase();
139
+
140
+ if (ext && FILE_TYPES[ext]) {
141
+ return FILE_TYPES[ext];
142
+ }
143
+
144
+ // Default to text file
145
+ return { type: 'text', icon: 'text', lang: null, binary: false };
146
+ }
147
+
148
+ export default { getFileType, FILE_TYPES };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Path security utilities
3
+ */
4
+
5
+ import path from 'path';
6
+
7
+ /**
8
+ * Validate that a path is within the allowed root directory
9
+ * Prevents path traversal attacks
10
+ * @param {string} targetPath - Path to validate (must be relative)
11
+ * @param {string} rootDir - Allowed root directory
12
+ * @returns {boolean} True if path is valid
13
+ */
14
+ export function validatePath(targetPath, rootDir) {
15
+ // Reject null bytes (null byte injection attack)
16
+ if (targetPath.includes('\0') || targetPath.includes('%00')) {
17
+ return false;
18
+ }
19
+
20
+ // Reject absolute paths (e.g., /etc/passwd)
21
+ if (path.isAbsolute(targetPath)) {
22
+ return false;
23
+ }
24
+
25
+ // Reject path traversal attempts
26
+ if (targetPath.includes('..')) {
27
+ return false;
28
+ }
29
+
30
+ const fullPath = path.join(rootDir, targetPath);
31
+ const resolved = path.resolve(fullPath);
32
+ const resolvedRoot = path.resolve(rootDir);
33
+
34
+ // Check if the resolved path starts with the root directory
35
+ return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep);
36
+ }
37
+
38
+ /**
39
+ * Get relative path from root
40
+ * @param {string} fullPath - Full path
41
+ * @param {string} rootDir - Root directory
42
+ * @returns {string} Relative path with forward slashes
43
+ */
44
+ export function getRelativePath(fullPath, rootDir) {
45
+ return path.relative(rootDir, fullPath).split(path.sep).join('/');
46
+ }
47
+
48
+ export default { validatePath, getRelativePath };
package/src/watcher.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * File watcher for MDV using chokidar
3
+ */
4
+
5
+ import chokidar from 'chokidar';
6
+ import path from 'path';
7
+ import { renderFile } from './rendering/index.js';
8
+
9
+ /**
10
+ * Setup file watcher
11
+ * @param {string} rootDir - Root directory to watch
12
+ * @param {WebSocketServer} wss - WebSocket server for broadcasting
13
+ * @returns {FSWatcher} Chokidar watcher instance
14
+ */
15
+ export function setupWatcher(rootDir, wss) {
16
+ 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
+ ],
43
+ persistent: true,
44
+ ignoreInitial: true,
45
+ awaitWriteFinish: {
46
+ stabilityThreshold: 100,
47
+ pollInterval: 50
48
+ }
49
+ });
50
+
51
+ // Helper to get relative path
52
+ const getRelativePath = (filePath) => {
53
+ return path.relative(rootDir, filePath).split(path.sep).join('/');
54
+ };
55
+
56
+ // File change handler
57
+ watcher.on('change', async (filePath) => {
58
+ const relativePath = getRelativePath(filePath);
59
+
60
+ try {
61
+ // Render the file content
62
+ const rendered = await renderFile(filePath);
63
+
64
+ // Broadcast to clients watching this file
65
+ wss.broadcastFileUpdate(relativePath, {
66
+ type: 'file_update',
67
+ path: relativePath,
68
+ ...rendered
69
+ });
70
+ } catch (err) {
71
+ console.error(`Error rendering ${relativePath}:`, err);
72
+ }
73
+ });
74
+
75
+ // Tree change handlers
76
+ const broadcastTreeUpdate = async () => {
77
+ // We'll implement getFileTree in api/tree.js
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);
89
+
90
+ watcher.on('error', (err) => {
91
+ console.error('Watcher error:', err);
92
+ });
93
+
94
+ return watcher;
95
+ }
96
+
97
+ export default setupWatcher;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * WebSocket management for MDV
3
+ */
4
+
5
+ import { WebSocketServer } from 'ws';
6
+
7
+ /**
8
+ * Setup WebSocket server
9
+ * @param {http.Server} server - HTTP server instance
10
+ * @returns {WebSocketServer} WebSocket server instance
11
+ */
12
+ export function setupWebSocket(server) {
13
+ const wss = new WebSocketServer({ server });
14
+
15
+ // Track watched files per client
16
+ const clientWatches = new Map();
17
+
18
+ wss.on('connection', (ws) => {
19
+ clientWatches.set(ws, new Set());
20
+
21
+ ws.on('message', (data) => {
22
+ try {
23
+ const message = JSON.parse(data.toString());
24
+
25
+ if (message.type === 'watch') {
26
+ // Client wants to watch a file
27
+ const watches = clientWatches.get(ws);
28
+ watches.clear();
29
+ watches.add(message.path);
30
+ }
31
+ } catch (err) {
32
+ console.error('WebSocket message error:', err);
33
+ }
34
+ });
35
+
36
+ ws.on('close', () => {
37
+ clientWatches.delete(ws);
38
+ });
39
+
40
+ ws.on('error', (err) => {
41
+ console.error('WebSocket error:', err);
42
+ clientWatches.delete(ws);
43
+ });
44
+ });
45
+
46
+ // Add broadcast helper
47
+ wss.broadcast = (data) => {
48
+ const message = JSON.stringify(data);
49
+ wss.clients.forEach((client) => {
50
+ if (client.readyState === 1) { // WebSocket.OPEN
51
+ client.send(message);
52
+ }
53
+ });
54
+ };
55
+
56
+ // Add targeted broadcast for file updates
57
+ wss.broadcastFileUpdate = (filePath, data) => {
58
+ const message = JSON.stringify(data);
59
+ wss.clients.forEach((client) => {
60
+ if (client.readyState === 1) {
61
+ const watches = clientWatches.get(client);
62
+ if (watches && watches.has(filePath)) {
63
+ client.send(message);
64
+ }
65
+ }
66
+ });
67
+ };
68
+
69
+ // Store clientWatches for external access
70
+ wss.clientWatches = clientWatches;
71
+
72
+ return wss;
73
+ }
74
+
75
+ export default setupWebSocket;