Repository: st3v3nmw/obsidian-spaced-repetition
Pull request #818 (2023-12-19 13:58:45)

Bug 800/812 nested lists indentation
Fixes bugs #800, #812 


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 1b01a23..f89266c 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
 
 #### [Unreleased]
 
+-   Bug fix Problem with nested list item's indentation [`#800`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/800)
+-   Bug fix Problem with nested list item's indentation [`#812`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/812)
 -   Bug Cloze Breaks When }} Encountered [`#799`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/799)
 -   Bug fix: When reviewing an entire note, the metadata is applied incorrectly (on top of the already existing metadata) [`#776`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/776)
 
diff --git a/src/Question.ts b/src/Question.ts
index c94d879..58dda1d 100644
--- a/src/Question.ts
+++ b/src/Question.ts
@@ -1,11 +1,15 @@
 import { Card } from "./Card";
 import { CardScheduleInfo, NoteCardScheduleParser } from "./CardSchedule";
-import { SR_HTML_COMMENT_BEGIN, SR_HTML_COMMENT_END } from "./constants";
+import {
+    OBSIDIAN_TAG_AT_STARTOFLINE_REGEX,
+    SR_HTML_COMMENT_BEGIN,
+    SR_HTML_COMMENT_END,
+} from "./constants";
 import { Note } from "./Note";
 import { SRSettings } from "./settings";
-import { TopicPath } from "./TopicPath";
+import { TopicPath, TopicPathWithWs } from "./TopicPath";
 import { MultiLineTextFinder } from "./util/MultiLineTextFinder";
