reviw 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli.cjs +265 -2
  2. package/package.json +1 -1
package/cli.cjs CHANGED
@@ -849,7 +849,7 @@ function diffHtmlTemplate(diffData) {
849
849
  #submit-modal.visible { display: flex; }
850
850
  #submit-modal .modal-dialog {
851
851
  pointer-events: auto;
852
- margin: 20px;
852
+ margin: 60px 20px 20px 20px; /* top margin avoids header button overlap */
853
853
  }
854
854
  .modal-dialog {
855
855
  background: var(--panel);
@@ -2122,7 +2122,7 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
2122
2122
  #submit-modal.visible { display: flex; }
2123
2123
  #submit-modal .modal-dialog {
2124
2124
  pointer-events: auto;
2125
- margin: 20px;
2125
+ margin: 60px 20px 20px 20px; /* top margin avoids header button overlap */
2126
2126
  }
2127
2127
  .modal-dialog {
2128
2128
  background: var(--panel-solid);
@@ -3801,6 +3801,249 @@ function htmlTemplate(dataRows, cols, title, mode, previewHtml) {
3801
3801
  }
3802
3802
  });
3803
3803
  })();
3804
+
3805
+ // --- Preview Commenting ---
3806
+ (function initPreviewCommenting() {
3807
+ if (MODE !== 'markdown') return;
3808
+
3809
+ const preview = document.querySelector('.md-preview');
3810
+ if (!preview) return;
3811
+
3812
+ // Add visual hint for clickable elements
3813
+ const style = document.createElement('style');
3814
+ style.textContent = \`
3815
+ .md-preview > p:hover, .md-preview > h1:hover, .md-preview > h2:hover,
3816
+ .md-preview > h3:hover, .md-preview > h4:hover, .md-preview > h5:hover,
3817
+ .md-preview > h6:hover, .md-preview > ul > li:hover, .md-preview > ol > li:hover,
3818
+ .md-preview > pre:hover, .md-preview > blockquote:hover {
3819
+ background: rgba(99, 102, 241, 0.08);
3820
+ cursor: pointer;
3821
+ border-radius: 4px;
3822
+ }
3823
+ .md-preview img:hover {
3824
+ outline: 2px solid var(--accent);
3825
+ cursor: pointer;
3826
+ }
3827
+ \`;
3828
+ document.head.appendChild(style);
3829
+
3830
+ // Helper: find matching source line for text
3831
+ function findSourceLine(text) {
3832
+ if (!text) return -1;
3833
+ const normalized = text.trim().replace(/\\s+/g, ' ').slice(0, 100);
3834
+ if (!normalized) return -1;
3835
+
3836
+ for (let i = 0; i < DATA.length; i++) {
3837
+ const lineText = (DATA[i][0] || '').trim();
3838
+ if (!lineText) continue;
3839
+
3840
+ const lineNorm = lineText.replace(/\\s+/g, ' ').slice(0, 100);
3841
+ if (lineNorm === normalized) return i + 1;
3842
+ if (lineNorm.includes(normalized.slice(0, 30)) && normalized.length > 5) return i + 1;
3843
+ if (normalized.includes(lineNorm.slice(0, 30)) && lineNorm.length > 5) return i + 1;
3844
+
3845
+ // Check for markdown headings: strip # from source and compare
3846
+ if (lineText.match(/^#+\\s/)) {
3847
+ const headingText = lineText.replace(/^#+\\s*/, '').trim();
3848
+ if (headingText === normalized || headingText.toLowerCase() === normalized.toLowerCase()) {
3849
+ return i + 1;
3850
+ }
3851
+ }
3852
+ }
3853
+ return -1;
3854
+ }
3855
+
3856
+ // Helper: find matching source line for table cell (prioritizes table rows)
3857
+ function findTableSourceLine(text) {
3858
+ if (!text) return -1;
3859
+ const normalized = text.trim().replace(/\\s+/g, ' ').slice(0, 100);
3860
+ if (!normalized) return -1;
3861
+
3862
+ // First pass: look for table rows (lines starting with |) containing the text
3863
+ for (let i = 0; i < DATA.length; i++) {
3864
+ const lineText = (DATA[i][0] || '').trim();
3865
+ if (!lineText || !lineText.startsWith('|')) continue;
3866
+
3867
+ const lineNorm = lineText.replace(/\\s+/g, ' ').slice(0, 100);
3868
+ if (lineNorm.includes(normalized.slice(0, 30)) && normalized.length > 5) return i + 1;
3869
+ }
3870
+
3871
+ // Fallback to normal search
3872
+ return findSourceLine(text);
3873
+ }
3874
+
3875
+ // Helper: find code block range in source (fenced code blocks)
3876
+ function findCodeBlockRange(codeText) {
3877
+ const clickedLines = codeText.split('\\n').map(l => l.trim()).filter(l => l);
3878
+ const clickedContent = clickedLines.join('\\n');
3879
+
3880
+ // Extract all code blocks from DATA
3881
+ const codeBlocks = [];
3882
+ let currentBlock = null;
3883
+
3884
+ for (let i = 0; i < DATA.length; i++) {
3885
+ const lineText = (DATA[i][0] || '').trim();
3886
+
3887
+ if (lineText.startsWith('\`\`\`') && !currentBlock) {
3888
+ // Start of a code block
3889
+ currentBlock = { startLine: i + 1, lines: [] };
3890
+ } else if (lineText === '\`\`\`' && currentBlock) {
3891
+ // End of a code block
3892
+ currentBlock.endLine = i + 1;
3893
+ currentBlock.content = currentBlock.lines.map(l => l.trim()).filter(l => l).join('\\n');
3894
+ codeBlocks.push(currentBlock);
3895
+ currentBlock = null;
3896
+ } else if (currentBlock) {
3897
+ // Inside a code block
3898
+ currentBlock.lines.push(DATA[i][0] || '');
3899
+ }
3900
+ }
3901
+
3902
+ // Find the best matching code block by content similarity
3903
+ let bestMatch = null;
3904
+ let bestScore = 0;
3905
+
3906
+ for (const block of codeBlocks) {
3907
+ // Calculate similarity score
3908
+ let score = 0;
3909
+
3910
+ // Exact match
3911
+ if (block.content === clickedContent) {
3912
+ score = 1000;
3913
+ } else {
3914
+ // Check line-by-line matches
3915
+ const blockLines = block.content.split('\\n');
3916
+ for (const clickedLine of clickedLines) {
3917
+ if (clickedLine.length > 3) {
3918
+ for (const blockLine of blockLines) {
3919
+ if (blockLine.includes(clickedLine) || clickedLine.includes(blockLine)) {
3920
+ score += clickedLine.length;
3921
+ }
3922
+ }
3923
+ }
3924
+ }
3925
+ }
3926
+
3927
+ if (score > bestScore) {
3928
+ bestScore = score;
3929
+ bestMatch = block;
3930
+ }
3931
+ }
3932
+
3933
+ if (bestMatch) {
3934
+ return { startLine: bestMatch.startLine, endLine: bestMatch.endLine };
3935
+ }
3936
+
3937
+ // Fallback: find by first line content matching
3938
+ const firstCodeLine = clickedLines[0];
3939
+ if (firstCodeLine && firstCodeLine.length > 3) {
3940
+ for (let i = 0; i < DATA.length; i++) {
3941
+ const lineText = (DATA[i][0] || '').trim();
3942
+ if (lineText.includes(firstCodeLine.slice(0, 30))) {
3943
+ return { startLine: i + 1, endLine: i + 1 };
3944
+ }
3945
+ }
3946
+ }
3947
+
3948
+ return { startLine: -1, endLine: -1 };
3949
+ }
3950
+
3951
+ // Helper: find source line for image by src
3952
+ function findImageSourceLine(src) {
3953
+ if (!src) return -1;
3954
+ const filename = src.split('/').pop().split('?')[0];
3955
+ for (let i = 0; i < DATA.length; i++) {
3956
+ const lineText = DATA[i][0] || '';
3957
+ if (lineText.includes(filename) || lineText.includes(src)) {
3958
+ return i + 1;
3959
+ }
3960
+ }
3961
+ return -1;
3962
+ }
3963
+
3964
+ // Trigger source cell selection (reuse existing comment flow)
3965
+ function selectSourceRange(startRow, endRow) {
3966
+ selection = { startRow, endRow: endRow || startRow, startCol: 1, endCol: 1 };
3967
+ updateSelectionVisual();
3968
+
3969
+ // Clear header selection
3970
+ document.querySelectorAll('thead th.selected').forEach(el => el.classList.remove('selected'));
3971
+
3972
+ // Scroll source table to show the selected row, then open card
3973
+ const sourceTd = tbody.querySelector('td[data-row="' + startRow + '"][data-col="1"]');
3974
+ if (sourceTd) {
3975
+ sourceTd.scrollIntoView({ behavior: 'smooth', block: 'center' });
3976
+ // Wait for scroll to complete before positioning card
3977
+ setTimeout(() => openCardForSelection(), 350);
3978
+ } else {
3979
+ openCardForSelection();
3980
+ }
3981
+ }
3982
+
3983
+ // Click on block elements
3984
+ preview.addEventListener('click', (e) => {
3985
+ // Handle image clicks
3986
+ if (e.target.tagName === 'IMG') {
3987
+ if (!e.defaultPrevented) {
3988
+ const line = findImageSourceLine(e.target.src);
3989
+ if (line > 0) {
3990
+ e.preventDefault();
3991
+ e.stopPropagation();
3992
+ selectSourceRange(line);
3993
+ }
3994
+ }
3995
+ return;
3996
+ }
3997
+
3998
+ // Ignore clicks on links, mermaid, video overlay
3999
+ if (e.target.closest('a, .mermaid-container, .video-fullscreen-overlay')) return;
4000
+
4001
+ // Handle code blocks - select entire block
4002
+ const pre = e.target.closest('pre');
4003
+ if (pre) {
4004
+ const code = pre.querySelector('code') || pre;
4005
+ const { startLine, endLine } = findCodeBlockRange(code.textContent);
4006
+ if (startLine > 0) {
4007
+ e.preventDefault();
4008
+ selectSourceRange(startLine, endLine);
4009
+ }
4010
+ return;
4011
+ }
4012
+
4013
+ const target = e.target.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th');
4014
+ if (!target) return;
4015
+
4016
+ // Use table-specific search for table cells
4017
+ const isTableCell = target.tagName === 'TD' || target.tagName === 'TH';
4018
+ const line = isTableCell ? findTableSourceLine(target.textContent) : findSourceLine(target.textContent);
4019
+ if (line <= 0) return;
4020
+
4021
+ e.preventDefault();
4022
+ selectSourceRange(line);
4023
+ });
4024
+
4025
+ // Text selection to open comment for range
4026
+ preview.addEventListener('mouseup', (e) => {
4027
+ setTimeout(() => {
4028
+ const sel = window.getSelection();
4029
+ if (!sel || sel.isCollapsed) return;
4030
+
4031
+ const text = sel.toString().trim();
4032
+ if (!text || text.length < 5) return;
4033
+
4034
+ const lines = text.split('\\n').filter(l => l.trim());
4035
+ if (lines.length === 0) return;
4036
+
4037
+ const startLine = findSourceLine(lines[0]);
4038
+ const endLine = lines.length > 1 ? findSourceLine(lines[lines.length - 1]) : startLine;
4039
+
4040
+ if (startLine <= 0) return;
4041
+
4042
+ sel.removeAllRanges();
4043
+ selectSourceRange(startLine, endLine > 0 ? endLine : startLine);
4044
+ }, 10);
4045
+ });
4046
+ })();
3804
4047
  </script>
3805
4048
  </body>
3806
4049
  </html>`;
@@ -4039,7 +4282,10 @@ function createFileServer(filePath) {
4039
4282
  res.end("not found");
4040
4283
  });
4041
4284
 
4285
+ let serverStarted = false;
4286
+
4042
4287
  function tryListen(attemptPort, attempts = 0) {
4288
+ if (serverStarted) return; // Prevent double-start race condition
4043
4289
  if (attempts >= MAX_PORT_ATTEMPTS) {
4044
4290
  console.error(
4045
4291
  `Could not find an available port for ${baseName} after ${MAX_PORT_ATTEMPTS} attempts.`,
@@ -4050,6 +4296,7 @@ function createFileServer(filePath) {
4050
4296
  }
4051
4297
 
4052
4298
  ctx.server.once("error", (err) => {
4299
+ if (serverStarted) return; // Already started on another port
4053
4300
  if (err.code === "EADDRINUSE") {
4054
4301
  tryListen(attemptPort + 1, attempts + 1);
4055
4302
  } else {
@@ -4060,6 +4307,12 @@ function createFileServer(filePath) {
4060
4307
  });
4061
4308
 
4062
4309
  ctx.server.listen(attemptPort, () => {
4310
+ if (serverStarted) {
4311
+ // Race condition: server started on multiple ports, close this one
4312
+ try { ctx.server.close(); } catch (_) {}
4313
+ return;
4314
+ }
4315
+ serverStarted = true;
4063
4316
  ctx.port = attemptPort;
4064
4317
  nextPort = attemptPort + 1;
4065
4318
  activeServers.set(filePath, ctx);
@@ -4193,7 +4446,10 @@ function createDiffServer(diffContent) {
4193
4446
  res.end("not found");
4194
4447
  });
4195
4448
 
4449
+ let serverStarted = false;
4450
+
4196
4451
  function tryListen(attemptPort, attempts = 0) {
4452
+ if (serverStarted) return; // Prevent double-start race condition
4197
4453
  if (attempts >= MAX_PORT_ATTEMPTS) {
4198
4454
  console.error(
4199
4455
  `Could not find an available port for diff viewer after ${MAX_PORT_ATTEMPTS} attempts.`,
@@ -4204,6 +4460,7 @@ function createDiffServer(diffContent) {
4204
4460
  }
4205
4461
 
4206
4462
  ctx.server.once("error", (err) => {
4463
+ if (serverStarted) return; // Already started on another port
4207
4464
  if (err.code === "EADDRINUSE") {
4208
4465
  tryListen(attemptPort + 1, attempts + 1);
4209
4466
  } else {
@@ -4214,6 +4471,12 @@ function createDiffServer(diffContent) {
4214
4471
  });
4215
4472
 
4216
4473
  ctx.server.listen(attemptPort, () => {
4474
+ if (serverStarted) {
4475
+ // Race condition: server started on multiple ports, close this one
4476
+ try { ctx.server.close(); } catch (_) {}
4477
+ return;
4478
+ }
4479
+ serverStarted = true;
4217
4480
  ctx.port = attemptPort;
4218
4481
  ctx.heartbeat = setInterval(() => broadcast("ping"), 25000);
4219
4482
  console.log(`Diff viewer started: http://localhost:${attemptPort}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
5
5
  "type": "module",
6
6
  "bin": {