feat: add live preview support

This commit is contained in:
Munckenh 2025-08-12 23:18:20 +08:00
parent 8a762395df
commit 646edc1d99
14 changed files with 2590 additions and 139 deletions

View file

@ -1,10 +0,0 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

View file

@ -1,3 +0,0 @@
node_modules/
main.js

View file

@ -1,23 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

14
.gitignore vendored
View file

@ -1,22 +1,22 @@
# vscode
# VS Code
.vscode
# Intellij
*.iml
.idea
# npm
# Dependency directories
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
# Don't include the compiled main.js file
# They should be uploaded to GitHub releases instead
main.js
# Exclude sourcemaps
# Sourcemaps
*.map
# obsidian
# Obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
# OS X
.DS_Store

1
.npmrc
View file

@ -1 +0,0 @@
tag-version-prefix=""

View file

@ -1,6 +1,21 @@
# Dynamic Date
A simple plugin for Obsidian that transforms date references in task lists into colored, dynamic, easy-to-read pills.
A simple plugin for Obsidian that transforms date references in task lists into color-coded, easy-to-read pills.
## Features
- Converts date references in the format `📅 YYYY-MM-DD` (with optional time `HH:MM`) into dynamic pills
- Works in both Reading mode and Live Preview mode
- Color-coded pills based on date proximity (overdue, today, tomorrow, this week, future)
- Customizable colors via settings
## Usage
Add dates in your task lists using the format:
- `📅 2025-08-12` for dates
- `📅 2025-08-12 14:30` for dates with time
The plugin will automatically convert these into easy-to-read pills like "Today", "Tomorrow", "Monday 2 PM", etc.
## Releasing new releases

42
eslint.config.mjs Normal file
View file

@ -0,0 +1,42 @@
import js from '@eslint/js';
import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser';
import globals from 'globals';
export default [
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsparser,
parserOptions: {
sourceType: 'module',
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
'@typescript-eslint': tseslint,
},
rules: {
...js.configs.recommended.rules,
...tseslint.configs.recommended.rules,
'no-trailing-spaces': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'none',
},
],
},
},
{
ignores: [
'main.js',
'node_modules/**',
'dist/**',
'build/**',
],
},
];

View file

@ -3,7 +3,7 @@
"name": "Dynamic Date",
"version": "0.1.0",
"minAppVersion": "1.8.0",
"description": "Converts date references in text into dynamic pills",
"description": "Dynamic date plugin for Obsidian",
"author": "Munckenh",
"authorUrl": "https://github.com/Munckenh",
"fundingUrl": "https://ko-fi.com/munckenh",

2328
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,36 @@
{
"name": "obsidian-dynamic-date",
"version": "0.1.0",
"description": "Converts date references in text into dynamic pills",
"description": "Dynamic date plugin for Obsidian",
"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"
"version": "node version-bump.mjs && git add manifest.json versions.json",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix"
},
"keywords": ["obsidian", "plugin", "date", "dynamic"],
"keywords": [
"obsidian",
"plugin",
"date",
"dynamic"
],
"author": "Munckenh",
"license": "MIT",
"devDependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@eslint/js": "^9.0.0",
"@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",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"builtin-modules": "^3.3.0",
"esbuild": "^0.25.8",
"eslint": "^9.0.0",
"globals": "^16.3.0",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
"typescript": "^5.3.3"
}
}

72
src/extension.ts Normal file
View file

