reviw 0.10.6 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/cli.cjs +1005 -106
- package/package.json +1 -1
package/cli.cjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require("fs");
|
|
15
15
|
const http = require("http");
|
|
16
|
+
const os = require("os");
|
|
16
17
|
const path = require("path");
|
|
17
18
|
const { spawn } = require("child_process");
|
|
18
19
|
const chardet = require("chardet");
|
|
@@ -20,17 +21,157 @@ const iconv = require("iconv-lite");
|
|
|
20
21
|
const marked = require("marked");
|
|
21
22
|
const yaml = require("js-yaml");
|
|
22
23
|
|
|
24
|
+
// --- XSS Protection for marked (Whitelist approach) ---
|
|
25
|
+
// 許可タグリスト(Markdown由来の安全なタグのみ)
|
|
26
|
+
const allowedTags = new Set([
|
|
27
|
+
'p', 'br', 'hr',
|
|
28
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
29
|
+
'ul', 'ol', 'li',
|
|
30
|
+
'blockquote', 'pre', 'code',
|
|
31
|
+
'em', 'strong', 'del', 's',
|
|
32
|
+
'a', 'img',
|
|
33
|
+
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
34
|
+
'div', 'span', // Markdown拡張用
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// 許可属性リスト(タグごとに定義)
|
|
38
|
+
const allowedAttributes = {
|
|
39
|
+
'a': ['href', 'title', 'target', 'rel'],
|
|
40
|
+
'img': ['src', 'alt', 'title', 'width', 'height'],
|
|
41
|
+
'code': ['class'], // 言語ハイライト用
|
|
42
|
+
'pre': ['class'],
|
|
43
|
+
'div': ['class'],
|
|
44
|
+
'span': ['class'],
|
|
45
|
+
'th': ['align'],
|
|
46
|
+
'td': ['align'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// HTMLエスケープ関数(XSS対策用)
|
|
50
|
+
function escapeHtmlForXss(html) {
|
|
51
|
+
return html
|
|
52
|
+
.replace(/&/g, "&")
|
|
53
|
+
.replace(/</g, "<")
|
|
54
|
+
.replace(/>/g, ">")
|
|
55
|
+
.replace(/"/g, """)
|
|
56
|
+
.replace(/'/g, "'");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// href/src属性のURLバリデーション
|
|
60
|
+
function isSafeUrl(url) {
|
|
61
|
+
if (!url) return true;
|
|
62
|
+
// 空白・制御文字を除去して正規化
|
|
63
|
+
var normalized = url.toLowerCase().replace(/[\s\x00-\x1f]/g, '');
|
|
64
|
+
// HTMLエンティティのデコード(
 など)
|
|
65
|
+
var decoded = normalized.replace(/&#x?[0-9a-f]+;?/gi, '');
|
|
66
|
+
if (decoded.startsWith('javascript:')) return false;
|
|
67
|
+
if (decoded.startsWith('vbscript:')) return false;
|
|
68
|
+
if (decoded.startsWith('data:') && !decoded.startsWith('data:image/')) return false;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// HTML文字列をサニタイズ(ホワイトリストに含まれないタグ/属性を除去)
|
|
73
|
+
function sanitizeHtml(html) {
|
|
74
|
+
// より堅牢なタグマッチング:属性値内の < > を考慮
|
|
75
|
+
// 引用符で囲まれた属性値を正しく処理するパターン
|
|
76
|
+
var tagPattern = /<\/?([a-z][a-z0-9]*)((?:\s+[a-z][a-z0-9-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>"']*))?)*)\s*\/?>/gi;
|
|
77
|
+
|
|
78
|
+
return html.replace(tagPattern, function(match, tag, attrsStr) {
|
|
79
|
+
var tagLower = tag.toLowerCase();
|
|
80
|
+
|
|
81
|
+
// 許可されていないタグはエスケープ
|
|
82
|
+
if (!allowedTags.has(tagLower)) {
|
|
83
|
+
return escapeHtmlForXss(match);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 終了タグはそのまま
|
|
87
|
+
if (match.startsWith('</')) {
|
|
88
|
+
return '</' + tagLower + '>';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 属性をフィルタリング
|
|
92
|
+
var allowed = allowedAttributes[tagLower] || [];
|
|
93
|
+
var safeAttrs = [];
|
|
94
|
+
|
|
95
|
+
// 属性を解析(引用符で囲まれた値を正しく処理)
|
|
96
|
+
var attrRegex = /([a-z][a-z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"']*))/gi;
|
|
97
|
+
var attrMatch;
|
|
98
|
+
while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
|
|
99
|
+
var attrName = attrMatch[1].toLowerCase();
|
|
100
|
+
var attrValue = attrMatch[2] !== undefined ? attrMatch[2] :
|
|
101
|
+
attrMatch[3] !== undefined ? attrMatch[3] :
|
|
102
|
+
attrMatch[4] || '';
|
|
103
|
+
|
|
104
|
+
// on*イベントハンドラは常に拒否
|
|
105
|
+
if (attrName.startsWith('on')) continue;
|
|
106
|
+
|
|
107
|
+
// 許可属性のみ
|
|
108
|
+
if (!allowed.includes(attrName)) continue;
|
|
109
|
+
|
|
110
|
+
// href/srcのURL検証
|
|
111
|
+
if ((attrName === 'href' || attrName === 'src') && !isSafeUrl(attrValue)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
safeAttrs.push(attrName + '="' + attrValue.replace(/"/g, '"') + '"');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var finalAttrs = safeAttrs.length > 0 ? ' ' + safeAttrs.join(' ') : '';
|
|
119
|
+
return '<' + tagLower + finalAttrs + '>';
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
marked.use({
|
|
124
|
+
renderer: {
|
|
125
|
+
// 生HTMLブロックをサニタイズ
|
|
126
|
+
html: function(token) {
|
|
127
|
+
var text = token.raw || token.text || token;
|
|
128
|
+
return sanitizeHtml(text);
|
|
129
|
+
},
|
|
130
|
+
// リンクに安全なURL検証を追加(別タブで開く)
|
|
131
|
+
link: function(href, title, text) {
|
|
132
|
+
href = href || "";
|
|
133
|
+
title = title || "";
|
|
134
|
+
text = text || "";
|
|
135
|
+
if (!isSafeUrl(href)) {
|
|
136
|
+
// 危険なURLはプレーンテキストとして表示
|
|
137
|
+
return escapeHtmlForXss(text);
|
|
138
|
+
}
|
|
139
|
+
var titleAttr = title ? ' title="' + escapeHtmlForXss(title) + '"' : "";
|
|
140
|
+
return '<a href="' + escapeHtmlForXss(href) + '"' + titleAttr + ' target="_blank" rel="noopener noreferrer">' + text + '</a>';
|
|
141
|
+
},
|
|
142
|
+
// 画像にも安全なURL検証を追加
|
|
143
|
+
image: function(href, title, text) {
|
|
144
|
+
href = href || "";
|
|
145
|
+
title = title || "";
|
|
146
|
+
text = text || "";
|
|
147
|
+
if (!isSafeUrl(href)) {
|
|
148
|
+
return escapeHtmlForXss(text || "image");
|
|
149
|
+
}
|
|
150
|
+
var titleAttr = title ? ' title="' + escapeHtmlForXss(title) + '"' : "";
|
|
151
|
+
var altAttr = text ? ' alt="' + escapeHtmlForXss(text) + '"' : "";
|
|
152
|
+
return '<img src="' + escapeHtmlForXss(href) + '"' + altAttr + titleAttr + '>';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
23
157
|
// --- CLI arguments ---------------------------------------------------------
|
|
24
158
|
const VERSION = require("./package.json").version;
|
|
25
|
-
const args = process.argv.slice(2);
|
|
26
159
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
160
|
+
// ===== CLI設定のデフォルト値(import時に使用) =====
|
|
161
|
+
const DEFAULT_CONFIG = {
|
|
162
|
+
basePort: 4989,
|
|
163
|
+
encodingOpt: null,
|
|
164
|
+
noOpen: false,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ===== グローバル設定変数(デフォルト値で初期化、require.main時に更新) =====
|
|
168
|
+
let basePort = DEFAULT_CONFIG.basePort;
|
|
169
|
+
let encodingOpt = DEFAULT_CONFIG.encodingOpt;
|
|
170
|
+
let noOpen = DEFAULT_CONFIG.noOpen;
|
|
31
171
|
let stdinMode = false;
|
|
32
172
|
let diffMode = false;
|
|
33
173
|
let stdinContent = null;
|
|
174
|
+
let resolvedPaths = []; // ファイルパス(require.main時に設定)
|
|
34
175
|
|
|
35
176
|
function showHelp() {
|
|
36
177
|
console.log(`reviw v${VERSION} - Lightweight file reviewer with in-browser comments
|
|
@@ -75,25 +216,55 @@ function showVersion() {
|
|
|
75
216
|
console.log(VERSION);
|
|
76
217
|
}
|
|
77
218
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
219
|
+
// ===== CLI引数パース関数(require.main時のみ呼ばれる) =====
|
|
220
|
+
function parseCliArgs(argv) {
|
|
221
|
+
const args = argv.slice(2);
|
|
222
|
+
const config = { ...DEFAULT_CONFIG };
|
|
223
|
+
const filePaths = [];
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
226
|
+
const arg = args[i];
|
|
227
|
+
if (arg === "--port" && args[i + 1]) {
|
|
228
|
+
config.basePort = Number(args[i + 1]);
|
|
229
|
+
i += 1;
|
|
230
|
+
} else if ((arg === "--encoding" || arg === "-e") && args[i + 1]) {
|
|
231
|
+
config.encodingOpt = args[i + 1];
|
|
232
|
+
i += 1;
|
|
233
|
+
} else if (arg === "--no-open") {
|
|
234
|
+
config.noOpen = true;
|
|
235
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
236
|
+
showHelp();
|
|
237
|
+
process.exit(0);
|
|
238
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
239
|
+
showVersion();
|
|
240
|
+
process.exit(0);
|
|
241
|
+
} else if (!arg.startsWith("-")) {
|
|
242
|
+
filePaths.push(arg);
|
|
243
|
+
}
|
|
96
244
|
}
|
|
245
|
+
|
|
246
|
+
return { config, filePaths };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ===== ファイルパス検証・解決関数(require.main時のみ呼ばれる) =====
|
|
250
|
+
function validateAndResolvePaths(filePaths) {
|
|
251
|
+
const resolved = [];
|
|
252
|
+
for (const fp of filePaths) {
|
|
253
|
+
const resolvedPath = path.resolve(fp);
|
|
254
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
255
|
+
console.error(`File not found: ${resolvedPath}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
const stat = fs.statSync(resolvedPath);
|
|
259
|
+
if (stat.isDirectory()) {
|
|
260
|
+
console.error(`Cannot open directory: ${resolvedPath}`);
|
|
261
|
+
console.error(`Usage: reviw <file> [file2...]`);
|
|
262
|
+
console.error(`Please specify a file, not a directory.`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
resolved.push(resolvedPath);
|
|
266
|
+
}
|
|
267
|
+
return resolved;
|
|
97
268
|
}
|
|
98
269
|
|
|
99
270
|
// Check if stdin has data (pipe mode)
|
|
@@ -148,24 +319,6 @@ function runGitDiff() {
|
|
|
148
319
|
});
|
|
149
320
|
}
|
|
150
321
|
|
|
151
|
-
// Validate all files exist and are not directories (if files specified)
|
|
152
|
-
const resolvedPaths = [];
|
|
153
|
-
for (const fp of filePaths) {
|
|
154
|
-
const resolved = path.resolve(fp);
|
|
155
|
-
if (!fs.existsSync(resolved)) {
|
|
156
|
-
console.error(`File not found: ${resolved}`);
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
const stat = fs.statSync(resolved);
|
|
160
|
-
if (stat.isDirectory()) {
|
|
161
|
-
console.error(`Cannot open directory: ${resolved}`);
|
|
162
|
-
console.error(`Usage: reviw <file> [file2...]`);
|
|
163
|
-
console.error(`Please specify a file, not a directory.`);
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
resolvedPaths.push(resolved);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
322
|
// --- Diff parsing -----------------------------------------------------------
|
|
170
323
|
function parseDiff(diffText) {
|
|
171
324
|
const files = [];
|
|
@@ -313,7 +466,8 @@ function loadDiff(diffText) {
|
|
|
313
466
|
return {
|
|
314
467
|
rows,
|
|
315
468
|
files: sortedFiles,
|
|
316
|
-
|
|
469
|
+
projectRoot: "",
|
|
470
|
+
relativePath: "Git Diff",
|
|
317
471
|
mode: "diff",
|
|
318
472
|
};
|
|
319
473
|
}
|
|
@@ -422,7 +576,7 @@ function loadCsv(filePath) {
|
|
|
422
576
|
return {
|
|
423
577
|
rows,
|
|
424
578
|
cols: Math.max(1, maxCols),
|
|
425
|
-
|
|
579
|
+
...formatTitlePaths(filePath),
|
|
426
580
|
};
|
|
427
581
|
}
|
|
428
582
|
|
|
@@ -433,7 +587,7 @@ function loadText(filePath) {
|
|
|
433
587
|
return {
|
|
434
588
|
rows: lines.map((line) => [line]),
|
|
435
589
|
cols: 1,
|
|
436
|
-
|
|
590
|
+
...formatTitlePaths(filePath),
|
|
437
591
|
preview: null,
|
|
438
592
|
};
|
|
439
593
|
}
|
|
@@ -446,6 +600,7 @@ function loadMarkdown(filePath) {
|
|
|
446
600
|
// Parse YAML frontmatter
|
|
447
601
|
let frontmatterHtml = "";
|
|
448
602
|
let contentStart = 0;
|
|
603
|
+
let reviwQuestions = []; // Extract reviw questions for modal
|
|
449
604
|
|
|
450
605
|
if (lines[0] && lines[0].trim() === "---") {
|
|
451
606
|
let frontmatterEnd = -1;
|
|
@@ -463,30 +618,91 @@ function loadMarkdown(filePath) {
|
|
|
463
618
|
try {
|
|
464
619
|
const frontmatter = yaml.load(frontmatterText);
|
|
465
620
|
if (frontmatter && typeof frontmatter === "object") {
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
621
|
+
// Extract reviw questions if present
|
|
622
|
+
if (frontmatter.reviw && Array.isArray(frontmatter.reviw.questions)) {
|
|
623
|
+
reviwQuestions = frontmatter.reviw.questions.map((q, idx) => ({
|
|
624
|
+
id: q.id || "q" + (idx + 1),
|
|
625
|
+
question: q.question || "",
|
|
626
|
+
resolved: q.resolved === true,
|
|
627
|
+
answer: q.answer || "",
|
|
628
|
+
options: Array.isArray(q.options) ? q.options : [],
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Create HTML table for frontmatter (show reviw questions in detail for 1:1 correspondence with YAML source)
|
|
633
|
+
const displayFrontmatter = { ...frontmatter };
|
|
634
|
+
// Keep reviw as-is for detailed rendering
|
|
635
|
+
|
|
636
|
+
if (Object.keys(displayFrontmatter).length > 0) {
|
|
637
|
+
frontmatterHtml = '<div class="frontmatter-table"><table>';
|
|
638
|
+
frontmatterHtml += '<colgroup><col style="width:12%"><col style="width:88%"></colgroup>';
|
|
639
|
+
frontmatterHtml += '<thead><tr><th colspan="2">Document Metadata</th></tr></thead>';
|
|
640
|
+
frontmatterHtml += "<tbody>";
|
|
641
|
+
|
|
642
|
+
// Render reviw.questions as detailed cards
|
|
643
|
+
function renderReviwQuestions(questions) {
|
|
644
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
645
|
+
return '<span class="fm-tag">質問なし</span>';
|
|
646
|
+
}
|
|
647
|
+
let html = '<div class="reviw-questions-preview">';
|
|
648
|
+
questions.forEach((q, idx) => {
|
|
649
|
+
const statusIcon = q.resolved ? '✅' : '⏳';
|
|
650
|
+
const statusClass = q.resolved ? 'resolved' : 'pending';
|
|
651
|
+
html += '<div class="reviw-q-card ' + statusClass + '">';
|
|
652
|
+
html += '<div class="reviw-q-header">' + statusIcon + ' <strong>' + escapeHtmlChars(q.id || 'Q' + (idx + 1)) + '</strong></div>';
|
|
653
|
+
html += '<div class="reviw-q-question">' + escapeHtmlChars(q.question || '') + '</div>';
|
|
654
|
+
if (q.options && Array.isArray(q.options) && q.options.length > 0) {
|
|
655
|
+
html += '<div class="reviw-q-options">';
|
|
656
|
+
q.options.forEach(opt => {
|
|
657
|
+
html += '<span class="fm-tag">' + escapeHtmlChars(opt) + '</span>';
|
|
658
|
+
});
|
|
659
|
+
html += '</div>';
|
|
660
|
+
}
|
|
661
|
+
if (q.answer) {
|
|
662
|
+
html += '<div class="reviw-q-answer">💬 ' + escapeHtmlChars(q.answer) + '</div>';
|
|
663
|
+
}
|
|
664
|
+
html += '</div>';
|
|
665
|
+
});
|
|
666
|
+
html += '</div>';
|
|
667
|
+
return html;
|
|
477
668
|
}
|
|
478
|
-
|
|
479
|
-
|
|
669
|
+
|
|
670
|
+
function renderValue(val, key) {
|
|
671
|
+
// Special handling for reviw object
|
|
672
|
+
if (key === 'reviw' && typeof val === 'object' && val !== null) {
|
|
673
|
+
let html = '';
|
|
674
|
+
if (val.questions && Array.isArray(val.questions)) {
|
|
675
|
+
html += renderReviwQuestions(val.questions);
|
|
676
|
+
}
|
|
677
|
+
// Render other reviw properties
|
|
678
|
+
const { questions, ...rest } = val;
|
|
679
|
+
if (Object.keys(rest).length > 0) {
|
|
680
|
+
html += '<div style="margin-top: 8px;">';
|
|
681
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
682
|
+
html += '<div><strong>' + escapeHtmlChars(k) + ':</strong> ' + escapeHtmlChars(String(v)) + '</div>';
|
|
683
|
+
}
|
|
684
|
+
html += '</div>';
|
|
685
|
+
}
|
|
686
|
+
return html || '<span class="fm-tag">-</span>';
|
|
687
|
+
}
|
|
688
|
+
if (Array.isArray(val)) {
|
|
689
|
+
return val
|
|
690
|
+
.map((v) => '<span class="fm-tag">' + escapeHtmlChars(String(v)) + "</span>")
|
|
691
|
+
.join(" ");
|
|
692
|
+
}
|
|
693
|
+
if (typeof val === "object" && val !== null) {
|
|
694
|
+
return "<pre>" + escapeHtmlChars(JSON.stringify(val, null, 2)) + "</pre>";
|
|
695
|
+
}
|
|
696
|
+
return escapeHtmlChars(String(val));
|
|
480
697
|
}
|
|
481
|
-
return escapeHtmlChars(String(val));
|
|
482
|
-
}
|
|
483
698
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
699
|
+
for (const [key, val] of Object.entries(displayFrontmatter)) {
|
|
700
|
+
frontmatterHtml +=
|
|
701
|
+
"<tr><th>" + escapeHtmlChars(key) + "</th><td>" + renderValue(val, key) + "</td></tr>";
|
|
702
|
+
}
|
|
488
703
|
|
|
489
|
-
|
|
704
|
+
frontmatterHtml += "</tbody></table></div>";
|
|
705
|
+
}
|
|
490
706
|
contentStart = frontmatterEnd + 1;
|
|
491
707
|
}
|
|
492
708
|
} catch (e) {
|
|
@@ -502,8 +718,9 @@ function loadMarkdown(filePath) {
|
|
|
502
718
|
return {
|
|
503
719
|
rows: lines.map((line) => [line]),
|
|
504
720
|
cols: 1,
|
|
505
|
-
|
|
721
|
+
...formatTitlePaths(filePath),
|
|
506
722
|
preview,
|
|
723
|
+
reviwQuestions, // Pass questions to UI
|
|
507
724
|
};
|
|
508
725
|
}
|
|
509
726
|
|
|
@@ -515,6 +732,20 @@ function escapeHtmlChars(str) {
|
|
|
515
732
|
.replace(/"/g, """);
|
|
516
733
|
}
|
|
517
734
|
|
|
735
|
+
function formatTitlePaths(filePath) {
|
|
736
|
+
const cwd = process.cwd();
|
|
737
|
+
const home = os.homedir();
|
|
738
|
+
const relativePath = path.relative(cwd, filePath) || path.basename(filePath);
|
|
739
|
+
let projectRoot = cwd;
|
|
740
|
+
if (projectRoot.startsWith(home)) {
|
|
741
|
+
projectRoot = "~" + projectRoot.slice(home.length);
|
|
742
|
+
}
|
|
743
|
+
if (!projectRoot.endsWith("/")) {
|
|
744
|
+
projectRoot += "/";
|
|
745
|
+
}
|
|
746
|
+
return { projectRoot, relativePath };
|
|
747
|
+
}
|
|
748
|
+
|
|
518
749
|
function loadData(filePath) {
|
|
519
750
|
// Check if path exists
|
|
520
751
|
if (!fs.existsSync(filePath)) {
|
|
@@ -562,7 +793,7 @@ function serializeForScript(value) {
|
|
|
562
793
|
}
|
|
563
794
|
|
|
564
795
|
function diffHtmlTemplate(diffData) {
|
|
565
|
-
const { rows,
|
|
796
|
+
const { rows, projectRoot, relativePath } = diffData;
|
|
566
797
|
const serialized = serializeForScript(rows);
|
|
567
798
|
const fileCount = rows.filter((r) => r.type === "file").length;
|
|
568
799
|
|
|
@@ -574,7 +805,7 @@ function diffHtmlTemplate(diffData) {
|
|
|
574
805
|
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
|
|
575
806
|
<meta http-equiv="Pragma" content="no-cache" />
|
|
576
807
|
<meta http-equiv="Expires" content="0" />
|
|
577
|
-
<title>${
|
|
808
|
+
<title>${relativePath} | reviw</title>
|
|
578
809
|
<style>
|
|
579
810
|
:root {
|
|
580
811
|
color-scheme: dark;
|
|
@@ -643,7 +874,9 @@ function diffHtmlTemplate(diffData) {
|
|
|
643
874
|
}
|
|
644
875
|
header .meta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
645
876
|
header .actions { display: flex; gap: 8px; align-items: center; }
|
|
646
|
-
header h1 {
|
|
877
|
+
header h1 { display: flex; flex-direction: column; margin: 0; line-height: 1.3; }
|
|
878
|
+
header h1 .title-path { font-size: 11px; font-weight: 400; color: var(--muted); }
|
|
879
|
+
header h1 .title-file { font-size: 16px; font-weight: 600; }
|
|
647
880
|
header .badge {
|
|
648
881
|
background: var(--selected-bg);
|
|
649
882
|
color: var(--text);
|
|
@@ -806,11 +1039,14 @@ function diffHtmlTemplate(diffData) {
|
|
|
806
1039
|
display: none;
|
|
807
1040
|
}
|
|
808
1041
|
.floating header {
|
|
809
|
-
position:
|
|
1042
|
+
position: static;
|
|
810
1043
|
background: transparent;
|
|
1044
|
+
backdrop-filter: none;
|
|
811
1045
|
border: none;
|
|
812
1046
|
padding: 0 0 10px 0;
|
|
1047
|
+
display: flex;
|
|
813
1048
|
justify-content: space-between;
|
|
1049
|
+
align-items: center;
|
|
814
1050
|
}
|
|
815
1051
|
.floating h2 { font-size: 14px; margin: 0; font-weight: 600; }
|
|
816
1052
|
.floating button {
|
|
@@ -835,6 +1071,10 @@ function diffHtmlTemplate(diffData) {
|
|
|
835
1071
|
font-size: 13px;
|
|
836
1072
|
font-family: inherit;
|
|
837
1073
|
}
|
|
1074
|
+
.floating textarea:focus {
|
|
1075
|
+
outline: none;
|
|
1076
|
+
border-color: var(--accent);
|
|
1077
|
+
}
|
|
838
1078
|
.floating .actions {
|
|
839
1079
|
display: flex;
|
|
840
1080
|
gap: 8px;
|
|
@@ -952,7 +1192,7 @@ function diffHtmlTemplate(diffData) {
|
|
|
952
1192
|
<body>
|
|
953
1193
|
<header>
|
|
954
1194
|
<div class="meta">
|
|
955
|
-
<h1>${title}</h1>
|
|
1195
|
+
<h1>${projectRoot ? `<span class="title-path">${projectRoot}</span>` : ""}<span class="title-file">${relativePath}</span></h1>
|
|
956
1196
|
<span class="badge">${fileCount} file${fileCount !== 1 ? "s" : ""} changed</span>
|
|
957
1197
|
<span class="pill">Comments <strong id="comment-count">0</strong></span>
|
|
958
1198
|
</div>
|
|
@@ -1387,7 +1627,19 @@ function diffHtmlTemplate(diffData) {
|
|
|
1387
1627
|
globalComment = globalCommentInput.value;
|
|
1388
1628
|
hideSubmitModal();
|
|
1389
1629
|
sendAndExit('button');
|
|
1390
|
-
|
|
1630
|
+
// Try to close window; if it fails (browser security), show completion message
|
|
1631
|
+
setTimeout(() => {
|
|
1632
|
+
window.close();
|
|
1633
|
+
// If window.close() didn't work, show a completion message
|
|
1634
|
+
setTimeout(() => {
|
|
1635
|
+
document.body.innerHTML = \`
|
|
1636
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;background:var(--bg,#1a1a2e);color:var(--text,#e0e0e0);font-family:system-ui,sans-serif;">
|
|
1637
|
+
<h1 style="font-size:2rem;margin-bottom:1rem;">✅ Review Submitted</h1>
|
|
1638
|
+
<p style="color:var(--muted,#888);">You can close this tab now.</p>
|
|
1639
|
+
</div>
|
|
1640
|
+
\`;
|
|
1641
|
+
}, 100);
|
|
1642
|
+
}, 200);
|
|
1391
1643
|
}
|
|
1392
1644
|
document.getElementById('modal-submit').addEventListener('click', doSubmit);
|
|
1393
1645
|
globalCommentInput.addEventListener('keydown', e => {
|
|
@@ -1430,10 +1682,11 @@ function diffHtmlTemplate(diffData) {
|
|
|
1430
1682
|
}
|
|
1431
1683
|
|
|
1432
1684
|
// --- HTML template ---------------------------------------------------------
|
|
1433
|
-
function htmlTemplate(dataRows, cols,
|
|
1685
|
+
function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHtml, reviwQuestions = []) {
|
|
1434
1686
|
const serialized = serializeForScript(dataRows);
|
|
1435
1687
|
const modeJson = serializeForScript(mode);
|
|
1436
|
-
const titleJson = serializeForScript(
|
|
1688
|
+
const titleJson = serializeForScript(relativePath); // Use relativePath as file identifier
|
|
1689
|
+
const questionsJson = serializeForScript(reviwQuestions || []);
|
|
1437
1690
|
const hasPreview = !!previewHtml;
|
|
1438
1691
|
return `<!doctype html>
|
|
1439
1692
|
<html lang="ja">
|
|
@@ -1443,7 +1696,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1443
1696
|
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
|
|
1444
1697
|
<meta http-equiv="Pragma" content="no-cache" />
|
|
1445
1698
|
<meta http-equiv="Expires" content="0" />
|
|
1446
|
-
<title>${
|
|
1699
|
+
<title>${relativePath} | reviw</title>
|
|
1447
1700
|
<style>
|
|
1448
1701
|
/* Dark theme (default) */
|
|
1449
1702
|
:root {
|
|
@@ -1470,6 +1723,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1470
1723
|
--hover-bg: rgba(96,165,250,0.08);
|
|
1471
1724
|
--shadow-color: rgba(0,0,0,0.35);
|
|
1472
1725
|
--code-bg: #1e293b;
|
|
1726
|
+
--error: #dc3545;
|
|
1473
1727
|
}
|
|
1474
1728
|
/* Light theme */
|
|
1475
1729
|
[data-theme="light"] {
|
|
@@ -1496,6 +1750,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1496
1750
|
--hover-bg: rgba(59,130,246,0.06);
|
|
1497
1751
|
--shadow-color: rgba(0,0,0,0.1);
|
|
1498
1752
|
--code-bg: #f1f5f9;
|
|
1753
|
+
--error: #dc3545;
|
|
1499
1754
|
}
|
|
1500
1755
|
* { box-sizing: border-box; }
|
|
1501
1756
|
body {
|
|
@@ -1522,7 +1777,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1522
1777
|
}
|
|
1523
1778
|
header .meta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
1524
1779
|
header .actions { display: flex; gap: 8px; align-items: center; }
|
|
1525
|
-
header h1 {
|
|
1780
|
+
header h1 { display: flex; flex-direction: column; margin: 0; line-height: 1.3; }
|
|
1781
|
+
header h1 .title-path { font-size: 11px; font-weight: 400; color: var(--muted); }
|
|
1782
|
+
header h1 .title-file { font-size: 16px; font-weight: 700; }
|
|
1526
1783
|
header .badge {
|
|
1527
1784
|
background: var(--selected-bg);
|
|
1528
1785
|
color: var(--text);
|
|
@@ -1743,12 +2000,14 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1743
2000
|
transition: background 200ms ease, border-color 200ms ease;
|
|
1744
2001
|
}
|
|
1745
2002
|
.floating header {
|
|
1746
|
-
position:
|
|
1747
|
-
top: 0;
|
|
2003
|
+
position: static;
|
|
1748
2004
|
background: transparent;
|
|
2005
|
+
backdrop-filter: none;
|
|
1749
2006
|
border: none;
|
|
1750
2007
|
padding: 0 0 8px 0;
|
|
2008
|
+
display: flex;
|
|
1751
2009
|
justify-content: space-between;
|
|
2010
|
+
align-items: center;
|
|
1752
2011
|
}
|
|
1753
2012
|
.floating h2 { font-size: 14px; margin: 0; color: var(--text); }
|
|
1754
2013
|
.floating button {
|
|
@@ -1777,6 +2036,10 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1777
2036
|
line-height: 1.4;
|
|
1778
2037
|
transition: background 200ms ease, border-color 200ms ease;
|
|
1779
2038
|
}
|
|
2039
|
+
.floating textarea:focus {
|
|
2040
|
+
outline: none;
|
|
2041
|
+
border-color: var(--accent);
|
|
2042
|
+
}
|
|
1780
2043
|
.floating .actions {
|
|
1781
2044
|
display: flex;
|
|
1782
2045
|
gap: 8px;
|
|
@@ -1954,6 +2217,50 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1954
2217
|
border-radius: 4px;
|
|
1955
2218
|
font-size: 11px;
|
|
1956
2219
|
}
|
|
2220
|
+
/* Reviw questions preview cards */
|
|
2221
|
+
.reviw-questions-preview {
|
|
2222
|
+
display: flex;
|
|
2223
|
+
flex-direction: column;
|
|
2224
|
+
gap: 8px;
|
|
2225
|
+
}
|
|
2226
|
+
.reviw-q-card {
|
|
2227
|
+
background: var(--code-bg);
|
|
2228
|
+
border: 1px solid var(--border);
|
|
2229
|
+
border-radius: 8px;
|
|
2230
|
+
padding: 10px 12px;
|
|
2231
|
+
}
|
|
2232
|
+
.reviw-q-card.resolved {
|
|
2233
|
+
border-left: 3px solid #22c55e;
|
|
2234
|
+
}
|
|
2235
|
+
.reviw-q-card.pending {
|
|
2236
|
+
border-left: 3px solid #f59e0b;
|
|
2237
|
+
}
|
|
2238
|
+
.reviw-q-header {
|
|
2239
|
+
font-size: 12px;
|
|
2240
|
+
color: var(--text-dim);
|
|
2241
|
+
margin-bottom: 4px;
|
|
2242
|
+
}
|
|
2243
|
+
.reviw-q-header strong {
|
|
2244
|
+
color: var(--accent);
|
|
2245
|
+
}
|
|
2246
|
+
.reviw-q-question {
|
|
2247
|
+
font-size: 13px;
|
|
2248
|
+
color: var(--text);
|
|
2249
|
+
margin-bottom: 6px;
|
|
2250
|
+
}
|
|
2251
|
+
.reviw-q-options {
|
|
2252
|
+
display: flex;
|
|
2253
|
+
flex-wrap: wrap;
|
|
2254
|
+
gap: 4px;
|
|
2255
|
+
margin-bottom: 6px;
|
|
2256
|
+
}
|
|
2257
|
+
.reviw-q-answer {
|
|
2258
|
+
font-size: 12px;
|
|
2259
|
+
color: #22c55e;
|
|
2260
|
+
background: rgba(34, 197, 94, 0.1);
|
|
2261
|
+
padding: 4px 8px;
|
|
2262
|
+
border-radius: 4px;
|
|
2263
|
+
}
|
|
1957
2264
|
[data-theme="light"] .frontmatter-table tbody th {
|
|
1958
2265
|
color: #7c3aed;
|
|
1959
2266
|
}
|
|
@@ -1973,9 +2280,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1973
2280
|
text-align: left;
|
|
1974
2281
|
border-bottom: 1px solid var(--border);
|
|
1975
2282
|
}
|
|
1976
|
-
.md-preview table:not(.frontmatter-table table) th {
|
|
1977
|
-
background: rgba(255,255,255,0.05);
|
|
1978
|
-
}
|
|
1979
2283
|
.md-preview table:not(.frontmatter-table table) th {
|
|
1980
2284
|
background: var(--panel);
|
|
1981
2285
|
font-weight: 600;
|
|
@@ -2104,6 +2408,267 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2104
2408
|
border-radius: 8px;
|
|
2105
2409
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
2106
2410
|
}
|
|
2411
|
+
/* Reviw Questions Modal */
|
|
2412
|
+
.reviw-questions-overlay {
|
|
2413
|
+
display: none;
|
|
2414
|
+
position: fixed;
|
|
2415
|
+
inset: 0;
|
|
2416
|
+
background: rgba(0, 0, 0, 0.8);
|
|
2417
|
+
z-index: 1100;
|
|
2418
|
+
justify-content: center;
|
|
2419
|
+
align-items: center;
|
|
2420
|
+
}
|
|
2421
|
+
.reviw-questions-overlay.visible {
|
|
2422
|
+
display: flex;
|
|
2423
|
+
}
|
|
2424
|
+
.reviw-questions-modal {
|
|
2425
|
+
background: var(--card-bg);
|
|
2426
|
+
border: 1px solid var(--border);
|
|
2427
|
+
border-radius: 16px;
|
|
2428
|
+
width: 90%;
|
|
2429
|
+
max-width: 600px;
|
|
2430
|
+
max-height: 80vh;
|
|
2431
|
+
display: flex;
|
|
2432
|
+
flex-direction: column;
|
|
2433
|
+
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
|
|
2434
|
+
}
|
|
2435
|
+
.reviw-questions-header {
|
|
2436
|
+
display: flex;
|
|
2437
|
+
justify-content: space-between;
|
|
2438
|
+
align-items: center;
|
|
2439
|
+
padding: 16px 20px;
|
|
2440
|
+
border-bottom: 1px solid var(--border);
|
|
2441
|
+
}
|
|
2442
|
+
.reviw-questions-header h2 {
|
|
2443
|
+
margin: 0;
|
|
2444
|
+
font-size: 16px;
|
|
2445
|
+
font-weight: 600;
|
|
2446
|
+
color: var(--text);
|
|
2447
|
+
}
|
|
2448
|
+
.reviw-questions-header h2 span {
|
|
2449
|
+
font-size: 14px;
|
|
2450
|
+
color: var(--text-dim);
|
|
2451
|
+
font-weight: 400;
|
|
2452
|
+
}
|
|
2453
|
+
.reviw-questions-close {
|
|
2454
|
+
width: 32px;
|
|
2455
|
+
height: 32px;
|
|
2456
|
+
border: none;
|
|
2457
|
+
background: transparent;
|
|
2458
|
+
color: var(--text-dim);
|
|
2459
|
+
font-size: 18px;
|
|
2460
|
+
cursor: pointer;
|
|
2461
|
+
border-radius: 8px;
|
|
2462
|
+
transition: all 150ms ease;
|
|
2463
|
+
}
|
|
2464
|
+
.reviw-questions-close:hover {
|
|
2465
|
+
background: var(--border);
|
|
2466
|
+
color: var(--text);
|
|
2467
|
+
}
|
|
2468
|
+
.reviw-questions-body {
|
|
2469
|
+
flex: 1;
|
|
2470
|
+
overflow-y: auto;
|
|
2471
|
+
padding: 16px 20px;
|
|
2472
|
+
}
|
|
2473
|
+
.reviw-questions-footer {
|
|
2474
|
+
padding: 12px 20px;
|
|
2475
|
+
border-top: 1px solid var(--border);
|
|
2476
|
+
display: flex;
|
|
2477
|
+
justify-content: flex-end;
|
|
2478
|
+
}
|
|
2479
|
+
.reviw-questions-later {
|
|
2480
|
+
padding: 8px 16px;
|
|
2481
|
+
border: 1px solid var(--border);
|
|
2482
|
+
background: transparent;
|
|
2483
|
+
color: var(--text-dim);
|
|
2484
|
+
border-radius: 8px;
|
|
2485
|
+
cursor: pointer;
|
|
2486
|
+
font-size: 13px;
|
|
2487
|
+
transition: all 150ms ease;
|
|
2488
|
+
}
|
|
2489
|
+
.reviw-questions-later:hover {
|
|
2490
|
+
background: var(--border);
|
|
2491
|
+
color: var(--text);
|
|
2492
|
+
}
|
|
2493
|
+
/* Question Item */
|
|
2494
|
+
.reviw-question-item {
|
|
2495
|
+
margin-bottom: 20px;
|
|
2496
|
+
padding-bottom: 20px;
|
|
2497
|
+
border-bottom: 1px solid var(--border);
|
|
2498
|
+
}
|
|
2499
|
+
.reviw-question-item:last-child {
|
|
2500
|
+
margin-bottom: 0;
|
|
2501
|
+
padding-bottom: 0;
|
|
2502
|
+
border-bottom: none;
|
|
2503
|
+
}
|
|
2504
|
+
.reviw-question-text {
|
|
2505
|
+
font-size: 14px;
|
|
2506
|
+
color: var(--text);
|
|
2507
|
+
margin-bottom: 12px;
|
|
2508
|
+
line-height: 1.5;
|
|
2509
|
+
}
|
|
2510
|
+
.reviw-question-options {
|
|
2511
|
+
display: flex;
|
|
2512
|
+
flex-wrap: wrap;
|
|
2513
|
+
gap: 8px;
|
|
2514
|
+
margin-bottom: 12px;
|
|
2515
|
+
}
|
|
2516
|
+
.reviw-question-option {
|
|
2517
|
+
padding: 8px 14px;
|
|
2518
|
+
border: 1px solid var(--border);
|
|
2519
|
+
background: transparent;
|
|
2520
|
+
color: var(--text);
|
|
2521
|
+
border-radius: 8px;
|
|
2522
|
+
cursor: pointer;
|
|
2523
|
+
font-size: 13px;
|
|
2524
|
+
transition: all 150ms ease;
|
|
2525
|
+
}
|
|
2526
|
+
.reviw-question-option:hover {
|
|
2527
|
+
border-color: var(--accent);
|
|
2528
|
+
background: rgba(96, 165, 250, 0.1);
|
|
2529
|
+
}
|
|
2530
|
+
.reviw-question-option.selected {
|
|
2531
|
+
border-color: var(--accent);
|
|
2532
|
+
background: var(--accent);
|
|
2533
|
+
color: var(--text-inverse);
|
|
2534
|
+
}
|
|
2535
|
+
.reviw-question-input {
|
|
2536
|
+
width: 100%;
|
|
2537
|
+
padding: 10px 12px;
|
|
2538
|
+
border: 1px solid var(--border);
|
|
2539
|
+
background: var(--input-bg);
|
|
2540
|
+
color: var(--text);
|
|
2541
|
+
border-radius: 8px;
|
|
2542
|
+
font-size: 13px;
|
|
2543
|
+
resize: vertical;
|
|
2544
|
+
min-height: 60px;
|
|
2545
|
+
}
|
|
2546
|
+
.reviw-question-input:focus {
|
|
2547
|
+
outline: none;
|
|
2548
|
+
border-color: var(--accent);
|
|
2549
|
+
}
|
|
2550
|
+
.reviw-question-input::placeholder {
|
|
2551
|
+
color: var(--text-dim);
|
|
2552
|
+
}
|
|
2553
|
+
.reviw-check-mark {
|
|
2554
|
+
color: #22c55e;
|
|
2555
|
+
font-weight: bold;
|
|
2556
|
+
}
|
|
2557
|
+
.reviw-question-item.answered {
|
|
2558
|
+
border-color: #22c55e;
|
|
2559
|
+
background: rgba(34, 197, 94, 0.05);
|
|
2560
|
+
}
|
|
2561
|
+
.reviw-question-submit {
|
|
2562
|
+
margin-top: 10px;
|
|
2563
|
+
padding: 8px 16px;
|
|
2564
|
+
border: none;
|
|
2565
|
+
background: var(--accent);
|
|
2566
|
+
color: var(--text-inverse);
|
|
2567
|
+
border-radius: 8px;
|
|
2568
|
+
cursor: pointer;
|
|
2569
|
+
font-size: 13px;
|
|
2570
|
+
font-weight: 500;
|
|
2571
|
+
transition: all 150ms ease;
|
|
2572
|
+
}
|
|
2573
|
+
.reviw-question-submit:hover {
|
|
2574
|
+
filter: brightness(1.1);
|
|
2575
|
+
}
|
|
2576
|
+
.reviw-question-submit:disabled {
|
|
2577
|
+
opacity: 0.5;
|
|
2578
|
+
cursor: not-allowed;
|
|
2579
|
+
}
|
|
2580
|
+
/* Resolved Section */
|
|
2581
|
+
.reviw-resolved-section {
|
|
2582
|
+
margin-top: 16px;
|
|
2583
|
+
border-top: 1px solid var(--border);
|
|
2584
|
+
padding-top: 12px;
|
|
2585
|
+
}
|
|
2586
|
+
.reviw-resolved-toggle {
|
|
2587
|
+
display: flex;
|
|
2588
|
+
align-items: center;
|
|
2589
|
+
gap: 8px;
|
|
2590
|
+
background: none;
|
|
2591
|
+
border: none;
|
|
2592
|
+
color: var(--text-dim);
|
|
2593
|
+
font-size: 13px;
|
|
2594
|
+
cursor: pointer;
|
|
2595
|
+
padding: 4px 0;
|
|
2596
|
+
}
|
|
2597
|
+
.reviw-resolved-toggle:hover {
|
|
2598
|
+
color: var(--text);
|
|
2599
|
+
}
|
|
2600
|
+
.reviw-resolved-toggle .arrow {
|
|
2601
|
+
transition: transform 150ms ease;
|
|
2602
|
+
}
|
|
2603
|
+
.reviw-resolved-toggle.open .arrow {
|
|
2604
|
+
transform: rotate(90deg);
|
|
2605
|
+
}
|
|
2606
|
+
.reviw-resolved-list {
|
|
2607
|
+
display: none;
|
|
2608
|
+
margin-top: 12px;
|
|
2609
|
+
}
|
|
2610
|
+
.reviw-resolved-list.visible {
|
|
2611
|
+
display: block;
|
|
2612
|
+
}
|
|
2613
|
+
.reviw-resolved-item {
|
|
2614
|
+
padding: 10px 12px;
|
|
2615
|
+
background: var(--input-bg);
|
|
2616
|
+
border-radius: 8px;
|
|
2617
|
+
margin-bottom: 8px;
|
|
2618
|
+
opacity: 0.7;
|
|
2619
|
+
}
|
|
2620
|
+
.reviw-resolved-item:last-child {
|
|
2621
|
+
margin-bottom: 0;
|
|
2622
|
+
}
|
|
2623
|
+
.reviw-resolved-q {
|
|
2624
|
+
font-size: 12px;
|
|
2625
|
+
color: var(--text-dim);
|
|
2626
|
+
margin-bottom: 4px;
|
|
2627
|
+
}
|
|
2628
|
+
.reviw-resolved-a {
|
|
2629
|
+
font-size: 13px;
|
|
2630
|
+
color: var(--text);
|
|
2631
|
+
}
|
|
2632
|
+
/* Notice Bar */
|
|
2633
|
+
.reviw-questions-bar {
|
|
2634
|
+
display: none;
|
|
2635
|
+
position: fixed;
|
|
2636
|
+
top: 0;
|
|
2637
|
+
left: 0;
|
|
2638
|
+
right: 0;
|
|
2639
|
+
background: var(--accent);
|
|
2640
|
+
color: var(--text-inverse);
|
|
2641
|
+
padding: 8px 16px;
|
|
2642
|
+
font-size: 13px;
|
|
2643
|
+
z-index: 1050;
|
|
2644
|
+
justify-content: center;
|
|
2645
|
+
align-items: center;
|
|
2646
|
+
gap: 12px;
|
|
2647
|
+
}
|
|
2648
|
+
.reviw-questions-bar.visible {
|
|
2649
|
+
display: flex;
|
|
2650
|
+
}
|
|
2651
|
+
.reviw-questions-bar button {
|
|
2652
|
+
padding: 4px 12px;
|
|
2653
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
2654
|
+
background: rgba(255, 255, 255, 0.1);
|
|
2655
|
+
color: var(--text-inverse);
|
|
2656
|
+
border-radius: 6px;
|
|
2657
|
+
cursor: pointer;
|
|
2658
|
+
font-size: 12px;
|
|
2659
|
+
transition: all 150ms ease;
|
|
2660
|
+
}
|
|
2661
|
+
.reviw-questions-bar button:hover {
|
|
2662
|
+
background: rgba(255, 255, 255, 0.2);
|
|
2663
|
+
}
|
|
2664
|
+
/* Adjust layout when bar is visible */
|
|
2665
|
+
body.has-questions-bar header {
|
|
2666
|
+
top: 36px;
|
|
2667
|
+
}
|
|
2668
|
+
body.has-questions-bar .toolbar,
|
|
2669
|
+
body.has-questions-bar .table-wrap {
|
|
2670
|
+
margin-top: 36px;
|
|
2671
|
+
}
|
|
2107
2672
|
/* Copy notification toast */
|
|
2108
2673
|
.copy-toast {
|
|
2109
2674
|
position: fixed;
|
|
@@ -2344,7 +2909,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2344
2909
|
bottom: 20px;
|
|
2345
2910
|
left: 50%;
|
|
2346
2911
|
transform: translateX(-50%);
|
|
2347
|
-
background:
|
|
2912
|
+
background: var(--error);
|
|
2348
2913
|
color: white;
|
|
2349
2914
|
padding: 12px 24px;
|
|
2350
2915
|
border-radius: 8px;
|
|
@@ -2366,7 +2931,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2366
2931
|
<body>
|
|
2367
2932
|
<header>
|
|
2368
2933
|
<div class="meta">
|
|
2369
|
-
<h1>${title}</h1>
|
|
2934
|
+
<h1><span class="title-path">${projectRoot}</span><span class="title-file">${relativePath}</span></h1>
|
|
2370
2935
|
<span class="badge">Click to comment / ESC to cancel</span>
|
|
2371
2936
|
<span class="pill">Comments <strong id="comment-count">0</strong></span>
|
|
2372
2937
|
</div>
|
|
@@ -2519,11 +3084,32 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2519
3084
|
<div class="video-container" id="video-container"></div>
|
|
2520
3085
|
</div>
|
|
2521
3086
|
|
|
3087
|
+
<!-- Reviw Questions Modal -->
|
|
3088
|
+
<div class="reviw-questions-overlay" id="reviw-questions-overlay">
|
|
3089
|
+
<div class="reviw-questions-modal" id="reviw-questions-modal">
|
|
3090
|
+
<div class="reviw-questions-header">
|
|
3091
|
+
<h2>📋 AIからの質問 <span id="reviw-questions-count"></span></h2>
|
|
3092
|
+
<button class="reviw-questions-close" id="reviw-questions-close" aria-label="Close">✕</button>
|
|
3093
|
+
</div>
|
|
3094
|
+
<div class="reviw-questions-body" id="reviw-questions-body"></div>
|
|
3095
|
+
<div class="reviw-questions-footer">
|
|
3096
|
+
<button class="reviw-questions-later" id="reviw-questions-later">後で回答する</button>
|
|
3097
|
+
</div>
|
|
3098
|
+
</div>
|
|
3099
|
+
</div>
|
|
3100
|
+
|
|
3101
|
+
<!-- Reviw Questions Notice Bar -->
|
|
3102
|
+
<div class="reviw-questions-bar" id="reviw-questions-bar">
|
|
3103
|
+
<span id="reviw-questions-bar-message">\ud83d\udccb \u672a\u56de\u7b54\u306e\u8cea\u554f\u304c<span id="reviw-questions-bar-count">0</span>\u4ef6\u3042\u308a\u307e\u3059</span>
|
|
3104
|
+
<button id="reviw-questions-bar-open">\u8cea\u554f\u3092\u898b\u308b</button>
|
|
3105
|
+
</div>
|
|
3106
|
+
|
|
2522
3107
|
<script>
|
|
2523
3108
|
const DATA = ${serialized};
|
|
2524
3109
|
const MAX_COLS = ${cols};
|
|
2525
3110
|
const FILE_NAME = ${titleJson};
|
|
2526
3111
|
const MODE = ${modeJson};
|
|
3112
|
+
const REVIW_QUESTIONS = ${questionsJson};
|
|
2527
3113
|
|
|
2528
3114
|
// --- Theme Management ---
|
|
2529
3115
|
(function initTheme() {
|
|
@@ -2882,6 +3468,12 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2882
3468
|
|
|
2883
3469
|
function openCardForSelection() {
|
|
2884
3470
|
if (!selection) return;
|
|
3471
|
+
// Don't open card while image/video modal is visible
|
|
3472
|
+
const imageOverlay = document.getElementById('image-fullscreen');
|
|
3473
|
+
const videoOverlay = document.getElementById('video-fullscreen');
|
|
3474
|
+
if (imageOverlay?.classList.contains('visible') || videoOverlay?.classList.contains('visible')) {
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
2885
3477
|
const { startRow, endRow, startCol, endCol } = selection;
|
|
2886
3478
|
const isSingleCell = startRow === endRow && startCol === endCol;
|
|
2887
3479
|
|
|
@@ -3398,6 +3990,22 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3398
3990
|
if (globalComment.trim()) {
|
|
3399
3991
|
data.summary = globalComment.trim();
|
|
3400
3992
|
}
|
|
3993
|
+
// Include answered questions
|
|
3994
|
+
if (window.REVIW_ANSWERS) {
|
|
3995
|
+
const answeredQuestions = [];
|
|
3996
|
+
for (const [id, answer] of Object.entries(window.REVIW_ANSWERS)) {
|
|
3997
|
+
if (answer.selected || answer.text.trim()) {
|
|
3998
|
+
answeredQuestions.push({
|
|
3999
|
+
id,
|
|
4000
|
+
selected: answer.selected,
|
|
4001
|
+
text: answer.text.trim()
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
if (answeredQuestions.length > 0) {
|
|
4006
|
+
data.reviwAnswers = answeredQuestions;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
3401
4009
|
return data;
|
|
3402
4010
|
}
|
|
3403
4011
|
function sendAndExit(reason = 'pagehide') {
|
|
@@ -3425,7 +4033,19 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3425
4033
|
globalComment = globalCommentInput.value;
|
|
3426
4034
|
hideSubmitModal();
|
|
3427
4035
|
sendAndExit('button');
|
|
3428
|
-
|
|
4036
|
+
// Try to close window; if it fails (browser security), show completion message
|
|
4037
|
+
setTimeout(() => {
|
|
4038
|
+
window.close();
|
|
4039
|
+
// If window.close() didn't work, show a completion message
|
|
4040
|
+
setTimeout(() => {
|
|
4041
|
+
document.body.innerHTML = \`
|
|
4042
|
+
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;background:var(--bg,#1a1a2e);color:var(--text,#e0e0e0);font-family:system-ui,sans-serif;">
|
|
4043
|
+
<h1 style="font-size:2rem;margin-bottom:1rem;">✅ Review Submitted</h1>
|
|
4044
|
+
<p style="color:var(--muted,#888);">You can close this tab now.</p>
|
|
4045
|
+
</div>
|
|
4046
|
+
\`;
|
|
4047
|
+
}, 100);
|
|
4048
|
+
}, 200);
|
|
3429
4049
|
}
|
|
3430
4050
|
modalSubmit.addEventListener('click', doSubmit);
|
|
3431
4051
|
globalCommentInput.addEventListener('keydown', (e) => {
|
|
@@ -3867,8 +4487,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3867
4487
|
}
|
|
3868
4488
|
|
|
3869
4489
|
if (imageOverlay) {
|
|
4490
|
+
// Close on any click (including image itself)
|
|
3870
4491
|
imageOverlay.addEventListener('click', (e) => {
|
|
3871
|
-
|
|
4492
|
+
closeImageOverlay();
|
|
3872
4493
|
});
|
|
3873
4494
|
}
|
|
3874
4495
|
|
|
@@ -3883,7 +4504,8 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3883
4504
|
img.title = 'Click to view fullscreen';
|
|
3884
4505
|
|
|
3885
4506
|
img.addEventListener('click', (e) => {
|
|
3886
|
-
|
|
4507
|
+
// Don't stop propagation - allow select to work
|
|
4508
|
+
e.preventDefault();
|
|
3887
4509
|
|
|
3888
4510
|
imageContainer.innerHTML = '';
|
|
3889
4511
|
const clonedImg = img.cloneNode(true);
|
|
@@ -3928,8 +4550,9 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3928
4550
|
}
|
|
3929
4551
|
|
|
3930
4552
|
if (videoOverlay) {
|
|
4553
|
+
// Close on any click (including video itself)
|
|
3931
4554
|
videoOverlay.addEventListener('click', (e) => {
|
|
3932
|
-
|
|
4555
|
+
closeVideoOverlay();
|
|
3933
4556
|
});
|
|
3934
4557
|
}
|
|
3935
4558
|
|
|
@@ -3948,7 +4571,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3948
4571
|
|
|
3949
4572
|
link.addEventListener('click', (e) => {
|
|
3950
4573
|
e.preventDefault();
|
|
3951
|
-
|
|
4574
|
+
// Don't stop propagation - allow select to work
|
|
3952
4575
|
|
|
3953
4576
|
// Remove existing video if any
|
|
3954
4577
|
const existingVideo = videoContainer.querySelector('video');
|
|
@@ -4034,13 +4657,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
4034
4657
|
}
|
|
4035
4658
|
}
|
|
4036
4659
|
|
|
4037
|
-
//
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
if (normalized.includes(strippedLine.slice(0, 30)) && strippedLine.length > 5) return i + 1;
|
|
4043
|
-
}
|
|
4660
|
+
// Try stripping all markdown formatting (links, bold, italic, etc.)
|
|
4661
|
+
const strippedLine = stripMarkdown(lineText).replace(/\\s+/g, ' ').slice(0, 100);
|
|
4662
|
+
if (strippedLine === normalized) return i + 1;
|
|
4663
|
+
if (strippedLine.includes(normalized.slice(0, 30)) && normalized.length > 5) return i + 1;
|
|
4664
|
+
if (normalized.includes(strippedLine.slice(0, 30)) && strippedLine.length > 5) return i + 1;
|
|
4044
4665
|
}
|
|
4045
4666
|
return -1;
|
|
4046
4667
|
}
|
|
@@ -4174,21 +4795,33 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
4174
4795
|
|
|
4175
4796
|
// Click on block elements
|
|
4176
4797
|
preview.addEventListener('click', (e) => {
|
|
4177
|
-
// Handle image clicks
|
|
4798
|
+
// Handle image clicks - always select, even if modal is showing
|
|
4178
4799
|
if (e.target.tagName === 'IMG') {
|
|
4179
|
-
|
|
4180
|
-
|
|
4800
|
+
const line = findImageSourceLine(e.target.src);
|
|
4801
|
+
if (line > 0) {
|
|
4802
|
+
selectSourceRange(line);
|
|
4803
|
+
}
|
|
4804
|
+
return;
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
// Handle links - sync to source but let link work normally
|
|
4808
|
+
const link = e.target.closest('a');
|
|
4809
|
+
if (link) {
|
|
4810
|
+
// Find the parent block element containing this link
|
|
4811
|
+
const parentBlock = link.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th');
|
|
4812
|
+
if (parentBlock) {
|
|
4813
|
+
const isTableCell = parentBlock.tagName === 'TD' || parentBlock.tagName === 'TH';
|
|
4814
|
+
const line = isTableCell ? findTableSourceLine(parentBlock.textContent) : findSourceLine(parentBlock.textContent);
|
|
4181
4815
|
if (line > 0) {
|
|
4182
|
-
e.preventDefault();
|
|
4183
|
-
e.stopPropagation();
|
|
4184
4816
|
selectSourceRange(line);
|
|
4185
4817
|
}
|
|
4186
4818
|
}
|
|
4819
|
+
// Let the link open naturally (target="_blank" is set by marked)
|
|
4187
4820
|
return;
|
|
4188
4821
|
}
|
|
4189
4822
|
|
|
4190
|
-
// Ignore clicks on
|
|
4191
|
-
if (e.target.closest('
|
|
4823
|
+
// Ignore clicks on mermaid, video overlay
|
|
4824
|
+
if (e.target.closest('.mermaid-container, .video-fullscreen-overlay')) return;
|
|
4192
4825
|
|
|
4193
4826
|
// Handle code blocks - select entire block
|
|
4194
4827
|
const pre = e.target.closest('pre');
|
|
@@ -4236,6 +4869,227 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
4236
4869
|
}, 10);
|
|
4237
4870
|
});
|
|
4238
4871
|
})();
|
|
4872
|
+
|
|
4873
|
+
// --- Reviw Questions Modal ---
|
|
4874
|
+
(function initReviwQuestions() {
|
|
4875
|
+
if (MODE !== 'markdown') return;
|
|
4876
|
+
if (!REVIW_QUESTIONS || REVIW_QUESTIONS.length === 0) return;
|
|
4877
|
+
|
|
4878
|
+
const overlay = document.getElementById('reviw-questions-overlay');
|
|
4879
|
+
const modal = document.getElementById('reviw-questions-modal');
|
|
4880
|
+
const body = document.getElementById('reviw-questions-body');
|
|
4881
|
+
const closeBtn = document.getElementById('reviw-questions-close');
|
|
4882
|
+
const laterBtn = document.getElementById('reviw-questions-later');
|
|
4883
|
+
const countSpan = document.getElementById('reviw-questions-count');
|
|
4884
|
+
const bar = document.getElementById('reviw-questions-bar');
|
|
4885
|
+
const barMessage = document.getElementById('reviw-questions-bar-message');
|
|
4886
|
+
const barCount = document.getElementById('reviw-questions-bar-count');
|
|
4887
|
+
const barOpenBtn = document.getElementById('reviw-questions-bar-open');
|
|
4888
|
+
|
|
4889
|
+
if (!overlay || !modal || !body) return;
|
|
4890
|
+
|
|
4891
|
+
// Store answers locally
|
|
4892
|
+
const answers = {};
|
|
4893
|
+
REVIW_QUESTIONS.forEach(q => {
|
|
4894
|
+
answers[q.id] = { selected: '', text: '' };
|
|
4895
|
+
});
|
|
4896
|
+
|
|
4897
|
+
// Count unresolved questions
|
|
4898
|
+
const unresolvedQuestions = REVIW_QUESTIONS.filter(q => !q.resolved);
|
|
4899
|
+
const resolvedQuestions = REVIW_QUESTIONS.filter(q => q.resolved);
|
|
4900
|
+
|
|
4901
|
+
function getUnansweredCount() {
|
|
4902
|
+
// Count questions that have no answer (no selection and no text)
|
|
4903
|
+
return unresolvedQuestions.filter(q => {
|
|
4904
|
+
const a = answers[q.id];
|
|
4905
|
+
return !a.selected && !a.text.trim();
|
|
4906
|
+
}).length;
|
|
4907
|
+
}
|
|
4908
|
+
|
|
4909
|
+
function updateCounts() {
|
|
4910
|
+
const unansweredCount = getUnansweredCount();
|
|
4911
|
+
if (unansweredCount > 0) {
|
|
4912
|
+
countSpan.textContent = '(' + unansweredCount + '\u4ef6\u672a\u56de\u7b54)';
|
|
4913
|
+
barMessage.innerHTML = '\ud83d\udccb \u672a\u56de\u7b54\u306e\u8cea\u554f\u304c<span id="reviw-questions-bar-count">' + unansweredCount + '</span>\u4ef6\u3042\u308a\u307e\u3059';
|
|
4914
|
+
laterBtn.textContent = '\u5f8c\u3067\u56de\u7b54\u3059\u308b';
|
|
4915
|
+
} else {
|
|
4916
|
+
countSpan.textContent = '(\u5168\u3066\u56de\u7b54\u6e08\u307f)';
|
|
4917
|
+
barMessage.innerHTML = '\u2705 \u5168\u3066\u306e\u8cea\u554f\u306b\u56de\u7b54\u3057\u307e\u3057\u305f';
|
|
4918
|
+
laterBtn.textContent = '\u9589\u3058\u308b';
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
function checkAllAnswered() {
|
|
4923
|
+
if (getUnansweredCount() === 0) {
|
|
4924
|
+
// All answered - close modal but keep bar visible
|
|
4925
|
+
setTimeout(() => {
|
|
4926
|
+
overlay.classList.remove('visible');
|
|
4927
|
+
// Keep bar visible with different message
|
|
4928
|
+
bar.classList.add('visible');
|
|
4929
|
+
document.body.classList.add('has-questions-bar');
|
|
4930
|
+
}, 500);
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
function renderQuestions() {
|
|
4935
|
+
body.innerHTML = '';
|
|
4936
|
+
|
|
4937
|
+
// Render unresolved questions
|
|
4938
|
+
unresolvedQuestions.forEach((q, idx) => {
|
|
4939
|
+
const item = document.createElement('div');
|
|
4940
|
+
item.className = 'reviw-question-item';
|
|
4941
|
+
item.dataset.id = q.id;
|
|
4942
|
+
|
|
4943
|
+
let optionsHtml = '';
|
|
4944
|
+
if (q.options && q.options.length > 0) {
|
|
4945
|
+
optionsHtml = '<div class="reviw-question-options">' +
|
|
4946
|
+
q.options.map(opt =>
|
|
4947
|
+
'<button class="reviw-question-option" data-value="' + escapeAttr(opt) + '">' + escapeHtml(opt) + '</button>'
|
|
4948
|
+
).join('') +
|
|
4949
|
+
'</div>';
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
const isOkOnly = q.options && q.options.length === 1 && q.options[0] === 'OK';
|
|
4953
|
+
|
|
4954
|
+
item.innerHTML =
|
|
4955
|
+
'<div class="reviw-question-text">Q' + (idx + 1) + '. ' + escapeHtml(q.question) + '<span class="reviw-check-mark"></span></div>' +
|
|
4956
|
+
optionsHtml +
|
|
4957
|
+
(isOkOnly ? '' : '<textarea class="reviw-question-input" placeholder="\u30c6\u30ad\u30b9\u30c8\u3067\u56de\u7b54\u30fb\u88dc\u8db3..."></textarea>');
|
|
4958
|
+
|
|
4959
|
+
body.appendChild(item);
|
|
4960
|
+
|
|
4961
|
+
// Check mark element
|
|
4962
|
+
const checkMark = item.querySelector('.reviw-check-mark');
|
|
4963
|
+
|
|
4964
|
+
function updateCheckMark() {
|
|
4965
|
+
const answer = answers[q.id];
|
|
4966
|
+
const hasAnswer = answer.selected || answer.text.trim();
|
|
4967
|
+
if (hasAnswer) {
|
|
4968
|
+
checkMark.textContent = ' \u2713';
|
|
4969
|
+
item.classList.add('answered');
|
|
4970
|
+
} else {
|
|
4971
|
+
checkMark.textContent = '';
|
|
4972
|
+
item.classList.remove('answered');
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4976
|
+
// Option click handlers - always toggle
|
|
4977
|
+
const optionBtns = item.querySelectorAll('.reviw-question-option');
|
|
4978
|
+
optionBtns.forEach(btn => {
|
|
4979
|
+
btn.addEventListener('click', () => {
|
|
4980
|
+
const wasSelected = btn.classList.contains('selected');
|
|
4981
|
+
optionBtns.forEach(b => b.classList.remove('selected'));
|
|
4982
|
+
if (!wasSelected) {
|
|
4983
|
+
btn.classList.add('selected');
|
|
4984
|
+
answers[q.id].selected = btn.dataset.value;
|
|
4985
|
+
} else {
|
|
4986
|
+
answers[q.id].selected = '';
|
|
4987
|
+
}
|
|
4988
|
+
updateCheckMark();
|
|
4989
|
+
updateCounts();
|
|
4990
|
+
checkAllAnswered();
|
|
4991
|
+
});
|
|
4992
|
+
});
|
|
4993
|
+
|
|
4994
|
+
// Text input handler
|
|
4995
|
+
const textarea = item.querySelector('.reviw-question-input');
|
|
4996
|
+
if (textarea) {
|
|
4997
|
+
textarea.addEventListener('input', () => {
|
|
4998
|
+
answers[q.id].text = textarea.value;
|
|
4999
|
+
updateCheckMark();
|
|
5000
|
+
updateCounts();
|
|
5001
|
+
checkAllAnswered();
|
|
5002
|
+
});
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
updateCheckMark();
|
|
5006
|
+
});
|
|
5007
|
+
|
|
5008
|
+
// Render resolved questions (collapsed)
|
|
5009
|
+
if (resolvedQuestions.length > 0) {
|
|
5010
|
+
const section = document.createElement('div');
|
|
5011
|
+
section.className = 'reviw-resolved-section';
|
|
5012
|
+
section.innerHTML =
|
|
5013
|
+
'<button class="reviw-resolved-toggle">' +
|
|
5014
|
+
'<span class="arrow">\u25b6</span> \u89e3\u6c7a\u6e08\u307f (' + resolvedQuestions.length + '\u4ef6)' +
|
|
5015
|
+
'</button>' +
|
|
5016
|
+
'<div class="reviw-resolved-list">' +
|
|
5017
|
+
resolvedQuestions.map(q =>
|
|
5018
|
+
'<div class="reviw-resolved-item">' +
|
|
5019
|
+
'<div class="reviw-resolved-q">' + escapeHtml(q.question) + '</div>' +
|
|
5020
|
+
'<div class="reviw-resolved-a">\u2192 ' + escapeHtml(q.answer || '(no answer)') + '</div>' +
|
|
5021
|
+
'</div>'
|
|
5022
|
+
).join('') +
|
|
5023
|
+
'</div>';
|
|
5024
|
+
body.appendChild(section);
|
|
5025
|
+
|
|
5026
|
+
const toggle = section.querySelector('.reviw-resolved-toggle');
|
|
5027
|
+
const list = section.querySelector('.reviw-resolved-list');
|
|
5028
|
+
toggle.addEventListener('click', () => {
|
|
5029
|
+
toggle.classList.toggle('open');
|
|
5030
|
+
list.classList.toggle('visible');
|
|
5031
|
+
});
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
function escapeHtml(str) {
|
|
5036
|
+
return String(str)
|
|
5037
|
+
.replace(/&/g, '&')
|
|
5038
|
+
.replace(/</g, '<')
|
|
5039
|
+
.replace(/>/g, '>');
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
function escapeAttr(str) {
|
|
5043
|
+
return String(str)
|
|
5044
|
+
.replace(/&/g, '&')
|
|
5045
|
+
.replace(/"/g, '"');
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
function openModal() {
|
|
5049
|
+
overlay.classList.add('visible');
|
|
5050
|
+
bar.classList.remove('visible');
|
|
5051
|
+
document.body.classList.remove('has-questions-bar');
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
function closeModal(allAnswered) {
|
|
5055
|
+
overlay.classList.remove('visible');
|
|
5056
|
+
const unansweredCount = getUnansweredCount();
|
|
5057
|
+
if (unansweredCount > 0 && !allAnswered) {
|
|
5058
|
+
bar.classList.add('visible');
|
|
5059
|
+
document.body.classList.add('has-questions-bar');
|
|
5060
|
+
} else {
|
|
5061
|
+
bar.classList.remove('visible');
|
|
5062
|
+
document.body.classList.remove('has-questions-bar');
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
|
|
5066
|
+
// Event listeners
|
|
5067
|
+
closeBtn.addEventListener('click', () => closeModal(false));
|
|
5068
|
+
laterBtn.addEventListener('click', () => closeModal(false));
|
|
5069
|
+
barOpenBtn.addEventListener('click', openModal);
|
|
5070
|
+
|
|
5071
|
+
overlay.addEventListener('click', (e) => {
|
|
5072
|
+
if (e.target === overlay) closeModal(false);
|
|
5073
|
+
});
|
|
5074
|
+
|
|
5075
|
+
document.addEventListener('keydown', (e) => {
|
|
5076
|
+
if (e.key === 'Escape' && overlay.classList.contains('visible')) {
|
|
5077
|
+
closeModal(false);
|
|
5078
|
+
}
|
|
5079
|
+
});
|
|
5080
|
+
|
|
5081
|
+
// Expose answers for submit (only answered ones)
|
|
5082
|
+
window.REVIW_ANSWERS = answers;
|
|
5083
|
+
|
|
5084
|
+
// Initialize
|
|
5085
|
+
updateCounts();
|
|
5086
|
+
renderQuestions();
|
|
5087
|
+
|
|
5088
|
+
// Show modal on load if there are unresolved questions
|
|
5089
|
+
if (unresolvedQuestions.length > 0) {
|
|
5090
|
+
setTimeout(() => openModal(), 300);
|
|
5091
|
+
}
|
|
5092
|
+
})();
|
|
4239
5093
|
</script>
|
|
4240
5094
|
</body>
|
|
4241
5095
|
</html>`;
|
|
@@ -4246,8 +5100,8 @@ function buildHtml(filePath) {
|
|
|
4246
5100
|
if (data.mode === "diff") {
|
|
4247
5101
|
return diffHtmlTemplate(data);
|
|
4248
5102
|
}
|
|
4249
|
-
const { rows, cols,
|
|
4250
|
-
return htmlTemplate(rows, cols,
|
|
5103
|
+
const { rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions } = data;
|
|
5104
|
+
return htmlTemplate(rows, cols, projectRoot, relativePath, mode, preview, reviwQuestions);
|
|
4251
5105
|
}
|
|
4252
5106
|
|
|
4253
5107
|
// --- HTTP Server -----------------------------------------------------------
|
|
@@ -4279,6 +5133,20 @@ function outputAllResults() {
|
|
|
4279
5133
|
const yamlOut = yaml.dump(combined, { noRefs: true, lineWidth: 120 });
|
|
4280
5134
|
console.log(yamlOut.trim());
|
|
4281
5135
|
}
|
|
5136
|
+
|
|
5137
|
+
// Output answered questions if any
|
|
5138
|
+
const allAnswers = [];
|
|
5139
|
+
for (const result of allResults) {
|
|
5140
|
+
if (result.reviwAnswers && result.reviwAnswers.length > 0) {
|
|
5141
|
+
allAnswers.push(...result.reviwAnswers);
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
if (allAnswers.length > 0) {
|
|
5145
|
+
console.log("\n[REVIW_ANSWERS]");
|
|
5146
|
+
const answersYaml = yaml.dump(allAnswers, { noRefs: true, lineWidth: 120 });
|
|
5147
|
+
console.log(answersYaml.trim());
|
|
5148
|
+
console.log("[/REVIW_ANSWERS]");
|
|
5149
|
+
}
|
|
4282
5150
|
}
|
|
4283
5151
|
|
|
4284
5152
|
function checkAllDone() {
|
|
@@ -4525,7 +5393,14 @@ function createFileServer(filePath, fileIndex = 0) {
|
|
|
4525
5393
|
const delay = fileIndex * 300;
|
|
4526
5394
|
setTimeout(() => {
|
|
4527
5395
|
try {
|
|
4528
|
-
spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
5396
|
+
const child = spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
5397
|
+
child.on('error', (err) => {
|
|
5398
|
+
console.warn(
|
|
5399
|
+
"Failed to open browser automatically. Please open this URL manually:",
|
|
5400
|
+
url,
|
|
5401
|
+
);
|
|
5402
|
+
});
|
|
5403
|
+
child.unref();
|
|
4529
5404
|
} catch (err) {
|
|
4530
5405
|
console.warn(
|
|
4531
5406
|
"Failed to open browser automatically. Please open this URL manually:",
|
|
@@ -4693,7 +5568,14 @@ function createDiffServer(diffContent) {
|
|
|
4693
5568
|
? "start"
|
|
4694
5569
|
: "xdg-open";
|
|
4695
5570
|
try {
|
|
4696
|
-
spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
5571
|
+
const child = spawn(opener, [url], { stdio: "ignore", detached: true });
|
|
5572
|
+
child.on('error', (err) => {
|
|
5573
|
+
console.warn(
|
|
5574
|
+
"Failed to open browser automatically. Please open this URL manually:",
|
|
5575
|
+
url,
|
|
5576
|
+
);
|
|
5577
|
+
});
|
|
5578
|
+
child.unref();
|
|
4697
5579
|
} catch (err) {
|
|
4698
5580
|
console.warn(
|
|
4699
5581
|
"Failed to open browser automatically. Please open this URL manually:",
|
|
@@ -4709,7 +5591,18 @@ function createDiffServer(diffContent) {
|
|
|
4709
5591
|
});
|
|
4710
5592
|
}
|
|
4711
5593
|
|
|
4712
|
-
// Main entry point
|
|
5594
|
+
// Main entry point - only run when executed directly (not when required for testing)
|
|
5595
|
+
if (require.main === module) {
|
|
5596
|
+
// Parse CLI arguments and apply configuration
|
|
5597
|
+
const { config, filePaths } = parseCliArgs(process.argv);
|
|
5598
|
+
basePort = config.basePort;
|
|
5599
|
+
encodingOpt = config.encodingOpt;
|
|
5600
|
+
noOpen = config.noOpen;
|
|
5601
|
+
nextPort = config.basePort; // Update nextPort to match configured basePort
|
|
5602
|
+
|
|
5603
|
+
// Validate and resolve file paths
|
|
5604
|
+
resolvedPaths = validateAndResolvePaths(filePaths);
|
|
5605
|
+
|
|
4713
5606
|
(async () => {
|
|
4714
5607
|
// Check for stdin input first
|
|
4715
5608
|
const stdinData = await checkStdin();
|
|
@@ -4778,3 +5671,9 @@ function createDiffServer(diffContent) {
|
|
|
4778
5671
|
}
|
|
4779
5672
|
}
|
|
4780
5673
|
})();
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5676
|
+
// Export parser functions for testing
|
|
5677
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
5678
|
+
module.exports = { parseDiff, parseCsv, DEFAULT_CONFIG };
|
|
5679
|
+
}
|