Repository: st3v3nmw/obsidian-spaced-repetition
Pull request #927 (2024-04-09 01:30:37)

Missing flashcards (bug introduced in 1.12.0)
This fixes:
- [[BUG] Plugin not picking up certain flashcards #915](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/915)
- [[BUG] Unable to recognize multi-line card that begins immediately after the frontmatter's closing line #922](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/922)
- [[BUG] Most of my flashcards are now missing #923](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/923)

@st3v3nmw I've marked this as draft whilst waiting for final confirmation from user who raised issue #923 

Also, probably there will be conflicts between this and PR https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/926.

If you approve and merge one of the PRs, then I'll "update from upstream master" on my branch related to the other PR and resolve any conflicts.

If you prefer a different method of resolving the anticipated conflicts, please let me know.

Cheers
Ronny



ronzulu: Hi @st3v3nmw I haven't heard back from the OP of #923,  but I have checked with the file he posted, and all ok.

Because it's an important fix, I don't want to delay any longer.

Cheers
Ronny

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index b444307..d5e07a0 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. Dates are d
 
 Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
 
+#### [Unreleased]
+
+-   [BUG] Plugin not picking up certain flashcards https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/915
+-   [BUG] Unable to recognize multi-line card that begins immediately after the frontmatter's closing line https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/922
+-   [BUG] Most of my flashcards are now missing https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/923
+
 #### [1.12.3](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.12.2...1.12.3)
 
 -   Fixed slow load of flashcard modal (bug introduced in 1.12.0) [`#926`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/926)
diff --git a/src/NoteQuestionParser.ts b/src/NoteQuestionParser.ts
index 208a03a..0b8a430 100644
--- a/src/NoteQuestionParser.ts
+++ b/src/NoteQuestionParser.ts
@@ -5,7 +5,7 @@ import { parseEx, ParsedQuestionInfo } from "./parser";
 import { Question, QuestionText } from "./Question";
 import { CardFrontBack, CardFrontBackUtil } from "./QuestionType";
 import { SRSettings, SettingsUtil } from "./settings";
-import { ISRFile } from "./SRFile";
+import { ISRFile, frontmatterTagPseudoLineNum } from "./SRFile";
 import { TopicPath, TopicPathList } from "./TopicPath";
 import { extractFrontmatter, splitTextIntoLineArray } from "./util/utils";
 
@@ -14,10 +14,24 @@ export class NoteQuestionParser {
     noteFile: ISRFile;
     folderTopicPath: TopicPath;
     noteText: string;
+    frontmatterText: string;
+
+    // This is the note text, but with the frontmatter blanked out (see extractFrontmatter for reasoning)
+    contentText: string;
     noteLines: string[];
+
+    // Complete list of tags
     tagCacheList: TagCache[];
+
+    // tagCacheList filtered to those specified in the user settings (e.g. "#flashcards")
+    flashcardTagList: TagCache[];
+
+    // flashcardTagList filtered to those within the frontmatter
     frontmatterTopicPathList: TopicPathList;
+
+    // flashcardTagList filtered to those within the note's content and are note-level tags (i.e. not question specific)
     contentTopicPathInfo: TopicPathList[];
+
     questionList: Question[];
 
     constructor(settings: SRSettings) {
@@ -47,6 +61,7 @@ export class NoteQuestionParser {
 
             // The following analysis can require fair computation.
             // There is no point doing it if there aren't any topic paths
+            [this.frontmatterText, this.contentText] = extractFrontmatter(noteText);
 
             // Create the question list
             this.questionList = this.doCreateQuestionList(
@@ -114,9 +129,10 @@ export class NoteQuestionParser {
     }
 
     private parseQuestions(): ParsedQuestionInfo[] {
+        // We pass contentText which has the frontmatter blanked out; see extractFrontmatter for reasoning
         const settings: SRSettings = this.settings;
         const result: ParsedQuestionInfo[] = parseEx(
-            this.noteText,
+            this.contentText,
             settings.singleLineCardSeparator,
             settings.singleLineReversedCardSeparator,
             settings.multilineCardSeparator,
@@ -178,60 +194,104 @@ export class NoteQuestionParser {
     //      within frontmatter appear on separate lines)
     //
     private analyseTagCacheList(tagCacheList: TagCache[]): [TopicPathList, TopicPathList[]] {
-        let frontmatterTopicPathList: TopicPathList = null;
-        const contentTopicPathList: TopicPathList[] = [] as TopicPathList[];
+        // The tag (e.g. "#flashcards") must be a valid flashcard tag as per the user settings
+        this.flashcardTagList = tagCacheList.filter((item) =>
+            SettingsUtil.isFlashcardTag(this.settings, item.tag),
+        );
+        if (this.flashcardTagList.length > 0) {
+            // To simplify analysis, sort the flashcard list ordered by line number
+            this.flashcardTagList.sort((a, b) => a.position.start.line - b.position.start.line);
+        }
+
+        let frontmatterLineCount: number = 0;
+        if (this.frontmatterText) {
+            frontmatterLineCount = splitTextIntoLineArray(this.frontmatterText).length;
+        }
+
+        const frontmatterTopicPathList: TopicPathList = this.determineFrontmatterTopicPathList(
+            this.flashcardTagList,
+            frontmatterLineCount,
+        );
+        const contentTopicPathList: TopicPathList[] = this.determineContentTopicPathList(
+            this.flashcardTagList,
+            frontmatterLineCount,
+        );
+
+        return [frontmatterTopicPathList, contentTopicPathList];
+    }
+
+    private determineFrontmatterTopicPathList(
+        flashcardTagList: TagCache[],
+        frontmatterLineCount: number,
+    ): TopicPathList {
+        let result: TopicPathList = null;
 
-        // Only keep tags that are:
+        // Filter for tags that are:
         //      1. specified in the user settings as flashcardTags, and
-        //      2. is not question specific (determined by line number)
-        const filteredTagCacheList: TagCache[] = tagCacheList.filter(
+        //      2. is not question specific (determined by line number) - i.e. is "note level"
+        const noteLevelTagList: TagCache[] = flashcardTagList.filter(
             (item) =>
-                SettingsUtil.isFlashcardTag(this.settings, item.tag) &&
-                this.questionList.every(
-                    (q) => !q.parsedQuestionInfo.isQuestionLineNum(item.position.start.line),
-                ),
+                item.position.start.line == frontmatterTagPseudoLineNum &&
+                this.isNoteLevelFlashcardTag(item),
         );
-        let frontmatterLineCount: number = null;
-        if (filteredTagCacheList.length > 0) {
-            // To simplify analysis, ensure that the supplied list is ordered by line number
-            filteredTagCacheList.sort((a, b) => a.position.start.line - b.position.start.line);
-
+        if (noteLevelTagList.length > 0) {
             // Treat the frontmatter slightly differently (all tags grouped together even if on separate lines)
-            const [frontmatter, _] = extractFrontmatter(this.noteText);
-            if (frontmatter) {
-                frontmatterLineCount = splitTextIntoLineArray(frontmatter).length;
-                const frontmatterTagCacheList = filteredTagCacheList.filter(
+            if (this.frontmatterText) {
+                const frontmatterTagCacheList = noteLevelTagList.filter(
                     (item) => item.position.start.line < frontmatterLineCount,
                 );
 
-                // Doesn't matter what line number we specify, as long as it's less than frontmatterLineCount
                 if (frontmatterTagCacheList.length > 0)
-                    frontmatterTopicPathList = this.createTopicPathList(frontmatterTagCacheList, 0);
+                    result = this.createTopicPathList(
+                        frontmatterTagCacheList,
+                        frontmatterTagPseudoLineNum,
+                    );
             }
         }
-        //
-        const contentStartLineNum: number = frontmatterLineCount > 0 ? frontmatterLineCount + 1 : 0;
-        const contentTagCacheList: TagCache[] = filteredTagCacheList.filter(
-            (item) => item.position.start.line >= contentStartLineNum,
+        return result;
+    }
+
+    private determineContentTopicPathList(
+        flashcardTagList: TagCache[],
+        frontmatterLineCount: number,
+    ): TopicPathList[] {
+        const result: TopicPathList[] = [] as TopicPathList[];
+
+        // NOTE: Line numbers are zero based, therefore don't add 1 to frontmatterLineCount to get contentStartLineNum
+        const contentStartLineNum: number = frontmatterLineCount;
+        const contentTagCacheList: TagCache[] = flashcardTagList.filter(
+            (item) =>
+                item.position.start.line >= contentStartLineNum &&
+                this.isNoteLevelFlashcardTag(item),
         );
 
+        // We group together all tags that are on the same line, taking advantage of flashcardTagList being ordered by line number
         let list: TagCache[] = [] as TagCache[];
-        for (const t of contentTagCacheList) {
+        for (const tag of contentTagCacheList) {
             if (list.length != 0) {
                 const startLineNum: number = list[0].position.start.line;
-                if (startLineNum != t.position.start.line) {
-                    contentTopicPathList.push(this.createTopicPathList(list, startLineNum));
+                if (startLineNum != tag.position.start.line) {
+                    result.push(this.createTopicPathList(list, startLineNum));
                     list = [] as TagCache[];
                 }
             }
-            list.push(t);
+            list.push(tag);
         }
         if (list.length > 0) {
             const startLineNum: number = list[0].position.start.line;
-            contentTopicPathList.push(this.createTopicPathList(list, startLineNum));
+            result.push(this.createTopicPathList(list, startLineNum));
         }
+        return result;
+    }
 
-        return [frontmatterTopicPathList, contentTopicPathList];
+    private isNoteLevelFlashcardTag(tagItem: TagCache): boolean {
+        const tagLineNum: number = tagItem.position.start.line;
+
+        // Check that the tag is not question specific (determined by line number)
+        const isQuestionSpecific: boolean = this.questionList.some((q) =>
+            q.parsedQuestionInfo.isQuestionLineNum(tagLineNum),
+        );
+        return !isQuestionSpecific;
     }
 
     private createTopicPathList(tagCacheList: TagCache[], lineNum: number): TopicPathList {
@@ -242,6 +302,11 @@ export class NoteQuestionParser {
         return new TopicPathList(list, lineNum);
     }
 
+    private createTopicPathList_FromSingleTag(tagCache: TagCache): TopicPathList {
+        const list: TopicPath[] = [TopicPath.getTopicPathFromTag(tagCache.tag)];
+        return new TopicPathList(list, tagCache.position.start.line);
+    }
+
     //
     // A question can be associated with multiple topics (hence returning TopicPathList and not just TopicPath).
     //
@@ -268,17 +333,18 @@ export class NoteQuestionParser {
 
                 // Find the last TopicPathList prior to the question (in the order present in the file)
                 for (let i = this.contentTopicPathInfo.length - 1; i >= 0; i--) {
-                    const info: TopicPathList = this.contentTopicPathInfo[i];
-                    if (info.lineNum < question.parsedQuestionInfo.firstLineNum) {
-                        result = info;
+                    const topicPathList: TopicPathList = this.contentTopicPathInfo[i];
+                    if (topicPathList.lineNum < question.parsedQuestionInfo.firstLineNum) {
+                        result = topicPathList;
                         break;
                     }
                 }
 
                 // For backward compatibility with functionality pre https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/495:
                 // if nothing matched, then use the first one
-                if (!result && this.contentTopicPathInfo.length > 0) {
-                    result = this.contentTopicPathInfo[0];
+                // This could occur if the only topic tags present are question specific
+                if (!result && this.flashcardTagList.length > 0) {
+                    result = this.createTopicPathList_FromSingleTag(this.flashcardTagList[0]);
                 }
             }
         }
diff --git a/src/SRFile.ts b/src/SRFile.ts
index ce28356..eb71ca1 100644
--- a/src/SRFile.ts
+++ b/src/SRFile.ts
@@ -9,6 +9,7 @@ import {
 } from "obsidian";
 import { parseObsidianFrontmatterTag } from "./util/utils";
 
+// NOTE: Line numbers are zero based
 export interface ISRFile {
     get path(): string;
     get basename(): string;
@@ -19,6 +20,11 @@ export interface ISRFile {
     write(content: string): Promise<void>;
 }
 
+// The Obsidian frontmatter cache doesn't include the line number for the specific tag.
+// We define as -1 so that we can differentiate tags within the frontmatter and tags within the content
+export const frontmatterTagPseudoLineNum: number = -1;
+
+// NOTE: Line numbers are zero based
 export class SrTFile implements ISRFile {
     file: TFile;
     vault: Vault;
@@ -48,6 +54,7 @@ export class SrTFile implements ISRFile {
         const result: TagCache[] = [] as TagCache[];
         const fileCachedData = this.metadataCache.getFileCache(this.file) || {};
         if (fileCachedData.tags?.length > 0) {
+            // console.log(`getAllTagsFromText: tags: ${fileCachedData.tags.map((item) => `(${item.position.start.line}: ${item.tag})`).join("|")}`);
             result.push(...fileCachedData.tags);
         }
 
@@ -63,19 +70,14 @@ export class SrTFile implements ISRFile {
         const result: TagCache[] = [] as TagCache[];
         const frontmatterTags: string = frontmatter != null ? frontmatter["tags"] + "" : null;
         if (frontmatterTags) {
-            // The frontmatter doesn't include the line number for the specific tag, defining as line 1 is good enough.
-            // (determineQuestionTopicPathList() only needs to know that these frontmatter tags come before all others
-            // in the file)
-            const line: number = 1;
-
             // Parse the frontmatter tag string into a list, each entry including the leading "#"
             const tagStrList: string[] = parseObsidianFrontmatterTag(frontmatterTags);
             for (const str of tagStrList) {
                 const tag: TagCache = {
                     tag: str,
                     position: {
-                        start: { line: line, col: null, offset: null },
-                        end: { line: line, col: null, offset: null },
+                        start: { line: frontmatterTagPseudoLineNum, col: null, offset: null },
+                        end: { line: frontmatterTagPseudoLineNum, col: null, offset: null },
                     },
                 };
                 result.push(tag);
@@ -87,6 +89,7 @@ export class SrTFile implements ISRFile {
     getQuestionContext(cardLine: number): string[] {
         const fileCachedData = this.metadataCache.getFileCache(this.file) || {};
         const headings: HeadingCache[] = fileCachedData.headings || [];
+        // console.log(`getQuestionContext: headings: ${headings.map((item) => `(${item.position.start.line}: ${item.heading})`).join("|")}`);
         const stack: HeadingCache[] = [];
         for (const heading of headings) {
             if (heading.position.start.line > cardLine) {
diff --git a/src/parser.ts b/src/parser.ts
index 2e8e8c1..3cc0654 100644
--- a/src/parser.ts
+++ b/src/parser.ts
@@ -23,6 +23,13 @@ export class ParsedQuestionInfo {
 /**
  * Returns flashcards found in `text`
  *
+ * It is best that the text does not contain frontmatter, see extractFrontmatter for reasoning
+ *
+ * Multi-line question with blank lines user workaround:
+ *      As of 3/04/2024 there is no support for including blank lines within multi-line questions
+ *      As a workaround, one user uses a zero width Unicode character - U+200B
+ *      https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/915#issuecomment-2031003092
+ *
  * @param text - The text to extract flashcards from
  * @param singlelineCardSeparator - Separator for inline basic cards
  * @param singlelineReversedCardSeparator - Separator for inline reversed cards
diff --git a/src/util/utils.ts b/src/util/utils.ts
index ee7974b..6e4bfef 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -96,6 +96,18 @@ export function stringTrimStart(str: string): [string, string] {
     return [ws, trimmed];
 }
 
+//
+// This returns [frontmatter, content]
+//
+// The returned content has the same number of lines as the supplied str string, but with the
+// frontmatter lines (if present) blanked out.
+//
+// 1. We don't want the parser to see the frontmatter, as it would deem it to be part of a multi-line question
+// if one started on the line immediately after the "---" closing marker.
+//
+// 2. The lines are blanked out rather than deleted so that line numbers are not affected
+// e.g. for calls to getQuestionContext(cardLine: number)
+//
 export function extractFrontmatter(str: string): [string, string] {
     let frontmatter: string = "";
     let content: string = "";
@@ -113,12 +125,11 @@ export function extractFrontmatter(str: string): [string, string] {
 
         if (frontmatterEndLineNum) {
             const frontmatterStartLineNum: number = 0;
-            const frontmatterLineCount: number =
-                frontmatterEndLineNum - frontmatterStartLineNum + 1;
-            const frontmatterLines: string[] = lines.splice(
-                frontmatterStartLineNum,
-                frontmatterLineCount,
-            );
+            const frontmatterLines: string[] = [];
+            for (let i = frontmatterStartLineNum; i <= frontmatterEndLineNum; i++) {
+                frontmatterLines.push(lines[i]);
+                lines[i] = "";
+            }
             frontmatter = frontmatterLines.join("\n");
             content = lines.join("\n");
         }


Test patch:
diff --git a/tests/unit/NoteQuestionParser.test.ts b/tests/unit/NoteQuestionParser.test.ts
index eb63a9b..ae42d74 100644
--- a/tests/unit/NoteQuestionParser.test.ts
+++ b/tests/unit/NoteQuestionParser.test.ts
@@ -5,9 +5,10 @@ import { CardType, Question } from "src/Question";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
 import { TopicPath, TopicPathList } from "src/TopicPath";
 import { createTest_NoteQuestionParser } from "./SampleItems";
-import { ISRFile } from "src/SRFile";
+import { ISRFile, frontmatterTagPseudoLineNum } from "src/SRFile";
 import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
 import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
+import { Card } from "src/Card";
 
 let parserWithDefaultSettings: NoteQuestionParser = createTest_NoteQuestionParser(DEFAULT_SETTINGS);
 let settings_ConvertFoldersToDecks: SRSettings = { ...DEFAULT_SETTINGS };
@@ -155,7 +156,7 @@ In computer-science, a *heap* is a tree-based data-structure, that satisfies the
             {
                 questionType: CardType.MultiLineBasic,
                 // Explicitly checking that #data-structure and #2024/03-11 are not included
-                topicPathList: TopicPathList.fromPsv("#flashcards", 0),
+                topicPathList: TopicPathList.fromPsv("#flashcards", frontmatterTagPseudoLineNum),
             },
         ];
         expect(
@@ -529,6 +530,38 @@ Q1::A1
             expect(questionList[2].topicPathList.formatPsv()).toEqual(expectedPath);
         });
 
+        // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/915#issuecomment-2017508391
+        test("Topic tag on first line after frontmatter", async () => {
+            let noteText: string = `---
+created: 2023-10-26T07:34
+---
+#flashcards/English 
+
+## taunting & teasing & irony & sarcasm
+
+Stop trying ==to milk the crowd== for sympathy. // доить толпу
+`;
+            let noteFile: ISRFile = new UnitTestSRFile(noteText);
+
+            let expectedPath: string = "#flashcards/English";
+            let folderTopicPath: TopicPath = TopicPath.emptyPath;
+            let expected = [
+                {
+                    questionType: CardType.Cloze,
+                    topicPathList: TopicPathList.fromPsv("#flashcards/English", 3), // #flashcards/English is on the 4th line, line number 3
+                    cards: [
+                        new Card({
+                            front: "Stop trying <span style='color:#2196f3'>[...]</span> for sympathy. // доить толпу",
+                            back: `Stop trying <span style='color:#2196f3'>to milk the crowd</span> for sympathy. // доить толпу`,
+                        }),
+                    ],
+                },
+            ];
+            expect(
+                await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+            ).toMatchObject(expected);
+        });
+
         test("Topic tag within question overrides the note topic, for that topic only", async () => {
             let noteText: string = `#flashcards/test
     Q1::A1
@@ -614,6 +647,149 @@ Q1::A1
             expect(questionList[0].cards.length).toEqual(1);
             expect(questionList[0].cards[0].front).toEqual("Q5");
         });
+
+        // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/915#issuecomment-2016580471
+        test("Topic tag at end of line question line (no other tags present)", async () => {
+            let noteText: string = `---
+Title: "The Taliban at war: 2001-2018"
+Authors: "Antonio Giustozzi"
+Year: 2019
+URL: 
+DOI: 
+Unique Citekey: Talibanwar20012018
+Zotero Link: zotero://select/items/@Talibanwar20012018
+---
+> [!PDF|255, 208, 0] [[The Taliban at War_ 2001 - 2018.pdf#page=10&annotation=1440R|The Taliban at War_ 2001 - 2018, page 10]]
+> > The Taliban Emirate, established in 1996, was in 2001 overthrown relatively easily by a coalition of US forces and various Afghan anti-Taliban groups. Few at the end of 2001 expected to hear again from the Taliban, except in the annals of history. Even as signs emerged in 2003 of a Taliban comeback, in the shape of an insurgency against the post-2001 Afghan government and its international sponsors, many did not take it seriously. It was hard to imagine that the Taliban would be able to mount a resilient challenge to a large-scale commitment of forces by the US and its allies.
+
+What year was the Taliban Emirate founded?::1996 #flashcards
+`;
+            let noteFile: ISRFile = new UnitTestSRFile(noteText);
+
+            let folderTopicPath: TopicPath = TopicPath.emptyPath;
+            let expected = [
+                {
+                    questionType: CardType.SingleLineBasic,
+                    topicPathList: TopicPathList.fromPsv("#flashcards", 12),
+                    cards: [
+                        new Card({
+                            front: "What year was the Taliban Emirate founded?",
+                            back: "1996 #flashcards",
+                        }),
+                    ],
+                },
+            ];
+            expect(
+                await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+            ).toMatchObject(expected);
+        });
+    });
+});
+
+describe("Questions immediately after closing line of frontmatter", () => {
+    // The frontmatter should be discarded
+    // (only the specified question text should be used)
+    test("Multi-line with question", async () => {
+        let noteText: string = `---
+created: 2024-03-11 10:41
+tags:
+  - flashcards
+  - data-structure
+---
+**What is a Heap?**
+?
+In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*!
+`;
+        let noteFile: ISRFile = new UnitTestSRFile(noteText);
+
+        let folderTopicPath: TopicPath = TopicPath.emptyPath;
+        let expected = [
+            {
+                questionType: CardType.MultiLineBasic,
+                // Explicitly checking that #data-structure is not included
+                topicPathList: TopicPathList.fromPsv("#flashcards", frontmatterTagPseudoLineNum),
+                cards: [
+                    new Card({
+                        front: `**What is a Heap?**`,
+                        back: "In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*!",
+                    }),
+                ],
+            },
+        ];
+        expect(
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+        ).toMatchObject(expected);
+    });
+
+    test("Multi-line without question (i.e. question is blank)", async () => {
+        let noteText: string = `---
+created: 2024-03-11 10:41
+tags:
+  - flashcards
+  - data-structure
+---
+?
+In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*!
+    `;
+        let noteFile: ISRFile = new UnitTestSRFile(noteText);
+
+        let folderTopicPath: TopicPath = TopicPath.emptyPath;
+        let expected = [
+            {
+                questionType: CardType.MultiLineBasic,
+                // Explicitly checking that #data-structure is not included
+                topicPathList: TopicPathList.fromPsv("#flashcards", frontmatterTagPseudoLineNum),
+                cards: [
+                    new Card({
+                        front: "",
+                        back: "In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*!",
+                    }),
+                ],
+            },
+        ];
+        expect(
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+        ).toMatchObject(expected);
+    });
+
+    test("single-line question", async () => {
+        let noteText: string = `---
+created: 2024-03-11 10:41
+tags:
+  - flashcards
+  - data-structure
+---
+In computer-science, a *heap* is::a tree-based data-structure
+A::B
+    `;
+        let noteFile: ISRFile = new UnitTestSRFile(noteText);
+
+        let folderTopicPath: TopicPath = TopicPath.emptyPath;
+        let expected = [
+            {
+                questionType: CardType.SingleLineBasic,
+                topicPathList: TopicPathList.fromPsv("#flashcards", frontmatterTagPseudoLineNum),
+                cards: [
+                    new Card({
+                        front: "In computer-science, a *heap* is",
+                        back: "a tree-based data-structure",
+                    }),
+                ],
+            },
+            {
+                questionType: CardType.SingleLineBasic,
+                topicPathList: TopicPathList.fromPsv("#flashcards", frontmatterTagPseudoLineNum),
+                cards: [
+                    new Card({
+                        front: "A",
+                        back: "B",
+                    }),
+                ],
+            },
+        ];
+        expect(
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+        ).toMatchObject(expected);
     });
 });
 
diff --git a/tests/unit/helpers/UnitTestHelper.ts b/tests/unit/helpers/UnitTestHelper.ts
index 2864d16..52f2bf0 100644
--- a/tests/unit/helpers/UnitTestHelper.ts
+++ b/tests/unit/helpers/UnitTestHelper.ts
@@ -1,4 +1,5 @@
 import { TagCache } from "obsidian";
+import { frontmatterTagPseudoLineNum } from "src/SRFile";
 import { extractFrontmatter, splitTextIntoLineArray } from "src/util/utils";
 
 export function unitTest_CreateTagCache(tag: string, lineNum: number): TagCache {
@@ -25,7 +26,7 @@ export function unitTest_GetAllTagsFromTextEx(text: string): TagCache[] {
             if (foundTagHeading) {
                 if (line.startsWith(dataPrefix)) {
                     const tagStr: string = line.substring(dataPrefix.length);
-                    result.push(unitTest_CreateTagCache("#" + tagStr, i));
+                    result.push(unitTest_CreateTagCache("#" + tagStr, frontmatterTagPseudoLineNum));
                 } else {
                     break;
                 }
diff --git a/tests/unit/util/utils.test.ts b/tests/unit/util/utils.test.ts
index b4b79e6..bcfcf46 100644
--- a/tests/unit/util/utils.test.ts
+++ b/tests/unit/util/utils.test.ts
@@ -126,7 +126,15 @@ tags:
         let content: string;
         [frontmatter, content] = extractFrontmatter(text);
         expect(frontmatter).toEqual(text);
-        expect(content).toEqual("");
+        const frontmatterBlankedOut: string = `
+
+
+
+
+
+
+`;
+        expect(content).toEqual(frontmatterBlankedOut);
     });
 
     test("With frontmatter (and content)", () => {
@@ -157,7 +165,17 @@ ${content}`;
 
         const [f, c] = extractFrontmatter(text);
         expect(f).toEqual(frontmatter);
-        expect(c).toEqual(content);
+        const frontmatterBlankedOut: string = `
+
+
+
+
+
+
+`;
+        const expectedContent: string = `${frontmatterBlankedOut}
+${content}`;
+        expect(c).toEqual(expectedContent);
     });
 });