This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/packages/csv-table/js/script.js
Jeffrey Smith c65674965f
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Failing after 2m26s
CI/CD / test-sqlite (pull_request) Successful in 2m41s
CI/CD / build-and-deploy (pull_request) Has been skipped
Feat v0.2.9 builtin extension retirement
Stop auto-seeding 6 chat-centric extensions on startup. Move them to
standard packages with js/ layout and "requires": ["chat"] metadata.
Delete SeedBuiltinPackages, seed tests, Dockerfile COPY, and
extensions/builtin/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:45:45 +00:00

255 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==========================================
// CSV Table Viewer — Browser Extension
// ==========================================
// Renders ```csv and ```tsv code blocks as sortable HTML tables.
// No external dependencies.
// ==========================================
Extensions.register({
id: 'csv-table',
async init(ctx) {
const self = this;
// ── Inject styles ──
this._injectStyles();
ctx.renderers.register('csv-block', {
type: 'block',
priority: 10,
match(lang) {
const l = (lang || '').toLowerCase();
return l === 'csv' || l === 'tsv';
},
render(lang, code, container) {
const delimiter = lang.toLowerCase() === 'tsv' ? '\t' : ',';
const rows = self._parseCSV(code.trim(), delimiter);
if (rows.length === 0) {
container.innerHTML = '<div class="csv-empty">No data</div>';
return;
}
const headers = rows[0];
const data = rows.slice(1);
const tableId = 'csv-' + Math.random().toString(36).slice(2, 9);
container.innerHTML = `
<div class="csv-table-block ext-rendered" data-csv-id="${tableId}">
<div class="csv-toolbar">
<span class="csv-info">${data.length} row${data.length !== 1 ? 's' : ''} × ${headers.length} col${headers.length !== 1 ? 's' : ''}</span>
<button class="csv-copy-btn" title="Copy as CSV">📋 Copy</button>
</div>
<div class="csv-table-scroll">
<table class="csv-table">
<thead>
<tr>${headers.map((h, i) => `<th data-col="${i}" title="Click to sort">${self._escapeHtml(h)}<span class="csv-sort-icon"></span></th>`).join('')}</tr>
</thead>
<tbody>
${data.map(row => `<tr>${row.map(cell => `<td>${self._escapeHtml(cell)}</td>`).join('')}</tr>`).join('')}
</tbody>
</table>
</div>
<details class="csv-source">
<summary>📄 View raw</summary>
<pre><code>${self._escapeHtml(code.trim())}</code></pre>
</details>
</div>
`;
// Attach sort handlers + copy
self._attachHandlers(container, tableId, code.trim());
}
});
},
_attachHandlers(container, tableId, rawCSV) {
const block = container.querySelector(`[data-csv-id="${tableId}"]`);
if (!block) return;
// ── Column sorting ──
const ths = block.querySelectorAll('th[data-col]');
let sortCol = -1;
let sortAsc = true;
ths.forEach(th => {
th.style.cursor = 'pointer';
th.addEventListener('click', () => {
const col = parseInt(th.dataset.col, 10);
if (sortCol === col) {
sortAsc = !sortAsc;
} else {
sortCol = col;
sortAsc = true;
}
const tbody = block.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const cellA = (a.children[col]?.textContent || '').trim();
const cellB = (b.children[col]?.textContent || '').trim();
const numA = parseFloat(cellA);
const numB = parseFloat(cellB);
// Numeric sort if both are numbers
if (!isNaN(numA) && !isNaN(numB)) {
return sortAsc ? numA - numB : numB - numA;
}
// String sort
const cmp = cellA.localeCompare(cellB, undefined, { numeric: true, sensitivity: 'base' });
return sortAsc ? cmp : -cmp;
});
rows.forEach(row => tbody.appendChild(row));
// Update sort icons
ths.forEach(h => {
const icon = h.querySelector('.csv-sort-icon');
if (icon) {
const hCol = parseInt(h.dataset.col, 10);
icon.textContent = hCol === sortCol ? (sortAsc ? ' ▲' : ' ▼') : '';
}
});
});
});
// ── Copy button ──
const copyBtn = block.querySelector('.csv-copy-btn');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(rawCSV).then(() => {
copyBtn.textContent = '✓ Copied';
setTimeout(() => { copyBtn.textContent = '📋 Copy'; }, 1500);
}).catch(() => {
copyBtn.textContent = '✗ Failed';
setTimeout(() => { copyBtn.textContent = '📋 Copy'; }, 1500);
});
});
}
},
/**
* Simple CSV parser that handles quoted fields.
*/
_parseCSV(text, delimiter) {
const rows = [];
let row = [];
let field = '';
let inQuotes = false;
let i = 0;
while (i < text.length) {
const ch = text[i];
if (inQuotes) {
if (ch === '"') {
// Peek ahead: escaped quote or end of quoted field
if (i + 1 < text.length && text[i + 1] === '"') {
field += '"';
i += 2;
} else {
inQuotes = false;
i++;
}
} else {
field += ch;
i++;
}
} else {
if (ch === '"' && field === '') {
inQuotes = true;
i++;
} else if (ch === delimiter) {
row.push(field.trim());
field = '';
i++;
} else if (ch === '\n' || (ch === '\r' && text[i + 1] === '\n')) {
row.push(field.trim());
if (row.some(cell => cell !== '')) rows.push(row);
row = [];
field = '';
i += (ch === '\r') ? 2 : 1;
} else {
field += ch;
i++;
}
}
}
// Last field/row
row.push(field.trim());
if (row.some(cell => cell !== '')) rows.push(row);
// Normalize: pad short rows to header length
if (rows.length > 0) {
const maxCols = Math.max(...rows.map(r => r.length));
rows.forEach(r => {
while (r.length < maxCols) r.push('');
});
}
return rows;
},
_escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
},
_injectStyles() {
if (document.getElementById('ext-style-csv-table')) return;
const style = document.createElement('style');
style.id = 'ext-style-csv-table';
style.textContent = `
.csv-table-block {
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 8px; overflow: hidden; margin: 12px 0;
}
.csv-toolbar {
display: flex; align-items: center; gap: 8px;
padding: 6px 12px; border-bottom: 1px solid var(--border);
font-size: 12px; color: var(--text-3);
}
.csv-info { flex: 1; }
.csv-copy-btn {
background: none; border: 1px solid var(--border); border-radius: 4px;
padding: 2px 8px; font-size: 12px; cursor: pointer; color: var(--text-3);
}
.csv-copy-btn:hover { color: var(--text-2); border-color: var(--text-3); }
.csv-table-scroll { overflow-x: auto; }
.csv-table {
width: 100%; border-collapse: collapse; font-size: 13px;
font-family: var(--mono);
}
.csv-table th {
background: var(--bg-3); color: var(--text-2); font-weight: 600;
padding: 6px 12px; text-align: left; white-space: nowrap;
border-bottom: 2px solid var(--border); user-select: none;
}
.csv-table th:hover { color: var(--text-1); }
.csv-sort-icon { font-size: 11px; opacity: 0.7; }
.csv-table td {
padding: 4px 12px; border-bottom: 1px solid var(--border);
white-space: nowrap; max-width: 300px; overflow: hidden; text-overflow: ellipsis;
}
.csv-table tr:hover td { background: var(--bg-3); }
.csv-source, .csv-empty { border-top: 1px solid var(--border); font-size: 12px; }
.csv-source summary {
padding: 6px 12px; cursor: pointer; color: var(--text-3); user-select: none;
}
.csv-source summary:hover { color: var(--text-2); }
.csv-source pre {
margin: 0; border-radius: 0; border: none;
max-height: 200px; overflow: auto;
}
.csv-empty { padding: 16px; text-align: center; color: var(--text-3); }`;
document.head.appendChild(style);
},
destroy() {
document.getElementById('ext-style-csv-table')?.remove();
}
});