Add double-quote aware CSV parsing and column copy button
- CsvParser._parseRow() now handles quoted fields: commas inside double quotes are treated as literal, and "" is an escaped quote. - Empty lines are filtered out during parsing. - Each header cell gets a copy button (clipboard icon) that appears on hover, right-aligned. Clicking it copies the entire column (header + all data rows) joined by newlines to the clipboard. - Styling: button is absolutely positioned, fades in on header hover, uses Obsidian CSS variables for theming. Co-authored-by: Shelley <shelley@exe.dev>
This commit is contained in:
parent
26de64d667
commit
4ca6f224c9
83
main.js
83
main.js
|
|
@ -2,8 +2,56 @@ const { Plugin } = require('obsidian');
|
|||
|
||||
class CsvParser {
|
||||
constructor(lines) {
|
||||
let rows = lines.split("\n");
|
||||
this._cells = rows.map((l) => l.split(","));
|
||||
let rows = lines.split("\n").filter((l) => l.length > 0);
|
||||
this._cells = rows.map((l) => this._parseRow(l));
|
||||
}
|
||||
|
||||
_parseRow(line) {
|
||||
const cells = [];
|
||||
let i = 0;
|
||||
while (i <= line.length) {
|
||||
if (i === line.length) {
|
||||
// trailing comma produced an empty final field
|
||||
break;
|
||||
}
|
||||
if (line[i] === '"') {
|
||||
// quoted field
|
||||
i++; // skip opening quote
|
||||
let value = '';
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"') {
|
||||
if (i + 1 < line.length && line[i + 1] === '"') {
|
||||
// escaped double quote
|
||||
value += '"';
|
||||
i += 2;
|
||||
} else {
|
||||
// closing quote
|
||||
i++; // skip closing quote
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
value += line[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
cells.push(value);
|
||||
// skip comma after closing quote
|
||||
if (i < line.length && line[i] === ',') {
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
// unquoted field
|
||||
let end = line.indexOf(',', i);
|
||||
if (end === -1) {
|
||||
cells.push(line.substring(i));
|
||||
i = line.length;
|
||||
} else {
|
||||
cells.push(line.substring(i, end));
|
||||
i = end + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
cells() {
|
||||
|
|
@ -11,6 +59,8 @@ class CsvParser {
|
|||
}
|
||||
}
|
||||
|
||||
const COPY_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
||||
|
||||
class CsvPlugin extends Plugin {
|
||||
async onload() {
|
||||
this.addStyle();
|
||||
|
|
@ -22,9 +72,11 @@ class CsvPlugin extends Plugin {
|
|||
table.addClass('csv-table');
|
||||
|
||||
let cp = new CsvParser(source);
|
||||
let allCells = cp.cells();
|
||||
let twrap = null;
|
||||
|
||||
for (let row of cp.cells()) {
|
||||
for (let rowIdx = 0; rowIdx < allCells.length; rowIdx++) {
|
||||
let row = allCells[rowIdx];
|
||||
if (twrap == null) {
|
||||
twrap = table.createEl('thead');
|
||||
} else if (twrap.nodeName == 'THEAD') {
|
||||
|
|
@ -32,10 +84,33 @@ class CsvPlugin extends Plugin {
|
|||
}
|
||||
|
||||
let rowElem = twrap.createEl('tr');
|
||||
for (let col of row) {
|
||||
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
||||
let col = row[colIdx];
|
||||
if (rowIdx === 0) {
|
||||
// Header cell with copy button
|
||||
let td = rowElem.createEl('td');
|
||||
td.addClass('csv-table-header');
|
||||
td.createEl('span', { text: col });
|
||||
|
||||
let btn = td.createEl('button');
|
||||
btn.addClass('csv-copy-btn');
|
||||
btn.setAttribute('aria-label', 'Copy column');
|
||||
btn.innerHTML = COPY_ICON_SVG;
|
||||
|
||||
const ci = colIdx;
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let columnText = allCells
|
||||
.map((r) => (ci < r.length ? r[ci] : ''))
|
||||
.join('\n');
|
||||
navigator.clipboard.writeText(columnText);
|
||||
});
|
||||
} else {
|
||||
rowElem.createEl('td', { text: col });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
30
styles.css
30
styles.css
|
|
@ -15,3 +15,33 @@ div.csv-table-wrapper {
|
|||
border: 1px solid var(--background-modifier-border);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.csv-table-header {
|
||||
position: relative;
|
||||
padding-right: 28px !important;
|
||||
}
|
||||
|
||||
.csv-copy-btn {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
line-height: 0;
|
||||
color: var(--text-muted);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.csv-table-header:hover .csv-copy-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.csv-copy-btn:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue