commit 9295e8055af22c26f9cc9ba1603f3de3a6976520 Author: Sayuop Date: Wed Nov 26 10:14:58 2025 +0100 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 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..cc8e03b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(git init:*)", + "Bash(git add:*)", + "Bash(git commit:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2549c4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Node +node_modules +package-lock.json + +# Build +main.js +*.js.map + +# macOS +.DS_Store + +# IDE +.vscode +.idea + +# Obsidian +data.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc64846 --- /dev/null +++ b/README.md @@ -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 diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..e45242b --- /dev/null +++ b/esbuild.config.mjs @@ -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(); +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..64e3caf --- /dev/null +++ b/main.ts @@ -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; + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..67343a6 --- /dev/null +++ b/manifest.json @@ -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 +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2ec004a --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..55bad88 --- /dev/null +++ b/styles.css @@ -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; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c44b729 --- /dev/null +++ b/tsconfig.json @@ -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" + ] +}