feat: Exact URL backlinking via Obsidian URI custom protocol

This commit is contained in:
lukmay 2026-03-01 11:48:42 +01:00
parent 5f4ed17569
commit 79587eb2ec
4 changed files with 47 additions and 9 deletions

View file

@ -134,10 +134,10 @@ This removes all Anki note references from the file and deletes the correspondin
Embed `.excalidraw` drawings seamlessly. They are automatically converted to images on sync.
- **Math auto-formatting**
Dollar-delimited LaTeX in Obsidian becomes nicely rendered in Anki.
- **Automatic deletion**
- [x] Automatic deletion
Delete a card in Obsidian and the corresponding Anki card is removed on sync. **IMPORTANT: don't delete the Anki-ID below manually**
- **Backlinks**
Each card carries a URL back to its source note for easy context retrieval.
- **Precise Backlinks**
Each card carries a URL back to its source note. Clicking it from Anki will open Obsidian and scroll you down directly to the flashcard's exact row!
- **Anki-Tags**
Each card in Anki has a assigned tag `obsidian_simple_anki_sync_created` to easely filter for this automatically created cards.

View file

@ -1,7 +1,7 @@
{
"id": "simple-anki-sync",
"name": "Simple Anki Sync",
"version": "1.5.0",
"version": "1.5.1",
"minAppVersion": "0.15.0",
"description": "The simplest way of syncing simple Flashchards with Anki.",
"author": "Lukas Mayr",

View file

@ -1,6 +1,6 @@
{
"name": "simple-anki-sync",
"version": "1.5.0",
"version": "1.5.1",
"description": "Personal Anki Sync Plugin for Obsidian.",
"main": "main.js",
"scripts": {

View file

@ -1,4 +1,4 @@
import { Plugin, TFile, MarkdownView, Notice, arrayBufferToBase64 } from 'obsidian';
import { Plugin, TFile, MarkdownView, Notice, arrayBufferToBase64, ObsidianProtocolData } from 'obsidian';
import { AnkiService } from './anki-service';
import { ObsidianNote, ProcessedMediaResult } from './types';
import { DEFAULT_SETTINGS, SimpleAnkiSyncSettingTab, SimpleAnkiSyncSettings } from './settings';
@ -114,6 +114,40 @@ export default class SimpleAnkiSyncPlugin extends Plugin {
return;
},
});
this.registerObsidianProtocolHandler('simple-anki-sync', this.handleCustomProtocol.bind(this));
}
private async handleCustomProtocol(params: ObsidianProtocolData) {
if (params.action === 'simple-anki-sync') {
const { file, noteId } = params;
if (!file || !noteId) return;
const abstractFile = this.app.vault.getAbstractFileByPath(file);
if (!(abstractFile instanceof TFile)) return;
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(abstractFile);
const content = await this.app.vault.read(abstractFile);
const lines = content.split('\n');
const searchFor = `<!--ANKI_NOTE_ID:${noteId}-->`;
const lineNumber = lines.findIndex(line => line.includes(searchFor));
if (lineNumber !== -1) {
// Find where the trailing Anki comment or `<br>` tags start, so we can place the cursor exactly there
const line = lines[lineNumber];
const match = line.match(/(?:<br\s*\/?>\s*)*<!--ANKI_NOTE_ID:\d+-->/i);
const column = match ? match.index || 0 : 0;
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.editor) { // only if in edit mode
view.editor.setCursor(lineNumber, column);
view.editor.scrollIntoView({ from: { line: lineNumber, ch: 0 }, to: { line: lineNumber, ch: 0 } }, true);
}
}
}
}
onunload() {
@ -472,9 +506,13 @@ export default class SimpleAnkiSyncPlugin extends Plugin {
// Obsidian-Link
const vault = this.app.vault.getName();
const url = `obsidian://open?vault=${encodeURIComponent(vault)}&file=${encodeURIComponent(
file.path
)}`;
let url = '';
if (note.noteId) {
url = `obsidian://simple-anki-sync?vault=${encodeURIComponent(vault)}&file=${encodeURIComponent(file.path)}&noteId=${note.noteId}`;
} else {
url = `obsidian://open?vault=${encodeURIComponent(vault)}&file=${encodeURIComponent(file.path)}`;
}
backHtml += `<br><small><a href="${url}" style="text-decoration:none;color:grey;font-size:0.8em;">Obsidian Note</a></small>`;
const fields = { Front: frontHtml, Back: backHtml };