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.
- package/README.md +7 -7
- package/cli.cjs +663 -323
- 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
|
|
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(
|
|
15
|
-
const http = require(
|
|
16
|
-
const path = require(
|
|
17
|
-
const { spawn } = require(
|
|
18
|
-
const chardet = require(
|
|
19
|
-
const iconv = require(
|
|
20
|
-
const marked = require(
|
|
21
|
-
const yaml = require(
|
|
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 =
|
|
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 ===
|
|
36
|
+
if (arg === "--port" && args[i + 1]) {
|
|
37
37
|
basePort = Number(args[i + 1]);
|
|
38
38
|
i += 1;
|
|
39
|
-
} else if ((arg ===
|
|
39
|
+
} else if ((arg === "--encoding" || arg === "-e") && args[i + 1]) {
|
|
40
40
|
encodingOpt = args[i + 1];
|
|
41
41
|
i += 1;
|
|
42
|
-
} else if (arg ===
|
|
42
|
+
} else if (arg === "--no-open") {
|
|
43
43
|
noOpen = true;
|
|
44
|
-
} else if (arg ===
|
|
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:
|
|
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(
|
|
83
|
-
process.stdin.on(
|
|
82
|
+
process.stdin.setEncoding("utf8");
|
|
83
|
+
process.stdin.on("data", (chunk) => {
|
|
84
84
|
data += chunk;
|
|
85
85
|
});
|
|
86
|
-
process.stdin.on(
|
|
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(
|
|
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(
|
|
106
|
+
const { execSync } = require("child_process");
|
|
107
107
|
try {
|
|
108
108
|
// Check if we're in a git repo
|
|
109
|
-
execSync(
|
|
109
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
110
110
|
// Run git diff HEAD
|
|
111
|
-
const diff = execSync(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
159
|
+
if (line.startsWith("new file mode")) {
|
|
160
160
|
currentFile.isNew = true;
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
|
-
if (line.startsWith(
|
|
163
|
+
if (line.startsWith("deleted file mode")) {
|
|
164
164
|
currentFile.isDeleted = true;
|
|
165
165
|
continue;
|
|
166
166
|
}
|
|
167
|
-
if (line.startsWith(
|
|
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(
|
|
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:
|
|
196
|
-
} else if (line.startsWith(
|
|
197
|
-
hunk.lines.push({ type:
|
|
198
|
-
} else if (line.startsWith(
|
|
199
|
-
hunk.lines.push({ type:
|
|
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 +=
|
|
239
|
-
if (file.isDeleted) label +=
|
|
240
|
-
if (file.isBinary) label +=
|
|
238
|
+
if (file.isNew) label += " (new)";
|
|
239
|
+
if (file.isDeleted) label += " (deleted)";
|
|
240
|
+
if (file.isBinary) label += " (binary)";
|
|
241
241
|
rows.push({
|
|
242
|
-
type:
|
|
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:
|
|
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:
|
|
278
|
-
mode:
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
339
|
-
utf8:
|
|
340
|
-
|
|
341
|
-
sjis:
|
|
342
|
-
|
|
343
|
-
cp932:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
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) ||
|
|
361
|
-
if (encoding !==
|
|
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(
|
|
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 ===
|
|
378
|
-
if (!csvText.includes(
|
|
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
|
-
|
|
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, "&")
|
|
474
|
+
.replace(/</g, "<")
|
|
475
|
+
.replace(/>/g, ">")
|
|
476
|
+
.replace(/"/g, """);
|
|
477
|
+
}
|
|
478
|
+
|
|
415
479
|
function loadData(filePath) {
|
|
416
480
|
const ext = path.extname(filePath).toLowerCase();
|
|
417
|
-
if (ext ===
|
|
481
|
+
if (ext === ".csv" || ext === ".tsv") {
|
|
418
482
|
const data = loadCsv(filePath);
|
|
419
|
-
return { ...data, mode:
|
|
483
|
+
return { ...data, mode: "csv" };
|
|
420
484
|
}
|
|
421
|
-
if (ext ===
|
|
485
|
+
if (ext === ".md" || ext === ".markdown") {
|
|
422
486
|
const data = loadMarkdown(filePath);
|
|
423
|
-
return { ...data, mode:
|
|
487
|
+
return { ...data, mode: "markdown" };
|
|
424
488
|
}
|
|
425
|
-
if (ext ===
|
|
426
|
-
const content = fs.readFileSync(filePath,
|
|
489
|
+
if (ext === ".diff" || ext === ".patch") {
|
|
490
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
427
491
|
const data = loadDiff(content);
|
|
428
|
-
return { ...data, mode:
|
|
492
|
+
return { ...data, mode: "diff" };
|
|
429
493
|
}
|
|
430
494
|
// default text
|
|
431
495
|
const data = loadText(filePath);
|
|
432
|
-
return { ...data, mode:
|
|
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,
|
|
441
|
-
.replace(/>/g,
|
|
442
|
-
.replace(/\u2028/g,
|
|
443
|
-
.replace(/\u2029/g,
|
|
444
|
-
.replace(/`/g,
|
|
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 ===
|
|
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 ?
|
|
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:
|
|
1755
|
-
padding:
|
|
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
|
-
${
|
|
1998
|
-
|
|
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 })
|
|
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 })
|
|
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 =
|
|
2186
|
-
const MAX_COL_WIDTH =
|
|
2187
|
-
const DEFAULT_COL_WIDTH =
|
|
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 ===
|
|
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(
|
|
3670
|
+
let data = "";
|
|
3671
|
+
req.on("data", (chunk) => {
|
|
3372
3672
|
data += chunk;
|
|
3373
3673
|
if (data.length > 2 * 1024 * 1024) {
|
|
3374
|
-
reject(new Error(
|
|
3674
|
+
reject(new Error("payload too large"));
|
|
3375
3675
|
req.destroy();
|
|
3376
3676
|
}
|
|
3377
3677
|
});
|
|
3378
|
-
req.on(
|
|
3379
|
-
req.on(
|
|
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(
|
|
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) => {
|
|
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(
|
|
3417
|
-
process.on(
|
|
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 ===
|
|
3742
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
3439
3743
|
ctx.sseClients.forEach((res) => {
|
|
3440
|
-
try {
|
|
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(
|
|
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(
|
|
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) => {
|
|
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 ===
|
|
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
|
-
|
|
3485
|
-
|
|
3486
|
-
Pragma:
|
|
3487
|
-
Expires:
|
|
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(
|
|
3492
|
-
res.writeHead(500, {
|
|
3493
|
-
res.end(
|
|
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 ===
|
|
3499
|
-
res.writeHead(200, {
|
|
3500
|
-
res.end(
|
|
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 ===
|
|
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, {
|
|
3512
|
-
res.end(
|
|
3821
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
3822
|
+
res.end("bye");
|
|
3513
3823
|
shutdownServer(payload);
|
|
3514
3824
|
} catch (err) {
|
|
3515
|
-
console.error(
|
|
3516
|
-
res.writeHead(400, {
|
|
3517
|
-
res.end(
|
|
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 ===
|
|
3833
|
+
if (req.method === "GET" && req.url === "/sse") {
|
|
3524
3834
|
res.writeHead(200, {
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
Connection:
|
|
3528
|
-
|
|
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(
|
|
3840
|
+
res.write("retry: 3000\n\n");
|
|
3531
3841
|
ctx.sseClients.add(res);
|
|
3532
|
-
req.on(
|
|
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 ===
|
|
3847
|
+
if (req.method === "GET") {
|
|
3538
3848
|
const MIME_TYPES = {
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
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(
|
|
3553
|
-
if (urlPath.includes(
|
|
3554
|
-
res.writeHead(403, {
|
|
3555
|
-
res.end(
|
|
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, {
|
|
3561
|
-
res.end(
|
|
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] ||
|
|
3876
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
3567
3877
|
const content = fs.readFileSync(staticPath);
|
|
3568
|
-
res.writeHead(200, {
|
|
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, {
|
|
3578
|
-
res.end(
|
|
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(
|
|
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(
|
|
3590
|
-
if (err.code ===
|
|
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 =
|
|
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:
|
|
3925
|
+
spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
3609
3926
|
} catch (err) {
|
|
3610
|
-
console.warn(
|
|
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 ===
|
|
3956
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
3637
3957
|
ctx.sseClients.forEach((res) => {
|
|
3638
|
-
try {
|
|
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) => {
|
|
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 ===
|
|
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
|
-
|
|
3664
|
-
|
|
3665
|
-
Pragma:
|
|
3666
|
-
Expires:
|
|
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(
|
|
3671
|
-
res.writeHead(500, {
|
|
3672
|
-
res.end(
|
|
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 ===
|
|
3678
|
-
res.writeHead(200, {
|
|
3679
|
-
res.end(
|
|
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 ===
|
|
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, {
|
|
3691
|
-
res.end(
|
|
4016
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
4017
|
+
res.end("bye");
|
|
3692
4018
|
shutdownServer(payload);
|
|
3693
4019
|
} catch (err) {
|
|
3694
|
-
console.error(
|
|
3695
|
-
res.writeHead(400, {
|
|
3696
|
-
res.end(
|
|
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 ===
|
|
4028
|
+
if (req.method === "GET" && req.url === "/sse") {
|
|
3703
4029
|
res.writeHead(200, {
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
Connection:
|
|
3707
|
-
|
|
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(
|
|
4035
|
+
res.write("retry: 3000\n\n");
|
|
3710
4036
|
ctx.sseClients.add(res);
|
|
3711
|
-
req.on(
|
|
4037
|
+
req.on("close", () => ctx.sseClients.delete(res));
|
|
3712
4038
|
return;
|
|
3713
4039
|
}
|
|
3714
4040
|
|
|
3715
|
-
res.writeHead(404, {
|
|
3716
|
-
res.end(
|
|
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(
|
|
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(
|
|
3728
|
-
if (err.code ===
|
|
4055
|
+
ctx.server.once("error", (err) => {
|
|
4056
|
+
if (err.code === "EADDRINUSE") {
|
|
3729
4057
|
tryListen(attemptPort + 1, attempts + 1);
|
|
3730
4058
|
} else {
|
|
3731
|
-
console.error(
|
|
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(
|
|
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 =
|
|
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:
|
|
4078
|
+
spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
3746
4079
|
} catch (err) {
|
|
3747
|
-
console.warn(
|
|
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 (
|
|
4105
|
+
if (
|
|
4106
|
+
stdinContent.startsWith("diff --git") ||
|
|
4107
|
+
stdinContent.includes("\n+++ ") ||
|
|
4108
|
+
stdinContent.includes("\n--- ")
|
|
4109
|
+
) {
|
|
3770
4110
|
diffMode = true;
|
|
3771
|
-
console.log(
|
|
4111
|
+
console.log("Starting diff viewer from stdin...");
|
|
3772
4112
|
serversRunning = 1;
|
|
3773
4113
|
await createDiffServer(stdinContent);
|
|
3774
|
-
console.log(
|
|
4114
|
+
console.log("Close the browser tab or Submit & Exit to finish.");
|
|
3775
4115
|
} else {
|
|
3776
4116
|
// Treat as plain text
|
|
3777
|
-
console.log(
|
|
4117
|
+
console.log("Starting text viewer from stdin...");
|
|
3778
4118
|
// For now, just show message - could enhance to support any text
|
|
3779
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
3797
|
-
console.log(
|
|
3798
|
-
console.log(
|
|
3799
|
-
console.log(
|
|
3800
|
-
console.log(
|
|
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(
|
|
4145
|
+
console.log("Starting diff viewer...");
|
|
3806
4146
|
serversRunning = 1;
|
|
3807
4147
|
await createDiffServer(gitDiff);
|
|
3808
|
-
console.log(
|
|
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(
|
|
3813
|
-
console.log(
|
|
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
|
}
|