md-lv 1.0.2 → 1.1.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/LICENSE +1 -1
- package/README.md +3 -3
- package/package.json +4 -4
- package/public/js/app.js +1 -1
- package/public/js/navigation.js +1 -1
- package/public/js/search.js +1 -1
- package/public/styles/base.css +1 -1
- package/public/styles/modern.css +1 -1
- package/src/routes/api.js +0 -3
- package/src/routes/assets.js +65 -0
- package/src/routes/directory.js +3 -5
- package/src/routes/markdown.js +2 -1
- package/src/routes/raw.js +2 -1
- package/src/server.js +5 -0
- package/templates/page.html +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# md-lv
|
|
2
2
|
|
|
3
3
|
> Serve Markdown files as HTML with live features
|
|
4
4
|
|
|
@@ -123,8 +123,8 @@ $$
|
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
125
|
# Clone repository
|
|
126
|
-
git clone https://github.com/
|
|
127
|
-
cd markdown-viewer
|
|
126
|
+
git clone https://github.com/no-problem-dev/markdown-live-viewer.git
|
|
127
|
+
cd markdown-live-viewer
|
|
128
128
|
|
|
129
129
|
# Install dependencies
|
|
130
130
|
npm install
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-lv",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Serve Markdown files as HTML with live features - syntax highlighting, Mermaid diagrams, and MathJax formulas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"license": "MIT",
|
|
46
46
|
"repository": {
|
|
47
47
|
"type": "git",
|
|
48
|
-
"url": "git+https://github.com/no-problem-dev/markdown-viewer.git"
|
|
48
|
+
"url": "git+https://github.com/no-problem-dev/markdown-live-viewer.git"
|
|
49
49
|
},
|
|
50
50
|
"bugs": {
|
|
51
|
-
"url": "https://github.com/no-problem-dev/markdown-viewer/issues"
|
|
51
|
+
"url": "https://github.com/no-problem-dev/markdown-live-viewer/issues"
|
|
52
52
|
},
|
|
53
|
-
"homepage": "https://github.com/no-problem-dev/markdown-viewer#readme",
|
|
53
|
+
"homepage": "https://github.com/no-problem-dev/markdown-live-viewer#readme",
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"express": "^5.0.0",
|
|
56
56
|
"commander": "^12.0.0",
|
package/public/js/app.js
CHANGED
package/public/js/navigation.js
CHANGED
package/public/js/search.js
CHANGED
package/public/styles/base.css
CHANGED
package/public/styles/modern.css
CHANGED
package/src/routes/api.js
CHANGED
|
@@ -22,9 +22,6 @@ async function getAllFiles(dir, baseDir, maxDepth = 5, currentDepth = 0) {
|
|
|
22
22
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
23
23
|
|
|
24
24
|
for (const entry of entries) {
|
|
25
|
-
// 隠しファイル・ディレクトリをスキップ
|
|
26
|
-
if (entry.name.startsWith('.')) continue;
|
|
27
|
-
|
|
28
25
|
// node_modules をスキップ
|
|
29
26
|
if (entry.name === 'node_modules') continue;
|
|
30
27
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { validatePath } from '../utils/path.js';
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 画像ファイルの MIME タイプマップ
|
|
10
|
+
*/
|
|
11
|
+
const IMAGE_MIME_TYPES = {
|
|
12
|
+
'.png': 'image/png',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.jpeg': 'image/jpeg',
|
|
15
|
+
'.gif': 'image/gif',
|
|
16
|
+
'.svg': 'image/svg+xml',
|
|
17
|
+
'.webp': 'image/webp',
|
|
18
|
+
'.ico': 'image/x-icon',
|
|
19
|
+
'.bmp': 'image/bmp',
|
|
20
|
+
'.avif': 'image/avif'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 画像ファイル配信ルート
|
|
25
|
+
* Markdownから相対パスで参照される画像を配信
|
|
26
|
+
*/
|
|
27
|
+
router.get(/^\/.*/, async (req, res, next) => {
|
|
28
|
+
try {
|
|
29
|
+
const requestPath = req.path;
|
|
30
|
+
const ext = path.extname(requestPath).toLowerCase();
|
|
31
|
+
|
|
32
|
+
// 画像ファイル以外は次のハンドラへ
|
|
33
|
+
const mimeType = IMAGE_MIME_TYPES[ext];
|
|
34
|
+
if (!mimeType) {
|
|
35
|
+
return next();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const docRoot = req.app.get('docRoot');
|
|
39
|
+
let filePath;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
filePath = validatePath(requestPath, docRoot);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code === 'ETRAVERSAL') {
|
|
45
|
+
return res.status(403).send('Access denied');
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ファイル存在確認
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(filePath);
|
|
53
|
+
} catch {
|
|
54
|
+
return next(); // 存在しない場合は次のハンドラへ
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 画像ファイルを配信
|
|
58
|
+
const imageData = await fs.readFile(filePath);
|
|
59
|
+
res.type(mimeType).send(imageData);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
next(error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export default router;
|
package/src/routes/directory.js
CHANGED
|
@@ -73,11 +73,8 @@ router.get(/^\/.*/, async (req, res, next) => {
|
|
|
73
73
|
return a.name.localeCompare(b.name);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
// 隠しファイルをフィルタ(オプション)
|
|
77
|
-
const visible = sorted.filter(entry => !entry.name.startsWith('.'));
|
|
78
|
-
|
|
79
76
|
// HTML リスト生成
|
|
80
|
-
const listHtml =
|
|
77
|
+
const listHtml = sorted.map(entry => {
|
|
81
78
|
const isDir = entry.isDirectory;
|
|
82
79
|
const href = path.join(requestPath, entry.name) + (isDir ? '/' : '');
|
|
83
80
|
const iconClass = getIconClass(entry.name, isDir);
|
|
@@ -94,8 +91,9 @@ router.get(/^\/.*/, async (req, res, next) => {
|
|
|
94
91
|
? `<li class="folder parent"><a href="${path.dirname(requestPath)}/">..</a><span class="size">-</span></li>\n`
|
|
95
92
|
: '';
|
|
96
93
|
|
|
94
|
+
const docRootName = req.app.get('docRootName');
|
|
97
95
|
const html = renderTemplate('page', {
|
|
98
|
-
title:
|
|
96
|
+
title: `${requestPath === '/' ? docRootName : requestPath} - ${docRootName}`,
|
|
99
97
|
content: `<h1>Index of ${escapeHtml(requestPath)}</h1>
|
|
100
98
|
<ul class="directory-listing">${parentLink}${listHtml}</ul>`,
|
|
101
99
|
breadcrumbs: generateBreadcrumbs(requestPath)
|
package/src/routes/markdown.js
CHANGED
|
@@ -31,8 +31,9 @@ router.get(/.*\.md$/i, async (req, res, next) => {
|
|
|
31
31
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
32
32
|
|
|
33
33
|
// テンプレートにMarkdownを埋め込み(クライアント側でレンダリング)
|
|
34
|
+
const docRootName = req.app.get('docRootName');
|
|
34
35
|
const html = renderTemplate('page', {
|
|
35
|
-
title: path.basename(filePath)
|
|
36
|
+
title: `${path.basename(filePath)} - ${docRootName}`,
|
|
36
37
|
content: `<div id="markdown-source" style="display:none">${escapeHtml(content)}</div>
|
|
37
38
|
<div id="markdown-rendered"></div>`,
|
|
38
39
|
breadcrumbs: generateBreadcrumbs(requestPath)
|
package/src/routes/raw.js
CHANGED
|
@@ -58,8 +58,9 @@ router.get(/^\/.*/, async (req, res, next) => {
|
|
|
58
58
|
const language = getLanguageFromExtension(ext);
|
|
59
59
|
const fileName = path.basename(filePath);
|
|
60
60
|
|
|
61
|
+
const docRootName = req.app.get('docRootName');
|
|
61
62
|
const html = renderTemplate('page', {
|
|
62
|
-
title: fileName
|
|
63
|
+
title: `${fileName} - ${docRootName}`,
|
|
63
64
|
content: `<h1>${escapeHtml(fileName)}</h1>
|
|
64
65
|
<pre><code class="language-${language}">${escapeHtml(content)}</code></pre>`,
|
|
65
66
|
breadcrumbs: generateBreadcrumbs(requestPath)
|
package/src/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import markdownRouter from './routes/markdown.js';
|
|
5
5
|
import rawRouter from './routes/raw.js';
|
|
6
|
+
import assetsRouter from './routes/assets.js';
|
|
6
7
|
import directoryRouter from './routes/directory.js';
|
|
7
8
|
import apiRouter from './routes/api.js';
|
|
8
9
|
import { securityHeaders, pathTraversalGuard } from './middleware/security.js';
|
|
@@ -23,6 +24,7 @@ export function createServer(options = {}) {
|
|
|
23
24
|
// ドキュメントルートの設定
|
|
24
25
|
const docRoot = path.resolve(options.dir || '.');
|
|
25
26
|
app.set('docRoot', docRoot);
|
|
27
|
+
app.set('docRootName', path.basename(docRoot));
|
|
26
28
|
|
|
27
29
|
// セキュリティミドルウェア(最初に登録)
|
|
28
30
|
app.use(securityHeaders);
|
|
@@ -50,6 +52,9 @@ export function createServer(options = {}) {
|
|
|
50
52
|
// Raw コード表示ルート(.js, .py 等)
|
|
51
53
|
app.use(rawRouter);
|
|
52
54
|
|
|
55
|
+
// 画像ファイル配信ルート
|
|
56
|
+
app.use(assetsRouter);
|
|
57
|
+
|
|
53
58
|
// ディレクトリ一覧ルート
|
|
54
59
|
app.use(directoryRouter);
|
|
55
60
|
|
package/templates/page.html
CHANGED