@youtyan/code-viewer 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -183,6 +183,11 @@ visible, and file pages show a preview when the browser can safely render the
183
183
  file. Unsupported binary files show a clear unavailable state with file
184
184
  metadata instead of dumping bytes as text.
185
185
 
186
+ CSV and TSV files open as a table with all-column search, per-column filters, and
187
+ three-state column sorting (ascending, descending, then source order). The
188
+ visible-row count and reset action stay above the table while original file row
189
+ numbers remain attached to their data after filtering or sorting.
190
+
186
191
  Markdown files use a dedicated preview tab. Relative links and images are
187
192
  resolved inside the repository, code blocks are highlighted with Shiki, and
188
193
  Mermaid diagrams are rendered lazily in the browser (click any diagram to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,10 +67,12 @@
67
67
  "@biomejs/biome": "2.4.14",
68
68
  "@happy-dom/global-registrator": "^20.10.6",
69
69
  "@types/better-sqlite3": "^7.6.13",
70
+ "@types/d3-dsv": "3.0.7",
70
71
  "@types/markdown-it": "14.1.2",
71
72
  "@types/pg": "8.20.0",
72
73
  "@xterm/addon-fit": "^0.11.0",
73
74
  "@xterm/xterm": "^6.0.0",
75
+ "d3-dsv": "3.0.1",
74
76
  "esbuild": "^0.28.1",
75
77
  "highlight.js": "11.9.0",
76
78
  "markdown-it": "14.1.1",
package/web/app.js CHANGED
@@ -1818,7 +1818,7 @@ ${lines.join("\n")}
1818
1818
  return SOURCE_SHIKI_LANG_ALIASES[lang] || lang;
1819
1819
  }
1820
1820
  function isPreviewableSource(path) {
1821
- return /\.(md|markdown|mdown|mkdn|mdx|html|htm)$/i.test(path);
1821
+ return /\.(md|markdown|mdown|mkdn|mdx|html|htm|csv|tsv)$/i.test(path);
1822
1822
  }
