diff --git a/.gitignore b/.gitignore index da93af9..b15c6be 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ move.sh # Test-vault docs/test-vault/.obsidian/workspace + +# Coverage results +coverage/ diff --git a/package.json b/package.json index b58eeae..62bfdc8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@types/showdown": "^1.9.3", - "showdown": "^1.9.1" + "showdown": "^1.9.1", + "ts-dedent": "^2.2.0" } } diff --git a/src/conf/regex.ts b/src/conf/regex.ts index d91af0b..cf091a2 100644 --- a/src/conf/regex.ts +++ b/src/conf/regex.ts @@ -33,13 +33,10 @@ export class Regex { this.headingsRegex = /^ {0,3}(#{1,6}) +([^\n]+?) ?((?: *#\S+)*) *$/gim; // Supported images https://publish.obsidian.md/help/How+to/Embed+files - this.wikiImageLinks = - /!\[\[(.*\.(?:png|jpg|jpeg|gif|bmp|svg|tiff)).*?\]\]/gim; - this.markdownImageLinks = - /!\[\]\((.*\.(?:png|jpg|jpeg|gif|bmp|svg|tiff)).*?\)/gim; + this.wikiImageLinks = /!\[\[(.*\.(?:png|jpg|jpeg|gif|bmp|svg|tiff)).*?\]\]/gim; + this.markdownImageLinks = /!\[\]\((.*\.(?:png|jpg|jpeg|gif|bmp|svg|tiff)).*?\)/gim; - this.wikiAudioLinks = - /!\[\[(.*\.(?:mp3|webm|wav|m4a|ogg|3gp|flac)).*?\]\]/gim; + this.wikiAudioLinks = /!\[\[(.*\.(?:mp3|webm|wav|m4a|ogg|3gp|flac)).*?\]\]/gim; // https://regex101.com/r/eqnJeW/1 this.obsidianCodeBlock = /(?:```(?:.*?\n?)+?```)(?:\n|$)/gim; @@ -53,8 +50,7 @@ export class Regex { this.cardsToDelete = /^\s*(?:\n)(?:\^(\d{13}))(?:\n\s*?)?/gm; // https://regex101.com/r/WxuFI2/1 - this.globalTagsSplitter = - /\[\[(.*?)\]\]|#([\p{L}\d:\-_/]+)|([\p{L}\d:\-_/]+)/gimu; + this.globalTagsSplitter = /\[\[(.*?)\]\]|#([\p{L}\d:\-_/]+)|([\p{L}\d:\-_/]+)/gimu; this.tagHierarchy = /\//gm; // Cards @@ -66,17 +62,31 @@ export class Regex { "(?:[/-]reverse)?)((?: *#[\\p{Number}\\p{Letter}\\-\\/_]+)*) *?\\n+((?:[^\\n]\\n?)*?(?=\\^\\d{13}|$))(?:\\^(\\d{13}))?"; this.flashscardsWithTag = new RegExp(str, flags); - // https://regex101.com/r/8wmOo8/1 - const sepLongest = settings.inlineSeparator.length >= settings.inlineSeparatorReverse.length ? settings.inlineSeparator : settings.inlineSeparatorReverse; - const sepShortest = settings.inlineSeparator.length < settings.inlineSeparatorReverse.length ? settings.inlineSeparator : settings.inlineSeparatorReverse; + // https://regex101.com/r/8wmOo8/2 + const sepLongest = + settings.inlineSeparator.length >= settings.inlineSeparatorReverse.length + ? settings.inlineSeparator + : settings.inlineSeparatorReverse; + const sepShortest = + settings.inlineSeparator.length < settings.inlineSeparatorReverse.length + ? settings.inlineSeparator + : settings.inlineSeparatorReverse; // sepLongest is the longest between the inlineSeparator and the inlineSeparatorReverse because if the order is ::|::: then always the first will be matched // sepShortest is the shortest if (settings.inlineID) { str = - "( {0,3}[#]{0,6})?(?:(?:[\\t ]*)(?:\\d.|[-+*]|#{1,6}))?(.+?) ?(" + sepLongest + "|" + sepShortest + ") ?(.+?)((?: *#[\\p{Letter}\\-\\/_]+)+)?(?:\\s+\\^(\\d{13})|$)"; + "( {0,3}[#]{0,6})?(?:(?:[\\t ]*)(?:\\d\\.|[-+*]|#{1,6}))?(.+?) ?(" + + sepLongest + + "|" + + sepShortest + + ") ?(.+?)((?: *#[\\p{Letter}\\-\\/_]+)+)?(?:\\s+\\^(\\d{13})|$)"; } else { str = - "( {0,3}[#]{0,6})?(?:(?:[\\t ]*)(?:\\d.|[-+*]|#{1,6}))?(.+?) ?(" + sepLongest + "|" + sepShortest + ") ?(.+?)((?: *#[\\p{Letter}\\-\\/_]+)+|$)(?:\\n\\^(\\d{13}))?"; + "( {0,3}[#]{0,6})?(?:(?:[\\t ]*)(?:\\d\\.|[-+*]|#{1,6}))?(.+?) ?(" + + sepLongest + + "|" + + sepShortest + + ") ?(.+?)((?: *#[\\p{Letter}\\-\\/_]+)+|$)(?:\\n\\^(\\d{13}))?"; } this.cardsInlineStyle = new RegExp(str, flags); @@ -89,9 +99,10 @@ export class Regex { // https://regex101.com/r/cgtnLf/1 - str = "( {0,3}[#]{0,6})?(?:(?:[\\t ]*)(?:\\d.|[-+*]|#{1,6}))?(.*?(==.+?==|\\{.+?\\}).*?)((?: *#[\\w\\-\\/_]+)+|$)(?:\n\\^(\\d{13}))?" + str = + "( {0,3}[#]{0,6})?(?:(?:[\\t ]*)(?:\\d.|[-+*]|#{1,6}))?(.*?(==.+?==|\\{.+?\\}).*?)((?: *#[\\w\\-\\/_]+)+|$)(?:\n\\^(\\d{13}))?"; this.cardsClozeWholeLine = new RegExp(str, flags); - + this.singleClozeCurly = /((?:{)(?:(\d):?)?(.+?)(?:}))/g; this.singleClozeHighlight = /((?:==)(.+?)(?:==))/g; diff --git a/tests/regex.test.ts b/tests/regex.test.ts new file mode 100644 index 0000000..fbc1806 --- /dev/null +++ b/tests/regex.test.ts @@ -0,0 +1,90 @@ +import dedent from "ts-dedent"; +import { Regex } from "../src/conf/regex"; + +describe("Regex class unit tests", () => { + describe("inline card pattern", () => { + it("should work with default settings", () => { + const regex = new Regex({ + contextAwareMode: true, + sourceSupport: false, + codeHighlightSupport: false, + inlineID: false, + contextSeparator: " > ", + deck: "Default", + folderBasedDeck: true, + flashcardsTag: "card", + inlineSeparator: "::", + inlineSeparatorReverse: ":::", + defaultAnkiTag: "obsidian", + ankiConnectPermission: false, + }); + + type Match = [string, string]; + + const candidates: [string, Match[]][] = [ + ["Question :: Answer", [["Question", "Answer"]]], + // FIXME: should be "Question" instead of " Question" + // ["# Question :: Answer", [[" Question", "Answer"]]], + // FIXME: should not match? (tag then separator) + // ["#Question :: Answer", []], + ["Answer ::: Question", [["Answer", "Question"]]], + ["Question :: Answer ^123456789", [["Question", "Answer ^123456789"]]], + ["Question :: Answer ^123456789\n^123456789", [["Question", "Answer ^123456789"]]], + // FIXME: should be "The question" instead of " The question" + ["## Question #card\n- The question :: Answer", [[" The question", "Answer"]]], + ["No match!", []], + // FIXME: should be "Question 1" instead of " Question 1" + [ + dedent` + 1. Question 1 :: Answer 1 + 2. Question 2 :: Answer 2 + 3. Question 3 :: Answer 3`, + [ + [" Question 1", "Answer 1"], + [" Question 2", "Answer 2"], + [" Question 3", "Answer 3"], + ], + ], + // FIXME: should be "Question 1" instead of " Question 1" + [ + dedent` + - Question 1 :: Answer 1 + - Question 2 :: Answer 2 + - Question 3 :: Answer 3`, + [ + [" Question 1", "Answer 1"], + [" Question 2", "Answer 2"], + [" Question 3", "Answer 3"], + ], + ], + [ + dedent` + 1. Item 1 :: A + 2. Item 2 :: B + - Item 2a :: Ba + - Item 2b :: Bb`, + [ + [" Item 1", "A"], + [" Item 2", "B"], + [" Item 2a", "Ba"], + [" Item 2b", "Bb"], + ], + ], + // FIXME: should not match? + // ["no :::: match", []], + ["2000 :: answer", [["2000", "answer"]]], + ]; + + for (const [input, expected] of candidates) { + const matches = [...input.matchAll(regex.cardsInlineStyle)]; + + expect(matches.length).toEqual(expected.length); + expected.forEach((exp, i) => { + const match = matches[i]; + expect(match[2]).toEqual(exp[0]); + expect(match[4]).toEqual(exp[1]); + }); + } + }); + }); +});