Initial commit: Counter plugin for Obsidian

- Add counter syntax: ~ (number) label
- Implement interactive +/- buttons
- Add CSS styling for counter UI
- Include build configuration and dependencies
This commit is contained in:
2025-11-26 10:14:58 +01:00
commit 2588e72b39
8 changed files with 396 additions and 0 deletions

150
main.ts Normal file
View File

@@ -0,0 +1,150 @@
import { Plugin, MarkdownPostProcessorContext, MarkdownView } from 'obsidian';
export default class CounterPlugin extends Plugin {
async onload() {
console.log('Loading Counter Plugin');
this.registerMarkdownPostProcessor((element, context) => {
this.processCounters(element, context);
});
this.registerEvent(
this.app.workspace.on('editor-change', () => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
// Refresh the preview to update counters
view.previewMode.rerender(true);
}
})
);
}
onunload() {
console.log('Unloading Counter Plugin');
}
processCounters(element: HTMLElement, context: MarkdownPostProcessorContext) {
const counterRegex = /^~\s*\(\s*(\d+)\s*\)\s*(.*)$/;
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
const nodesToReplace: Array<{ node: Node; parent: Node }> = [];
let node;
while ((node = walker.nextNode())) {
const text = node.textContent || '';
const lines = text.split('\n');
lines.forEach((line, index) => {
const match = line.match(counterRegex);
if (match) {
nodesToReplace.push({ node, parent: node.parentNode! });
}
});
}
nodesToReplace.forEach(({ node, parent }) => {
const text = node.textContent || '';
const lines = text.split('\n');
const fragment = document.createDocumentFragment();
lines.forEach((line, index) => {
const match = line.match(counterRegex);
if (match) {
const currentValue = parseInt(match[1], 10);
const label = match[2].trim();
const counterContainer = this.createCounterElement(
currentValue,
label,
node,
context
);
fragment.appendChild(counterContainer);
} else {
if (line) {
fragment.appendChild(document.createTextNode(line));
}
}
if (index < lines.length - 1) {
fragment.appendChild(document.createTextNode('\n'));
}
});
parent.replaceChild(fragment, node);
});
}
createCounterElement(
value: number,
label: string,
sourceNode: Node,
context: MarkdownPostProcessorContext
): HTMLElement {
const container = document.createElement('div');
container.className = 'counter-container';
const minusButton = document.createElement('button');
minusButton.className = 'counter-button counter-minus';
minusButton.textContent = '';
minusButton.setAttribute('aria-label', 'Decrease counter');
const counterDisplay = document.createElement('span');
counterDisplay.className = 'counter-display';
counterDisplay.textContent = value.toString();
const plusButton = document.createElement('button');
plusButton.className = 'counter-button counter-plus';
plusButton.textContent = '+';
plusButton.setAttribute('aria-label', 'Increase counter');
const labelSpan = document.createElement('span');
labelSpan.className = 'counter-label';
labelSpan.textContent = label;
const updateSource = (newValue: number) => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) return;
const editor = view.editor;
const content = editor.getValue();
const counterRegex = /^~\s*\(\s*\d+\s*\)\s*(.*)$/gm;
let matchIndex = 0;
const newContent = content.replace(counterRegex, (match, capturedLabel) => {
const currentLabel = capturedLabel.trim();
if (currentLabel === label) {
matchIndex++;
return `~ (${newValue}) ${label}`;
}
return match;
});
if (content !== newContent) {
editor.setValue(newContent);
}
};
minusButton.addEventListener('click', () => {
const newValue = value - 1;
counterDisplay.textContent = newValue.toString();
updateSource(newValue);
});
plusButton.addEventListener('click', () => {
const newValue = value + 1;
counterDisplay.textContent = newValue.toString();
updateSource(newValue);
});
container.appendChild(minusButton);
container.appendChild(counterDisplay);
container.appendChild(plusButton);
if (label) {
container.appendChild(labelSpan);
}
return container;
}
}