┌   ┐
54
└   ┘

summaryrefslogtreecommitdiff
path: root/.obsidian/plugins
diff options
context:
space:
mode:
authorConway <[email protected]>2026-09-02 16:29:12 -0400
committerConway <[email protected]>2026-09-02 16:29:12 -0400
commit5eeea439d6ee3c2f5ddbdd56ebb6a19ef199686e (patch)
tree91a95ee6b2f1974e879673ae9cc1616f5ca1d753 /.obsidian/plugins
parent73de1553a09f72247f0df30d556c848bcd4f5cba (diff)
added new plugin Table Trimmer
Diffstat (limited to '.obsidian/plugins')
-rw-r--r--.obsidian/plugins/abacus-sorter/main.js14
-rw-r--r--.obsidian/plugins/table-trimmer/main.js334
-rw-r--r--.obsidian/plugins/table-trimmer/manifest.json9
3 files changed, 348 insertions, 9 deletions
diff --git a/.obsidian/plugins/abacus-sorter/main.js b/.obsidian/plugins/abacus-sorter/main.js
index 966e1a3..53bdb19 100644
--- a/.obsidian/plugins/abacus-sorter/main.js
+++ b/.obsidian/plugins/abacus-sorter/main.js
@@ -1,8 +1,4 @@
-/*
-Abacus Custom Sorter. It's a config-driven custom file explorer sorter for Obsidian.
-Inspired by SebastianMC's Custom File Explorer sorting.
-This plugin however does not use a bookmark dependency.
-*/
+/* Abacus Custom Sorter. Config-driven custom sorting for Obsidian's File Explorer. Manual ordering is stored internally (no bookmarks dependency). */
var obsidian = require('obsidian');
@@ -327,7 +323,7 @@ function sortItems(items, spec, app, manualOrderList) {
return result;
}
-// Manual Order Sorting (standalone, no spec)─
+// Manual Order Sorting (standalone, no spec)
function applyManualOrder(items, manualOrderList) {
if (!manualOrderList || manualOrderList.length === 0) return items;
@@ -349,7 +345,7 @@ function applyManualOrder(items, manualOrderList) {
return ordered.filter(Boolean).concat(unordered);
}
-// File Explorer Patcher─
+// File Explorer Patcher
function getFileExplorerLeaf(app) {
const leaves = app.workspace.getLeavesOfType('file-explorer');
@@ -449,7 +445,7 @@ function unpatchFileExplorer(plugin) {
}
}
-// Settings Tab─
+// Settings Tab
class AbacusSettingTab extends obsidian.PluginSettingTab {
constructor(app, plugin) {
@@ -578,7 +574,7 @@ class AbacusSettingTab extends obsidian.PluginSettingTab {
}
}
-// Manual Order Modal─
+// Manual Order Modal
class ManualOrderModal extends obsidian.Modal {
constructor(app, plugin, folderPath, items) {
diff --git a/.obsidian/plugins/table-trimmer/main.js b/.obsidian/plugins/table-trimmer/main.js
new file mode 100644
index 0000000..f162293
--- /dev/null
+++ b/.obsidian/plugins/table-trimmer/main.js
@@ -0,0 +1,334 @@
+"use strict";
+
+/* Table Blank Line Remover. Obsidian will not normally let you delete the blank line above a table in Live Preview. This plugin takes that section of the document and surgically removes the newline that gets reintroduced. */
+
+const { Plugin, Notice } = require("obsidian");
+
+const VERSION = "1.4.0";
+
+// Guard against pathological scans through a note that is mostly blank lines.
+const MAX_BLANK_SCAN = 100;
+
+/* ------------------------------------------------------------------ *
+ * Pure markdown helpers.
+ *
+ * They all take a `doc`: { count: number, get(i): string }, 0-indexed.
+ * ------------------------------------------------------------------ */
+
+function isBlank(text) {
+ return text.trim() === "";
+}
+
+function isQuoted(text) {
+ return /^\s*>/.test(text);
+}
+
+// `| --- | :--: |` and friends. Requires at least one pipe so a plain `---`
+// horizontal rule is never mistaken for a table delimiter.
+function isDelimiterRow(text) {
+ const trimmed = text.trim();
+ if (!trimmed.includes("|") || !trimmed.includes("-")) return false;
+ if (/[^\s|:\-]/.test(trimmed)) return false;
+ const cells = trimmed.replace(/^\|/, "").replace(/\|$/, "").split("|");
+ return cells.length > 0 && cells.every((cell) => /^\s*:?-+:?\s*$/.test(cell));
+}
+
+function isTableRowLine(text) {
+ return !isBlank(text) && text.includes("|") && !isQuoted(text);
+}
+
+function isTableStart(doc, i) {
+ return (
+ i >= 0 &&
+ i + 1 < doc.count &&
+ isTableRowLine(doc.get(i)) &&
+ isDelimiterRow(doc.get(i + 1))
+ );
+}
+
+// Walks fences from the top of the note. Only called once a table-shaped
+// pattern has already matched, so it stays off the hot path.
+function isInsideCodeBlock(doc, index) {
+ let open = null;
+ for (let i = 0; i < index; i++) {
+ const match = /^\s{0,3}(`{3,}|~{3,})(.*)$/.exec(doc.get(i));
+ if (!match) continue;
+ const char = match[1][0];
+ const len = match[1].length;
+ const rest = match[2].trim();
+ if (open) {
+ if (char === open.char && len >= open.len && rest === "") open = null;
+ } else if (!(char === "`" && rest.includes("`"))) {
+ open = { char, len };
+ }
+ }
+ return open !== null;
+}
+
+function tableStartsAt(doc, i) {
+ return isTableStart(doc, i) && !isInsideCodeBlock(doc, i);
+}
+
+// Index of the header row of the table containing line `i`, or -1.
+function tableStartFor(doc, i) {
+ if (i < 0 || i >= doc.count || !isTableRowLine(doc.get(i))) return -1;
+ let top = i;
+ while (top > 0 && isTableRowLine(doc.get(top - 1))) top--;
+ return tableStartsAt(doc, top) ? top : -1;
+}
+
+function firstNonBlankBelow(doc, i) {
+ let j = i;
+ let scanned = 0;
+ while (j < doc.count && isBlank(doc.get(j))) {
+ j++;
+ if (++scanned > MAX_BLANK_SCAN) return -1;
+ }
+ return j < doc.count ? j : -1;
+}
+
+function lastNonBlankAbove(doc, i) {
+ let j = i - 1;
+ while (j >= 0 && isBlank(doc.get(j))) j--;
+ return j;
+}
+
+/* ------------------------------------------------------------------ *
+ * The edit: replace the lines around the table wholesale, leaving the
+ * blank line out. Scoped to the smallest region that still reads as a
+ * block to Obsidian rather than as a newline being deleted.
+ * ------------------------------------------------------------------ */
+
+function blockRewrite(doc, plan) {
+ const n = plan.blankLine;
+ const t = plan.tableLine;
+ if (t < 0 || t >= doc.count) return null;
+
+ let from;
+ let to;
+ if (plan.direction === "above") {
+ // From the heading down through the table's delimiter row.
+ const heading = lastNonBlankAbove(doc, n);
+ from = heading < 0 ? n : heading;
+ to = Math.min(doc.count - 1, t + 1);
+ } else {
+ // From the table down through the line after the blank.
+ from = t;
+ to = Math.min(doc.count - 1, n + 1);
+ }
+ if (n < from || n > to) return null;
+
+ const kept = [];
+ for (let i = from; i <= to; i++) {
+ if (i !== n) kept.push(doc.get(i));
+ }
+ return {
+ range: { from: { line: from, ch: 0 }, to: { line: to, ch: doc.get(to).length } },
+ text: kept.join("\n"),
+ lines: to - from + 1,
+ };
+}
+
+/* ------------------------------------------------------------------ *
+ * Edit plans: { blankLine, caret, direction, tableLine }, where `caret`
+ * is already in post-deletion coordinates and `tableLine` is in
+ * pre-deletion coordinates. Null means "not our business".
+ * ------------------------------------------------------------------ */
+
+function planBackspace(doc, cursor) {
+ const { line, ch } = cursor;
+ const text = doc.get(line);
+
+ // Caret parked on the blank line itself.
+ if (isBlank(text) && line > 0) {
+ const above = firstNonBlankBelow(doc, line);
+ const aboveTable = above !== -1 && tableStartsAt(doc, above);
+ const belowStart = tableStartFor(doc, line - 1);
+ if (!aboveTable && belowStart === -1) return null;
+ return {
+ blankLine: line,
+ caret: { line: line - 1, ch: doc.get(line - 1).length },
+ direction: aboveTable ? "above" : "below",
+ tableLine: aboveTable ? above : belowStart,
+ };
+ }
+
+ // Caret at the very start of the table's header row, blank line above it.
+ if (ch === 0 && line > 0 && isBlank(doc.get(line - 1)) && tableStartsAt(doc, line)) {
+ return {
+ blankLine: line - 1,
+ caret: { line: line - 1, ch: 0 },
+ direction: "above",
+ tableLine: line,
+ };
+ }
+
+ return null;
+}
+
+function planDelete(doc, cursor) {
+ const { line, ch } = cursor;
+ const text = doc.get(line);
+ if (line + 1 >= doc.count) return null;
+
+ // Caret at end of the heading (or of an earlier blank), blank line next.
+ if (ch === text.length && isBlank(doc.get(line + 1))) {
+ const above = firstNonBlankBelow(doc, line + 1);
+ const aboveTable = above !== -1 && tableStartsAt(doc, above);
+ const belowStart = tableStartFor(doc, line);
+ if (!aboveTable && belowStart === -1) return null;
+ return {
+ blankLine: line + 1,
+ caret: { line, ch: text.length },
+ direction: aboveTable ? "above" : "below",
+ tableLine: aboveTable ? above : belowStart,
+ };
+ }
+
+ // Caret on the blank line directly above the table: pull the table up.
+ if (isBlank(text) && tableStartsAt(doc, line + 1)) {
+ return {
+ blankLine: line,
+ caret: { line, ch: 0 },
+ direction: "above",
+ tableLine: line + 1,
+ };
+ }
+
+ return null;
+}
+
+function docFromEditor(editor) {
+ return { count: editor.lineCount(), get: (i) => editor.getLine(i) };
+}
+
+/* ------------------------------------------------------------------ *
+ * Plugin
+ * ------------------------------------------------------------------ */
+
+class TableBlankLineRemover extends Plugin {
+ onload() {
+ console.log(`[TBLR] Table Blank Line Remover v${VERSION}`);
+
+ // Capture phase: ahead of CodeMirror and Obsidian's own handling.
+ this.registerDomEvent(window, "keydown", (evt) => this.onKeyDown(evt), { capture: true });
+
+ this.addCommand({
+ id: "tighten-headings-above-tables",
+ name: "Remove blank lines between headings and tables in this note",
+ editorCallback: (editor) => this.tightenHeadings(editor),
+ });
+ }
+
+ onKeyDown(evt) {
+ if (evt.key !== "Backspace" && evt.key !== "Delete") return;
+ if (evt.ctrlKey || evt.metaKey || evt.altKey || evt.isComposing) return;
+
+ const editor = this.app.workspace.activeEditor && this.app.workspace.activeEditor.editor;
+ if (!editor) return;
+
+ // Only act on the editor that actually owns the keystroke.
+ const contentDOM = editor.cm && editor.cm.contentDOM;
+ if (contentDOM && evt.target instanceof Node && !contentDOM.contains(evt.target)) return;
+ if (editor.somethingSelected()) return;
+
+ const cursor = editor.getCursor();
+ const planner = evt.key === "Backspace" ? planBackspace : planDelete;
+ const plan = planner(docFromEditor(editor), { line: cursor.line, ch: cursor.ch });
+ if (!plan) return;
+
+ // Take the key away from CodeMirror entirely.
+ evt.preventDefault();
+ evt.stopPropagation();
+ if (evt.stopImmediatePropagation) evt.stopImmediatePropagation();
+
+ this.applyPlan(editor, plan);
+ }
+
+ // Cheap length probe: getValue() copies the whole note, which is too
+ // expensive to call on a keystroke.
+ docLength(editor) {
+ if (editor.cm && editor.cm.state) return editor.cm.state.doc.length;
+ return editor.getValue().length;
+ }
+
+ applyPlan(editor, plan) {
+ const edit = blockRewrite(docFromEditor(editor), plan);
+ if (!edit) return false;
+
+ const before = this.docLength(editor);
+ editor.replaceRange(edit.text, edit.range.from, edit.range.to);
+
+ if (this.docLength(editor) === before) {
+ console.warn(
+ `[TBLR] the ${edit.lines}-line rewrite at line ${edit.range.from.line} left the document ` +
+ "unchanged; Obsidian compensated for it"
+ );
+ return false;
+ }
+
+ editor.setCursor(plan.caret);
+ return true;
+ }
+
+ // Bulk pass. Deliberately limited to headings: a table pulled tight against
+ // a paragraph gets absorbed into that paragraph by the markdown parser,
+ // which is not a change anyone wants applied to a whole note at once.
+ tightenHeadings(editor) {
+ const doc = docFromEditor(editor);
+ const targets = [];
+
+ for (let i = 0; i < doc.count; i++) {
+ if (!tableStartsAt(doc, i)) continue;
+ const above = lastNonBlankAbove(doc, i);
+ if (above < 0 || above === i - 1) continue;
+ if (!/^\s{0,3}#{1,6}\s/.test(doc.get(above))) continue;
+ targets.push({ table: i, firstBlank: above + 1 });
+ }
+
+ if (targets.length === 0) {
+ new Notice("No blank lines between headings and tables.");
+ return;
+ }
+
+ // Bottom-up, one blank line at a time, so the line numbers above each
+ // removal stay valid. The table climbs by one after each success.
+ let removed = 0;
+ for (let t = targets.length - 1; t >= 0; t--) {
+ const { table, firstBlank } = targets[t];
+ let shift = 0;
+ for (let i = table - 1; i >= firstBlank; i--) {
+ const ok = this.applyPlan(editor, {
+ blankLine: i,
+ caret: { line: i, ch: 0 },
+ direction: "above",
+ tableLine: table - shift,
+ });
+ if (ok) {
+ removed++;
+ shift++;
+ }
+ }
+ }
+
+ new Notice(
+ `Removed ${removed} blank line${removed === 1 ? "" : "s"} above ` +
+ `${targets.length} table${targets.length === 1 ? "" : "s"}.`
+ );
+ }
+}
+
+module.exports = TableBlankLineRemover;
+// Obsidian accepts either shape depending on how the file was built; expose both.
+module.exports.default = TableBlankLineRemover;
+module.exports._internals = {
+ isDelimiterRow,
+ isTableRowLine,
+ isTableStart,
+ isInsideCodeBlock,
+ tableStartsAt,
+ tableStartFor,
+ blockRewrite,
+ planBackspace,
+ planDelete,
+};
diff --git a/.obsidian/plugins/table-trimmer/manifest.json b/.obsidian/plugins/table-trimmer/manifest.json
new file mode 100644
index 0000000..65973d7
--- /dev/null
+++ b/.obsidian/plugins/table-trimmer/manifest.json
@@ -0,0 +1,9 @@
+{
+ "id": "table-blank-line-remover",
+ "name": "Table Trimmer",
+ "version": "1.4.0",
+ "minAppVersion": "1.4.0",
+ "description": "Lets you delete the blank line stuck above a table in Live Preview, without switching to source mode.",
+ "author": "Conway",
+ "isDesktopOnly": false
+}