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:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal 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
58
README.md
Normal 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
48
esbuild.config.mjs
Normal 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
150
main.ts
Normal 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
10
manifest.json
Normal 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
28
package.json
Normal 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
61
styles.css
Normal 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
24
tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user