mirror of
https://github.com/vitovt/obsidian-csv-modern-codeblock.git
synced 2026-07-22 14:30:23 +00:00
939 lines
26 KiB
JavaScript
939 lines
26 KiB
JavaScript
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
*/
|
|
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// main.ts
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
default: () => CsvCodeBlockPlugin
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian = require("obsidian");
|
|
|
|
class CsvParseError extends Error {
|
|
constructor(message, row, field, line) {
|
|
super(message);
|
|
this.name = "CsvParseError";
|
|
this.row = row;
|
|
this.field = field;
|
|
this.line = line;
|
|
}
|
|
}
|
|
|
|
class CsvParser {
|
|
constructor(source, delimiter = ',') {
|
|
this.source = source;
|
|
this.delimiter = delimiter;
|
|
}
|
|
|
|
forEachRow(onRow) {
|
|
const source = this.source;
|
|
const delimiter = this.delimiter;
|
|
const STATE_FIELD_START = 0;
|
|
const STATE_IN_UNQUOTED = 1;
|
|
const STATE_IN_QUOTED = 2;
|
|
const STATE_AFTER_QUOTE = 3;
|
|
let currentRow = [];
|
|
let currentFieldParts = [];
|
|
let fieldStart = 0;
|
|
let state = STATE_FIELD_START;
|
|
let rowNumber = 1;
|
|
let fieldNumber = 1;
|
|
let lineNumber = 1;
|
|
let lastTokenWasDelimiter = false;
|
|
|
|
const appendPendingFieldText = (end) => {
|
|
if (fieldStart < end) {
|
|
currentFieldParts.push(source.slice(fieldStart, end));
|
|
}
|
|
};
|
|
|
|
const throwParseError = (message) => {
|
|
throw new CsvParseError(message, rowNumber, fieldNumber, lineNumber);
|
|
};
|
|
|
|
const pushField = () => {
|
|
if (currentFieldParts.length === 0) {
|
|
currentRow.push('');
|
|
} else if (currentFieldParts.length === 1) {
|
|
currentRow.push(currentFieldParts[0]);
|
|
} else {
|
|
currentRow.push(currentFieldParts.join(''));
|
|
}
|
|
currentFieldParts = [];
|
|
fieldNumber++;
|
|
lastTokenWasDelimiter = false;
|
|
};
|
|
|
|
const pushRow = () => {
|
|
if (state === STATE_FIELD_START && lastTokenWasDelimiter) {
|
|
pushField();
|
|
} else if (state === STATE_IN_UNQUOTED || state === STATE_AFTER_QUOTE) {
|
|
pushField();
|
|
} else if (state === STATE_FIELD_START && currentRow.length === 0) {
|
|
pushField();
|
|
}
|
|
if (currentRow.length > 1 || currentRow[0] !== '') {
|
|
onRow(currentRow, rowNumber);
|
|
}
|
|
currentRow = [];
|
|
rowNumber++;
|
|
fieldNumber = 1;
|
|
state = STATE_FIELD_START;
|
|
lastTokenWasDelimiter = false;
|
|
};
|
|
|
|
for (let i = 0; i < source.length; i++) {
|
|
const char = source[i];
|
|
const nextChar = source[i + 1];
|
|
const isNewLine = char === "\n" || char === "\r";
|
|
|
|
if (state === STATE_FIELD_START) {
|
|
if (char === delimiter) {
|
|
pushField();
|
|
fieldStart = i + 1;
|
|
lastTokenWasDelimiter = true;
|
|
} else if (isNewLine) {
|
|
pushRow();
|
|
if (char === "\r" && nextChar === "\n") {
|
|
i++;
|
|
}
|
|
fieldStart = i + 1;
|
|
lineNumber++;
|
|
} else if (char === '"') {
|
|
state = STATE_IN_QUOTED;
|
|
fieldStart = i + 1;
|
|
} else {
|
|
state = STATE_IN_UNQUOTED;
|
|
fieldStart = i;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (state === STATE_IN_UNQUOTED) {
|
|
if (char === delimiter) {
|
|
appendPendingFieldText(i);
|
|
pushField();
|
|
fieldStart = i + 1;
|
|
state = STATE_FIELD_START;
|
|
lastTokenWasDelimiter = true;
|
|
} else if (isNewLine) {
|
|
appendPendingFieldText(i);
|
|
pushRow();
|
|
if (char === "\r" && nextChar === "\n") {
|
|
i++;
|
|
}
|
|
fieldStart = i + 1;
|
|
lineNumber++;
|
|
} else if (char === '"') {
|
|
throwParseError("Unexpected quote inside an unquoted field");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (state === STATE_IN_QUOTED) {
|
|
if (char === '"') {
|
|
appendPendingFieldText(i);
|
|
if (nextChar === '"') {
|
|
currentFieldParts.push('"');
|
|
i++;
|
|
fieldStart = i + 1;
|
|
} else {
|
|
state = STATE_AFTER_QUOTE;
|
|
fieldStart = i + 1;
|
|
}
|
|
} else if (char === "\r") {
|
|
if (nextChar === "\n") {
|
|
i++;
|
|
}
|
|
lineNumber++;
|
|
} else if (char === "\n") {
|
|
lineNumber++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (char === delimiter) {
|
|
pushField();
|
|
fieldStart = i + 1;
|
|
state = STATE_FIELD_START;
|
|
lastTokenWasDelimiter = true;
|
|
} else if (isNewLine) {
|
|
pushRow();
|
|
if (char === "\r" && nextChar === "\n") {
|
|
i++;
|
|
}
|
|
fieldStart = i + 1;
|
|
lineNumber++;
|
|
} else if (char === " " || char === "\t") {
|
|
fieldStart = i + 1;
|
|
} else {
|
|
throwParseError(`Unexpected character "${char}" after a closing quote`);
|
|
}
|
|
}
|
|
|
|
if (state === STATE_IN_QUOTED) {
|
|
throwParseError("Unclosed quoted field");
|
|
}
|
|
|
|
if (state === STATE_IN_UNQUOTED) {
|
|
appendPendingFieldText(source.length);
|
|
pushField();
|
|
state = STATE_FIELD_START;
|
|
} else if (state === STATE_AFTER_QUOTE) {
|
|
pushField();
|
|
state = STATE_FIELD_START;
|
|
} else if (state === STATE_FIELD_START && lastTokenWasDelimiter) {
|
|
pushField();
|
|
}
|
|
|
|
if (currentRow.length > 0) {
|
|
pushRow();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function detectCsvDelimiter(source) {
|
|
const candidates = [",", ";"];
|
|
const scores = new Map(candidates.map((delimiter) => [delimiter, 0]));
|
|
const matches = new Map(candidates.map((delimiter) => [delimiter, 0]));
|
|
let inQuotes = false;
|
|
let rowCounts = new Map(candidates.map((delimiter) => [delimiter, 0]));
|
|
let sampledRows = 0;
|
|
let sawRowData = false;
|
|
|
|
const commitRow = () => {
|
|
if (!sawRowData) {
|
|
rowCounts = new Map(candidates.map((delimiter) => [delimiter, 0]));
|
|
return;
|
|
}
|
|
sampledRows++;
|
|
for (const delimiter of candidates) {
|
|
const count = rowCounts.get(delimiter);
|
|
if (count > 0) {
|
|
scores.set(delimiter, scores.get(delimiter) + count);
|
|
matches.set(delimiter, matches.get(delimiter) + 1);
|
|
}
|
|
}
|
|
rowCounts = new Map(candidates.map((delimiter) => [delimiter, 0]));
|
|
sawRowData = false;
|
|
};
|
|
|
|
for (let i = 0; i < source.length && sampledRows < 10; i++) {
|
|
const char = source[i];
|
|
const nextChar = source[i + 1];
|
|
|
|
if (char === '"') {
|
|
if (inQuotes && nextChar === '"') {
|
|
i++;
|
|
} else {
|
|
inQuotes = !inQuotes;
|
|
}
|
|
sawRowData = true;
|
|
continue;
|
|
}
|
|
|
|
if (!inQuotes && (char === "\n" || char === "\r")) {
|
|
if (char === "\r" && nextChar === "\n") {
|
|
i++;
|
|
}
|
|
commitRow();
|
|
continue;
|
|
}
|
|
|
|
if (!inQuotes && scores.has(char)) {
|
|
rowCounts.set(char, rowCounts.get(char) + 1);
|
|
sawRowData = true;
|
|
} else if (char.trim().length > 0) {
|
|
sawRowData = true;
|
|
}
|
|
}
|
|
|
|
commitRow();
|
|
|
|
if (scores.get(";") > scores.get(",") && matches.get(";") >= matches.get(",")) {
|
|
return ";";
|
|
}
|
|
return ",";
|
|
}
|
|
|
|
function createCellContent(doc, cell, text) {
|
|
const trimmed = text.trim();
|
|
if (trimmed.length > 0) {
|
|
try {
|
|
const url = new URL(trimmed);
|
|
if (url.protocol === "http:" || url.protocol === "https:" || url.protocol === "mailto:") {
|
|
const link = doc.createElement("a");
|
|
link.className = "csv-codeblock__link";
|
|
link.href = trimmed;
|
|
link.textContent = text;
|
|
if (url.protocol !== "mailto:") {
|
|
link.target = "_blank";
|
|
link.rel = "noopener noreferrer";
|
|
}
|
|
cell.appendChild(link);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
}
|
|
}
|
|
cell.textContent = text;
|
|
}
|
|
|
|
function compareCellValues(leftValue, rightValue) {
|
|
const leftText = leftValue.trim();
|
|
const rightText = rightValue.trim();
|
|
|
|
if (leftText.length === 0 && rightText.length === 0) {
|
|
return 0;
|
|
}
|
|
if (leftText.length === 0) {
|
|
return 1;
|
|
}
|
|
if (rightText.length === 0) {
|
|
return -1;
|
|
}
|
|
|
|
const leftNumber = Number(leftText);
|
|
const rightNumber = Number(rightText);
|
|
const leftIsNumber = Number.isFinite(leftNumber);
|
|
const rightIsNumber = Number.isFinite(rightNumber);
|
|
|
|
if (leftIsNumber && rightIsNumber) {
|
|
return leftNumber - rightNumber;
|
|
}
|
|
|
|
return leftText.localeCompare(rightText, void 0, {
|
|
numeric: true,
|
|
sensitivity: "base"
|
|
});
|
|
}
|
|
|
|
function updateSortIndicators(headers, activeColumnIndex, direction, enabled) {
|
|
for (let i = 0; i < headers.length; i++) {
|
|
const header = headers[i];
|
|
const isActive = enabled && i === activeColumnIndex;
|
|
header.cell.setAttribute("aria-sort", isActive ? direction === 1 ? "ascending" : "descending" : "none");
|
|
header.indicator.textContent = isActive ? direction === 1 ? "▲" : "▼" : "";
|
|
header.button.className = enabled ? "csv-codeblock__sort-button" : "csv-codeblock__sort-button csv-codeblock__sort-button--disabled";
|
|
header.button.setAttribute("aria-disabled", enabled ? "false" : "true");
|
|
}
|
|
}
|
|
|
|
function createSortController(body, headers, rows) {
|
|
let activeColumnIndex = -1;
|
|
let direction = 1;
|
|
let enabled = false;
|
|
|
|
const resetOrder = () => {
|
|
const sortedRows = rows.slice().sort((left, right) => left.originalIndex - right.originalIndex);
|
|
for (const row of sortedRows) {
|
|
body.appendChild(row.element);
|
|
}
|
|
};
|
|
|
|
const applySort = () => {
|
|
if (!enabled || activeColumnIndex < 0) {
|
|
return;
|
|
}
|
|
const sortedRows = rows.slice().sort((left, right) => {
|
|
const result = compareCellValues(
|
|
left.values[activeColumnIndex] || "",
|
|
right.values[activeColumnIndex] || ""
|
|
);
|
|
if (result !== 0) {
|
|
return result * direction;
|
|
}
|
|
return left.originalIndex - right.originalIndex;
|
|
});
|
|
|
|
for (const row of sortedRows) {
|
|
body.appendChild(row.element);
|
|
}
|
|
};
|
|
|
|
for (let i = 0; i < headers.length; i++) {
|
|
const header = headers[i];
|
|
header.button.addEventListener("click", () => {
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
if (activeColumnIndex === i) {
|
|
direction = direction === 1 ? -1 : 1;
|
|
} else {
|
|
activeColumnIndex = i;
|
|
direction = 1;
|
|
}
|
|
applySort();
|
|
updateSortIndicators(headers, activeColumnIndex, direction, enabled);
|
|
});
|
|
}
|
|
|
|
const setEnabled = (nextEnabled) => {
|
|
enabled = nextEnabled && headers.length > 0 && rows.length > 0;
|
|
if (!enabled) {
|
|
activeColumnIndex = -1;
|
|
direction = 1;
|
|
resetOrder();
|
|
}
|
|
updateSortIndicators(headers, activeColumnIndex, direction, enabled);
|
|
};
|
|
|
|
setEnabled(false);
|
|
return {
|
|
setEnabled
|
|
};
|
|
}
|
|
|
|
function createFilterController(doc, head, headerRow, rows) {
|
|
if (!headerRow || rows.length === 0) {
|
|
return {
|
|
setEnabled() {
|
|
}
|
|
};
|
|
}
|
|
|
|
const filterRow = doc.createElement("tr");
|
|
const inputs = [];
|
|
const requestFrame = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (callback) => {
|
|
callback();
|
|
return 0;
|
|
};
|
|
const cancelFrame = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : () => {
|
|
};
|
|
let frameHandle = 0;
|
|
let enabled = false;
|
|
|
|
filterRow.className = "csv-codeblock__header-filter-row";
|
|
|
|
for (let i = 0; i < headerRow.childNodes.length; i++) {
|
|
const cell = doc.createElement("th");
|
|
const input = doc.createElement("input");
|
|
|
|
cell.className = "csv-codeblock__header-filter-cell";
|
|
input.type = "search";
|
|
input.className = "csv-codeblock__header-filter-input";
|
|
input.placeholder = "Filter";
|
|
|
|
input.addEventListener("input", () => {
|
|
if (frameHandle) {
|
|
cancelFrame(frameHandle);
|
|
}
|
|
frameHandle = requestFrame(() => {
|
|
frameHandle = 0;
|
|
applyFilter();
|
|
});
|
|
});
|
|
|
|
cell.appendChild(input);
|
|
filterRow.appendChild(cell);
|
|
inputs.push(input);
|
|
}
|
|
|
|
head.appendChild(filterRow);
|
|
|
|
const applyFilter = () => {
|
|
for (const row of rows) {
|
|
let isVisible = true;
|
|
for (let i = 0; i < inputs.length; i++) {
|
|
const query = inputs[i].value.trim().toLowerCase();
|
|
if (query.length > 0 && !(row.searchValues[i] || "").includes(query)) {
|
|
isVisible = false;
|
|
break;
|
|
}
|
|
}
|
|
row.element.hidden = !isVisible;
|
|
}
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
for (const input of inputs) {
|
|
input.value = "";
|
|
}
|
|
};
|
|
|
|
const setEnabled = (nextEnabled) => {
|
|
enabled = nextEnabled;
|
|
filterRow.hidden = !enabled;
|
|
if (!enabled) {
|
|
clearFilters();
|
|
}
|
|
applyFilter();
|
|
};
|
|
|
|
setEnabled(false);
|
|
return {
|
|
setEnabled
|
|
};
|
|
}
|
|
|
|
function resolveMaxHeight(maxHeight, highTableEnabled) {
|
|
if (!highTableEnabled) {
|
|
return maxHeight;
|
|
}
|
|
|
|
const normalized = maxHeight.trim().toLowerCase();
|
|
if (normalized === "none" || normalized === "auto" || normalized === "unset" || normalized === "inherit") {
|
|
return maxHeight;
|
|
}
|
|
|
|
return `calc(${maxHeight} * 2)`;
|
|
}
|
|
|
|
function setCssVariable(element, name, value) {
|
|
if (element.style && typeof element.style.setProperty === "function") {
|
|
element.style.setProperty(name, value);
|
|
} else {
|
|
element.style[name] = value;
|
|
}
|
|
}
|
|
|
|
function setWrapperClasses(wrapper, state, stickyEnabled) {
|
|
wrapper.className = [
|
|
"csv-codeblock",
|
|
state.compact ? "csv-codeblock--compact" : "",
|
|
state.zebra ? "csv-codeblock--zebra" : "",
|
|
state.highTable ? "csv-codeblock--high-table" : "",
|
|
stickyEnabled ? "csv-codeblock--sticky" : ""
|
|
].filter((className) => className.length > 0).join(" ");
|
|
}
|
|
|
|
function updateStickyOffsets(wrapper, headerRow) {
|
|
const setOffset = () => {
|
|
let height = 0;
|
|
if (headerRow && typeof headerRow.getBoundingClientRect === "function") {
|
|
height = headerRow.getBoundingClientRect().height;
|
|
} else if (headerRow && typeof headerRow.offsetHeight === "number") {
|
|
height = headerRow.offsetHeight;
|
|
}
|
|
setCssVariable(wrapper, "--csv-codeblock-header-height", `${Math.ceil(height || 0)}px`);
|
|
};
|
|
|
|
if (typeof requestAnimationFrame === "function") {
|
|
requestAnimationFrame(setOffset);
|
|
} else {
|
|
setOffset();
|
|
}
|
|
}
|
|
|
|
function setToolbarButtonState(button, pressed) {
|
|
button.className = pressed ? "csv-codeblock__toolbar-button csv-codeblock__toolbar-button--pressed" : "csv-codeblock__toolbar-button";
|
|
button.setAttribute("aria-pressed", pressed ? "true" : "false");
|
|
}
|
|
|
|
function createToolbar(doc, features, onToggle) {
|
|
const toolbar = doc.createElement("div");
|
|
const buttons = {};
|
|
const controls = [
|
|
{ key: "sort", label: "Sorting", available: features.sort },
|
|
{ key: "filter", label: "Filtering", available: features.filter },
|
|
{ key: "compact", label: "Compact", available: true },
|
|
{ key: "zebra", label: "Zebra", available: true },
|
|
{ key: "highTable", label: "High table", available: true }
|
|
];
|
|
|
|
toolbar.className = "csv-codeblock__toolbar";
|
|
|
|
for (const control of controls) {
|
|
const button = doc.createElement("button");
|
|
|
|
button.type = "button";
|
|
button.textContent = control.label;
|
|
button.disabled = !control.available;
|
|
button.className = "csv-codeblock__toolbar-button";
|
|
button.setAttribute("aria-pressed", "false");
|
|
if (button.disabled) {
|
|
button.className += " csv-codeblock__toolbar-button--disabled";
|
|
} else {
|
|
button.addEventListener("click", () => {
|
|
onToggle(control.key);
|
|
});
|
|
}
|
|
|
|
toolbar.appendChild(button);
|
|
buttons[control.key] = button;
|
|
}
|
|
|
|
return {
|
|
element: toolbar,
|
|
update(state) {
|
|
for (const control of controls) {
|
|
if (!buttons[control.key] || !control.available) {
|
|
continue;
|
|
}
|
|
setToolbarButtonState(buttons[control.key], state[control.key]);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
const DEFAULT_RENDER_OPTIONS = {
|
|
title: "",
|
|
header: true,
|
|
sticky: true,
|
|
zebra: false,
|
|
compact: false,
|
|
sort: false,
|
|
filter: false,
|
|
highTable: false,
|
|
links: true,
|
|
maxHeight: "24rem",
|
|
delimiter: "auto"
|
|
};
|
|
|
|
function normalizeOptionKey(key) {
|
|
if (key === "max-height") {
|
|
return "maxHeight";
|
|
}
|
|
if (key === "high-table") {
|
|
return "highTable";
|
|
}
|
|
return key;
|
|
}
|
|
|
|
function parseBooleanOption(key, rawValue) {
|
|
if (rawValue === "true" || rawValue === "on" || rawValue === "yes" || rawValue === "1") {
|
|
return true;
|
|
}
|
|
if (rawValue === "false" || rawValue === "off" || rawValue === "no" || rawValue === "0") {
|
|
return false;
|
|
}
|
|
throw new Error(`Invalid boolean value for option "${key}"`);
|
|
}
|
|
|
|
function parseStringOption(rawValue) {
|
|
if (rawValue.length >= 2) {
|
|
const firstChar = rawValue[0];
|
|
const lastChar = rawValue[rawValue.length - 1];
|
|
if ((firstChar === '"' || firstChar === "'") && firstChar === lastChar) {
|
|
return rawValue.slice(1, -1).replace(/\\(["'])/g, "$1");
|
|
}
|
|
}
|
|
return rawValue;
|
|
}
|
|
|
|
function tokenizeOptionString(optionText) {
|
|
const tokens = [];
|
|
let current = "";
|
|
let quote = "";
|
|
|
|
for (let i = 0; i < optionText.length; i++) {
|
|
const char = optionText[i];
|
|
|
|
if (quote) {
|
|
current += char;
|
|
if (char === "\\" && i + 1 < optionText.length) {
|
|
current += optionText[i + 1];
|
|
i++;
|
|
} else if (char === quote) {
|
|
quote = "";
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (char === '"' || char === "'") {
|
|
quote = char;
|
|
current += char;
|
|
continue;
|
|
}
|
|
|
|
if (/\s/.test(char)) {
|
|
if (current.length > 0) {
|
|
tokens.push(current);
|
|
current = "";
|
|
}
|
|
continue;
|
|
}
|
|
|
|
current += char;
|
|
}
|
|
|
|
if (quote) {
|
|
throw new Error("Unclosed quote in codeblock options");
|
|
}
|
|
if (current.length > 0) {
|
|
tokens.push(current);
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
function parseFenceOptions(optionText, defaultDelimiter) {
|
|
const options = Object.assign({}, DEFAULT_RENDER_OPTIONS, {
|
|
delimiter: defaultDelimiter
|
|
});
|
|
const tokens = tokenizeOptionString(optionText);
|
|
|
|
for (const token of tokens) {
|
|
const separatorIndex = token.indexOf(":");
|
|
if (separatorIndex <= 0) {
|
|
throw new Error(`Invalid codeblock option "${token}"`);
|
|
}
|
|
|
|
const rawKey = token.slice(0, separatorIndex).trim();
|
|
const key = normalizeOptionKey(rawKey);
|
|
const rawValue = token.slice(separatorIndex + 1).trim();
|
|
|
|
if (rawValue.length === 0) {
|
|
throw new Error(`Missing value for option "${rawKey}"`);
|
|
}
|
|
|
|
if (key === "title" || key === "maxHeight") {
|
|
options[key] = parseStringOption(rawValue);
|
|
} else if (
|
|
key === "header" ||
|
|
key === "sticky" ||
|
|
key === "zebra" ||
|
|
key === "compact" ||
|
|
key === "sort" ||
|
|
key === "filter" ||
|
|
key === "highTable" ||
|
|
key === "links"
|
|
) {
|
|
options[key] = parseBooleanOption(rawKey, rawValue);
|
|
} else if (key === "delimiter") {
|
|
const value = parseStringOption(rawValue);
|
|
if (value !== "auto" && value !== "comma" && value !== "semicolon" && value !== "tab") {
|
|
throw new Error(`Invalid delimiter value "${value}"`);
|
|
}
|
|
options.delimiter = value;
|
|
} else {
|
|
throw new Error(`Unknown codeblock option "${rawKey}"`);
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function getRenderOptions(ctx, el, defaultDelimiter) {
|
|
if (!ctx || typeof ctx.getSectionInfo !== "function") {
|
|
return Object.assign({}, DEFAULT_RENDER_OPTIONS, { delimiter: defaultDelimiter });
|
|
}
|
|
|
|
const sectionInfo = ctx.getSectionInfo(el) || (el.parentElement ? ctx.getSectionInfo(el.parentElement) : null);
|
|
if (!sectionInfo || typeof sectionInfo.text !== "string") {
|
|
return Object.assign({}, DEFAULT_RENDER_OPTIONS, { delimiter: defaultDelimiter });
|
|
}
|
|
|
|
const firstLine = sectionInfo.text.split(/\r?\n/, 1)[0];
|
|
const match = /^\s*(?:`{3,}|~{3,})\s*\S+\s*(.*)$/.exec(firstLine);
|
|
if (!match || match[1].trim().length === 0) {
|
|
return Object.assign({}, DEFAULT_RENDER_OPTIONS, { delimiter: defaultDelimiter });
|
|
}
|
|
|
|
return parseFenceOptions(match[1], defaultDelimiter);
|
|
}
|
|
|
|
function resolveDelimiter(source, configuredDelimiter) {
|
|
if (configuredDelimiter === "comma") {
|
|
return ",";
|
|
}
|
|
if (configuredDelimiter === "semicolon") {
|
|
return ";";
|
|
}
|
|
if (configuredDelimiter === "tab") {
|
|
return "\t";
|
|
}
|
|
return detectCsvDelimiter(source);
|
|
}
|
|
|
|
|
|
class CsvCodeBlockPlugin extends import_obsidian.Plugin {
|
|
async onload() {
|
|
// Register CSV code block processor
|
|
this.registerMarkdownCodeBlockProcessor("csv", (source, el, ctx) => {
|
|
this.renderTable(source, el, ctx, "auto");
|
|
});
|
|
|
|
// Register TSV code block processor
|
|
this.registerMarkdownCodeBlockProcessor("tsv", (source, el, ctx) => {
|
|
this.renderTable(source, el, ctx, "tab");
|
|
});
|
|
}
|
|
|
|
renderTable(source, el, ctx, defaultDelimiter) {
|
|
const options = getRenderOptions(ctx, el, defaultDelimiter);
|
|
const resolvedDelimiter = resolveDelimiter(source, options.delimiter);
|
|
const doc = el.ownerDocument;
|
|
const wrapper = doc.createElement("div");
|
|
const scrollContainer = doc.createElement("div");
|
|
const table = doc.createElement("table");
|
|
const head = doc.createElement("thead");
|
|
const body = doc.createElement("tbody");
|
|
const sortableHeaders = [];
|
|
const dataRows = [];
|
|
const state = {
|
|
sort: options.sort,
|
|
filter: options.filter,
|
|
compact: options.compact,
|
|
zebra: options.zebra,
|
|
highTable: options.highTable
|
|
};
|
|
let expectedColumnCount = 0;
|
|
let isHeaderRow = options.header;
|
|
let headerRowElement = null;
|
|
let applyState = () => {
|
|
setCssVariable(wrapper, "--csv-codeblock-max-height", resolveMaxHeight(options.maxHeight, state.highTable));
|
|
toolbar.update(state);
|
|
};
|
|
|
|
setWrapperClasses(wrapper, state, options.sticky);
|
|
scrollContainer.className = "csv-codeblock__scroll";
|
|
table.className = "csv-codeblock__table";
|
|
|
|
if (options.title.length > 0) {
|
|
const title = doc.createElement("div");
|
|
title.className = "csv-codeblock__title";
|
|
title.textContent = options.title;
|
|
wrapper.appendChild(title);
|
|
}
|
|
|
|
const toolbar = createToolbar(doc, {
|
|
sort: options.header,
|
|
filter: options.header
|
|
}, (key) => {
|
|
state[key] = !state[key];
|
|
applyState();
|
|
});
|
|
wrapper.appendChild(toolbar.element);
|
|
|
|
try {
|
|
const parser = new CsvParser(source, resolvedDelimiter);
|
|
|
|
parser.forEachRow((rowData, rowNumber) => {
|
|
if (expectedColumnCount === 0) {
|
|
expectedColumnCount = rowData.length;
|
|
} else if (rowData.length !== expectedColumnCount) {
|
|
throw new CsvParseError(
|
|
`Row has ${rowData.length} fields, expected ${expectedColumnCount}`,
|
|
rowNumber,
|
|
rowData.length + 1,
|
|
rowNumber
|
|
);
|
|
}
|
|
const row = doc.createElement("tr");
|
|
const cellTag = isHeaderRow ? "th" : "td";
|
|
for (let i = 0; i < rowData.length; i++) {
|
|
const cell = doc.createElement(cellTag);
|
|
if (isHeaderRow) {
|
|
cell.scope = "col";
|
|
cell.className = "csv-codeblock__header-cell";
|
|
const button = doc.createElement("button");
|
|
const label = doc.createElement("span");
|
|
const indicator = doc.createElement("span");
|
|
|
|
button.type = "button";
|
|
button.className = "csv-codeblock__sort-button";
|
|
label.className = "csv-codeblock__sort-label";
|
|
label.textContent = rowData[i];
|
|
indicator.className = "csv-codeblock__sort-indicator";
|
|
|
|
button.appendChild(label);
|
|
button.appendChild(indicator);
|
|
cell.appendChild(button);
|
|
|
|
sortableHeaders.push({
|
|
button,
|
|
cell,
|
|
indicator
|
|
});
|
|
} else {
|
|
if (options.links) {
|
|
createCellContent(doc, cell, rowData[i]);
|
|
} else {
|
|
cell.textContent = rowData[i];
|
|
}
|
|
}
|
|
row.appendChild(cell);
|
|
}
|
|
if (isHeaderRow) {
|
|
row.className = "csv-codeblock__header-row";
|
|
head.appendChild(row);
|
|
headerRowElement = row;
|
|
isHeaderRow = false;
|
|
} else {
|
|
dataRows.push({
|
|
element: row,
|
|
values: rowData,
|
|
searchValues: rowData.map((value) => value.toLowerCase()),
|
|
originalIndex: dataRows.length
|
|
});
|
|
body.appendChild(row);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
this.renderError(el, doc, error);
|
|
return;
|
|
}
|
|
|
|
if (head.childNodes.length > 0) {
|
|
const filterController = createFilterController(doc, head, headerRowElement, dataRows);
|
|
const sortController = createSortController(body, sortableHeaders, dataRows);
|
|
applyState = () => {
|
|
setWrapperClasses(wrapper, state, options.sticky);
|
|
setCssVariable(wrapper, "--csv-codeblock-max-height", resolveMaxHeight(options.maxHeight, state.highTable));
|
|
sortController.setEnabled(state.sort && options.header);
|
|
filterController.setEnabled(state.filter && options.header);
|
|
toolbar.update(state);
|
|
updateStickyOffsets(wrapper, headerRowElement);
|
|
};
|
|
|
|
table.appendChild(head);
|
|
table.appendChild(body);
|
|
wrapper.appendChild(scrollContainer);
|
|
scrollContainer.appendChild(table);
|
|
el.appendChild(wrapper);
|
|
applyState();
|
|
return;
|
|
}
|
|
|
|
table.appendChild(body);
|
|
wrapper.appendChild(scrollContainer);
|
|
scrollContainer.appendChild(table);
|
|
el.appendChild(wrapper);
|
|
applyState();
|
|
}
|
|
|
|
renderError(el, doc, error) {
|
|
const wrapper = doc.createElement("div");
|
|
const title = doc.createElement("div");
|
|
const details = doc.createElement("div");
|
|
|
|
wrapper.className = "csv-codeblock csv-codeblock--error";
|
|
title.className = "csv-codeblock__error-title";
|
|
details.className = "csv-codeblock__error-details";
|
|
title.textContent = "Malformed CSV";
|
|
|
|
if (error instanceof CsvParseError) {
|
|
details.textContent = `Row ${error.row}, field ${error.field} (line ${error.line}): ${error.message}.`;
|
|
} else if (error instanceof Error) {
|
|
details.textContent = error.message;
|
|
} else {
|
|
details.textContent = String(error);
|
|
}
|
|
|
|
wrapper.appendChild(title);
|
|
wrapper.appendChild(details);
|
|
el.appendChild(wrapper);
|
|
}
|
|
}
|