diff options
| author | Conway <[email protected]> | 2026-09-02 16:56:20 -0400 |
|---|---|---|
| committer | Conway <[email protected]> | 2026-09-02 16:56:20 -0400 |
| commit | 84560e948dbbc24cdcd593bb0be591085f0ad1b2 (patch) | |
| tree | 4aaadd4b07d38572627f134873976ed4d81edb2a | |
| parent | 5eeea439d6ee3c2f5ddbdd56ebb6a19ef199686e (diff) | |
| -rw-r--r-- | .obsidian/plugins/abacus-sorter/main.js | 334 | ||||
| -rw-r--r-- | .obsidian/plugins/abacus-sorter/manifest.json | 2 | ||||
| -rw-r--r-- | .obsidian/plugins/abacus-sorter/styles.css | 34 | ||||
| -rw-r--r-- | .obsidian/plugins/table-trimmer/main.js | 98 | ||||
| -rw-r--r-- | README.md | 7 |
5 files changed, 69 insertions, 406 deletions
diff --git a/.obsidian/plugins/abacus-sorter/main.js b/.obsidian/plugins/abacus-sorter/main.js index 53bdb19..f49d93c 100644 --- a/.obsidian/plugins/abacus-sorter/main.js +++ b/.obsidian/plugins/abacus-sorter/main.js @@ -1,15 +1,12 @@ -/* Abacus Custom Sorter. Config-driven custom sorting for Obsidian's File Explorer. Manual ordering is stored internally (no bookmarks dependency). */ +/* Abacus Sorter. Config-driven custom sorting for Obsidian's File Explorer. Manual ordering is stored internally (no bookmarks dependency). */ var obsidian = require('obsidian'); -// Default Settings - const DEFAULT_SETTINGS = { suspended: false, sortSpecFile: '', statusBarEnabled: true, notificationsEnabled: true, - contextMenuEnabled: true, delayForInitialApplication: 0, manualOrder: {} // { folderPath: [itemName, itemName, ...] } }; @@ -69,15 +66,12 @@ function parseSortSpec(text) { const line = rawLine.trim(); if (!line || line.startsWith('//') || line.startsWith('#')) continue; - // Folder target if (line.startsWith('target-folder:')) { finishFolder(); currentFolder = line.slice('target-folder:'.length).trim(); - if (currentFolder === '/') currentFolder = '/'; continue; } - // Wildcard folder target if (line.startsWith('target-folder-wildcard:')) { finishFolder(); currentFolder = line.slice('target-folder-wildcard:'.length).trim(); @@ -157,7 +151,7 @@ function getItemName(item) { function itemMatchesGroup(item, group, app) { const f = getItemFile(item); - const name = f ? (f.name || '') : ''; + const name = getItemName(item); const basename = f ? (f.basename || name.replace(/\.[^.]+$/, '')) : ''; switch (group.matchType) { @@ -212,9 +206,7 @@ function makeComparator(order, app, manualOrderList) { case 'natural': return (a, b) => { - const an = getItemName(a); - const bn = getItemName(b); - const cmp = naturalCompare(an, bn); + const cmp = naturalCompare(getItemName(a), getItemName(b)); return order.reverse ? -cmp : cmp; }; @@ -266,10 +258,8 @@ function makeComparator(order, app, manualOrderList) { case 'manual': return (a, b) => { if (!manualOrderList) return 0; - const an = getItemName(a); - const bn = getItemName(b); - const ai = manualOrderList.indexOf(an); - const bi = manualOrderList.indexOf(bn); + const ai = manualOrderList.indexOf(getItemName(a)); + const bi = manualOrderList.indexOf(getItemName(b)); if (ai === -1 && bi === -1) return 0; if (ai === -1) return 1; if (bi === -1) return -1; @@ -283,7 +273,6 @@ function makeComparator(order, app, manualOrderList) { function sortItems(items, spec, app, manualOrderList) { if (!spec || !spec.groups || spec.groups.length === 0) { - // No groups, just apply default order if present if (spec && spec.defaultOrder) { const cmp = makeComparator(spec.defaultOrder, app, manualOrderList); if (cmp) items.sort(cmp); @@ -316,7 +305,6 @@ function sortItems(items, spec, app, manualOrderList) { } if (defaultCmp) unmatched.sort(defaultCmp); - // Reassemble const result = []; for (const bucket of buckets) result.push(...bucket); result.push(...unmatched); @@ -332,8 +320,7 @@ function applyManualOrder(items, manualOrderList) { const unordered = []; for (const item of items) { - const name = getItemName(item); - const idx = manualOrderList.indexOf(name); + const idx = manualOrderList.indexOf(getItemName(item)); if (idx !== -1) { ordered[idx] = item; } else { @@ -347,14 +334,9 @@ function applyManualOrder(items, manualOrderList) { // File Explorer Patcher -function getFileExplorerLeaf(app) { - const leaves = app.workspace.getLeavesOfType('file-explorer'); - return leaves.length > 0 ? leaves[0] : null; -} - function getFileExplorer(app) { - const leaf = getFileExplorerLeaf(app); - return leaf ? leaf.view : null; + const leaves = app.workspace.getLeavesOfType('file-explorer'); + return leaves.length > 0 ? leaves[0].view : null; } /** @@ -370,7 +352,6 @@ function monkeyPatch(obj, methodName, wrapper) { return wrapper.call(this, original.bind(this), ...args); }; - // Return uninstaller return function uninstall() { if (hasOwn) { obj[methodName] = original; @@ -387,40 +368,29 @@ function patchFileExplorer(plugin) { return false; } - plugin.fileExplorer = explorer; - - // Verify the view has getSortedFolderItems and requestSort if (typeof explorer.getSortedFolderItems !== 'function') { console.warn('Abacus: getSortedFolderItems not found on file explorer view'); return false; } - if (typeof explorer.requestSort !== 'function') { - console.warn('Abacus: requestSort not found on file explorer view'); - } if (plugin.unpatchFn) return true; // already patched - // Determine where to patch: prefer prototype, fall back to instance + // Prefer patching the prototype, fall back to the instance const proto = explorer.constructor.prototype; const target = (typeof proto.getSortedFolderItems === 'function') ? proto : explorer; - console.log('Abacus: Patching getSortedFolderItems on', target === proto ? 'prototype' : 'instance'); - - // Patch getSortedFolderItems - this is called by Obsidian to get the ordered - // list of children (FileExplorerItem[]) for each folder in the file explorer + // getSortedFolderItems is called by Obsidian to get the ordered list of + // children (FileExplorerItem[]) for each folder in the file explorer plugin.unpatchFn = monkeyPatch( target, 'getSortedFolderItems', function(originalFn, folder) { - // Always call original first to get Obsidian's default sorted items const defaultResult = originalFn(folder); if (plugin.settings.suspended) return defaultResult; - - const folderPath = folder ? folder.path : ''; if (!defaultResult || defaultResult.length === 0) return defaultResult; - // Check for sort spec + const folderPath = folder ? folder.path : ''; const spec = plugin.getSpecForFolder(folderPath); const manualOrderList = plugin.settings.manualOrder[folderPath] || null; @@ -434,7 +404,6 @@ function patchFileExplorer(plugin) { } ); - console.log('Abacus: File explorer patched successfully'); return true; } @@ -457,19 +426,7 @@ class AbacusSettingTab extends obsidian.PluginSettingTab { const { containerEl } = this; containerEl.empty(); - containerEl.createEl('h2', { text: 'Abacus Custom Sorter' }); - - new obsidian.Setting(containerEl) - .setName('Suspend sorting') - .setDesc('Temporarily disable custom sorting and revert to Obsidian defaults.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.suspended) - .onChange(async (value) => { - this.plugin.settings.suspended = value; - await this.plugin.saveSettings(); - this.plugin.refreshExplorer(); - this.plugin.updateStatusBar(); - })); + containerEl.createEl('h2', { text: 'Abacus' }); new obsidian.Setting(containerEl) .setName('Sort specification file') @@ -506,16 +463,6 @@ class AbacusSettingTab extends obsidian.PluginSettingTab { })); new obsidian.Setting(containerEl) - .setName('Context menu') - .setDesc('Show sorting options in file explorer context menu.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.contextMenuEnabled) - .onChange(async (value) => { - this.plugin.settings.contextMenuEnabled = value; - await this.plugin.saveSettings(); - })); - - new obsidian.Setting(containerEl) .setName('Initial delay (ms)') .setDesc('Delay before applying sort on startup. Increase if sorting fails to apply.') .addText(text => text @@ -550,7 +497,7 @@ class AbacusSettingTab extends obsidian.PluginSettingTab { delete this.plugin.settings.manualOrder[folder]; await this.plugin.saveSettings(); this.plugin.refreshExplorer(); - this.display(); // refresh settings view + this.display(); })); } } @@ -587,14 +534,8 @@ class ManualOrderModal extends obsidian.Modal { // If there's an existing manual order, apply it const existing = plugin.settings.manualOrder[folderPath]; if (existing && existing.length > 0) { - const ordered = []; - const rest = []; - for (const name of existing) { - if (this.items.includes(name)) ordered.push(name); - } - for (const name of this.items) { - if (!ordered.includes(name)) rest.push(name); - } + const ordered = existing.filter(name => this.items.includes(name)); + const rest = this.items.filter(name => !ordered.includes(name)); this.orderList = ordered.concat(rest); } } @@ -624,9 +565,7 @@ class ManualOrderModal extends obsidian.Modal { this.plugin.settings.manualOrder[this.folderPath] = [...this.orderList]; await this.plugin.saveSettings(); this.plugin.refreshExplorer(); - if (this.plugin.settings.notificationsEnabled) { - new obsidian.Notice(`Abacus: Manual order saved for ${this.folderPath || '/'}`); - } + this.plugin.notify(`Abacus: Manual order saved for ${this.folderPath || '/'}`); this.close(); }); @@ -635,9 +574,7 @@ class ManualOrderModal extends obsidian.Modal { delete this.plugin.settings.manualOrder[this.folderPath]; await this.plugin.saveSettings(); this.plugin.refreshExplorer(); - if (this.plugin.settings.notificationsEnabled) { - new obsidian.Notice(`Abacus: Manual order cleared for ${this.folderPath || '/'}`); - } + this.plugin.notify(`Abacus: Manual order cleared for ${this.folderPath || '/'}`); this.close(); }); @@ -652,28 +589,21 @@ class ManualOrderModal extends obsidian.Modal { row.setAttribute('draggable', 'true'); row.dataset.index = String(i); - // Drag handle - const handle = row.createEl('span', { cls: 'abacus-drag-handle', text: '⠿' }); - - // Index number + row.createEl('span', { cls: 'abacus-drag-handle', text: '⠿' }); row.createEl('span', { cls: 'abacus-order-index', text: String(i + 1) }); - - // Item name row.createEl('span', { cls: 'abacus-order-name', text: this.orderList[i] }); - // Up/down buttons const btnContainer = row.createEl('span', { cls: 'abacus-order-buttons' }); if (i > 0) { - const upBtn = btnContainer.createEl('button', { cls: 'abacus-order-btn', text: '\u25B2' }); + const upBtn = btnContainer.createEl('button', { cls: 'abacus-order-btn', text: '▲' }); upBtn.addEventListener('click', () => this.moveItem(i, i - 1)); } if (i < this.orderList.length - 1) { - const downBtn = btnContainer.createEl('button', { cls: 'abacus-order-btn', text: '\u25BC' }); + const downBtn = btnContainer.createEl('button', { cls: 'abacus-order-btn', text: '▼' }); downBtn.addEventListener('click', () => this.moveItem(i, i + 1)); } - // Drag events row.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', String(i)); row.addClass('abacus-dragging'); @@ -710,74 +640,6 @@ class ManualOrderModal extends obsidian.Modal { } } -// Quick Sort Modal (apply a preset sort to a folder) - -class QuickSortModal extends obsidian.Modal { - constructor(app, plugin, folderPath, items) { - super(app); - this.plugin = plugin; - this.folderPath = folderPath; - this.items = items; - } - - onOpen() { - const { contentEl } = this; - contentEl.empty(); - contentEl.addClass('abacus-quick-sort-modal'); - - contentEl.createEl('h2', { text: 'Quick Sort' }); - contentEl.createEl('p', { - text: `Apply a sort order to: ${this.folderPath || '/ (vault root)'}`, - cls: 'abacus-modal-subtitle' - }); - - const options = [ - ['A \u2192 Z (natural)', 'natural-a-z'], - ['Z \u2192 A (natural)', 'natural-z-a'], - ['A \u2192 Z (strict)', 'a-z'], - ['Z \u2192 A (strict)', 'z-a'], - ['Newest modified first', 'modified-new'], - ['Oldest modified first', 'modified-old'], - ['Newest created first', 'created-new'], - ['Oldest created first', 'created-old'], - ['Folders first', 'folders-first'], - ['Files first', 'files-first'], - ]; - - const list = contentEl.createEl('div', { cls: 'abacus-quick-sort-list' }); - - for (const [label, orderKey] of options) { - const btn = list.createEl('button', { text: label, cls: 'abacus-quick-sort-option' }); - btn.addEventListener('click', async () => { - const order = SORT_ORDERS[orderKey]; - if (order) { - const cmp = makeComparator(order, this.app, null); - if (cmp) { - const sorted = [...this.items].sort((a, b) => { - return cmp(a, b); - }); - const names = sorted.map(i => getItemName(i)); - this.plugin.settings.manualOrder[this.folderPath] = names; - await this.plugin.saveSettings(); - this.plugin.refreshExplorer(); - if (this.plugin.settings.notificationsEnabled) { - new obsidian.Notice(`Abacus: Sorted ${this.folderPath || '/'} by ${label}`); - } - } - } - this.close(); - }); - } - - const cancelBtn = contentEl.createEl('button', { text: 'Cancel', cls: 'abacus-quick-sort-cancel' }); - cancelBtn.addEventListener('click', () => this.close()); - } - - onClose() { - this.contentEl.empty(); - } -} - // Main Plugin Class class AbacusSorterPlugin extends obsidian.Plugin { @@ -785,80 +647,26 @@ class AbacusSorterPlugin extends obsidian.Plugin { await this.loadSettings(); this.sortSpecs = null; - this.originalSort = null; - this.fileExplorer = null; - // Add settings tab this.addSettingTab(new AbacusSettingTab(this.app, this)); - // Ribbon icon - this.ribbonIconEl = this.addRibbonIcon('arrow-up-down', 'Abacus Custom Sorter', () => { - this.settings.suspended = !this.settings.suspended; - this.saveSettings(); - this.refreshExplorer(); - this.updateStatusBar(); - this.updateRibbonIcon(); - if (this.settings.notificationsEnabled) { - new obsidian.Notice(`Abacus: Sorting ${this.settings.suspended ? 'suspended' : 'active'}`); - } - }); + this.ribbonIconEl = this.addRibbonIcon('arrow-up-down', 'Abacus', () => this.toggleSuspended()); this.updateRibbonIcon(); - // Status bar this.statusBarEl = this.addStatusBarItem(); this.updateStatusBar(); - // Context menu this.registerEvent( this.app.workspace.on('file-menu', (menu, file, source) => { - if (!this.settings.contextMenuEnabled) return; if (source !== 'file-explorer-context-menu') return; const folderPath = file.children !== undefined ? file.path : (file.parent ? file.parent.path : ''); menu.addSeparator(); - menu.addItem((item) => { item.setTitle('Abacus: Set manual order...') .setIcon('arrow-up-down') - .onClick(() => { - this.openManualOrderModal(folderPath); - }); - }); - - menu.addItem((item) => { - item.setTitle('Abacus: Quick sort...') - .setIcon('sort-asc') - .onClick(() => { - this.openQuickSortModal(folderPath); - }); - }); - - if (this.settings.manualOrder[folderPath]) { - menu.addItem((item) => { - item.setTitle('Abacus: Clear manual order') - .setIcon('x') - .onClick(async () => { - delete this.settings.manualOrder[folderPath]; - await this.saveSettings(); - this.refreshExplorer(); - if (this.settings.notificationsEnabled) { - new obsidian.Notice(`Abacus: Manual order cleared for ${folderPath || '/'}`); - } - }); - }); - } - - menu.addItem((item) => { - item.setTitle(this.settings.suspended ? 'Abacus: Resume sorting' : 'Abacus: Suspend sorting') - .setIcon(this.settings.suspended ? 'play' : 'pause') - .onClick(async () => { - this.settings.suspended = !this.settings.suspended; - await this.saveSettings(); - this.refreshExplorer(); - this.updateStatusBar(); - this.updateRibbonIcon(); - }); + .onClick(() => this.openManualOrderModal(folderPath)); }); }) ); @@ -878,20 +686,10 @@ class AbacusSorterPlugin extends obsidian.Plugin { this.registerEvent(this.app.vault.on('create', () => this.debouncedRefresh())); this.registerEvent(this.app.vault.on('delete', () => this.debouncedRefresh())); - // Commands this.addCommand({ id: 'toggle-sorting', name: 'Toggle custom sorting', - callback: () => { - this.settings.suspended = !this.settings.suspended; - this.saveSettings(); - this.refreshExplorer(); - this.updateStatusBar(); - this.updateRibbonIcon(); - if (this.settings.notificationsEnabled) { - new obsidian.Notice(`Abacus: Sorting ${this.settings.suspended ? 'suspended' : 'active'}`); - } - } + callback: () => this.toggleSuspended() }); this.addCommand({ @@ -905,18 +703,14 @@ class AbacusSorterPlugin extends obsidian.Plugin { }); } - async onunload() { + onunload() { unpatchFileExplorer(this); // Trigger a re-sort so Obsidian reverts to its default order - const explorer = getFileExplorer(this.app); - if (explorer && typeof explorer.requestSort === 'function') { - explorer.requestSort(); - } + this.refreshExplorer(); } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - // Ensure manualOrder exists if (!this.settings.manualOrder) this.settings.manualOrder = {}; } @@ -924,26 +718,33 @@ class AbacusSorterPlugin extends obsidian.Plugin { await this.saveData(this.settings); } + notify(message) { + if (this.settings.notificationsEnabled) new obsidian.Notice(message); + } + + toggleSuspended() { + this.settings.suspended = !this.settings.suspended; + this.saveSettings(); + this.refreshExplorer(); + this.updateStatusBar(); + this.updateRibbonIcon(); + this.notify(`Abacus: Sorting ${this.settings.suspended ? 'suspended' : 'active'}`); + } + async initializeSorting() { await this.loadSortSpec(); - const patched = patchFileExplorer(this); - if (patched) { + if (patchFileExplorer(this)) { this.refreshExplorer(); - if (this.settings.notificationsEnabled) { - new obsidian.Notice('Abacus Custom Sorter: Active'); - } - } else { - // Retry once after a short delay - setTimeout(() => { - const retry = patchFileExplorer(this); - if (retry) { - this.refreshExplorer(); - if (this.settings.notificationsEnabled) { - new obsidian.Notice('Abacus Custom Sorter: Active'); - } - } - }, 1000); + this.notify('Abacus: Active'); + return; } + // Retry once after a short delay + setTimeout(() => { + if (patchFileExplorer(this)) { + this.refreshExplorer(); + this.notify('Abacus: Active'); + } + }, 1000); } async loadSortSpec() { @@ -965,17 +766,15 @@ class AbacusSorterPlugin extends obsidian.Plugin { getSpecForFolder(folderPath) { if (!this.sortSpecs) return null; - // Exact match if (this.sortSpecs[folderPath]) return this.sortSpecs[folderPath]; - // Check wildcards: try parent paths with /* + // Wildcards: try parent paths with /* const parts = folderPath.split('/'); for (let i = parts.length - 1; i >= 0; i--) { const partial = parts.slice(0, i).join('/') + '/*'; if (this.sortSpecs[partial]) return this.sortSpecs[partial]; } - // Global wildcard if (this.sortSpecs['/*']) return this.sortSpecs['/*']; if (this.sortSpecs['*']) return this.sortSpecs['*']; @@ -983,17 +782,14 @@ class AbacusSorterPlugin extends obsidian.Plugin { } openManualOrderModal(folderPath) { - // Get the TFolder and its children directly from the vault const folder = this.app.vault.getAbstractFileByPath(folderPath); if (!folder || !folder.children) { new obsidian.Notice('Abacus: Folder not found'); return; } - // Build item wrappers that match what getSortedFolderItems returns - // Each needs a .file property pointing to the TAbstractFile + // Wrap children the way getSortedFolderItems returns them: { file } const items = folder.children.map(child => ({ file: child })); - if (items.length === 0) { new obsidian.Notice('Abacus: No items found in folder'); return; @@ -1005,23 +801,6 @@ class AbacusSorterPlugin extends obsidian.Plugin { new ManualOrderModal(this.app, this, folderPath, items).open(); } - openQuickSortModal(folderPath) { - const folder = this.app.vault.getAbstractFileByPath(folderPath); - if (!folder || !folder.children) { - new obsidian.Notice('Abacus: Folder not found'); - return; - } - - const items = folder.children.map(child => ({ file: child })); - - if (items.length === 0) { - new obsidian.Notice('Abacus: No items found in folder'); - return; - } - - new QuickSortModal(this.app, this, folderPath, items).open(); - } - refreshExplorer() { // requestSort() tells the file explorer to re-call getSortedFolderItems // for all visible folders, which triggers our patched version @@ -1034,9 +813,7 @@ class AbacusSorterPlugin extends obsidian.Plugin { _refreshTimeout = null; debouncedRefresh() { if (this._refreshTimeout) clearTimeout(this._refreshTimeout); - this._refreshTimeout = setTimeout(() => { - this.refreshExplorer(); - }, 300); + this._refreshTimeout = setTimeout(() => this.refreshExplorer(), 300); } updateStatusBar() { @@ -1047,8 +824,7 @@ class AbacusSorterPlugin extends obsidian.Plugin { } const manualCount = Object.keys(this.settings.manualOrder).length; const specCount = this.sortSpecs ? Object.keys(this.sortSpecs).length : 0; - const state = this.settings.suspended ? 'OFF' : 'ON'; - let text = `Abacus: ${state}`; + let text = `Abacus: ${this.settings.suspended ? 'OFF' : 'ON'}`; if (!this.settings.suspended && (manualCount > 0 || specCount > 0)) { const parts = []; if (specCount > 0) parts.push(`${specCount} spec`); @@ -1062,10 +838,10 @@ class AbacusSorterPlugin extends obsidian.Plugin { if (!this.ribbonIconEl) return; if (this.settings.suspended) { this.ribbonIconEl.addClass('abacus-ribbon-suspended'); - this.ribbonIconEl.setAttribute('aria-label', 'Abacus Custom Sorter (suspended)'); + this.ribbonIconEl.setAttribute('aria-label', 'Abacus (suspended)'); } else { this.ribbonIconEl.removeClass('abacus-ribbon-suspended'); - this.ribbonIconEl.setAttribute('aria-label', 'Abacus Custom Sorter (active)'); + this.ribbonIconEl.setAttribute('aria-label', 'Abacus (active)'); } } } diff --git a/.obsidian/plugins/abacus-sorter/manifest.json b/.obsidian/plugins/abacus-sorter/manifest.json index 1d35c98..c49ccd1 100644 --- a/.obsidian/plugins/abacus-sorter/manifest.json +++ b/.obsidian/plugins/abacus-sorter/manifest.json @@ -1,6 +1,6 @@ { "id": "abacus-sorter", - "name": "Abacus Custom Sorter", + "name": "Abacus Sorter", "version": "1.0.0", "minAppVersion": "1.7.2", "description": "Config-driven custom sorting of files and folders in File Explorer, with manual drag-style reordering and sort specification support.", diff --git a/.obsidian/plugins/abacus-sorter/styles.css b/.obsidian/plugins/abacus-sorter/styles.css index 599055a..44437ce 100644 --- a/.obsidian/plugins/abacus-sorter/styles.css +++ b/.obsidian/plugins/abacus-sorter/styles.css @@ -1,4 +1,4 @@ -/* Abacus Custom Sorter */ +/* Abacus */ /* Ribbon icon when suspended */ .abacus-ribbon-suspended { @@ -105,35 +105,3 @@ gap: 8px; justify-content: flex-end; } - -/* Quick Sort Modal */ -.abacus-quick-sort-modal { - max-width: 350px; -} - -.abacus-quick-sort-list { - display: flex; - flex-direction: column; - gap: 4px; - margin-bottom: 12px; -} - -.abacus-quick-sort-option { - text-align: left; - padding: 8px 12px; - border: 1px solid var(--background-modifier-border); - border-radius: 6px; - background: var(--background-secondary); - cursor: pointer; - transition: background-color 0.1s; -} - -.abacus-quick-sort-option:hover { - background-color: var(--interactive-accent); - color: var(--text-on-accent); -} - -.abacus-quick-sort-cancel { - margin-top: 4px; - align-self: flex-end; -} diff --git a/.obsidian/plugins/table-trimmer/main.js b/.obsidian/plugins/table-trimmer/main.js index f162293..760b820 100644 --- a/.obsidian/plugins/table-trimmer/main.js +++ b/.obsidian/plugins/table-trimmer/main.js @@ -2,18 +2,12 @@ /* 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"; +const { Plugin } = require("obsidian"); // 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. - * ------------------------------------------------------------------ */ +// The markdown helpers below all take a `doc`: { count: number, get(i): string }, 0-indexed. function isBlank(text) { return text.trim() === ""; @@ -93,12 +87,9 @@ function lastNonBlankAbove(doc, i) { 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. - * ------------------------------------------------------------------ */ - +// 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; @@ -129,11 +120,9 @@ function blockRewrite(doc, plan) { }; } -/* ------------------------------------------------------------------ * - * 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". - * ------------------------------------------------------------------ */ +/* 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; @@ -202,22 +191,10 @@ 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) { @@ -270,65 +247,6 @@ class TableBlankLineRemover extends Plugin { 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, -}; @@ -1,11 +1,12 @@ Hello. This repository contains the following materials for my Obsidian vault: Plugins -- Abacus Sorter +- Abacus Sorter (it lets you custom-sort the file explorer) +- Table Trimmer (it lets you delete the leading newline above tables in Live Preview) Themes -- Spectroscope-Gruv -- Spectroscope-Noir +- Spectroscope-Gruv (gruvbox-colored background) +- Spectroscope-Noir (black background, gruvbox highlights) Keep in mind they're stored in .obsidian in this folder, since that's what obsidian vaults expect. |
