reviw 0.17.0 → 0.17.2

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 +130 -21
  2. package/package.json +1 -1
package/cli.cjs CHANGED
@@ -126,6 +126,13 @@ function sanitizeHtml(html) {
126
126
  }
127
127
 
128
128
  marked.use({
129
+ hooks: {
130
+ // テーブルをスクロールラッパーで囲む(後処理)
131
+ postprocess: function(html) {
132
+ return html.replace(/<table>/g, '<div class="table-scroll-container"><span class="scroll-hint">← scroll →</span><div class="table-scroll-wrapper"><table>')
133
+ .replace(/<\/table>/g, '</table></div></div>');
134
+ }
135
+ },
129
136
  renderer: {
130
137
  // 生HTMLブロックをサニタイズ
131
138
  html: function(token) {
@@ -159,7 +166,8 @@ marked.use({
159
166
  if (videoExtensions.test(href)) {
160
167
  // For videos, render as video element with controls and thumbnail preview
161
168
  var displayText = text || href.split('/').pop();
162
- return '<video src="' + escapeHtmlForXss(href) + '" controls preload="metadata" class="video-preview"' + titleAttr + '>' +
169
+ var dataAlt = text ? ' data-alt="' + escapeHtmlForXss(text) + '"' : "";
170
+ return '<video src="' + escapeHtmlForXss(href) + '" controls preload="metadata" class="video-preview"' + titleAttr + dataAlt + '>' +
163
171
  '<a href="' + escapeHtmlForXss(href) + '">📹' + escapeHtmlForXss(displayText) + '</a></video>';
164
172
  }
165
173
  return '<img src="' + escapeHtmlForXss(href) + '"' + altAttr + titleAttr + '>';
@@ -2729,7 +2737,8 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
2729
2737
  flex: 1;
2730
2738
  min-width: 0;
2731
2739
  overflow-y: auto;
2732
- overflow-x: hidden;
2740
+ overflow-x: auto;
2741
+ overscroll-behavior: contain;
2733
2742
  }
2734
2743
  .md-left .md-preview {
2735
2744
  max-height: none;
@@ -2739,6 +2748,7 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
2739
2748
  min-width: 0;
2740
2749
  overflow-y: auto;
2741
2750
  overflow-x: auto;
2751
+ overscroll-behavior: contain;
2742
2752
  }
2743
2753
  .md-right .table-box {
2744
2754
  max-width: none;
@@ -2769,8 +2779,6 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
2769
2779
  display: block;
2770
2780
  width: 100%;
2771
2781
  height: auto;
2772
- min-width: 120px;
2773
- max-width: 250px;
2774
2782
  }
2775
2783
  .md-preview code { background: rgba(255,255,255,0.08); padding: 2px 4px; border-radius: 4px; }
2776
2784
  .md-preview pre {
@@ -2896,12 +2904,38 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
2896
2904
  [data-theme="light"] .frontmatter-table tbody th {
2897
2905
  color: #7c3aed;
2898
2906
  }
2907
+ /* Table scroll container and indicator */
2908
+ .table-scroll-container {
2909
+ position: relative;
2910
+ margin: 16px 0;
2911
+ }
2912
+ .table-scroll-wrapper {
2913
+ overflow-x: auto;
2914
+ border-radius: 8px;
2915
+ }
2916
+ .scroll-hint {
2917
+ text-align: right;
2918
+ font-size: 12px;
2919
+ color: var(--accent);
2920
+ padding: 4px 8px;
2921
+ margin-bottom: 4px;
2922
+ opacity: 0;
2923
+ visibility: hidden;
2924
+ transition: opacity 200ms ease;
2925
+ }
2926
+ .table-scroll-container.can-scroll .scroll-hint {
2927
+ opacity: 0.8;
2928
+ visibility: visible;
2929
+ }
2930
+ .table-scroll-container.scrolled-end .scroll-hint {
2931
+ opacity: 0;
2932
+ visibility: hidden;
2933
+ }
2899
2934
  /* Markdown tables in preview */
2900
2935
  .md-preview table:not(.frontmatter-table table) {
2901
- width: 100%;
2936
+ min-width: 100%;
2937
+ width: max-content;
2902
2938
  border-collapse: collapse;
2903
- table-layout: fixed;
2904
- margin: 16px 0;
2905
2939
  border: 1px solid var(--border);
2906
2940
  border-radius: 8px;
2907
2941
  }
@@ -2913,17 +2947,12 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
2913
2947
  vertical-align: top;
2914
2948
  word-break: break-word;
2915
2949
  overflow-wrap: anywhere;
2950
+ width: auto;
2916
2951
  }
2917
- .md-preview table:not(.frontmatter-table table) th:first-child,
2918
- .md-preview table:not(.frontmatter-table table) td:first-child {
2919
- width: 30%;
2920
- min-width: 200px;
2921
- }
2922
- .md-preview table:not(.frontmatter-table table) th:last-child,
2923
- .md-preview table:not(.frontmatter-table table) td:last-child {
2924
- width: 180px;
2925
- min-width: 180px;
2926
- max-width: 180px;
2952
+ /* Force equal column widths when colgroup is not specified */
2953
+ .md-preview table:not(.frontmatter-table table) colgroup ~ * th,
2954
+ .md-preview table:not(.frontmatter-table table) colgroup ~ * td {
2955
+ width: auto;
2927
2956
  }
2928
2957
  .md-preview table:not(.frontmatter-table table) td:has(video),
2929
2958
  .md-preview table:not(.frontmatter-table table) td:has(img) {
@@ -4033,6 +4062,48 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
4033
4062
  themeToggle.addEventListener('click', toggleTheme);
4034
4063
  })();
4035
4064
 
4065
+ // --- Table Scroll Indicator ---
4066
+ (function initTableScrollIndicators() {
4067
+ function updateScrollIndicator(wrapper) {
4068
+ const container = wrapper.closest('.table-scroll-container');
4069
+ if (!container) return;
4070
+
4071
+ const canScroll = wrapper.scrollWidth > wrapper.clientWidth;
4072
+ const isAtEnd = wrapper.scrollLeft + wrapper.clientWidth >= wrapper.scrollWidth - 5;
4073
+
4074
+ container.classList.toggle('can-scroll', canScroll && !isAtEnd);
4075
+ container.classList.toggle('scrolled-end', isAtEnd);
4076
+ }
4077
+
4078
+ function initWrapper(wrapper) {
4079
+ updateScrollIndicator(wrapper);
4080
+ wrapper.addEventListener('scroll', () => updateScrollIndicator(wrapper));
4081
+ }
4082
+
4083
+ // Initialize existing wrappers
4084
+ document.querySelectorAll('.table-scroll-wrapper').forEach(initWrapper);
4085
+
4086
+ // Watch for dynamically added wrappers
4087
+ const observer = new MutationObserver((mutations) => {
4088
+ mutations.forEach((mutation) => {
4089
+ mutation.addedNodes.forEach((node) => {
4090
+ if (node.nodeType === 1) {
4091
+ if (node.classList?.contains('table-scroll-wrapper')) {
4092
+ initWrapper(node);
4093
+ }
4094
+ node.querySelectorAll?.('.table-scroll-wrapper').forEach(initWrapper);
4095
+ }
4096
+ });
4097
+ });
4098
+ });
4099
+ observer.observe(document.body, { childList: true, subtree: true });
4100
+
4101
+ // Update on resize
4102
+ window.addEventListener('resize', () => {
4103
+ document.querySelectorAll('.table-scroll-wrapper').forEach(updateScrollIndicator);
4104
+ });
4105
+ })();
4106
+
4036
4107
  // --- History Management ---
4037
4108
  // History is now server-side (file-based), HISTORY_DATA is provided by server
4038
4109
 
@@ -6117,8 +6188,46 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
6117
6188
  .trim();
6118
6189
  }
6119
6190
 
6120
- // Helper: find matching source line for text
6121
- function findSourceLine(text) {
6191
+ // Helper: find matching source line for text or element
6192
+ // If element is provided, also searches by media src attributes
6193
+ function findSourceLine(text, element = null) {
6194
+ // First, try to find by media src (images, videos) in the element
6195
+ if (element) {
6196
+ const mediaElements = element.querySelectorAll('img, video');
6197
+ for (const m of mediaElements) {
6198
+ const src = m.getAttribute('src');
6199
+ if (!src) continue;
6200
+
6201
+ const fileName = src.split('/').pop();
6202
+ const alt = m.getAttribute('alt') || m.getAttribute('data-alt') || m.getAttribute('title') || '';
6203
+
6204
+ // Search for lines containing this media file (![...](path) syntax)
6205
+ // Prioritize exact match with alt text
6206
+ let bestMatch = -1;
6207
+ for (let i = 0; i < DATA.length; i++) {
6208
+ const lineText = (DATA[i][0] || '');
6209
+ if (!lineText.includes(fileName)) continue;
6210
+
6211
+ // Check if it's an image/video markdown syntax
6212
+ const match = lineText.match(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/);
6213
+ if (!match) continue;
6214
+
6215
+ const [, mdAlt, mdPath] = match;
6216
+
6217
+ // Exact path match
6218
+ if (mdPath.includes(fileName)) {
6219
+ // If alt text matches exactly, this is definitely the right one
6220
+ if (alt && mdAlt && mdAlt === alt) {
6221
+ return i + 1;
6222
+ }
6223
+ // Otherwise, remember as fallback (prefer first match)
6224
+ if (bestMatch === -1) bestMatch = i + 1;
6225
+ }
6226
+ }
6227
+ if (bestMatch !== -1) return bestMatch;
6228
+ }
6229
+ }
6230
+
6122
6231
  if (!text) return -1;
6123
6232
  const normalized = text.trim().replace(/\\s+/g, ' ').slice(0, 100);
6124
6233
  if (!normalized) return -1;
@@ -6358,9 +6467,9 @@ function htmlTemplate(dataRows, cols, projectRoot, relativePath, mode, previewHt
6358
6467
  const target = e.target.closest('p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th');
6359
6468
  if (!target) return;
6360
6469
 
6361
- // Use table-specific search for table cells
6470
+ // Use table-specific search for table cells, otherwise use element-aware search
6362
6471
  const isTableCell = target.tagName === 'TD' || target.tagName === 'TH';
6363
- const line = isTableCell ? findTableSourceLine(target.textContent) : findSourceLine(target.textContent);
6472
+ const line = isTableCell ? findTableSourceLine(target.textContent) : findSourceLine(target.textContent, target);
6364
6473
  if (line <= 0) return;
6365
6474
 
6366
6475
  e.preventDefault();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reviw",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "Lightweight file reviewer with in-browser comments for CSV, TSV, Markdown, and Git diffs.",
5
5
  "type": "module",
6
6
  "bin": {