Compare commits
4 Commits
5f5f2f63ba
...
bb4a6e5d0b
| Author | SHA1 | Date | |
|---|---|---|---|
| bb4a6e5d0b | |||
| cc9e9d4cba | |||
| 63258fa1f3 | |||
| 99ebde08c9 |
188
main.ts
188
main.ts
@@ -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);
|
||||||
|
|||||||
30
styles.css
30
styles.css
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user