Repository: st3v3nmw/obsidian-spaced-repetition
Pull request #778 (2023-10-27 12:43:26)

Multi-line tag ('$$') converted to single-line ('$') after card reviewed
Fixes issue https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/766

Although not mentioned in that issue note, the bug also caused a problem with cards with the "$$" whilst being edited within card modal. The bug was in code used in both cases, and so has been fixed for both.


Patch:
diff --git a/src/util/MultiLineTextFinder.ts b/src/util/MultiLineTextFinder.ts
index bd5a43a..7977e6c 100644
--- a/src/util/MultiLineTextFinder.ts
+++ b/src/util/MultiLineTextFinder.ts
@@ -1,4 +1,4 @@
-import { splitTextIntoLineArray } from "./utils";
+import { literalStringReplace, splitTextIntoLineArray } from "./utils";
 
 export class MultiLineTextFinder {
     static findAndReplace(
@@ -8,7 +8,7 @@ export class MultiLineTextFinder {
     ): string | null {
         let result: string = null;
         if (sourceText.includes(searchText)) {
-            result = sourceText.replace(searchText, replacementText);
+            result = literalStringReplace(sourceText, searchText, replacementText);
         } else {
             const sourceTextArray = splitTextIntoLineArray(sourceText);
             const searchTextArray = splitTextIntoLineArray(searchText);
diff --git a/src/util/utils.ts b/src/util/utils.ts
index 1debe42..caa2869 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -38,6 +38,22 @@ export const getKeysPreserveType = Object.keys as <T extends Record<string, unkn
 export const escapeRegexString = (text: string): string =>
     text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 
+export function literalStringReplace(
+    text: string,
+    searchStr: string,
+    replacementStr: string,
+): string {
+    let result: string = text;
+    const startIdx: number = text.indexOf(searchStr);
+    if (startIdx >= 0) {
+        const startStr: string = text.substring(0, startIdx);
+        const endIdx: number = startIdx + searchStr.length;
+        const endStr: string = text.substring(endIdx);
+        result = startStr + replacementStr + endStr;
+    }
+    return result;
+}
+
 /**
  * Returns the cyrb53 hash (hex string) of the input string
  * Please see https://stackoverflow.com/a/52171480 for more details


Test patch:
diff --git a/tests/unit/FlashcardReviewSequencer.test.ts b/tests/unit/FlashcardReviewSequencer.test.ts
index dad014d..13f7024 100644
--- a/tests/unit/FlashcardReviewSequencer.test.ts
+++ b/tests/unit/FlashcardReviewSequencer.test.ts
@@ -658,6 +658,30 @@ Q1::A1
                 checkQuestionPostponementListCount(c, 0);
             });
         });
+
+        test("Answer includes MathJax within $$", async () => {
+            let fileText: string = `#flashcards
+What is Newman's equation for gravitational force
+?
+$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`;
+
+            let c: TestContext = TestContext.Create(
+                order_DueFirst_Sequential,
+                FlashcardReviewMode.Review,
+                DEFAULT_SETTINGS,
+                fileText,
+            );
+            await c.setSequencerDeckTreeFromOriginalText();
+            expect(c.reviewSequencer.currentCard.front).toContain("What is Newman's equation");
+
+            // Reviewing the card doesn't change the question, only adds the schedule info
+            await c.reviewSequencer.processReview(ReviewResponse.Easy);
+            let expectedFileText: string = `${fileText}
+<!--SR:!2023-09-10,4,270-->`;
+
+            let actual: string = await c.file.read();
+            expect(actual).toEqual(expectedFileText);
+        });
     });
 
     describe("FlashcardReviewMode.Cram", () => {
diff --git a/tests/unit/util/utils.test.ts b/tests/unit/util/utils.test.ts
new file mode 100644
index 0000000..d930316
--- /dev/null
+++ b/tests/unit/util/utils.test.ts
@@ -0,0 +1,74 @@
+import { literalStringReplace } from "src/util/utils";
+
+describe("literalStringReplace", () => {
+    test("Replacement string doesn't have any dollar signs", async () => {
+        const actual: string = literalStringReplace(
+            "Original string without dollar signs",
+            "dollar",
+            "pound",
+        );
+        expect(actual).toEqual("Original string without pound signs");
+    });
+
+    test("Replacement string has double dollar signs", async () => {
+        const actual: string = literalStringReplace(
+            "Original string without dollar signs",
+            "dollar",
+            "$",
+        );
+        expect(actual).toEqual("Original string without $ signs");
+    });
+
+    test("Original and search strings has double dollar signs at the end", async () => {
+        const originalStr: string = `Some stuff at the start
+
+Something
+?
+$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`;
+
+        const searchStr: string = `Something
+?
+$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`;
+
+        const replacementStr: string = `Something
+?
+$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$
+<!--SR:!2023-09-10,4,270-->`;
+
+        const expectedStr: string = `Some stuff at the start
+
+Something
+?
+$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$
+<!--SR:!2023-09-10,4,270-->`;
+
+        const actual: string = literalStringReplace(originalStr, searchStr, replacementStr);
+        expect(actual).toEqual(expectedStr);
+    });
+
+    test("Original and search strings has double dollar signs at the end", async () => {
+        const originalStr: string = `Some stuff at the start $$`;
+
+        const searchStr: string = `start $$`;
+
+        const replacementStr: string = `start $$ and end`;
+
+        const expectedStr: string = `Some stuff at the start $$ and end`;
+
+        const actual: string = literalStringReplace(originalStr, searchStr, replacementStr);
+        expect(actual).toEqual(expectedStr);
+    });
+
+    test("Search string not found", async () => {
+        const originalStr: string = "A very boring string";
+
+        const searchStr: string = "missing";
+
+        const replacementStr: string = "replacement";
+
+        const expectedStr: string = originalStr;
+
+        const actual: string = literalStringReplace(originalStr, searchStr, replacementStr);
+        expect(actual).toEqual(expectedStr);
+    });
+});