Compare commits

...

4 Commits

Author SHA1 Message Date
bb4a6e5d0b Fix counter to update only clicked instance
- Track each counter by its original text instead of label
- Use indexOf to find and replace specific counter occurrence
- Pass originalText to createCounterElement method
- Update both MarkdownPostProcessor and Live Preview widget
- Prevents all counters with same label from updating together

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:58:09 +01:00
cc9e9d4cba Add Live Preview support and reorder counter elements
- Move buttons to right side of number (number, -, +, label)
- Add CodeMirror ViewPlugin for Live Preview rendering
- Counter now works in edit mode (Live Preview)
- Create CounterWidget for editor decorations
- Use Decoration.replace to render counters in editor

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:42:35 +01:00
63258fa1f3 Fix counter increment logic and improve button styling
- Store currentValue in mutable variable to track state properly
- Counter now increments/decrements correctly beyond 1/-1
- Reduce button size from 24px to 16px
- Reduce font size and spacing for more compact appearance
- Improve vertical alignment with inline text
- Fix regex in updateSource to match new pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:38:35 +01:00
99ebde08c9 Fix counter rendering in Obsidian
- Remove start-of-line anchor from regex to match anywhere in text
- Improve text node processing to handle inline counters
- Simplify markdown post processor logic
- Remove unused editor-change event handler

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:34:33 +01:00
2 changed files with 143 additions and 75 deletions

188
main.ts
View File

