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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-lv",
3
- "version": "1.2.6",
3
+ "version": "1.3.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": {
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
  };
@@ -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 filename = currentSpan.textContent.trim();
611
- if (!filename) return;
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 filename';
618
- copyButton.setAttribute('aria-label', 'Copy filename');
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(filename);
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 = filename;
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
  */
@@ -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, isSupportedExtension } from '../utils/language.js';
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
- const fileName = path.basename(requestPath);
27
- const filenameLanguage = getLanguageFromFilename(fileName);
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
- const content = await fs.readFile(filePath, 'utf-8');
61
- // 拡張子ベースの言語判定を優先し、なければファイル名ベース
62
- const language = isSupportedExtension(ext) ? getLanguageFromExtension(ext) : (filenameLanguage || 'plaintext');
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: `<h1>${escapeHtml(fileName)}</h1>
68
- <pre><code class="language-${language}">${escapeHtml(content)}</code></pre>`,
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
 
@@ -26,13 +26,32 @@ export function getLanguageFromFilename(filename) {
26
26
  // Shell config
27
27
  '.bashrc': 'bash',
28
28
  '.bash_profile': 'bash',
29
- '.zshrc': 'zsh',
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': '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
+ }
@@ -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/highlight.js@11/styles/github.min.css">
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@11"></script>
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;