@ -0,0 +1,72 @@
import { Decoration, DecorationSet, EditorView, PluginValue, ViewPlugin, ViewUpdate, WidgetType } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
import moment from 'moment';
import { DATE_REGEX, getRelativeText, getDateCategory, createDateElement } from './utils';
export class DynamicPillWidget extends WidgetType {
constructor(
private text: string,
private category: string,
private originalText: string
) {
super();
}
toDOM(view: EditorView): HTMLElement {
return createDateElement(this.text, this.category, this.originalText);
}
}
export class DateHighlightingPlugin implements PluginValue {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged || update.selectionSet) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const cursorPos = view.state.selection.main.head;
for (let { from, to } of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to);
const matches = text.matchAll(DATE_REGEX);
for (const match of matches) {
const matchStart = from + match.index!;
const matchEnd = matchStart + match[0].length;
const cursorInRange = cursorPos >= matchStart && cursorPos <= matchEnd;
if (!cursorInRange) {
const dateString = `${match[1]} ${match[2] || ''}`.trim();
const date = moment(dateString, 'YYYY-MM-DD HH:mm');
if (date.isValid()) {
const relativeText = getRelativeText(date);
const category = getDateCategory(date);
const decoration = Decoration.replace({
widget: new DynamicPillWidget(relativeText, category, match[0]),
});
builder.add(matchStart, matchEnd, decoration);
}
}
}
}
return builder.finish();
}
}
export const getDateHighlightingPlugin = () => {
return ViewPlugin.fromClass(DateHighlightingPlugin, {
decorations: (value: DateHighlightingPlugin) => value.decorations,
});
};

View file