@@ -1,4 +1,43 @@
import { Plugin, MarkdownPostProcessorContext, MarkdownView } from 'obsidian'; import { Plugin, MarkdownPostProcessorContext, MarkdownView } from 'obsidian';
import { Decoration, DecorationSet, EditorView, WidgetType, ViewPlugin, ViewUpdate } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
class CounterWidget extends WidgetType {
constructor(private value: number, private label: string, private originalText: string, private plugin: CounterPlugin) {
super();
}
toDOM(view: EditorView): HTMLElement {
return this.plugin.createCounterElement(this.value, this.label, this.originalText, {} as MarkdownPostProcessorContext);
}
}
function buildCounterDecorations(view: EditorView, plugin: CounterPlugin): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const counterRegex = /~\s*\(\s*(\d*)\s*\)\s*(.+)/g;
for (let { from, to } of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to);
let match;
counterRegex.lastIndex = 0;
while ((match = counterRegex.exec(text)) !== null) {
const startPos = from + match.index;
const endPos = startPos + match[0].length;
const value = match[1] === '' ? 0 : parseInt(match[1], 10);
const label = match[2].trim();
const originalText = match[0];
const widget = Decoration.replace({
widget: new CounterWidget(value, label, originalText, plugin),
});
builder.add(startPos, endPos, widget);
}
}
return builder.finish();
}
export default class CounterPlugin extends Plugin { export default class CounterPlugin extends Plugin {
async onload() { async onload() {
@@ -8,14 +47,26 @@ export default class CounterPlugin extends Plugin {
this.processCounters(element, context); this.processCounters(element, context);
}); });
this.registerEvent( const plugin = this;
this.app.workspace.on('editor-change', () => { this.registerEditorExtension(
const view = this.app.workspace.getActiveViewOfType(MarkdownView); ViewPlugin.fromClass(
if (view) { class {
// Refresh the preview to update counters decorations: DecorationSet;
view.previewMode.rerender(true);
constructor(view: EditorView) {
this.decorations = buildCounterDecorations(view, plugin);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = buildCounterDecorations(update.view, plugin);
}
}
},
{
decorations: (v) => v.decorations,
} }
}) )
); );
} }
@@ -24,51 +75,71 @@ export default class CounterPlugin extends Plugin {
} }
processCounters(element: HTMLElement, context: MarkdownPostProcessorContext) { processCounters(element: HTMLElement, context: MarkdownPostProcessorContext) {
const counterRegex = /^~\s*\(\s*(\d*)\s*\)\s*(.*)$/; const counterRegex = /~\s*\(\s*(\d*)\s*\)\s*(.+)/g;
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
const nodesToReplace: Array<{ node: Node; parent: Node }> = []; const nodesToReplace: Array<{ node: Node; parent: Node; replacements: Array<{type: 'text' | 'counter', content: string, value?: number, label?: string, originalText?: string}> }> = [];
let node: Node | null; let node: Node | null;
while ((node = walker.nextNode()) !== null) { while ((node = walker.nextNode()) !== null) {
const text = node.textContent || ''; const text = node.textContent || '';
const lines = text.split('\n');
lines.forEach((line, index) => { if (counterRegex.test(text)) {
const match = line.match(counterRegex); counterRegex.lastIndex = 0;
if (match && node) { const replacements: Array<{type: 'text' | 'counter', content: string, value?: number, label?: string, originalText?: string}> = [];
nodesToReplace.push({ node, parent: node.parentNode! }); let lastIndex = 0;
} let match;
});
}
nodesToReplace.forEach(({ node, parent }) => { while ((match = counterRegex.exec(text)) !== null) {
const text = node.textContent || ''; if (match.index > lastIndex) {
const lines = text.split('\n'); replacements.push({
const fragment = document.createDocumentFragment(); type: 'text',
content: text.substring(lastIndex, match.index)
});
}
lines.forEach((line, index) => { const value = match[1] === '' ? 0 : parseInt(match[1], 10);
const match = line.match(counterRegex);
if (match) {
const currentValue = match[1] === '' ? 0 : parseInt(match[1], 10);
const label = match[2].trim(); const label = match[2].trim();
const counterContainer = this.createCounterElement( replacements.push({
currentValue, type: 'counter',
label, content: match[0],
node, value: value,
context label: label,
); originalText: match[0]
});
fragment.appendChild(counterContainer); lastIndex = match.index + match[0].length;
} else {
if (line) {
fragment.appendChild(document.createTextNode(line));
}
} }
if (index < lines.length - 1) { if (lastIndex < text.length) {
fragment.appendChild(document.createTextNode('\n')); replacements.push({
type: 'text',
content: text.substring(lastIndex)
});
}
if (replacements.length > 0) {
nodesToReplace.push({ node, parent: node.parentNode!, replacements });
}
counterRegex.lastIndex = 0;
}
}
nodesToReplace.forEach(({ node, parent, replacements }) => {
const fragment = document.createDocumentFragment();
replacements.forEach(replacement => {
if (replacement.type === 'counter') {
const counterContainer = this.createCounterElement(
replacement.value!,
replacement.label!,
replacement.originalText!,
context
);
fragment.appendChild(counterContainer);
} else {
fragment.appendChild(document.createTextNode(replacement.content));
} }
}); });
@@ -77,14 +148,16 @@ export default class CounterPlugin extends Plugin {
} }
createCounterElement( createCounterElement(
value: number, initialValue: number,
label: string, label: string,
sourceNode: Node, originalText: string,
context: MarkdownPostProcessorContext context: MarkdownPostProcessorContext
): HTMLElement { ): HTMLElement {
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'counter-container'; container.className = 'counter-container';
let currentValue = initialValue;
const minusButton = document.createElement('button'); const minusButton = document.createElement('button');
minusButton.className = 'counter-button counter-minus'; minusButton.className = 'counter-button counter-minus';
minusButton.textContent = ''; minusButton.textContent = '';
@@ -92,7 +165,7 @@ export default class CounterPlugin extends Plugin {
const counterDisplay = document.createElement('span'); const counterDisplay = document.createElement('span');
counterDisplay.className = 'counter-display'; counterDisplay.className = 'counter-display';
counterDisplay.textContent = value.toString(); counterDisplay.textContent = currentValue.toString();
const plusButton = document.createElement('button'); const plusButton = document.createElement('button');
plusButton.className = 'counter-button counter-plus'; plusButton.className = 'counter-button counter-plus';
@@ -109,37 +182,30 @@ export default class CounterPlugin extends Plugin {
const editor = view.editor; const editor = view.editor;
const content = editor.getValue(); const content = editor.getValue();
const counterRegex = /^~\s*\(\s*\d*\s*\)\s*(.*)$/gm;
let matchIndex = 0; // Find and replace only the first occurrence of this exact counter text
const newContent = content.replace(counterRegex, (match, capturedLabel) => { const index = content.indexOf(originalText);
const currentLabel = capturedLabel.trim(); if (index !== -1) {
if (currentLabel === label) { const newText = `~ (${newValue}) ${label}`;
matchIndex++; const newContent = content.substring(0, index) + newText + content.substring(index + originalText.length);
return `~ (${newValue}) ${label}`;
}
return match;
});
if (content !== newContent) {
editor.setValue(newContent); editor.setValue(newContent);
} }
}; };
minusButton.addEventListener('click', () => { minusButton.addEventListener('click', () => {
const newValue = value - 1; currentValue = currentValue - 1;
counterDisplay.textContent = newValue.toString(); counterDisplay.textContent = currentValue.toString();
updateSource(newValue); updateSource(currentValue);
}); });
plusButton.addEventListener('click', () => { plusButton.addEventListener('click', () => {
const newValue = value + 1; currentValue = currentValue + 1;
counterDisplay.textContent = newValue.toString(); counterDisplay.textContent = currentValue.toString();
updateSource(newValue); updateSource(currentValue);
}); });
container.appendChild(minusButton);
container.appendChild(counterDisplay); container.appendChild(counterDisplay);
container.appendChild(minusButton);
container.appendChild(plusButton); container.appendChild(plusButton);
if (label) { if (label) {
container.appendChild(labelSpan); container.appendChild(labelSpan);

View File

@@ -1,29 +1,31 @@
.counter-container { .counter-container {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 4px;
padding: 4px 8px; padding: 2px 6px;
background-color: var(--background-secondary); background-color: var(--background-secondary);
border-radius: 6px; border-radius: 4px;
margin: 4px 0; margin: 2px 0;
font-family: var(--font-interface); font-family: var(--font-interface);
vertical-align: middle;
} }
.counter-button { .counter-button {
width: 24px; width: 16px;
height: 24px; height: 16px;
padding: 0; padding: 0;
border: 1px solid var(--background-modifier-border); border: 1px solid var(--background-modifier-border);
background-color: var(--interactive-normal); background-color: var(--interactive-normal);
color: var(--text-normal); color: var(--text-normal);
border-radius: 4px; border-radius: 3px;
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 12px;
font-weight: bold; font-weight: bold;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.2s ease; transition: all 0.15s ease;
line-height: 1;
} }
.counter-button:hover { .counter-button:hover {
@@ -34,21 +36,21 @@
.counter-button:active { .counter-button:active {
background-color: var(--interactive-accent); background-color: var(--interactive-accent);
color: var(--text-on-accent); color: var(--text-on-accent);
transform: scale(0.95); transform: scale(0.9);
} }
.counter-display { .counter-display {
min-width: 32px; min-width: 20px;
text-align: center; text-align: center;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 13px;
color: var(--text-normal); color: var(--text-normal);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.counter-label { .counter-label {
margin-left: 4px; margin-left: 2px;
color: var(--text-muted); color: var(--text-normal);
font-size: 14px; font-size: 14px;
} }