reviw 0.7.1 → 0.9.1

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.
Files changed (3) hide show
  1. package/README.md +7 -7
  2. package/cli.cjs +663 -323
  3. package/package.json +1 -1
package/cli.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * Lightweight CSV/Text/Markdown viewer with comment collection server
4
4
  *
5
5
  * Usage:
6
- * reviw <file...> [--port 3000] [--encoding utf8|shift_jis|...] [--no-open]
6
+ * reviw <file...> [--port 4989] [--encoding utf8|shift_jis|...] [--no-open]
7
7
  *
8
8
  * Multiple files can be specified. Each file opens on a separate port.
9
9
  * Click cells in the browser to add comments.
@@ -11,20 +11,20 @@
11
11
  * When all files are closed, outputs combined YAML to stdout and exits.
12
12
  */
13
13
 
14
- const fs = require('fs');
15
- const http = require('http');
16
- const path = require('path');
17
- const { spawn } = require('child_process');
18
- const chardet = require('chardet');
19
- const iconv = require('iconv-lite');
20
- const marked = require('marked');
21
- const yaml = require('js-yaml');
14
+ const fs = require("fs");
15
+ const http = require("http");
16
+ const path = require("path");
17
+ const { spawn } = require("child_process");
18
+ const chardet = require("chardet");
19
+ const iconv = require("iconv-lite");
20
+ const marked = require("marked");
21
+ const yaml = require("js-yaml");
22
22
 
23
23
  // --- CLI arguments ---------------------------------------------------------
24
24
  const args = process.argv.slice(2);
25
25
 
26
26
  const filePaths = [];
27
- let basePort = 3000;
27
+ let basePort = 4989;
28
28
  let encodingOpt = null;
29
29
  let noOpen = false;
30
30
  let stdinMode = false;
@@ -33,21 +33,21 @@ let stdinContent = null;
33
33
 
