mdts 0.1.7 → 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.
- package/README.md +35 -5
- package/dist/cli.js +6 -2
- package/dist/frontend/bundle.js +4842 -281
- package/dist/server/routes/filetree.js +35 -0
- package/dist/server/routes/outline.js +54 -0
- package/dist/server/server.js +74 -0
- package/package.json +10 -3
- package/public/welcome.md +31 -0
- package/dist/server.js +0 -30
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const isDotFileOrDirectory = (entryName) => {
|
|
5
|
+
return entryName.startsWith('.');
|
|
6
|
+
};
|
|
7
|
+
const isLibraryDirectory = (entryName) => {
|
|
8
|
+
const libraryDirs = ['node_modules', 'vendor', 'bundle', 'venv', 'env', 'site-packages'];
|
|
9
|
+
return libraryDirs.includes(entryName);
|
|
10
|
+
};
|
|
11
|
+
const getFileTree = (baseDirectory, currentRelativePath) => {
|
|
12
|
+
const entries = fs.readdirSync(path.join(baseDirectory, currentRelativePath), { withFileTypes: true })
|
|
13
|
+
.filter(entry => !isDotFileOrDirectory(entry.name) && !isLibraryDirectory(entry.name));
|
|
14
|
+
const tree = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const entryPath = path.join(currentRelativePath, entry.name);
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
const subTree = getFileTree(baseDirectory, entryPath);
|
|
19
|
+
if (subTree.length > 0) { // Only include directory if it contains markdown files
|
|
20
|
+
tree.push({ [entry.name]: subTree });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else if (entry.name.endsWith('.md') || entry.name.endsWith('.markdown')) {
|
|
24
|
+
tree.push(entryPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return tree;
|
|
28
|
+
};
|
|
29
|
+
export const fileTreeRouter = (directory) => {
|
|
30
|
+
const router = Router();
|
|
31
|
+
router.get('/', (req, res) => {
|
|
32
|
+
res.json(getFileTree(directory, ''));
|
|
33
|
+
});
|
|
34
|
+
return router;
|
|
35
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import MarkdownIt from 'markdown-it';
|
|
4
|
+
import path, { dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const md = new MarkdownIt();
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const slugify = (text) => {
|
|
10
|
+
return text
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, '') // Remove non-alphanumeric, non-space, non-hyphen characters. \p{L} for unicode letters, \p{N} for unicode numbers. 'u' flag for unicode.
|
|
13
|
+
.replace(/\s+/g, '-') // Replace spaces with single hyphen
|
|
14
|
+
.replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
|
|
15
|
+
.replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
|
|
16
|
+
};
|
|
17
|
+
const getMarkdownOutline = (filePath) => {
|
|
18
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
19
|
+
const tokens = md.parse(fileContent, {});
|
|
20
|
+
const outline = [];
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
if (token.type === 'heading_open') {
|
|
23
|
+
const level = parseInt(token.tag.substring(1));
|
|
24
|
+
const nextToken = tokens[tokens.indexOf(token) + 1];
|
|
25
|
+
if (nextToken && nextToken.type === 'inline') {
|
|
26
|
+
const content = nextToken.content;
|
|
27
|
+
const id = slugify(content);
|
|
28
|
+
outline.push({ level, content, id });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return outline;
|
|
33
|
+
};
|
|
34
|
+
export const outlineRouter = (directory) => {
|
|
35
|
+
const router = Router();
|
|
36
|
+
router.get('/', (req, res) => {
|
|
37
|
+
const filePath = req.query.filePath;
|
|
38
|
+
if (!filePath) {
|
|
39
|
+
return res.status(400).send('filePath query parameter is required.');
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const absolutePath = filePath === 'mdts-welcome-markdown.md'
|
|
43
|
+
? path.join(__dirname, '../../../public/welcome.md')
|
|
44
|
+
: path.join(directory, filePath);
|
|
45
|
+
const outline = getMarkdownOutline(absolutePath);
|
|
46
|
+
res.json(outline);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error(`Error getting outline for ${filePath}:`, error);
|
|
50
|
+
res.status(500).send('Error getting outline.');
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return router;
|
|
54
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import chokidar from 'chokidar';
|
|
11
|
+
import express from 'express';
|
|
12
|
+
import { promises as fs } from 'fs';
|
|
13
|
+
import path, { dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { WebSocketServer } from 'ws';
|
|
16
|
+
import { fileTreeRouter } from './routes/filetree.js';
|
|
17
|
+
import { outlineRouter } from './routes/outline.js';
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
export const serve = (directory, port) => {
|
|
21
|
+
const app = express();
|
|
22
|
+
// Mount library static files
|
|
23
|
+
app.use(express.static(path.join(__dirname, '../../public')));
|
|
24
|
+
app.use(express.static(path.join(__dirname, '../../dist/frontend')));
|
|
25
|
+
// Define API
|
|
26
|
+
app.use('/api/filetree', fileTreeRouter(directory));
|
|
27
|
+
app.get('/api/markdown/mdts-welcome-markdown.md', (req, res) => {
|
|
28
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
29
|
+
res.sendFile(path.join(__dirname, '../../public/welcome.md'));
|
|
30
|
+
});
|
|
31
|
+
app.use('/api/markdown', express.static(directory));
|
|
32
|
+
app.use('/api/outline', outlineRouter(directory));
|
|
33
|
+
// Catch-all route to serve index.html for any other requests
|
|
34
|
+
app.get('*', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
35
|
+
const filePath = path.join(directory, req.path);
|
|
36
|
+
let isDirectory = false;
|
|
37
|
+
try {
|
|
38
|
+
const stats = yield fs.stat(filePath);
|
|
39
|
+
isDirectory = stats.isDirectory();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// File or directory does not exist, proceed as if it's a file
|
|
43
|
+
}
|
|
44
|
+
if (isDirectory || req.path.toLowerCase().endsWith('.md') || req.path.toLowerCase().endsWith('.markdown')) {
|
|
45
|
+
return res.sendFile(path.join(__dirname, '../../public/index.html'));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
return res.sendFile(req.path, { root: directory });
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
const server = app.listen(port, () => {
|
|
52
|
+
console.log(`🚀 Server listening at http://localhost:${port}`);
|
|
53
|
+
});
|
|
54
|
+
const wss = new WebSocketServer({ server });
|
|
55
|
+
const watcher = chokidar.watch(directory, { ignored: /node_modules/, ignoreInitial: true });
|
|
56
|
+
watcher.on('change', (filePath) => {
|
|
57
|
+
console.log(`🔃 File changed: ${filePath}, reloading...`);
|
|
58
|
+
wss.clients.forEach((client) => {
|
|
59
|
+
client.send(JSON.stringify({ type: 'reload-content' }));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
watcher.on('add', (filePath) => {
|
|
63
|
+
console.log(`🌲 File added: ${filePath}, reloading tree...`);
|
|
64
|
+
wss.clients.forEach((client) => {
|
|
65
|
+
client.send(JSON.stringify({ type: 'reload-tree' }));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
watcher.on('unlink', (filePath) => {
|
|
69
|
+
console.log('🌲 File removed: ${filePath}, reloading tree...');
|
|
70
|
+
wss.clients.forEach((client) => {
|
|
71
|
+
client.send(JSON.stringify({ type: 'reload-tree' }));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A markdown preview server.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,24 +48,31 @@
|
|
|
48
48
|
"@types/markdown-it": "^14.1.2",
|
|
49
49
|
"@types/react": "^19.1.8",
|
|
50
50
|
"@types/react-dom": "^19.1.6",
|
|
51
|
+
"@types/ws": "^8.18.1",
|
|
51
52
|
"babel-loader": "^10.0.0",
|
|
53
|
+
"chokidar": "^4.0.3",
|
|
52
54
|
"css-loader": "^7.1.2",
|
|
53
55
|
"html-webpack-plugin": "^5.6.3",
|
|
54
56
|
"style-loader": "^4.0.0",
|
|
55
57
|
"ts-node": "^10.9.2",
|
|
56
58
|
"typescript": "^5.8.3",
|
|
57
59
|
"webpack": "^5.100.1",
|
|
58
|
-
"webpack-cli": "^6.0.1"
|
|
60
|
+
"webpack-cli": "^6.0.1",
|
|
61
|
+
"ws": "^8.18.3"
|
|
59
62
|
},
|
|
60
63
|
"dependencies": {
|
|
61
64
|
"@mui/icons-material": "^7.2.0",
|
|
62
65
|
"@mui/x-tree-view": "^8.8.0",
|
|
63
66
|
"commander": "^14.0.0",
|
|
64
67
|
"express": "^4.19.2",
|
|
68
|
+
"markdown-it": "^14.1.0",
|
|
65
69
|
"open": "^10.1.2",
|
|
66
70
|
"react": "^19.1.0",
|
|
67
71
|
"react-dom": "^19.1.0",
|
|
68
72
|
"react-markdown": "^10.1.0",
|
|
69
|
-
"remark-gfm": "^4.0.1"
|
|
73
|
+
"remark-gfm": "^4.0.1",
|
|
74
|
+
"remark-slug": "^7.0.1",
|
|
75
|
+
"rehype-raw": "^7.0.0",
|
|
76
|
+
"react-syntax-highlighter": "^15.6.1"
|
|
70
77
|
}
|
|
71
78
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img src="logo.svg" alt="mdts" width="400">
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
mdts (Markdown Tree Server) is a simple and efficient tool for serving and viewing markdown files with an interactive file tree and outline. It's designed to help you navigate and read your markdown-based documentation or notes with ease.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* **Markdown Rendering:** Renders markdown files into clean, readable HTML.
|
|
10
|
+
* **File Tree Navigation:** Browse your markdown files and directories through an intuitive file tree.
|
|
11
|
+
* **Outline View:** Quickly jump to sections within a markdown file using the generated outline.
|
|
12
|
+
* **Live Reload:** Automatically reloads content when markdown files changes.
|
|
13
|
+
* **Dark Mode:** Supports dark mode for comfortable reading.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
To start the mdts server, run:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
mdts <path_to_your_markdown_directory>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then, open your browser and navigate to `http://localhost:<port>` (default port is 8521).
|
|
24
|
+
|
|
25
|
+
## About This Page
|
|
26
|
+
|
|
27
|
+
This is a dynamically generated welcome page. You can replace it with your own `README.md` or any other markdown file by navigating to its path in the URL.
|
|
28
|
+
|
|
29
|
+
## Links
|
|
30
|
+
|
|
31
|
+
* [GitHub Repository](https://github.com/unhappychoice/mdts)
|
package/dist/server.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { dirname } from 'path';
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
8
|
-
export const serve = (directory, port) => {
|
|
9
|
-
const app = express();
|
|
10
|
-
app.use(express.static(path.join(__dirname, '../public')));
|
|
11
|
-
app.use(express.static(path.join(__dirname, '../dist/frontend')));
|
|
12
|
-
app.get('/filetree', (req, res) => {
|
|
13
|
-
res.json(getFileTree(directory, '')); // Pass empty string as initial relative path
|
|
14
|
-
});
|
|
15
|
-
app.use('/content', express.static(directory));
|
|
16
|
-
// Catch-all route to serve index.html for any other requests
|
|
17
|
-
app.get('*', (req, res) => {
|
|
18
|
-
res.sendFile(path.join(__dirname, '../public/index.html'));
|
|
19
|
-
});
|
|
20
|
-
app.listen(port, () => {
|
|
21
|
-
console.log(`Server listening at http://localhost:${port}`);
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
const getFileTree = (baseDirectory, currentRelativePath) => fs.readdirSync(path.join(baseDirectory, currentRelativePath), { withFileTypes: true })
|
|
25
|
-
.map((entry) => {
|
|
26
|
-
const entryPath = path.join(currentRelativePath, entry.name);
|
|
27
|
-
return entry.isDirectory()
|
|
28
|
-
? { [entry.name]: getFileTree(baseDirectory, entryPath) }
|
|
29
|
-
: entryPath; // Return full relative path for files
|
|
30
|
-
});
|