1- import { Editor , Notice , Plugin , TFile } from "obsidian" ;
1+ import { Editor , Notice , Plugin } from "obsidian" ;
22import {
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
229export 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 ) ;
0 commit comments