34
34
  for (let i = 0; i < args.length; i += 1) {
35
35
  const arg = args[i];
36
- if (arg === '--port' && args[i + 1]) {
36
+ if (arg === "--port" && args[i + 1]) {
37
37
  basePort = Number(args[i + 1]);
38
38
  i += 1;
39
- } else if ((arg === '--encoding' || arg === '-e') && args[i + 1]) {
39
+ } else if ((arg === "--encoding" || arg === "-e") && args[i + 1]) {
40
40
  encodingOpt = args[i + 1];
41
41
  i += 1;
42
- } else if (arg === '--no-open') {
42
+ } else if (arg === "--no-open") {
43
43
  noOpen = true;
44
- } else if (arg === '--help' || arg === '-h') {
44
+ } else if (arg === "--help" || arg === "-h") {
45
45
  console.log(`Usage: reviw <file...> [options]
46
46
  git diff | reviw [options]
47
47
  reviw [options] (auto runs git diff HEAD)
48
48
 
49
49
  Options:
50
- --port <number> Server port (default: 3000)
50
+ --port <number> Server port (default: 4989)
51
51
  --encoding <enc> Force encoding (utf8, shift_jis, etc.)
52
52
  --no-open Don't open browser automatically
53
53
  --help, -h Show this help message
@@ -59,7 +59,7 @@ Examples:
59
59
  git diff HEAD~3 | reviw # Review diff from last 3 commits
60
60
  reviw # Auto run git diff HEAD`);
61
61
  process.exit(0);
62
- } else if (!arg.startsWith('-')) {
62
+ } else if (!arg.startsWith("-")) {
63
63
  filePaths.push(arg);
64
64
  }
65
65
  }
@@ -71,7 +71,7 @@ async function checkStdin() {
71
71
  resolve(false);
72
72
  return;
73
73
  }
74
- let data = '';
74
+ let data = "";
75
75
  let resolved = false;
76
76
  const timeout = setTimeout(() => {
77
77
  if (!resolved) {
@@ -79,18 +79,18 @@ async function checkStdin() {
79
79
  resolve(data.length > 0 ? data : false);
80
80
  }
81
81
  }, 100);
82
- process.stdin.setEncoding('utf8');
83
- process.stdin.on('data', (chunk) => {
82
+ process.stdin.setEncoding("utf8");
83
+ process.stdin.on("data", (chunk) => {
84
84
  data += chunk;
85
85
  });
86
- process.stdin.on('end', () => {
86
+ process.stdin.on("end", () => {
87
87
  clearTimeout(timeout);
88
88
  if (!resolved) {
89
89
  resolved = true;
90
90
  resolve(data.length > 0 ? data : false);
91
91
  }
92
92
  });
93
- process.stdin.on('error', () => {
93
+ process.stdin.on("error", () => {
94
94
  clearTimeout(timeout);
95
95
  if (!resolved) {
96
96
  resolved = true;
@@ -103,15 +103,15 @@ async function checkStdin() {
103
103
  // Run git diff HEAD if no files and no stdin
104
104
  function runGitDiff() {
105
105
  return new Promise((resolve, reject) => {
106
- const { execSync } = require('child_process');
106
+ const { execSync } = require("child_process");
107
107
  try {
108
108
  // Check if we're in a git repo
109
- execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
109
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
110
110
  // Run git diff HEAD
111
- const diff = execSync('git diff HEAD', { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
111
+ const diff = execSync("git diff HEAD", { encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
112
112
  resolve(diff);
113
113
  } catch (err) {
114
- reject(new Error('Not a git repository or git command failed'));
114
+ reject(new Error("Not a git repository or git command failed"));
115
115
  }
116
116
  });
117
117
  }
@@ -130,7 +130,7 @@ for (const fp of filePaths) {
130
130
  // --- Diff parsing -----------------------------------------------------------
131
131
  function parseDiff(diffText) {
132
132
  const files = [];
133
- const lines = diffText.split('\n');
133
+ const lines = diffText.split("\n");
134
134
  let currentFile = null;
135
135
  let lineNumber = 0;
136
136
 
@@ -138,16 +138,16 @@ function parseDiff(diffText) {
138
138
  const line = lines[i];
139
139
 
140
140
  // New file header
141
- if (line.startsWith('diff --git')) {
141
+ if (line.startsWith("diff --git")) {
142
142
  if (currentFile) files.push(currentFile);
143
143
  const match = line.match(/diff --git a\/(.+?) b\/(.+)/);
144
144
  currentFile = {
145
- oldPath: match ? match[1] : '',
146
- newPath: match ? match[2] : '',
145
+ oldPath: match ? match[1] : "",
146
+ newPath: match ? match[2] : "",
147
147
  hunks: [],
148
148
  isNew: false,
149
149
  isDeleted: false,
150
- isBinary: false
150
+ isBinary: false,
151
151
  };
152
152
  lineNumber = 0;
153
153
  continue;
@@ -156,47 +156,47 @@ function parseDiff(diffText) {
156
156
  if (!currentFile) continue;
157
157
 
158
158
  // File mode info
159
- if (line.startsWith('new file mode')) {
159
+ if (line.startsWith("new file mode")) {
160
160
  currentFile.isNew = true;
161
161
  continue;
162
162
  }
163
- if (line.startsWith('deleted file mode')) {
163
+ if (line.startsWith("deleted file mode")) {
164
164
  currentFile.isDeleted = true;
165
165
  continue;
166
166
  }
167
- if (line.startsWith('Binary files')) {
167
+ if (line.startsWith("Binary files")) {
168
168
  currentFile.isBinary = true;
169
169
  continue;
170
170
  }
171
171
 
172
172
  // Hunk header
173
- if (line.startsWith('@@')) {
173
+ if (line.startsWith("@@")) {
174
174
  const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)/);
175
175
  if (match) {
176
176
  currentFile.hunks.push({
177
177
  oldStart: parseInt(match[1], 10),
178
178
  newStart: parseInt(match[2], 10),
179
- context: match[3] || '',
180
- lines: []
179
+ context: match[3] || "",
180
+ lines: [],
181
181
  });
182
182
  }
183
183
  continue;
184
184
  }
185
185
 
186
186
  // Skip other headers
187
- if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('index ')) {
187
+ if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("index ")) {
188
188
  continue;
189
189
  }
190
190
 
191
191
  // Diff content
192
192
  if (currentFile.hunks.length > 0) {
193
193
  const hunk = currentFile.hunks[currentFile.hunks.length - 1];
194
- if (line.startsWith('+')) {
195
- hunk.lines.push({ type: 'add', content: line.slice(1), lineNum: ++lineNumber });
196
- } else if (line.startsWith('-')) {
197
- hunk.lines.push({ type: 'del', content: line.slice(1), lineNum: ++lineNumber });
198
- } else if (line.startsWith(' ') || line === '') {
199
- hunk.lines.push({ type: 'ctx', content: line.slice(1) || '', lineNum: ++lineNumber });
194
+ if (line.startsWith("+")) {
195
+ hunk.lines.push({ type: "add", content: line.slice(1), lineNum: ++lineNumber });
196
+ } else if (line.startsWith("-")) {
197
+ hunk.lines.push({ type: "del", content: line.slice(1), lineNum: ++lineNumber });
198
+ } else if (line.startsWith(" ") || line === "") {
199
+ hunk.lines.push({ type: "ctx", content: line.slice(1) || "", lineNum: ++lineNumber });
200
200
  }
201
201
  }
202
202
  }
@@ -235,18 +235,18 @@ function loadDiff(diffText) {
235
235
  sortedFiles.forEach((file, fileIdx) => {
236
236
  // File header row
237
237
  let label = file.newPath || file.oldPath;
238
- if (file.isNew) label += ' (new)';
239
- if (file.isDeleted) label += ' (deleted)';
240
- if (file.isBinary) label += ' (binary)';
238
+ if (file.isNew) label += " (new)";
239
+ if (file.isDeleted) label += " (deleted)";
240
+ if (file.isBinary) label += " (binary)";
241
241
  rows.push({
242
- type: 'file',
242
+ type: "file",
243
243
  content: label,
244
244
  filePath: file.newPath || file.oldPath,
245
245
  fileIndex: fileIdx,
246
246
  lineCount: file.lineCount,
247
247
  collapsed: file.collapsed,
248
248
  isBinary: file.isBinary,
249
- rowIndex: rowIndex++
249
+ rowIndex: rowIndex++,
250
250
  });
251
251
 
252
252
  if (file.isBinary) return;
@@ -254,10 +254,10 @@ function loadDiff(diffText) {
254
254
  file.hunks.forEach((hunk) => {
255
255
  // Hunk header
256
256
  rows.push({
257
- type: 'hunk',
257
+ type: "hunk",
258
258
  content: `@@ -${hunk.oldStart} +${hunk.newStart} @@${hunk.context}`,
259
259
  fileIndex: fileIdx,
260
- rowIndex: rowIndex++
260
+ rowIndex: rowIndex++,
261
261
  });
262
262
 
263
263
  hunk.lines.forEach((line) => {
@@ -265,7 +265,7 @@ function loadDiff(diffText) {
265
265
  type: line.type,
266
266
  content: line.content,
267
267
  fileIndex: fileIdx,
268
- rowIndex: rowIndex++
268
+ rowIndex: rowIndex++,
269
269
  });
270
270
  });
271
271
  });
@@ -274,8 +274,8 @@ function loadDiff(diffText) {
274
274
  return {
275
275
  rows,
276
276
  files: sortedFiles,
277
- title: 'Git Diff',
278
- mode: 'diff'
277
+ title: "Git Diff",
278
+ mode: "diff",
279
279
  };
280
280
  }
281
281
 
@@ -285,10 +285,10 @@ let serversRunning = 0;
285
285
  let nextPort = basePort;
286
286
 
287
287
  // --- Simple CSV/TSV parser (RFC4180-style, handles " escaping and newlines) ----
288
- function parseCsv(text, separator = ',') {
288
+ function parseCsv(text, separator = ",") {
289
289
  const rows = [];
290
290
  let row = [];
291
- let field = '';
291
+ let field = "";
292
292
  let inQuotes = false;
293
293
 
294
294
  for (let i = 0; i < text.length; i += 1) {
@@ -309,13 +309,13 @@ function parseCsv(text, separator = ',') {
309
309
  inQuotes = true;
310
310
  } else if (ch === separator) {
311
311
  row.push(field);
312
- field = '';
313
- } else if (ch === '\n') {
312
+ field = "";
313
+ } else if (ch === "\n") {
314
314
  row.push(field);
315
315
  rows.push(row);
316
316
  row = [];
317
- field = '';
318
- } else if (ch === '\r') {
317
+ field = "";
318
+ } else if (ch === "\r") {
319
319
  // Ignore CR (for CRLF handling)
320
320
  } else {
321
321
  field += ch;
@@ -327,7 +327,7 @@ function parseCsv(text, separator = ',') {
327
327
 
328
328
  // Remove trailing empty row if present
329
329
  const last = rows[rows.length - 1];
330
- if (last && last.every((v) => v === '')) {
330
+ if (last && last.every((v) => v === "")) {
331
331
  rows.pop();
332
332
  }
333
333
 
@@ -335,15 +335,15 @@ function parseCsv(text, separator = ',') {
335
335
  }
336
336
 
337
337
  const ENCODING_MAP = {
338
- 'utf-8': 'utf8',
339
- utf8: 'utf8',
340
- 'shift_jis': 'shift_jis',
341
- sjis: 'shift_jis',
342
- 'windows-31j': 'cp932',
343
- cp932: 'cp932',
344
- 'euc-jp': 'euc-jp',
345
- 'iso-8859-1': 'latin1',
346
- latin1: 'latin1'
338
+ "utf-8": "utf8",
339
+ utf8: "utf8",
340
+ shift_jis: "shift_jis",
341
+ sjis: "shift_jis",
342
+ "windows-31j": "cp932",
343
+ cp932: "cp932",
344
+ "euc-jp": "euc-jp",
345
+ "iso-8859-1": "latin1",
346
+ latin1: "latin1",
347
347
  };
348
348
 
349
349
  function normalizeEncoding(name) {
@@ -356,9 +356,9 @@ function decodeBuffer(buf) {
356
356
  const specified = normalizeEncoding(encodingOpt);
357
357
  let encoding = specified;
358
358
  if (!encoding) {
359
- const detected = chardet.detect(buf) || '';
360
- encoding = normalizeEncoding(detected) || 'utf8';
361
- if (encoding !== 'utf8') {
359
+ const detected = chardet.detect(buf) || "";
360
+ encoding = normalizeEncoding(detected) || "utf8";
361
+ if (encoding !== "utf8") {
362
362
  console.log(`Detected encoding: ${detected} -> ${encoding}`);
363
363
  }
364
364
  }
@@ -366,7 +366,7 @@ function decodeBuffer(buf) {
366
366
  return iconv.decode(buf, encoding);
367
367
  } catch (err) {
368
368
  console.warn(`Decode failed (${encoding}): ${err.message}, falling back to utf8`);
369
- return buf.toString('utf8');
369
+ return buf.toString("utf8");
370
370
  }
371
371
  }
372
372
 
@@ -374,8 +374,8 @@ function loadCsv(filePath) {
374
374
  const raw = fs.readFileSync(filePath);
375
375
  const csvText = decodeBuffer(raw);
376
376
  const ext = path.extname(filePath).toLowerCase();
377
- const separator = ext === '.tsv' ? '\t' : ',';
378
- if (!csvText.includes('\n') && !csvText.includes(separator)) {
377
+ const separator = ext === ".tsv" ? "\t" : ",";
378
+ if (!csvText.includes("\n") && !csvText.includes(separator)) {
379
379
  // heuristic: if no newline/separators, still treat as single row
380
380
  }
381
381
  const rows = parseCsv(csvText, separator);
@@ -383,7 +383,7 @@ function loadCsv(filePath) {
383
383
  return {
384
384
  rows,
385
385
  cols: Math.max(1, maxCols),
386
- title: path.basename(filePath)
386
+ title: path.basename(filePath),
387
387
  };
388
388
  }
389
389
 
@@ -395,7 +395,7 @@ function loadText(filePath) {
395
395
  rows: lines.map((line) => [line]),
396
396
  cols: 1,
397
397
  title: path.basename(filePath),
398
- preview: null
398
+ preview: null,
399
399
  };
400
400
  }
401
401
 
@@ -403,33 +403,97 @@ function loadMarkdown(filePath) {
403
403
  const raw = fs.readFileSync(filePath);
404
404
  const text = decodeBuffer(raw);
405
405
  const lines = text.split(/\r?\n/);
406
- const preview = marked.parse(text, { breaks: true });
406
+
407
+ // Parse YAML frontmatter
408
+ let frontmatterHtml = "";
409
+ let contentStart = 0;
410
+
411
+ if (lines[0] && lines[0].trim() === "---") {
412
+ let frontmatterEnd = -1;
413
+ for (let i = 1; i < lines.length; i++) {
414
+ if (lines[i].trim() === "---") {
415
+ frontmatterEnd = i;
416
+ break;
417
+ }
418
+ }
419
+
420
+ if (frontmatterEnd > 0) {
421
+ const frontmatterLines = lines.slice(1, frontmatterEnd);
422
+ const frontmatterText = frontmatterLines.join("\n");
423
+
424
+ try {
425
+ const frontmatter = yaml.load(frontmatterText);
426
+ if (frontmatter && typeof frontmatter === "object") {
427
+ // Create HTML table for frontmatter
428
+ frontmatterHtml = '<div class="frontmatter-table"><table>';
429
+ frontmatterHtml += '<colgroup><col style="width:12%"><col style="width:88%"></colgroup>';
430
+ frontmatterHtml += '<thead><tr><th colspan="2">Document Metadata</th></tr></thead>';
431
+ frontmatterHtml += "<tbody>";
432
+
433
+ function renderValue(val) {
434
+ if (Array.isArray(val)) {
435
+ return val
436
+ .map((v) => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + "</span>")
437
+ .join(" ");
438
+ }
439
+ if (typeof val === "object" && val !== null) {
440
+ return "<pre>" + escapeHtmlChars(JSON.stringify(val, null, 2)) + "</pre>";
441
+ }
442
+ return escapeHtmlChars(String(val));
443
+ }
444
+
445
+ for (const [key, val] of Object.entries(frontmatter)) {
446
+ frontmatterHtml +=
447
+ "<tr><th>" + escapeHtmlChars(key) + "</th><td>" + renderValue(val) + "</td></tr>";
448
+ }
449
+
450
+ frontmatterHtml += "</tbody></table></div>";
451
+ contentStart = frontmatterEnd + 1;
452
+ }
453
+ } catch (e) {
454
+ // Invalid YAML, skip frontmatter rendering
455
+ }
456
+ }
457
+ }
458
+
459
+ // Parse markdown content (without frontmatter)
460
+ const contentText = lines.slice(contentStart).join("\n");
461
+ const preview = frontmatterHtml + marked.parse(contentText, { breaks: true });
462
+
407
463
  return {
408
464
  rows: lines.map((line) => [line]),
409
465
  cols: 1,
410
466
  title: path.basename(filePath),
411
- preview
467
+ preview,
412
468
  };
413
469
  }
414
470
 
471
+ function escapeHtmlChars(str) {
472
+ return str
473
+ .replace(/&/g, "&amp;")
474
+ .replace(/</g, "&lt;")
475
+ .replace(/>/g, "&gt;")
476
+ .replace(/"/g, "&quot;");
477
+ }
478
+
415
479
  function loadData(filePath) {
416
480
  const ext = path.extname(filePath).toLowerCase();
417
- if (ext === '.csv' || ext === '.tsv') {
481
+ if (ext === ".csv" || ext === ".tsv") {
418
482
  const data = loadCsv(filePath);
419
- return { ...data, mode: 'csv' };
483
+ return { ...data, mode: "csv" };
420
484
  }
421
- if (ext === '.md' || ext === '.markdown') {
485
+ if (ext === ".md" || ext === ".markdown") {
422
486
  const data = loadMarkdown(filePath);
423
- return { ...data, mode: 'markdown' };
487
+ return { ...data, mode: "markdown" };
424
488
  }
425
- if (ext === '.diff' || ext === '.patch') {
426
- const content = fs.readFileSync(filePath, 'utf8');
489
+ if (ext === ".diff" || ext === ".patch") {
490
+ const content = fs.readFileSync(filePath, "utf8");
427
491
  const data = loadDiff(content);
428
- return { ...data, mode: 'diff' };
492
+ return { ...data, mode: "diff" };
429
493
  }
430
494
  // default text
431
495
  const data = loadText(filePath);
432
- return { ...data, mode: 'text' };
496
+ return { ...data, mode: "text" };
433
497
  }
434
498
 
435
499
  // --- Safe JSON serialization for inline scripts ---------------------------
@@ -437,18 +501,18 @@ function loadData(filePath) {
437
501
  // the original values intact once parsed by JS.
438
502
  function serializeForScript(value) {
439
503
  return JSON.stringify(value)
440
- .replace(/</g, '\\u003c') // avoid closing the script tag
441
- .replace(/>/g, '\\u003e')
442
- .replace(/\u2028/g, '\\u2028') // line separator
443
- .replace(/\u2029/g, '\\u2029') // paragraph separator
444
- .replace(/`/g, '\\`') // keep template literal boundaries safe
445
- .replace(/\$\{/g, '\\${');
504
+ .replace(/</g, "\\u003c") // avoid closing the script tag
505
+ .replace(/>/g, "\\u003e")
506
+ .replace(/\u2028/g, "\\u2028") // line separator
507
+ .replace(/\u2029/g, "\\u2029") // paragraph separator
508
+ .replace(/`/g, "\\`") // keep template literal boundaries safe
509
+ .replace(/\$\{/g, "\\${");
446
510
  }
447
511
 
448
512
  function diffHtmlTemplate(diffData) {
449
513
  const { rows, title } = diffData;
450
514
  const serialized = serializeForScript(rows);
451
- const fileCount = rows.filter(r => r.type === 'file').length;
515
+ const fileCount = rows.filter((r) => r.type === "file").length;
452
516
 
453
517
  return `<!doctype html>
454
518
  <html lang="ja">
@@ -825,7 +889,7 @@ function diffHtmlTemplate(diffData) {
825
889
  <header>
826
890
  <div class="meta">
827
891
  <h1>${title}</h1>
828
- <span class="badge">${fileCount} file${fileCount !== 1 ? 's' : ''} changed</span>
892
+ <span class="badge">${fileCount} file${fileCount !== 1 ? "s" : ""} changed</span>
829
893
  <span class="pill">Comments <strong id="comment-count">0</strong></span>
830
894
  </div>
831
895
  <div class="actions">
@@ -1751,10 +1815,201 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1751
1815
  .md-preview img { max-width: 100%; height: auto; border-radius: 8px; }
1752
1816
  .md-preview code { background: rgba(255,255,255,0.08); padding: 2px 4px; border-radius: 4px; }
1753
1817
  .md-preview pre {
1754
- background: rgba(255,255,255,0.06);
1755
- padding: 8px 10px;
1818
+ background: var(--code-bg);
1819
+ padding: 12px 16px;
1756
1820
  border-radius: 8px;
1757
1821
  overflow: auto;
1822
+ border: 1px solid var(--border);
1823
+ }
1824
+ .md-preview pre code {
1825
+ background: none;
1826
+ padding: 0;
1827
+ font-size: 13px;
1828
+ line-height: 1.5;
1829
+ }
1830
+ .md-preview pre code.hljs {
1831
+ background: transparent;
1832
+ padding: 0;
1833
+ }
1834
+ /* YAML Frontmatter table */
1835
+ .frontmatter-table {
1836
+ margin-bottom: 20px;
1837
+ border-radius: 8px;
1838
+ overflow: hidden;
1839
+ border: 1px solid var(--border);
1840
+ background: var(--panel);
1841
+ }
1842
+ .frontmatter-table table {
1843
+ width: 100%;
1844
+ border-collapse: collapse;
1845
+ table-layout: fixed;
1846
+ }
1847
+ .frontmatter-table thead th {
1848
+ background: linear-gradient(135deg, rgba(147, 51, 234, 0.15), rgba(96, 165, 250, 0.15));
1849
+ color: var(--text);
1850
+ font-size: 12px;
1851
+ font-weight: 600;
1852
+ padding: 10px 16px;
1853
+ text-align: left;
1854
+ border-bottom: 1px solid var(--border);
1855
+ }
1856
+ .frontmatter-table tbody th {
1857
+ background: rgba(147, 51, 234, 0.08);
1858
+ color: #c084fc;
1859
+ font-weight: 500;
1860
+ font-size: 12px;
1861
+ padding: 8px 10px;
1862
+ text-align: left;
1863
+ border-bottom: 1px solid var(--border);
1864
+ vertical-align: top;
1865
+ }
1866
+ .frontmatter-table tbody td {
1867
+ padding: 8px 14px;
1868
+ font-size: 13px;
1869
+ border-bottom: 1px solid var(--border);
1870
+ word-break: break-word;
1871
+ }
1872
+ .frontmatter-table tbody tr:last-child th,
1873
+ .frontmatter-table tbody tr:last-child td {
1874
+ border-bottom: none;
1875
+ }
1876
+ .frontmatter-table .fm-tag {
1877
+ display: inline-block;
1878
+ background: rgba(96, 165, 250, 0.15);
1879
+ color: var(--accent);
1880
+ padding: 2px 8px;
1881
+ border-radius: 12px;
1882
+ font-size: 11px;
1883
+ margin-right: 4px;
1884
+ margin-bottom: 4px;
1885
+ }
1886
+ .frontmatter-table pre {
1887
+ margin: 0;
1888
+ background: var(--code-bg);
1889
+ padding: 8px;
1890
+ border-radius: 4px;
1891
+ font-size: 11px;
1892
+ }
1893
+ [data-theme="light"] .frontmatter-table tbody th {
1894
+ color: #7c3aed;
1895
+ }
1896
+ /* Markdown tables in preview */
1897
+ .md-preview table:not(.frontmatter-table table) {
1898
+ width: 100%;
1899
+ border-collapse: collapse;
1900
+ margin: 16px 0;
1901
+ border: 1px solid var(--border);
1902
+ border-radius: 8px;
1903
+ overflow: hidden;
1904
+ }
1905
+ .md-preview table:not(.frontmatter-table table) th,
1906
+ .md-preview table:not(.frontmatter-table table) td {
1907
+ width: 50%;
1908
+ padding: 10px 16px;
1909
+ text-align: left;
1910
+ border-bottom: 1px solid var(--border);
1911
+ }
1912
+ .md-preview table:not(.frontmatter-table table) th {
1913
+ background: rgba(255,255,255,0.05);
1914
+ }
1915
+ .md-preview table:not(.frontmatter-table table) th {
1916
+ background: var(--panel);
1917
+ font-weight: 600;
1918
+ font-size: 13px;
1919
+ }
1920
+ .md-preview table:not(.frontmatter-table table) td {
1921
+ font-size: 13px;
1922
+ }
1923
+ .md-preview table:not(.frontmatter-table table) tr:last-child td {
1924
+ border-bottom: none;
1925
+ }
1926
+ .md-preview table:not(.frontmatter-table table) tr:hover td {
1927
+ background: var(--hover-bg);
1928
+ }
1929
+ /* Source table (右ペイン) */
1930
+ .table-box table {
1931
+ table-layout: fixed;
1932
+ width: 100%;
1933
+ }
1934
+ .table-box th,
1935
+ .table-box td {
1936
+ word-break: break-word;
1937
+ min-width: 140px;
1938
+ }
1939
+ .table-box th:first-child,
1940
+ .table-box td:first-child {
1941
+ min-width: 320px;
1942
+ max-width: 480px;
1943
+ }
1944
+ /* Image fullscreen overlay */
1945
+ .image-fullscreen-overlay {
1946
+ position: fixed;
1947
+ inset: 0;
1948
+ background: rgba(0, 0, 0, 0.9);
1949
+ z-index: 1001;
1950
+ display: none;
1951
+ justify-content: center;
1952
+ align-items: center;
1953
+ }
1954
+ .image-fullscreen-overlay.visible {
1955
+ display: flex;
1956
+ }
1957
+ .image-close-btn {
1958
+ position: absolute;
1959
+ top: 14px;
1960
+ right: 14px;
1961
+ width: 40px;
1962
+ height: 40px;
1963
+ display: flex;
1964
+ align-items: center;
1965
+ justify-content: center;
1966
+ background: rgba(0, 0, 0, 0.55);
1967
+ border: 1px solid rgba(255, 255, 255, 0.25);
1968
+ border-radius: 50%;
1969
+ cursor: pointer;
1970
+ color: #fff;
1971
+ font-size: 18px;
1972
+ z-index: 10;
1973
+ backdrop-filter: blur(4px);
1974
+ transition: background 120ms ease, transform 120ms ease;
1975
+ }
1976
+ .image-close-btn:hover {
1977
+ background: rgba(0, 0, 0, 0.75);
1978
+ transform: scale(1.04);
1979
+ }
1980
+ .image-container {
1981
+ max-width: 90vw;
1982
+ max-height: 90vh;
1983
+ display: flex;
1984
+ justify-content: center;
1985
+ align-items: center;
1986
+ }
1987
+ .image-container img {
1988
+ max-width: 100%;
1989
+ max-height: 90vh;
1990
+ object-fit: contain;
1991
+ border-radius: 8px;
1992
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
1993
+ }
1994
+ /* Copy notification toast */
1995
+ .copy-toast {
1996
+ position: fixed;
1997
+ bottom: 60px;
1998
+ left: 50%;
1999
+ transform: translateX(-50%) translateY(20px);
2000
+ background: var(--accent);
2001
+ color: var(--text-inverse);
2002
+ padding: 8px 16px;
2003
+ border-radius: 8px;
2004
+ font-size: 13px;
2005
+ opacity: 0;
2006
+ pointer-events: none;
2007
+ transition: opacity 200ms ease, transform 200ms ease;
2008
+ z-index: 1000;
2009
+ }
2010
+ .copy-toast.visible {
2011
+ opacity: 1;
2012
+ transform: translateX(-50%) translateY(0);
1758
2013
  }
1759
2014
  @media (max-width: 960px) {
1760
2015
  .md-layout { flex-direction: column; }
@@ -1859,6 +2114,8 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1859
2114
  .mermaid-container .mermaid svg {
1860
2115
  max-width: 100%;
1861
2116
  height: auto;
2117
+ cursor: pointer;
2118
+ pointer-events: auto;
1862
2119
  }
1863
2120
  .mermaid-fullscreen-btn {
1864
2121
  position: absolute;
@@ -1923,39 +2180,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1923
2180
  .fullscreen-content .mermaid svg {
1924
2181
  display: block;
1925
2182
  }
1926
- /* Minimap */
1927
- .minimap {
1928
- position: absolute;
1929
- top: 70px;
1930
- right: 20px;
1931
- width: 200px;
1932
- height: 150px;
1933
- background: var(--panel-alpha);
1934
- border: 1px solid var(--border);
1935
- border-radius: 8px;
1936
- overflow: hidden;
1937
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1938
- }
1939
- .minimap-content {
1940
- width: 100%;
1941
- height: 100%;
1942
- display: flex;
1943
- align-items: center;
1944
- justify-content: center;
1945
- padding: 8px;
1946
- }
1947
- .minimap-content svg {
1948
- max-width: 100%;
1949
- max-height: 100%;
1950
- opacity: 0.6;
1951
- }
1952
- .minimap-viewport {
1953
- position: absolute;
1954
- border: 2px solid var(--accent);
1955
- background: rgba(102, 126, 234, 0.2);
1956
- pointer-events: none;
1957
- border-radius: 2px;
1958
- }
1959
2183
  /* Error toast */
1960
2184
  .mermaid-error-toast {
1961
2185
  position: fixed;
@@ -1977,6 +2201,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1977
2201
  .mermaid-error-toast.visible { display: block; }
1978
2202
  </style>
1979
2203
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
2204
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css" id="hljs-theme-dark">
2205
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css" id="hljs-theme-light" disabled>
2206
+ <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
1980
2207
  </head>
1981
2208
  <body>
1982
2209
  <header>
@@ -1994,8 +2221,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
1994
2221
  </header>
1995
2222
 
1996
2223
  <div class="wrap">
1997
- ${hasPreview && mode === 'markdown'
1998
- ? `<div class="md-layout">
2224
+ ${
2225
+ hasPreview && mode === "markdown"
2226
+ ? `<div class="md-layout">
1999
2227
  <div class="md-left">
2000
2228
  <div class="md-preview">${previewHtml}</div>
2001
2229
  </div>
@@ -2006,7 +2234,12 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2006
2234
  <thead>
2007
2235
  <tr>
2008
2236
  <th aria-label="row/col corner"></th>
2009
- ${Array.from({ length: cols }).map((_, i) => `<th data-col="${i + 1}"><div class="th-inner">${mode === 'csv' ? `C${i + 1}` : 'Text'}<span class="resizer" data-col="${i + 1}"></span></div></th>`).join('')}
2237
+ ${Array.from({ length: cols })
2238
+ .map(
2239
+ (_, i) =>
2240
+ `<th data-col="${i + 1}"><div class="th-inner">${mode === "csv" ? `C${i + 1}` : "Text"}<span class="resizer" data-col="${i + 1}"></span></div></th>`,
2241
+ )
2242
+ .join("")}
2010
2243
  </tr>
2011
2244
  </thead>
2012
2245
  <tbody id="tbody"></tbody>
@@ -2014,8 +2247,8 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2014
2247
  </div>
2015
2248
  </div>
2016
2249
  </div>`
2017
- : `
2018
- ${hasPreview ? `<div class="md-preview">${previewHtml}</div>` : ''}
2250
+ : `
2251
+ ${hasPreview ? `<div class="md-preview">${previewHtml}</div>` : ""}
2019
2252
  <div class="toolbar">
2020
2253
  <button id="fit-width">Fit to width</button>
2021
2254
  <span>Drag header edge to resize columns</span>
@@ -2026,13 +2259,19 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2026
2259
  <thead>
2027
2260
  <tr>
2028
2261
  <th aria-label="row/col corner"></th>
2029
- ${Array.from({ length: cols }).map((_, i) => `<th data-col="${i + 1}"><div class="th-inner">${mode === 'csv' ? `C${i + 1}` : 'Text'}<span class="resizer" data-col="${i + 1}"></span></div></th>`).join('')}
2262
+ ${Array.from({ length: cols })
2263
+ .map(
2264
+ (_, i) =>
2265
+ `<th data-col="${i + 1}"><div class="th-inner">${mode === "csv" ? `C${i + 1}` : "Text"}<span class="resizer" data-col="${i + 1}"></span></div></th>`,
2266
+ )
2267
+ .join("")}
2030
2268
  </tr>
2031
2269
  </thead>
2032
2270
  <tbody id="tbody"></tbody>
2033
2271
  </table>
2034
2272
  </div>
2035
- `}
2273
+ `
2274
+ }
2036
2275
  </div>
2037
2276
 
2038
2277
  <div class="floating" id="comment-card">
@@ -2106,12 +2345,13 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2106
2345
  <div class="fullscreen-content" id="fs-content">
2107
2346
  <div class="mermaid-wrapper" id="fs-wrapper"></div>
2108
2347
  </div>
2109
- <div class="minimap" id="fs-minimap">
2110
- <div class="minimap-content" id="fs-minimap-content"></div>
2111
- <div class="minimap-viewport" id="fs-minimap-viewport"></div>
2112
- </div>
2113
2348
  </div>
2114
2349
  <div class="mermaid-error-toast" id="mermaid-error-toast"></div>
2350
+ <div class="copy-toast" id="copy-toast">Copied to clipboard!</div>
2351
+ <div class="image-fullscreen-overlay" id="image-fullscreen">
2352
+ <button class="image-close-btn" id="image-close" aria-label="Close image" title="Close (ESC)">✕</button>
2353
+ <div class="image-container" id="image-container"></div>
2354
+ </div>
2115
2355
 
2116
2356
  <script>
2117
2357
  const DATA = ${serialized};
@@ -2133,12 +2373,18 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2133
2373
  }
2134
2374
 
2135
2375
  function setTheme(theme) {
2376
+ const hljsDark = document.getElementById('hljs-theme-dark');
2377
+ const hljsLight = document.getElementById('hljs-theme-light');
2136
2378
  if (theme === 'light') {
2137
2379
  document.documentElement.setAttribute('data-theme', 'light');
2138
2380
  themeIcon.textContent = '☀️';
2381
+ if (hljsDark) hljsDark.disabled = true;
2382
+ if (hljsLight) hljsLight.disabled = false;
2139
2383
  } else {
2140
2384
  document.documentElement.removeAttribute('data-theme');
2141
2385
  themeIcon.textContent = '🌙';
2386
+ if (hljsDark) hljsDark.disabled = false;
2387
+ if (hljsLight) hljsLight.disabled = true;
2142
2388
  }
2143
2389
  localStorage.setItem('reviw-theme', theme);
2144
2390
  }
@@ -2182,11 +2428,14 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2182
2428
  const freezeRowCheck = document.getElementById('freeze-row-check');
2183
2429
 
2184
2430
  const ROW_HEADER_WIDTH = 28;
2185
- const MIN_COL_WIDTH = 80;
2186
- const MAX_COL_WIDTH = 420;
2187
- const DEFAULT_COL_WIDTH = 120;
2431
+ const MIN_COL_WIDTH = 140;
2432
+ const MAX_COL_WIDTH = 520;
2433
+ const DEFAULT_COL_WIDTH = 240;
2188
2434
 
2189
2435
  let colWidths = Array.from({ length: MAX_COLS }, () => DEFAULT_COL_WIDTH);
2436
+ if (MODE !== 'csv' && MAX_COLS === 1) {
2437
+ colWidths[0] = 480;
2438
+ }
2190
2439
  let panelOpen = false;
2191
2440
  let filters = {}; // colIndex -> predicate
2192
2441
  let filterTargetCol = null;
@@ -2343,6 +2592,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2343
2592
  const th = document.createElement('th');
2344
2593
  th.textContent = rIdx + 1;
2345
2594
  tr.appendChild(th);
2595
+
2346
2596
  for (let c = 0; c < MAX_COLS; c += 1) {
2347
2597
  const td = document.createElement('td');
2348
2598
  const val = row[c] || '';
@@ -2422,11 +2672,48 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2422
2672
  isDragging = false;
2423
2673
  document.body.classList.remove('dragging');
2424
2674
  if (selection) {
2675
+ // Copy selected text to clipboard
2676
+ copySelectionToClipboard(selection);
2425
2677
  openCardForSelection();
2426
2678
  }
2427
2679
  });
2428
2680
  }
2429
2681
 
2682
+ // Copy selected range to clipboard
2683
+ function copySelectionToClipboard(sel) {
2684
+ const { startRow, endRow, startCol, endCol } = sel;
2685
+ const lines = [];
2686
+ for (let r = startRow; r <= endRow; r++) {
2687
+ const rowData = [];
2688
+ for (let c = startCol; c <= endCol; c++) {
2689
+ const td = tbody.querySelector('td[data-row="' + r + '"][data-col="' + c + '"]');
2690
+ if (td) {
2691
+ // Get text content (strip HTML tags from inline code highlighting)
2692
+ rowData.push(td.textContent || '');
2693
+ }
2694
+ }
2695
+ lines.push(rowData.join('\\t'));
2696
+ }
2697
+ const text = lines.join('\\n');
2698
+ if (text && navigator.clipboard) {
2699
+ navigator.clipboard.writeText(text).then(() => {
2700
+ showCopyToast();
2701
+ }).catch(() => {
2702
+ // Fallback: silent fail
2703
+ });
2704
+ }
2705
+ }
2706
+
2707
+ // Show copy toast notification
2708
+ function showCopyToast() {
2709
+ const toast = document.getElementById('copy-toast');
2710
+ if (!toast) return;
2711
+ toast.classList.add('visible');
2712
+ setTimeout(() => {
2713
+ toast.classList.remove('visible');
2714
+ }, 1500);
2715
+ }
2716
+
2430
2717
  function openCardForSelection() {
2431
2718
  if (!selection) return;
2432
2719
  const { startRow, endRow, startCol, endCol } = selection;
@@ -3162,15 +3449,12 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3162
3449
  const fsWrapper = document.getElementById('fs-wrapper');
3163
3450
  const fsContent = document.getElementById('fs-content');
3164
3451
  const fsZoomInfo = document.getElementById('fs-zoom-info');
3165
- const minimapContent = document.getElementById('fs-minimap-content');
3166
- const minimapViewport = document.getElementById('fs-minimap-viewport');
3167
3452
  let currentZoom = 1;
3168
3453
  let initialZoom = 1;
3169
3454
  let panX = 0, panY = 0;
3170
3455
  let isPanning = false;
3171
3456
  let startX, startY;
3172
3457
  let svgNaturalWidth = 0, svgNaturalHeight = 0;
3173
- let minimapScale = 1;
3174
3458
 
3175
3459
  function openFullscreen(mermaidEl) {
3176
3460
  const svg = mermaidEl.querySelector('svg');
@@ -3179,11 +3463,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3179
3463
  const clonedSvg = svg.cloneNode(true);
3180
3464
  fsWrapper.appendChild(clonedSvg);
3181
3465
 
3182
- // Setup minimap
3183
- minimapContent.innerHTML = '';
3184
- const minimapSvg = svg.cloneNode(true);
3185
- minimapContent.appendChild(minimapSvg);
3186
-
3187
3466
  // Get SVG's intrinsic/natural size from viewBox or attributes
3188
3467
  const viewBox = svg.getAttribute('viewBox');
3189
3468
  let naturalWidth, naturalHeight;
@@ -3200,11 +3479,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3200
3479
  svgNaturalWidth = naturalWidth;
3201
3480
  svgNaturalHeight = naturalHeight;
3202
3481
 
3203
- // Calculate minimap scale
3204
- const minimapMaxWidth = 184; // 200 - 16 padding
3205
- const minimapMaxHeight = 134; // 150 - 16 padding
3206
- minimapScale = Math.min(minimapMaxWidth / naturalWidth, minimapMaxHeight / naturalHeight);
3207
-
3208
3482
  clonedSvg.style.width = naturalWidth + 'px';
3209
3483
  clonedSvg.style.height = naturalHeight + 'px';
3210
3484
 
@@ -3236,48 +3510,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3236
3510
  function updateTransform() {
3237
3511
  fsWrapper.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + currentZoom + ')';
3238
3512
  fsZoomInfo.textContent = Math.round(currentZoom * 100) + '%';
3239
- updateMinimap();
3240
- }
3241
-
3242
- function updateMinimap() {
3243
- if (!svgNaturalWidth || !svgNaturalHeight) return;
3244
-
3245
- const viewportWidth = window.innerWidth - 40;
3246
- const viewportHeight = window.innerHeight - 80;
3247
-
3248
- // Minimap dimensions
3249
- const mmWidth = 184;
3250
- const mmHeight = 134;
3251
- const mmPadding = 8;
3252
-
3253
- // SVG size in minimap (centered)
3254
- const mmSvgWidth = svgNaturalWidth * minimapScale;
3255
- const mmSvgHeight = svgNaturalHeight * minimapScale;
3256
- const mmSvgLeft = (mmWidth - mmSvgWidth) / 2 + mmPadding;
3257
- const mmSvgTop = (mmHeight - mmSvgHeight) / 2 + mmPadding;
3258
-
3259
- // Calculate visible area in SVG coordinates (accounting for transform origin at 0,0)
3260
- // panX/panY are the translation values, currentZoom is the scale
3261
- // The visible area starts at -panX/currentZoom in SVG coordinates
3262
- const visibleLeft = Math.max(0, -panX / currentZoom);
3263
- const visibleTop = Math.max(0, (-panY + 60) / currentZoom);
3264
- const visibleWidth = viewportWidth / currentZoom;
3265
- const visibleHeight = viewportHeight / currentZoom;
3266
-
3267
- // Clamp to SVG bounds
3268
- const clampedLeft = Math.min(visibleLeft, svgNaturalWidth);
3269
- const clampedTop = Math.min(visibleTop, svgNaturalHeight);
3270
-
3271
- // Position viewport indicator in minimap coordinates
3272
- const vpLeft = mmSvgLeft + clampedLeft * minimapScale;
3273
- const vpTop = mmSvgTop + clampedTop * minimapScale;
3274
- const vpWidth = Math.min(mmWidth - vpLeft + mmPadding, visibleWidth * minimapScale);
3275
- const vpHeight = Math.min(mmHeight - vpTop + mmPadding, visibleHeight * minimapScale);
3276
-
3277
- minimapViewport.style.left = vpLeft + 'px';
3278
- minimapViewport.style.top = vpTop + 'px';
3279
- minimapViewport.style.width = Math.max(20, vpWidth) + 'px';
3280
- minimapViewport.style.height = Math.max(15, vpHeight) + 'px';
3281
3513
  }
3282
3514
 
3283
3515
  // Use multiplicative zoom for consistent behavior
@@ -3350,6 +3582,74 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3350
3582
  }
3351
3583
  });
3352
3584
  })();
3585
+
3586
+ // --- Highlight.js Initialization ---
3587
+ (function initHighlightJS() {
3588
+ if (typeof hljs === 'undefined') return;
3589
+
3590
+ // Highlight all code blocks in preview (skip mermaid blocks)
3591
+ const preview = document.querySelector('.md-preview');
3592
+ if (preview) {
3593
+ preview.querySelectorAll('pre code').forEach(block => {
3594
+ // Skip if inside mermaid container or already highlighted
3595
+ if (block.closest('.mermaid-container') || block.classList.contains('hljs')) {
3596
+ return;
3597
+ }
3598
+ hljs.highlightElement(block);
3599
+ });
3600
+ }
3601
+ })();
3602
+
3603
+ // --- Image Fullscreen ---
3604
+ (function initImageFullscreen() {
3605
+ const preview = document.querySelector('.md-preview');
3606
+ if (!preview) return;
3607
+
3608
+ const imageOverlay = document.getElementById('image-fullscreen');
3609
+ const imageContainer = document.getElementById('image-container');
3610
+ const imageClose = document.getElementById('image-close');
3611
+ if (!imageOverlay || !imageContainer) return;
3612
+
3613
+ function closeImageOverlay() {
3614
+ imageOverlay.classList.remove('visible');
3615
+ imageContainer.innerHTML = '';
3616
+ }
3617
+
3618
+ if (imageClose) {
3619
+ imageClose.addEventListener('click', closeImageOverlay);
3620
+ }
3621
+
3622
+ if (imageOverlay) {
3623
+ imageOverlay.addEventListener('click', (e) => {
3624
+ if (e.target === imageOverlay) closeImageOverlay();
3625
+ });
3626
+ }
3627
+
3628
+ document.addEventListener('keydown', (e) => {
3629
+ if (e.key === 'Escape' && imageOverlay.classList.contains('visible')) {
3630
+ closeImageOverlay();
3631
+ }
3632
+ });
3633
+
3634
+ preview.querySelectorAll('img').forEach(img => {
3635
+ img.style.cursor = 'pointer';
3636
+ img.title = 'Click to view fullscreen';
3637
+
3638
+ img.addEventListener('click', (e) => {
3639
+ e.stopPropagation();
3640
+
3641
+ imageContainer.innerHTML = '';
3642
+ const clonedImg = img.cloneNode(true);
3643
+ clonedImg.style.maxWidth = '90vw';
3644
+ clonedImg.style.maxHeight = '90vh';
3645
+ clonedImg.style.objectFit = 'contain';
3646
+ clonedImg.style.cursor = 'default';
3647
+ imageContainer.appendChild(clonedImg);
3648
+
3649
+ imageOverlay.classList.add('visible');
3650
+ });
3651
+ });
3652
+ })();
3353
3653
  </script>
3354
3654
  </body>
3355
3655
  </html>`;
@@ -3357,7 +3657,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3357
3657
 
3358
3658
  function buildHtml(filePath) {
3359
3659
  const data = loadData(filePath);
3360
- if (data.mode === 'diff') {
3660
+ if (data.mode === "diff") {
3361
3661
  return diffHtmlTemplate(data);
3362
3662
  }
3363
3663
  const { rows, cols, title, mode, preview } = data;
@@ -3367,16 +3667,16 @@ function buildHtml(filePath) {
3367
3667
  // --- HTTP Server -----------------------------------------------------------
3368
3668
  function readBody(req) {
3369
3669
  return new Promise((resolve, reject) => {
3370
- let data = '';
3371
- req.on('data', (chunk) => {
3670
+ let data = "";
3671
+ req.on("data", (chunk) => {
3372
3672
  data += chunk;
3373
3673
  if (data.length > 2 * 1024 * 1024) {
3374
- reject(new Error('payload too large'));
3674
+ reject(new Error("payload too large"));
3375
3675
  req.destroy();
3376
3676
  }
3377
3677
  });
3378
- req.on('end', () => resolve(data));
3379
- req.on('error', reject);
3678
+ req.on("end", () => resolve(data));
3679
+ req.on("error", reject);
3380
3680
  });
3381
3681
  }
3382
3682
 
@@ -3384,7 +3684,7 @@ const MAX_PORT_ATTEMPTS = 100;
3384
3684
  const activeServers = new Map();
3385
3685
 
3386
3686
  function outputAllResults() {
3387
- console.log('=== All comments received ===');
3687
+ console.log("=== All comments received ===");
3388
3688
  if (allResults.length === 1) {
3389
3689
  const yamlOut = yaml.dump(allResults[0], { noRefs: true, lineWidth: 120 });
3390
3690
  console.log(yamlOut.trim());
@@ -3406,15 +3706,19 @@ function shutdownAll() {
3406
3706
  for (const ctx of activeServers.values()) {
3407
3707
  if (ctx.watcher) ctx.watcher.close();
3408
3708
  if (ctx.heartbeat) clearInterval(ctx.heartbeat);
3409
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3709
+ ctx.sseClients.forEach((res) => {
3710
+ try {
3711
+ res.end();
3712
+ } catch (_) {}
3713
+ });
3410
3714
  if (ctx.server) ctx.server.close();
3411
3715
  }
3412
3716
  outputAllResults();
3413
3717
  setTimeout(() => process.exit(0), 500).unref();
3414
3718
  }
3415
3719
 
3416
- process.on('SIGINT', shutdownAll);
3417
- process.on('SIGTERM', shutdownAll);
3720
+ process.on("SIGINT", shutdownAll);
3721
+ process.on("SIGTERM", shutdownAll);
3418
3722
 
3419
3723
  function createFileServer(filePath) {
3420
3724
  return new Promise((resolve) => {
@@ -3431,19 +3735,21 @@ function createFileServer(filePath) {
3431
3735
  reloadTimer: null,
3432
3736
  server: null,
3433
3737
  opened: false,
3434
- port: 0
3738
+ port: 0,
3435
3739
  };
3436
3740
 
3437
3741
  function broadcast(data) {
3438
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
3742
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
3439
3743
  ctx.sseClients.forEach((res) => {
3440
- try { res.write(`data: ${payload}\n\n`); } catch (_) {}
3744
+ try {
3745
+ res.write(`data: ${payload}\n\n`);
3746
+ } catch (_) {}
3441
3747
  });
3442
3748
  }
3443
3749
 
3444
3750
  function notifyReload() {
3445
3751
  clearTimeout(ctx.reloadTimer);
3446
- ctx.reloadTimer = setTimeout(() => broadcast('reload'), 150);
3752
+ ctx.reloadTimer = setTimeout(() => broadcast("reload"), 150);
3447
3753
  }
3448
3754
 
3449
3755
  function startWatcher() {
@@ -3452,7 +3758,7 @@ function createFileServer(filePath) {
3452
3758
  } catch (err) {
3453
3759
  console.warn(`Failed to start file watcher for ${baseName}:`, err);
3454
3760
  }
3455
- ctx.heartbeat = setInterval(() => broadcast('ping'), 25000);
3761
+ ctx.heartbeat = setInterval(() => broadcast("ping"), 25000);
3456
3762
  }
3457
3763
 
3458
3764
  function shutdownServer(result) {
@@ -3464,7 +3770,11 @@ function createFileServer(filePath) {
3464
3770
  clearInterval(ctx.heartbeat);
3465
3771
  ctx.heartbeat = null;
3466
3772
  }
3467
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3773
+ ctx.sseClients.forEach((res) => {
3774
+ try {
3775
+ res.end();
3776
+ } catch (_) {}
3777
+ });
3468
3778
  if (ctx.server) {
3469
3779
  ctx.server.close();
3470
3780
  ctx.server = null;
@@ -3477,95 +3787,95 @@ function createFileServer(filePath) {
3477
3787
  }
3478
3788
 
3479
3789
  ctx.server = http.createServer(async (req, res) => {
3480
- if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
3790
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
3481
3791
  try {
3482
3792
  const html = buildHtml(filePath);
3483
3793
  res.writeHead(200, {
3484
- 'Content-Type': 'text/html; charset=utf-8',
3485
- 'Cache-Control': 'no-store, no-cache, must-revalidate',
3486
- Pragma: 'no-cache',
3487
- Expires: '0'
3794
+ "Content-Type": "text/html; charset=utf-8",
3795
+ "Cache-Control": "no-store, no-cache, must-revalidate",
3796
+ Pragma: "no-cache",
3797
+ Expires: "0",
3488
3798
  });
3489
3799
  res.end(html);
3490
3800
  } catch (err) {
3491
- console.error('File load error', err);
3492
- res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
3493
- res.end('Failed to load file. Please check the file.');
3801
+ console.error("File load error", err);
3802
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
3803
+ res.end("Failed to load file. Please check the file.");
3494
3804
  }
3495
3805
  return;
3496
3806
  }
3497
3807
 
3498
- if (req.method === 'GET' && req.url === '/healthz') {
3499
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3500
- res.end('ok');
3808
+ if (req.method === "GET" && req.url === "/healthz") {
3809
+ res.writeHead(200, { "Content-Type": "text/plain" });
3810
+ res.end("ok");
3501
3811
  return;
3502
3812
  }
3503
3813
 
3504
- if (req.method === 'POST' && req.url === '/exit') {
3814
+ if (req.method === "POST" && req.url === "/exit") {
3505
3815
  try {
3506
3816
  const raw = await readBody(req);
3507
3817
  let payload = {};
3508
3818
  if (raw && raw.trim()) {
3509
3819
  payload = JSON.parse(raw);
3510
3820
  }
3511
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3512
- res.end('bye');
3821
+ res.writeHead(200, { "Content-Type": "text/plain" });
3822
+ res.end("bye");
3513
3823
  shutdownServer(payload);
3514
3824
  } catch (err) {
3515
- console.error('payload parse error', err);
3516
- res.writeHead(400, { 'Content-Type': 'text/plain' });
3517
- res.end('bad request');
3825
+ console.error("payload parse error", err);
3826
+ res.writeHead(400, { "Content-Type": "text/plain" });
3827
+ res.end("bad request");
3518
3828
  shutdownServer(null);
3519
3829
  }
3520
3830
  return;
3521
3831
  }
3522
3832
 
3523
- if (req.method === 'GET' && req.url === '/sse') {
3833
+ if (req.method === "GET" && req.url === "/sse") {
3524
3834
  res.writeHead(200, {
3525
- 'Content-Type': 'text/event-stream',
3526
- 'Cache-Control': 'no-cache',
3527
- Connection: 'keep-alive',
3528
- 'X-Accel-Buffering': 'no'
3835
+ "Content-Type": "text/event-stream",
3836
+ "Cache-Control": "no-cache",
3837
+ Connection: "keep-alive",
3838
+ "X-Accel-Buffering": "no",
3529
3839
  });
3530
- res.write('retry: 3000\n\n');
3840
+ res.write("retry: 3000\n\n");
3531
3841
  ctx.sseClients.add(res);
3532
- req.on('close', () => ctx.sseClients.delete(res));
3842
+ req.on("close", () => ctx.sseClients.delete(res));
3533
3843
  return;
3534
3844
  }
3535
3845
 
3536
3846
  // Static file serving for images and other assets
3537
- if (req.method === 'GET') {
3847
+ if (req.method === "GET") {
3538
3848
  const MIME_TYPES = {
3539
- '.png': 'image/png',
3540
- '.jpg': 'image/jpeg',
3541
- '.jpeg': 'image/jpeg',
3542
- '.gif': 'image/gif',
3543
- '.webp': 'image/webp',
3544
- '.svg': 'image/svg+xml',
3545
- '.ico': 'image/x-icon',
3546
- '.css': 'text/css',
3547
- '.js': 'application/javascript',
3548
- '.json': 'application/json',
3549
- '.pdf': 'application/pdf',
3849
+ ".png": "image/png",
3850
+ ".jpg": "image/jpeg",
3851
+ ".jpeg": "image/jpeg",
3852
+ ".gif": "image/gif",
3853
+ ".webp": "image/webp",
3854
+ ".svg": "image/svg+xml",
3855
+ ".ico": "image/x-icon",
3856
+ ".css": "text/css",
3857
+ ".js": "application/javascript",
3858
+ ".json": "application/json",
3859
+ ".pdf": "application/pdf",
3550
3860
  };
3551
3861
  try {
3552
- const urlPath = decodeURIComponent(req.url.split('?')[0]);
3553
- if (urlPath.includes('..')) {
3554
- res.writeHead(403, { 'Content-Type': 'text/plain' });
3555
- res.end('forbidden');
3862
+ const urlPath = decodeURIComponent(req.url.split("?")[0]);
3863
+ if (urlPath.includes("..")) {
3864
+ res.writeHead(403, { "Content-Type": "text/plain" });
3865
+ res.end("forbidden");
3556
3866
  return;
3557
3867
  }
3558
3868
  const staticPath = path.join(baseDir, urlPath);
3559
3869
  if (!staticPath.startsWith(baseDir)) {
3560
- res.writeHead(403, { 'Content-Type': 'text/plain' });
3561
- res.end('forbidden');
3870
+ res.writeHead(403, { "Content-Type": "text/plain" });
3871
+ res.end("forbidden");
3562
3872
  return;
3563
3873
  }
3564
3874
  if (fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) {
3565
3875
  const ext = path.extname(staticPath).toLowerCase();
3566
- const contentType = MIME_TYPES[ext] || 'application/octet-stream';
3876
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
3567
3877
  const content = fs.readFileSync(staticPath);
3568
- res.writeHead(200, { 'Content-Type': contentType });
3878
+ res.writeHead(200, { "Content-Type": contentType });
3569
3879
  res.end(content);
3570
3880
  return;
3571
3881
  }
@@ -3574,20 +3884,22 @@ function createFileServer(filePath) {
3574
3884
  }
3575
3885
  }
3576
3886
 
3577
- res.writeHead(404, { 'Content-Type': 'text/plain' });
3578
- res.end('not found');
3887
+ res.writeHead(404, { "Content-Type": "text/plain" });
3888
+ res.end("not found");
3579
3889
  });
3580
3890
 
3581
3891
  function tryListen(attemptPort, attempts = 0) {
3582
3892
  if (attempts >= MAX_PORT_ATTEMPTS) {
3583
- console.error(`Could not find an available port for ${baseName} after ${MAX_PORT_ATTEMPTS} attempts.`);
3893
+ console.error(
3894
+ `Could not find an available port for ${baseName} after ${MAX_PORT_ATTEMPTS} attempts.`,
3895
+ );
3584
3896
  serversRunning--;
3585
3897
  checkAllDone();
3586
3898
  return;
3587
3899
  }
3588
3900
 
3589
- ctx.server.once('error', (err) => {
3590
- if (err.code === 'EADDRINUSE') {
3901
+ ctx.server.once("error", (err) => {
3902
+ if (err.code === "EADDRINUSE") {
3591
3903
  tryListen(attemptPort + 1, attempts + 1);
3592
3904
  } else {
3593
3905
  console.error(`Server error for ${baseName}:`, err);
@@ -3603,11 +3915,19 @@ function createFileServer(filePath) {
3603
3915
  console.log(`Viewer started: http://localhost:${attemptPort} (file: ${baseName})`);
3604
3916
  if (!noOpen) {
3605
3917
  const url = `http://localhost:${attemptPort}`;
3606
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
3918
+ const opener =
3919
+ process.platform === "darwin"
3920
+ ? "open"
3921
+ : process.platform === "win32"
3922
+ ? "start"
3923
+ : "xdg-open";
3607
3924
  try {
3608
- spawn(opener, [url], { stdio: 'ignore', detached: true });
3925
+ spawn(opener, [url], { stdio: "ignore", detached: true });
3609
3926
  } catch (err) {
3610
- console.warn('Failed to open browser automatically. Please open this URL manually:', url);
3927
+ console.warn(
3928
+ "Failed to open browser automatically. Please open this URL manually:",
3929
+ url,
3930
+ );
3611
3931
  }
3612
3932
  }
3613
3933
  startWatcher();
@@ -3629,13 +3949,15 @@ function createDiffServer(diffContent) {
3629
3949
  sseClients: new Set(),
3630
3950
  heartbeat: null,
3631
3951
  server: null,
3632
- port: 0
3952
+ port: 0,
3633
3953
  };
3634
3954
 
3635
3955
  function broadcast(data) {
3636
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
3956
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
3637
3957
  ctx.sseClients.forEach((res) => {
3638
- try { res.write(`data: ${payload}\n\n`); } catch (_) {}
3958
+ try {
3959
+ res.write(`data: ${payload}\n\n`);
3960
+ } catch (_) {}
3639
3961
  });
3640
3962
  }
3641
3963
 
@@ -3644,7 +3966,11 @@ function createDiffServer(diffContent) {
3644
3966
  clearInterval(ctx.heartbeat);
3645
3967
  ctx.heartbeat = null;
3646
3968
  }
3647
- ctx.sseClients.forEach((res) => { try { res.end(); } catch (_) {} });
3969
+ ctx.sseClients.forEach((res) => {
3970
+ try {
3971
+ res.end();
3972
+ } catch (_) {}
3973
+ });
3648
3974
  if (ctx.server) {
3649
3975
  ctx.server.close();
3650
3976
  ctx.server = null;
@@ -3656,79 +3982,81 @@ function createDiffServer(diffContent) {
3656
3982
  }
3657
3983
 
3658
3984
  ctx.server = http.createServer(async (req, res) => {
3659
- if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
3985
+ if (req.method === "GET" && (req.url === "/" || req.url === "/index.html")) {
3660
3986
  try {
3661
3987
  const html = diffHtmlTemplate(diffData);
3662
3988
  res.writeHead(200, {
3663
- 'Content-Type': 'text/html; charset=utf-8',
3664
- 'Cache-Control': 'no-store, no-cache, must-revalidate',
3665
- Pragma: 'no-cache',
3666
- Expires: '0'
3989
+ "Content-Type": "text/html; charset=utf-8",
3990
+ "Cache-Control": "no-store, no-cache, must-revalidate",
3991
+ Pragma: "no-cache",
3992
+ Expires: "0",
3667
3993
  });
3668
3994
  res.end(html);
3669
3995
  } catch (err) {
3670
- console.error('Diff render error', err);
3671
- res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
3672
- res.end('Failed to render diff view.');
3996
+ console.error("Diff render error", err);
3997
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
3998
+ res.end("Failed to render diff view.");
3673
3999
  }
3674
4000
  return;
3675
4001
  }
3676
4002
 
3677
- if (req.method === 'GET' && req.url === '/healthz') {
3678
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3679
- res.end('ok');
4003
+ if (req.method === "GET" && req.url === "/healthz") {
4004
+ res.writeHead(200, { "Content-Type": "text/plain" });
4005
+ res.end("ok");
3680
4006
  return;
3681
4007
  }
3682
4008
 
3683
- if (req.method === 'POST' && req.url === '/exit') {
4009
+ if (req.method === "POST" && req.url === "/exit") {
3684
4010
  try {
3685
4011
  const raw = await readBody(req);
3686
4012
  let payload = {};
3687
4013
  if (raw && raw.trim()) {
3688
4014
  payload = JSON.parse(raw);
3689
4015
  }
3690
- res.writeHead(200, { 'Content-Type': 'text/plain' });
3691
- res.end('bye');
4016
+ res.writeHead(200, { "Content-Type": "text/plain" });
4017
+ res.end("bye");
3692
4018
  shutdownServer(payload);
3693
4019
  } catch (err) {
3694
- console.error('payload parse error', err);
3695
- res.writeHead(400, { 'Content-Type': 'text/plain' });
3696
- res.end('bad request');
4020
+ console.error("payload parse error", err);
4021
+ res.writeHead(400, { "Content-Type": "text/plain" });
4022
+ res.end("bad request");
3697
4023
  shutdownServer(null);
3698
4024
  }
3699
4025
  return;
3700
4026
  }
3701
4027
 
3702
- if (req.method === 'GET' && req.url === '/sse') {
4028
+ if (req.method === "GET" && req.url === "/sse") {
3703
4029
  res.writeHead(200, {
3704
- 'Content-Type': 'text/event-stream',
3705
- 'Cache-Control': 'no-cache',
3706
- Connection: 'keep-alive',
3707
- 'X-Accel-Buffering': 'no'
4030
+ "Content-Type": "text/event-stream",
4031
+ "Cache-Control": "no-cache",
4032
+ Connection: "keep-alive",
4033
+ "X-Accel-Buffering": "no",
3708
4034
  });
3709
- res.write('retry: 3000\n\n');
4035
+ res.write("retry: 3000\n\n");
3710
4036
  ctx.sseClients.add(res);
3711
- req.on('close', () => ctx.sseClients.delete(res));
4037
+ req.on("close", () => ctx.sseClients.delete(res));
3712
4038
  return;
3713
4039
  }
3714
4040
 
3715
- res.writeHead(404, { 'Content-Type': 'text/plain' });
3716
- res.end('not found');
4041
+ res.writeHead(404, { "Content-Type": "text/plain" });
4042
+ res.end("not found");
3717
4043
  });
3718
4044
 
3719
4045
  function tryListen(attemptPort, attempts = 0) {
3720
4046
  if (attempts >= MAX_PORT_ATTEMPTS) {
3721
- console.error(`Could not find an available port for diff viewer after ${MAX_PORT_ATTEMPTS} attempts.`);
4047
+ console.error(
4048
+ `Could not find an available port for diff viewer after ${MAX_PORT_ATTEMPTS} attempts.`,
4049
+ );
3722
4050
  serversRunning--;
3723
4051
  checkAllDone();
3724
4052
  return;
3725
4053
  }
3726
4054
 
3727
- ctx.server.once('error', (err) => {
3728
- if (err.code === 'EADDRINUSE') {
4055
+ ctx.server.once("error", (err) => {
4056
+ if (err.code === "EADDRINUSE") {
3729
4057
  tryListen(attemptPort + 1, attempts + 1);
3730
4058
  } else {
3731
- console.error('Diff server error:', err);
4059
+ console.error("Diff server error:", err);
3732
4060
  serversRunning--;
3733
4061
  checkAllDone();
3734
4062
  }
@@ -3736,15 +4064,23 @@ function createDiffServer(diffContent) {
3736
4064
 
3737
4065
  ctx.server.listen(attemptPort, () => {
3738
4066
  ctx.port = attemptPort;
3739
- ctx.heartbeat = setInterval(() => broadcast('ping'), 25000);
4067
+ ctx.heartbeat = setInterval(() => broadcast("ping"), 25000);
3740
4068
  console.log(`Diff viewer started: http://localhost:${attemptPort}`);
3741
4069
  if (!noOpen) {
3742
4070
  const url = `http://localhost:${attemptPort}`;
3743
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
4071
+ const opener =
4072
+ process.platform === "darwin"
4073
+ ? "open"
4074
+ : process.platform === "win32"
4075
+ ? "start"
4076
+ : "xdg-open";
3744
4077
  try {
3745
- spawn(opener, [url], { stdio: 'ignore', detached: true });
4078
+ spawn(opener, [url], { stdio: "ignore", detached: true });
3746
4079
  } catch (err) {
3747
- console.warn('Failed to open browser automatically. Please open this URL manually:', url);
4080
+ console.warn(
4081
+ "Failed to open browser automatically. Please open this URL manually:",
4082
+ url,
4083
+ );
3748
4084
  }
3749
4085
  }
3750
4086
  resolve(ctx);
@@ -3766,17 +4102,21 @@ function createDiffServer(diffContent) {
3766
4102
  stdinContent = stdinData;
3767
4103
 
3768
4104
  // Check if it looks like a diff
3769
- if (stdinContent.startsWith('diff --git') || stdinContent.includes('\n+++ ') || stdinContent.includes('\n--- ')) {
4105
+ if (
4106
+ stdinContent.startsWith("diff --git") ||
4107
+ stdinContent.includes("\n+++ ") ||
4108
+ stdinContent.includes("\n--- ")
4109
+ ) {
3770
4110
  diffMode = true;
3771
- console.log('Starting diff viewer from stdin...');
4111
+ console.log("Starting diff viewer from stdin...");
3772
4112
  serversRunning = 1;
3773
4113
  await createDiffServer(stdinContent);
3774
- console.log('Close the browser tab or Submit & Exit to finish.');
4114
+ console.log("Close the browser tab or Submit & Exit to finish.");
3775
4115
  } else {
3776
4116
  // Treat as plain text
3777
- console.log('Starting text viewer from stdin...');
4117
+ console.log("Starting text viewer from stdin...");
3778
4118
  // For now, just show message - could enhance to support any text
3779
- console.error('Non-diff stdin content is not supported yet. Use a file instead.');
4119
+ console.error("Non-diff stdin content is not supported yet. Use a file instead.");
3780
4120
  process.exit(1);
3781
4121
  }
3782
4122
  } else if (resolvedPaths.length > 0) {
@@ -3786,31 +4126,31 @@ function createDiffServer(diffContent) {
3786
4126
  for (const filePath of resolvedPaths) {
3787
4127
  await createFileServer(filePath);
3788
4128
  }
3789
- console.log('Close all browser tabs or Submit & Exit to finish.');
4129
+ console.log("Close all browser tabs or Submit & Exit to finish.");
3790
4130
  } else {
3791
4131
  // No files and no stdin: try auto git diff
3792
- console.log('No files specified. Running git diff HEAD...');
4132
+ console.log("No files specified. Running git diff HEAD...");
3793
4133
  try {
3794
4134
  const gitDiff = await runGitDiff();
3795
- if (gitDiff.trim() === '') {
3796
- console.log('No changes detected (working tree clean).');
3797
- console.log('');
3798
- console.log('Usage: reviw <file...> [options]');
3799
- console.log(' git diff | reviw [options]');
3800
- console.log(' reviw (auto runs git diff HEAD)');
4135
+ if (gitDiff.trim() === "") {
4136
+ console.log("No changes detected (working tree clean).");
4137
+ console.log("");
4138
+ console.log("Usage: reviw <file...> [options]");
4139
+ console.log(" git diff | reviw [options]");
4140
+ console.log(" reviw (auto runs git diff HEAD)");
3801
4141
  process.exit(0);
3802
4142
  }
3803
4143
  diffMode = true;
3804
4144
  stdinContent = gitDiff;
3805
- console.log('Starting diff viewer...');
4145
+ console.log("Starting diff viewer...");
3806
4146
  serversRunning = 1;
3807
4147
  await createDiffServer(gitDiff);
3808
- console.log('Close the browser tab or Submit & Exit to finish.');
4148
+ console.log("Close the browser tab or Submit & Exit to finish.");
3809
4149
  } catch (err) {
3810
4150
  console.error(err.message);
3811
- console.log('');
3812
- console.log('Usage: reviw <file...> [options]');
3813
- console.log(' git diff | reviw [options]');
4151
+ console.log("");
4152
+ console.log("Usage: reviw <file...> [options]");
4153
+ console.log(" git diff | reviw [options]");
3814
4154
  process.exit(1);
3815
4155
  }
3816
4156
  }