diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index fe48d755..43cbfd70 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -st3v3n.mw@gmail.com. +mail@stephenmwangi.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/src/algorithms/base/srs-algorithm.ts b/src/algorithms/base/srs-algorithm.ts index 8054d5d9..4e375ae6 100644 --- a/src/algorithms/base/srs-algorithm.ts +++ b/src/algorithms/base/srs-algorithm.ts @@ -5,7 +5,7 @@ export class SrsAlgorithm { public static getInstance(): ISrsAlgorithm { if (!SrsAlgorithm.instance) { - throw Error("there is no SrsAlgorithm instance."); + throw new Error("there is no SrsAlgorithm instance."); } return SrsAlgorithm.instance; } diff --git a/src/data-store-algorithm/data-store-algorithm.ts b/src/data-store-algorithm/data-store-algorithm.ts index 091129b5..339c7472 100644 --- a/src/data-store-algorithm/data-store-algorithm.ts +++ b/src/data-store-algorithm/data-store-algorithm.ts @@ -5,7 +5,7 @@ export class DataStoreAlgorithm { public static getInstance(): IDataStoreAlgorithm { if (!DataStoreAlgorithm.instance) { - throw Error("there is no DataStoreAlgorithm instance."); + throw new Error("there is no DataStoreAlgorithm instance."); } return DataStoreAlgorithm.instance; } diff --git a/src/data-stores/base/data-store.ts b/src/data-stores/base/data-store.ts index ef9750fa..9fb5c7c5 100644 --- a/src/data-stores/base/data-store.ts +++ b/src/data-stores/base/data-store.ts @@ -17,7 +17,7 @@ export class DataStore { public static getInstance(): IDataStore { if (!DataStore.instance) { - throw Error("there is no DataStore instance."); + throw new Error("there is no DataStore instance."); } return DataStore.instance; } diff --git a/src/parser.ts b/src/parser.ts index fe56bc23..efe8d5b1 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -74,13 +74,18 @@ export function generateParser(options: ParserOptions): Parser { } function generateGrammar(options: ParserOptions): string { - const close_rules_list: string[] = []; + const cloze_rules_list: string[] = []; - if (options.convertHighlightsToClozes) close_rules_list.push("close_equal"); - if (options.convertBoldTextToClozes) close_rules_list.push("close_star"); - if (options.convertCurlyBracketsToClozes) close_rules_list.push("close_bracket"); + if (options.convertHighlightsToClozes) cloze_rules_list.push("cloze_equal"); + if (options.convertBoldTextToClozes) cloze_rules_list.push("cloze_star"); + if (options.convertCurlyBracketsToClozes) cloze_rules_list.push("cloze_bracket"); - const close_rules = close_rules_list.join(" / "); + let cloze_rules: string; + if (cloze_rules_list.length > 0) { + cloze_rules = cloze_rules_list.join(" / "); + } else { + cloze_rules = "tombstone"; + } return `{ // The fallback case is important if we want to test the rules with https://peggyjs.org/online.html @@ -111,7 +116,7 @@ main /* The input text to the parser contains arbitrary text, not just card definitions. Hence we fallback to matching on loose_line. The result from loose_line is filtered out by filterBlocks() */ block -= html_comment / tilde_code / backprime_code / inline_rev_card / inline_card / multiline_rev_card / multiline_card / close_card / loose_line += html_comment / tilde_code / backprime_code / inline_code / inline_rev_card / inline_card / multiline_rev_card / multiline_card / cloze_card / loose_line html_comment = $("" (html_comment / .))* "-->" newline?) { @@ -129,7 +134,7 @@ inline_card = e:inline newline? { return e; } inline -= $(left:(!inline_mark (inline_code / non_newline))+ inline_mark right:text_till_newline (newline annotation)?) { += $(left:(!inline_mark !inline_code non_newline)+ inline_mark right:text_till_newline (newline annotation)?) { return createParsedQuestionInfo(CardType.SingleLineBasic,text(),location().start.line-1,location().end.line-1); } @@ -137,9 +142,9 @@ inline_rev_card = e:inline_rev newline? { return e; } inline_rev -= left:(!inline_rev_mark (inline_code / non_newline))+ inline_rev_mark right:text_till_newline (newline annotation)? { += left:(!inline_rev_mark !inline_code non_newline)+ inline_rev_mark right:text_till_newline (newline annotation)? { return createParsedQuestionInfo(CardType.SingleLineReversed,text(),location().start.line-1,location().end.line-1); - } +} multiline_card = c:multiline separator_line { @@ -158,7 +163,7 @@ multiline_after = $(!separator_line (tilde_code / backprime_code / text_line))+ inline_code -= $("\`" (!"\`" .)* "\`") += $("\`" (!"\`" .)* "\`") { return null; } tilde_code = $( @@ -196,42 +201,42 @@ multiline_rev_before multiline_rev_after = $(!separator_line text_line)+ -close_card -= $(multiline_before_close? close_line (multiline_after_close)? (newline annotation)?) { +cloze_card += $(multiline_before_cloze? cloze_line (multiline_after_cloze)? (newline annotation)?) { return createParsedQuestionInfo(CardType.Cloze,text().trimEnd(),location().start.line-1,location().end.line-1); } -close_line -= ((!close_text non_newline)* close_text) text_line_nonterminated? +cloze_line += ((!cloze_text !inline_code non_newline)* cloze_text) text_line_nonterminated? -multiline_before_close -= (!close_line nonempty_text_line)+ +multiline_before_cloze += (!cloze_line nonempty_text_line)+ -multiline_after_close +multiline_after_cloze = e:(!(newline separator_line) text_line1)+ -close_text -= ${close_rules} +cloze_text += ${cloze_rules} -close_equal -= close_mark_equal (!close_mark_equal non_newline)+ close_mark_equal +cloze_equal += cloze_mark_equal (!cloze_mark_equal non_newline)+ cloze_mark_equal -close_mark_equal +cloze_mark_equal = "==" -close_star -= close_mark_star (!close_mark_star non_newline)+ close_mark_star +cloze_star += cloze_mark_star (!cloze_mark_star non_newline)+ cloze_mark_star -close_mark_star +cloze_mark_star = "**" -close_bracket -= close_mark_bracket_open (!close_mark_bracket_close non_newline)+ close_mark_bracket_close +cloze_bracket += cloze_mark_bracket_open (!cloze_mark_bracket_close non_newline)+ cloze_mark_bracket_close -close_mark_bracket_open +cloze_mark_bracket_open = "{{" -close_mark_bracket_close +cloze_mark_bracket_close = "}}" inline_mark @@ -295,6 +300,9 @@ optional_whitespaces = whitespace_char* whitespace_char = ([ \\f\\t\\v\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]) + +tombstone += "\0" `; } @@ -343,7 +351,7 @@ export function parseEx(text: string, options: ParserOptions): ParsedQuestionInf let cards: ParsedQuestionInfo[] = []; try { - if (!options) throw Error("No parser options provided."); + if (!options) throw new Error("No parser options provided."); const parser: Parser = generateParser(options); @@ -370,8 +378,7 @@ export function parseEx(text: string, options: ParserOptions): ParsedQuestionInf } if (debugParser) { - console.log("Parsed cards:"); - console.log(cards); + console.log("Parsed cards:\n", cards); } return cards; diff --git a/tests/unit/parser.test.ts b/tests/unit/parser.test.ts index d830318c..bd0d0f49 100644 --- a/tests/unit/parser.test.ts +++ b/tests/unit/parser.test.ts @@ -15,12 +15,11 @@ const parserOptions: ParserOptions = { /** * This function is a small wrapper around parseEx used for testing only. + * It generates a parser each time, overwriting the default one. * Created when the actual parser changed from returning [CardType, string, number, number] to ParsedQuestionInfo. * It's purpose is to minimise changes to all the test cases here during the parser()->parserEx() change. */ function parse(text: string, options: ParserOptions): [CardType, string, number, number][] { - // for testing purposes, generate parser each time, overwriting the default one - const list: ParsedQuestionInfo[] = parseEx(text, options); const result: [CardType, string, number, number][] = []; for (const item of list) { @@ -30,6 +29,7 @@ function parse(text: string, options: ParserOptions): [CardType, string, number, } test("Test parsing of single line basic cards", () => { + // standard symbols expect(parse("Question::Answer", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question::Answer", 0, 0], ]); @@ -49,9 +49,36 @@ test("Test parsing of single line basic cards", () => { expect(parse("#flashcards/science Question ::Answer", parserOptions)).toEqual([ [CardType.SingleLineBasic, "#flashcards/science Question ::Answer", 0, 0], ]); + + // custom symbols + expect( + parse("Question&&Answer", { + singleLineCardSeparator: "&&", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.SingleLineBasic, "Question&&Answer", 0, 0]]); + expect( + parse("Question=Answer", { + singleLineCardSeparator: "=", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.SingleLineBasic, "Question=Answer", 0, 0]]); }); test("Test parsing of single line reversed cards", () => { + // standard symbols expect(parse("Question:::Answer", parserOptions)).toEqual([ [CardType.SingleLineReversed, "Question:::Answer", 0, 0], ]); @@ -62,9 +89,24 @@ test("Test parsing of single line reversed cards", () => { [CardType.SingleLineReversed, "Q1:::A1", 2, 2], [CardType.SingleLineReversed, "Q2::: A2", 3, 3], ]); + + // custom symbols + expect( + parse("Question&&&Answer", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: "&&&", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.SingleLineReversed, "Question&&&Answer", 0, 0]]); }); test("Test parsing of multi line basic cards", () => { + // standard symbols expect(parse("Question\n?\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer", 0, 2], ]); @@ -84,12 +126,7 @@ test("Test parsing of multi line basic cards", () => { [CardType.MultiLineBasic, "Question\n?\nAnswer line 1\nAnswer line 2", 0, 3], ]); expect(parse("#Title\n\nLine0\nQ1\n?\nA1\nAnswerExtra\n\nQ2\n?\nA2", parserOptions)).toEqual([ - [ - CardType.MultiLineBasic, - "Line0\nQ1\n?\nA1\nAnswerExtra", - /* Line0 */ 2, - /* AnswerExtra */ 6, - ], + [CardType.MultiLineBasic, "Line0\nQ1\n?\nA1\nAnswerExtra", 2, 6], [CardType.MultiLineBasic, "Q2\n?\nA2", 8, 10], ]); expect(parse("#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", parserOptions)).toEqual([ @@ -148,9 +185,24 @@ test("Test parsing of multi line basic cards", () => { 10, ], ]); + + // custom symbols + expect( + parse("Question\n@@\nAnswer\n\nsfdg", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "@@", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.MultiLineBasic, "Question\n@@\nAnswer", 0, 2]]); }); test("Test parsing of multi line reversed cards", () => { + // standard symbols expect(parse("Question\n??\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineReversed, "Question\n??\nAnswer", 0, 2], ]); @@ -161,12 +213,7 @@ test("Test parsing of multi line reversed cards", () => { [CardType.MultiLineReversed, "Question\n??\nAnswer line 1\nAnswer line 2", 0, 3], ]); expect(parse("#Title\n\nLine0\nQ1\n??\nA1\nAnswerExtra\n\nQ2\n??\nA2", parserOptions)).toEqual([ - [ - CardType.MultiLineReversed, - "Line0\nQ1\n??\nA1\nAnswerExtra", - /* Line0 */ 2, - /* AnswerExtra */ 6, - ], + [CardType.MultiLineReversed, "Line0\nQ1\n??\nA1\nAnswerExtra", 2, 6], [CardType.MultiLineReversed, "Q2\n??\nA2", 8, 10], ]); expect( @@ -199,6 +246,20 @@ test("Test parsing of multi line reversed cards", () => { [CardType.MultiLineBasic, "Question 1\n?\nAnswer line 1\nAnswer line 2", 0, 4], [CardType.MultiLineReversed, "Question 2\n??\nAnswer line 1\nAnswer line 2", 6, 9], ]); + + // custom symbols + expect( + parse("Question\n@@@\nAnswer\n---", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "@@", + multilineReversedCardSeparator: "@@@", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.MultiLineReversed, "Question\n@@@\nAnswer", 0, 2]]); }); test("Test parsing of cloze cards", () => { @@ -228,6 +289,7 @@ test("Test parsing of cloze cards", () => { expect(parse("srdf ==", parserOptions)).toEqual([]); expect(parse("lorem ipsum ==p\ndolor won==", parserOptions)).toEqual([]); expect(parse("lorem ipsum ==dolor won=", parserOptions)).toEqual([]); + // ==highlights== turned off expect( parse("cloze ==deletion== test", { @@ -238,7 +300,7 @@ test("Test parsing of cloze cards", () => { multilineCardEndMarker: "", convertHighlightsToClozes: false, convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: false, + convertCurlyBracketsToClozes: true, }), ).toEqual([]); @@ -268,6 +330,7 @@ test("Test parsing of cloze cards", () => { expect(parse("srdf **", parserOptions)).toEqual([]); expect(parse("lorem ipsum **p\ndolor won**", parserOptions)).toEqual([]); expect(parse("lorem ipsum **dolor won*", parserOptions)).toEqual([]); + // **bolded** turned off expect( parse("cloze **deletion** test", { @@ -278,15 +341,56 @@ test("Test parsing of cloze cards", () => { multilineCardEndMarker: "", convertHighlightsToClozes: true, convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: true, + }), + ).toEqual([]); + + // {{curly}} + expect(parse("cloze {{deletion}} test", parserOptions)).toEqual([ + [CardType.Cloze, "cloze {{deletion}} test", 0, 0], + ]); + expect(parse("cloze {{deletion}} test\n", parserOptions)).toEqual([ + [CardType.Cloze, "cloze {{deletion}} test\n", 0, 1], + ]); + expect(parse("cloze {{deletion}} test ", parserOptions)).toEqual([ + [CardType.Cloze, "cloze {{deletion}} test ", 0, 0], + ]); + expect(parse("{{this}} is a {{deletion}}\n", parserOptions)).toEqual([ + [CardType.Cloze, "{{this}} is a {{deletion}}", 0, 0], + ]); + expect( + parse( + "some text before\n\na deletion on\nsuch {{wow}}\n\n" + + "many text\nsuch surprise {{wow}} more {{text}}\nsome text after\n\nHmm", + parserOptions, + ), + ).toEqual([ + [CardType.Cloze, "a deletion on\nsuch {{wow}}", 2, 3], + [CardType.Cloze, "many text\nsuch surprise {{wow}} more {{text}}\nsome text after", 5, 7], + ]); + expect(parse("srdf {{", parserOptions)).toEqual([]); + expect(parse("srdf }}", parserOptions)).toEqual([]); + expect(parse("lorem ipsum {{p\ndolor won}}", parserOptions)).toEqual([]); + expect(parse("lorem ipsum {{dolor won}", parserOptions)).toEqual([]); + + // {{curly}} turned off + expect( + parse("cloze {{deletion}} test", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: true, + convertBoldTextToClozes: true, convertCurlyBracketsToClozes: false, }), ).toEqual([]); - // both + // combo expect(parse("cloze **deletion** test ==another deletion==!", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test ==another deletion==!", 0, 0], ]); - expect( parse( "Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nTest 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nHere is some more text.", @@ -315,6 +419,20 @@ test("Test parsing of cloze cards", () => { 17, ], ]); + + // all disabled + expect( + parse("cloze {{deletion}} test and **deletion** ==another deletion==!", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([]); }); test("Test parsing of a mix of card types", () => { @@ -340,12 +458,12 @@ test("Test parsing of a mix of card types", () => { CardType.MultiLineReversed, "Donec dapibus ullamcorper aliquam.\n??\nDonec dapibus ullamcorper aliquam.\n", 8, - 11 /* */, + 11, ], ]); }); -test("Test codeblocks", () => { +test("Test parsing cards with codeblocks", () => { // no blank lines expect( parse( @@ -359,7 +477,7 @@ test("Test codeblocks", () => { "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\nprint('Howdy?')\nlambda x: x[0]\n```", 0, - 6 /* ``` */, + 6, ], ]); @@ -376,7 +494,7 @@ test("Test codeblocks", () => { "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\n\n\nprint('Howdy?')\n\nlambda x: x[0]\n```", 0, - 9 /* ``` */, + 9, ], ]); @@ -409,12 +527,14 @@ test("Test codeblocks", () => { "~~~\n" + "````", 0, - 12 /* ``` */, + 12, ], ]); }); test("Test not parsing cards in HTML comments", () => { + expect(parse("", parserOptions)).toEqual([]); + expect(parse("", parserOptions)).toEqual([]); expect( parse("\n-->", parserOptions), ).toEqual([]); @@ -426,6 +546,46 @@ test("Test not parsing cards in HTML comments", () => { ).toEqual([]); expect(parse("", parserOptions)).toEqual([]); expect(parse("", parserOptions)).toEqual([]); + expect(parse("", parserOptions)).toEqual([]); +}); + +test("Test not parsing 'cards' in codeblocks", () => { + // block + expect(parse("```\nCodeblockq::CodeblockA\n```", parserOptions)).toEqual([]); + expect(parse("```\nCodeblockq:::CodeblockA\n```", parserOptions)).toEqual([]); + expect( + parse("# Title\n\n```markdown\nsome ==highlighted text==!\n```\n\nmore!", parserOptions), + ).toEqual([]); + expect( + parse("# Title\n```markdown\nsome **bolded text**!\n```\n\nmore!", parserOptions), + ).toEqual([]); + expect(parse("# Title\n\n```\nfoo = {{'a': 2}}\n```\n\nmore!", parserOptions)).toEqual([]); + + // inline + expect(parse("`Inlineq::InlineA`", parserOptions)).toEqual([]); + expect( + parse("# Title\n`if (a & b) {}`\nmore!", { + singleLineCardSeparator: "&", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: true, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: true, + }), + ).toEqual([]); + expect(parse("# Title\n`if a == b && c == d {}`\nmore!", parserOptions)).toEqual([]); + expect(parse("# Title\n\n`z = a ** b + 5 ** 7`\n\nmore!", parserOptions)).toEqual([]); + expect(parse("# Title\n`foo = {{'a': 2}}`\n\nmore!", parserOptions)).toEqual([]); + + // combo + expect( + parse( + "Question::Answer\n\n```\nCodeblockq::CodeblockA\n```\n\n`Inlineq::InlineA`\n", + parserOptions, + ), + ).toEqual([[CardType.SingleLineBasic, "Question::Answer", 0, 0]]); }); test("Unexpected Error case", () => {