1823
1823
  function sourceInternalPathKind(path) {
1824
1824
  for (const part of path.split(/[\\/]+/)) {
@@ -1831,6 +1831,8 @@ ${lines.join("\n")}
1831
1831
  function sourcePreviewKind(path) {
1832
1832
  if (/\.(md|markdown|mdown|mkdn|mdx)$/i.test(path)) return "markdown";
1833
1833
  if (/\.(html|htm)$/i.test(path)) return "html";
1834
+ if (/\.csv$/i.test(path)) return "csv";
1835
+ if (/\.tsv$/i.test(path)) return "tsv";
1834
1836
  return null;
1835
1837
  }
1836
1838
  var EXT_TO_LANG = {
@@ -15642,8 +15644,8 @@ Details: ${JSON.stringify(output)}` : "";
15642
15644
  function formatTime(iso) {
15643
15645
  try {
15644
15646
  const d2 = new Date(iso);
15645
- const pad = (n2) => String(n2).padStart(2, "0");
15646
- return `${d2.getFullYear()}-${pad(d2.getMonth() + 1)}-${pad(d2.getDate())} ${pad(d2.getHours())}:${pad(d2.getMinutes())}:${pad(d2.getSeconds())}`;
15647
+ const pad2 = (n2) => String(n2).padStart(2, "0");
15648
+ return `${d2.getFullYear()}-${pad2(d2.getMonth() + 1)}-${pad2(d2.getDate())} ${pad2(d2.getHours())}:${pad2(d2.getMinutes())}:${pad2(d2.getSeconds())}`;
15647
15649
  } catch {
15648
15650
  return iso;
15649
15651
  }
@@ -16376,7 +16378,419 @@ Details: ${JSON.stringify(output)}` : "";
16376
16378
  return idx >= 0 ? trimmed.slice(idx + 1) : trimmed;
16377
16379
  }
16378
16380
 
16381
+ // node_modules/.pnpm/d3-dsv@3.0.1/node_modules/d3-dsv/src/dsv.js
16382
+ var EOL = {};
16383
+ var EOF = {};
16384
+ var QUOTE = 34;
16385
+ var NEWLINE = 10;
16386
+ var RETURN = 13;
16387
+ function objectConverter(columns) {
16388
+ return new Function("d", "return {" + columns.map(function(name, i2) {
16389
+ return JSON.stringify(name) + ": d[" + i2 + '] || ""';
16390
+ }).join(",") + "}");
16391
+ }
16392
+ function customConverter(columns, f2) {
16393
+ var object = objectConverter(columns);
16394
+ return function(row, i2) {
16395
+ return f2(object(row), i2, columns);
16396
+ };
16397
+ }
16398
+ function inferColumns(rows) {
16399
+ var columnSet = /* @__PURE__ */ Object.create(null), columns = [];
16400
+ rows.forEach(function(row) {
16401
+ for (var column in row) {
16402
+ if (!(column in columnSet)) {
16403
+ columns.push(columnSet[column] = column);
16404
+ }
16405
+ }
16406
+ });
16407
+ return columns;
16408
+ }
16409
+ function pad(value, width) {
16410
+ var s2 = value + "", length = s2.length;
16411
+ return length < width ? new Array(width - length + 1).join(0) + s2 : s2;
16412
+ }
16413
+ function formatYear(year) {
16414
+ return year < 0 ? "-" + pad(-year, 6) : year > 9999 ? "+" + pad(year, 6) : pad(year, 4);
16415
+ }
16416
+ function formatDate(date) {
16417
+ var hours = date.getUTCHours(), minutes = date.getUTCMinutes(), seconds = date.getUTCSeconds(), milliseconds = date.getUTCMilliseconds();
16418
+ return isNaN(date) ? "Invalid Date" : formatYear(date.getUTCFullYear(), 4) + "-" + pad(date.getUTCMonth() + 1, 2) + "-" + pad(date.getUTCDate(), 2) + (milliseconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "." + pad(milliseconds, 3) + "Z" : seconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "Z" : minutes || hours ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + "Z" : "");
16419
+ }
16420
+ function dsv_default(delimiter2) {
16421
+ var reFormat = new RegExp('["' + delimiter2 + "\n\r]"), DELIMITER = delimiter2.charCodeAt(0);
16422
+ function parse(text3, f2) {
16423
+ var convert, columns, rows = parseRows(text3, function(row, i2) {
16424
+ if (convert) return convert(row, i2 - 1);
16425
+ columns = row, convert = f2 ? customConverter(row, f2) : objectConverter(row);
16426
+ });
16427
+ rows.columns = columns || [];
16428
+ return rows;
16429
+ }
16430
+ function parseRows(text3, f2) {
16431
+ var rows = [], N = text3.length, I = 0, n2 = 0, t2, eof = N <= 0, eol = false;
16432
+ if (text3.charCodeAt(N - 1) === NEWLINE) --N;
16433
+ if (text3.charCodeAt(N - 1) === RETURN) --N;
16434
+ function token() {
16435
+ if (eof) return EOF;
16436
+ if (eol) return eol = false, EOL;
16437
+ var i2, j = I, c2;
16438
+ if (text3.charCodeAt(j) === QUOTE) {
16439
+ while (I++ < N && text3.charCodeAt(I) !== QUOTE || text3.charCodeAt(++I) === QUOTE) ;
16440
+ if ((i2 = I) >= N) eof = true;
16441
+ else if ((c2 = text3.charCodeAt(I++)) === NEWLINE) eol = true;
16442
+ else if (c2 === RETURN) {
16443
+ eol = true;
16444
+ if (text3.charCodeAt(I) === NEWLINE) ++I;
16445
+ }
16446
+ return text3.slice(j + 1, i2 - 1).replace(/""/g, '"');
16447
+ }
16448
+ while (I < N) {
16449
+ if ((c2 = text3.charCodeAt(i2 = I++)) === NEWLINE) eol = true;
16450
+ else if (c2 === RETURN) {
16451
+ eol = true;
16452
+ if (text3.charCodeAt(I) === NEWLINE) ++I;
16453
+ } else if (c2 !== DELIMITER) continue;
16454
+ return text3.slice(j, i2);
16455
+ }
16456
+ return eof = true, text3.slice(j, N);
16457
+ }
16458
+ while ((t2 = token()) !== EOF) {
16459
+ var row = [];
16460
+ while (t2 !== EOL && t2 !== EOF) row.push(t2), t2 = token();
16461
+ if (f2 && (row = f2(row, n2++)) == null) continue;
16462
+ rows.push(row);
16463
+ }
16464
+ return rows;
16465
+ }
16466
+ function preformatBody(rows, columns) {
16467
+ return rows.map(function(row) {
16468
+ return columns.map(function(column) {
16469
+ return formatValue2(row[column]);
16470
+ }).join(delimiter2);
16471
+ });
16472
+ }
16473
+ function format2(rows, columns) {
16474
+ if (columns == null) columns = inferColumns(rows);
16475
+ return [columns.map(formatValue2).join(delimiter2)].concat(preformatBody(rows, columns)).join("\n");
16476
+ }
16477
+ function formatBody(rows, columns) {
16478
+ if (columns == null) columns = inferColumns(rows);
16479
+ return preformatBody(rows, columns).join("\n");
16480
+ }
16481
+ function formatRows(rows) {
16482
+ return rows.map(formatRow).join("\n");
16483
+ }
16484
+ function formatRow(row) {
16485
+ return row.map(formatValue2).join(delimiter2);
16486
+ }
16487
+ function formatValue2(value) {
16488
+ return value == null ? "" : value instanceof Date ? formatDate(value) : reFormat.test(value += "") ? '"' + value.replace(/"/g, '""') + '"' : value;
16489
+ }
16490
+ return {
16491
+ parse,
16492
+ parseRows,
16493
+ format: format2,
16494
+ formatBody,
16495
+ formatRows,
16496
+ formatRow,
16497
+ formatValue: formatValue2
16498
+ };
16499
+ }
16500
+
16501
+ // node_modules/.pnpm/d3-dsv@3.0.1/node_modules/d3-dsv/src/csv.js
16502
+ var csv = dsv_default(",");
16503
+ var csvParse = csv.parse;
16504
+ var csvParseRows = csv.parseRows;
16505
+ var csvFormat = csv.format;
16506
+ var csvFormatBody = csv.formatBody;
16507
+ var csvFormatRows = csv.formatRows;
16508
+ var csvFormatRow = csv.formatRow;
16509
+ var csvFormatValue = csv.formatValue;
16510
+
16511
+ // node_modules/.pnpm/d3-dsv@3.0.1/node_modules/d3-dsv/src/tsv.js
16512
+ var tsv = dsv_default(" ");
16513
+ var tsvParse = tsv.parse;
16514
+ var tsvParseRows = tsv.parseRows;
16515
+ var tsvFormat = tsv.format;
16516
+ var tsvFormatBody = tsv.formatBody;
16517
+ var tsvFormatRows = tsv.formatRows;
16518
+ var tsvFormatRow = tsv.formatRow;
16519
+ var tsvFormatValue = tsv.formatValue;
16520
+
16521
+ // web-src/views/source-preview-i18n.ts
16522
+ var DELIMITED_PREVIEW_TEXT = {
16523
+ en: (format2) => ({
16524
+ searchLabel: `Search all ${format2.toUpperCase()} columns`,
16525
+ searchPlaceholder: "Search all columns…",
16526
+ resetLabel: "Reset",
16527
+ resetAction: "Clear search, column filters, and sorting",
16528
+ resultCount: (visible, total) => `${visible} / ${total} rows`,
16529
+ resultCountLabel: `Visible ${format2.toUpperCase()} rows`,
16530
+ columnLabel: (index) => `Column ${index}`,
16531
+ columnFilterLabel: (column) => `Filter ${column}`,
16532
+ columnFilterPlaceholder: "Filter…",
16533
+ sortAscending: (column) => `Sort ${column} ascending`,
16534
+ sortDescending: (column) => `Sort ${column} descending`,
16535
+ clearSort: (column) => `Clear sorting for ${column}`,
16536
+ noMatches: "No rows match the current filters."
16537
+ }),
16538
+ ja: (format2) => ({
16539
+ searchLabel: `${format2.toUpperCase()}の全列を検索`,
16540
+ searchPlaceholder: "全列を検索…",
16541
+ resetLabel: "リセット",
16542
+ resetAction: "検索、列フィルタ、並べ替えを解除",
16543
+ resultCount: (visible, total) => `${visible} / ${total} 行`,
16544
+ resultCountLabel: `表示中の${format2.toUpperCase()}行数`,
16545
+ columnLabel: (index) => `列 ${index}`,
16546
+ columnFilterLabel: (column) => `${column}を絞り込み`,
16547
+ columnFilterPlaceholder: "絞り込み…",
16548
+ sortAscending: (column) => `${column}を昇順に並べ替え`,
16549
+ sortDescending: (column) => `${column}を降順に並べ替え`,
16550
+ clearSort: (column) => `${column}の並べ替えを解除`,
16551
+ noMatches: "現在の条件に一致する行はありません。"
16552
+ })
16553
+ };
16554
+ function delimitedPreviewText(language, format2) {
16555
+ return DELIMITED_PREVIEW_TEXT[language](format2);
16556
+ }
16557
+
16379
16558
  // web-src/views/source-preview-elements.ts
16559
+ function renderDelimitedPreview(text3, format2, getText = () => delimitedPreviewText("en", format2)) {
16560
+ const preview = Object.assign(
16561
+ document.createElement("div"),
16562
+ { localize: () => void 0 }
16563
+ );
16564
+ preview.className = "gdp-csv-preview";
16565
+ const parseRows = format2 === "tsv" ? tsvParseRows : csvParseRows;
16566
+ const rows = parseRows(text3.charCodeAt(0) === 65279 ? text3.slice(1) : text3);
16567
+ if (rows.length === 0) return preview;
16568
+ const columnCount = rows.reduce(
16569
+ (largest, row) => Math.max(largest, row.length),
16570
+ 0
16571
+ );
16572
+ const dataRows = rows.slice(1).map((row, index) => ({
16573
+ sourceIndex: index + 1,
16574
+ cells: Array.from(
16575
+ { length: columnCount },
16576
+ (_, column) => row[column] ?? ""
16577
+ )
16578
+ }));
16579
+ const collator = new Intl.Collator(void 0, {
16580
+ numeric: true,
16581
+ sensitivity: "base"
16582
+ });
16583
+ const columnFilters = Array.from({ length: columnCount }, () => "");
16584
+ let sortColumn = null;
16585
+ let sortDirection = null;
16586
+ let visibleCount = dataRows.length;
16587
+ let emptyCell = null;
16588
+ const shell = document.createElement("section");
16589
+ shell.className = "gdp-csv-shell";
16590
+ const toolbar = document.createElement("div");
16591
+ toolbar.className = "gdp-csv-toolbar";
16592
+ const searchIcon = document.createElement("span");
16593
+ searchIcon.className = "gdp-csv-search-icon";
16594
+ searchIcon.setAttribute("aria-hidden", "true");
16595
+ searchIcon.innerHTML = iconSvg("octicon-search", SEARCH_16_PATH);
16596
+ const search = document.createElement("input");
16597
+ search.type = "search";
16598
+ search.className = "gdp-csv-search";
16599
+ search.autocomplete = "off";
16600
+ search.spellcheck = false;
16601
+ const status = document.createElement("span");
16602
+ status.className = "gdp-csv-result-count";
16603
+ status.setAttribute("role", "status");
16604
+ status.setAttribute("aria-live", "polite");
16605
+ const reset = document.createElement("button");
16606
+ reset.type = "button";
16607
+ reset.className = "gdp-csv-reset";
16608
+ toolbar.append(searchIcon, search, status, reset);
16609
+ const table2 = document.createElement("table");
16610
+ table2.className = "gdp-csv-table";
16611
+ const thead = document.createElement("thead");
16612
+ const headRow = document.createElement("tr");
16613
+ const corner = document.createElement("th");
16614
+ corner.className = "gdp-csv-row-number";
16615
+ corner.setAttribute("aria-hidden", "true");
16616
+ headRow.appendChild(corner);
16617
+ const sortHeaders = [];
16618
+ for (let column = 0; column < columnCount; column++) {
16619
+ const th = document.createElement("th");
16620
+ th.scope = "col";
16621
+ const button = document.createElement("button");
16622
+ button.type = "button";
16623
+ button.className = "gdp-csv-sort-button";
16624
+ button.dataset.csvSortColumn = String(column);
16625
+ const label = document.createElement("span");
16626
+ label.textContent = rows[0][column] ?? "";
16627
+ button.appendChild(label);
16628
+ button.addEventListener("click", () => {
16629
+ if (sortColumn !== column) {
16630
+ sortColumn = column;
16631
+ sortDirection = "asc";
16632
+ } else if (sortDirection === "asc") {
16633
+ sortDirection = "desc";
16634
+ } else if (sortDirection === "desc") {
16635
+ sortColumn = null;
16636
+ sortDirection = null;
16637
+ } else {
16638
+ sortDirection = "asc";
16639
+ }
16640
+ renderBody();
16641
+ button.focus();
16642
+ });
16643
+ th.appendChild(button);
16644
+ headRow.appendChild(th);
16645
+ sortHeaders.push({ cell: th, button, label });
16646
+ }
16647
+ thead.appendChild(headRow);
16648
+ const filterRow = document.createElement("tr");
16649
+ filterRow.className = "gdp-csv-filter-row";
16650
+ const filterCorner = document.createElement("th");
16651
+ filterCorner.className = "gdp-csv-row-number";
16652
+ filterCorner.setAttribute("aria-hidden", "true");
16653
+ filterRow.appendChild(filterCorner);
16654
+ const filterInputs = [];
16655
+ for (let column = 0; column < columnCount; column++) {
16656
+ const th = document.createElement("th");
16657
+ const input2 = document.createElement("input");
16658
+ input2.type = "search";
16659
+ input2.className = "gdp-csv-column-filter";
16660
+ input2.dataset.csvColumnFilter = String(column);
16661
+ input2.autocomplete = "off";
16662
+ input2.spellcheck = false;
16663
+ input2.addEventListener("input", () => {
16664
+ columnFilters[column] = input2.value;
16665
+ renderBody();
16666
+ });
16667
+ input2.addEventListener("keydown", (event) => {
16668
+ if (isImeComposing(event) || event.key !== "Escape" || !input2.value)
16669
+ return;
16670
+ input2.value = "";
16671
+ columnFilters[column] = "";
16672
+ renderBody();
16673
+ });
16674
+ th.appendChild(input2);
16675
+ filterRow.appendChild(th);
16676
+ filterInputs.push(input2);
16677
+ }
16678
+ thead.appendChild(filterRow);
16679
+ table2.appendChild(thead);
16680
+ const tbody = document.createElement("tbody");
16681
+ table2.appendChild(tbody);
16682
+ shell.append(toolbar, table2);
16683
+ preview.appendChild(shell);
16684
+ function normalized(value) {
16685
+ return value.trim().toLocaleLowerCase();
16686
+ }
16687
+ function activeStateCount() {
16688
+ return (normalized(search.value) ? 1 : 0) + columnFilters.filter((value) => normalized(value)).length + (sortColumn === null ? 0 : 1);
16689
+ }
16690
+ function localizedColumnLabel(column, textValue) {
16691
+ return rows[0][column] || textValue.columnLabel(column + 1);
16692
+ }
16693
+ function syncLocalizedText() {
16694
+ const textValue = getText();
16695
+ toolbar.setAttribute("aria-label", textValue.searchLabel);
16696
+ search.placeholder = textValue.searchPlaceholder;
16697
+ search.setAttribute("aria-label", textValue.searchLabel);
16698
+ reset.textContent = textValue.resetLabel;
16699
+ reset.title = textValue.resetAction;
16700
+ reset.setAttribute("aria-label", textValue.resetAction);
16701
+ status.textContent = textValue.resultCount(visibleCount, dataRows.length);
16702
+ status.setAttribute("aria-label", textValue.resultCountLabel);
16703
+ if (emptyCell) emptyCell.textContent = textValue.noMatches;
16704
+ sortHeaders.forEach(({ cell, button }, column) => {
16705
+ const columnLabel = localizedColumnLabel(column, textValue);
16706
+ const active = sortColumn === column ? sortDirection : null;
16707
+ cell.setAttribute(
16708
+ "aria-sort",
16709
+ active === "asc" ? "ascending" : active === "desc" ? "descending" : "none"
16710
+ );
16711
+ button.dataset.sortDirection = active ?? "none";
16712
+ const action = active === "asc" ? textValue.sortDescending(columnLabel) : active === "desc" ? textValue.clearSort(columnLabel) : textValue.sortAscending(columnLabel);
16713
+ button.title = action;
16714
+ button.setAttribute("aria-label", action);
16715
+ });
16716
+ filterInputs.forEach((input2, column) => {
16717
+ const columnLabel = localizedColumnLabel(column, textValue);
16718
+ input2.placeholder = textValue.columnFilterPlaceholder;
16719
+ input2.setAttribute(
16720
+ "aria-label",
16721
+ textValue.columnFilterLabel(columnLabel)
16722
+ );
16723
+ });
16724
+ }
16725
+ function renderBody() {
16726
+ const globalQuery = normalized(search.value);
16727
+ const filters = columnFilters.map(normalized);
16728
+ const filtered = dataRows.filter(
16729
+ (row) => (!globalQuery || row.cells.some(
16730
+ (cell) => cell.toLocaleLowerCase().includes(globalQuery)
16731
+ )) && filters.every(
16732
+ (filter, column) => !filter || row.cells[column].toLocaleLowerCase().includes(filter)
16733
+ )
16734
+ );
16735
+ const visibleRows = sortColumn === null || sortDirection === null ? filtered : [...filtered].sort((a2, b2) => {
16736
+ const aValue = a2.cells[sortColumn];
16737
+ const bValue = b2.cells[sortColumn];
16738
+ if (!aValue && bValue) return 1;
16739
+ if (aValue && !bValue) return -1;
16740
+ const compared = collator.compare(aValue, bValue);
16741
+ if (compared === 0) return a2.sourceIndex - b2.sourceIndex;
16742
+ return sortDirection === "asc" ? compared : -compared;
16743
+ });
16744
+ tbody.replaceChildren();
16745
+ emptyCell = null;
16746
+ for (const row of visibleRows) {
16747
+ const tr = document.createElement("tr");
16748
+ const rowNumber = document.createElement("th");
16749
+ rowNumber.className = "gdp-csv-row-number";
16750
+ rowNumber.scope = "row";
16751
+ rowNumber.textContent = String(row.sourceIndex);
16752
+ tr.appendChild(rowNumber);
16753
+ for (const value of row.cells) {
16754
+ const td = document.createElement("td");
16755
+ td.textContent = value;
16756
+ tr.appendChild(td);
16757
+ }
16758
+ tbody.appendChild(tr);
16759
+ }
16760
+ if (visibleRows.length === 0) {
16761
+ const tr = document.createElement("tr");
16762
+ tr.className = "gdp-csv-empty-row";
16763
+ emptyCell = document.createElement("td");
16764
+ emptyCell.colSpan = columnCount + 1;
16765
+ tr.appendChild(emptyCell);
16766
+ tbody.appendChild(tr);
16767
+ }
16768
+ visibleCount = visibleRows.length;
16769
+ reset.disabled = activeStateCount() === 0;
16770
+ syncLocalizedText();
16771
+ }
16772
+ search.addEventListener("input", renderBody);
16773
+ search.addEventListener("keydown", (event) => {
16774
+ if (isImeComposing(event) || event.key !== "Escape" || !search.value)
16775
+ return;
16776
+ search.value = "";
16777
+ renderBody();
16778
+ });
16779
+ reset.addEventListener("click", () => {
16780
+ search.value = "";
16781
+ columnFilters.fill("");
16782
+ filterInputs.forEach((input2) => {
16783
+ input2.value = "";
16784
+ });
16785
+ sortColumn = null;
16786
+ sortDirection = null;
16787
+ renderBody();
16788
+ search.focus();
16789
+ });
16790
+ preview.localize = syncLocalizedText;
16791
+ renderBody();
16792
+ return preview;
16793
+ }
16380
16794
  function renderHtmlPreviewFrame(title, html, extraClass = "") {
16381
16795
  const preview = document.createElement("div");
16382
16796
  preview.className = ["gdp-html-preview", extraClass].filter(Boolean).join(" ");
@@ -17106,6 +17520,8 @@ Details: ${JSON.stringify(output)}` : "";
17106
17520
  data.text,
17107
17521
  "s3-html-preview"
17108
17522
  );
17523
+ } else if (previewKind === "csv" || previewKind === "tsv") {
17524
+ body = renderDelimitedPreview(data.text, previewKind);
17109
17525
  } else if (previewKind === "markdown") {
17110
17526
  body = await renderMarkdownPreview(
17111
17527
  data.text,
@@ -18166,8 +18582,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18166
18582
  }
18167
18583
  function formatTime2(timestamp) {
18168
18584
  const d2 = new Date(timestamp);
18169
- const pad = (n2) => String(n2).padStart(2, "0");
18170
- return `${pad(d2.getHours())}:${pad(d2.getMinutes())}:${pad(d2.getSeconds())}`;
18585
+ const pad2 = (n2) => String(n2).padStart(2, "0");
18586
+ return `${pad2(d2.getHours())}:${pad2(d2.getMinutes())}:${pad2(d2.getSeconds())}`;
18171
18587
  }
18172
18588
 
18173
18589
  // web-src/views/database/snapshot-view.ts
@@ -26510,8 +26926,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26510
26926
  const t2 = Date.parse(iso);
26511
26927
  if (!Number.isFinite(t2)) return iso;
26512
26928
  const d2 = new Date(t2);
26513
- const pad = (n2) => String(n2).padStart(2, "0");
26514
- return `${d2.getFullYear()}-${pad(d2.getMonth() + 1)}-${pad(d2.getDate())} ${pad(d2.getHours())}:${pad(d2.getMinutes())}`;
26929
+ const pad2 = (n2) => String(n2).padStart(2, "0");
26930
+ return `${d2.getFullYear()}-${pad2(d2.getMonth() + 1)}-${pad2(d2.getDate())} ${pad2(d2.getHours())}:${pad2(d2.getMinutes())}`;
26515
26931
  }
26516
26932
  function displayWhen(iso) {
26517
26933
  const relative = relativeWhen(iso);
@@ -28054,6 +28470,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28054
28470
  kind: "paragraph",
28055
28471
  text: "A file detail page exposes up to four tabs — Preview, Code, Blame, History. For text files Code is the default and ?preview=1 opts in to the Markdown / HTML preview; media files (images, video, audio, PDF) show a Preview tab only, with no Code tab. Blame and History have their own canonical URLs (view=blame, view=history) so deep links and the browser back/forward stay in sync, and they keep the Repository sidebar visible. Opening another file from the repository tree keeps the active tab (a file that cannot be previewed falls back to Code)."
28056
28472
  },
28473
+ {
28474
+ kind: "paragraph",
28475
+ text: "CSV and TSV previews provide all-column search, per-column filters, three-state column sorting (ascending, descending, then source order), a visible-row count, and a reset action. Filtering and sorting keep each row's original file row number."
28476
+ },
28057
28477
  {
28058
28478
  kind: "paragraph",
28059
28479
  text: "Relative links inside a Markdown preview lead to the same destinations as they do on GitHub: another Markdown file opens its file page, an #anchor opens the preview and scrolls to that heading, a non-Markdown file opens in the Code view, and a link to a directory opens that folder in the repository tree."
@@ -28784,6 +29204,10 @@ code-viewer annotate add-db --db app.db --tab query \\
28784
29204
  kind: "paragraph",
28785
29205
  text: "ファイル詳細ページには最大 4 つのタブ (Preview / Code / Blame / History) があります。テキストファイルは Code がデフォルトで、?preview=1 を付けると Markdown / HTML プレビューに切り替わります。画像・動画・音声・PDF などのメディアファイルは Preview タブのみ表示され、Code タブは表示されません。Blame と History はそれぞれ専用 URL (view=blame, view=history) を持つため、ディープリンクとブラウザの戻る / 進むが同期し、いずれも Repository サイドバーは表示されたままです。ツリーから別のファイルを開いても選択中のタブは維持されます (プレビューできないファイルでは Code に戻ります)。"
28786
29206
  },
29207
+ {
29208
+ kind: "paragraph",
29209
+ text: "CSV・TSVプレビューでは、全列検索、列ごとの絞り込み、昇順・降順・元の順番の3段階ソート、表示件数、リセットを利用できます。絞り込みや並べ替えの後も、各行には元ファイル上の行番号が表示されます。"
29210
+ },
28787
29211
  {
28788
29212
  kind: "paragraph",
28789
29213
  text: "Markdown プレビュー内の相対リンクは GitHub と同じ行き先に解決されます。別の Markdown ファイルはそのファイルページを開き、#見出し 付きのリンクはプレビューを開いて該当見出しまでスクロールし、Markdown 以外のファイルは Code ビュー、ディレクトリへのリンクはリポジトリツリーのそのフォルダを開きます。"
@@ -32497,8 +32921,8 @@ code-viewer annotate add-db --db app.db --tab query \\
32497
32921
  const t2 = Date.parse(iso);
32498
32922
  if (!Number.isFinite(t2)) return iso;
32499
32923
  const d2 = new Date(t2);
32500
- const pad = (n2) => String(n2).padStart(2, "0");
32501
- return `${d2.getFullYear()}-${pad(d2.getMonth() + 1)}-${pad(d2.getDate())} ${pad(d2.getHours())}:${pad(d2.getMinutes())}`;
32924
+ const pad2 = (n2) => String(n2).padStart(2, "0");
32925
+ return `${d2.getFullYear()}-${pad2(d2.getMonth() + 1)}-${pad2(d2.getDate())} ${pad2(d2.getHours())}:${pad2(d2.getMinutes())}`;
32502
32926
  }
32503
32927
  function displayWhen(iso) {
32504
32928
  const relative = relativeWhen(iso);
@@ -36872,7 +37296,8 @@ code-viewer annotate add-db --db app.db --tab query \\
36872
37296
  setViewFileButtonState,
36873
37297
  scrollMainPanel,
36874
37298
  focusMainSurface,
36875
- isPaletteOpen
37299
+ isPaletteOpen,
37300
+ getLanguage
36876
37301
  } = deps;
36877
37302
  function markdownLinkNavigationDeps() {
36878
37303
  return {
@@ -37189,15 +37614,10 @@ code-viewer annotate add-db --db app.db --tab query \\
37189
37614
  function setPreferredSourceTab(tab) {
37190
37615
  PREFERRED_SOURCE_TAB = tab;
37191
37616
  }
37192
- function consumePreferredSourceTab(previewable) {
37193
- if (!previewable) {
37194
- PREFERRED_SOURCE_TAB = null;
37195
- return "code";
37196
- }
37617
+ function preferredSourceTabFor(previewable) {
37618
+ if (!previewable) return "code";
37197
37619
  const routeTab = STATE.route.screen === "file" && STATE.route.view === "blob" && STATE.route.preview ? "preview" : "code";
37198
- const tab = PREFERRED_SOURCE_TAB || routeTab;
37199
- PREFERRED_SOURCE_TAB = null;
37200
- return tab;
37620
+ return PREFERRED_SOURCE_TAB || routeTab;
37201
37621
  }
37202
37622
  let SOURCE_REQ_SEQ = 0;
37203
37623
  let ACTIVE_SOURCE_LOAD = null;
@@ -37449,7 +37869,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37449
37869
  if (signal?.aborted) return false;
37450
37870
  const previewable = isPreviewableSource(target.path);
37451
37871
  const previewKind = sourcePreviewKind(target.path);
37452
- const initialSourceTab = consumePreferredSourceTab(previewable);
37872
+ const initialSourceTab = preferredSourceTabFor(previewable);
37453
37873
  const tabsHost = card.querySelector(".gdp-file-detail-tabs");
37454
37874
  if (usesVirtualSource) {
37455
37875
  const virtualCode = renderVirtualSource(
@@ -37471,7 +37891,11 @@ code-viewer annotate add-db --db app.db --tab query \\
37471
37891
  tabsHost.hidden = false;
37472
37892
  tabsHost.replaceChildren(tabs3);
37473
37893
  }
37474
- let preview = previewKind === "html" ? renderHtmlPreview(target, textValue) : await (deps.renderMarkdownPreview ?? renderMarkdownPreview)(
37894
+ let preview = previewKind === "html" ? renderHtmlPreview(target, textValue) : previewKind === "csv" || previewKind === "tsv" ? renderDelimitedPreview(
37895
+ textValue,
37896
+ previewKind,
37897
+ () => delimitedPreviewText(getLanguage(), previewKind)
37898
+ ) : await (deps.renderMarkdownPreview ?? renderMarkdownPreview)(
37475
37899
  textValue,
37476
37900
  target,
37477
37901
  {
@@ -37484,7 +37908,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37484
37908
  preview.dataset.sourcePane = "preview";
37485
37909
  let previewHighlightScheduled = false;
37486
37910
  const ensurePreviewHighlight = () => {
37487
- if (previewKind === "html" || !STATE.syntaxHighlight || previewHighlightScheduled)
37911
+ if (previewKind !== "markdown" || !STATE.syntaxHighlight || previewHighlightScheduled)
37488
37912
  return;
37489
37913
  previewHighlightScheduled = true;
37490
37914
  scheduleMarkdownPreviewHighlight(
@@ -37591,7 +38015,11 @@ code-viewer annotate add-db --db app.db --tab query \\
37591
38015
  tabsHost.replaceChildren(tabs);
37592
38016
  }
37593
38017
  if (previewable) {
37594
- let preview = previewKind === "html" ? renderHtmlPreview(target, textValue) : await (deps.renderMarkdownPreview ?? renderMarkdownPreview)(
38018
+ let preview = previewKind === "html" ? renderHtmlPreview(target, textValue) : previewKind === "csv" || previewKind === "tsv" ? renderDelimitedPreview(
38019
+ textValue,
38020
+ previewKind,
38021
+ () => delimitedPreviewText(getLanguage(), previewKind)
38022
+ ) : await (deps.renderMarkdownPreview ?? renderMarkdownPreview)(
37595
38023
  textValue,
37596
38024
  target,
37597
38025
  {
@@ -37604,7 +38032,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37604
38032
  preview.dataset.sourcePane = "preview";
37605
38033
  let previewHighlightScheduled = false;
37606
38034
  const ensurePreviewHighlight = () => {
37607
- if (previewKind === "html" || !STATE.syntaxHighlight || previewHighlightScheduled)
38035
+ if (previewKind !== "markdown" || !STATE.syntaxHighlight || previewHighlightScheduled)
37608
38036
  return;
37609
38037
  previewHighlightScheduled = true;
37610
38038
  scheduleMarkdownPreviewHighlight(
@@ -38770,6 +39198,11 @@ code-viewer annotate add-db --db app.db --tab query \\
38770
39198
  function handleVirtualSourcePagingKeydown(e2) {
38771
39199
  handleVirtualSourcePagingKey(e2, e2.target);
38772
39200
  }
39201
+ function localize() {
39202
+ document.querySelectorAll(".gdp-csv-preview").forEach((preview) => {
39203
+ preview.localize();
39204
+ });
39205
+ }
38773
39206
  return {
38774
39207
  renderStandaloneSource,
38775
39208
  applySourceRouteToShell,
@@ -38804,7 +39237,8 @@ code-viewer annotate add-db --db app.db --tab query \\
38804
39237
  loadSourceShikiHighlighter,
38805
39238
  sourceShikiLines,
38806
39239
  shouldVirtualizeSource,
38807
- inferLang
39240
+ inferLang,
39241
+ localize
38808
39242
  };
38809
39243
  }
38810
39244
 
@@ -42833,7 +43267,8 @@ ${formatErrorDetail(error2)}`);
42833
43267
  setViewFileButtonState: (button, sourceMode) => DIFF_VIEW.setViewFileButtonState(button, sourceMode),
42834
43268
  scrollMainPanel,
42835
43269
  focusMainSurface,
42836
- isPaletteOpen: () => SEARCH_PALETTE.isPaletteOpen()
43270
+ isPaletteOpen: () => SEARCH_PALETTE.isPaletteOpen(),
43271
+ getLanguage: () => STATE.language
42837
43272
  });
42838
43273
  const {
42839
43274
  renderStandaloneSource,
@@ -43793,6 +44228,7 @@ ${formatErrorDetail(error2)}`);
43793
44228
  relocalizeHistory?.();
43794
44229
  relocalizeJournal?.();
43795
44230
  relocalizeViewerSettings?.();
44231
+ SOURCE_VIEW.localize();
43796
44232
  setElementText(".annotation-panel-head strong", text3.annotations.title);
43797
44233
  const followLabel = document.querySelector(
43798
44234
  ".annotation-follow-label"
package/web/style.css CHANGED
@@ -4488,6 +4488,243 @@ table.d2h-diff-table td.d2h-code-side-linenumber {
4488
4488
  font-size: var(--markdown-font-size);
4489
4489
  line-height: 1.75;
4490
4490
  }
4491
+ .gdp-csv-preview {
4492
+ width: 100%;
4493
+ padding: 20px 24px 40px;
4494
+ overflow: auto;
4495
+ background: var(--bg);
4496
+ }
4497
+ .gdp-csv-shell {
4498
+ width: max-content;
4499
+ min-width: min(100%, 880px);
4500
+ border: 1px solid var(--border);
4501
+ border-radius: 8px;
4502
+ background: var(--bg);
4503
+ box-shadow: var(--shadow-sm);
4504
+ }
4505
+ .gdp-csv-toolbar {
4506
+ display: flex;
4507
+ align-items: center;
4508
+ gap: 8px;
4509
+ min-height: var(--ui-control-md);
4510
+ padding: 8px 10px;
4511
+ border-bottom: 1px solid var(--border-strong);
4512
+ border-radius: 7px 7px 0 0;
4513
+ background: var(--bg-soft);
4514
+ }
4515
+ .gdp-csv-search-icon {
4516
+ display: inline-flex;
4517
+ flex: 0 0 16px;
4518
+ align-items: center;
4519
+ justify-content: center;
4520
+ width: 16px;
4521
+ height: 16px;
4522
+ color: var(--fg-muted);
4523
+ }
4524
+ .gdp-csv-search-icon svg {
4525
+ display: block;
4526
+ width: 16px;
4527
+ height: 16px;
4528
+ }
4529
+ .gdp-csv-search {
4530
+ width: min(360px, 34vw);
4531
+ min-width: 180px;
4532
+ height: var(--ui-control-md);
4533
+ padding: 0 10px;
4534
+ border: 1px solid var(--border);
4535
+ border-radius: 6px;
4536
+ background: var(--bg);
4537
+ color: var(--fg);
4538
+ font: inherit;
4539
+ font-size: var(--ui-font-md);
4540
+ }
4541
+ .gdp-csv-search:focus,
4542
+ .gdp-csv-column-filter:focus {
4543
+ outline: 2px solid var(--accent);
4544
+ outline-offset: -1px;
4545
+ border-color: var(--accent);
4546
+ }
4547
+ .gdp-csv-result-count {
4548
+ flex: 0 0 14ch;
4549
+ margin-left: auto;
4550
+ color: var(--fg-muted);
4551
+ font-size: var(--ui-font-sm);
4552
+ font-variant-numeric: tabular-nums;
4553
+ text-align: right;
4554
+ white-space: nowrap;
4555
+ }
4556
+ .gdp-csv-reset {
4557
+ flex: 0 0 88px;
4558
+ width: 88px;
4559
+ height: var(--ui-control-md);
4560
+ padding: 0 10px;
4561
+ border: 1px solid var(--border);
4562
+ border-radius: 6px;
4563
+ background: var(--bg);
4564
+ color: var(--fg-muted);
4565
+ font: inherit;
4566
+ font-size: var(--ui-font-sm);
4567
+ font-weight: 600;
4568
+ cursor: pointer;
4569
+ }
4570
+ .gdp-csv-reset:hover:not(:disabled) {
4571
+ border-color: var(--accent);
4572
+ background: var(--accent-subtle);
4573
+ color: var(--fg);
4574
+ }
4575
+ .gdp-csv-reset:focus-visible {
4576
+ outline: 2px solid var(--accent);
4577
+ outline-offset: 1px;
4578
+ }
4579
+ .gdp-csv-reset:disabled {
4580
+ color: var(--fg-subtle);
4581
+ cursor: default;
4582
+ opacity: 0.58;
4583
+ }
4584
+ .gdp-csv-table {
4585
+ width: 100%;
4586
+ min-width: 100%;
4587
+ border-collapse: separate;
4588
+ border-spacing: 0;
4589
+ color: var(--fg);
4590
+ background: var(--bg);
4591
+ font-family: "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
4592
+ font-size: var(--code-font-size);
4593
+ line-height: var(--code-line-height);
4594
+ }
4595
+ .gdp-csv-table th,
4596
+ .gdp-csv-table td {
4597
+ min-width: 11ch;
4598
+ max-width: 52ch;
4599
+ padding: 8px 14px;
4600
+ border-right: 1px solid var(--border);
4601
+ border-bottom: 1px solid var(--border);
4602
+ overflow-wrap: anywhere;
4603
+ text-align: left;
4604
+ transition: background-color 0.12s ease;
4605
+ vertical-align: top;
4606
+ white-space: pre-wrap;
4607
+ }
4608
+ .gdp-csv-table tr > :last-child {
4609
+ border-right: 0;
4610
+ }
4611
+ .gdp-csv-table tbody tr:last-child > * {
4612
+ border-bottom: 0;
4613
+ }
4614
+ .gdp-csv-table thead {
4615
+ position: sticky;
4616
+ top: 0;
4617
+ z-index: 2;
4618
+ }
4619
+ .gdp-csv-table thead th {
4620
+ background: var(--bg-mute);
4621
+ font-weight: 700;
4622
+ }
4623
+ .gdp-csv-table thead tr:first-child > th:not(.gdp-csv-row-number) {
4624
+ padding: 0;
4625
+ }
4626
+ .gdp-csv-sort-button {
4627
+ display: grid;
4628
+ grid-template-columns: minmax(0, 1fr) 16px;
4629
+ align-items: center;
4630
+ width: 100%;
4631
+ min-height: var(--ui-control-md);
4632
+ padding: 6px 14px;
4633
+ border: 0;
4634
+ background: transparent;
4635
+ color: var(--fg);
4636
+ font: inherit;
4637
+ font-weight: 700;
4638
+ text-align: left;
4639
+ cursor: pointer;
4640
+ }
4641
+ .gdp-csv-sort-button:hover {
4642
+ background: var(--accent-subtle);
4643
+ }
4644
+ .gdp-csv-sort-button:focus-visible {
4645
+ outline: 2px solid var(--accent);
4646
+ outline-offset: -2px;
4647
+ }
4648
+ .gdp-csv-sort-button::after {
4649
+ display: block;
4650
+ width: 16px;
4651
+ color: var(--accent);
4652
+ content: "";
4653
+ font-size: var(--ui-font-sm);
4654
+ text-align: center;
4655
+ }
4656
+ .gdp-csv-sort-button[data-sort-direction="asc"]::after {
4657
+ content: "↑";
4658
+ }
4659
+ .gdp-csv-sort-button[data-sort-direction="desc"]::after {
4660
+ content: "↓";
4661
+ }
4662
+ .gdp-csv-filter-row th {
4663
+ padding: 5px 8px;
4664
+ border-bottom-color: var(--border-strong);
4665
+ background: var(--bg-soft);
4666
+ }
4667
+ .gdp-csv-column-filter {
4668
+ width: 100%;
4669
+ min-width: 8ch;
4670
+ height: var(--ui-control-sm);
4671
+ padding: 0 7px;
4672
+ border: 1px solid var(--border);
4673
+ border-radius: 4px;
4674
+ background: var(--bg);
4675
+ color: var(--fg);
4676
+ font: inherit;
4677
+ font-size: var(--ui-font-sm);
4678
+ font-weight: 400;
4679
+ }
4680
+ .gdp-csv-column-filter::placeholder,
4681
+ .gdp-csv-search::placeholder {
4682
+ color: var(--fg-subtle);
4683
+ }
4684
+ .gdp-csv-table tbody tr:nth-child(2n) td {
4685
+ background: color-mix(in srgb, var(--bg-soft) 76%, var(--bg));
4686
+ }
4687
+ .gdp-csv-table tbody tr:hover td {
4688
+ background: var(--accent-subtle);
4689
+ }
4690
+ .gdp-csv-table .gdp-csv-row-number {
4691
+ position: sticky;
4692
+ left: 0;
4693
+ z-index: 1;
4694
+ width: 44px;
4695
+ min-width: 44px;
4696
+ max-width: 44px;
4697
+ background: var(--bg-soft);
4698
+ color: var(--fg-subtle);
4699
+ border-right-color: var(--border-strong);
4700
+ font-weight: 400;
4701
+ text-align: right;
4702
+ user-select: none;
4703
+ }
4704
+ .gdp-csv-table tbody tr:hover .gdp-csv-row-number {
4705
+ background: color-mix(in srgb, var(--accent-subtle) 72%, var(--bg-soft));
4706
+ color: var(--fg);
4707
+ }
4708
+ .gdp-csv-table thead .gdp-csv-row-number {
4709
+ z-index: 3;
4710
+ background: var(--bg-mute);
4711
+ }
4712
+ .gdp-csv-table .gdp-csv-filter-row .gdp-csv-row-number {
4713
+ background: var(--bg-soft);
4714
+ }
4715
+ .gdp-csv-empty-row td {
4716
+ padding: 32px 20px;
4717
+ background: var(--bg);
4718
+ color: var(--fg-muted);
4719
+ font-family: inherit;
4720
+ text-align: center;
4721
+ }
4722
+ .gdp-csv-table tbody tr:last-child > :first-child {
4723
+ border-bottom-left-radius: 7px;
4724
+ }
4725
+ .gdp-csv-table tbody tr:last-child > :last-child {
4726
+ border-bottom-right-radius: 7px;
4727
+ }
4491
4728
  .gdp-html-preview {
4492
4729
  width: 100%;
4493
4730
  min-height: calc(var(--content-h) - var(--file-detail-head-h));