11import { App , TFile } from "obsidian" ;
22import {
3+ ANKI_LINK_MODEL_NAME ,
34 ANKI_LINK_TAG ,
45 Note ,
56 addTagToNotes ,
@@ -12,7 +13,11 @@ import {
1213 noteIsInDeck ,
1314 sendAddNoteRequest ,
1415 sendCreateDeckRequest ,
16+ sendCreateModelRequest ,
1517 sendDeckNamesRequest ,
18+ sendModelNamesRequest ,
19+ sendUpdateModelStylingRequest ,
20+ sendUpdateModelTemplatesRequest ,
1621 updateNoteById ,
1722} from "./ankiConnectUtil" ;
1823import { FC_PREAMBLE_P } from "./regexUtil" ;
@@ -55,6 +60,7 @@ export async function syncVaultNotes(app: App): Promise<SyncSummary> {
5560 }
5661
5762 await ensureDecksExist ( decksInUse ) ;
63+ await ensureModelIsConfigured ( ) ;
5864 const taggedNoteIdsAtStart = new Set ( await findNoteIdsByTag ( ANKI_LINK_TAG ) ) ;
5965
6066 let totalAdded = 0 ;
@@ -153,6 +159,19 @@ async function ensureDecksExist(deckNames: Set<string>) {
153159 }
154160}
155161
162+ async function ensureModelIsConfigured ( ) {
163+ const modelNamesRes = await sendModelNamesRequest ( ) ;
164+ if ( modelNamesRes . error ) throw new Error ( `AnkiConnect: ${ modelNamesRes . error } ` ) ;
165+ if ( ! modelNamesRes . result . includes ( ANKI_LINK_MODEL_NAME ) ) {
166+ const createModelRes = await sendCreateModelRequest ( ANKI_LINK_MODEL_NAME ) ;
167+ if ( createModelRes . error ) throw new Error ( `AnkiConnect: ${ createModelRes . error } ` ) ;
168+ }
169+ const updateTemplatesRes = await sendUpdateModelTemplatesRequest ( ANKI_LINK_MODEL_NAME ) ;
170+ if ( updateTemplatesRes . error ) throw new Error ( `AnkiConnect: ${ updateTemplatesRes . error } ` ) ;
171+ const updateStylingRes = await sendUpdateModelStylingRequest ( ANKI_LINK_MODEL_NAME ) ;
172+ if ( updateStylingRes . error ) throw new Error ( `AnkiConnect: ${ updateStylingRes . error } ` ) ;
173+ }
174+
156175function parseDocument ( lines : string [ ] , deckName : string ) : ParsedNoteData [ ] {
157176 const output = new Array < ParsedNoteData > ( ) ;
158177 let i = 0 ;
@@ -164,14 +183,117 @@ function parseDocument(lines: string[], deckName: string): ParsedNoteData[] {
164183 }
165184
166185 const bodyLines = parseBody ( lines . slice ( i + 1 ) ) ;
167- const body = bodyLines . join ( "<br>" ) ;
186+ const body = formatBodyForAnki ( bodyLines ) ;
168187 const note = buildNote ( title , body , deckName ) ;
169188 output . push ( { id : id ? Number ( id ) : undefined , index : i , note } ) ;
170189 i += bodyLines . length + 1 ;
171190 }
172191 return output ;
173192}
174193
194+ type BodyToken =
195+ | { type : "text" ; raw : string }
196+ | { type : "fence" ; raw : string ; marker : "```" | "~~~" ; info : string } ;
197+
198+ type BodySegment =
199+ | { type : "text" ; lines : string [ ] }
200+ | { type : "code" ; language : string ; code : string } ;
201+
202+ function formatBodyForAnki ( lines : string [ ] ) : string {
203+ const tokens = lexBody ( lines ) ;
204+ const segments = parseBodyTokens ( tokens ) ;
205+ return renderBodySegments ( segments ) ;
206+ }
207+
208+ function lexBody ( lines : string [ ] ) : BodyToken [ ] {
209+ return lines . map ( ( line ) => lexLine ( line ) ) ;
210+ }
211+
212+ function lexLine ( line : string ) : BodyToken {
213+ const trimmed = line . trim ( ) ;
214+ if ( trimmed . startsWith ( "```" ) ) {
215+ return { type : "fence" , raw : line , marker : "```" , info : trimmed . slice ( 3 ) . trim ( ) } ;
216+ }
217+ if ( trimmed . startsWith ( "~~~" ) ) {
218+ return { type : "fence" , raw : line , marker : "~~~" , info : trimmed . slice ( 3 ) . trim ( ) } ;
219+ }
220+ return { type : "text" , raw : line } ;
221+ }
222+
223+ function parseBodyTokens ( tokens : BodyToken [ ] ) : BodySegment [ ] {
224+ const segments : BodySegment [ ] = [ ] ;
225+ const textBuffer : string [ ] = [ ] ;
226+
227+ const flushText = ( ) => {
228+ if ( textBuffer . length === 0 ) return ;
229+ segments . push ( { type : "text" , lines : [ ...textBuffer ] } ) ;
230+ textBuffer . length = 0 ;
231+ } ;
232+
233+ let i = 0 ;
234+ while ( i < tokens . length ) {
235+ const token = tokens [ i ] ! ;
236+ if ( token . type !== "fence" ) {
237+ textBuffer . push ( token . raw ) ;
238+ i ++ ;
239+ continue ;
240+ }
241+
242+ const closingFenceIdx = findClosingFenceToken ( tokens , i + 1 , token . marker ) ;
243+ if ( closingFenceIdx === - 1 ) {
244+ // Keep unmatched fences as regular text to avoid dropping content.
245+ textBuffer . push ( token . raw ) ;
246+ i ++ ;
247+ continue ;
248+ }
249+
250+ flushText ( ) ;
251+ const code = tokens . slice ( i + 1 , closingFenceIdx ) . map ( ( currentToken ) => currentToken . raw ) . join ( "\n" ) ;
252+ segments . push ( { type : "code" , language : token . info , code } ) ;
253+ i = closingFenceIdx + 1 ;
254+ }
255+
256+ flushText ( ) ;
257+ return segments ;
258+ }
259+
260+ function findClosingFenceToken ( tokens : BodyToken [ ] , startIdx : number , marker : "```" | "~~~" ) : number {
261+ for ( let i = startIdx ; i < tokens . length ; i ++ ) {
262+ const token = tokens [ i ] ! ;
263+ if ( token . type === "fence" && token . marker === marker && token . info . length === 0 ) {
264+ return i ;
265+ }
266+ }
267+ return - 1 ;
268+ }
269+
270+ function renderBodySegments ( segments : BodySegment [ ] ) : string {
271+ return segments . map ( ( segment ) => renderSegment ( segment ) ) . join ( "<br>" ) ;
272+ }
273+
274+ function renderSegment ( segment : BodySegment ) : string {
275+ if ( segment . type === "text" ) {
276+ return segment . lines . join ( "<br>" ) ;
277+ }
278+ const languageClass = segment . language . length > 0 ? ` class="language-${ escapeHtmlAttribute ( segment . language ) } "` : "" ;
279+ return `<pre><code${ languageClass } >${ escapeHtml ( segment . code ) } </code></pre>` ;
280+ }
281+
282+ function escapeHtml ( value : string ) : string {
283+ return value
284+ . split ( "&" ) . join ( "&" )
285+ . split ( "<" ) . join ( "<" )
286+ . split ( ">" ) . join ( ">" ) ;
287+ }
288+
289+ function escapeHtmlAttribute ( value : string ) : string {
290+ return value
291+ . split ( "&" ) . join ( "&" )
292+ . split ( "\"" ) . join ( """ )
293+ . split ( "<" ) . join ( "<" )
294+ . split ( ">" ) . join ( ">" ) ;
295+ }
296+
175297function parseBody ( lines : string [ ] ) {
176298 const bodyLines : string [ ] = [ ] ;
177299 for ( const line of lines ) {
0 commit comments