@ -1,36 +1,13 @@
import { Plugin } from 'obsidian';
import { DynamicDateSettingTab } from './settings';
import moment from 'moment';
const DATE_REGEX = /📅\s*(\d{4}-\d{2}-\d{2})(?:\s*(\d{2}:\d{2}))?/g;
const DEFAULT_SETTINGS: DynamicDateOptions = {
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm',
pillColors: {
overdue: '#d1453b',
today: '#058527',
tomorrow: '#ad6200',
thisWeek: '#692ec2',
future: '#808080'
},
pillTextColor: '#ffffff'
};
interface DynamicDateOptions {
dateFormat: string;
timeFormat: string;
pillColors: {
overdue: string;
today: string;
tomorrow: string;
thisWeek: string;
future: string;
};
pillTextColor: string;
}
import { getDateHighlightingPlugin } from './extension';
import { DATE_REGEX, DEFAULT_SETTINGS, DynamicDateSettings, getRelativeText, getDateCategory, createDateElement } from './utils';
import { Extension } from '@codemirror/state';
export default class DynamicDatePlugin extends Plugin {
settings: DynamicDateOptions;
settings: DynamicDateSettings;
private editorExtensions: Extension[] = [];
async onload() {
await this.loadSettings();
@ -41,21 +18,34 @@ export default class DynamicDatePlugin extends Plugin {
this.processListItem(item as HTMLElement)
});
});
this.registerEditorExtension(this.editorExtensions);
}
async loadSettings() {
this.setEditorExtensions();
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.updateColor();
this.setPillColors();
}
async saveSettings() {
await this.saveData(this.settings);
this.updateColor();
this.setPillColors();
}
onunload() {}
onunload() {
const body = document.body;
body.style.removeProperty('--date-pill-overdue');
body.style.removeProperty('--date-pill-today');
body.style.removeProperty('--date-pill-tomorrow');
body.style.removeProperty('--date-pill-this-week');
body.style.removeProperty('--date-pill-future');
}
private updateColor(): void {
private setEditorExtensions() {
this.editorExtensions.push(getDateHighlightingPlugin());
}
private setPillColors(): void {
const body = document.body;
body.style.setProperty('--date-pill-overdue', this.settings.pillColors.overdue);
body.style.setProperty('--date-pill-today', this.settings.pillColors.today);
@ -74,14 +64,14 @@ export default class DynamicDatePlugin extends Plugin {
nodes.push(node);
}
}
nodes.forEach((node) => {
const fragment = document.createDocumentFragment();
const value = node.nodeValue || '';
let lastIndex = 0;
for (const match of value.matchAll(DATE_REGEX)) {
const matchIndex = match.index;
const matchIndex = match.index!;
const dateString = `${match[1]} ${match[2] || ''}`.trim();
const date = moment(dateString, 'YYYY-MM-DD HH:mm');
@ -90,9 +80,9 @@ export default class DynamicDatePlugin extends Plugin {
}
if (date.isValid()) {
fragment.appendChild(this.createDateElement(
this.getRelativeDateText(date),
this.getDateCategory(date),
fragment.appendChild(createDateElement(
getRelativeText(date),
getDateCategory(date),
dateString
));
} else {
@ -109,50 +99,4 @@ export default class DynamicDatePlugin extends Plugin {
node.parentNode!.replaceChild(fragment, node);
});
}
getRelativeDateText(date: moment.Moment): string {
const today = moment().startOf('day');
const tomorrow = moment().add(1, 'day').startOf('day');
const hasTime = date.minutes() !== 0 || date.hours() !== 0;
const timeString = hasTime ? date.minutes() === 0 ? ` ${date.format('h A')}` : ` ${date.format('h:mm A')}` : '';
if (date.isSame(today, 'day')) {
return `Today${timeString}`;
} else if (date.isSame(tomorrow, 'day')) {
return `Tomorrow${timeString}`;
} else if (date.isBetween(today, moment().add(7, 'days').endOf('day'), 'day')) {
return `${date.format('dddd')}${timeString}`;
} else if (date.year() === today.year()) {
return `${date.format('D MMM')}${timeString}`;
} else {
return `${date.format('D MMM YYYY')}${timeString}`;
}
}
getDateCategory(date: moment.Moment): string {
const today = moment().startOf('day');
const tomorrow = moment().add(1, 'day').startOf('day');
const sevenDaysFromNow = moment().add(7, 'days').endOf('day');
if (date.isBefore(today, 'day')) {
return 'overdue';
} else if (date.isSame(today, 'day')) {
return 'today';
} else if (date.isSame(tomorrow, 'day')) {
return 'tomorrow';
} else if (date.isBetween(tomorrow, sevenDaysFromNow, 'day')) {
return 'this-week';
}
return 'future';
}
createDateElement(text: string, category: string, originalDate?: string): HTMLElement {
const span = document.createElement('span');
span.textContent = text;
span.className = `date-pill date-pill-${category}`;
if (originalDate) {
span.title = originalDate;
}
return span;
}
}

75
src/utils.ts Normal file
View file

@ -0,0 +1,75 @@
import moment from 'moment';
export const DATE_REGEX = /📅\s*(\d{4}-\d{2}-\d{2})(?:\s*(\d{2}:\d{2}))?/g;
export const DEFAULT_SETTINGS: DynamicDateSettings = {
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm',
pillColors: {
overdue: '#d1453b',
today: '#058527',
tomorrow: '#ad6200',
thisWeek: '#692ec2',
future: '#808080'
},
pillTextColor: '#ffffff'
};
export interface DynamicDateSettings {
dateFormat: string;
timeFormat: string;
pillColors: {
overdue: string;
today: string;
tomorrow: string;
thisWeek: string;
future: string;
};
pillTextColor: string;
}
export function getRelativeText(date: moment.Moment): string {
const today = moment().startOf('day');
const tomorrow = moment().add(1, 'day').startOf('day');
const hasTime = date.minutes() !== 0 || date.hours() !== 0;
const timeString = hasTime ? date.minutes() === 0 ? ` ${date.format('h A')}` : ` ${date.format('h:mm A')}` : '';
if (date.isSame(today, 'day')) {
return `Today${timeString}`;
} else if (date.isSame(tomorrow, 'day')) {
return `Tomorrow${timeString}`;
} else if (date.isBetween(today, moment().add(7, 'days').endOf('day'), 'day')) {
return `${date.format('dddd')}${timeString}`;
} else if (date.year() === today.year()) {
return `${date.format('D MMM')}${timeString}`;
} else {
return `${date.format('D MMM YYYY')}${timeString}`;
}
}
export function getDateCategory(date: moment.Moment): string {
const today = moment().startOf('day');
const tomorrow = moment().add(1, 'day').startOf('day');
const sevenDaysFromNow = moment().add(7, 'days').endOf('day');
if (date.isBefore(today, 'day')) {
return 'overdue';
} else if (date.isSame(today, 'day')) {
return 'today';
} else if (date.isSame(tomorrow, 'day')) {
return 'tomorrow';
} else if (date.isBetween(tomorrow, sevenDaysFromNow, 'day')) {
return 'this-week';
}
return 'future';
}
export function createDateElement(text: string, category: string, originalText?: string): HTMLElement {
const span = document.createElement('span');
span.textContent = text;
span.className = `date-pill date-pill-${category}`;
if (originalText) {
span.title = originalText;
}
return span;
}

View file

@ -2,13 +2,13 @@ import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
// Read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
// Update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));