From 318ace114de9d59a773bf93872013c120acacc40 Mon Sep 17 00:00:00 2001 From: Stefan <42220813+Stefanuk12@users.noreply.github.com> Date: Sat, 21 Oct 2023 16:45:53 +0100 Subject: [PATCH 1/3] Initial commit --- src/AnkiParser.ts | 296 ++++++++++++++++++++++++++++++++++++ src/Question.ts | 1 + src/QuestionType.ts | 14 +- src/parser.ts | 11 ++ test.ts | 309 ++++++++++++++++++++++++++++++++++++++ test.zip | Bin 0 -> 2480 bytes tests/unit/parser.test.ts | 4 + 7 files changed, 633 insertions(+), 2 deletions(-) create mode 100644 src/AnkiParser.ts create mode 100644 test.ts create mode 100644 test.zip diff --git a/src/AnkiParser.ts b/src/AnkiParser.ts new file mode 100644 index 00000000..0e94c797 --- /dev/null +++ b/src/AnkiParser.ts @@ -0,0 +1,296 @@ +/* +Source / Inspiration: https://github.com/ankitects/anki/blob/80d807e08a6d3148f973829c48fe633a760546c5/rslib/src/cloze.rs + +The "BNF" for this. + + ::= [0-9]+ + ::= :: + ::= {{c + ::= }} + ::= .* + ::= | + ::= | | ?? + ::= ?? +*/ + +import { CardFrontBack, QuestionType_ClozeUtil } from "./QuestionType"; + +// The types of tokens. +enum TokenKind { + Open = "C_OPEN", + Close = "C_CLOSE", + Seperator = "SEPERATOR", + Text = "TEXT", +} + +interface OpenToken { + kind: TokenKind.Open; + ordinal: number; + i: number; +} +interface CloseToken { + kind: TokenKind.Close; + i: number; +} +interface SeperatorToken { + kind: TokenKind.Seperator; + i: number; +} +interface TextToken { + kind: TokenKind.Text; + text: string; + i: number; +} + +type Token = OpenToken | CloseToken | SeperatorToken | TextToken; + +// Returns the "Anki tokens" of a given text, given an anchor. +// This does not return the TEXT token. +function anki_tokens( + input: string, + i: number, +): OpenToken | CloseToken | SeperatorToken | undefined { + // The following statements are sorted high -> low `i`. + // Avoids any similar statements being mixed with each other. + + // C_OPEN + if (input.slice(i, i + 3) === "{{c") { + i += 3; + + // Extract the number + let num = ""; + while (input[i] !== ":") { + num += input[i]; + i++; + } + + // Attempt to parse the number + const number = parseInt(num); + if (isNaN(number)) return; + + // Return the object + return { + kind: TokenKind.Open, + ordinal: number, + i, + } satisfies OpenToken; + } + // SEPERATOR + else if (input.slice(i, i + 2) === "::") { + return { + kind: TokenKind.Seperator, + i: i + 2, + } satisfies SeperatorToken; + } + // C_CLOSE + else if (input.slice(i, i + 2) === "}}") { + return { + kind: TokenKind.Close, + i: i + 2, + } satisfies CloseToken; + } +} + +// Fully gets the "Anki tokens" of a given text. +// This does return the TEXT token. +// NOTE: This function does not error and does not check for invalid syntax +function text_anki_tokens(input: string): Token[] { + // Vars + const tokens: Token[] = []; + let i = 0; + let buf_start; + + // Loop through the input + while (i < input.length) { + // Grab the token at position `i` + const token = anki_tokens(input, i); + if (!token) { + // No token found, wait until the next token... + if (buf_start === undefined) buf_start = i; + i++; + continue; + } + + // Assume any "in between text" is TEXT + if (buf_start !== undefined) { + tokens.push({ + kind: TokenKind.Text, + text: input.slice(buf_start, i), + i, + }); + + // Reset for next + buf_start = undefined; + } + + // Add the token + tokens.push(token); + i = token.i; + } + + // We still have some text left over + if (buf_start !== undefined) { + tokens.push({ + kind: TokenKind.Text, + text: input.slice(buf_start), + i: buf_start, + }); + } + + // Return the tokens + return tokens; +} + +interface ExtractedCloze { + kind: "cloze"; + ordinal: number; + nodes: (TextToken | ExtractedCloze)[]; + hint?: string; +} + +/// Parses the tokens into a tree of clozes and text. +function parse_anki_tokens(tokens: Token[]): (ExtractedCloze | string)[] { + const open_clozes: ExtractedCloze[] = []; + const output: (ExtractedCloze | string)[] = []; + + // Loop through the tokens + for (let i = 0; i < tokens.length; i++) { + // Make sure the token exists + const token = tokens[i]; + if (!token) break; + + switch (token.kind) { + // Open a cloze + case TokenKind.Open: { + open_clozes.push({ + kind: "cloze", + ordinal: token.ordinal, + nodes: [], + }); + break; + } + // Add text to the cloze + case TokenKind.Text: { + const last_open_cloze = open_clozes[open_clozes.length - 1]; + + if (!last_open_cloze) { + output.push(token.text); + break; + } + + // Look ahead to see whether is a hint or not + if ( + tokens[i - 2].kind != TokenKind.Open && + tokens[i - 1].kind == TokenKind.Seperator && + tokens[i + 1].kind == TokenKind.Close + ) { + last_open_cloze.hint = token.text; + break; + } + + last_open_cloze.nodes.push(token); + break; + } + // Close a cloze + case TokenKind.Close: { + const cloze = open_clozes.pop(); + if (!cloze) { + output.push("}}"); + break; + } + const last_cloze = open_clozes[open_clozes.length - 1]; + + if (last_cloze) { + last_cloze.nodes.push(cloze); + } else { + output.push(cloze); + } + + break; + } + // Ignore + case TokenKind.Seperator: { + break; + } + } + } + + return output; +} + +// Reveal / hides a singular cloze ordinal +function reveal_cloze( + cloze: ExtractedCloze, + cloze_ord: number, + show_question: boolean, + data: { buf: string }, +) { + // We want to hide the cloze + const ord_match = cloze.ordinal === cloze_ord; + if (ord_match && show_question) { + data.buf += QuestionType_ClozeUtil.renderClozeFront(cloze.hint); + return; + } + + // Show all of the text inside the nodes + for (const node of cloze.nodes) { + if (node.kind == TokenKind.Text) { + data.buf += ord_match ? QuestionType_ClozeUtil.renderClozeBack(node.text) : node.text; + } else if (node.kind === "cloze") { + reveal_cloze(node, cloze_ord, show_question, data); + } + } +} + +// Reveal / hides all cloze ordinals +function reveal_cloze_text( + tokens: (ExtractedCloze | string)[], + cloze_ord: number, + question: boolean, +): string { + const data = { buf: "" }; + + for (const node of tokens) { + if (typeof node === "string") { + data.buf += node; + } else { + reveal_cloze(node, cloze_ord, question, data); + } + } + + return data.buf; +} + +// Recursively loops to find all of the ordinals +function get_ordinals(tokens: (ExtractedCloze | string)[], ordinals: Set) { + for (const token of tokens) { + if (typeof token === "string") continue; + ordinals.add(token.ordinal); + get_ordinals(token.nodes.filter((x) => x.kind == "cloze") as ExtractedCloze[], ordinals); + } +} + +// This can probably be done better. +function tokens_to_cards(tokens: (ExtractedCloze | string)[]): CardFrontBack[] { + // Grab each ordinal + const ordinals = new Set(); + get_ordinals(tokens, ordinals); + + // Create the cards + const cards: CardFrontBack[] = []; + for (const ordinal of ordinals) { + cards.push( + new CardFrontBack( + reveal_cloze_text(tokens, ordinal, true), + reveal_cloze_text(tokens, ordinal, false), + ), + ); + } + + return cards; +} + +// Converts text to cards (assumes the regex has already been used) +export function text_to_cards(text: string): CardFrontBack[] { + return tokens_to_cards(parse_anki_tokens(text_anki_tokens(text))); +} diff --git a/src/Question.ts b/src/Question.ts index c94d879f..43ee9343 100644 --- a/src/Question.ts +++ b/src/Question.ts @@ -13,6 +13,7 @@ export enum CardType { MultiLineBasic, MultiLineReversed, Cloze, + AnkiCloze, } // diff --git a/src/QuestionType.ts b/src/QuestionType.ts index 2592d07b..9ff4b3d1 100644 --- a/src/QuestionType.ts +++ b/src/QuestionType.ts @@ -1,3 +1,4 @@ +import { text_to_cards } from "./AnkiParser"; import { CardType } from "./Question"; import { SRSettings } from "./settings"; @@ -136,9 +137,15 @@ class QuestionType_Cloze implements IQuestionTypeHandler { } } +class QuestionType_AnkiCloze implements IQuestionTypeHandler { + expand(questionText: string, _settings: SRSettings): CardFrontBack[] { + return text_to_cards(questionText); + } +} + export class QuestionType_ClozeUtil { - static renderClozeFront(): string { - return "[...]"; + static renderClozeFront(hint?: string): string { + return `[${hint || "..."}]`; } static renderClozeBack(str: string): string { @@ -165,6 +172,9 @@ export class QuestionTypeFactory { case CardType.Cloze: handler = new QuestionType_Cloze(); break; + case CardType.AnkiCloze: + handler = new QuestionType_AnkiCloze(); + break; } return handler; } diff --git a/src/parser.ts b/src/parser.ts index b1701089..65d27707 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,5 +1,8 @@ import { CardType } from "./Question"; +const ANKI_RE = + /((?:.+\n)*(?:.*\{\{[^{}]*::[^{}]*\}\}.*)(?:\n(?:^.{1,3}$|^.{4}(?)?/gm; + /** * Returns flashcards found in `text` * @@ -25,6 +28,14 @@ export function parse( let cardType: CardType | null = null; let lineNo = 0; + let match; + while ((match = ANKI_RE.exec(text)) !== null) { + const upTo = text.substring(0, match.index); + cards.push([CardType.AnkiCloze, match[0], upTo.split("\n").length]); + + text = upTo + text.substring(match.index + match[0].length); + } + const lines: string[] = text.replaceAll("\r\n", "\n").split("\n"); for (let i = 0; i < lines.length; i++) { const currentLine = lines[i]; diff --git a/test.ts b/test.ts new file mode 100644 index 00000000..bd149803 --- /dev/null +++ b/test.ts @@ -0,0 +1,309 @@ +/* +Source / Inspiration: https://github.com/ankitects/anki/blob/80d807e08a6d3148f973829c48fe633a760546c5/rslib/src/cloze.rs + +The "BNF" for this. + + ::= [0-9]+ + ::= :: + ::= {{c + ::= }} + ::= .* + ::= | + ::= | | ?? + ::= ?? +*/ + +// The types of tokens. +enum TokenKind { + Open = "C_OPEN", + Close = "C_CLOSE", + Seperator = "SEPERATOR", + Text = "TEXT", +} + +interface OpenToken { + kind: TokenKind.Open; + ordinal: number; + i: number; +} +interface CloseToken { + kind: TokenKind.Close; + i: number; +} +interface SeperatorToken { + kind: TokenKind.Seperator; + i: number; +} +interface TextToken { + kind: TokenKind.Text; + text: string; + i: number; +} + +type Token = OpenToken | CloseToken | SeperatorToken | TextToken; + +// Returns the "Anki tokens" of a given text, given an anchor. +// This does not return the TEXT token. +function anki_tokens( + input: string, + i: number, +): OpenToken | CloseToken | SeperatorToken | undefined { + // The following statements are sorted high -> low `i`. + // Avoids any similar statements being mixed with each other. + + // C_OPEN + if (input.slice(i, i + 3) === "{{c") { + i += 3; + + // Extract the number + let num = ""; + while (input[i] !== ":") { + num += input[i]; + i++; + } + + // Attempt to parse the number + let number = parseInt(num); + if (isNaN(number)) return; + + // Return the object + return { + kind: TokenKind.Open, + ordinal: number, + i, + } satisfies OpenToken; + } + // SEPERATOR + else if (input.slice(i, i + 2) === "::") { + return { + kind: TokenKind.Seperator, + i: i + 2, + } satisfies SeperatorToken; + } + // C_CLOSE + else if (input.slice(i, i + 2) === "}}") { + return { + kind: TokenKind.Close, + i: i + 2, + } satisfies CloseToken; + } +} + +// Fully gets the "Anki tokens" of a given text. +// This does return the TEXT token. +// NOTE: This function does not error and does not check for invalid syntax +function text_anki_tokens(input: string): Token[] { + // Vars + const tokens: Token[] = []; + let i = 0; + let buf_start; + + // Loop through the input + while (i < input.length) { + // Grab the token at position `i` + const token = anki_tokens(input, i); + if (!token) { + // No token found, wait until the next token... + if (buf_start === undefined) buf_start = i; + i++; + continue; + } + + // Assume any "in between text" is TEXT + if (buf_start !== undefined) { + tokens.push({ + kind: TokenKind.Text, + text: input.slice(buf_start, i), + i, + }); + + // Reset for next + buf_start = undefined; + } + + // Add the token + tokens.push(token); + i = token.i; + } + + // We still have some text left over + if (buf_start !== undefined) { + tokens.push({ + kind: TokenKind.Text, + text: input.slice(buf_start), + i: buf_start, + }); + } + + // Return the tokens + return tokens; +} + +interface ExtractedCloze { + kind: "cloze"; + ordinal: number; + nodes: (TextToken | ExtractedCloze)[]; + hint?: string; +} + +/// Parses the tokens into a tree of clozes and text. +function parse_anki_tokens(tokens: Token[]): (ExtractedCloze | string)[] { + const open_clozes: ExtractedCloze[] = []; + const output: (ExtractedCloze | string)[] = []; + + // Loop through the tokens + for (let i = 0; i < tokens.length; i++) { + // Make sure the token exists + const token = tokens[i]; + if (!token) break; + + switch (token.kind) { + // Open a cloze + case TokenKind.Open: + open_clozes.push({ + kind: "cloze", + ordinal: token.ordinal, + nodes: [], + }); + break; + // Add text to the cloze + case TokenKind.Text: + const last_open_cloze = open_clozes[open_clozes.length - 1]; + + if (!last_open_cloze) { + output.push(token.text); + break; + } + + // Look ahead to see whether is a hint or not + if ( + tokens[i - 2].kind != TokenKind.Open && + tokens[i - 1].kind == TokenKind.Seperator && + tokens[i + 1].kind == TokenKind.Close + ) { + last_open_cloze.hint = token.text; + break; + } + + last_open_cloze.nodes.push(token); + break; + // Close a cloze + case TokenKind.Close: + const cloze = open_clozes.pop(); + if (!cloze) { + output.push("}}"); + break; + } + const last_cloze = open_clozes[open_clozes.length - 1]; + + if (last_cloze) { + last_cloze.nodes.push(cloze); + } else { + output.push(cloze); + } + + break; + // Ignore + case TokenKind.Seperator: + break; + } + } + + return output; +} + +// Reveal / hides a singular cloze ordinal +function reveal_cloze( + cloze: ExtractedCloze, + cloze_ord: number, + show_question: boolean, + data: { buf: string }, +) { + // We want to hide the cloze + if (cloze.ordinal === cloze_ord && show_question) { + data.buf += `[${cloze.hint || "..."}]`; + return; + } + + // Show all of the text inside the nodes + for (const node of cloze.nodes) { + if (node.kind == TokenKind.Text) { + data.buf += node.text; + } else if (node.kind === "cloze") { + reveal_cloze(node, cloze_ord, show_question, data); + } + } +} + +// Reveal / hides all cloze ordinals +function reveal_cloze_text( + tokens: (ExtractedCloze | string)[], + cloze_ord: number, + question: boolean, +): string { + let data = { buf: "" }; + + for (const node of tokens) { + if (typeof node === "string") { + data.buf += node; + } else { + reveal_cloze(node, cloze_ord, question, data); + } + } + + return data.buf; +} + +// Recursively loops to find all of the ordinals +function get_ordinals(tokens: (ExtractedCloze | string)[], ordinals: Set) { + for (const token of tokens) { + if (typeof token === "string") continue; + ordinals.add(token.ordinal); + get_ordinals(token.nodes.filter((x) => x.kind == "cloze") as ExtractedCloze[], ordinals); + } +} + +interface Card { + front: string; + back: string; +} + +// This can probably be done better. +function tokens_to_cards(tokens: (ExtractedCloze | string)[]): Card[] { + // Grab each ordinal + const ordinals = new Set(); + get_ordinals(tokens, ordinals); + + // Create the cards + const cards: Card[] = []; + for (const ordinal of ordinals) { + cards.push({ + front: reveal_cloze_text(tokens, ordinal, true), + back: reveal_cloze_text(tokens, ordinal, false), + }); + } + + return cards; +} + +// Converts text to cards +const regex = /((?:.+\n)*(?:.*\{\{[^\{\}]*::[^\{\}]*\}\}.*)(?:\n(?:^.{1,3}$|^.{4}(?BaTS*(@$ zG@7PzIy&4-4)@-(y+isgIoSXCaQ5N-!QtBvF??e04i4!1cYDA5{4V}wl&O?YM=Fa) zaVr00!%S^&ZEs!98R`A??AIQd$&BbZS3~UnDDt~0%Z`a{QFKJE_TGHB-hm;-77UD( zb`*!A2)g5mTrlC!czTLUJ!Tx%RqVJPFAC_^>_L~4Z1?cB>z>GL64NX>Hr-D0nS3;T zs1g28xDeLP4jKH=Q1r<&J{WQOnxAe?Q>X-9FgA14NFWotLZ6F8UirCOyEuqm;7~3SHsgX-7kSIlq~cH^&5o$ansISWW(XFL?r@;*ltR$5Wd58m0_Y-th-e zu>^)BjK*^!!Qsphm{VdmkW1Z+SXYO@gfZ)LaFy(kg8?}@g8cz()f<$?8+o#GL=MKK zHmC9QL1#49CbTw8^>E5G_Tg^pjqC2^oTtpKeZ{ZID_kyWE`|-K!jh%jxH-yqcD$Y< zFzd61$SlAtNftB%npk5W^nr;+?2FKS=oxsLO_=!mNPMd?Ca3>~W2qi> z@C*FwqL$sJNI)^cDEIs)3eHJ2;{f;yq;|NdvR`qb?O`dzWd#b}Iut}fUN-Zj+6|$> zkyYMpcXJPg#ygVV-1MUOUraZ<`bB2fJW!cjI1>9cPt!+o!}JC`7Sh%#$^xOY^UKr7 zidFf;oXYs8mXt=niDNl%c6q-JG{AUjTl_WTD5xTne*z6w^OD6mh zOQE#$tCoZksob-;(KvMMdEck`Yyt$9>9NO8ze~9Qmou3I3!>}B3_D!X2Ki`vhba>` zdfvz}sQ6n(r)H|LhfqxxQgP!X@M$gT84Z@TTn8+LEbz*Vw`d9zBAud{1m52zOUgBz z0Gg+kcd+EJ`5O)cXo21;5(c-`!7(7-0O4!t4;wh7xyYH<-C{+h@;heEkRBIsXy_$# z@aqvk69TR2b8W(_rfoqGZ0LrIT+REz5Vo$OVz(`33k-ixSL{D7yT|%3<5XDow-@OJZxc?w~ziGTc}pF9t*7u**RS{+iTVa z5m~LG%i0y0|8gT_wm#+6zUWAMsMSThvcQ!a*6jL_hw9{>(Ugo}cPDt169orkj?XJL zD~_MNlRYytY#^=c;hDT^=T2a5b#*6dE|-)4)P$*;ZHFxf6ST0m)vH_fxHpPTRKlWC2lm%t;%?Z52-VWNs)2W4Y z-yt(EV7Q`wIq=mUl}C0pGFw z0^OiQTInv9c4$3tkOg9|S7)~)U)GXVs_^j_Mvat-s$HcW)-X@)6ul@9o1~z@0_FOC zX5u_k{GO$cB!yB~;f9+bJUmc$Wa)Vu6AyO zyW=gOTy{v4q~9n#{dl$AaYx0Br*JdtKaiti@=$HZYTHoNx|Y~g8B`cFtI)#)?8A+sI_@trDlXv~HWr7EHF!RIKMmd7*frd3+ zyUeqaeN-UaNYe11HS@RV$sqL8cwgqs9J0rlYdXyZ3ppDbE3b zMy20}j?vM1i*XrPMLd { ]); }); +test("Testing parsing of anki cloze cards", () => { + // TODO! +}); + test("Test parsing of a mix of card types", () => { expect( parse( From c15008415b3b6608ada28207d174947d5441459b Mon Sep 17 00:00:00 2001 From: Stefan <42220813+Stefanuk12@users.noreply.github.com> Date: Sat, 21 Oct 2023 16:47:38 +0100 Subject: [PATCH 2/3] Remove test files --- test.ts | 309 ------------------------------------------------------- test.zip | Bin 2480 -> 0 bytes 2 files changed, 309 deletions(-) delete mode 100644 test.ts delete mode 100644 test.zip diff --git a/test.ts b/test.ts deleted file mode 100644 index bd149803..00000000 --- a/test.ts +++ /dev/null @@ -1,309 +0,0 @@ -/* -Source / Inspiration: https://github.com/ankitects/anki/blob/80d807e08a6d3148f973829c48fe633a760546c5/rslib/src/cloze.rs - -The "BNF" for this. - - ::= [0-9]+ - ::= :: - ::= {{c - ::= }} - ::= .* - ::= | - ::= | | ?? - ::= ?? -*/ - -// The types of tokens. -enum TokenKind { - Open = "C_OPEN", - Close = "C_CLOSE", - Seperator = "SEPERATOR", - Text = "TEXT", -} - -interface OpenToken { - kind: TokenKind.Open; - ordinal: number; - i: number; -} -interface CloseToken { - kind: TokenKind.Close; - i: number; -} -interface SeperatorToken { - kind: TokenKind.Seperator; - i: number; -} -interface TextToken { - kind: TokenKind.Text; - text: string; - i: number; -} - -type Token = OpenToken | CloseToken | SeperatorToken | TextToken; - -// Returns the "Anki tokens" of a given text, given an anchor. -// This does not return the TEXT token. -function anki_tokens( - input: string, - i: number, -): OpenToken | CloseToken | SeperatorToken | undefined { - // The following statements are sorted high -> low `i`. - // Avoids any similar statements being mixed with each other. - - // C_OPEN - if (input.slice(i, i + 3) === "{{c") { - i += 3; - - // Extract the number - let num = ""; - while (input[i] !== ":") { - num += input[i]; - i++; - } - - // Attempt to parse the number - let number = parseInt(num); - if (isNaN(number)) return; - - // Return the object - return { - kind: TokenKind.Open, - ordinal: number, - i, - } satisfies OpenToken; - } - // SEPERATOR - else if (input.slice(i, i + 2) === "::") { - return { - kind: TokenKind.Seperator, - i: i + 2, - } satisfies SeperatorToken; - } - // C_CLOSE - else if (input.slice(i, i + 2) === "}}") { - return { - kind: TokenKind.Close, - i: i + 2, - } satisfies CloseToken; - } -} - -// Fully gets the "Anki tokens" of a given text. -// This does return the TEXT token. -// NOTE: This function does not error and does not check for invalid syntax -function text_anki_tokens(input: string): Token[] { - // Vars - const tokens: Token[] = []; - let i = 0; - let buf_start; - - // Loop through the input - while (i < input.length) { - // Grab the token at position `i` - const token = anki_tokens(input, i); - if (!token) { - // No token found, wait until the next token... - if (buf_start === undefined) buf_start = i; - i++; - continue; - } - - // Assume any "in between text" is TEXT - if (buf_start !== undefined) { - tokens.push({ - kind: TokenKind.Text, - text: input.slice(buf_start, i), - i, - }); - - // Reset for next - buf_start = undefined; - } - - // Add the token - tokens.push(token); - i = token.i; - } - - // We still have some text left over - if (buf_start !== undefined) { - tokens.push({ - kind: TokenKind.Text, - text: input.slice(buf_start), - i: buf_start, - }); - } - - // Return the tokens - return tokens; -} - -interface ExtractedCloze { - kind: "cloze"; - ordinal: number; - nodes: (TextToken | ExtractedCloze)[]; - hint?: string; -} - -/// Parses the tokens into a tree of clozes and text. -function parse_anki_tokens(tokens: Token[]): (ExtractedCloze | string)[] { - const open_clozes: ExtractedCloze[] = []; - const output: (ExtractedCloze | string)[] = []; - - // Loop through the tokens - for (let i = 0; i < tokens.length; i++) { - // Make sure the token exists - const token = tokens[i]; - if (!token) break; - - switch (token.kind) { - // Open a cloze - case TokenKind.Open: - open_clozes.push({ - kind: "cloze", - ordinal: token.ordinal, - nodes: [], - }); - break; - // Add text to the cloze - case TokenKind.Text: - const last_open_cloze = open_clozes[open_clozes.length - 1]; - - if (!last_open_cloze) { - output.push(token.text); - break; - } - - // Look ahead to see whether is a hint or not - if ( - tokens[i - 2].kind != TokenKind.Open && - tokens[i - 1].kind == TokenKind.Seperator && - tokens[i + 1].kind == TokenKind.Close - ) { - last_open_cloze.hint = token.text; - break; - } - - last_open_cloze.nodes.push(token); - break; - // Close a cloze - case TokenKind.Close: - const cloze = open_clozes.pop(); - if (!cloze) { - output.push("}}"); - break; - } - const last_cloze = open_clozes[open_clozes.length - 1]; - - if (last_cloze) { - last_cloze.nodes.push(cloze); - } else { - output.push(cloze); - } - - break; - // Ignore - case TokenKind.Seperator: - break; - } - } - - return output; -} - -// Reveal / hides a singular cloze ordinal -function reveal_cloze( - cloze: ExtractedCloze, - cloze_ord: number, - show_question: boolean, - data: { buf: string }, -) { - // We want to hide the cloze - if (cloze.ordinal === cloze_ord && show_question) { - data.buf += `[${cloze.hint || "..."}]`; - return; - } - - // Show all of the text inside the nodes - for (const node of cloze.nodes) { - if (node.kind == TokenKind.Text) { - data.buf += node.text; - } else if (node.kind === "cloze") { - reveal_cloze(node, cloze_ord, show_question, data); - } - } -} - -// Reveal / hides all cloze ordinals -function reveal_cloze_text( - tokens: (ExtractedCloze | string)[], - cloze_ord: number, - question: boolean, -): string { - let data = { buf: "" }; - - for (const node of tokens) { - if (typeof node === "string") { - data.buf += node; - } else { - reveal_cloze(node, cloze_ord, question, data); - } - } - - return data.buf; -} - -// Recursively loops to find all of the ordinals -function get_ordinals(tokens: (ExtractedCloze | string)[], ordinals: Set) { - for (const token of tokens) { - if (typeof token === "string") continue; - ordinals.add(token.ordinal); - get_ordinals(token.nodes.filter((x) => x.kind == "cloze") as ExtractedCloze[], ordinals); - } -} - -interface Card { - front: string; - back: string; -} - -// This can probably be done better. -function tokens_to_cards(tokens: (ExtractedCloze | string)[]): Card[] { - // Grab each ordinal - const ordinals = new Set(); - get_ordinals(tokens, ordinals); - - // Create the cards - const cards: Card[] = []; - for (const ordinal of ordinals) { - cards.push({ - front: reveal_cloze_text(tokens, ordinal, true), - back: reveal_cloze_text(tokens, ordinal, false), - }); - } - - return cards; -} - -// Converts text to cards -const regex = /((?:.+\n)*(?:.*\{\{[^\{\}]*::[^\{\}]*\}\}.*)(?:\n(?:^.{1,3}$|^.{4}(?BaTS*(@$ zG@7PzIy&4-4)@-(y+isgIoSXCaQ5N-!QtBvF??e04i4!1cYDA5{4V}wl&O?YM=Fa) zaVr00!%S^&ZEs!98R`A??AIQd$&BbZS3~UnDDt~0%Z`a{QFKJE_TGHB-hm;-77UD( zb`*!A2)g5mTrlC!czTLUJ!Tx%RqVJPFAC_^>_L~4Z1?cB>z>GL64NX>Hr-D0nS3;T zs1g28xDeLP4jKH=Q1r<&J{WQOnxAe?Q>X-9FgA14NFWotLZ6F8UirCOyEuqm;7~3SHsgX-7kSIlq~cH^&5o$ansISWW(XFL?r@;*ltR$5Wd58m0_Y-th-e zu>^)BjK*^!!Qsphm{VdmkW1Z+SXYO@gfZ)LaFy(kg8?}@g8cz()f<$?8+o#GL=MKK zHmC9QL1#49CbTw8^>E5G_Tg^pjqC2^oTtpKeZ{ZID_kyWE`|-K!jh%jxH-yqcD$Y< zFzd61$SlAtNftB%npk5W^nr;+?2FKS=oxsLO_=!mNPMd?Ca3>~W2qi> z@C*FwqL$sJNI)^cDEIs)3eHJ2;{f;yq;|NdvR`qb?O`dzWd#b}Iut}fUN-Zj+6|$> zkyYMpcXJPg#ygVV-1MUOUraZ<`bB2fJW!cjI1>9cPt!+o!}JC`7Sh%#$^xOY^UKr7 zidFf;oXYs8mXt=niDNl%c6q-JG{AUjTl_WTD5xTne*z6w^OD6mh zOQE#$tCoZksob-;(KvMMdEck`Yyt$9>9NO8ze~9Qmou3I3!>}B3_D!X2Ki`vhba>` zdfvz}sQ6n(r)H|LhfqxxQgP!X@M$gT84Z@TTn8+LEbz*Vw`d9zBAud{1m52zOUgBz z0Gg+kcd+EJ`5O)cXo21;5(c-`!7(7-0O4!t4;wh7xyYH<-C{+h@;heEkRBIsXy_$# z@aqvk69TR2b8W(_rfoqGZ0LrIT+REz5Vo$OVz(`33k-ixSL{D7yT|%3<5XDow-@OJZxc?w~ziGTc}pF9t*7u**RS{+iTVa z5m~LG%i0y0|8gT_wm#+6zUWAMsMSThvcQ!a*6jL_hw9{>(Ugo}cPDt169orkj?XJL zD~_MNlRYytY#^=c;hDT^=T2a5b#*6dE|-)4)P$*;ZHFxf6ST0m)vH_fxHpPTRKlWC2lm%t;%?Z52-VWNs)2W4Y z-yt(EV7Q`wIq=mUl}C0pGFw z0^OiQTInv9c4$3tkOg9|S7)~)U)GXVs_^j_Mvat-s$HcW)-X@)6ul@9o1~z@0_FOC zX5u_k{GO$cB!yB~;f9+bJUmc$Wa)Vu6AyO zyW=gOTy{v4q~9n#{dl$AaYx0Br*JdtKaiti@=$HZYTHoNx|Y~g8B`cFtI)#)?8A+sI_@trDlXv~HWr7EHF!RIKMmd7*frd3+ zyUeqaeN-UaNYe11HS@RV$sqL8cwgqs9J0rlYdXyZ3ppDbE3b zMy20}j?vM1i*XrPMLd Date: Sun, 22 Oct 2023 18:27:43 +0100 Subject: [PATCH 3/3] Fix typo --- src/AnkiParser.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/AnkiParser.ts b/src/AnkiParser.ts index 0e94c797..27d0ec58 100644 --- a/src/AnkiParser.ts +++ b/src/AnkiParser.ts @@ -4,13 +4,13 @@ Source / Inspiration: https://github.com/ankitects/anki/blob/80d807e08a6d3148f97 The "BNF" for this. ::= [0-9]+ - ::= :: + ::= :: ::= {{c ::= }} ::= .* ::= | - ::= | | ?? - ::= ?? + ::= | | ?? + ::= ?? */ import { CardFrontBack, QuestionType_ClozeUtil } from "./QuestionType"; @@ -19,7 +19,7 @@ import { CardFrontBack, QuestionType_ClozeUtil } from "./QuestionType"; enum TokenKind { Open = "C_OPEN", Close = "C_CLOSE", - Seperator = "SEPERATOR", + Separator = "SEPARATOR", Text = "TEXT", } @@ -32,8 +32,8 @@ interface CloseToken { kind: TokenKind.Close; i: number; } -interface SeperatorToken { - kind: TokenKind.Seperator; +interface SeparatorToken { + kind: TokenKind.Separator; i: number; } interface TextToken { @@ -42,14 +42,14 @@ interface TextToken { i: number; } -type Token = OpenToken | CloseToken | SeperatorToken | TextToken; +type Token = OpenToken | CloseToken | SeparatorToken | TextToken; // Returns the "Anki tokens" of a given text, given an anchor. // This does not return the TEXT token. function anki_tokens( input: string, i: number, -): OpenToken | CloseToken | SeperatorToken | undefined { +): OpenToken | CloseToken | SeparatorToken | undefined { // The following statements are sorted high -> low `i`. // Avoids any similar statements being mixed with each other. @@ -75,12 +75,12 @@ function anki_tokens( i, } satisfies OpenToken; } - // SEPERATOR + // SEPARATOR else if (input.slice(i, i + 2) === "::") { return { - kind: TokenKind.Seperator, + kind: TokenKind.Separator, i: i + 2, - } satisfies SeperatorToken; + } satisfies SeparatorToken; } // C_CLOSE else if (input.slice(i, i + 2) === "}}") { @@ -181,7 +181,7 @@ function parse_anki_tokens(tokens: Token[]): (ExtractedCloze | string)[] { // Look ahead to see whether is a hint or not if ( tokens[i - 2].kind != TokenKind.Open && - tokens[i - 1].kind == TokenKind.Seperator && + tokens[i - 1].kind == TokenKind.Separator && tokens[i + 1].kind == TokenKind.Close ) { last_open_cloze.hint = token.text; @@ -209,7 +209,7 @@ function parse_anki_tokens(tokens: Token[]): (ExtractedCloze | string)[] { break; } // Ignore - case TokenKind.Seperator: { + case TokenKind.Separator: { break; } }