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

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Node
node_modules
package-lock.json
# Build
main.js
*.js.map
# macOS
.DS_Store
# IDE
.vscode
.idea
# Obsidian
data.json

58
README.md Normal file
View File

@@ -0,0 +1,58 @@
# Counter Plugin for Obsidian
A simple Obsidian plugin that creates interactive number counters with +/- buttons.
## Usage
To create a counter, use the following syntax in your notes:
```
~ (0) counter label
```
This will render as an interactive counter starting at 0, with minus and plus buttons to decrease or increase the value.
## Examples
```
~ (0) Tasks completed
~ (5) Days streak
~ (100) Points earned
```
Each counter is independent and maintains its own value in your markdown file.
## Installation
### From Release
1. Download `main.js`, `manifest.json`, and `styles.css` from the latest release
2. Create a folder named `counter-plugin` in your vault's `.obsidian/plugins/` directory
3. Copy the downloaded files into this folder
4. Reload Obsidian
5. Enable the plugin in Settings → Community plugins
### Manual Installation
1. Clone this repository into your vault's `.obsidian/plugins/` directory
2. Run `npm install` to install dependencies
3. Run `npm run build` to build the plugin
4. Reload Obsidian
5. Enable the plugin in Settings → Community plugins
## Development
```bash
# Install dependencies
npm install
# Build the plugin
npm run build
# Development mode (auto-rebuild on changes)
npm run dev
```
## License
MIT

48
esbuild.config.mjs Normal file
View File

@@ -0,0 +1,48 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === 'production');
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ['main.ts'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins],
format: 'cjs',
target: 'es2018',
logLevel: "info",
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

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;
}
}

10
manifest.json Normal file
View File

@@ -0,0 +1,10 @@
{
"id": "counter-plugin",
"name": "Counter Plugin",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Create interactive counters with +/- buttons using ~ ( ) syntax",
"author": "crib",
"authorUrl": "https://git.cribdev.com/crib",
"isDesktopOnly": false
}

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "obsidian-counter-plugin",
"version": "1.0.0",
"description": "A simple counter plugin for Obsidian that creates interactive number counters with +/- buttons",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
"obsidian",
"plugin",
"counter"
],
"author": "crib",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

61
styles.css Normal file
View File

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

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
}