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