md-lv 1.0.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/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/mdv.js +5 -0
- package/package.json +64 -0
- package/public/favicon.svg +4 -0
- package/public/js/.gitkeep +0 -0
- package/public/js/app.js +174 -0
- package/public/js/navigation.js +201 -0
- package/public/js/search.js +286 -0
- package/public/styles/.gitkeep +0 -0
- package/public/styles/base.css +314 -0
- package/public/styles/modern.css +289 -0
- package/src/cli.js +137 -0
- package/src/index.js +15 -0
- package/src/middleware/.gitkeep +0 -0
- package/src/middleware/error.js +118 -0
- package/src/middleware/security.js +49 -0
- package/src/routes/.gitkeep +0 -0
- package/src/routes/api.js +92 -0
- package/src/routes/directory.js +110 -0
- package/src/routes/markdown.js +53 -0
- package/src/routes/raw.js +74 -0
- package/src/server.js +61 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/html.js +37 -0
- package/src/utils/icons.js +79 -0
- package/src/utils/language.js +102 -0
- package/src/utils/logger.js +81 -0
- package/src/utils/navigation.js +41 -0
- package/src/utils/path.js +86 -0
- package/src/utils/port.js +49 -0
- package/src/utils/readme.js +36 -0
- package/src/utils/template.js +35 -0
- package/templates/.gitkeep +0 -0
- package/templates/error.html +76 -0
- package/templates/page.html +60 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.0.0] - 2026-01-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **ESM Support**: Full ECMAScript Modules support with `"type": "module"`
|
|
13
|
+
- **Express 5.x**: Upgraded from Connect to Express 5.x framework
|
|
14
|
+
- **Mermaid Diagrams**: Render flowcharts, sequence diagrams, and more
|
|
15
|
+
- **MathJax Support**: LaTeX math formula rendering with `$...$` and `$$...$$`
|
|
16
|
+
- **Dark Mode**: Automatic theme switching based on system preferences
|
|
17
|
+
- **Search Functionality**: Client-side file search with real-time results
|
|
18
|
+
- **Keyboard Navigation**: Vim-style navigation (j/k) and shortcuts
|
|
19
|
+
- **Security Headers**: CSP, X-Content-Type-Options, X-Frame-Options, etc.
|
|
20
|
+
- **Path Traversal Protection**: Enhanced security against directory traversal attacks
|
|
21
|
+
- **Modern UI**: Improved styling with CSS custom properties
|
|
22
|
+
- **README Command**: `mdv readme` to find and open nearest README.md
|
|
23
|
+
- **Port Auto-Selection**: Automatically find available port if specified port is in use
|
|
24
|
+
- **Integration Tests**: Comprehensive test suite with Jest and Supertest
|
|
25
|
+
- **GitHub Actions CI**: Automated testing on Node.js 18, 20, and 22
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **CLI Parser**: Migrated from meow to Commander.js
|
|
30
|
+
- **Directory Structure**: Reorganized to `src/`, `routes/`, `middleware/`, `utils/`
|
|
31
|
+
- **Template System**: Simplified HTML templating with mustache-style variables
|
|
32
|
+
- **Logging**: Unified logger with configurable log levels
|
|
33
|
+
- **Error Handling**: Centralized error handling with custom error pages
|
|
34
|
+
- **Node.js Requirement**: Minimum version is now Node.js 18.0.0
|
|
35
|
+
|
|
36
|
+
### Removed
|
|
37
|
+
|
|
38
|
+
- **CommonJS Support**: No longer supports `require()` imports
|
|
39
|
+
- **Connect Framework**: Replaced with Express 5.x
|
|
40
|
+
- **Bluebird**: Using native Promises
|
|
41
|
+
- **meow**: Replaced with Commander.js
|
|
42
|
+
- **markdown-it**: Moved to client-side marked.js rendering
|
|
43
|
+
- **LiveReload**: Removed in favor of manual refresh (may be re-added)
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
|
|
47
|
+
- Path traversal prevention with symlink resolution
|
|
48
|
+
- Null byte injection protection
|
|
49
|
+
- Security headers (CSP, X-Frame-Options, etc.)
|
|
50
|
+
- Input validation and HTML escaping
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Unicode file name handling
|
|
55
|
+
- URL encoding in breadcrumbs
|
|
56
|
+
- Memory leaks in template caching
|
|
57
|
+
|
|
58
|
+
## [1.x.x] - Previous Versions
|
|
59
|
+
|
|
60
|
+
See the [archived markserv changelog](archived-markserv/CHANGELOG.md) for previous version history.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 markdown-viewer contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# markdown-viewer
|
|
2
|
+
|
|
3
|
+
> Serve Markdown files as HTML with live features
|
|
4
|
+
|
|
5
|
+
A lightweight local server that renders Markdown files as beautiful HTML pages, with support for GitHub Flavored Markdown, syntax highlighting, Mermaid diagrams, and MathJax formulas.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **GitHub-style Markdown** - Full GFM support with tables, task lists, and more
|
|
10
|
+
- **Syntax Highlighting** - Code blocks with automatic language detection
|
|
11
|
+
- **Mermaid Diagrams** - Flowcharts, sequence diagrams, and more
|
|
12
|
+
- **MathJax Support** - LaTeX math formulas (`$...$` and `$$...$$`)
|
|
13
|
+
- **Directory Browsing** - Navigate through your files with ease
|
|
14
|
+
- **Dark Mode** - Automatic theme switching based on system preferences
|
|
15
|
+
- **Search** - Find files quickly with built-in search
|
|
16
|
+
- **Keyboard Navigation** - Navigate with vim-style keys (j/k)
|
|
17
|
+
- **Security** - Path traversal protection and security headers
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g markdown-viewer
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or use npx:
|
|
26
|
+
```bash
|
|
27
|
+
npx markdown-viewer
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Basic Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Serve current directory
|
|
36
|
+
mdv
|
|
37
|
+
|
|
38
|
+
# Serve specific directory
|
|
39
|
+
mdv --dir /path/to/docs
|
|
40
|
+
|
|
41
|
+
# Serve on custom port
|
|
42
|
+
mdv --port 8080
|
|
43
|
+
|
|
44
|
+
# Open README.md automatically
|
|
45
|
+
mdv readme
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### CLI Options
|
|
49
|
+
|
|
50
|
+
| Option | Short | Default | Description |
|
|
51
|
+
|--------|-------|---------|-------------|
|
|
52
|
+
| `--port` | `-p` | `3000` | Server port |
|
|
53
|
+
| `--host` | `-H` | `localhost` | Bind address |
|
|
54
|
+
| `--dir` | `-d` | `.` | Document root |
|
|
55
|
+
| `--no-watch` | | `false` | Disable file watching |
|
|
56
|
+
| `--quiet` | `-q` | `false` | Suppress output |
|
|
57
|
+
| `--debug` | | `false` | Enable debug mode |
|
|
58
|
+
|
|
59
|
+
### Subcommands
|
|
60
|
+
|
|
61
|
+
#### `mdv readme`
|
|
62
|
+
|
|
63
|
+
Find and display the nearest README.md file:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd /path/to/project
|
|
67
|
+
mdv readme
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This command searches up the directory tree to find README.md and opens it in your browser.
|
|
71
|
+
|
|
72
|
+
## Supported Content
|
|
73
|
+
|
|
74
|
+
### Markdown Features
|
|
75
|
+
|
|
76
|
+
- Headings, paragraphs, lists
|
|
77
|
+
- Tables (GFM)
|
|
78
|
+
- Task lists
|
|
79
|
+
- Code blocks with syntax highlighting
|
|
80
|
+
- Blockquotes
|
|
81
|
+
- Links and images
|
|
82
|
+
- Horizontal rules
|
|
83
|
+
|
|
84
|
+
### Mermaid Diagrams
|
|
85
|
+
|
|
86
|
+
````markdown
|
|
87
|
+
```mermaid
|
|
88
|
+
flowchart TD
|
|
89
|
+
A[Start] --> B{Decision}
|
|
90
|
+
B -->|Yes| C[OK]
|
|
91
|
+
B -->|No| D[Cancel]
|
|
92
|
+
```
|
|
93
|
+
````
|
|
94
|
+
|
|
95
|
+
### Math Formulas
|
|
96
|
+
|
|
97
|
+
Inline: `$E = mc^2$`
|
|
98
|
+
|
|
99
|
+
Block:
|
|
100
|
+
```markdown
|
|
101
|
+
$$
|
|
102
|
+
\sum_{i=1}^n i = \frac{n(n+1)}{2}
|
|
103
|
+
$$
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Keyboard Shortcuts
|
|
107
|
+
|
|
108
|
+
| Shortcut | Action |
|
|
109
|
+
|----------|--------|
|
|
110
|
+
| `Alt + ←` | Go to parent directory |
|
|
111
|
+
| `Alt + Home` | Go to root |
|
|
112
|
+
| `j` / `↓` | Next item (in directory listing) |
|
|
113
|
+
| `k` / `↑` | Previous item |
|
|
114
|
+
| `Enter` | Open selected item |
|
|
115
|
+
| `/` | Focus search |
|
|
116
|
+
| `Escape` | Close search results |
|
|
117
|
+
|
|
118
|
+
## Requirements
|
|
119
|
+
|
|
120
|
+
- Node.js 18.0.0 or higher
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Clone repository
|
|
126
|
+
git clone https://github.com/your-username/markdown-viewer.git
|
|
127
|
+
cd markdown-viewer
|
|
128
|
+
|
|
129
|
+
# Install dependencies
|
|
130
|
+
npm install
|
|
131
|
+
|
|
132
|
+
# Run tests
|
|
133
|
+
npm test
|
|
134
|
+
|
|
135
|
+
# Start development server
|
|
136
|
+
npm start
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Contributing
|
|
140
|
+
|
|
141
|
+
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
146
|
+
|
|
147
|
+
## Acknowledgments
|
|
148
|
+
|
|
149
|
+
- [marked](https://marked.js.org/) - Markdown parser
|
|
150
|
+
- [highlight.js](https://highlightjs.org/) - Syntax highlighting
|
|
151
|
+
- [Mermaid](https://mermaid.js.org/) - Diagram rendering
|
|
152
|
+
- [MathJax](https://www.mathjax.org/) - Math formulas
|
package/bin/mdv.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "md-lv",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Serve Markdown files as HTML with live features - syntax highlighting, Mermaid diagrams, and MathJax formulas",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.0.0"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"bin": {
|
|
11
|
+
"mdv": "bin/mdv.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"src/",
|
|
16
|
+
"templates/",
|
|
17
|
+
"public/",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "node bin/mdv.js",
|
|
24
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
25
|
+
"test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/unit",
|
|
26
|
+
"test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/integration",
|
|
27
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
28
|
+
"prepublishOnly": "npm test"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"markdown",
|
|
32
|
+
"server",
|
|
33
|
+
"html",
|
|
34
|
+
"viewer",
|
|
35
|
+
"mermaid",
|
|
36
|
+
"mathjax",
|
|
37
|
+
"syntax-highlighting",
|
|
38
|
+
"gfm",
|
|
39
|
+
"github-flavored-markdown",
|
|
40
|
+
"documentation",
|
|
41
|
+
"docs",
|
|
42
|
+
"cli"
|
|
43
|
+
],
|
|
44
|
+
"author": "Watanabe Riku",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/no-problem-dev/markdown-viewer.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/no-problem-dev/markdown-viewer/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/no-problem-dev/markdown-viewer#readme",
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"express": "^5.0.0",
|
|
56
|
+
"commander": "^12.0.0",
|
|
57
|
+
"chokidar": "^3.5.0",
|
|
58
|
+
"open": "^10.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"jest": "^29.0.0",
|
|
62
|
+
"supertest": "^6.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
File without changes
|
package/public/js/app.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-viewer Client-side JavaScript
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
6
|
+
// Markdown レンダリング
|
|
7
|
+
renderMarkdown();
|
|
8
|
+
|
|
9
|
+
// シンタックスハイライト
|
|
10
|
+
highlightCode();
|
|
11
|
+
|
|
12
|
+
// Mermaid 図のレンダリング
|
|
13
|
+
await renderMermaid();
|
|
14
|
+
|
|
15
|
+
// MathJax レンダリング(Mermaid の後)
|
|
16
|
+
await renderMath();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Markdown をレンダリング
|
|
21
|
+
*/
|
|
22
|
+
function renderMarkdown() {
|
|
23
|
+
const source = document.getElementById('markdown-source');
|
|
24
|
+
const rendered = document.getElementById('markdown-rendered');
|
|
25
|
+
|
|
26
|
+
if (source && rendered && typeof marked !== 'undefined') {
|
|
27
|
+
const markdown = source.textContent;
|
|
28
|
+
|
|
29
|
+
// カスタムレンダラーで Mermaid コードブロックを処理
|
|
30
|
+
let mermaidCounter = 0;
|
|
31
|
+
const renderer = {
|
|
32
|
+
code({ text, lang }) {
|
|
33
|
+
if (lang === 'mermaid') {
|
|
34
|
+
const id = `mermaid-diagram-${mermaidCounter++}`;
|
|
35
|
+
return `<div class="mermaid" id="${id}">${escapeHtmlForMermaid(text)}</div>`;
|
|
36
|
+
}
|
|
37
|
+
// デフォルトのコードブロックレンダリング
|
|
38
|
+
const escaped = escapeHtmlForMermaid(text);
|
|
39
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
40
|
+
return `<pre><code${langClass}>${escaped}</code></pre>`;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// marked.js の設定とカスタムレンダラーを適用
|
|
45
|
+
marked.use({
|
|
46
|
+
gfm: true,
|
|
47
|
+
breaks: false,
|
|
48
|
+
pedantic: false,
|
|
49
|
+
renderer
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Markdown をレンダリング
|
|
53
|
+
rendered.innerHTML = marked.parse(markdown);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Mermaid 図をレンダリング
|
|
59
|
+
*/
|
|
60
|
+
async function renderMermaid() {
|
|
61
|
+
const mermaidDivs = document.querySelectorAll('.mermaid');
|
|
62
|
+
|
|
63
|
+
if (mermaidDivs.length === 0) return;
|
|
64
|
+
|
|
65
|
+
// Mermaid がロードされるまで待機(ESM モジュールは window.mermaid に公開される)
|
|
66
|
+
if (typeof window.mermaid === 'undefined') {
|
|
67
|
+
// Mermaid は ESM モジュールとして読み込まれるため、ポーリングで待機
|
|
68
|
+
let attempts = 0;
|
|
69
|
+
while (typeof window.mermaid === 'undefined' && attempts < 30) {
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
71
|
+
attempts++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 再度チェック
|
|
75
|
+
if (typeof window.mermaid === 'undefined') {
|
|
76
|
+
console.warn('Mermaid library not loaded');
|
|
77
|
+
mermaidDivs.forEach(div => {
|
|
78
|
+
div.innerHTML = `<pre class="mermaid-error">Mermaid library not loaded</pre>`;
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Mermaid を初期化(まだ初期化されていない場合)
|
|
86
|
+
window.mermaid.initialize({
|
|
87
|
+
startOnLoad: false,
|
|
88
|
+
theme: 'default',
|
|
89
|
+
securityLevel: 'loose',
|
|
90
|
+
flowchart: {
|
|
91
|
+
useMaxWidth: true,
|
|
92
|
+
htmlLabels: true
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// 各 Mermaid ブロックをレンダリング
|
|
97
|
+
for (const div of mermaidDivs) {
|
|
98
|
+
const code = div.textContent;
|
|
99
|
+
const id = div.id || `mermaid-${Date.now()}`;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const { svg } = await window.mermaid.render(id + '-svg', code);
|
|
103
|
+
div.innerHTML = svg;
|
|
104
|
+
div.classList.add('mermaid-rendered');
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error('Mermaid rendering error:', err);
|
|
107
|
+
div.innerHTML = `<pre class="mermaid-error">Mermaid Error: ${escapeHtmlForMermaid(err.message || 'Unknown error')}</pre>`;
|
|
108
|
+
div.classList.add('mermaid-error');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('Mermaid initialization error:', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* シンタックスハイライトを適用
|
|
118
|
+
*/
|
|
119
|
+
function highlightCode() {
|
|
120
|
+
if (typeof hljs !== 'undefined') {
|
|
121
|
+
document.querySelectorAll('pre code').forEach((block) => {
|
|
122
|
+
hljs.highlightElement(block);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* MathJax で数式をレンダリング
|
|
129
|
+
* @param {Element} container - レンダリング対象のコンテナ要素(省略時は content 全体)
|
|
130
|
+
*/
|
|
131
|
+
async function renderMath(container) {
|
|
132
|
+
const target = container || document.getElementById('markdown-rendered') || document.getElementById('content');
|
|
133
|
+
|
|
134
|
+
if (!target) return;
|
|
135
|
+
|
|
136
|
+
// MathJax がロードされるまで待機(最大2秒)
|
|
137
|
+
if (!window.MathJax || !window.MathJax.typesetPromise) {
|
|
138
|
+
let attempts = 0;
|
|
139
|
+
while ((!window.MathJax || !window.MathJax.typesetPromise) && attempts < 20) {
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
141
|
+
attempts++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!window.MathJax || !window.MathJax.typesetPromise) {
|
|
145
|
+
console.warn('MathJax library not loaded after 2 seconds');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await window.MathJax.typesetPromise([target]);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error('MathJax rendering error:', err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Mermaid 用の HTML エスケープ(最小限)
|
|
159
|
+
*/
|
|
160
|
+
function escapeHtmlForMermaid(str) {
|
|
161
|
+
if (typeof str !== 'string') return '';
|
|
162
|
+
return str
|
|
163
|
+
.replace(/&/g, '&')
|
|
164
|
+
.replace(/</g, '<')
|
|
165
|
+
.replace(/>/g, '>');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// グローバルに公開
|
|
169
|
+
window.mdv = {
|
|
170
|
+
renderMarkdown,
|
|
171
|
+
highlightCode,
|
|
172
|
+
renderMermaid,
|
|
173
|
+
renderMath
|
|
174
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-viewer Navigation Enhancement
|
|
3
|
+
* Keyboard navigation and UI improvements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
7
|
+
initKeyboardNavigation();
|
|
8
|
+
initDirectoryListNavigation();
|
|
9
|
+
initBackToTop();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* キーボードナビゲーションを初期化
|
|
14
|
+
*/
|
|
15
|
+
function initKeyboardNavigation() {
|
|
16
|
+
document.addEventListener('keydown', (e) => {
|
|
17
|
+
// Alt + Left Arrow: 親ディレクトリへ
|
|
18
|
+
if (e.altKey && e.key === 'ArrowLeft') {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
navigateToParent();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Alt + Home: ルートへ
|
|
24
|
+
if (e.altKey && e.key === 'Home') {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
navigateToRoot();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Alt + Up Arrow: ページ先頭へ
|
|
30
|
+
if (e.altKey && e.key === 'ArrowUp') {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Alt + Down Arrow: ページ末尾へ
|
|
36
|
+
if (e.altKey && e.key === 'ArrowDown') {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// "/" キー: 検索フォーカス(検索機能がある場合)
|
|
42
|
+
if (e.key === '/' && !isInputFocused()) {
|
|
43
|
+
const searchInput = document.getElementById('search-input');
|
|
44
|
+
if (searchInput) {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
searchInput.focus();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Escape: フォーカス解除
|
|
51
|
+
if (e.key === 'Escape') {
|
|
52
|
+
document.activeElement.blur();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 親ディレクトリへナビゲート
|
|
59
|
+
*/
|
|
60
|
+
function navigateToParent() {
|
|
61
|
+
const breadcrumbs = document.querySelectorAll('#breadcrumbs a');
|
|
62
|
+
if (breadcrumbs.length >= 1) {
|
|
63
|
+
// 最後から2番目のリンク(親ディレクトリ)
|
|
64
|
+
const parentLink = breadcrumbs[breadcrumbs.length - 1];
|
|
65
|
+
if (parentLink) {
|
|
66
|
+
window.location.href = parentLink.href;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* ルートディレクトリへナビゲート
|
|
73
|
+
*/
|
|
74
|
+
function navigateToRoot() {
|
|
75
|
+
window.location.href = '/';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ディレクトリ一覧のキーボードナビゲーション
|
|
80
|
+
*/
|
|
81
|
+
function initDirectoryListNavigation() {
|
|
82
|
+
const directoryList = document.querySelector('.directory-listing');
|
|
83
|
+
if (!directoryList) return;
|
|
84
|
+
|
|
85
|
+
const items = directoryList.querySelectorAll('li a');
|
|
86
|
+
let currentIndex = -1;
|
|
87
|
+
|
|
88
|
+
document.addEventListener('keydown', (e) => {
|
|
89
|
+
if (isInputFocused()) return;
|
|
90
|
+
|
|
91
|
+
// j/k または ArrowDown/ArrowUp でリスト内を移動
|
|
92
|
+
if (e.key === 'j' || e.key === 'ArrowDown') {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
currentIndex = Math.min(currentIndex + 1, items.length - 1);
|
|
95
|
+
focusItem(currentIndex);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (e.key === 'k' || e.key === 'ArrowUp') {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
currentIndex = Math.max(currentIndex - 1, 0);
|
|
101
|
+
focusItem(currentIndex);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Enter で選択したアイテムを開く
|
|
105
|
+
if (e.key === 'Enter' && currentIndex >= 0) {
|
|
106
|
+
items[currentIndex].click();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// g で先頭へ
|
|
110
|
+
if (e.key === 'g' && !e.ctrlKey && !e.metaKey) {
|
|
111
|
+
currentIndex = 0;
|
|
112
|
+
focusItem(currentIndex);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// G (Shift + g) で末尾へ
|
|
116
|
+
if (e.key === 'G') {
|
|
117
|
+
currentIndex = items.length - 1;
|
|
118
|
+
focusItem(currentIndex);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function focusItem(index) {
|
|
123
|
+
// 前のフォーカスを解除
|
|
124
|
+
items.forEach(item => item.parentElement.classList.remove('keyboard-focus'));
|
|
125
|
+
|
|
126
|
+
if (index >= 0 && index < items.length) {
|
|
127
|
+
items[index].parentElement.classList.add('keyboard-focus');
|
|
128
|
+
items[index].focus();
|
|
129
|
+
items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 「トップへ戻る」ボタンを初期化
|
|
136
|
+
*/
|
|
137
|
+
function initBackToTop() {
|
|
138
|
+
// ボタンを作成
|
|
139
|
+
const button = document.createElement('button');
|
|
140
|
+
button.id = 'back-to-top';
|
|
141
|
+
button.innerHTML = '↑';
|
|
142
|
+
button.title = 'Back to top (Alt + ↑)';
|
|
143
|
+
button.setAttribute('aria-label', 'Back to top');
|
|
144
|
+
|
|
145
|
+
// スタイル設定
|
|
146
|
+
Object.assign(button.style, {
|
|
147
|
+
position: 'fixed',
|
|
148
|
+
bottom: '20px',
|
|
149
|
+
right: '20px',
|
|
150
|
+
width: '40px',
|
|
151
|
+
height: '40px',
|
|
152
|
+
borderRadius: '50%',
|
|
153
|
+
border: 'none',
|
|
154
|
+
backgroundColor: 'var(--color-link, #0366d6)',
|
|
155
|
+
color: 'white',
|
|
156
|
+
fontSize: '20px',
|
|
157
|
+
cursor: 'pointer',
|
|
158
|
+
opacity: '0',
|
|
159
|
+
visibility: 'hidden',
|
|
160
|
+
transition: 'opacity 0.3s, visibility 0.3s',
|
|
161
|
+
zIndex: '1000',
|
|
162
|
+
boxShadow: 'var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1))'
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
document.body.appendChild(button);
|
|
166
|
+
|
|
167
|
+
// スクロール時の表示制御
|
|
168
|
+
window.addEventListener('scroll', () => {
|
|
169
|
+
if (window.scrollY > 300) {
|
|
170
|
+
button.style.opacity = '1';
|
|
171
|
+
button.style.visibility = 'visible';
|
|
172
|
+
} else {
|
|
173
|
+
button.style.opacity = '0';
|
|
174
|
+
button.style.visibility = 'hidden';
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// クリックでトップへ
|
|
179
|
+
button.addEventListener('click', () => {
|
|
180
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 入力フィールドにフォーカスがあるかチェック
|
|
186
|
+
*/
|
|
187
|
+
function isInputFocused() {
|
|
188
|
+
const active = document.activeElement;
|
|
189
|
+
return active && (
|
|
190
|
+
active.tagName === 'INPUT' ||
|
|
191
|
+
active.tagName === 'TEXTAREA' ||
|
|
192
|
+
active.isContentEditable
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// グローバルに公開
|
|
197
|
+
window.mdvNav = {
|
|
198
|
+
navigateToParent,
|
|
199
|
+
navigateToRoot,
|
|
200
|
+
isInputFocused
|
|
201
|
+
};
|