Skip to content

Commit b213d8f

Browse files
Move sync stuff into syncUtil
1 parent 44f9b13 commit b213d8f

File tree

2 files changed

+144
-173
lines changed

2 files changed

+144
-173
lines changed

src/main.ts

Lines changed: 4 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
1-
import { Editor, Notice, Plugin, TFile } from "obsidian";
1+
import { Editor, Notice, Plugin } from "obsidian";
22
import {
33
DEFAULT_SETTINGS,
44
AnkiLinkSettings,
55
AnkiLinkSettingsTab,
66
} from "./settings";
7-
import { TARGET_DECK, sendAddNoteRequest, buildNote, sendCreateDeckRequest, sendDeckNamesRequest, Note, getNoteById, updateNoteById } from "./ankiConnectUtil";
8-
import { FC_PREAMBLE_P } from "./regexUtil";
9-
10-
interface ParsedNoteData {
11-
id: number | undefined,
12-
index: number,
13-
note: Note
14-
}
15-
16-
interface NoteSyncResult {
17-
added: number;
18-
linesModified: boolean;
19-
lines: string[];
20-
}
7+
import { syncVaultNotes } from "./syncUtil";
218

229
export default class AnkiLink extends Plugin {
2310
settings!: AnkiLinkSettings;
@@ -31,7 +18,7 @@ export default class AnkiLink extends Plugin {
3118
"Sample",
3219
(evt: MouseEvent) => {
3320
// Called when the user clicks the icon.
34-
const numStr = this.syncNotes().then((n) => n.toString());
21+
const numStr = syncVaultNotes(this.app).then((n) => n.toString());
3522
numStr.then(
3623
(n) => new Notice(n),
3724
(e) => console.error(e),
@@ -44,7 +31,7 @@ export default class AnkiLink extends Plugin {
4431
id: "sync-cards",
4532
name: "Sync cards",
4633
callback: async () => {
47-
const added = await this.syncNotes();
34+
const added = await syncVaultNotes(this.app);
4835
new Notice(`Synced flashcards. Added ${added} note${added === 1 ? "" : "s"}.`);
4936
},
5037
});
@@ -76,162 +63,6 @@ export default class AnkiLink extends Plugin {
7663
await this.saveData(this.settings);
7764
}
7865

79-
/**
80-
* Sync all notes from all markdown files in the vault.
81-
* @returns The number of notes added
82-
*/
83-
async syncNotes(): Promise<number> {
84-
const markdownFiles = this.app.vault.getMarkdownFiles();
85-
await this.addMissingDecks();
86-
87-
let totalAdded = 0;
88-
for (const file of markdownFiles) {
89-
const added = await this.syncSingleFile(file);
90-
totalAdded += added;
91-
}
92-
return totalAdded;
93-
}
94-
95-
/**
96-
* Sync all notes from a document. Updates the document lines to include the new note IDs.
97-
* @param file The document to sync
98-
* @returns The number of notes added
99-
*/
100-
private async syncSingleFile(file: TFile): Promise<number> {
101-
const originalLines = (await this.app.vault.read(file)).split("\n");
102-
const notesData = this.parseDocument(originalLines);
103-
if (notesData.length === 0) return 0;
104-
105-
let totalAdded = 0;
106-
let linesModified = false;
107-
let lines = originalLines;
108-
for (const noteData of notesData) {
109-
const result = await this.syncSingleNote(noteData, lines);
110-
totalAdded += result.added;
111-
linesModified = linesModified || result.linesModified;
112-
lines = result.lines;
113-
}
114-
115-
if (linesModified) {
116-
await this.app.vault.modify(file, lines.join("\n"));
117-
}
118-
return totalAdded;
119-
}
120-
121-
/**
122-
* Sync a single extracted note from a document.
123-
* @param noteData The note data to sync
124-
* @param lines The lines of the document
125-
* @returns The note sync result
126-
*/
127-
private async syncSingleNote(noteData: ParsedNoteData, lines: string[]): Promise<NoteSyncResult> {
128-
if (noteData.id == undefined) {
129-
return this.createAndWriteNoteId(noteData, lines);
130-
}
131-
132-
const ankiNote = await getNoteById(noteData.id);
133-
if (!ankiNote) {
134-
// Missing note for this ID in Anki (notesInfo returned [] or [{}]). Recreate it.
135-
return this.createAndWriteNoteId(noteData, lines);
136-
}
137-
138-
const obsidianFields = noteData.note.fields;
139-
const ankiFields = ankiNote.fields;
140-
if (obsidianFields.Front !== ankiFields.Front.value || obsidianFields.Back !== ankiFields.Back.value) {
141-
await updateNoteById(ankiNote.noteId, obsidianFields);
142-
}
143-
return { added: 0, linesModified: false, lines };
144-
}
145-
146-
/**
147-
* Create a new note in Anki and update document lines to include the new note ID.
148-
* @param noteData The note data to create
149-
* @param lines The lines of the document
150-
* @returns The note sync result
151-
*/
152-
private async createAndWriteNoteId(noteData: ParsedNoteData, lines: string[]): Promise<NoteSyncResult> {
153-
const newId = await this.sendNote(noteData.note);
154-
const updatedLines = [...lines];
155-
updatedLines[noteData.index] = `> [!flashcard] %%${newId}%% ${noteData.note.fields.Front}`;
156-
return { added: 1, linesModified: true, lines: updatedLines };
157-
}
158-
159-
/**
160-
* Check if the target deck exists in Anki and create it if it doesn't.
161-
*/
162-
private async addMissingDecks() {
163-
const deckNamesRes = await sendDeckNamesRequest();
164-
if (deckNamesRes.error) throw new Error(`AnkiConnect: ${deckNamesRes.error}`)
165-
const decks = deckNamesRes.result;
166-
if (!decks.includes(TARGET_DECK)) {
167-
const createDeckRes = await sendCreateDeckRequest(TARGET_DECK);
168-
if (createDeckRes.error) throw new Error(`AnkiConnect: ${createDeckRes.error}`)
169-
}
170-
}
171-
172-
/**
173-
* Read flashcard data from an obsidian document.
174-
* @param lines Plain text lines from an Obsidian document
175-
* @returns Flashcard data
176-
*/
177-
private parseDocument(lines: string[]): ParsedNoteData[] {
178-
const output = new Array<ParsedNoteData>();
179-
let i = 0;
180-
while (i < lines.length) {
181-
const { id, title } = this.parsePreamble(lines[i]!) || {};
182-
if (!title) {
183-
i++;
184-
continue;
185-
}
186-
187-
const bodyLines = this.parseBody(lines.slice(i + 1));
188-
const body = bodyLines.join("<br>");
189-
const note = buildNote(title, body);
190-
output.push({ id: id ? Number(id) : undefined, index: i, note });
191-
i += bodyLines.length + 1;
192-
}
193-
return output;
194-
}
195-
196-
/**
197-
* Read a flashcard body from an array of plaintext lines.
198-
* @param lines Plain text lines, starting after a flashcard title line and continuing indefinitely
199-
* @returns The text content of each flashcard body line with the leading > and whitespace removed
200-
*/
201-
private parseBody(lines: string[]) {
202-
const bodyLines: string[] = [];
203-
for (const line of lines) {
204-
// Stop when we reach the next flashcard preamble.
205-
if (this.parsePreamble(line)) {
206-
return bodyLines;
207-
}
208-
if (!line.startsWith(">")) {
209-
return bodyLines;
210-
}
211-
bodyLines.push(line.replace(/^>\s?/, ""));
212-
}
213-
return bodyLines;
214-
}
215-
216-
/**
217-
* Read a flashcard title line and preamble.
218-
* @param str A flashcard title line, including flashcard callout and optionally id comment
219-
* @returns A title and optionally an id
220-
*/
221-
private parsePreamble(str: string) {
222-
const match = FC_PREAMBLE_P.exec(str);
223-
if (!match) {
224-
return undefined
225-
}
226-
return { id: match[1], title: match[2]!}
227-
}
228-
229-
private async sendNote(note: Note): Promise<number> {
230-
const res = await sendAddNoteRequest(note);
231-
if (res.error) throw new Error(`AnkiConnect ${res.error}`);
232-
return res.result;
233-
}
234-
23566
private insertFlashcard(editor: Editor) {
23667
const template = "> [!flashcard] ";
23768
editor.replaceSelection(template);

src/syncUtil.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { App, TFile } from "obsidian";
2+
import {
3+
Note,
4+
TARGET_DECK,
5+
buildNote,
6+
getNoteById,
7+
sendAddNoteRequest,
8+
sendCreateDeckRequest,
9+
sendDeckNamesRequest,
10+
updateNoteById,
11+
} from "./ankiConnectUtil";
12+
import { FC_PREAMBLE_P } from "./regexUtil";
13+
14+
interface ParsedNoteData {
15+
id: number | undefined;
16+
index: number;
17+
note: Note;
18+
}
19+
20+
interface NoteSyncResult {
21+
added: number;
22+
linesModified: boolean;
23+
lines: string[];
24+
}
25+
26+
export async function syncVaultNotes(app: App): Promise<number> {
27+
const markdownFiles = app.vault.getMarkdownFiles();
28+
await addMissingDecks();
29+
30+
let totalAdded = 0;
31+
for (const file of markdownFiles) {
32+
totalAdded += await syncSingleFile(app, file);
33+
}
34+
return totalAdded;
35+
}
36+
37+
async function syncSingleFile(app: App, file: TFile): Promise<number> {
38+
const originalLines = (await app.vault.read(file)).split("\n");
39+
const notesData = parseDocument(originalLines);
40+
if (notesData.length === 0) return 0;
41+
42+
let totalAdded = 0;
43+
let linesModified = false;
44+
let lines = originalLines;
45+
for (const noteData of notesData) {
46+
const result = await syncSingleNote(noteData, lines);
47+
totalAdded += result.added;
48+
linesModified = linesModified || result.linesModified;
49+
lines = result.lines;
50+
}
51+
52+
if (linesModified) {
53+
await app.vault.modify(file, lines.join("\n"));
54+
}
55+
return totalAdded;
56+
}
57+
58+
async function syncSingleNote(noteData: ParsedNoteData, lines: string[]): Promise<NoteSyncResult> {
59+
if (noteData.id == undefined) {
60+
return createAndWriteNoteId(noteData, lines);
61+
}
62+
63+
const ankiNote = await getNoteById(noteData.id);
64+
if (!ankiNote) {
65+
// Missing note for this ID in Anki (notesInfo returned [] or [{}]). Recreate it.
66+
return createAndWriteNoteId(noteData, lines);
67+
}
68+
69+
const obsidianFields = noteData.note.fields;
70+
const ankiFields = ankiNote.fields;
71+
if (obsidianFields.Front !== ankiFields.Front.value || obsidianFields.Back !== ankiFields.Back.value) {
72+
await updateNoteById(ankiNote.noteId, obsidianFields);
73+
}
74+
return { added: 0, linesModified: false, lines };
75+
}
76+
77+
async function createAndWriteNoteId(noteData: ParsedNoteData, lines: string[]): Promise<NoteSyncResult> {
78+
const newId = await sendNote(noteData.note);
79+
const updatedLines = [...lines];
80+
updatedLines[noteData.index] = `> [!flashcard] %%${newId}%% ${noteData.note.fields.Front}`;
81+
return { added: 1, linesModified: true, lines: updatedLines };
82+
}
83+
84+
async function addMissingDecks() {
85+
const deckNamesRes = await sendDeckNamesRequest();
86+
if (deckNamesRes.error) throw new Error(`AnkiConnect: ${deckNamesRes.error}`);
87+
const decks = deckNamesRes.result;
88+
if (!decks.includes(TARGET_DECK)) {
89+
const createDeckRes = await sendCreateDeckRequest(TARGET_DECK);
90+
if (createDeckRes.error) throw new Error(`AnkiConnect: ${createDeckRes.error}`);
91+
}
92+
}
93+
94+
function parseDocument(lines: string[]): ParsedNoteData[] {
95+
const output = new Array<ParsedNoteData>();
96+
let i = 0;
97+
while (i < lines.length) {
98+
const { id, title } = parsePreamble(lines[i]!) || {};
99+
if (!title) {
100+
i++;
101+
continue;
102+
}
103+
104+
const bodyLines = parseBody(lines.slice(i + 1));
105+
const body = bodyLines.join("<br>");
106+
const note = buildNote(title, body);
107+
output.push({ id: id ? Number(id) : undefined, index: i, note });
108+
i += bodyLines.length + 1;
109+
}
110+
return output;
111+
}
112+
113+
function parseBody(lines: string[]) {
114+
const bodyLines: string[] = [];
115+
for (const line of lines) {
116+
// Stop when we reach the next flashcard preamble.
117+
if (parsePreamble(line)) {
118+
return bodyLines;
119+
}
120+
if (!line.startsWith(">")) {
121+
return bodyLines;
122+
}
123+
bodyLines.push(line.replace(/^>\s?/, ""));
124+
}
125+
return bodyLines;
126+
}
127+
128+
function parsePreamble(str: string) {
129+
const match = FC_PREAMBLE_P.exec(str);
130+
if (!match) {
131+
return undefined;
132+
}
133+
return { id: match[1], title: match[2]! };
134+
}
135+
136+
async function sendNote(note: Note): Promise<number> {
137+
const res = await sendAddNoteRequest(note);
138+
if (res.error) throw new Error(`AnkiConnect ${res.error}`);
139+
return res.result;
140+
}

0 commit comments

Comments
 (0)