-import { cyrb53 } from "./util/utils";
+import { cyrb53, stringTrimStart } from "./util/utils";
 
 export enum CardType {
     SingleLineBasic,
@@ -17,9 +21,39 @@ export enum CardType {
 
 //
 // QuestionText comprises the following components:
-//      1. QuestionTopicPath (optional)
+//      1. QuestionTopicPath (optional, and if present there may be whitespace before)
+//
 //      2. Actual question text (mandatory)
-//      3. Card schedule info as HTML comment (optional)
+//
+//      3. Card schedule info as HTML comment (optional). If present then there is
+//          optional whitespace after the question text, before this.
+//          (whitespace always included when text is generated by formatForNote(), would only
+//          be missing if manually removed by the user)
+//
+// Actual Question - Whitespace Handling
+//
+//      It is important that whitespace is maintained accurately by this class.
+//
+//      **Leading Whitespace**
+//
+//          It's important to retain the leading whitespace in the case where there is no QuestionTopicPath,
+//          as leading whitespace is an indicator in markdown of the indent level.
+//          see "[BUG] Problem with nested list item's indentation"
+//          https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/800
+//
+//          In the case where QuestionTopicPath is present, whitespace pre and post QuestionTopicPath
+//          are retained so that if the question is written back to the file, for aesthetic reasons
+//          there won't be any change to the whitespace.
+//
+//          However, actualQuestion will not have any leading spaces.
+//
+//      **Trailing Whitespace**
+//
+//         Trailing whitespace is always removed.
+//
+//         This is because Question.formatForNote() uses the whitespace generated by Question.getHtmlCommentSeparator()
+//         as the separator between the end of the question text and the OSR html comment -
+//         either a single space or a new line (settings based)
 //
 // For example
 //
@@ -37,28 +71,19 @@ export class QuestionText {
     original: string;
 
     // The question topic path (only present if topic path included in original text)
-    topicPath: TopicPath;
-
-    // The white space after the topic path, before the actualQuestion
-    // We keep this so that when a question is updated, we can retain the original spacing
-    postTopicPathWhiteSpace: string;
+    // If present, it also includes whitespace before and after the topic path itself
+    topicPathWithWs: TopicPathWithWs;
 
-    // Just the question text, e.g. "Q1::A1"
+    // The question text, e.g. "Q1::A1" with leading/trailing whitespace as described above
     actualQuestion: string;
 
     // Hash of string  (topicPath + actualQuestion)
     // Explicitly excludes the HTML comment with the scheduling info
     textHash: string;
 
-    constructor(
-        original: string,
-        topicPath: TopicPath,
-        postTopicPathWhiteSpace: string,
-        actualQuestion: string,
-    ) {
+    constructor(original: string, topicPathWithWs: TopicPathWithWs, actualQuestion: string) {
         this.original = original;
-        this.topicPath = topicPath;
-        this.postTopicPathWhiteSpace = postTopicPathWhiteSpace;
+        this.topicPathWithWs = topicPathWithWs;
         this.actualQuestion = actualQuestion;
         this.textHash = cyrb53(this.formatForNote());
     }
@@ -68,38 +93,43 @@ export class QuestionText {
     }
 
     static create(original: string, settings: SRSettings): QuestionText {
-        const [topicPath, postTopicPathWhiteSpace, actualQuestion] = this.splitText(
-            original,
-            settings,
-        );
+        const [topicPathWithWs, actualQuestion] = this.splitText(original, settings);
 
-        return new QuestionText(original, topicPath, postTopicPathWhiteSpace, actualQuestion);
+        return new QuestionText(original, topicPathWithWs, actualQuestion);
     }
 
-    static splitText(original: string, settings: SRSettings): [TopicPath, string, string] {
-        const strippedSR = NoteCardScheduleParser.removeCardScheduleInfo(original).trim();
-        let actualQuestion: string = strippedSR;
-        let whiteSpace: string = "";
+    static splitText(original: string, settings: SRSettings): [TopicPathWithWs, string] {
+        const originalWithoutSR = NoteCardScheduleParser.removeCardScheduleInfo(original);
+        let actualQuestion: string = originalWithoutSR.trimEnd();
 
-        let topicPath: TopicPath = TopicPath.emptyPath;
+        let topicPathWithWs: TopicPathWithWs = null;
         if (!settings.convertFoldersToDecks) {
-            const t = TopicPath.getTopicPathFromCardText(strippedSR);
-            if (t?.hasPath) {
-                topicPath = t;
-                [actualQuestion, whiteSpace] =
-                    TopicPath.removeTopicPathFromStartOfCardText(strippedSR);
+            const topicPath = TopicPath.getTopicPathFromCardText(originalWithoutSR);
+            if (topicPath?.hasPath) {
+                // originalWithoutSR - preTopicPathWs TopicPath postTopicPathWs Question
+                // cardText1 - TopicPath postTopicPathWs Question
+                const [preTopicPathWs, cardText1] = stringTrimStart(originalWithoutSR);
+                const actualQuestionWithWs: string = cardText1.replaceAll(
+                    OBSIDIAN_TAG_AT_STARTOFLINE_REGEX,
+                    "",
+                );
+
+                // actualQuestionWithWs - postTopicPathWs Question
+                let postTopicPathWs: string;
+                [postTopicPathWs, actualQuestion] = stringTrimStart(actualQuestionWithWs);
+                topicPathWithWs = new TopicPathWithWs(topicPath, preTopicPathWs, postTopicPathWs);
             }
         }
 
-        return [topicPath, whiteSpace, actualQuestion];
+        return [topicPathWithWs, actualQuestion];
     }
 
     formatForNote(): string {
         let result: string = "";
-        if (this.topicPath.hasPath) {
-            result += this.topicPath.formatAsTag();
-            result += this.postTopicPathWhiteSpace ?? " ";
+        if (this.topicPathWithWs) {
+            result += this.topicPathWithWs.formatWithWs();
         }
+
         result += this.actualQuestion;
         return result;
     }
@@ -153,8 +183,10 @@ export class Question {
     formatForNote(settings: SRSettings): string {
         let result: string = this.questionText.formatForNote();
         if (this.cards.some((card) => card.hasSchedule)) {
-            result +=
-                this.getHtmlCommentSeparator(settings) + this.formatScheduleAsHtmlComment(settings);
+            result =
+                result.trimEnd() +
+                this.getHtmlCommentSeparator(settings) +
+                this.formatScheduleAsHtmlComment(settings);
         }
         return result;
     }
@@ -203,8 +235,8 @@ export class Question {
         const questionText: QuestionText = QuestionText.create(originalText, settings);
 
         let topicPath: TopicPath = noteTopicPath;
-        if (questionText.topicPath.hasPath) {
-            topicPath = questionText.topicPath;
+        if (questionText.topicPathWithWs) {
+            topicPath = questionText.topicPathWithWs.topicPath;
         }
 
         const result: Question = new Question({
diff --git a/src/QuestionType.ts b/src/QuestionType.ts
index fbfc0ee..0a65d8d 100644
--- a/src/QuestionType.ts
+++ b/src/QuestionType.ts
@@ -5,9 +5,10 @@ export class CardFrontBack {
     front: string;
     back: string;
 
+    // The caller is responsible for any required trimming of leading/trailing spaces
     constructor(front: string, back: string) {
-        this.front = front.trim();
-        this.back = back.trim();
+        this.front = front;
+        this.back = back;
     }
 }
 
diff --git a/src/TopicPath.ts b/src/TopicPath.ts
index 2391406..6ced4af 100644
--- a/src/TopicPath.ts
+++ b/src/TopicPath.ts
@@ -1,6 +1,7 @@
 import { SRSettings } from "src/settings";
 import { OBSIDIAN_TAG_AT_STARTOFLINE_REGEX } from "./constants";
 import { ISRFile } from "./SRFile";
+import { stringTrimStart } from "./util/utils";
 
 export class TopicPath {
     path: string[];
@@ -80,16 +81,6 @@ export class TopicPath {
         return path?.length > 0 ? TopicPath.getTopicPathFromTag(path) : null;
     }
 
-    static removeTopicPathFromStartOfCardText(cardText: string): [string, string] {
-        const cardText1: string = cardText
-            .trimStart()
-            .replaceAll(OBSIDIAN_TAG_AT_STARTOFLINE_REGEX, "");
-        const cardText2: string = cardText1.trimStart();
-        const whiteSpaceLength: number = cardText1.length - cardText2.length;
-        const whiteSpace: string = cardText1.substring(0, whiteSpaceLength);
-        return [cardText2, whiteSpace];
-    }
-
     static getTopicPathsFromTagList(tagList: string[]): TopicPath[] {
         const result: TopicPath[] = [];
         for (const tag of tagList) {
@@ -118,3 +109,25 @@ export class TopicPath {
         return new TopicPath(path);
     }
 }
+
+export class TopicPathWithWs {
+    topicPath: TopicPath;
+
+    // The white space prior to the topic path
+    // We keep this so that when a question is updated, we can retain the original spacing
+    preWhitespace: string;
+
+    postWhitespace: string;
+
+    constructor(topicPath: TopicPath, preWhitespace: string, postWhitespace: string) {
+        if (!topicPath || topicPath.isEmptyPath) throw "topicPath null";
+
+        this.topicPath = topicPath;
+        this.preWhitespace = preWhitespace;
+        this.postWhitespace = postWhitespace;
+    }
+
+    formatWithWs(): string {
+        return `${this.preWhitespace}${this.topicPath.formatAsTag()}${this.postWhitespace}`;
+    }
+}
diff --git a/src/util/utils.ts b/src/util/utils.ts
index caa2869..6517c83 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -95,3 +95,10 @@ export function getAllTagsFromText(text: string): string[] {
 export function splitTextIntoLineArray(text: string): string[] {
     return text.replaceAll("\r\n", "\n").split("\n");
 }
+
+export function stringTrimStart(str: string): [string, string] {
+    const trimmed: string = str.trimStart();
+    const wsCount: number = str.length - trimmed.length;
+    const ws: string = str.substring(0, wsCount);
+    return [ws, trimmed];
+}


Test patch:
diff --git a/tests/unit/DeckTreeIterator.test.ts b/tests/unit/DeckTreeIterator.test.ts
index 92cb6f9..018294b 100644
--- a/tests/unit/DeckTreeIterator.test.ts
+++ b/tests/unit/DeckTreeIterator.test.ts
@@ -42,9 +42,9 @@ beforeAll(() => {
 describe("setDeck", () => {
     test("currentDeck null immediately after setDeck", async () => {
         let text: string = `
-        Q1::A1
-        Q2::A2
-        Q3::A3`;
+Q1::A1
+Q2::A2
+Q3::A3`;
         let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
         let iterator: DeckTreeIterator = new DeckTreeIterator(
             order_NewFirst_Sequential,
@@ -60,9 +60,9 @@ describe("nextCard", () => {
         describe("Due cards before new cards", () => {
             test("Single topic, new cards only", async () => {
                 let text: string = `
-                Q1::A1
-                Q2::A2
-                Q3::A3`;
+Q1::A1
+Q2::A2
+Q3::A3`;
                 let deck: Deck = await SampleItemDecks.createDeckFromText(
                     text,
                     new TopicPath(["Root"]),
@@ -90,12 +90,12 @@ describe("nextCard", () => {
             describe("Single topic, mixture of new and scheduled cards", () => {
                 test("Get the scheduled cards first", async () => {
                     let text: string = `
-                    Q1::A1
-                    Q2::A2 <!--SR:!2023-09-02,4,270-->
-                    Q3::A3
-                    Q4::A4 <!--SR:!2023-09-02,4,270-->
-                    Q5::A5 <!--SR:!2023-09-02,4,270-->
-                    Q6::A6`;
+Q1::A1
+Q2::A2 <!--SR:!2023-09-02,4,270-->
+Q3::A3
+Q4::A4 <!--SR:!2023-09-02,4,270-->
+Q5::A5 <!--SR:!2023-09-02,4,270-->
+Q6::A6`;
                     let deck: Deck = await SampleItemDecks.createDeckFromText(
                         text,
                         new TopicPath(["Root"]),
@@ -173,9 +173,9 @@ describe("nextCard", () => {
         describe("New cards before due cards", () => {
             test("Single topic, new cards only", async () => {
                 let text: string = `
-                Q1::A1
-                Q2::A2
-                Q3::A3`;
+Q1::A1
+Q2::A2
+Q3::A3`;
                 let deck: Deck = await SampleItemDecks.createDeckFromText(
                     text,
                     new TopicPath(["Root"]),
@@ -202,12 +202,12 @@ describe("nextCard", () => {
             describe("Single topic, mixture of new and scheduled cards", () => {
                 test("Get the new cards first", async () => {
                     let text: string = `
-                    Q1::A1
-                    Q2::A2 <!--SR:!2023-09-02,4,270-->
-                    Q3::A3
-                    Q4::A4 <!--SR:!2023-09-02,4,270-->
-                    Q5::A5 <!--SR:!2023-09-02,4,270-->
-                    Q6::A6`;
+Q1::A1
+Q2::A2 <!--SR:!2023-09-02,4,270-->
+Q3::A3
+Q4::A4 <!--SR:!2023-09-02,4,270-->
+Q5::A5 <!--SR:!2023-09-02,4,270-->
+Q6::A6`;
                     let deck: Deck = await SampleItemDecks.createDeckFromText(
                         text,
                         new TopicPath(["Root"]),
@@ -234,12 +234,12 @@ describe("nextCard", () => {
 
                 test("Get the scheduled cards first", async () => {
                     let text: string = `
-                    Q1::A1
-                    Q2::A2 <!--SR:!2023-09-02,4,270-->
-                    Q3::A3
-                    Q4::A4 <!--SR:!2023-09-02,4,270-->
-                    Q5::A5 <!--SR:!2023-09-02,4,270-->
-                    Q6::A6`;
+Q1::A1
+Q2::A2 <!--SR:!2023-09-02,4,270-->
+Q3::A3
+Q4::A4 <!--SR:!2023-09-02,4,270-->
+Q5::A5 <!--SR:!2023-09-02,4,270-->
+Q6::A6`;
                     let deck: Deck = await SampleItemDecks.createDeckFromText(
                         text,
                         new TopicPath(["Root"]),
@@ -317,13 +317,13 @@ describe("nextCard", () => {
         describe("Due cards before new cards", () => {
             test("All new cards", async () => {
                 let text: string = `
-                Q0::A0
-                Q1::A1
-                Q2::A2
-                Q3::A3
-                Q4::A4
-                Q5::A5
-                Q6::A6`;
+Q0::A0
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4
+Q5::A5
+Q6::A6`;
                 let deck: Deck = await SampleItemDecks.createDeckFromText(
                     text,
                     new TopicPath(["Root"]),
@@ -362,14 +362,14 @@ describe("nextCard", () => {
 
             test("Mixture new/scheduled", async () => {
                 let text: string = `
-                QN0::A
-                QS0::A <!--SR:!2023-09-02,4,270-->
-                QN1::A
-                QS1::A <!--SR:!2023-09-02,4,270-->
-                QS2::A <!--SR:!2023-09-02,4,270-->
-                QN2::A
-                QN3::A
-                QS3::Q <!--SR:!2023-09-02,4,270-->`;
+QN0::A
+QS0::A <!--SR:!2023-09-02,4,270-->
+QN1::A
+QS1::A <!--SR:!2023-09-02,4,270-->
+QS2::A <!--SR:!2023-09-02,4,270-->
+QN2::A
+QN3::A
+QS3::Q <!--SR:!2023-09-02,4,270-->`;
                 let deck: Deck = await SampleItemDecks.createDeckFromText(
                     text,
                     new TopicPath(["Root"]),
diff --git a/tests/unit/FlashcardReviewSequencer.test.ts b/tests/unit/FlashcardReviewSequencer.test.ts
index 13f7024..87e2a3a 100644
--- a/tests/unit/FlashcardReviewSequencer.test.ts
+++ b/tests/unit/FlashcardReviewSequencer.test.ts
@@ -684,6 +684,36 @@ $$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`;
         });
     });
 
+    describe("Checking leading/trailing spaces", () => {
+        test("Leading spaces are retained post review", async () => {
+            // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/800
+            let settings: SRSettings = { ...DEFAULT_SETTINGS };
+            settings.burySiblingCards = true;
+            let indent: string = "    ";
+
+            // Note that "- bar?::baz" is intentionally indented
+            let text: string = `
+- foo
+${indent}- bar?::baz
+`;
+
+            let c: TestContext = TestContext.Create(
+                order_DueFirst_Sequential,
+                FlashcardReviewMode.Review,
+                settings,
+                text,
+            );
+            await c.setSequencerDeckTreeFromOriginalText();
+
+            expect(c.reviewSequencer.currentCard.front).toMatch(`${indent}- bar?`);
+
+            // After reviewing, check the text (explicitly text includes the whitespace before "- bar?::baz"at)
+            await c.reviewSequencer.processReview(ReviewResponse.Easy);
+            const expectedText: string = `${text}<!--SR:!2023-09-10,4,270-->\n`;
+            expect(await c.file.read()).toEqual(expectedText);
+        });
+    });
+
     describe("FlashcardReviewMode.Cram", () => {
         describe("ReviewResponse.Easy", () => {
             test("Next card after reviewed card becomes current; reviewed easy card doesn't resurface", async () => {
diff --git a/tests/unit/NoteQuestionParser.test.ts b/tests/unit/NoteQuestionParser.test.ts
index 323f9cb..b02754f 100644
--- a/tests/unit/NoteQuestionParser.test.ts
+++ b/tests/unit/NoteQuestionParser.test.ts
@@ -48,7 +48,6 @@ A::B
                 questionText: {
                     original: `A::B`,
                     actualQuestion: "A::B",
-                    topicPath: TopicPath.emptyPath,
                 },
 
                 lineNo: 1,
@@ -88,7 +87,6 @@ A::B
                     original: `A::B
 <!--SR:!2023-09-03,1,230-->`,
                     actualQuestion: "A::B",
-                    topicPath: TopicPath.emptyPath,
                     textHash: "1c6b0b01215dc4",
                 },
                 lineNo: 1,
diff --git a/tests/unit/Question.test.ts b/tests/unit/Question.test.ts
index d61c466..e9b901e 100644
--- a/tests/unit/Question.test.ts
+++ b/tests/unit/Question.test.ts
@@ -13,7 +13,7 @@ describe("Question", () => {
                 "```\nprint('Hello World!')\nprint('Howdy?')\nlambda x: x[0]\n```";
 
             let question: Question = new Question({
-                questionText: new QuestionText(text, TopicPath.emptyPath, "", text),
+                questionText: new QuestionText(text, null, text),
             });
 
             expect(question.getHtmlCommentSeparator(DEFAULT_SETTINGS)).toEqual("\n");
@@ -24,7 +24,7 @@ describe("Question", () => {
             let text: string = "Q1::A1";
 
             let question: Question = new Question({
-                questionText: new QuestionText(text, TopicPath.emptyPath, "", text),
+                questionText: new QuestionText(text, null, text),
             });
 
             expect(question.getHtmlCommentSeparator(DEFAULT_SETTINGS)).toEqual("\n");
diff --git a/tests/unit/TopicPath.test.ts b/tests/unit/TopicPath.test.ts
index ba77f02..e6bb2ab 100644
--- a/tests/unit/TopicPath.test.ts
+++ b/tests/unit/TopicPath.test.ts
@@ -87,50 +87,6 @@ describe("getTopicPathFromCardText", () => {
     });
 });
 
-describe("removeTopicPathFromCardText", () => {
-    test("Card text doesn't include tag", () => {
-        let cardText: string = "Card text doesn't include tag";
-        let expectedCardText: string = cardText;
-        let [actualQuestion, whiteSpace] = TopicPath.removeTopicPathFromStartOfCardText(cardText);
-
-        expect(actualQuestion).toEqual(expectedCardText);
-        expect(whiteSpace).toEqual("");
-    });
-
-    test("Card text includes single level tag", () => {
-        let cardText: string = "#flashcards Card text does include tag";
-        let [actualQuestion, whiteSpace] = TopicPath.removeTopicPathFromStartOfCardText(cardText);
-
-        expect(actualQuestion).toEqual("Card text does include tag");
-        expect(whiteSpace).toEqual(" ");
-    });
-
-    test("Card text includes multi level tag", () => {
-        let cardText: string = "#flashcards/science/chemistry Card text does include tag";
-        let [actualQuestion, whiteSpace] = TopicPath.removeTopicPathFromStartOfCardText(cardText);
-
-        expect(actualQuestion).toEqual("Card text does include tag");
-        expect(whiteSpace).toEqual(" ");
-    });
-
-    test("White space present before topic tag", () => {
-        let cardText: string = "   #flashcards/science/chemistry Card text does include tag";
-        let [actualQuestion, whiteSpace] = TopicPath.removeTopicPathFromStartOfCardText(cardText);
-
-        expect(actualQuestion).toEqual("Card text does include tag");
-        expect(whiteSpace).toEqual(" ");
-    });
-
-    test("Multiple spaces after topic tag", () => {
-        let spaces: string = "    ";
-        let cardText: string = `#flashcards/science/chemistry${spaces}Card text does include tag`;
-        let [actualQuestion, whiteSpace] = TopicPath.removeTopicPathFromStartOfCardText(cardText);
-
-        expect(actualQuestion).toEqual("Card text does include tag");
-        expect(whiteSpace).toEqual(spaces);
-    });
-});
-
 describe("getTopicPathFromTag", () => {
     test("Null string", () => {
         const t = () => {
diff --git a/tests/unit/deck.test.ts b/tests/unit/deck.test.ts
index 092f042..1fe0960 100644
--- a/tests/unit/deck.test.ts
+++ b/tests/unit/deck.test.ts
@@ -195,9 +195,9 @@ describe("copyWithCardFilter()", () => {
 
         test("With new cards", async () => {
             let text: string = `
-            Q1::A1
-            Q2::A2
-            Q3::A3`;
+Q1::A1
+Q2::A2
+Q3::A3`;
             let original: Deck = await SampleItemDecks.createDeckFromText(
                 text,
                 new TopicPath(["Root"]),
@@ -210,9 +210,9 @@ describe("copyWithCardFilter()", () => {
 
         test("With scheduled cards", async () => {
             let text: string = `
-            Q1::A1 <!--SR:!2023-09-02,4,270-->
-            Q2::A2 <!--SR:!2023-09-02,4,270-->
-            Q3::A3 <!--SR:!2023-09-02,4,270-->`;
+Q1::A1 <!--SR:!2023-09-02,4,270-->
+Q2::A2 <!--SR:!2023-09-02,4,270-->
+Q3::A3 <!--SR:!2023-09-02,4,270-->`;
             let original: Deck = await SampleItemDecks.createDeckFromText(
                 text,
                 new TopicPath(["Root"]),