mirror of
https://github.com/munckenh/obsidian-relative-dates.git
synced 2026-07-22 06:43:16 +00:00
feat: add live preview support
This commit is contained in:
parent
8a762395df
commit
646edc1d99
14 changed files with 2590 additions and 139 deletions
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
23
.eslintrc
|
|
@ -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
14
.gitignore
vendored
|
|
@ -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
1
.npmrc
|
|
@ -1 +0,0 @@
|
|||
tag-version-prefix=""
|
||||
17
README.md
17
README.md
|
|
@ -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
42
eslint.config.mjs
Normal 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/**',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -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
2328
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
28
package.json
28
package.json
|
|
@ -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
72
src/extension.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
110
src/main.ts
110
src/main.ts
|
|
@ -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
75
src/utils.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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"));
|
||||
|
|
|
|||
Loading…
Reference in a new issue