md-lv 1.2.6 → 1.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/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/public/js/app.js +213 -0
- package/public/js/navigation.js +21 -7
- package/public/styles/modern.css +185 -0
- package/src/routes/raw.js +33 -12
- package/src/utils/language.js +94 -2
- package/templates/page.html +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-02-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Code Viewer (VSCode-like)**: Non-Markdown files now display with line numbers, indent guides, filename header, and language label in a VSCode-inspired code viewer
|
|
13
|
+
- **Dark Mode Syntax Highlighting**: Automatic highlight.js theme switching (github / github-dark) based on system color scheme preference
|
|
14
|
+
- **Binary File Detection**: Binary files (images, archives, executables, etc.) are now properly skipped instead of attempting to render
|
|
15
|
+
- **Language Display Names**: Code viewer header shows human-readable language names (e.g., "JavaScript" instead of "javascript")
|
|
16
|
+
- **Expanded Language Support**: Added Dockerfile syntax highlighting, extended filename-based language detection (.env, .prettierrc, .eslintrc, ignore files, etc.)
|
|
17
|
+
- **File Size Limit**: Files larger than 1MB are skipped to prevent browser performance issues
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Copy Button**: Breadcrumb copy button now copies the relative file path instead of just the filename
|
|
22
|
+
- **highlight.js CDN**: Switched from `highlight.js` to `@highlightjs/cdn-assets` package for better modularity
|
|
23
|
+
- **Language Detection Priority**: Filename-based detection now takes priority over extension-based detection for more accurate results
|
|
24
|
+
|
|
8
25
|
## [1.2.6] - 2026-02-05
|
|
9
26
|
|
|
10
27
|
### Fixed
|
package/package.json
CHANGED
package/public/js/app.js
CHANGED
|
@@ -9,6 +9,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
9
9
|
// シンタックスハイライト
|
|
10
10
|
highlightCode();
|
|
11
11
|
|
|
12
|
+
// コードビューア拡張(行番号・インデントガイド)
|
|
13
|
+
enhanceCodeViewer();
|
|
14
|
+
|
|
12
15
|
// Mermaid 図のレンダリング
|
|
13
16
|
await renderMermaid();
|
|
14
17
|
|
|
@@ -329,6 +332,215 @@ async function renderMath(container) {
|
|
|
329
332
|
}
|
|
330
333
|
}
|
|
331
334
|
|
|
335
|
+
/**
|
|
336
|
+
* コードビューアを拡張(行番号・インデントガイド)
|
|
337
|
+
* .code-viewer 内の pre>code ブロックを変換する
|
|
338
|
+
*/
|
|
339
|
+
function enhanceCodeViewer() {
|
|
340
|
+
const viewers = document.querySelectorAll('.code-viewer');
|
|
341
|
+
if (viewers.length === 0) return;
|
|
342
|
+
|
|
343
|
+
viewers.forEach(viewer => {
|
|
344
|
+
const codeBlock = viewer.querySelector('pre code');
|
|
345
|
+
if (!codeBlock) return;
|
|
346
|
+
|
|
347
|
+
const pre = codeBlock.parentElement;
|
|
348
|
+
const body = viewer.querySelector('.code-viewer-body');
|
|
349
|
+
if (!body) return;
|
|
350
|
+
|
|
351
|
+
// highlight.js が適用済みの HTML を取得
|
|
352
|
+
const highlightedHtml = codeBlock.innerHTML;
|
|
353
|
+
|
|
354
|
+
// 行に分割(highlight.js の span タグを考慮)
|
|
355
|
+
const lines = splitHighlightedLines(highlightedHtml);
|
|
356
|
+
const tabSize = detectTabSize(codeBlock.textContent);
|
|
357
|
+
|
|
358
|
+
// テーブル構造を構築
|
|
359
|
+
const table = document.createElement('table');
|
|
360
|
+
table.className = 'code-table';
|
|
361
|
+
table.setAttribute('role', 'presentation');
|
|
362
|
+
|
|
363
|
+
const tbody = document.createElement('tbody');
|
|
364
|
+
|
|
365
|
+
lines.forEach((lineHtml, index) => {
|
|
366
|
+
const tr = document.createElement('tr');
|
|
367
|
+
tr.className = 'code-line';
|
|
368
|
+
|
|
369
|
+
// 行番号セル
|
|
370
|
+
const lineNumTd = document.createElement('td');
|
|
371
|
+
lineNumTd.className = 'line-number';
|
|
372
|
+
lineNumTd.setAttribute('data-line', index + 1);
|
|
373
|
+
lineNumTd.textContent = index + 1;
|
|
374
|
+
|
|
375
|
+
// コード内容セル
|
|
376
|
+
const contentTd = document.createElement('td');
|
|
377
|
+
contentTd.className = 'line-content';
|
|
378
|
+
|
|
379
|
+
// インデントガイドを生成
|
|
380
|
+
const textContent = stripHtmlTags(lineHtml);
|
|
381
|
+
const indentLevel = calculateIndentLevel(textContent, tabSize);
|
|
382
|
+
|
|
383
|
+
if (indentLevel > 0) {
|
|
384
|
+
const guides = document.createElement('span');
|
|
385
|
+
guides.className = 'indent-guides';
|
|
386
|
+
guides.setAttribute('aria-hidden', 'true');
|
|
387
|
+
// 各インデントレベルのガイド線を生成
|
|
388
|
+
for (let i = 0; i < indentLevel; i++) {
|
|
389
|
+
const guide = document.createElement('span');
|
|
390
|
+
guide.className = 'indent-guide';
|
|
391
|
+
guide.style.left = (i * tabSize) + 'ch';
|
|
392
|
+
guides.appendChild(guide);
|
|
393
|
+
}
|
|
394
|
+
contentTd.appendChild(guides);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ハイライト済みコード
|
|
398
|
+
const codeSpan = document.createElement('span');
|
|
399
|
+
codeSpan.className = 'line-code';
|
|
400
|
+
codeSpan.innerHTML = lineHtml || '\n';
|
|
401
|
+
contentTd.appendChild(codeSpan);
|
|
402
|
+
|
|
403
|
+
tr.appendChild(lineNumTd);
|
|
404
|
+
tr.appendChild(contentTd);
|
|
405
|
+
tbody.appendChild(tr);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
table.appendChild(tbody);
|
|
409
|
+
|
|
410
|
+
// 元の pre>code を置き換え
|
|
411
|
+
// highlight.js のクラスを body に引き継ぐ(CSSセレクタ継承用)
|
|
412
|
+
const langClass = codeBlock.className || '';
|
|
413
|
+
if (langClass) {
|
|
414
|
+
langClass.split(/\s+/).forEach(cls => {
|
|
415
|
+
if (cls) body.classList.add(cls);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
body.innerHTML = '';
|
|
420
|
+
body.appendChild(table);
|
|
421
|
+
|
|
422
|
+
// CSS変数でタブサイズを設定
|
|
423
|
+
viewer.style.setProperty('--tab-size', tabSize);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* highlight.js のハイライト済み HTML を行ごとに分割
|
|
429
|
+
* span タグが行をまたぐ場合を考慮する
|
|
430
|
+
*/
|
|
431
|
+
function splitHighlightedLines(html) {
|
|
432
|
+
// まず改行で分割
|
|
433
|
+
const rawLines = html.split('\n');
|
|
434
|
+
|
|
435
|
+
// 開いた span タグを追跡し、行をまたぐ場合に閉じ/開きを補完
|
|
436
|
+
const result = [];
|
|
437
|
+
let openTags = [];
|
|
438
|
+
|
|
439
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
440
|
+
let line = rawLines[i];
|
|
441
|
+
|
|
442
|
+
// 前の行から引き継いだ開きタグを先頭に追加
|
|
443
|
+
if (openTags.length > 0) {
|
|
444
|
+
line = openTags.join('') + line;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// この行の開きタグと閉じタグを解析
|
|
448
|
+
openTags = getOpenTags(line);
|
|
449
|
+
|
|
450
|
+
// 開いたままのタグがある場合、行末で閉じる
|
|
451
|
+
if (openTags.length > 0) {
|
|
452
|
+
line += '</span>'.repeat(openTags.length);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
result.push(line);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 末尾の空行を削除
|
|
459
|
+
while (result.length > 1 && result[result.length - 1].trim() === '') {
|
|
460
|
+
result.pop();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* HTML 文字列内の未閉じの span タグを取得
|
|
468
|
+
*/
|
|
469
|
+
function getOpenTags(html) {
|
|
470
|
+
const openTags = [];
|
|
471
|
+
const tagRegex = /<\/?span[^>]*>/g;
|
|
472
|
+
let match;
|
|
473
|
+
|
|
474
|
+
while ((match = tagRegex.exec(html)) !== null) {
|
|
475
|
+
if (match[0].startsWith('</')) {
|
|
476
|
+
openTags.pop();
|
|
477
|
+
} else {
|
|
478
|
+
openTags.push(match[0]);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return openTags;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* HTML タグを除去してテキストのみを取得
|
|
487
|
+
*/
|
|
488
|
+
function stripHtmlTags(html) {
|
|
489
|
+
const tmp = document.createElement('div');
|
|
490
|
+
tmp.innerHTML = html;
|
|
491
|
+
return tmp.textContent || '';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* コードのタブサイズを自動検出
|
|
496
|
+
* 最も一般的なインデント幅を返す
|
|
497
|
+
*/
|
|
498
|
+
function detectTabSize(text) {
|
|
499
|
+
const lines = text.split('\n');
|
|
500
|
+
const indents = {};
|
|
501
|
+
|
|
502
|
+
for (const line of lines) {
|
|
503
|
+
const match = line.match(/^( +)\S/);
|
|
504
|
+
if (match) {
|
|
505
|
+
const len = match[1].length;
|
|
506
|
+
indents[len] = (indents[len] || 0) + 1;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// インデント幅の最小差分を求める
|
|
511
|
+
const sizes = Object.keys(indents).map(Number).sort((a, b) => a - b);
|
|
512
|
+
if (sizes.length === 0) return 2;
|
|
513
|
+
|
|
514
|
+
// 最小のインデント幅
|
|
515
|
+
const minIndent = sizes[0];
|
|
516
|
+
if (minIndent <= 4 && minIndent >= 1) return minIndent;
|
|
517
|
+
|
|
518
|
+
return 2; // デフォルト
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* テキストのインデントレベルを計算
|
|
523
|
+
* @param {string} text - テキスト行
|
|
524
|
+
* @param {number} tabSize - タブサイズ(スペース数)
|
|
525
|
+
* @returns {number} インデントレベル
|
|
526
|
+
*/
|
|
527
|
+
function calculateIndentLevel(text, tabSize) {
|
|
528
|
+
if (!text || text.trim() === '') return 0;
|
|
529
|
+
|
|
530
|
+
let spaces = 0;
|
|
531
|
+
for (let i = 0; i < text.length; i++) {
|
|
532
|
+
if (text[i] === ' ') {
|
|
533
|
+
spaces++;
|
|
534
|
+
} else if (text[i] === '\t') {
|
|
535
|
+
spaces += tabSize;
|
|
536
|
+
} else {
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return Math.floor(spaces / tabSize);
|
|
542
|
+
}
|
|
543
|
+
|
|
332
544
|
/**
|
|
333
545
|
* Mermaid 用の HTML エスケープ(最小限)
|
|
334
546
|
*/
|
|
@@ -344,6 +556,7 @@ function escapeHtmlForMermaid(str) {
|
|
|
344
556
|
window.mdv = {
|
|
345
557
|
renderMarkdown,
|
|
346
558
|
highlightCode,
|
|
559
|
+
enhanceCodeViewer,
|
|
347
560
|
renderMermaid,
|
|
348
561
|
renderMath
|
|
349
562
|
};
|
package/public/js/navigation.js
CHANGED
|
@@ -598,7 +598,8 @@ function initBackToTop() {
|
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
/**
|
|
601
|
-
*
|
|
601
|
+
* パスコピーボタンを初期化
|
|
602
|
+
* ファイル名ではなく相対パスをコピーする
|
|
602
603
|
*/
|
|
603
604
|
function initCopyFilename() {
|
|
604
605
|
const breadcrumbs = document.getElementById('breadcrumbs');
|
|
@@ -607,15 +608,19 @@ function initCopyFilename() {
|
|
|
607
608
|
const currentSpan = breadcrumbs.querySelector('.current');
|
|
608
609
|
if (!currentSpan) return;
|
|
609
610
|
|
|
610
|
-
const
|
|
611
|
-
if (!
|
|
611
|
+
const currentName = currentSpan.textContent.trim();
|
|
612
|
+
if (!currentName || currentName === 'Home') return;
|
|
613
|
+
|
|
614
|
+
// 相対パスを取得(先頭の / を除去)
|
|
615
|
+
const relativePath = getRelativePath(window.location.pathname);
|
|
616
|
+
if (!relativePath) return;
|
|
612
617
|
|
|
613
618
|
// コピーボタンを作成
|
|
614
619
|
const copyButton = document.createElement('button');
|
|
615
620
|
copyButton.id = 'copy-filename';
|
|
616
621
|
copyButton.type = 'button';
|
|
617
|
-
copyButton.title = 'Copy
|
|
618
|
-
copyButton.setAttribute('aria-label', 'Copy
|
|
622
|
+
copyButton.title = 'Copy path: ' + relativePath;
|
|
623
|
+
copyButton.setAttribute('aria-label', 'Copy path');
|
|
619
624
|
copyButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
|
|
620
625
|
|
|
621
626
|
// currentSpanの後にボタンを挿入
|
|
@@ -627,12 +632,12 @@ function initCopyFilename() {
|
|
|
627
632
|
e.stopPropagation();
|
|
628
633
|
|
|
629
634
|
try {
|
|
630
|
-
await navigator.clipboard.writeText(
|
|
635
|
+
await navigator.clipboard.writeText(relativePath);
|
|
631
636
|
showCopySuccess(copyButton);
|
|
632
637
|
} catch (err) {
|
|
633
638
|
// フォールバック: execCommandを使用
|
|
634
639
|
const textArea = document.createElement('textarea');
|
|
635
|
-
textArea.value =
|
|
640
|
+
textArea.value = relativePath;
|
|
636
641
|
textArea.style.position = 'fixed';
|
|
637
642
|
textArea.style.left = '-9999px';
|
|
638
643
|
document.body.appendChild(textArea);
|
|
@@ -648,6 +653,15 @@ function initCopyFilename() {
|
|
|
648
653
|
});
|
|
649
654
|
}
|
|
650
655
|
|
|
656
|
+
/**
|
|
657
|
+
* URLパスから相対パスを取得
|
|
658
|
+
* @param {string} pathname - window.location.pathname
|
|
659
|
+
* @returns {string} 先頭の / を除いた相対パス
|
|
660
|
+
*/
|
|
661
|
+
function getRelativePath(pathname) {
|
|
662
|
+
return pathname.replace(/^\//, '');
|
|
663
|
+
}
|
|
664
|
+
|
|
651
665
|
/**
|
|
652
666
|
* コピー成功時のフィードバック表示
|
|
653
667
|
*/
|
package/public/styles/modern.css
CHANGED
|
@@ -148,6 +148,183 @@ code {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/* ============================================
|
|
152
|
+
Code Viewer (VSCode-like)
|
|
153
|
+
============================================ */
|
|
154
|
+
.code-viewer {
|
|
155
|
+
--cv-bg: var(--color-code-bg);
|
|
156
|
+
--cv-header-bg: var(--color-bg-secondary);
|
|
157
|
+
--cv-border: var(--color-border);
|
|
158
|
+
--cv-line-number-color: var(--color-text-muted);
|
|
159
|
+
--cv-line-number-bg: var(--color-code-bg);
|
|
160
|
+
--cv-line-hover-bg: rgba(128, 128, 128, 0.08);
|
|
161
|
+
--cv-indent-guide-color: rgba(128, 128, 128, 0.2);
|
|
162
|
+
--tab-size: 2;
|
|
163
|
+
border: 1px solid var(--cv-border);
|
|
164
|
+
border-radius: 8px;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
box-shadow: var(--shadow-sm);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.code-viewer-header {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
justify-content: space-between;
|
|
173
|
+
padding: 8px 16px;
|
|
174
|
+
background-color: var(--cv-header-bg);
|
|
175
|
+
border-bottom: 1px solid var(--cv-border);
|
|
176
|
+
font-size: 13px;
|
|
177
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.code-viewer-filename {
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
color: var(--color-text);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.code-viewer-language {
|
|
186
|
+
color: var(--color-text-muted);
|
|
187
|
+
font-size: 12px;
|
|
188
|
+
padding: 2px 8px;
|
|
189
|
+
background-color: var(--cv-bg);
|
|
190
|
+
border-radius: 4px;
|
|
191
|
+
border: 1px solid var(--cv-border);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.code-viewer-body {
|
|
195
|
+
overflow-x: auto;
|
|
196
|
+
background-color: var(--cv-bg);
|
|
197
|
+
-webkit-overflow-scrolling: touch;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Override default pre/code styles inside code-viewer */
|
|
201
|
+
.code-viewer pre {
|
|
202
|
+
margin: 0;
|
|
203
|
+
padding: 0;
|
|
204
|
+
border: none;
|
|
205
|
+
border-radius: 0;
|
|
206
|
+
box-shadow: none;
|
|
207
|
+
background: transparent;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.code-viewer pre code {
|
|
211
|
+
background: transparent;
|
|
212
|
+
padding: 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Code table layout */
|
|
216
|
+
.code-table {
|
|
217
|
+
width: 100%;
|
|
218
|
+
border-collapse: collapse;
|
|
219
|
+
border-spacing: 0;
|
|
220
|
+
border: none;
|
|
221
|
+
border-radius: 0;
|
|
222
|
+
overflow: visible;
|
|
223
|
+
margin: 0;
|
|
224
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
line-height: 1.5;
|
|
227
|
+
tab-size: var(--tab-size);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.code-table tr {
|
|
231
|
+
background: transparent;
|
|
232
|
+
border: none;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.code-table tr:nth-child(2n) {
|
|
236
|
+
background: transparent;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.code-table th,
|
|
240
|
+
.code-table td {
|
|
241
|
+
border: none;
|
|
242
|
+
padding: 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.code-line:hover {
|
|
246
|
+
background-color: var(--cv-line-hover-bg);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Line numbers */
|
|
250
|
+
.line-number {
|
|
251
|
+
position: sticky;
|
|
252
|
+
left: 0;
|
|
253
|
+
width: 1px;
|
|
254
|
+
min-width: 50px;
|
|
255
|
+
padding: 0 12px 0 16px !important;
|
|
256
|
+
text-align: right;
|
|
257
|
+
color: var(--cv-line-number-color);
|
|
258
|
+
background-color: var(--cv-line-number-bg);
|
|
259
|
+
user-select: none;
|
|
260
|
+
white-space: nowrap;
|
|
261
|
+
vertical-align: top;
|
|
262
|
+
font-size: 13px;
|
|
263
|
+
line-height: 1.5;
|
|
264
|
+
border-right: 1px solid var(--cv-border);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.code-line:hover .line-number {
|
|
268
|
+
color: var(--color-text-secondary);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Line content */
|
|
272
|
+
.line-content {
|
|
273
|
+
position: relative;
|
|
274
|
+
padding: 0 16px 0 16px !important;
|
|
275
|
+
white-space: pre;
|
|
276
|
+
word-wrap: normal;
|
|
277
|
+
vertical-align: top;
|
|
278
|
+
font-size: 13px;
|
|
279
|
+
line-height: 1.5;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Indent guides */
|
|
283
|
+
.indent-guides {
|
|
284
|
+
position: absolute;
|
|
285
|
+
top: 0;
|
|
286
|
+
left: 16px;
|
|
287
|
+
bottom: 0;
|
|
288
|
+
pointer-events: none;
|
|
289
|
+
user-select: none;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.indent-guide {
|
|
293
|
+
position: absolute;
|
|
294
|
+
top: 0;
|
|
295
|
+
bottom: 0;
|
|
296
|
+
width: 1px;
|
|
297
|
+
background-color: var(--cv-indent-guide-color);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Line code span */
|
|
301
|
+
.line-code {
|
|
302
|
+
position: relative;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Override highlight.js background on code-viewer-body */
|
|
306
|
+
.code-viewer-body.hljs {
|
|
307
|
+
background: transparent !important;
|
|
308
|
+
padding: 0 !important;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* Dark mode adjustments */
|
|
312
|
+
@media (prefers-color-scheme: dark) {
|
|
313
|
+
.code-viewer {
|
|
314
|
+
--cv-line-hover-bg: rgba(128, 128, 128, 0.1);
|
|
315
|
+
--cv-indent-guide-color: rgba(128, 128, 128, 0.25);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* First and last lines padding */
|
|
320
|
+
.code-table tbody tr:first-child td {
|
|
321
|
+
padding-top: 12px !important;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.code-table tbody tr:last-child td {
|
|
325
|
+
padding-bottom: 12px !important;
|
|
326
|
+
}
|
|
327
|
+
|
|
151
328
|
/* ============================================
|
|
152
329
|
Directory Listing Enhancement
|
|
153
330
|
============================================ */
|
|
@@ -812,4 +989,12 @@ body.sidebar-open #sidebar-overlay {
|
|
|
812
989
|
#main-wrapper {
|
|
813
990
|
margin-left: 0 !important;
|
|
814
991
|
}
|
|
992
|
+
|
|
993
|
+
.line-number {
|
|
994
|
+
position: static;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.code-viewer-body {
|
|
998
|
+
overflow: visible;
|
|
999
|
+
}
|
|
815
1000
|
}
|
package/src/routes/raw.js
CHANGED
|
@@ -3,12 +3,15 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { validatePath } from '../utils/path.js';
|
|
5
5
|
import { renderTemplate } from '../utils/template.js';
|
|
6
|
-
import { getLanguageFromExtension, getLanguageFromFilename,
|
|
6
|
+
import { getLanguageFromExtension, getLanguageFromFilename, getLanguageDisplayName, isBinaryExtension } from '../utils/language.js';
|
|
7
7
|
import { escapeHtml } from '../utils/html.js';
|
|
8
8
|
import { generateBreadcrumbs } from '../utils/navigation.js';
|
|
9
9
|
|
|
10
10
|
const router = Router();
|
|
11
11
|
|
|
12
|
+
// 1MB - 巨大ファイルの表示を防止
|
|
13
|
+
const MAX_FILE_SIZE = 1 * 1024 * 1024;
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Raw コード表示ルート
|
|
14
17
|
* 非 Markdown ファイルをシンタックスハイライト付きで表示
|
|
@@ -23,14 +26,13 @@ router.get(/^\/.*/, async (req, res, next) => {
|
|
|
23
26
|
return next();
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// サポートされていない拡張子かつファイル名でも判定できない場合は次のハンドラへ
|
|
30
|
-
if (!isSupportedExtension(ext) && !filenameLanguage) {
|
|
29
|
+
// バイナリファイルは次のハンドラへ
|
|
30
|
+
if (isBinaryExtension(ext)) {
|
|
31
31
|
return next();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
const fileName = path.basename(requestPath);
|
|
35
|
+
|
|
34
36
|
const docRoot = req.app.get('docRoot');
|
|
35
37
|
let filePath;
|
|
36
38
|
|
|
@@ -56,16 +58,35 @@ router.get(/^\/.*/, async (req, res, next) => {
|
|
|
56
58
|
return next();
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// ファイルサイズ制限チェック
|
|
62
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
63
|
+
return next();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ファイル読み込み(非UTF-8ファイルを安全にスキップ)
|
|
67
|
+
let content;
|
|
68
|
+
try {
|
|
69
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
70
|
+
} catch {
|
|
71
|
+
return next();
|
|
72
|
+
}
|
|
73
|
+
// ファイル名ベースの判定を優先し、なければ拡張子ベース(未知の拡張子は plaintext)
|
|
74
|
+
const filenameLanguage = getLanguageFromFilename(fileName);
|
|
75
|
+
const language = filenameLanguage || getLanguageFromExtension(ext);
|
|
76
|
+
const languageDisplayName = getLanguageDisplayName(language);
|
|
63
77
|
|
|
64
78
|
const docRootName = req.app.get('docRootName');
|
|
65
79
|
const html = renderTemplate('page', {
|
|
66
80
|
title: `${fileName} - ${docRootName}`,
|
|
67
|
-
content: `<
|
|
68
|
-
|
|
81
|
+
content: `<div class="code-viewer" data-file-path="${escapeHtml(requestPath)}">
|
|
82
|
+
<div class="code-viewer-header">
|
|
83
|
+
<span class="code-viewer-filename">${escapeHtml(fileName)}</span>
|
|
84
|
+
<span class="code-viewer-language">${escapeHtml(languageDisplayName)}</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="code-viewer-body">
|
|
87
|
+
<pre><code class="language-${language}">${escapeHtml(content)}</code></pre>
|
|
88
|
+
</div>
|
|
89
|
+
</div>`,
|
|
69
90
|
breadcrumbs: generateBreadcrumbs(requestPath)
|
|
70
91
|
});
|
|
71
92
|
|
package/src/utils/language.js
CHANGED
|
@@ -26,13 +26,32 @@ export function getLanguageFromFilename(filename) {
|
|
|
26
26
|
// Shell config
|
|
27
27
|
'.bashrc': 'bash',
|
|
28
28
|
'.bash_profile': 'bash',
|
|
29
|
-
'.zshrc': '
|
|
29
|
+
'.zshrc': 'bash',
|
|
30
30
|
'.profile': 'bash',
|
|
31
31
|
|
|
32
32
|
// Node.js
|
|
33
33
|
'.npmrc': 'ini',
|
|
34
34
|
'.nvmrc': 'plaintext',
|
|
35
35
|
|
|
36
|
+
// Config (JSON-based)
|
|
37
|
+
'.prettierrc': 'json',
|
|
38
|
+
'.eslintrc': 'json',
|
|
39
|
+
'.babelrc': 'json',
|
|
40
|
+
|
|
41
|
+
// Env files
|
|
42
|
+
'.env': 'bash',
|
|
43
|
+
'.env.local': 'bash',
|
|
44
|
+
'.env.example': 'bash',
|
|
45
|
+
'.env.development': 'bash',
|
|
46
|
+
'.env.production': 'bash',
|
|
47
|
+
'.env.test': 'bash',
|
|
48
|
+
|
|
49
|
+
// Ignore files
|
|
50
|
+
'.prettierignore': 'plaintext',
|
|
51
|
+
'.eslintignore': 'plaintext',
|
|
52
|
+
'.dockerignore': 'plaintext',
|
|
53
|
+
'.npmignore': 'plaintext',
|
|
54
|
+
|
|
36
55
|
// Misc
|
|
37
56
|
'Vagrantfile': 'ruby',
|
|
38
57
|
'Rakefile': 'ruby',
|
|
@@ -106,7 +125,7 @@ export function getLanguageFromExtension(ext) {
|
|
|
106
125
|
// Shell
|
|
107
126
|
'.sh': 'bash',
|
|
108
127
|
'.bash': 'bash',
|
|
109
|
-
'.zsh': '
|
|
128
|
+
'.zsh': 'bash',
|
|
110
129
|
'.fish': 'shell',
|
|
111
130
|
|
|
112
131
|
// SQL
|
|
@@ -149,3 +168,76 @@ export function isSupportedExtension(ext) {
|
|
|
149
168
|
|
|
150
169
|
return supportedExtensions.includes(ext.toLowerCase());
|
|
151
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* バイナリファイルの拡張子かどうかをチェック
|
|
174
|
+
* @param {string} ext - ファイル拡張子(.付き)
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
export function isBinaryExtension(ext) {
|
|
178
|
+
const binaryExtensions = [
|
|
179
|
+
// 画像
|
|
180
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif',
|
|
181
|
+
// 動画
|
|
182
|
+
'.mp4', '.avi', '.mov', '.webm', '.mkv', '.flv', '.wmv',
|
|
183
|
+
// 音声
|
|
184
|
+
'.mp3', '.wav', '.ogg', '.flac', '.aac', '.wma',
|
|
185
|
+
// 圧縮
|
|
186
|
+
'.zip', '.tar', '.gz', '.7z', '.rar', '.bz2', '.xz',
|
|
187
|
+
// 実行・ライブラリ
|
|
188
|
+
'.exe', '.dll', '.so', '.dylib', '.bin', '.class',
|
|
189
|
+
// フォント
|
|
190
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
191
|
+
// ドキュメント(バイナリ形式)
|
|
192
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
193
|
+
// データベース
|
|
194
|
+
'.sqlite', '.db',
|
|
195
|
+
// その他
|
|
196
|
+
'.DS_Store',
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
return binaryExtensions.includes(ext.toLowerCase());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* highlight.js 言語名から表示用の言語名を取得
|
|
204
|
+
* @param {string} language - highlight.js の言語名
|
|
205
|
+
* @returns {string} - 表示用の言語名
|
|
206
|
+
*/
|
|
207
|
+
export function getLanguageDisplayName(language) {
|
|
208
|
+
if (!language) return 'Plain Text';
|
|
209
|
+
|
|
210
|
+
const displayNames = {
|
|
211
|
+
'javascript': 'JavaScript',
|
|
212
|
+
'typescript': 'TypeScript',
|
|
213
|
+
'python': 'Python',
|
|
214
|
+
'json': 'JSON',
|
|
215
|
+
'html': 'HTML',
|
|
216
|
+
'css': 'CSS',
|
|
217
|
+
'scss': 'SCSS',
|
|
218
|
+
'less': 'Less',
|
|
219
|
+
'xml': 'XML',
|
|
220
|
+
'yaml': 'YAML',
|
|
221
|
+
'toml': 'TOML',
|
|
222
|
+
'bash': 'Bash',
|
|
223
|
+
'zsh': 'Zsh',
|
|
224
|
+
'shell': 'Shell',
|
|
225
|
+
'sql': 'SQL',
|
|
226
|
+
'php': 'PHP',
|
|
227
|
+
'cpp': 'C++',
|
|
228
|
+
'c': 'C',
|
|
229
|
+
'go': 'Go',
|
|
230
|
+
'rust': 'Rust',
|
|
231
|
+
'java': 'Java',
|
|
232
|
+
'ruby': 'Ruby',
|
|
233
|
+
'erb': 'ERB',
|
|
234
|
+
'makefile': 'Makefile',
|
|
235
|
+
'dockerfile': 'Dockerfile',
|
|
236
|
+
'cmake': 'CMake',
|
|
237
|
+
'ini': 'INI',
|
|
238
|
+
'markdown': 'Markdown',
|
|
239
|
+
'plaintext': 'Plain Text'
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return displayNames[language] || language.charAt(0).toUpperCase() + language.slice(1);
|
|
243
|
+
}
|
package/templates/page.html
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
<link rel="stylesheet" href="/static/styles/base.css">
|
|
13
13
|
<link rel="stylesheet" href="/static/styles/modern.css">
|
|
14
14
|
<link rel="stylesheet"
|
|
15
|
-
href="https://cdn.jsdelivr.net/npm/
|
|
15
|
+
href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github.min.css"
|
|
16
|
+
media="(prefers-color-scheme: light)">
|
|
17
|
+
<link rel="stylesheet"
|
|
18
|
+
href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css"
|
|
19
|
+
media="(prefers-color-scheme: dark)">
|
|
16
20
|
|
|
17
21
|
<!-- MathJax 設定 -->
|
|
18
22
|
<script>
|
|
@@ -100,7 +104,8 @@
|
|
|
100
104
|
|
|
101
105
|
<!-- クライアント側ライブラリ -->
|
|
102
106
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
103
|
-
<script src="https://cdn.jsdelivr.net/npm/highlight.js
|
|
107
|
+
<script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/highlight.min.js"></script>
|
|
108
|
+
<script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/languages/dockerfile.min.js"></script>
|
|
104
109
|
<script type="module">
|
|
105
110
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
106
111
|
window.mermaid = mermaid;
|