reviw 0.10.5 → 0.10.7
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/cli.cjs +382 -58
- package/package.json +1 -1
package/cli.cjs
CHANGED
|
@@ -20,17 +20,157 @@ const iconv = require("iconv-lite");
|
|
|
20
20
|
const marked = require("marked");
|
|
21
21
|
const yaml = require("js-yaml");
|
|
22
22
|
|
|
23
|
+
// --- XSS Protection for marked (Whitelist approach) ---
|
|
24
|
+
// 許可タグリスト(Markdown由来の安全なタグのみ)
|
|
25
|
+
const allowedTags = new Set([
|
|
26
|
+
'p', 'br', 'hr',
|
|
27
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
28
|
+
'ul', 'ol', 'li',
|
|
29
|
+
'blockquote', 'pre', 'code',
|
|
30
|
+
'em', 'strong', 'del', 's',
|
|
31
|
+
'a', 'img',
|
|
32
|
+
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
33
|
+
'div', 'span', // Markdown拡張用
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// 許可属性リスト(タグごとに定義)
|
|
37
|
+
const allowedAttributes = {
|
|
38
|
+
'a': ['href', 'title', 'target', 'rel'],
|
|
39
|
+
'img': ['src', 'alt', 'title', 'width', 'height'],
|
|
40
|
+
'code': ['class'], // 言語ハイライト用
|
|
41
|
+
'pre': ['class'],
|
|
42
|
+
'div': ['class'],
|
|
43
|
+
'span': ['class'],
|
|
44
|
+
'th': ['align'],
|
|
45
|
+
'td': ['align'],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// HTMLエスケープ関数(XSS対策用)
|
|
49
|
+
function escapeHtmlForXss(html) {
|
|
50
|
+
return html
|
|
51
|
+
.replace(/&/g, "&")
|
|
52
|
+
.replace(/</g, "<")
|
|
53
|
+
.replace(/>/g, ">")
|
|
54
|
+
.replace(/"/g, """)
|
|
55
|
+
.replace(/'/g, "'");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// href/src属性のURLバリデーション
|
|
59
|
+
function isSafeUrl(url) {
|
|
60
|
+
if (!url) return true;
|
|
61
|
+
// 空白・制御文字を除去して正規化
|
|
62
|
+
var normalized = url.toLowerCase().replace(/[\s\x00-\x1f]/g, '');
|
|
63
|
+
// HTMLエンティティのデコード(
 など)
|
|
64
|
+
var decoded = normalized.replace(/&#x?[0-9a-f]+;?/gi, '');
|
|
65
|
+
if (decoded.startsWith('javascript:')) return false;
|
|
66
|
+
if (decoded.startsWith('vbscript:')) return false;
|
|
67
|
+
if (decoded.startsWith('data:') && !decoded.startsWith('data:image/')) return false;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// HTML文字列をサニタイズ(ホワイトリストに含まれないタグ/属性を除去)
|
|
72
|
+
function sanitizeHtml(html) {
|
|
73
|
+
// より堅牢なタグマッチング:属性値内の < > を考慮
|
|
74
|
+
// 引用符で囲まれた属性値を正しく処理するパターン
|
|
75
|
+
var tagPattern = /<\/?([a-z][a-z0-9]*)((?:\s+[a-z][a-z0-9-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>"']*))?)*)\s*\/?>/gi;
|
|
76
|
+
|
|
77
|
+
return html.replace(tagPattern, function(match, tag, attrsStr) {
|
|
78
|
+
var tagLower = tag.toLowerCase();
|
|
79
|
+
|
|
80
|
+
// 許可されていないタグはエスケープ
|
|
81
|
+
if (!allowedTags.has(tagLower)) {
|
|
82
|
+
return escapeHtmlForXss(match);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 終了タグはそのまま
|
|
86
|
+
if (match.startsWith('</')) {
|
|
87
|
+
return '</' + tagLower + '>';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 属性をフィルタリング
|
|
91
|
+
var allowed = allowedAttributes[tagLower] || [];
|
|
92
|
+
var safeAttrs = [];
|
|
93
|
+
|
|
94
|
+
// 属性を解析(引用符で囲まれた値を正しく処理)
|
|
95
|
+
var attrRegex = /([a-z][a-z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"']*))/gi;
|
|
96
|
+
var attrMatch;
|
|
97
|
+
while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
|
|
98
|
+
var attrName = attrMatch[1].toLowerCase();
|
|
99
|
+
var attrValue = attrMatch[2] !== undefined ? attrMatch[2] :
|
|
100
|
+
attrMatch[3] !== undefined ? attrMatch[3] :
|
|
101
|
+
attrMatch[4] || '';
|
|
102
|
+
|
|
103
|
+
// on*イベントハンドラは常に拒否
|
|
104
|
+
if (attrName.startsWith('on')) continue;
|
|
105
|
+
|
|
106
|
+
// 許可属性のみ
|
|
107
|
+
if (!allowed.includes(attrName)) continue;
|
|
108
|
+
|
|
109
|
+
// href/srcのURL検証
|
|
110
|
+
if ((attrName === 'href' || attrName === 'src') && !isSafeUrl(attrValue)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
safeAttrs.push(attrName + '="' + attrValue.replace(/"/g, '"') + '"');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
var finalAttrs = safeAttrs.length > 0 ? ' ' + safeAttrs.join(' ') : '';
|
|
118
|
+
return '<' + tagLower + finalAttrs + '>';
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
marked.use({
|
|
123
|
+
renderer: {
|
|
124
|
+
// 生HTMLブロックをサニタイズ
|
|
125
|
+
html: function(token) {
|
|
126
|
+
var text = token.raw || token.text || token;
|
|
127
|
+
return sanitizeHtml(text);
|
|
128
|
+
},
|
|
129
|
+
// リンクに安全なURL検証を追加(別タブで開く)
|
|
130
|
+
link: function(href, title, text) {
|
|
131
|
+
href = href || "";
|
|
132
|
+
title = title || "";
|
|
133
|
+
text = text || "";
|
|
134
|
+
if (!isSafeUrl(href)) {
|
|
135
|
+
// 危険なURLはプレーンテキストとして表示
|
|
136
|
+
return escapeHtmlForXss(text);
|
|
137
|
+
}
|
|
138
|
+
var titleAttr = title ? ' title="' + escapeHtmlForXss(title) + '"' : "";
|
|
139
|
+
return '<a href="' + escapeHtmlForXss(href) + '"' + titleAttr + ' target="_blank" rel="noopener noreferrer">' + text + '</a>';
|
|
140
|
+
},
|
|
141
|
+
// 画像にも安全なURL検証を追加
|
|
142
|
+
image: function(href, title, text) {
|
|
143
|
+
href = href || "";
|
|
144
|
+
title = title || "";
|
|
145
|
+
text = text || "";
|
|
146
|
+
if (!isSafeUrl(href)) {
|
|
147
|
+
return escapeHtmlForXss(text || "image");
|
|
148
|
+
}
|
|
149
|
+
var titleAttr = title ? ' title="' + escapeHtmlForXss(title) + '"' : "";
|
|
150
|
+
var altAttr = text ? ' alt="' + escapeHtmlForXss(text) + '"' : "";
|
|
151
|
+
return '<img src="' + escapeHtmlForXss(href) + '"' + altAttr + titleAttr + '>';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
23
156
|
// --- CLI arguments ---------------------------------------------------------
|
|
24
157
|
const VERSION = require("./package.json").version;
|
|
25
|
-
const args = process.argv.slice(2);
|
|
26
158
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
159
|
+
// ===== CLI設定のデフォルト値(import時に使用) =====
|
|
160
|
+
const DEFAULT_CONFIG = {
|
|
161
|
+
basePort: 4989,
|
|
162
|
+
encodingOpt: null,
|
|
163
|
+
noOpen: false,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ===== グローバル設定変数(デフォルト値で初期化、require.main時に更新) =====
|
|
167
|
+
let basePort = DEFAULT_CONFIG.basePort;
|
|
168
|
+
let encodingOpt = DEFAULT_CONFIG.encodingOpt;
|
|
169
|
+
let noOpen = DEFAULT_CONFIG.noOpen;
|
|
31
170
|
let stdinMode = false;
|
|
32
171
|
let diffMode = false;
|
|
33
172
|
let stdinContent = null;
|
|
173
|
+
let resolvedPaths = []; // ファイルパス(require.main時に設定)
|
|
34
174
|
|
|
35
175
|
function showHelp() {
|
|
36
176
|
console.log(`reviw v${VERSION} - Lightweight file reviewer with in-browser comments
|
|
@@ -75,25 +215,55 @@ function showVersion() {
|
|
|
75
215
|
console.log(VERSION);
|
|
76
216
|
}
|
|
77
217
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
218
|
+
// ===== CLI引数パース関数(require.main時のみ呼ばれる) =====
|
|
219
|
+
function parseCliArgs(argv) {
|
|
220
|
+
const args = argv.slice(2);
|
|
221
|
+
const config = { ...DEFAULT_CONFIG };
|
|
222
|
+
const filePaths = [];
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
225
|
+
const arg = args[i];
|
|
226
|
+
if (arg === "--port" && args[i + 1]) {
|
|
227
|
+
config.basePort = Number(args[i + 1]);
|
|
228
|
+
i += 1;
|
|
229
|
+
} else if ((arg === "--encoding" || arg === "-e") && args[i + 1]) {
|
|
230
|
+
config.encodingOpt = args[i + 1];
|
|
231
|
+
i += 1;
|
|
232
|
+
} else if (arg === "--no-open") {
|
|
233
|
+
config.noOpen = true;
|
|
234
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
235
|
+
showHelp();
|
|
236
|
+
process.exit(0);
|
|
237
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
238
|
+
showVersion();
|
|
239
|
+
process.exit(0);
|
|
240
|
+
} else if (!arg.startsWith("-")) {
|
|
241
|
+
filePaths.push(arg);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { config, filePaths };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ===== ファイルパス検証・解決関数(require.main時のみ呼ばれる) =====
|
|
249
|
+
function validateAndResolvePaths(filePaths) {
|
|
250
|
+
const resolved = [];
|
|
251
|
+
for (const fp of filePaths) {
|
|
252
|
+
const resolvedPath = path.resolve(fp);
|
|
253
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
254
|
+
console.error(`File not found: ${resolvedPath}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
const stat = fs.statSync(resolvedPath);
|
|
258
|
+
if (stat.isDirectory()) {
|
|
259
|
+
console.error(`Cannot open directory: ${resolvedPath}`);
|
|
260
|
+
console.error(`Usage: reviw <file> [file2...]`);
|
|
261
|
+
console.error(`Please specify a file, not a directory.`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
resolved.push(resolvedPath);
|
|
96
265
|
}
|
|
266
|
+
return resolved;
|
|
97
267
|
}
|
|
98
268
|
|
|
99
269
|
// Check if stdin has data (pipe mode)
|
|
@@ -148,24 +318,6 @@ function runGitDiff() {
|
|
|
148
318
|
});
|
|
149
319
|
}
|
|
150
320
|
|
|
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
321
|
// --- Diff parsing -----------------------------------------------------------
|
|
170
322
|
function parseDiff(diffText) {
|
|
171
323
|
const files = [];
|
|
@@ -1387,7 +1539,19 @@ function diffHtmlTemplate(diffData) {
|
|
|
1387
1539
|
globalComment = globalCommentInput.value;
|
|
1388
1540
|
hideSubmitModal();
|
|
1389
1541
|
sendAndExit('button');
|
|
1390
|
-
|
|
1542
|
+
// Try to close window; if it fails (browser security), show completion message
|
|
1543
|
+
setTimeout(() => {
|
|
1544
|
+
window.close();
|
|
1545
|
+
// If window.close() didn't work, show a completion message
|
|
1546
|
+
setTimeout(() => {
|
|
1547
|
+
document.body.innerHTML = \`
|
|
1548
|
+
<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;">
|
|
1549
|
+
<h1 style="font-size:2rem;margin-bottom:1rem;">✅ Review Submitted</h1>
|
|
1550
|
+
<p style="color:var(--muted,#888);">You can close this tab now.</p>
|
|
1551
|
+
</div>
|
|
1552
|
+
\`;
|
|
1553
|
+
}, 100);
|
|
1554
|
+
}, 200);
|
|
1391
1555
|
}
|
|
1392
1556
|
document.getElementById('modal-submit').addEventListener('click', doSubmit);
|
|
1393
1557
|
globalCommentInput.addEventListener('keydown', e => {
|
|
@@ -1470,6 +1634,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1470
1634
|
--hover-bg: rgba(96,165,250,0.08);
|
|
1471
1635
|
--shadow-color: rgba(0,0,0,0.35);
|
|
1472
1636
|
--code-bg: #1e293b;
|
|
1637
|
+
--error: #dc3545;
|
|
1473
1638
|
}
|
|
1474
1639
|
/* Light theme */
|
|
1475
1640
|
[data-theme="light"] {
|
|
@@ -1496,6 +1661,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1496
1661
|
--hover-bg: rgba(59,130,246,0.06);
|
|
1497
1662
|
--shadow-color: rgba(0,0,0,0.1);
|
|
1498
1663
|
--code-bg: #f1f5f9;
|
|
1664
|
+
--error: #dc3545;
|
|
1499
1665
|
}
|
|
1500
1666
|
* { box-sizing: border-box; }
|
|
1501
1667
|
body {
|
|
@@ -1973,9 +2139,6 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
1973
2139
|
text-align: left;
|
|
1974
2140
|
border-bottom: 1px solid var(--border);
|
|
1975
2141
|
}
|
|
1976
|
-
.md-preview table:not(.frontmatter-table table) th {
|
|
1977
|
-
background: rgba(255,255,255,0.05);
|
|
1978
|
-
}
|
|
1979
2142
|
.md-preview table:not(.frontmatter-table table) th {
|
|
1980
2143
|
background: var(--panel);
|
|
1981
2144
|
font-weight: 600;
|
|
@@ -2305,13 +2468,46 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2305
2468
|
.fullscreen-content .mermaid svg {
|
|
2306
2469
|
display: block;
|
|
2307
2470
|
}
|
|
2471
|
+
/* Minimap */
|
|
2472
|
+
.minimap {
|
|
2473
|
+
position: absolute;
|
|
2474
|
+
top: 70px;
|
|
2475
|
+
right: 20px;
|
|
2476
|
+
width: 200px;
|
|
2477
|
+
height: 150px;
|
|
2478
|
+
background: var(--panel-alpha);
|
|
2479
|
+
border: 1px solid var(--border);
|
|
2480
|
+
border-radius: 8px;
|
|
2481
|
+
overflow: hidden;
|
|
2482
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
2483
|
+
}
|
|
2484
|
+
.minimap-content {
|
|
2485
|
+
width: 100%;
|
|
2486
|
+
height: 100%;
|
|
2487
|
+
display: flex;
|
|
2488
|
+
align-items: center;
|
|
2489
|
+
justify-content: center;
|
|
2490
|
+
padding: 8px;
|
|
2491
|
+
}
|
|
2492
|
+
.minimap-content svg {
|
|
2493
|
+
max-width: 100%;
|
|
2494
|
+
max-height: 100%;
|
|
2495
|
+
opacity: 0.6;
|
|
2496
|
+
}
|
|
2497
|
+
.minimap-viewport {
|
|
2498
|
+
position: absolute;
|
|
2499
|
+
border: 2px solid var(--accent);
|
|
2500
|
+
background: rgba(102, 126, 234, 0.2);
|
|
2501
|
+
pointer-events: none;
|
|
2502
|
+
border-radius: 2px;
|
|
2503
|
+
}
|
|
2308
2504
|
/* Error toast */
|
|
2309
2505
|
.mermaid-error-toast {
|
|
2310
2506
|
position: fixed;
|
|
2311
2507
|
bottom: 20px;
|
|
2312
2508
|
left: 50%;
|
|
2313
2509
|
transform: translateX(-50%);
|
|
2314
|
-
background:
|
|
2510
|
+
background: var(--error);
|
|
2315
2511
|
color: white;
|
|
2316
2512
|
padding: 12px 24px;
|
|
2317
2513
|
border-radius: 8px;
|
|
@@ -2470,6 +2666,10 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
2470
2666
|
<div class="fullscreen-content" id="fs-content">
|
|
2471
2667
|
<div class="mermaid-wrapper" id="fs-wrapper"></div>
|
|
2472
2668
|
</div>
|
|
2669
|
+
<div class="minimap" id="fs-minimap">
|
|
2670
|
+
<div class="minimap-content" id="fs-minimap-content"></div>
|
|
2671
|
+
<div class="minimap-viewport" id="fs-minimap-viewport"></div>
|
|
2672
|
+
</div>
|
|
2473
2673
|
</div>
|
|
2474
2674
|
<div class="mermaid-error-toast" id="mermaid-error-toast"></div>
|
|
2475
2675
|
<div class="copy-toast" id="copy-toast">Copied to clipboard!</div>
|
|
@@ -3388,7 +3588,19 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3388
3588
|
globalComment = globalCommentInput.value;
|
|
3389
3589
|
hideSubmitModal();
|
|
3390
3590
|
sendAndExit('button');
|
|
3391
|
-
|
|
3591
|
+
// Try to close window; if it fails (browser security), show completion message
|
|
3592
|
+
setTimeout(() => {
|
|
3593
|
+
window.close();
|
|
3594
|
+
// If window.close() didn't work, show a completion message
|
|
3595
|
+
setTimeout(() => {
|
|
3596
|
+
document.body.innerHTML = \`
|
|
3597
|
+
<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;">
|
|
3598
|
+
<h1 style="font-size:2rem;margin-bottom:1rem;">✅ Review Submitted</h1>
|
|
3599
|
+
<p style="color:var(--muted,#888);">You can close this tab now.</p>
|
|
3600
|
+
</div>
|
|
3601
|
+
\`;
|
|
3602
|
+
}, 100);
|
|
3603
|
+
}, 200);
|
|
3392
3604
|
}
|
|
3393
3605
|
modalSubmit.addEventListener('click', doSubmit);
|
|
3394
3606
|
globalCommentInput.addEventListener('keydown', (e) => {
|
|
@@ -3578,12 +3790,15 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3578
3790
|
const fsWrapper = document.getElementById('fs-wrapper');
|
|
3579
3791
|
const fsContent = document.getElementById('fs-content');
|
|
3580
3792
|
const fsZoomInfo = document.getElementById('fs-zoom-info');
|
|
3793
|
+
const minimapContent = document.getElementById('fs-minimap-content');
|
|
3794
|
+
const minimapViewport = document.getElementById('fs-minimap-viewport');
|
|
3581
3795
|
let currentZoom = 1;
|
|
3582
3796
|
let initialZoom = 1;
|
|
3583
3797
|
let panX = 0, panY = 0;
|
|
3584
3798
|
let isPanning = false;
|
|
3585
3799
|
let startX, startY;
|
|
3586
3800
|
let svgNaturalWidth = 0, svgNaturalHeight = 0;
|
|
3801
|
+
let minimapScale = 1;
|
|
3587
3802
|
|
|
3588
3803
|
function openFullscreen(mermaidEl) {
|
|
3589
3804
|
const svg = mermaidEl.querySelector('svg');
|
|
@@ -3592,6 +3807,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3592
3807
|
const clonedSvg = svg.cloneNode(true);
|
|
3593
3808
|
fsWrapper.appendChild(clonedSvg);
|
|
3594
3809
|
|
|
3810
|
+
// Setup minimap
|
|
3811
|
+
minimapContent.innerHTML = '';
|
|
3812
|
+
const minimapSvg = svg.cloneNode(true);
|
|
3813
|
+
minimapContent.appendChild(minimapSvg);
|
|
3814
|
+
|
|
3595
3815
|
// Get SVG's intrinsic/natural size from viewBox or attributes
|
|
3596
3816
|
const viewBox = svg.getAttribute('viewBox');
|
|
3597
3817
|
let naturalWidth, naturalHeight;
|
|
@@ -3608,6 +3828,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3608
3828
|
svgNaturalWidth = naturalWidth;
|
|
3609
3829
|
svgNaturalHeight = naturalHeight;
|
|
3610
3830
|
|
|
3831
|
+
// Calculate minimap scale
|
|
3832
|
+
const minimapMaxWidth = 184; // 200 - 16 padding
|
|
3833
|
+
const minimapMaxHeight = 134; // 150 - 16 padding
|
|
3834
|
+
minimapScale = Math.min(minimapMaxWidth / naturalWidth, minimapMaxHeight / naturalHeight);
|
|
3835
|
+
|
|
3611
3836
|
clonedSvg.style.width = naturalWidth + 'px';
|
|
3612
3837
|
clonedSvg.style.height = naturalHeight + 'px';
|
|
3613
3838
|
|
|
@@ -3628,8 +3853,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3628
3853
|
panX = (viewportWidth - scaledWidth) / 2 + 20;
|
|
3629
3854
|
panY = (viewportHeight - scaledHeight) / 2 + 60;
|
|
3630
3855
|
|
|
3631
|
-
updateTransform();
|
|
3632
3856
|
fsOverlay.classList.add('visible');
|
|
3857
|
+
// Wait for DOM to render before calculating minimap position
|
|
3858
|
+
requestAnimationFrame(() => {
|
|
3859
|
+
updateTransform();
|
|
3860
|
+
});
|
|
3633
3861
|
}
|
|
3634
3862
|
|
|
3635
3863
|
function closeFullscreen() {
|
|
@@ -3639,6 +3867,71 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3639
3867
|
function updateTransform() {
|
|
3640
3868
|
fsWrapper.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + currentZoom + ')';
|
|
3641
3869
|
fsZoomInfo.textContent = Math.round(currentZoom * 100) + '%';
|
|
3870
|
+
updateMinimap();
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
function updateMinimap() {
|
|
3874
|
+
if (!svgNaturalWidth || !svgNaturalHeight) return;
|
|
3875
|
+
|
|
3876
|
+
const viewportWidth = fsContent.clientWidth;
|
|
3877
|
+
const viewportHeight = fsContent.clientHeight;
|
|
3878
|
+
|
|
3879
|
+
// Minimap container dimensions (inner area)
|
|
3880
|
+
const mmWidth = 184; // 200 - 16 padding
|
|
3881
|
+
const mmHeight = 134; // 150 - 16 padding
|
|
3882
|
+
const mmPadding = 8;
|
|
3883
|
+
|
|
3884
|
+
// SVG thumbnail size in minimap (scaled to fit)
|
|
3885
|
+
const mmSvgWidth = svgNaturalWidth * minimapScale;
|
|
3886
|
+
const mmSvgHeight = svgNaturalHeight * minimapScale;
|
|
3887
|
+
// SVG thumbnail position (centered in minimap)
|
|
3888
|
+
const mmSvgLeft = (mmWidth - mmSvgWidth) / 2 + mmPadding;
|
|
3889
|
+
const mmSvgTop = (mmHeight - mmSvgHeight) / 2 + mmPadding;
|
|
3890
|
+
|
|
3891
|
+
// Calculate which part of the SVG is visible in the viewport
|
|
3892
|
+
// transform: translate(panX, panY) scale(currentZoom)
|
|
3893
|
+
// The viewport shows SVG region starting at (-panX/zoom, -panY/zoom)
|
|
3894
|
+
const svgVisibleLeft = -panX / currentZoom;
|
|
3895
|
+
const svgVisibleTop = -panY / currentZoom;
|
|
3896
|
+
const svgVisibleWidth = viewportWidth / currentZoom;
|
|
3897
|
+
const svgVisibleHeight = viewportHeight / currentZoom;
|
|
3898
|
+
|
|
3899
|
+
// Convert to minimap coordinates
|
|
3900
|
+
let vpLeft = mmSvgLeft + svgVisibleLeft * minimapScale;
|
|
3901
|
+
let vpTop = mmSvgTop + svgVisibleTop * minimapScale;
|
|
3902
|
+
let vpWidth = svgVisibleWidth * minimapScale;
|
|
3903
|
+
let vpHeight = svgVisibleHeight * minimapScale;
|
|
3904
|
+
|
|
3905
|
+
// Clamp to minimap bounds (the viewport rect should stay within minimap)
|
|
3906
|
+
const mmLeft = mmPadding;
|
|
3907
|
+
const mmTop = mmPadding;
|
|
3908
|
+
const mmRight = mmWidth + mmPadding;
|
|
3909
|
+
const mmBottom = mmHeight + mmPadding;
|
|
3910
|
+
|
|
3911
|
+
// Adjust if viewport extends beyond minimap bounds
|
|
3912
|
+
if (vpLeft < mmLeft) {
|
|
3913
|
+
vpWidth -= (mmLeft - vpLeft);
|
|
3914
|
+
vpLeft = mmLeft;
|
|
3915
|
+
}
|
|
3916
|
+
if (vpTop < mmTop) {
|
|
3917
|
+
vpHeight -= (mmTop - vpTop);
|
|
3918
|
+
vpTop = mmTop;
|
|
3919
|
+
}
|
|
3920
|
+
if (vpLeft + vpWidth > mmRight) {
|
|
3921
|
+
vpWidth = mmRight - vpLeft;
|
|
3922
|
+
}
|
|
3923
|
+
if (vpTop + vpHeight > mmBottom) {
|
|
3924
|
+
vpHeight = mmBottom - vpTop;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
// Ensure minimum size and positive dimensions
|
|
3928
|
+
vpWidth = Math.max(20, vpWidth);
|
|
3929
|
+
vpHeight = Math.max(15, vpHeight);
|
|
3930
|
+
|
|
3931
|
+
minimapViewport.style.left = vpLeft + 'px';
|
|
3932
|
+
minimapViewport.style.top = vpTop + 'px';
|
|
3933
|
+
minimapViewport.style.width = vpWidth + 'px';
|
|
3934
|
+
minimapViewport.style.height = vpHeight + 'px';
|
|
3642
3935
|
}
|
|
3643
3936
|
|
|
3644
3937
|
// Use multiplicative zoom for consistent behavior
|
|
@@ -3916,13 +4209,11 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
3916
4209
|
}
|
|
3917
4210
|
}
|
|
3918
4211
|
|
|
3919
|
-
//
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
if (normalized.includes(strippedLine.slice(0, 30)) && strippedLine.length > 5) return i + 1;
|
|
3925
|
-
}
|
|
4212
|
+
// Try stripping all markdown formatting (links, bold, italic, etc.)
|
|
4213
|
+
const strippedLine = stripMarkdown(lineText).replace(/\\s+/g, ' ').slice(0, 100);
|
|
4214
|
+
if (strippedLine === normalized) return i + 1;
|
|
4215
|
+
if (strippedLine.includes(normalized.slice(0, 30)) && normalized.length > 5) return i + 1;
|
|
4216
|
+
if (normalized.includes(strippedLine.slice(0, 30)) && strippedLine.length > 5) return i + 1;
|
|
3926
4217
|
}
|
|
3927
4218
|
return -1;
|
|
3928
4219
|
}
|
|
@@ -4069,8 +4360,24 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
|
|
|
4069
4360
|
return;
|
|
4070
4361
|
}
|
|
4071
4362
|
|
|
4072
|
-
//
|
|
4073
|
-
|
|
4363
|
+
// Handle links - sync to source but let link work normally
|
|
4364
|
+
const link = e.target.closest('a');
|
|
4365
|
+
if (link) {
|
|
4366
|
+
// Find the parent block element containing this link
|
|
4367
|
+
const parentBlock = link.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th');
|
|
4368
|
+
if (parentBlock) {
|
|
4369
|
+
const isTableCell = parentBlock.tagName === 'TD' || parentBlock.tagName === 'TH';
|
|
4370
|
+
const line = isTableCell ? findTableSourceLine(parentBlock.textContent) : findSourceLine(parentBlock.textContent);
|
|
4371
|
+
if (line > 0) {
|
|
4372
|
+
selectSourceRange(line);
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
// Let the link open naturally (target="_blank" is set by marked)
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
// Ignore clicks on mermaid, video overlay
|
|
4380
|
+
if (e.target.closest('.mermaid-container, .video-fullscreen-overlay')) return;
|
|
4074
4381
|
|
|
4075
4382
|
// Handle code blocks - select entire block
|
|
4076
4383
|
const pre = e.target.closest('pre');
|
|
@@ -4591,7 +4898,18 @@ function createDiffServer(diffContent) {
|
|
|
4591
4898
|
});
|
|
4592
4899
|
}
|
|
4593
4900
|
|
|
4594
|
-
// Main entry point
|
|
4901
|
+
// Main entry point - only run when executed directly (not when required for testing)
|
|
4902
|
+
if (require.main === module) {
|
|
4903
|
+
// Parse CLI arguments and apply configuration
|
|
4904
|
+
const { config, filePaths } = parseCliArgs(process.argv);
|
|
4905
|
+
basePort = config.basePort;
|
|
4906
|
+
encodingOpt = config.encodingOpt;
|
|
4907
|
+
noOpen = config.noOpen;
|
|
4908
|
+
nextPort = config.basePort; // Update nextPort to match configured basePort
|
|
4909
|
+
|
|
4910
|
+
// Validate and resolve file paths
|
|
4911
|
+
resolvedPaths = validateAndResolvePaths(filePaths);
|
|
4912
|
+
|
|
4595
4913
|
(async () => {
|
|
4596
4914
|
// Check for stdin input first
|
|
4597
4915
|
const stdinData = await checkStdin();
|
|
@@ -4660,3 +4978,9 @@ function createDiffServer(diffContent) {
|
|
|
4660
4978
|
}
|
|
4661
4979
|
}
|
|
4662
4980
|
})();
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
// Export parser functions for testing
|
|
4984
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
4985
|
+
module.exports = { parseDiff, parseCsv, DEFAULT_CONFIG };
|
|
4986
|
+
}
|