vitovt_obsidian-csv-modern-.../main.js

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);
}
}