Repository: st3v3nmw/obsidian-spaced-repetition
Pull request #834 (2024-01-11 12:30:35)

Support multi-deck cards #495, and multiple decks in a single note #705
This PR addresses issues:
#178 
#495
#705 

For example, in the following:
```
#flashcards/language/words #flashcards/trivia #flashcards/learned-from-tv

A group of cats is called a::clowder

#flashcards/language/words
A group of crows is called a::murder
The branch of medical science dealing with the classification of diseases::nosology

#flashcards/trivia 
The 1988 movie "Mystic Pizza" launched the career of what 'Pretty Woman?' actress::Julia Roberts
What is Australia’s most popular spread?::Vegemite
Who was Australia’s first female prime minister?::Julia Gillard
```

| Question | Decks | 
|-|-|
| A group of cats is called a::clowder | #flashcards/language/words #flashcards/trivia #flashcards/learned-from-tv | 
| A group of crows is called a::murder | #flashcards/language/words | 
| The branch of medical science dealing with the classification of diseases::nosology | #flashcards/language/words | 
| The 1988 movie "Mystic Pizza" launched the career of what 'Pretty Woman?' actress::Julia Roberts | #flashcards/trivia  | 
Etc

# Syntax

For a question to appear in multiple decks, preceding the question in the file must be the list of tags on the same line, such as:
```
#flashcards/language/words #flashcards/trivia #flashcards/learned-from-tv
A group of cats is called a::clowder
```

The list of deck(s) can be changed using this format multiple times within a single note.

The list of deck(s) can be included in the frontmatter, in which case multiple tags can be on multiple lines, such as:
```
---
tags:
  - flashcards
  - flashcards/learned-from-tv
---
```

A question specific tag takes precedence over the note-level tags described above. For example, in the following:
- "Q1::A1" is in deck `#flashcards/spatial`
- "Which 1986 movie..." is in deck  `#flashcards/trivia`, i.e. the last note-level tag

```
#flashcards/trivia 
The 1988 movie "Mystic Pizza" launched the career of what 'Pretty Woman?' actress::Julia Roberts

#flashcards/spatial Q1::A1

Which 1986 movie is the highest-grossing Australian movie of all time?::Crocodile Dundee
```

# Card counting

Multi-deck cards are only counted once, e.g.:
- There are 6 cards, so at (1) the total shown is 6

In the case of multi-deck cards, totals may not add up. E.g. under `flashcards` is `language`, `learned-from-tv` and `trivia`. Those decks have 3, 1, 4 cards listed respectively, totaling 8. Which is 2 more than the total of 6 shown at (1). This is because the `A group of cats is called a::clowder` question is present in 3 decks, but is only counted as if it was in one.

![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/75528127/883ad821-c4b9-4e08-967b-31b60c0a719b)

# Reviewing

A multi-deck card will only be shown once. For example, after reviewing only the `learned-from-tv` deck, the counts are:
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/75528127/695dc18e-49de-4e94-9f08-f6734b59d919)

The new-card counts in blue, across the 3 decks in which the cat question appears, have all decreased by 1.

i.e. lines (3) (4) and (5). (2) has also decreased as it is the parent of (3).

# Backward Compatibility

For compatibility with plug-in functionality before this PR, if there are no note-level tags prior to a question, and that question doesn't have a question specific tag, the question will be placed in the deck specified by the first tag found after the question within the file.

e.g. if the complete content of a note is:
```
The 1988 movie "Mystic Pizza" launched the career of what 'Pretty Woman?' actress::Julia Roberts
What is Australia’s most popular spread?::Vegemite
Who was Australia’s first female prime minister?::Julia Gillard

#flashcards/backward-compatibility
```

All the questions will be found in deck `#flashcards/backward-compatibility`

# Beta Testing & Feedback

Yes please!

ronzulu: Hi all

Beta release is now available.

There is a file called `main.js` in this zip file
[main_495A.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/13909934/main_495A.zip)

You will need to copy that to the obsidian plug-in folder, e.g.
`D:\Obsidian\Obsidian\.obsidian\plugins\obsidian-spaced-repetition`

Then reload the plug-in, or restart obsidian. 

Any feedback appreciated.

Ronny
pikatwinky: Hi Ronny, thank you so much for your hard work. All we use this plugin really appreciate it.

Im not very tech skilled, but i wanted to give a try to this beta version (using the sandbox vault first and then with my own vault also restarting the plugin in the second case); so i copied the main.js file in the plugin route, but when using it my existent cards are not shown in the review window
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/253acd79-e686-4bfc-9de9-645c86d67235)

ronzulu: Hi @pikatwinky thanks for trying, and oops that's not a good start!

Please attach one of your Markdown .md files so I can isolate the problem.

Also, can you send a screenshot of the relevant settings, including:
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/75528127/6b6b1191-158d-4248-982a-29a4441bc336)

Cheers
Ronny
Steindvart: A great addition! This is exactly what I REALLY miss in using this plugin 😅.

In basic case is work fine in my test Obsidian vault.

![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/53004350/af0c0652-137e-44cd-9148-ea1c182ab7aa)

![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/53004350/454393c5-3af8-4d4b-af62-5c09b1829a38)

![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/53004350/a3f13bba-4e1d-4fbc-831d-d13c1038a45c)
pikatwinky: Hi @ronzulu, i tried with the sandbox vault and with my own vault. In both cases i use this settings
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/8e5f9ad3-aeda-49d7-9aba-053a9cdbb8a5)

The md i use in each case looks like this
Sandbox vault
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/3f4eda12-13b8-40eb-9ada-1faad440c2e7)
My own vault:
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/e6226287-8ca9-4898-a823-a92d2f82179c)

I copied the main.js file in the plugin's route, but at reviewing time the cards aren't shown and the window looks as in my previous attachment
I tried restarting both the plugin and obsidian. Im using the latest obsidian version btw
Lucas256-cmd: So I had a file with a lot of hierarchical (if this is even a word:) ) card tags. 3 layers deep, some with one deck in them some with more. I downloaded the zip, put the file where it belongs, reloaded obsidian AND IT WORKED LIKE A CHARM. Thank you. This is exactly how i had it in mind since the tags where already in the file before you added this functionality. You added it and it just works.
ronzulu: Hi @pikatwinky  from your screenshots you have done everything correctly.

Could you please zip up your test vault and post here. Hopefully I'll be able to reproduce the issue at my end.

Thanks
Ronny
ronzulu: > Hi @pikatwinky from your screenshots you have done everything correctly.
> 
> Could you please zip up your test vault and post here. Hopefully I'll be able to reproduce the issue at my end.
> 
> Thanks Ronny

Sorry to hassle you, would be great if you can send this through. Many thanks!
pikatwinky: > > Hi @pikatwinky from your screenshots you have done everything correctly.
> > Could you please zip up your test vault and post here. Hopefully I'll be able to reproduce the issue at my end.
> > Thanks Ronny
> 
> Sorry to hassle you, would be great if you can send this through. Many thanks!

Hi Ronny, sorry for the late reply. I had that problem using the sandbox vault and a personal vault. Unfortunately i cant publish the personal one. Does the sandbox work for you?
Maybe i did something wrong with my process which was the next one:
-Open Sandbox vault
-Download Spaced Repetition plugin
-Open Obsidian's Spaced Repetition plugin folder and paste there the beta main.js file
-When returning Obsidian plugin wasn't working even after restarting from the plugins section




 
ronzulu: Hi @pikatwinky 

I don't know why I didn't think of it before, but here is a demo vault with the beta version of `main.js`.
[Vault_495A.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/14004789/Vault_495A.zip)

When I open the vault, and click on the flashcards icon, I see:
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/75528127/b2680ed7-b538-4165-83ff-68f8d3795888)

What do you see?

If you see the same as above, perhaps copy one of your test files into this vault and see what happens. If the beta code doesn't work with your file in this demo vault, please zip it up and post the zip file here.

Thanks for your help!

Cheers
Ronny

pikatwinky: > Hi @pikatwinky
> 
> I don't know why I didn't think of it before, but here is a demo vault with the beta version of `main.js`. [Vault_495A.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/14004789/Vault_495A.zip)
> 
> When I open the vault, and click on the flashcards icon, I see: ![image](https://private-user-images.githubusercontent.com/75528127/298460907-b2680ed7-b538-4165-83ff-68f8d3795888.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MDU5NDE1MDUsIm5iZiI6MTcwNTk0MTIwNSwicGF0aCI6Ii83NTUyODEyNy8yOTg0NjA5MDctYjI2ODBlZDctYjUzOC00MTY1LTgzZmYtNjhmOGQzNzk1ODg4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDAxMjIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwMTIyVDE2MzMyNVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTI3YTdiNDM4NjVhZWFhMDIzN2FhNjBlY2IxZGJmNGJhZjk2NmRjYmI0YTBiOGZiOGRhZGQ1OWYyNzc1OGMyMTYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.0AT7GfWNJhC49xk4Kec2iLPfoj_0rIuEnfqwShcltW8)
> 
> What do you see?
> 
> If you see the same as above, perhaps copy one of your test files into this vault and see what happens. If the beta code doesn't work with your file in this demo vault, please zip it up and post the zip file here.
> 
> Thanks for your help!
> 
> Cheers Ronny

Hi @ronzulu.
I tested your vault and it works perfectly and the behaviour is as described.
Thank you so much for your hard work!
  
ronzulu: Hi @pikatwinky great.

Does that mean it works when you copy your file into the test vault?

I assume that the problem still occurs in your main vault? If so, I'll need to send you a version with extra debug to assist diagnosing.

Cheers
Ronny
pikatwinky: > > > Hi @pikatwinky from your screenshots you have done everything correctly.
> > > Could you please zip up your test vault and post here. Hopefully I'll be able to reproduce the issue at my end.
> > > Thanks Ronny
> > 
> > 
> > Sorry to hassle you, would be great if you can send this through. Many thanks!
> 
> Hi Ronny, sorry for the late reply. I had that problem using the sandbox vault and a personal vault. Unfortunately i cant publish the personal one. Does the sandbox work for you? Maybe i did something wrong with my process which was the next one: -Open Sandbox vault -Download Spaced Repetition plugin -Open Obsidian's Spaced Repetition plugin folder and paste there the beta main.js file -When returning Obsidian plugin wasn't working even after restarting from the plugins section

Hi @ronzulu! If the steps i followed here are all right, then yes, all my existent cards still dissapear from the review window, but the other plugin features like the note review work properly.
How can i provide you info about this?
ronzulu: Hi @pikatwinky 

No worries, I understand you can't send through your personal vault. I'm hoping to be able to reproduce the problem on my machine to simplify solving it.

1. Is there one file from your vault that contains cards that you can copy to the test vault that I sent? Let's call it `file 1`

2. What happens in the test vault?

When you click on the flashcards icon, do the cards from `file 1` appear, or only the ones that were originally present in the test vault?
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/75528127/b2680ed7-b538-4165-83ff-68f8d3795888)

3. Please take a screenshot of the flashcard review dialog box.

4. In file 1, are you able to make this as compact as possible that still highlights the problem? E.g. remove all unnecessary text and just keep a single card.

Replace any private info in the file with just boring general text, retaining the flashcard tags.

5. Does the problem still occur?

6. As this doesn't have any private info, do you feel comfortable zipping up this test vault and sending it to me?

Sorry for all this hassle, but I think this is a useful new feature and there isn't any point releasing it if it doesn't work for you!

Cheers
Ronny
pikatwinky: Hi @ronzulu 
Here i share with you 3 vaults testing the next simple note:
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/21fc8392-9c38-4687-9152-1189f9444e9f)
I'm using the default settings
- The first is a test vault with the plugin's latest release and one note with one flashcard. In this vault the review window works well 
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/67a46a70-3653-49a5-8a7f-f028465e4b79)
[MyTestVault.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/14073781/MyTestVault.zip)

- Then in the second vault i copied the beta main.js attached above in the corresponding plugin's route, and after restarting obsidian, the review window doesn't show the card 
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/ff9c2025-8973-44ad-ac45-31355486e0a4)
[MyTestVaultAfterUpdate.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/14073782/MyTestVaultAfterUpdate.zip)

- Finally in the vault you shared previously with the beta plugin included, i put this same note but i changed its deck's tag to **flashcardsTest** only for easy identification.  
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/755d2f50-89d9-4679-a2ad-4aaf103bbcc4)
However the flashcard doesn't appear in the review window
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/f9a9eded-a299-4f06-ab6f-28a35cd6871c)

I was expecting a **flashcardsTest** deck appeared in the window because that tag was added in the settings
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/5855edc5-4acf-45bf-adb3-5cb0d579d395)

[Demo.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/14073817/Demo.zip)

I hope this info could be found useful  

ronzulu: Hi @pikatwinky 

Thanks so much for the clear and comprehensive info, it enabled me (hopefully) to solve the problem.
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/75528127/93b1f8e7-d173-40bc-b88a-239dd2b86364)

![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/75528127/18df57e3-fe95-49b1-83cb-154a0f264693)


If you could please try this version now, and let me know how it goes.
[main_495B.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/14075882/main_495B.zip)

Cheers
Ronny
pikatwinky: > Hi @pikatwinky
> 
> Thanks so much for the clear and comprehensive info, it enabled me (hopefully) to solve the problem. ![image](https://private-user-images.githubusercontent.com/75528127/300259866-93b1f8e7-d173-40bc-b88a-239dd2b86364.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MDY1ODMyNDksIm5iZiI6MTcwNjU4Mjk0OSwicGF0aCI6Ii83NTUyODEyNy8zMDAyNTk4NjYtOTNiMWY4ZTctZDE3My00MGJjLWI4OGEtMjM5ZGQyYjg2MzY0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDAxMzAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwMTMwVDAyNDkwOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTNlZWIxYjE4NTUxMDY2NmUxZTBhNTBhYTc3NmY3M2NjNjM4OGQ1YmM3ODRlMGFjMTkzZTU0OWRjNjVlYWZjNTUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.k9oe_oYKNgO0j4FGsPjk0IyTX28FgXH8-XVbEVLoozc)
> 
> ![image](https://private-user-images.githubusercontent.com/75528127/300259967-18df57e3-fe95-49b1-83cb-154a0f264693.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MDY1ODMyNDksIm5iZiI6MTcwNjU4Mjk0OSwicGF0aCI6Ii83NTUyODEyNy8zMDAyNTk5NjctMThkZjU3ZTMtZmU5NS00OWIxLTgzY2ItMTU0YTBmMjY0NjkzLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDAxMzAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwMTMwVDAyNDkwOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTQ1N2UyNDk0NWNjODlmM2M2ZjExMmMxNzU0YmQyY2UxMWZhNDA2OWMzNzc5N2I5YzdmNTA4ZjU5YjRmNTVkZGImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.uxwqJnPzRAksRiFt1h7PYtMmTVP87k1tcPwhKHX3nJ8)
> 
> If you could please try this version now, and let me know how it goes. [main_495B.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/14075882/main_495B.zip)
> 
> Cheers Ronny

Hi @ronzulu, i tested this new version in my own vault and it works perfectly, thanks again for all your hard work
![image](https://github.com/st3v3nmw/obsidian-spaced-repetition/assets/31860235/e448e605-3e85-4344-acbc-d58adf15f1e0)


ronzulu: Hi @st3v3nmw this has been tried out by a few people, and I've fixed the one bug reported.

This PR has required moderate change (more than I anticipated), so a review by you really is needed. The original PR request has a comprehensive description of the enhanced functionality, and I have also updated the English user documentation to match.

Also, will you have time at some stage for us to talk through all the PRs contributed and create a plan? 

Cheers
Ronny
ronzulu: Hi @st3v3nmw,  just wondering how you are - you must be very busy 😄 

It would be great if you could review and merge this PR.

> Hi @st3v3nmw this has been tried out by a few people, and I've fixed the one bug reported.
> 
> This PR has required moderate change (more than I anticipated), so a review by you really is needed. The original PR request has a comprehensive description of the enhanced functionality, and I have also updated the English user documentation to match.
> 
> Also, will you have time at some stage for us to talk through all the PRs contributed and create a plan?
> 
> Cheers Ronny



Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 8a8ace6..66d8c2f 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,10 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
 
 #### [1.11.2](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.11.1...1.11.2)
 
+-   Bug 495 Obsidian SRS ignores more than one tag https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/834
+-   Fixes [BUG] Malformed card text during review, when multi-line card has space on Q/A separator line https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/853
+-   Fixes bug #815 HTML review comment deactivates block identifier https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/815
+-   Implementation of some little UI improvements https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/869
 -   Add polish translation [`#889`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/889)
 -   Add support for Italian [`#886`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/886)
 -   update dependencies [`#892`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/892)
diff --git a/docs/en/flashcards.md b/docs/en/flashcards.md
index a97aa42..afabfdc 100644
--- a/docs/en/flashcards.md
+++ b/docs/en/flashcards.md
@@ -94,6 +94,60 @@ The green and blue counts on the right of each deck name represent due and new c
 
 Note that `#flashcards` will match nested tags like `#flashcards/subdeck/subdeck`.
 
+#### Multiple Tags Within a Single File
+
+A single file can contain cards for multiple different decks.
+
+This is possible because a tag pertains to all subsequent cards in a file until any subsequent tag.
+
+For example:
+
+```markdown
+#flashcards/deckA
+Question1 (in deckA)::Answer1
+Question2 (also in deckA)::Answer2
+Question3 (also in deckA)::Answer3
+
+#flashcards/deckB
+Question4 (in deckB)::Answer4
+Question5 (also in deckB)::Answer5
+
+#flashcards/deckC
+Question6 (in deckC)::Answer6
+```
+
+#### A Single Card Within Multiple Decks
+
+Usually the content of a card is only relevant to a single deck. However, sometimes content doesn't fall neatly into a single deck of the hierarchy.
+
+In these cases, a card can be tagged as being part of multiple decks. The following card is specified as being in the three different decks listed.
+
+```markdown
+#flashcards/language/words #flashcards/trivia #flashcards/learned-from-tv
+A group of cats is called a::clowder
+```
+
+Note that as shown in the above example, all tags must be placed on the same line, separated by spaces.
+
+#### Question Specific Tags
+
+A tag that is present at the start of the first line of a card is "question specific", and applies only to that card.
+
+For example:
+
+```markdown
+#flashcards/deckA
+Question1 (in deckA)::Answer1
+Question2 (also in deckA)::Answer2
+Question3 (also in deckA)::Answer3
+
+#flashcards/deckB Question4 (in deckB)::Answer4
+
+Question6 (in deckA)::Answer6
+```
+
+Here `Question6` will be part of `deckA` and not `deckB` as `deckB` is specific to `Question4` only.
+
 ### Using Folder Structure
 
 The plugin will automatically search for folders that contain flashcards & use their paths to create decks & sub-decks i.e. `Folder/sub-folder/sub-sub-folder` ⇔ `Deck/sub-deck/sub-sub-deck`.
diff --git a/src/Deck.ts b/src/Deck.ts
index 66f06d2..c7f53b1 100644
--- a/src/Deck.ts
+++ b/src/Deck.ts
@@ -2,7 +2,7 @@ import { Card } from "./Card";
 import { FlashcardReviewMode } from "./FlashcardReviewSequencer";
 import { Question } from "./Question";
 import { IQuestionPostponementList } from "./QuestionPostponementList";
-import { TopicPath } from "./TopicPath";
+import { TopicPath, TopicPathList } from "./TopicPath";
 
 export enum CardListType {
     NewCard,
@@ -10,6 +10,12 @@ export enum CardListType {
     All,
 }
 
+//
+// The same card can be added to multiple decks e.g.
+//      #flashcards/language/words
+//      #flashcards/trivia
+// To simplify certain functions (e.g. getDistinctCardCount), we explicitly use the same card object (and not a copy)
+//
 export class Deck {
     public deckName: string;
     public newFlashcards: Card[];
@@ -40,6 +46,41 @@ export class Deck {
         return result;
     }
 
+    public getDistinctCardCount(cardListType: CardListType, includeSubdeckCounts: boolean): number {
+        const cardList: Card[] = this.getFlattenedCardArray(cardListType, includeSubdeckCounts);
+
+        // The following selects distinct cards from cardList (based on reference equality)
+        const distinctCardSet = new Set(cardList);
+        // console.log(`getDistinctCardCount: ${this.deckName} ${distinctCardSet.size} ${this.getCardCount(cardListType, includeSubdeckCounts)}`);
+        return distinctCardSet.size;
+    }
+
+    public getFlattenedCardArray(
+        cardListType: CardListType,
+        includeSubdeckCounts: boolean,
+    ): Card[] {
+        let result: Card[] = [] as Card[];
+        switch (cardListType) {
+            case CardListType.NewCard:
+                result = this.newFlashcards;
+                break;
+            case CardListType.DueCard:
+                result = this.dueFlashcards;
+                break;
+            case CardListType.All:
+                result = this.newFlashcards.concat(this.dueFlashcards);
+        }
+
+        if (includeSubdeckCounts) {
+            for (const subdeck of this.subdecks) {
+                result = result.concat(
+                    subdeck.getFlattenedCardArray(cardListType, includeSubdeckCounts),
+                );
+            }
+        }
+        return result;
+    }
+
     //
     // Returns a count of the number of this question's cards are present in this deck.
     // (The returned value would be <= question.cards.length)
@@ -67,6 +108,10 @@ export class Deck {
         return this.parent == null;
     }
 
+    getDeckByTopicTag(tag: string): Deck {
+        return this.getDeck(TopicPath.getTopicPathFromTag(tag));
+    }
+
     getDeck(topicPath: TopicPath): Deck {
         return this._getOrCreateDeck(topicPath, false);
     }
@@ -100,6 +145,8 @@ export class Deck {
         const list: string[] = [];
         // eslint-disable-next-line  @typescript-eslint/no-this-alias
         let deck: Deck = this;
+        // The root deck may have a dummy deck name, which we don't want
+        // So we first check that this isn't the root deck
         while (!deck.isRootDeck) {
             list.push(deck.deckName);
             deck = deck.parent;
@@ -125,17 +172,64 @@ export class Deck {
         return cardListType == CardListType.DueCard ? this.dueFlashcards : this.newFlashcards;
     }
 
-    appendCard(topicPath: TopicPath, cardObj: Card): void {
+    appendCard(topicPathList: TopicPathList, cardObj: Card): void {
+        if (topicPathList.list.length == 0) {
+            this.appendCardToRootDeck(cardObj);
+        } else {
+            // We explicitly are adding the same card object to each of the specified decks
+            // This is required by getDistinctCardCount()
+            for (const topicPath of topicPathList.list) {
+                this.appendCard_SingleTopic(topicPath, cardObj);
+            }
+        }
+    }
+
+    appendCardToRootDeck(cardObj: Card): void {
+        this.appendCard_SingleTopic(TopicPath.emptyPath, cardObj);
+    }
+
+    appendCard_SingleTopic(topicPath: TopicPath, cardObj: Card): void {
         const deck: Deck = this.getOrCreateDeck(topicPath);
         const cardList: Card[] = deck.getCardListForCardType(cardObj.cardListType);
 
         cardList.push(cardObj);
     }
 
-    deleteCard(card: Card): void {
-        const cardList: Card[] = this.getCardListForCardType(card.cardListType);
-        const idx = cardList.indexOf(card);
-        if (idx != -1) cardList.splice(idx, 1);
+    //
+    // The question lists all the topics in which this card is included.
+    // The topics are relative to the base deck, and this method must be called on that deck
+    //
+    deleteQuestionFromAllDecks(question: Question, exceptionIfMissing: boolean): void {
+        for (const card of question.cards) {
+            this.deleteCardFromAllDecks(card, exceptionIfMissing);
+        }
+    }
+
+    deleteQuestion(question: Question, exceptionIfMissing: boolean): void {
+        for (const card of question.cards) {
+            this.deleteCardFromThisDeck(card, exceptionIfMissing);
+        }
+    }
+
+    //
+    // The card's question lists all the topics in which this card is included.
+    // The topics are relative to the base deck, and this method must be called on that deck
+    //
+    deleteCardFromAllDecks(card: Card, exceptionIfMissing: boolean): void {
+        for (const topicPath of card.question.topicPathList.list) {
+            const deck: Deck = this.getDeck(topicPath);
+            deck.deleteCardFromThisDeck(card, exceptionIfMissing);
+        }
+    }
+
+    deleteCardFromThisDeck(card: Card, exceptionIfMissing: boolean): void {
+        const newIdx = this.newFlashcards.indexOf(card);
+        if (newIdx != -1) this.newFlashcards.splice(newIdx, 1);
+        const dueIdx = this.dueFlashcards.indexOf(card);
+        if (dueIdx != -1) this.dueFlashcards.splice(dueIdx, 1);
+        if (newIdx == -1 && dueIdx == -1 && exceptionIfMissing) {
+            throw `deleteCardFromThisDeck: Card: ${card.front} not found in deck: ${this.deckName}`;
+        }
     }
 
     deleteCardAtIndex(index: number, cardListType: CardListType): void {
@@ -167,9 +261,9 @@ export class Deck {
         }
     }
 
-    debugLogToConsole(desc: string = null) {
+    debugLogToConsole(desc: string = null, indent: number = 0) {
         let str: string = desc != null ? `${desc}: ` : "";
-        console.log((str += this.toString()));
+        console.log((str += this.toString(indent)));
     }
 
     toString(indent: number = 0): string {
diff --git a/src/DeckTreeIterator.ts b/src/DeckTreeIterator.ts
index 4aef6f0..1c5d0ae 100644
--- a/src/DeckTreeIterator.ts
+++ b/src/DeckTreeIterator.ts
@@ -15,10 +15,6 @@ export enum DeckOrder {
     PrevDeckComplete_Sequential,
     PrevDeckComplete_Random,
 }
-export enum IteratorDeckSource {
-    UpdatedByIterator,
-    CloneBeforeUse,
-}
 
 export interface IIteratorOrder {
     // Within a deck this specifies the order the cards should be reviewed
@@ -33,9 +29,10 @@ export interface IDeckTreeIterator {
     get currentDeck(): Deck;
     get currentCard(): Card;
     get hasCurrentCard(): boolean;
-    setDeck(deck: Deck): void;
-    deleteCurrentCard(): boolean;
-    deleteCurrentQuestion(): boolean;
+    setBaseDeck(baseDeck: Deck): void;
+    setIteratorTopicPath(topicPath: TopicPath): void;
+    deleteCurrentCardFromAllDecks(): boolean;
+    deleteCurrentQuestionFromAllDecks(): boolean;
     moveCurrentCardToEndOfList(): void;
     nextCard(): boolean;
 }
@@ -153,45 +150,21 @@ class SingleDeckIterator {
         return result;
     }
 
-    deleteCurrentQuestion(): void {
-        this.ensureCurrentCard();
-        const q: Question = this.currentCard.question;
-
-        // A question could have some cards in the new list and some in the due list
-        this.deleteQuestionFromList(q, CardListType.NewCard);
-        this.deleteQuestionFromList(q, CardListType.DueCard);
-
-        this.setNoCurrentCard();
-    }
-
-    private deleteQuestionFromList(q: Question, cardListType: CardListType): void {
-        const cards: Card[] = this.deck.getCardListForCardType(cardListType);
-        for (let i = cards.length - 1; i >= 0; i--) {
-            if (Object.is(q, cards[i].question)) this.deck.deleteCardAtIndex(i, cardListType);
-        }
-    }
-
-    deleteCurrentCard(): void {
-        this.ensureCurrentCard();
-        this.deck.deleteCardAtIndex(this.cardIdx, this.cardListType);
-        this.setNoCurrentCard();
-    }
-
     moveCurrentCardToEndOfList(): void {
         this.ensureCurrentCard();
         const cardList: Card[] = this.deck.getCardListForCardType(this.cardListType);
         if (cardList.length <= 1) return;
         const card = this.currentCard;
         this.deck.deleteCardAtIndex(this.cardIdx, this.cardListType);
-        this.deck.appendCard(TopicPath.emptyPath, card);
+        this.deck.appendCardToRootDeck(card);
         this.setNoCurrentCard();
     }
 
-    private setNoCurrentCard() {
+    setNoCurrentCard() {
         this.cardIdx = null;
     }
 
-    private ensureCurrentCard() {
+    ensureCurrentCard() {
         if (this.cardIdx == null || this.cardListType == null) throw "no current card";
     }
 
@@ -212,19 +185,41 @@ class SingleDeckIterator {
     }
 }
 
+//
+// Note that this iterator is destructive over the deck tree supplied to setBaseDeck()
+// The caller is required to first make a clone if this behavior is unwanted.
+//
+// Handling of multi-deck cards (implemented for https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/495):
+//      A "multi-deck card" is a card that is present in multiple decks, e.g. the following cat question is present in
+//      two separate decks.
+//
+//      #flashcards/language/words #flashcards/trivia/interesting
+//      A group of cats is called a::clowder
+//
+//      1. Whilst iterating, any multi-deck card is only returned once.
+//      2. All copies are removed from the deck tree supplied to setBaseDeck()
+//
 export class DeckTreeIterator implements IDeckTreeIterator {
     private iteratorOrder: IIteratorOrder;
-    private deckSource: IteratorDeckSource;
 
     private singleDeckIterator: SingleDeckIterator;
+    private baseDeckTree: Deck;
+
+    // The subset of baseDeckTree over which we are iterating
+    // Each item is treated as a single deck, i.e. any subdecks are ignored
     private deckArray: Deck[];
     private deckIdx?: number;
+
     private weightedRandomNumber: WeightedRandomNumber;
 
     get hasCurrentCard(): boolean {
         return this.deckIdx != null && this.singleDeckIterator.hasCurrentCard;
     }
 
+    get currentTopicPath(): TopicPath {
+        return this.currentDeck?.getTopicPath();
+    }
+
     get currentDeck(): Deck {
         if (this.deckIdx == null) return null;
         return this.deckArray[this.deckIdx];
@@ -237,18 +232,25 @@ export class DeckTreeIterator implements IDeckTreeIterator {
         return result;
     }
 
-    constructor(iteratorOrder: IIteratorOrder, deckSource: IteratorDeckSource) {
+    get currentQuestion(): Question {
+        return this.currentCard?.question;
+    }
+
+    constructor(iteratorOrder: IIteratorOrder, baseDeckTree: Deck) {
         this.singleDeckIterator = new SingleDeckIterator(iteratorOrder);
         this.iteratorOrder = iteratorOrder;
-        this.deckSource = deckSource;
         this.weightedRandomNumber = WeightedRandomNumber.create();
+        this.setBaseDeck(baseDeckTree);
     }
 
-    setDeck(deck: Deck): void {
-        // We don't want to change the supplied deck, so first clone
-        if (this.deckSource == IteratorDeckSource.CloneBeforeUse) deck = deck.clone();
+    setBaseDeck(baseDeck: Deck): void {
+        this.baseDeckTree = baseDeck;
+        this.singleDeckIterator.setNoCurrentCard();
+    }
 
-        this.deckArray = DeckTreeIterator.filterForDecksWithCards(deck.toDeckArray());
+    setIteratorTopicPath(topicPath: TopicPath): void {
+        const iteratorDeck: Deck = this.baseDeckTree.getDeck(topicPath);
+        this.deckArray = DeckTreeIterator.filterForDecksWithCards(iteratorDeck.toDeckArray());
         this.setDeckIdx(null);
     }
 
@@ -274,7 +276,7 @@ export class DeckTreeIterator implements IDeckTreeIterator {
 
         // Delete the current card so we don't return it again
         if (this.hasCurrentCard) {
-            this.singleDeckIterator.deleteCurrentCard();
+            this.baseDeckTree.deleteCardFromAllDecks(this.currentCard, true);
         }
 
         if (this.iteratorOrder.cardOrder == CardOrder.EveryCardRandomDeckAndCard) {
@@ -345,13 +347,20 @@ export class DeckTreeIterator implements IDeckTreeIterator {
         return true;
     }
 
-    deleteCurrentQuestion(): boolean {
-        this.singleDeckIterator.deleteCurrentQuestion();
+    deleteCurrentQuestionFromAllDecks(): boolean {
+        this.singleDeckIterator.ensureCurrentCard();
+
+        // Delete every card of this question from every deck specified for the question
+        // Note that not every card will necessarily be present, so we pass false to the following
+        this.baseDeckTree.deleteQuestionFromAllDecks(this.currentQuestion, false);
+        this.singleDeckIterator.setNoCurrentCard();
         return this.nextCard();
     }
 
-    deleteCurrentCard(): boolean {
-        this.singleDeckIterator.deleteCurrentCard();
+    deleteCurrentCardFromAllDecks(): boolean {
+        this.singleDeckIterator.ensureCurrentCard();
+        this.baseDeckTree.deleteCardFromAllDecks(this.currentCard, true);
+        this.singleDeckIterator.setNoCurrentCard();
         return this.nextCard();
     }
 
diff --git a/src/DeckTreeStatsCalculator.ts b/src/DeckTreeStatsCalculator.ts
index 5338719..2375cd7 100644
--- a/src/DeckTreeStatsCalculator.ts
+++ b/src/DeckTreeStatsCalculator.ts
@@ -5,11 +5,11 @@ import {
     DeckTreeIterator,
     IDeckTreeIterator,
     IIteratorOrder,
-    IteratorDeckSource,
 } from "./DeckTreeIterator";
 import { Card } from "./Card";
 import { Stats } from "./stats";
 import { CardScheduleInfo } from "./CardSchedule";
+import { TopicPath } from "./TopicPath";
 
 export class DeckTreeStatsCalculator {
     private deckTree: Deck;
@@ -20,12 +20,10 @@ export class DeckTreeStatsCalculator {
             deckOrder: DeckOrder.PrevDeckComplete_Sequential,
             cardOrder: CardOrder.DueFirstSequential,
         };
-        const iterator: IDeckTreeIterator = new DeckTreeIterator(
-            iteratorOrder,
-            IteratorDeckSource.CloneBeforeUse,
-        );
+        // Iteration is a destructive operation on the supplied tree, so we first take a copy
+        const iterator: IDeckTreeIterator = new DeckTreeIterator(iteratorOrder, deckTree.clone());
         const result = new Stats();
-        iterator.setDeck(deckTree);
+        iterator.setIteratorTopicPath(TopicPath.emptyPath);
         while (iterator.nextCard()) {
             const card: Card = iterator.currentCard;
             if (card.hasSchedule) {
diff --git a/src/FlashcardReviewSequencer.ts b/src/FlashcardReviewSequencer.ts
index 072e43f..d4c9eda 100644
--- a/src/FlashcardReviewSequencer.ts
+++ b/src/FlashcardReviewSequencer.ts
@@ -44,8 +44,12 @@ export enum FlashcardReviewMode {
 }
 
 export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
+    // We need the original deck tree so that we can still provide the total cards in each deck
     private _originalDeckTree: Deck;
+
+    // This is set by the caller, and must have the same deck hierarchy as originalDeckTree.
     private remainingDeckTree: Deck;
+
     private reviewMode: FlashcardReviewMode;
     private cardSequencer: IDeckTreeIterator;
     private settings: SRSettings;
@@ -86,15 +90,17 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
         return this.currentQuestion.note;
     }
 
+    // originalDeckTree isn't modified by the review process
+    // Only remainingDeckTree
     setDeckTree(originalDeckTree: Deck, remainingDeckTree: Deck): void {
+        this.cardSequencer.setBaseDeck(remainingDeckTree);
         this._originalDeckTree = originalDeckTree;
         this.remainingDeckTree = remainingDeckTree;
         this.setCurrentDeck(TopicPath.emptyPath);
     }
 
     setCurrentDeck(topicPath: TopicPath): void {
-        const deck: Deck = this.remainingDeckTree.getDeck(topicPath);
-        this.cardSequencer.setDeck(deck);
+        this.cardSequencer.setIteratorTopicPath(topicPath);
         this.cardSequencer.nextCard();
     }
 
@@ -105,19 +111,19 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
     getDeckStats(topicPath: TopicPath): DeckStats {
         const totalCount: number = this._originalDeckTree
             .getDeck(topicPath)
-            .getCardCount(CardListType.All, true);
+            .getDistinctCardCount(CardListType.All, true);
         const remainingDeck: Deck = this.remainingDeckTree.getDeck(topicPath);
-        const newCount: number = remainingDeck.getCardCount(CardListType.NewCard, true);
-        const dueCount: number = remainingDeck.getCardCount(CardListType.DueCard, true);
+        const newCount: number = remainingDeck.getDistinctCardCount(CardListType.NewCard, true);
+        const dueCount: number = remainingDeck.getDistinctCardCount(CardListType.DueCard, true);
         return new DeckStats(dueCount, newCount, totalCount);
     }
 
     skipCurrentCard(): void {
-        this.cardSequencer.deleteCurrentQuestion();
+        this.cardSequencer.deleteCurrentQuestionFromAllDecks();
     }
 
     private deleteCurrentCard(): void {
-        this.cardSequencer.deleteCurrentCard();
+        this.cardSequencer.deleteCurrentCardFromAllDecks();
     }
 
     async processReview(response: ReviewResponse): Promise<void> {
@@ -145,7 +151,7 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
         } else {
             if (this.settings.burySiblingCards) {
                 await this.burySiblingCards();
-                this.cardSequencer.deleteCurrentQuestion();
+                this.cardSequencer.deleteCurrentQuestionFromAllDecks();
             } else {
                 this.deleteCurrentCard();
             }
diff --git a/src/Note.ts b/src/Note.ts
index 46ae5cb..1546693 100644
--- a/src/Note.ts
+++ b/src/Note.ts
@@ -24,7 +24,7 @@ export class Note {
     appendCardsToDeck(deck: Deck): void {
         for (const question of this.questionList) {
             for (const card of question.cards) {
-                deck.appendCard(question.topicPath, card);
+                deck.appendCard(question.topicPathList, card);
             }
         }
     }
@@ -33,7 +33,9 @@ export class Note {
         let str: string = `Note: ${desc}: ${this.questionList.length} questions\r\n`;
         for (let i = 0; i < this.questionList.length; i++) {
             const q: Question = this.questionList[i];
-            str += `[${i}]: ${q.questionType}: ${q.lineNo}: ${q.topicPath?.path}: ${q.questionText.original}\r\n`;
+            str += `[${i}]: ${q.questionType}: ${q.lineNo}: ${q.topicPathList?.format("|")}: ${
+                q.questionText.original
+            }\r\n`;
         }
         console.debug(str);
     }
diff --git a/src/NoteFileLoader.ts b/src/NoteFileLoader.ts
index 83ed69e..252e863 100644
--- a/src/NoteFileLoader.ts
+++ b/src/NoteFileLoader.ts
@@ -16,14 +16,16 @@ export class NoteFileLoader {
         this.settings = settings;
     }
 
-    async load(noteFile: ISRFile, noteTopicPath: TopicPath): Promise<Note> {
+    async load(noteFile: ISRFile, folderTopicPath: TopicPath): Promise<Note | null> {
         this.noteFile = noteFile;
 
         const questionParser: NoteQuestionParser = new NoteQuestionParser(this.settings);
 
+        const onlyKeepQuestionsWithTopicPath: boolean = true;
         const questionList: Question[] = await questionParser.createQuestionList(
             noteFile,
-            noteTopicPath,
+            folderTopicPath,
+            onlyKeepQuestionsWithTopicPath,
         );
 
         const result: Note = new Note(noteFile, questionList);
diff --git a/src/NoteParser.ts b/src/NoteParser.ts
index 01a00ab..15b7fdb 100644
--- a/src/NoteParser.ts
+++ b/src/NoteParser.ts
@@ -14,7 +14,7 @@ export class NoteParser {
 
     async parse(noteFile: ISRFile, folderTopicPath: TopicPath): Promise<Note> {
         const questionParser: NoteQuestionParser = new NoteQuestionParser(this.settings);
-        const questions = await questionParser.createQuestionList(noteFile, folderTopicPath);
+        const questions = await questionParser.createQuestionList(noteFile, folderTopicPath, true);
 
         const result: Note = new Note(noteFile, questions);
         return result;
diff --git a/src/NoteQuestionParser.ts b/src/NoteQuestionParser.ts
index fdd63bc..6e75945 100644
--- a/src/NoteQuestionParser.ts
+++ b/src/NoteQuestionParser.ts
@@ -1,56 +1,84 @@
+import { TagCache } from "obsidian";
 import { Card } from "./Card";
 import { CardScheduleInfo, NoteCardScheduleParser } from "./CardSchedule";
-import { parse } from "./parser";
-import { CardType, Question } from "./Question";
+import { parseEx, ParsedQuestionInfo } from "./parser";
+import { Question, QuestionText } from "./Question";
 import { CardFrontBack, CardFrontBackUtil } from "./QuestionType";
-import { SRSettings } from "./settings";
+import { SRSettings, SettingsUtil } from "./settings";
 import { ISRFile } from "./SRFile";
-import { TopicPath } from "./TopicPath";
-
-export class ParsedQuestionInfo {
-    cardType: CardType;
-    cardText: string;
-    lineNo: number;
-
-    constructor(cardType: CardType, cardText: string, lineNo: number) {
-        this.cardType = cardType;
-        this.cardText = cardText;
-        this.lineNo = lineNo;
-    }
-}
+import { TopicPath, TopicPathList } from "./TopicPath";
+import { extractFrontmatter, splitTextIntoLineArray } from "./util/utils";
 
 export class NoteQuestionParser {
     settings: SRSettings;
     noteFile: ISRFile;
-    noteTopicPath: TopicPath;
+    folderTopicPath: TopicPath;
     noteText: string;
+    noteLines: string[];
+    tagCacheList: TagCache[];
+    frontmatterTopicPathList: TopicPathList;
+    contentTopicPathInfo: TopicPathList[];
+    questionList: Question[];
 
     constructor(settings: SRSettings) {
         this.settings = settings;
     }
 
-    async createQuestionList(noteFile: ISRFile, folderTopicPath: TopicPath): Promise<Question[]> {
+    async createQuestionList(
+        noteFile: ISRFile,
+        folderTopicPath: TopicPath,
+        onlyKeepQuestionsWithTopicPath: boolean,
+    ): Promise<Question[]> {
         this.noteFile = noteFile;
         const noteText: string = await noteFile.read();
-        let noteTopicPath: TopicPath;
-        if (this.settings.convertFoldersToDecks) {
-            noteTopicPath = folderTopicPath;
+
+        // Get the list of tags, and analyse for the topic list
+        const tagCacheList: TagCache[] = noteFile.getAllTagsFromText();
+
+        const hasTopicPaths =
+            tagCacheList.some((item) => SettingsUtil.isFlashcardTag(this.settings, item.tag)) ||
+            folderTopicPath.hasPath;
+        if (hasTopicPaths) {
+            // The following analysis can require fair computation.
+            // There is no point doing it if there aren't any topic paths
+
+            // Create the question list
+            this.questionList = this.doCreateQuestionList(
+                noteText,
+                folderTopicPath,
+                this.tagCacheList,
+            );
+
+            // For each question, determine it's TopicPathList
+            [this.frontmatterTopicPathList, this.contentTopicPathInfo] =
+                this.analyseTagCacheList(tagCacheList);
+            for (const question of this.questionList) {
+                question.topicPathList = this.determineQuestionTopicPathList(question);
+            }
+
+            // Now only keep questions that have a topic list
+            if (onlyKeepQuestionsWithTopicPath) {
+                this.questionList = this.questionList.filter((q) => q.topicPathList);
+            }
         } else {
-            const tagList: string[] = noteFile.getAllTags();
-            noteTopicPath = this.determineTopicPathFromTags(tagList);
+            this.questionList = [] as Question[];
         }
-        const result: Question[] = this.doCreateQuestionList(noteText, noteTopicPath);
-        return result;
+        return this.questionList;
     }
 
-    private doCreateQuestionList(noteText: string, noteTopicPath: TopicPath): Question[] {
+    private doCreateQuestionList(
+        noteText: string,
+        folderTopicPath: TopicPath,
+        tagCacheList: TagCache[],
+    ): Question[] {
         this.noteText = noteText;
-        this.noteTopicPath = noteTopicPath;
+        this.noteLines = splitTextIntoLineArray(noteText);
+        this.folderTopicPath = folderTopicPath;
+        this.tagCacheList = tagCacheList;
 
         const result: Question[] = [];
-        const parsedQuestionInfoList: [CardType, string, number][] = this.parseQuestions();
-        for (const t of parsedQuestionInfoList) {
-            const parsedQuestionInfo: ParsedQuestionInfo = new ParsedQuestionInfo(t[0], t[1], t[2]);
+        const parsedQuestionInfoList: ParsedQuestionInfo[] = this.parseQuestions();
+        for (const parsedQuestionInfo of parsedQuestionInfoList) {
             const question: Question = this.createQuestionObject(parsedQuestionInfo);
 
             // Each rawCardText can turn into multiple CardFrontBack's (e.g. CardType.Cloze, CardType.SingleLineReversed)
@@ -79,9 +107,9 @@ export class NoteQuestionParser {
         return result;
     }
 
-    private parseQuestions(): [CardType, string, number][] {
+    private parseQuestions(): ParsedQuestionInfo[] {
         const settings: SRSettings = this.settings;
-        const result: [CardType, string, number][] = parse(
+        const result: ParsedQuestionInfo[] = parseEx(
             this.noteText,
             settings.singleLineCardSeparator,
             settings.singleLineReversedCardSeparator,
@@ -95,15 +123,13 @@ export class NoteQuestionParser {
     }
 
     private createQuestionObject(parsedQuestionInfo: ParsedQuestionInfo): Question {
-        const { cardType, cardText, lineNo } = parsedQuestionInfo;
-
-        const questionContext: string[] = this.noteFile.getQuestionContext(lineNo);
+        const questionContext: string[] = this.noteFile.getQuestionContext(
+            parsedQuestionInfo.firstLineNum,
+        );
         const result = Question.Create(
             this.settings,
-            cardType,
-            this.noteTopicPath,
-            cardText,
-            lineNo,
+            parsedQuestionInfo,
+            null, // We haven't worked out the TopicPathList yet
             questionContext,
         );
         return result;
@@ -135,13 +161,118 @@ export class NoteQuestionParser {
         return siblings;
     }
 
-    private determineTopicPathFromTags(tagList: string[]): TopicPath {
-        let result: TopicPath = TopicPath.emptyPath;
-        outer: for (const tagToReview of this.settings.flashcardTags) {
-            for (const tag of tagList) {
-                if (tag === tagToReview || tag.startsWith(tagToReview + "/")) {
-                    result = TopicPath.getTopicPathFromTag(tag);
-                    break outer;
+    //
+    // Given the complete list of tags within a note:
+    // 1.   Only keep tags that are specified in the user settings as flashcardTags
+    // 2.   Filter out tags that are question specific
+    //      (these will be parsed separately by class QuestionText)
+    // 3.   Combine all tags present logically grouped together into a single entry
+    //      - All tags present on the same line grouped together
+    //      - All tags within frontmatter grouped together (note that multiple tags
+    //      within frontmatter appear on separate lines)
+    //
+    private analyseTagCacheList(tagCacheList: TagCache[]): [TopicPathList, TopicPathList[]] {
+        let frontmatterTopicPathList: TopicPathList = null;
+        const contentTopicPathList: TopicPathList[] = [] as TopicPathList[];
+
+        // Only keep 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(
+            (item) =>
+                SettingsUtil.isFlashcardTag(this.settings, item.tag) &&
+                this.questionList.every(
+                    (q) => !q.parsedQuestionInfo.isQuestionLineNum(item.position.start.line),
+                ),
+        );
+        let frontmatterLineCount: number = null;
+        if (filteredTagCacheList.length > 0) {
+            // To simplify analysis, ensure that the supplied list is ordered by line number
+            tagCacheList.sort((a, b) => a.position.start.line - b.position.start.line);
+
+            // 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(
+                    (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(tagCacheList, 0);
+            }
+        }
+        //
+        const contentStartLineNum: number = frontmatterLineCount > 0 ? frontmatterLineCount + 1 : 0;
+        const contentTagCacheList: TagCache[] = filteredTagCacheList.filter(
+            (item) => item.position.start.line >= contentStartLineNum,
+        );
+
+        let list: TagCache[] = [] as TagCache[];
+        for (const t 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));
+                    list = [] as TagCache[];
+                }
+            }
+            list.push(t);
+        }
+        if (list.length > 0) {
+            const startLineNum: number = list[0].position.start.line;
+            contentTopicPathList.push(this.createTopicPathList(list, startLineNum));
+        }
+
+        return [frontmatterTopicPathList, contentTopicPathList];
+    }
+
+    private createTopicPathList(tagCacheList: TagCache[], lineNum: number): TopicPathList {
+        const list: TopicPath[] = [] as TopicPath[];
+        for (const tagCache of tagCacheList) {
+            list.push(TopicPath.getTopicPathFromTag(tagCache.tag));
+        }
+        return new TopicPathList(list, lineNum);
+    }
+
+    //
+    // A question can be associated with multiple topics (hence returning TopicPathList and not just TopicPath).
+    //
+    // If the question has an associated question specific TopicPath, then that is returned.
+    //
+    // Else the first TopicPathList prior to the question (in the order present in the file) is returned.
+    // That could be either the tags within the note's frontmatter, or tags on lines within the note's content.
+    //
+    private determineQuestionTopicPathList(question: Question): TopicPathList {
+        let result: TopicPathList;
+        if (this.settings.convertFoldersToDecks) {
+            result = new TopicPathList([this.folderTopicPath]);
+        } else {
+            // If present, the question specific TopicPath takes precedence over everything else
+            const questionText: QuestionText = question.questionText;
+            if (questionText.topicPathWithWs)
+                result = new TopicPathList(
+                    [questionText.topicPathWithWs.topicPath],
+                    question.parsedQuestionInfo.firstLineNum,
+                );
+            else {
+                // By default we start off with any TopicPathList present in the frontmatter
+                result = this.frontmatterTopicPathList;
+
+                // 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;
+                        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];
                 }
             }
         }
diff --git a/src/Question.ts b/src/Question.ts
index 1e18138..16b1b27 100644
--- a/src/Question.ts
+++ b/src/Question.ts
@@ -7,8 +7,9 @@ import {
     SR_HTML_COMMENT_END,
 } from "./constants";
 import { Note } from "./Note";
+import { ParsedQuestionInfo } from "./parser";
 import { SRSettings } from "./settings";
-import { TopicPath, TopicPathWithWs } from "./TopicPath";
+import { TopicPath, TopicPathList, TopicPathWithWs } from "./TopicPath";
 import { MultiLineTextFinder } from "./util/MultiLineTextFinder";
 import { cyrb53, stringTrimStart } from "./util/utils";
 
@@ -177,15 +178,21 @@ export class QuestionText {
 
 export class Question {
     note: Note;
-    questionType: CardType;
-    topicPath: TopicPath;
+    parsedQuestionInfo: ParsedQuestionInfo;
+    topicPathList: TopicPathList;
     questionText: QuestionText;
-    lineNo: number;
     hasEditLaterTag: boolean;
     questionContext: string[];
     cards: Card[];
     hasChanged: boolean;
 
+    get questionType(): CardType {
+        return this.parsedQuestionInfo.cardType;
+    }
+    get lineNo(): number {
+        return this.parsedQuestionInfo.firstLineNum;
+    }
+
     constructor(init?: Partial<Question>) {
         Object.assign(this, init);
     }
@@ -278,27 +285,28 @@ export class Question {
         this.hasChanged = false;
     }
 
+    formatTopicPathList(): string {
+        return this.topicPathList.format("|");
+    }
+
     static Create(
         settings: SRSettings,
-        questionType: CardType,
-        noteTopicPath: TopicPath,
-        originalText: string,
-        lineNo: number,
+        parsedQuestionInfo: ParsedQuestionInfo,
+        noteTopicPathList: TopicPathList,
         context: string[],
     ): Question {
-        const hasEditLaterTag = originalText.includes(settings.editLaterTag);
-        const questionText: QuestionText = QuestionText.create(originalText, settings);
+        const hasEditLaterTag = parsedQuestionInfo.text.includes(settings.editLaterTag);
+        const questionText: QuestionText = QuestionText.create(parsedQuestionInfo.text, settings);
 
-        let topicPath: TopicPath = noteTopicPath;
+        let topicPathList: TopicPathList = noteTopicPathList;
         if (questionText.topicPathWithWs) {
-            topicPath = questionText.topicPathWithWs.topicPath;
+            topicPathList = new TopicPathList([questionText.topicPathWithWs.topicPath]);
         }
 
         const result: Question = new Question({
-            questionType,
-            topicPath,
+            parsedQuestionInfo,
+            topicPathList,
             questionText,
-            lineNo,
             hasEditLaterTag,
             questionContext: context,
             cards: null,
diff --git a/src/SRFile.ts b/src/SRFile.ts
index 4637807..f180c78 100644
--- a/src/SRFile.ts
+++ b/src/SRFile.ts
@@ -1,16 +1,9 @@
-import {
-    MetadataCache,
-    TFile,
-    Vault,
-    getAllTags as ObsidianGetAllTags,
-    HeadingCache,
-} from "obsidian";
-import { getAllTagsFromText } from "./util/utils";
+import { MetadataCache, TFile, Vault, HeadingCache, TagCache, FrontMatterCache } from "obsidian";
 
 export interface ISRFile {
     get path(): string;
     get basename(): string;
-    getAllTags(): string[];
+    getAllTagsFromText(): TagCache[];
     getQuestionContext(cardLine: number): string[];
     read(): Promise<string>;
     write(content: string): Promise<void>;
@@ -35,9 +28,44 @@ export class SrTFile implements ISRFile {
         return this.file.basename;
     }
 
-    getAllTags(): string[] {
+    getAllTagsFromText(): TagCache[] {
+        const result: TagCache[] = [] as TagCache[];
         const fileCachedData = this.metadataCache.getFileCache(this.file) || {};
-        return ObsidianGetAllTags(fileCachedData) || [];
+        if (fileCachedData.tags?.length > 0) {
+            result.push(...fileCachedData.tags);
+        }
+
+        // RZ: 2024-01-28 fileCachedData.tags doesn't include the tags within the frontmatter, need to access those separately
+        // This is different to the Obsidian function getAllTags() which does return all tags including those within the
+        // frontmatter.
+        result.push(...this.getFrontmatterTags(fileCachedData.frontmatter));
+
+        return result;
+    }
+
+    private getFrontmatterTags(frontmatter: FrontMatterCache): TagCache[] {
+        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;
+
+            // Frontmatter tags are comma separated and don't include the "#", so we need to add that in
+            const tagStrList: string[] = frontmatterTags.split(",");
+            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 },
+                    },
+                };
+                result.push(tag);
+            }
+        }
+        return result;
     }
 
     getQuestionContext(cardLine: number): string[] {
@@ -72,38 +100,3 @@ export class SrTFile implements ISRFile {
         await this.vault.modify(this.file, content);
     }
 }
-
-export class UnitTestSRFile implements ISRFile {
-    content: string;
-    _path: string;
-
-    constructor(content: string, path: string = null) {
-        this.content = content;
-        this._path = path;
-    }
-
-    get path(): string {
-        return this._path;
-    }
-
-    get basename(): string {
-        return "";
-    }
-
-    getAllTags(): string[] {
-        return getAllTagsFromText(this.content);
-    }
-
-    // eslint-disable-next-line  @typescript-eslint/no-unused-vars
-    getQuestionContext(cardLine: number): string[] {
-        return [];
-    }
-
-    async read(): Promise<string> {
-        return this.content;
-    }
-
-    async write(content: string): Promise<void> {
-        this.content = content;
-    }
-}
diff --git a/src/TopicPath.ts b/src/TopicPath.ts
index 6b34bc2..575edfa 100644
--- a/src/TopicPath.ts
+++ b/src/TopicPath.ts
@@ -38,34 +38,6 @@ export class TopicPath {
         return result;
     }
 
-    static getTopicPathOfFile(noteFile: ISRFile, settings: SRSettings): TopicPath {
-        let deckPath: string[] = [];
-        let result: TopicPath = TopicPath.emptyPath;
-
-        if (settings.convertFoldersToDecks) {
-            deckPath = noteFile.path.split("/");
-            deckPath.pop(); // remove filename
-            if (deckPath.length != 0) {
-                result = new TopicPath(deckPath);
-            }
-        } else {
-            const tagList: TopicPath[] = this.getTopicPathsFromTagList(noteFile.getAllTags());
-
-            outer: for (const tagToReview of this.getTopicPathsFromTagList(
-                settings.flashcardTags,
-            )) {
-                for (const tag of tagList) {
-                    if (tagToReview.isSameOrAncestorOf(tag)) {
-                        result = tag;
-                        break outer;
-                    }
-                }
-            }
-        }
-
-        return result;
-    }
-
     isSameOrAncestorOf(topicPath: TopicPath): boolean {
         if (this.isEmptyPath) return topicPath.isEmptyPath;
         if (this.path.length > topicPath.path.length) return false;
@@ -80,14 +52,6 @@ export class TopicPath {
         return path?.length > 0 ? TopicPath.getTopicPathFromTag(path) : null;
     }
 
-    static getTopicPathsFromTagList(tagList: string[]): TopicPath[] {
-        const result: TopicPath[] = [];
-        for (const tag of tagList) {
-            if (this.isValidTag(tag)) result.push(TopicPath.getTopicPathFromTag(tag));
-        }
-        return result;
-    }
-
     static isValidTag(tag: string): boolean {
         if (tag == null || tag.length == 0) return false;
         if (tag[0] != "#") return false;
@@ -107,6 +71,91 @@ export class TopicPath {
             .filter((str) => str);
         return new TopicPath(path);
     }
+
+    static getFolderPathFromFilename(noteFile: ISRFile, settings: SRSettings): TopicPath {
+        let result: TopicPath = TopicPath.emptyPath;
+
+        if (settings.convertFoldersToDecks) {
+            const deckPath: string[] = noteFile.path.split("/");
+            deckPath.pop(); // remove filename
+            if (deckPath.length != 0) {
+                result = new TopicPath(deckPath);
+            }
+        }
+
+        return result;
+    }
+}
+
+export class TopicPathList {
+    list: TopicPath[];
+    lineNum: number;
+
+    constructor(list: TopicPath[], lineNum: number = null) {
+        if (list == null) throw "TopicPathList null";
+        this.list = list;
+        this.lineNum = lineNum;
+    }
+
+    get length(): number {
+        return this.list.length;
+    }
+
+    isAnyElementSameOrAncestorOf(topicPath: TopicPath): boolean {
+        return this.list.some((item) => item.isSameOrAncestorOf(topicPath));
+    }
+
+    formatPsv() {
+        return this.format("|");
+    }
+
+    format(sep: string) {
+        return this.list.map((topicPath) => topicPath.formatAsTag()).join(sep);
+    }
+
+    static empty(): TopicPathList {
+        return new TopicPathList([]);
+    }
+
+    static fromPsv(str: string, lineNum: number): TopicPathList {
+        const result: TopicPathList = TopicPathList.convertTagListToTopicPathList(str.split("|"));
+        result.lineNum = lineNum;
+        return result;
+    }
+
+    //
+    // tagList is a list of tags such as:
+    //      ["#flashcards/computing", "#boring-stuff", "#news-worthy"]
+    // validTopicPathList is a list of valid tags, such as those from settings.flashcardTags,E.g.
+    //      ["#flashcards"]
+    //
+    // This returns a filtered version of tagList, containing only topic paths that are considered valid.
+    // Validity is defined as "isAnyElementSameOrAncestorOf", and "#flashcards" is considered the ancestor of
+    // "#flashcards/computing".
+    //
+    // Therefore this would return:
+    //      "#flashcards/computing" (but not "#boring-stuff" or "#news-worthy")
+    //
+    static filterValidTopicPathsFromTagList(
+        list: TopicPathList,
+        validTopicPathList: TopicPathList,
+        lineNum: number = null,
+    ): TopicPathList {
+        const result: TopicPath[] = [];
+        for (const tag of list.list) {
+            if (validTopicPathList.isAnyElementSameOrAncestorOf(tag)) result.push(tag);
+        }
+
+        return new TopicPathList(result, lineNum);
+    }
+
+    static convertTagListToTopicPathList(tagList: string[]): TopicPathList {
+        const result: TopicPath[] = [];
+        for (const tag of tagList) {
+            if (TopicPath.isValidTag(tag)) result.push(TopicPath.getTopicPathFromTag(tag));
+        }
+        return new TopicPathList(result);
+    }
 }
 
 export class TopicPathWithWs {
diff --git a/src/gui/flashcard-modal.tsx b/src/gui/flashcard-modal.tsx
index a574de4..f73cbca 100644
--- a/src/gui/flashcard-modal.tsx
+++ b/src/gui/flashcard-modal.tsx
@@ -449,7 +449,9 @@ export class FlashcardModal extends Modal {
 
         this.responseDiv.style.display = "none";
         this.resetButton.disabled = true;
-        this.titleEl.setText(`${deck.deckName}: ${deck.getCardCount(CardListType.All, true)}`);
+        this.titleEl.setText(
+            `${deck.deckName}: ${deck.getDistinctCardCount(CardListType.All, true)}`,
+        );
 
         this.answerBtn.style.display = "initial";
         this.flashcardView.empty();
diff --git a/src/gui/stats-modal.tsx b/src/gui/stats-modal.tsx
index e04cb87..0607258 100644
--- a/src/gui/stats-modal.tsx
+++ b/src/gui/stats-modal.tsx
@@ -170,7 +170,10 @@ export class StatsModal extends Modal {
         );
 
         // Add card types
-        const totalCardsCount: number = this.plugin.deckTree.getCardCount(CardListType.All, true);
+        const totalCardsCount: number = this.plugin.deckTree.getDistinctCardCount(
+            CardListType.All,
+            true,
+        );
         createStatsChart(
             "pie",
             "cardTypesChart",
diff --git a/src/main.ts b/src/main.ts
index 1a46390..c63a8e0 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -23,7 +23,6 @@ import {
     DeckTreeIterator,
     IDeckTreeIterator,
     IIteratorOrder,
-    IteratorDeckSource,
     DeckOrder,
 } from "./DeckTreeIterator";
 import { CardScheduleCalculator } from "./CardSchedule";
@@ -286,8 +285,7 @@ export default class SRPlugin extends Plugin {
         noteFile: TFile,
         reviewMode: FlashcardReviewMode,
     ): Promise<void> {
-        const topicPath: TopicPath = this.findTopicPath(this.createSrTFile(noteFile));
-        const note: Note = await this.loadNote(noteFile, topicPath);
+        const note: Note = await this.loadNote(noteFile);
 
         const deckTree = new Deck("root", null);
         note.appendCardsToDeck(deckTree);
@@ -304,7 +302,7 @@ export default class SRPlugin extends Plugin {
         remainingDeckTree: Deck,
         reviewMode: FlashcardReviewMode,
     ): void {
-        const deckIterator = SRPlugin.createDeckTreeIterator(this.data.settings);
+        const deckIterator = SRPlugin.createDeckTreeIterator(this.data.settings, remainingDeckTree);
         const cardScheduleCalculator = new CardScheduleCalculator(
             this.data.settings,
             this.easeByPath,
@@ -321,18 +319,17 @@ export default class SRPlugin extends Plugin {
         new FlashcardModal(this.app, this, this.data.settings, reviewSequencer, reviewMode).open();
     }
 
-    private static createDeckTreeIterator(settings: SRSettings): IDeckTreeIterator {
+    private static createDeckTreeIterator(settings: SRSettings, baseDeck: Deck): IDeckTreeIterator {
         let cardOrder: CardOrder = CardOrder[settings.flashcardCardOrder as keyof typeof CardOrder];
         if (cardOrder === undefined) cardOrder = CardOrder.DueFirstSequential;
         let deckOrder: DeckOrder = DeckOrder[settings.flashcardDeckOrder as keyof typeof DeckOrder];
         if (deckOrder === undefined) deckOrder = DeckOrder.PrevDeckComplete_Sequential;
-        console.log(`createDeckTreeIterator: CardOrder: ${cardOrder}, DeckOrder: ${deckOrder}`);
 
         const iteratorOrder: IIteratorOrder = {
             deckOrder,
             cardOrder,
         };
-        return new DeckTreeIterator(iteratorOrder, IteratorDeckSource.UpdatedByIterator);
+        return new DeckTreeIterator(iteratorOrder, baseDeck);
     }
 
     async sync(): Promise<void> {
@@ -392,9 +389,8 @@ export default class SRPlugin extends Plugin {
                 }
             }
 
-            const topicPath: TopicPath = this.findTopicPath(this.createSrTFile(noteFile));
-            if (topicPath.hasPath) {
-                const note: Note = await this.loadNote(noteFile, topicPath);
+            const note: Note = await this.loadNote(noteFile);
+            if (note.questionList.length > 0) {
                 const flashcardsInNoteAvgEase: number = NoteEaseCalculator.Calculate(
                     note,
                     this.data.settings,
@@ -405,7 +401,6 @@ export default class SRPlugin extends Plugin {
                     this.easeByPath.setEaseForPath(note.filePath, flashcardsInNoteAvgEase);
                 }
             }
-
             const fileCachedData = this.app.metadataCache.getFileCache(noteFile) || {};
 
             const frontmatter: FrontMatterCache | Record<string, unknown> =
@@ -525,17 +520,28 @@ export default class SRPlugin extends Plugin {
         this.statusBar.setText(
             t("STATUS_BAR", {
                 dueNotesCount: this.dueNotesCount,
-                dueFlashcardsCount: this.remainingDeckTree.getCardCount(CardListType.All, true),
+                dueFlashcardsCount: this.remainingDeckTree.getDistinctCardCount(
+                    CardListType.All,
+                    true,
+                ),
             }),
         );
 
         if (this.data.settings.enableNoteReviewPaneOnStartup) this.reviewQueueView.redraw();
     }
 
-    async loadNote(noteFile: TFile, topicPath: TopicPath): Promise<Note> {
+    async loadNote(noteFile: TFile): Promise<Note> {
         const loader: NoteFileLoader = new NoteFileLoader(this.data.settings);
-        const note: Note = await loader.load(this.createSrTFile(noteFile), topicPath);
-        if (note.hasChanged) note.writeNoteFile(this.data.settings);
+        const srFile: ISRFile = this.createSrTFile(noteFile);
+        const folderTopicPath: TopicPath = TopicPath.getFolderPathFromFilename(
+            srFile,
+            this.data.settings,
+        );
+
+        const note: Note = await loader.load(this.createSrTFile(noteFile), folderTopicPath);
+        if (note.hasChanged) {
+            note.writeNoteFile(this.data.settings);
+        }
         return note;
     }
 
@@ -665,8 +671,7 @@ export default class SRPlugin extends Plugin {
         }
 
         if (this.data.settings.burySiblingCards) {
-            const topicPath: TopicPath = this.findTopicPath(this.createSrTFile(note));
-            const noteX: Note = await this.loadNote(note, topicPath);
+            const noteX: Note = await this.loadNote(note);
             for (const question of noteX.questionList) {
                 this.data.buryList.push(question.questionText.textHash);
             }
@@ -758,10 +763,6 @@ export default class SRPlugin extends Plugin {
         return new SrTFile(this.app.vault, this.app.metadataCache, note);
     }
 
-    findTopicPath(note: ISRFile): TopicPath {
-        return TopicPath.getTopicPathOfFile(note, this.data.settings);
-    }
-
     async loadPluginData(): Promise<void> {
         const loadedData: PluginData = await this.loadData();
         if (loadedData?.settings) upgradeSettings(loadedData.settings);
diff --git a/src/parser.ts b/src/parser.ts
index b170108..2e8e8c1 100644
--- a/src/parser.ts
+++ b/src/parser.ts
@@ -1,5 +1,25 @@
 import { CardType } from "./Question";
 
+export class ParsedQuestionInfo {
+    cardType: CardType;
+    text: string;
+
+    // Line numbers start at 0
+    firstLineNum: number;
+    lastLineNum: number;
+
+    constructor(cardType: CardType, text: string, firstLineNum: number, lastLineNum: number) {
+        this.cardType = cardType;
+        this.text = text;
+        this.firstLineNum = firstLineNum;
+        this.lastLineNum = lastLineNum;
+    }
+
+    isQuestionLineNum(lineNum: number): boolean {
+        return lineNum >= this.firstLineNum && lineNum <= this.lastLineNum;
+    }
+}
+
 /**
  * Returns flashcards found in `text`
  *
@@ -10,7 +30,7 @@ import { CardType } from "./Question";
  * @param multilineReversedCardSeparator - Separator for multiline basic card
  * @returns An array of [CardType, card text, line number] tuples
  */
-export function parse(
+export function parseEx(
     text: string,
     singlelineCardSeparator: string,
     singlelineReversedCardSeparator: string,
@@ -19,18 +39,20 @@ export function parse(
     convertHighlightsToClozes: boolean,
     convertBoldTextToClozes: boolean,
     convertCurlyBracketsToClozes: boolean,
-): [CardType, string, number][] {
+): ParsedQuestionInfo[] {
     let cardText = "";
-    const cards: [CardType, string, number][] = [];
+    const cards: ParsedQuestionInfo[] = [];
     let cardType: CardType | null = null;
-    let lineNo = 0;
+    let firstLineNo = 0;
+    let lastLineNo = 0;
 
     const lines: string[] = text.replaceAll("\r\n", "\n").split("\n");
     for (let i = 0; i < lines.length; i++) {
         const currentLine = lines[i];
         if (currentLine.length === 0) {
             if (cardType) {
-                cards.push([cardType, cardText, lineNo]);
+                lastLineNo = i - 1;
+                cards.push(new ParsedQuestionInfo(cardType, cardText, firstLineNo, lastLineNo));
                 cardType = null;
             }
 
@@ -44,6 +66,9 @@ export function parse(
 
         if (cardText.length > 0) {
             cardText += "\n";
+        } else if (cardText.length === 0) {
+            // This could be the first line of a multi line question
+            firstLineNo = i;
         }
         cardText += currentLine.trimEnd();
 
@@ -55,12 +80,13 @@ export function parse(
                 ? CardType.SingleLineReversed
                 : CardType.SingleLineBasic;
             cardText = lines[i];
-            lineNo = i;
+            firstLineNo = i;
             if (i + 1 < lines.length && lines[i + 1].startsWith("<!--SR:")) {
                 cardText += "\n" + lines[i + 1];
                 i++;
             }
-            cards.push([cardType, cardText, lineNo]);
+            lastLineNo = i;
+            cards.push(new ParsedQuestionInfo(cardType, cardText, firstLineNo, lastLineNo));
             cardType = null;
             cardText = "";
         } else if (
@@ -70,13 +96,15 @@ export function parse(
                 (convertCurlyBracketsToClozes && /{{.*?}}/gm.test(currentLine)))
         ) {
             cardType = CardType.Cloze;
-            lineNo = i;
+
+            // Explicitly don't change firstLineNo, as we might not see the cloze markers on the first line
+            // of a multi line cloze question. I.e. firstLineNo may be less than i;
         } else if (currentLine.trim() === multilineCardSeparator) {
             cardType = CardType.MultiLineBasic;
-            lineNo = i;
+            // Explicitly don't change firstLineNo, as per above comment
         } else if (currentLine.trim() === multilineReversedCardSeparator) {
             cardType = CardType.MultiLineReversed;
-            lineNo = i;
+            // Explicitly don't change firstLineNo, as per above comment
         } else if (currentLine.startsWith("```") || currentLine.startsWith("~~~")) {
             const codeBlockClose = currentLine.match(/`+|~+/)[0];
             while (i + 1 < lines.length && !lines[i + 1].startsWith(codeBlockClose)) {
@@ -89,7 +117,8 @@ export function parse(
     }
 
     if (cardType && cardText) {
-        cards.push([cardType, cardText, lineNo]);
+        lastLineNo = lines.length - 1;
+        cards.push(new ParsedQuestionInfo(cardType, cardText, firstLineNo, lastLineNo));
     }
 
     return cards;
diff --git a/src/settings.ts b/src/settings.ts
index d5bcfe8..3622e73 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -106,6 +106,21 @@ export function upgradeSettings(settings: SRSettings) {
     }
 }
 
+export class SettingsUtil {
+    static isFlashcardTag(settings: SRSettings, tag: string): boolean {
+        return SettingsUtil.isTagInList(settings.flashcardTags, tag);
+    }
+
+    private static isTagInList(tagList: string[], tag: string): boolean {
+        for (const tagFromList of tagList) {
+            if (tag === tagFromList || tag.startsWith(tagFromList + "/")) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
+
 // https://github.com/mgmeyers/obsidian-kanban/blob/main/src/Settings.ts
 let applyDebounceTimer = 0;
 function applySettingsUpdate(callback: () => void): void {
diff --git a/src/util/utils.ts b/src/util/utils.ts
index 8fc2717..6407fb2 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -1,6 +1,6 @@
 import moment from "moment";
 import { Moment } from "moment";
-import { PREFERRED_DATE_FORMAT } from "src/constants";
+import { PREFERRED_DATE_FORMAT, YAML_FRONT_MATTER_REGEX } from "src/constants";
 
 type Hex = number;
 
@@ -85,13 +85,6 @@ export function formatDate_YYYY_MM_DD(ticks: Moment): string {
     return ticks.format(PREFERRED_DATE_FORMAT);
 }
 
-export function getAllTagsFromText(text: string): string[] {
-    const tagRegex = /#[^\s#]+/gi;
-    const result: RegExpMatchArray = text.match(tagRegex);
-    if (!result) return [];
-    return result;
-}
-
 export function splitTextIntoLineArray(text: string): string[] {
     return text.replaceAll("\r\n", "\n").split("\n");
 }
@@ -103,6 +96,37 @@ export function stringTrimStart(str: string): [string, string] {
     return [ws, trimmed];
 }
 
+export function extractFrontmatter(str: string): [string, string] {
+    let frontmatter: string = "";
+    let content: string = "";
+    let frontmatterEndLineNum: number = null;
+    if (YAML_FRONT_MATTER_REGEX.test) {
+        const lines: string[] = splitTextIntoLineArray(str);
+
+        // The end "---" marker must be on the third line (index 2) or later
+        for (let i = 2; i < lines.length; i++) {
+            if (lines[i] == "---") {
+                frontmatterEndLineNum = i;
+                break;
+            }
+        }
+
+        if (frontmatterEndLineNum) {
+            const frontmatterStartLineNum: number = 0;
+            const frontmatterLineCount: number =
+                frontmatterEndLineNum - frontmatterStartLineNum + 1;
+            const frontmatterLines: string[] = lines.splice(
+                frontmatterStartLineNum,
+                frontmatterLineCount,
+            );
+            frontmatter = frontmatterLines.join("\n");
+            content = lines.join("\n");
+        }
+    }
+    if (frontmatter.length == 0) content = str;
+    return [frontmatter, content];
+}
+
 //
 // Returns the index of the line that consists of the search string.
 //


Test patch:
diff --git a/tests/unit/DeckTreeIterator.test.ts b/tests/unit/DeckTreeIterator.test.ts
index c245603..742bb41 100644
--- a/tests/unit/DeckTreeIterator.test.ts
+++ b/tests/unit/DeckTreeIterator.test.ts
@@ -3,13 +3,7 @@ import { CardListType, Deck } from "src/Deck";
 import { DEFAULT_SETTINGS } from "src/settings";
 import { SampleItemDecks } from "./SampleItems";
 import { TopicPath } from "src/TopicPath";
-import {
-    CardOrder,
-    DeckTreeIterator,
-    IIteratorOrder,
-    IteratorDeckSource,
-    DeckOrder,
-} from "src/DeckTreeIterator";
+import { CardOrder, DeckTreeIterator, IIteratorOrder, DeckOrder } from "src/DeckTreeIterator";
 import {
     StaticDateProvider,
     globalDateProvider,
@@ -44,37 +38,37 @@ Q3::A3`;
                 cardOrder: CardOrder.NewFirstSequential,
                 deckOrder: DeckOrder.PrevDeckComplete_Sequential,
             },
-            IteratorDeckSource.UpdatedByIterator,
+            deck,
         );
-        iterator.setDeck(deck);
+        iterator.setIteratorTopicPath(TopicPath.emptyPath);
         expect(iterator.currentDeck).toEqual(null);
     });
 });
 
-describe("nextCard", () => {
+describe("nextCard - Cards only present in a single deck", () => {
     describe("DeckOrder.PrevDeckComplete_Sequential; Sequential card ordering", () => {
         describe("Due cards before new cards", () => {
             test("Single topic, new cards only", async () => {
-                let text: string = `
+                let text: string = `#flashcards
 Q1::A1
 Q2::A2
 Q3::A3`;
                 let deck: Deck = await SampleItemDecks.createDeckFromText(
                     text,
-                    new TopicPath(["Root"]),
+                    TopicPath.emptyPath,
                 );
                 let iterator: DeckTreeIterator = new DeckTreeIterator(
                     {
                         cardOrder: CardOrder.DueFirstSequential,
                         deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                     },
-                    IteratorDeckSource.UpdatedByIterator,
+                    deck,
                 );
-                iterator.setDeck(deck);
+                iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                 // No due cards, so expect the new ones immediately
                 expect(iterator.nextCard()).toEqual(true);
-                expect(iterator.currentDeck.deckName).toEqual("Root");
+                expect(iterator.currentDeck.deckName).toEqual("flashcards");
                 expect(iterator.currentCard.front).toEqual("Q1");
 
                 expect(iterator.nextCard()).toEqual(true);
@@ -88,7 +82,7 @@ Q3::A3`;
 
             describe("Single topic, mixture of new and scheduled cards", () => {
                 test("Get the scheduled cards first", async () => {
-                    let text: string = `
+                    let text: string = `#flashcards
 Q1::A1
 Q2::A2 <!--SR:!2023-09-02,4,270-->
 Q3::A3
@@ -97,16 +91,16 @@ Q5::A5 <!--SR:!2023-09-02,4,270-->
 Q6::A6`;
                     let deck: Deck = await SampleItemDecks.createDeckFromText(
                         text,
-                        new TopicPath(["Root"]),
+                        TopicPath.emptyPath,
                     );
                     iterator = new DeckTreeIterator(
                         {
                             cardOrder: CardOrder.DueFirstSequential,
                             deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                         },
-                        IteratorDeckSource.UpdatedByIterator,
+                        deck,
                     );
-                    iterator.setDeck(deck);
+                    iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                     // Scheduled cards first
                     nextCardThenCheck("Q2");
@@ -140,16 +134,16 @@ Q6::A6`;
                                 `;
                     let deck: Deck = await SampleItemDecks.createDeckFromText(
                         text,
-                        new TopicPath(["Root"]),
+                        TopicPath.emptyPath,
                     );
                     iterator = new DeckTreeIterator(
                         {
                             cardOrder: CardOrder.DueFirstSequential,
                             deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                         },
-                        IteratorDeckSource.UpdatedByIterator,
+                        deck,
                     );
-                    iterator.setDeck(deck);
+                    iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                     // Due root deck's cards first
                     nextCardThenCheck("Q2");
@@ -177,25 +171,25 @@ Q6::A6`;
 
         describe("New cards before due cards", () => {
             test("Single topic, new cards only", async () => {
-                let text: string = `
+                let text: string = `#flashcards
 Q1::A1
 Q2::A2
 Q3::A3`;
                 let deck: Deck = await SampleItemDecks.createDeckFromText(
                     text,
-                    new TopicPath(["Root"]),
+                    TopicPath.emptyPath,
                 );
                 let iterator: DeckTreeIterator = new DeckTreeIterator(
                     {
                         cardOrder: CardOrder.NewFirstSequential,
                         deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                     },
-                    IteratorDeckSource.UpdatedByIterator,
+                    deck,
                 );
-                iterator.setDeck(deck);
+                iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                 expect(iterator.nextCard()).toEqual(true);
-                expect(iterator.currentDeck.deckName).toEqual("Root");
+                expect(iterator.currentDeck.deckName).toEqual("flashcards");
                 expect(iterator.currentCard.front).toEqual("Q1");
 
                 expect(iterator.nextCard()).toEqual(true);
@@ -209,7 +203,7 @@ Q3::A3`;
 
             describe("Single topic, mixture of new and scheduled cards", () => {
                 test("Get the new cards first", async () => {
-                    let text: string = `
+                    let text: string = `#flashcards
 Q1::A1
 Q2::A2 <!--SR:!2023-09-02,4,270-->
 Q3::A3
@@ -218,16 +212,16 @@ Q5::A5 <!--SR:!2023-09-02,4,270-->
 Q6::A6`;
                     let deck: Deck = await SampleItemDecks.createDeckFromText(
                         text,
-                        new TopicPath(["Root"]),
+                        TopicPath.emptyPath,
                     );
                     iterator = new DeckTreeIterator(
                         {
                             cardOrder: CardOrder.NewFirstSequential,
                             deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                         },
-                        IteratorDeckSource.UpdatedByIterator,
+                        deck,
                     );
-                    iterator.setDeck(deck);
+                    iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                     // New cards first
                     nextCardThenCheck("Q1");
@@ -244,7 +238,7 @@ Q6::A6`;
                 });
 
                 test("Get the scheduled cards first", async () => {
-                    let text: string = `
+                    let text: string = `#flashcards
 Q1::A1
 Q2::A2 <!--SR:!2023-09-02,4,270-->
 Q3::A3
@@ -253,16 +247,16 @@ Q5::A5 <!--SR:!2023-09-02,4,270-->
 Q6::A6`;
                     let deck: Deck = await SampleItemDecks.createDeckFromText(
                         text,
-                        new TopicPath(["Root"]),
+                        TopicPath.emptyPath,
                     );
                     iterator = new DeckTreeIterator(
                         {
                             cardOrder: CardOrder.DueFirstSequential,
                             deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                         },
-                        IteratorDeckSource.UpdatedByIterator,
+                        deck,
                     );
-                    iterator.setDeck(deck);
+                    iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                     // Scheduled cards first
                     nextCardThenCheck("Q2");
@@ -296,16 +290,16 @@ Q6::A6`;
                                 `;
                     let deck: Deck = await SampleItemDecks.createDeckFromText(
                         text,
-                        new TopicPath(["Root"]),
+                        TopicPath.emptyPath,
                     );
                     iterator = new DeckTreeIterator(
                         {
                             cardOrder: CardOrder.NewFirstSequential,
                             deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                         },
-                        IteratorDeckSource.UpdatedByIterator,
+                        deck,
                     );
-                    iterator.setDeck(deck);
+                    iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                     // New root deck's cards first
                     nextCardThenCheck("Q1");
@@ -333,7 +327,7 @@ Q6::A6`;
     describe("DeckOrder.PrevDeckComplete_Sequential; Random card ordering", () => {
         describe("Due cards before new cards", () => {
             test("All new cards", async () => {
-                let text: string = `
+                let text: string = `#flashcards
 Q0::A0
 Q1::A1
 Q2::A2
@@ -343,16 +337,16 @@ Q5::A5
 Q6::A6`;
                 let deck: Deck = await SampleItemDecks.createDeckFromText(
                     text,
-                    new TopicPath(["Root"]),
+                    TopicPath.emptyPath,
                 );
                 iterator = new DeckTreeIterator(
                     {
                         cardOrder: CardOrder.DueFirstRandom,
                         deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                     },
-                    IteratorDeckSource.UpdatedByIterator,
+                    deck,
                 );
-                iterator.setDeck(deck);
+                iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                 // [0, 1, 2, 3, 4, 5, 6]
                 setupNextRandomNumber({ lower: 0, upper: 6, next: 5 });
@@ -381,7 +375,7 @@ Q6::A6`;
             });
 
             test("Mixture new/scheduled", async () => {
-                let text: string = `
+                let text: string = `#flashcards
 QN0::A
 QS0::A <!--SR:!2023-09-02,4,270-->
 QN1::A
@@ -392,16 +386,16 @@ QN3::A
 QS3::Q <!--SR:!2023-09-02,4,270-->`;
                 let deck: Deck = await SampleItemDecks.createDeckFromText(
                     text,
-                    new TopicPath(["Root"]),
+                    TopicPath.emptyPath,
                 );
                 iterator = new DeckTreeIterator(
                     {
                         cardOrder: CardOrder.DueFirstRandom,
                         deckOrder: DeckOrder.PrevDeckComplete_Sequential,
                     },
-                    IteratorDeckSource.UpdatedByIterator,
+                    deck,
                 );
-                iterator.setDeck(deck);
+                iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
                 // Scheduled cards first
                 // [QN0, QN1, QN2, QN3], [QS0, QS1, QS2, QS3]
@@ -458,18 +452,15 @@ QS3::Q <!--SR:!2023-09-02,4,270-->`;
 
 #flashcards/science/chemistry Q8::A8
                         `;
-            let deck: Deck = await SampleItemDecks.createDeckFromText(
-                text,
-                new TopicPath(["Root"]),
-            );
+            let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath);
             iterator = new DeckTreeIterator(
                 {
                     cardOrder: CardOrder.NewFirstSequential,
                     deckOrder: DeckOrder.PrevDeckComplete_Random,
                 },
-                IteratorDeckSource.UpdatedByIterator,
+                deck,
             );
-            iterator.setDeck(deck);
+            iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
             // New root deck's cards first Q1/Q3, then due cards - Q2
             setupNextRandomNumber({ lower: 0, upper: 3, next: 0 });
@@ -516,18 +507,15 @@ QS3::Q <!--SR:!2023-09-02,4,270-->`;
 
 #flashcards/science/chemistry Q8::A8
                         `;
-            let deck: Deck = await SampleItemDecks.createDeckFromText(
-                text,
-                new TopicPath(["Root"]),
-            );
+            let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath);
             iterator = new DeckTreeIterator(
                 {
                     cardOrder: CardOrder.EveryCardRandomDeckAndCard,
                     deckOrder: null,
                 },
-                IteratorDeckSource.UpdatedByIterator,
+                deck,
             );
-            iterator.setDeck(deck);
+            iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
             // 8 cards to choose from (hence we expect the random number provider to be asked
             // for a random number 0... 7):
@@ -570,38 +558,129 @@ QS3::Q <!--SR:!2023-09-02,4,270-->`;
     });
 });
 
+describe("nextCard - Some cards present in multiple decks", () => {
+    describe("DeckOrder.PrevDeckComplete_Sequential; Sequential card ordering", () => {
+        test("Iterating over complete deck tree", async () => {
+            let text: string = `#flashcards
+Q1::A1
+
+#flashcards/folder1
+Q21::A21
+
+#flashcards/folder2
+Q31::A31
+
+#flashcards/folder1 #flashcards/folder2
+Q11::A11
+Q12::A12
+`;
+            const [deck, iterator] = await SampleItemDecks.createDeckAndIteratorFromText(
+                text,
+                TopicPath.emptyPath,
+                CardOrder.DueFirstSequential,
+                DeckOrder.PrevDeckComplete_Sequential,
+            );
+            iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
+
+            // Start off with cards in the top most deck, i.e. #flashcards
+            expect(iterator.nextCard()).toEqual(true);
+            expect(iterator.currentDeck.deckName).toEqual("flashcards");
+            expect(iterator.currentCard.front).toEqual("Q1");
+
+            // Now those in #flashcards/folder1
+            expect(iterator.nextCard()).toEqual(true);
+            expect(iterator.currentCard.front).toEqual("Q21"); // Specific to #flashcards/folder1
+            expect(iterator.nextCard()).toEqual(true);
+            expect(iterator.currentCard.front).toEqual("Q11"); // Common to #flashcards/folder1 & folder2
+            expect(iterator.nextCard()).toEqual(true);
+            expect(iterator.currentCard.front).toEqual("Q12"); // Common to #flashcards/folder1 & folder2
+
+            // Now those in #flashcards/folder2
+            expect(iterator.nextCard()).toEqual(true);
+            expect(iterator.currentCard.front).toEqual("Q31");
+
+            // Ones common to both folder1 and folder2 are not returned for folder2
+            // i.e. we don't see Q11 or Q12 again
+            expect(iterator.nextCard()).toEqual(false);
+        });
+
+        test("Iterating over portion of deck tree still deletes hard-linked cards in non-iterated portion of the deck", async () => {
+            let text: string = `#flashcards
+Q1::A1
+
+#flashcards/folder1
+Q21::A21
+
+#flashcards/folder2
+Q31::A31
+
+#flashcards/folder1 #flashcards/folder2
+Q11::A11
+Q12::A12
+`;
+            const [deck, iterator] = await SampleItemDecks.createDeckAndIteratorFromText(
+                text,
+                TopicPath.emptyPath,
+                CardOrder.DueFirstSequential,
+                DeckOrder.PrevDeckComplete_Sequential,
+            );
+
+            // Before iterating folder2, there are (1 + 2) cards in folder1
+            let subdeck: Deck = deck.getDeckByTopicTag("#flashcards/folder1");
+            expect(subdeck.getCardCount(CardListType.All, false)).toEqual(3);
+
+            // Iterate cards in #flashcards/folder2
+            iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards/folder2"));
+
+            expect(iterator.nextCard()).toEqual(true);
+            expect(iterator.currentCard.front).toEqual("Q31");
+            expect(iterator.nextCard()).toEqual(true);
+            expect(iterator.currentCard.front).toEqual("Q11");
+            expect(iterator.nextCard()).toEqual(true);
+            expect(iterator.currentCard.front).toEqual("Q12");
+            expect(iterator.nextCard()).toEqual(false);
+
+            // After iterating folder2, there are (1 + 0) cards in folder1
+            subdeck = deck.getDeckByTopicTag("#flashcards/folder1");
+            expect(subdeck.getCardCount(CardListType.All, false)).toEqual(1);
+        });
+    });
+});
+
 describe("hasCurrentCard", () => {
     test("false immediately after setDeck", async () => {
-        let text: string = `
+        let text: string = `#flashcards
         Q1::A1
         Q2::A2
         Q3::A3`;
-        let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
+        let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath);
         let iterator: DeckTreeIterator = new DeckTreeIterator(
             {
                 cardOrder: CardOrder.NewFirstSequential,
                 deckOrder: DeckOrder.PrevDeckComplete_Sequential,
             },
-            IteratorDeckSource.UpdatedByIterator,
+            deck,
         );
-        iterator.setDeck(deck);
+        iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
+
         expect(iterator.hasCurrentCard).toEqual(false);
     });
 
     test("true immediately after nextCard", async () => {
-        let text: string = `
+        let text: string = `#flashcards
         Q1::A1
         Q2::A2
         Q3::A3`;
-        let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
+        let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath);
         let iterator: DeckTreeIterator = new DeckTreeIterator(
             {
                 cardOrder: CardOrder.NewFirstSequential,
                 deckOrder: DeckOrder.PrevDeckComplete_Sequential,
             },
-            IteratorDeckSource.UpdatedByIterator,
+            deck,
         );
-        iterator.setDeck(deck);
+        iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
+
         expect(iterator.nextCard()).toEqual(true);
         expect(iterator.hasCurrentCard).toEqual(true);
     });
@@ -609,19 +688,19 @@ describe("hasCurrentCard", () => {
 
 describe("deleteCurrentCard", () => {
     test("Delete after all cards iterated - exception throw", async () => {
-        let text: string = `
+        let text: string = `#flashcards
 Q1::A1
 Q2::A2
 Q3::A3`;
-        let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
+        let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath);
         iterator = new DeckTreeIterator(
             {
                 cardOrder: CardOrder.NewFirstSequential,
                 deckOrder: DeckOrder.PrevDeckComplete_Sequential,
             },
-            IteratorDeckSource.UpdatedByIterator,
+            deck,
         );
-        iterator.setDeck(deck);
+        iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
         expect(iterator.nextCard()).toEqual(true);
         expect(iterator.nextCard()).toEqual(true);
@@ -629,32 +708,33 @@ Q3::A3`;
         expect(iterator.nextCard()).toEqual(false);
 
         const t = () => {
-            iterator.deleteCurrentCard();
+            iterator.deleteCurrentCardFromAllDecks();
         };
         expect(t).toThrow();
     });
 
     test("Delete card, with single card remaining after it", async () => {
-        let text: string = `
+        let text: string = `#flashcards
 Q1::A1
 Q2::A2
 Q3::A3`;
-        let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
-        expect(deck.newFlashcards.length).toEqual(3);
+        let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath);
+        const flashcardDeck: Deck = deck.getDeckByTopicTag("#flashcards");
+        expect(flashcardDeck.newFlashcards.length).toEqual(3);
         iterator = new DeckTreeIterator(
             {
                 cardOrder: CardOrder.NewFirstSequential,
                 deckOrder: DeckOrder.PrevDeckComplete_Sequential,
             },
-            IteratorDeckSource.UpdatedByIterator,
+            deck,
         );
-        iterator.setDeck(deck);
+        iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards"));
 
         nextCardThenCheck("Q1");
         nextCardThenCheck("Q2");
-        expect(iterator.deleteCurrentCard()).toEqual(true);
+        expect(iterator.deleteCurrentCardFromAllDecks()).toEqual(true);
         expect(iterator.currentCard.front).toEqual("Q3");
-        expect(iterator.deleteCurrentCard()).toEqual(false);
+        expect(iterator.deleteCurrentCardFromAllDecks()).toEqual(false);
     });
 });
 
diff --git a/tests/unit/FlashcardReviewSequencer.test.ts b/tests/unit/FlashcardReviewSequencer.test.ts
index 9207c8b..7d67a93 100644
--- a/tests/unit/FlashcardReviewSequencer.test.ts
+++ b/tests/unit/FlashcardReviewSequencer.test.ts
@@ -5,9 +5,9 @@ import {
     DeckTreeIterator,
     IDeckTreeIterator,
     IIteratorOrder,
-    IteratorDeckSource,
 } from "src/DeckTreeIterator";
 import {
+    DeckStats,
     FlashcardReviewMode,
     FlashcardReviewSequencer,
     IFlashcardReviewSequencer,
@@ -16,7 +16,6 @@ import { TopicPath } from "src/TopicPath";
 import { CardListType, Deck, DeckTreeFilter } from "src/Deck";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
 import { SampleItemDecks } from "./SampleItems";
-import { UnitTestSRFile } from "src/SRFile";
 import { ReviewResponse } from "src/scheduling";
 import {
     setupStaticDateProvider,
@@ -26,6 +25,7 @@ import {
 import moment from "moment";
 import { INoteEaseList, NoteEaseList } from "src/NoteEaseList";
 import { QuestionPostponementList, IQuestionPostponementList } from "src/QuestionPostponementList";
+import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
 
 let order_DueFirst_Sequential: IIteratorOrder = {
     cardOrder: CardOrder.DueFirstSequential,
@@ -44,7 +44,7 @@ class TestContext {
     cardSequencer: IDeckTreeIterator;
     noteEaseList: INoteEaseList;
     cardScheduleCalculator: CardScheduleCalculator;
-    reviewSequencer: IFlashcardReviewSequencer;
+    reviewSequencer: FlashcardReviewSequencer;
     questionPostponementList: QuestionPostponementList;
     file: UnitTestSRFile;
     originalText: string;
@@ -57,11 +57,8 @@ class TestContext {
     async resetContext(text: string, daysAfterOrigin: number): Promise<void> {
         this.originalText = text;
         this.file.content = text;
-        let cardSequencer: IDeckTreeIterator = new DeckTreeIterator(
-            this.iteratorOrder,
-            IteratorDeckSource.UpdatedByIterator,
-        );
-        let reviewSequencer: IFlashcardReviewSequencer = new FlashcardReviewSequencer(
+        let cardSequencer: IDeckTreeIterator = new DeckTreeIterator(this.iteratorOrder, null);
+        let reviewSequencer: FlashcardReviewSequencer = new FlashcardReviewSequencer(
             this.reviewMode,
             cardSequencer,
             this.settings,
@@ -80,7 +77,7 @@ class TestContext {
     }
 
     async setSequencerDeckTreeFromOriginalText(): Promise<Deck> {
-        const deckTree: Deck = await SampleItemDecks.createDeckFromFile(
+        let deckTree: Deck = await SampleItemDecks.createDeckFromFile(
             this.file,
             new TopicPath(["Root"]),
         );
@@ -93,6 +90,10 @@ class TestContext {
         return deckTree;
     }
 
+    getDeckStats(topicTag: string): DeckStats {
+        return this.reviewSequencer.getDeckStats(TopicPath.getTopicPathFromTag(topicTag));
+    }
+
     static Create(
         iteratorOrder: IIteratorOrder,
         reviewMode: FlashcardReviewMode,
@@ -100,10 +101,7 @@ class TestContext {
         text: string,
         fakeFilePath?: string,
     ): TestContext {
-        let cardSequencer: IDeckTreeIterator = new DeckTreeIterator(
-            iteratorOrder,
-            IteratorDeckSource.UpdatedByIterator,
-        );
+        let cardSequencer: IDeckTreeIterator = new DeckTreeIterator(iteratorOrder, null);
         let noteEaseList = new NoteEaseList(settings);
         let cardScheduleCalculator: CardScheduleCalculator = new CardScheduleCalculator(
             settings,
@@ -114,7 +112,7 @@ class TestContext {
             settings,
             [],
         );
-        let reviewSequencer: IFlashcardReviewSequencer = new FlashcardReviewSequencer(
+        let reviewSequencer: FlashcardReviewSequencer = new FlashcardReviewSequencer(
             reviewMode,
             cardSequencer,
             settings,
@@ -316,6 +314,7 @@ describe("setDeckTree", () => {
             DEFAULT_SETTINGS,
             "",
         );
+        c.setSequencerDeckTreeFromOriginalText();
         c.reviewSequencer.setDeckTree(Deck.emptyDeck, Deck.emptyDeck);
         expect(c.reviewSequencer.currentDeck).toEqual(null);
         expect(c.reviewSequencer.currentCard).toEqual(null);
@@ -323,7 +322,7 @@ describe("setDeckTree", () => {
 
     // After setDeckTree, the first card in the deck is the current card
     test("Single level deck with some new cards", async () => {
-        let text: string = `
+        let text: string = `#flashcards
 Q1::A1
 Q2::A2
 Q3::A3`;
@@ -334,7 +333,8 @@ Q3::A3`;
             text,
         );
         let deck: Deck = await c.setSequencerDeckTreeFromOriginalText();
-        expect(deck.newFlashcards.length).toEqual(3);
+        const flashcardDeck: Deck = deck.getDeckByTopicTag("#flashcards");
+        expect(flashcardDeck.newFlashcards.length).toEqual(3);
 
         expect(c.reviewSequencer.currentDeck.newFlashcards.length).toEqual(3);
         let expected = {
@@ -505,7 +505,8 @@ describe("processReview", () => {
                 let settings: SRSettings = { ...DEFAULT_SETTINGS };
                 settings.burySiblingCards = false;
 
-                let text: string = `
+                let text: string = `#flashcards
+
 #flashcards This single ==question== turns into ==3 separate== ==cards==
 
 Q1::A1
@@ -547,6 +548,7 @@ Q1::A1
                 let text: string = `
 #flashcards ${clozeQuestion1}
 
+#flashcards
 Q1::A1
     `;
 
@@ -580,6 +582,7 @@ Q1::A1
                 let text: string = `
 #flashcards ${clozeQuestion1}
 
+#flashcards
 Q1::A1
     `;
 
@@ -698,7 +701,7 @@ $$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`;
             let indent: string = "    ";
 
             // Note that "- bar?::baz" is intentionally indented
-            let text: string = `
+            let text: string = `#flashcards
 - foo
 ${indent}- bar?::baz
 `;
@@ -1020,6 +1023,85 @@ ${updatedQuestionText}`;
     });
 });
 
+describe("getDeckStats", () => {
+    describe("Single level deck with some new and due cards", () => {
+        test("Initial stats", async () => {
+            let text: string = `#flashcards
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4 <!--SR:!2023-01-21,15,290-->
+`;
+            let c: TestContext = TestContext.Create(
+                order_DueFirst_Sequential,
+                FlashcardReviewMode.Review,
+                DEFAULT_SETTINGS,
+                text,
+            );
+            let deck: Deck = await c.setSequencerDeckTreeFromOriginalText();
+            expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(1, 3, 4));
+        });
+
+        test("Reduction in due count after skipping card", async () => {
+            let text: string = `#flashcards
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4 <!--SR:!2023-01-21,15,290-->
+`;
+            let c: TestContext = TestContext.Create(
+                order_DueFirst_Sequential,
+                FlashcardReviewMode.Review,
+                DEFAULT_SETTINGS,
+                text,
+            );
+            let deck: Deck = await c.setSequencerDeckTreeFromOriginalText();
+
+            expect(c.reviewSequencer.currentCard.front).toEqual("Q4"); // This is the first card as we are using order_DueFirst_Sequential
+            expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(1, 3, 4));
+            c.reviewSequencer.skipCurrentCard();
+            // One less due card
+            expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(0, 3, 4));
+        });
+
+        test("Change in stats after reviewing each card", async () => {
+            let text: string = `#flashcards
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4 <!--SR:!2023-01-21,15,290-->
+`;
+            let c: TestContext = TestContext.Create(
+                order_DueFirst_Sequential,
+                FlashcardReviewMode.Review,
+                DEFAULT_SETTINGS,
+                text,
+            );
+            let deck: Deck = await c.setSequencerDeckTreeFromOriginalText();
+
+            await checkStats(c, "#flashcards", [
+                [new DeckStats(1, 3, 4), "Q4", ReviewResponse.Easy], // This is the first card as we are using order_DueFirst_Sequential
+                [new DeckStats(0, 3, 4), "Q1", ReviewResponse.Easy], // Iterated through all the due cards, now the new ones
+                [new DeckStats(0, 2, 4), "Q2", ReviewResponse.Easy],
+            ]);
+        });
+    });
+});
+
+async function checkStats(
+    c: TestContext,
+    topicPath: string,
+    expectedStats: [DeckStats, string, ReviewResponse][],
+): Promise<void> {
+    for (const item of expectedStats) {
+        const [expectedDeckStats, expectedCardFront, reviewResponse] = item;
+        expect(c.getDeckStats(topicPath)).toEqual(expectedDeckStats);
+        if (expectedCardFront)
+            expect(c.reviewSequencer.currentCard.front).toEqual(expectedCardFront);
+        if (reviewResponse != null) await c.reviewSequencer.processReview(reviewResponse);
+    }
+}
+
 describe("Sequences", () => {
     test("Update question text, followed by review response", async () => {
         let text1: string = `
diff --git a/tests/unit/Note.test.ts b/tests/unit/Note.test.ts
index 96b5902..c38af8b 100644
--- a/tests/unit/Note.test.ts
+++ b/tests/unit/Note.test.ts
@@ -1,11 +1,11 @@
 import { NoteParser } from "src/NoteParser";
-import { UnitTestSRFile } from "src/SRFile";
 import { TopicPath } from "src/TopicPath";
 import { Deck } from "src/Deck";
 import { Note } from "src/Note";
 import { Question } from "src/Question";
 import { DEFAULT_SETTINGS } from "src/settings";
 import { NoteFileLoader } from "src/NoteFileLoader";
+import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
 
 let parser: NoteParser = new NoteParser(DEFAULT_SETTINGS);
 var noteFileLoader: NoteFileLoader = new NoteFileLoader(DEFAULT_SETTINGS);
diff --git a/tests/unit/NoteFileLoader.test.ts b/tests/unit/NoteFileLoader.test.ts
index ab07ae5..ce2c2fb 100644
--- a/tests/unit/NoteFileLoader.test.ts
+++ b/tests/unit/NoteFileLoader.test.ts
@@ -1,8 +1,8 @@
 import { Note } from "src/Note";
 import { NoteFileLoader } from "src/NoteFileLoader";
-import { UnitTestSRFile } from "src/SRFile";
 import { TopicPath } from "src/TopicPath";
 import { DEFAULT_SETTINGS } from "src/settings";
+import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
 
 var noteFileLoader: NoteFileLoader = new NoteFileLoader(DEFAULT_SETTINGS);
 
diff --git a/tests/unit/NoteParser.test.ts b/tests/unit/NoteParser.test.ts
index fb57ed0..a776849 100644
--- a/tests/unit/NoteParser.test.ts
+++ b/tests/unit/NoteParser.test.ts
@@ -1,10 +1,10 @@
 import { NoteParser } from "src/NoteParser";
-import { UnitTestSRFile } from "src/SRFile";
 import { TopicPath } from "src/TopicPath";
 import { Note } from "src/Note";
 import { Question } from "src/Question";
 import { DEFAULT_SETTINGS } from "src/settings";
 import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
+import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
 
 let parser: NoteParser = new NoteParser(DEFAULT_SETTINGS);
 
diff --git a/tests/unit/NoteQuestionParser.test.ts b/tests/unit/NoteQuestionParser.test.ts
index f012601..ca00c23 100644
--- a/tests/unit/NoteQuestionParser.test.ts
+++ b/tests/unit/NoteQuestionParser.test.ts
@@ -3,10 +3,11 @@ import { CardScheduleInfo } from "src/CardSchedule";
 import { TICKS_PER_DAY } from "src/constants";
 import { CardType, Question } from "src/Question";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
-import { TopicPath } from "src/TopicPath";
+import { TopicPath, TopicPathList } from "src/TopicPath";
 import { createTest_NoteQuestionParser } from "./SampleItems";
-import { ISRFile, UnitTestSRFile } from "src/SRFile";
+import { ISRFile } from "src/SRFile";
 import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
+import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
 
 let parserWithDefaultSettings: NoteQuestionParser = createTest_NoteQuestionParser(DEFAULT_SETTINGS);
 let settings_ConvertFoldersToDecks: SRSettings = { ...DEFAULT_SETTINGS };
@@ -19,19 +20,31 @@ beforeAll(() => {
     setupStaticDateProvider_20230906();
 });
 
-test("No questions in the text", async () => {
-    let noteText: string = "An interesting note, but no questions";
-    let folderTopicPath: TopicPath = TopicPath.emptyPath;
-    let noteFile: ISRFile = new UnitTestSRFile(noteText);
+describe("No flashcard questions", () => {
+    test("No questions in the text", async () => {
+        let noteText: string = "An interesting note, but no questions";
+        let folderTopicPath: TopicPath = TopicPath.emptyPath;
+        let noteFile: ISRFile = new UnitTestSRFile(noteText);
+
+        expect(
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+        ).toEqual([]);
+    });
+
+    test("A question in the text, but no flashcard tag", async () => {
+        let noteText: string = "A::B";
+        let folderTopicPath: TopicPath = TopicPath.emptyPath;
+        let noteFile: ISRFile = new UnitTestSRFile(noteText);
 
-    expect(await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath)).toEqual(
-        [],
-    );
+        expect(
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+        ).toEqual([]);
+    });
 });
 
 describe("Single question in the text (without block identifier)", () => {
     test("SingleLineBasic: No schedule info", async () => {
-        let noteText: string = `
+        let noteText: string = `#flashcards
 A::B
 `;
         let noteFile: ISRFile = new UnitTestSRFile(noteText);
@@ -44,20 +57,19 @@ A::B
         let expected = [
             {
                 questionType: CardType.SingleLineBasic,
-                topicPath: TopicPath.emptyPath,
+                topicPathList: TopicPathList.fromPsv("#flashcards", 0),
                 questionText: {
                     original: `A::B`,
                     actualQuestion: "A::B",
                 },
 
-                lineNo: 1,
                 hasEditLaterTag: false,
                 cards: [card1],
                 hasChanged: false,
             },
         ];
         expect(
-            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath),
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
         ).toMatchObject(expected);
     });
 
@@ -82,7 +94,7 @@ A::B
         let expected = [
             {
                 questionType: CardType.SingleLineBasic,
-                topicPath: new TopicPath(["flashcards", "test"]),
+                topicPathList: TopicPathList.fromPsv("#flashcards/test", 0),
                 questionText: {
                     original: `A::B
 <!--SR:!2023-09-03,1,230-->`,
@@ -96,14 +108,14 @@ A::B
             },
         ];
         expect(
-            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath),
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
         ).toMatchObject(expected);
     });
 });
 
 describe("Single question in the text (with block identifier)", () => {
     test("SingleLineBasic: No schedule info", async () => {
-        let noteText: string = `
+        let noteText: string = `#flashcards
 A::B ^d7cee0
 `;
         let noteFile: ISRFile = new UnitTestSRFile(noteText);
@@ -115,22 +127,27 @@ A::B ^d7cee0
         };
         let expected = [
             {
-                questionType: CardType.SingleLineBasic,
-                topicPath: TopicPath.emptyPath,
+                topicPathList: {
+                    list: [TopicPath.getTopicPathFromTag("#flashcards")],
+                    lineNum: 0,
+                },
+                parsedQuestionInfo: {
+                    cardType: CardType.SingleLineBasic,
+                    firstLineNum: 1,
+                },
                 questionText: {
                     original: `A::B ^d7cee0`,
                     actualQuestion: "A::B",
                     obsidianBlockId: "^d7cee0",
                 },
 
-                lineNo: 1,
                 hasEditLaterTag: false,
                 cards: [card1],
                 hasChanged: false,
             },
         ];
         expect(
-            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath),
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
         ).toMatchObject(expected);
     });
 
@@ -154,8 +171,14 @@ A::B ^d7cee0
         };
         let expected = [
             {
-                questionType: CardType.SingleLineBasic,
-                topicPath: new TopicPath(["flashcards", "test"]),
+                topicPathList: {
+                    list: [TopicPath.getTopicPathFromTag("#flashcards/test")],
+                    lineNum: 0,
+                },
+                parsedQuestionInfo: {
+                    cardType: CardType.SingleLineBasic,
+                    firstLineNum: 1,
+                },
                 questionText: {
                     original: `A::B ^d7cee0
 <!--SR:!2023-09-03,1,230-->`,
@@ -163,14 +186,13 @@ A::B ^d7cee0
                     textHash: "1c6b0b01215dc4",
                     obsidianBlockId: "^d7cee0",
                 },
-                lineNo: 1,
                 hasEditLaterTag: false,
                 cards: [card1],
                 hasChanged: false,
             },
         ];
         expect(
-            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath),
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
         ).toMatchObject(expected);
     });
 
@@ -193,22 +215,27 @@ A::B <!--SR:!2023-09-03,1,230--> ^d7cee0
         };
         let expected = [
             {
-                questionType: CardType.SingleLineBasic,
-                topicPath: new TopicPath(["flashcards", "test"]),
+                topicPathList: {
+                    list: [TopicPath.getTopicPathFromTag("#flashcards/test")],
+                    lineNum: 0, // Line numbers start at zero
+                },
+                parsedQuestionInfo: {
+                    cardType: CardType.SingleLineBasic,
+                    firstLineNum: 1,
+                },
                 questionText: {
                     original: `A::B <!--SR:!2023-09-03,1,230--> ^d7cee0`,
                     actualQuestion: "A::B",
                     textHash: "1c6b0b01215dc4",
                     obsidianBlockId: "^d7cee0",
                 },
-                lineNo: 1,
                 hasEditLaterTag: false,
                 cards: [card1],
                 hasChanged: false,
             },
         ];
         expect(
-            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath),
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
         ).toMatchObject(expected);
     });
 
@@ -231,21 +258,26 @@ A::B <!--SR:!2023-09-03,1,230--> ^d7cee0
         };
         let expected = [
             {
-                questionType: CardType.SingleLineBasic,
-                topicPath: new TopicPath(["flashcards", "test"]),
+                topicPathList: {
+                    list: [TopicPath.getTopicPathFromTag("#flashcards/test")],
+                    lineNum: 1,
+                },
+                parsedQuestionInfo: {
+                    cardType: CardType.SingleLineBasic,
+                    firstLineNum: 1,
+                },
                 questionText: {
                     original: `#flashcards/test A::B <!--SR:!2023-09-03,1,230--> ^d7cee0`,
                     actualQuestion: "A::B",
                     obsidianBlockId: "^d7cee0",
                 },
-                lineNo: 1,
                 hasEditLaterTag: false,
                 cards: [card1],
                 hasChanged: false,
             },
         ];
         expect(
-            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath),
+            await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
         ).toMatchObject(expected);
     });
 });
@@ -261,6 +293,7 @@ Q2::A2
         let questionList: Question[] = await parser_ConvertFoldersToDecks.createQuestionList(
             noteFile,
             folderTopicPath,
+            true,
         );
         expect(questionList.length).toEqual(2);
     });
@@ -277,11 +310,38 @@ Q3::A3
         let questionList: Question[] = await parser_ConvertFoldersToDecks.createQuestionList(
             noteFile,
             folderTopicPath,
+            true,
         );
         expect(questionList.length).toEqual(3);
-        expect(questionList[0].topicPath).toEqual(new TopicPath(["flashcards", "science"]));
-        expect(questionList[1].topicPath).toEqual(new TopicPath(["flashcards", "science"]));
-        expect(questionList[2].topicPath).toEqual(new TopicPath(["flashcards", "science"]));
+        expect(questionList[0].topicPathList.formatPsv()).toEqual("#flashcards/science");
+        expect(questionList[1].topicPathList.formatPsv()).toEqual("#flashcards/science");
+        expect(questionList[2].topicPathList.formatPsv()).toEqual("#flashcards/science");
+    });
+
+    test("SingleLineBasic: Tags within frontmatter applies to all questions when not overriden", async () => {
+        let noteText: string = `---
+sr-due: 2024-01-17
+sr-interval: 16
+sr-ease: 278
+tags:
+  - flashcards/aws
+---
+Q1::A1
+Q2::A2
+Q3::A3
+`;
+        let noteFile: ISRFile = new UnitTestSRFile(noteText);
+
+        let folderTopicPath: TopicPath = TopicPath.emptyPath;
+        let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
+            noteFile,
+            folderTopicPath,
+            true,
+        );
+        expect(questionList.length).toEqual(3);
+        expect(questionList[0].topicPathList.formatPsv()).toEqual("#flashcards/aws");
+        expect(questionList[1].topicPathList.formatPsv()).toEqual("#flashcards/aws");
+        expect(questionList[2].topicPathList.formatPsv()).toEqual("#flashcards/aws");
     });
 
     test("MultiLine: Space before multi line separator", async () => {
@@ -305,6 +365,7 @@ Multiline answer2
         let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
             noteFile,
             TopicPath.emptyPath,
+            true,
         );
         expect(questionList.length).toEqual(3);
         expect(questionList[0].cards).toMatchObject([
@@ -350,10 +411,11 @@ describe("Handling tags within note", () => {
             let questionList: Question[] = await parser2.createQuestionList(
                 noteFile,
                 folderTopicPath,
+                true,
             );
             expect(questionList.length).toEqual(3);
             for (let i = 0; i < questionList.length; i++)
-                expect(questionList[i].topicPath).toEqual(new TopicPath(["folder", "subfolder"]));
+                expect(questionList[i].topicPathList.formatPsv()).toEqual("#folder/subfolder");
         });
 
         test("Topic tag within note is ignored (outside all questions)", async () => {
@@ -366,9 +428,10 @@ Q1::A1
             let questionList: Question[] = await parser2.createQuestionList(
                 noteFile,
                 folderTopicPath,
+                true,
             );
             expect(questionList.length).toEqual(1);
-            expect(questionList[0].topicPath).toEqual(new TopicPath(["folder", "subfolder"]));
+            expect(questionList[0].topicPathList.formatPsv()).toEqual("#folder/subfolder");
         });
 
         // Behavior here mimics SR_ORIGINAL
@@ -384,9 +447,10 @@ Q1::A1
             let questionList: Question[] = await parser2.createQuestionList(
                 noteFile,
                 folderTopicPath,
+                true,
             );
             expect(questionList.length).toEqual(1);
-            expect(questionList[0].topicPath).toEqual(new TopicPath(["folder", "subfolder"]));
+            expect(questionList[0].topicPathList.formatPsv()).toEqual("#folder/subfolder");
         });
     });
 
@@ -401,16 +465,17 @@ Q1::A1
     `;
             let noteFile: ISRFile = new UnitTestSRFile(noteText);
 
-            let expectedPath: TopicPath = new TopicPath(["flashcards", "test"]);
+            let expectedPath: string = "#flashcards/test";
             let folderTopicPath: TopicPath = TopicPath.emptyPath;
             let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
                 noteFile,
                 folderTopicPath,
+                true,
             );
             expect(questionList.length).toEqual(3);
-            expect(questionList[0].topicPath).toEqual(expectedPath);
-            expect(questionList[1].topicPath).toEqual(expectedPath);
-            expect(questionList[2].topicPath).toEqual(expectedPath);
+            expect(questionList[0].topicPathList.formatPsv()).toEqual(expectedPath);
+            expect(questionList[1].topicPathList.formatPsv()).toEqual(expectedPath);
+            expect(questionList[2].topicPathList.formatPsv()).toEqual(expectedPath);
         });
 
         test("Topic tag within question overrides the note topic, for that topic only", async () => {
@@ -425,11 +490,12 @@ Q1::A1
             let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
                 noteFile,
                 folderTopicPath,
+                true,
             );
             expect(questionList.length).toEqual(3);
-            expect(questionList[0].topicPath).toEqual(new TopicPath(["flashcards", "test"]));
-            expect(questionList[1].topicPath).toEqual(new TopicPath(["flashcards", "examination"]));
-            expect(questionList[2].topicPath).toEqual(new TopicPath(["flashcards", "test"]));
+            expect(questionList[0].topicPathList.formatPsv()).toEqual("#flashcards/test");
+            expect(questionList[1].topicPathList.formatPsv()).toEqual("#flashcards/examination");
+            expect(questionList[2].topicPathList.formatPsv()).toEqual("#flashcards/test");
         });
 
         test("First topic tag within note (outside questions) is used as the note's topic tag, even if it appears after the first question", async () => {
@@ -446,31 +512,33 @@ Q1::A1
             let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
                 noteFile,
                 folderTopicPath,
+                true,
             );
             expect(questionList.length).toEqual(3);
             for (let i = 0; i < questionList.length; i++)
-                expect(questionList[i].topicPath).toEqual(expectedPath);
+                expect(questionList[i].topicPathList.formatPsv()).toEqual("#flashcards/test");
         });
 
-        test("Only first topic tag within note (outside questions) is used as the note's topic tag, subsequent ignored", async () => {
+        test("The last topic tag within note prior to the question is used as the note's topic tag", async () => {
             let noteText: string = `
     Q1::A1
     #flashcards/test
     Q2::A2
     #flashcards/examination
-    Q3::This has the "flashcards/test" topic, not "flashcards/examination"
+    Q3::This has the "flashcards/examination" topic, not "flashcards/test"
     `;
             let noteFile: ISRFile = new UnitTestSRFile(noteText);
 
-            let expectedPath: TopicPath = new TopicPath(["flashcards", "test"]);
             let folderTopicPath: TopicPath = TopicPath.emptyPath;
             let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
                 noteFile,
                 folderTopicPath,
+                true,
             );
             expect(questionList.length).toEqual(3);
-            for (let i = 0; i < questionList.length; i++)
-                expect(questionList[i].topicPath).toEqual(expectedPath);
+            expect(questionList[0].topicPathList.formatPsv()).toEqual("#flashcards/test");
+            expect(questionList[1].topicPathList.formatPsv()).toEqual("#flashcards/test");
+            expect(questionList[2].topicPathList.formatPsv()).toEqual("#flashcards/examination");
         });
     });
 
@@ -488,9 +556,10 @@ Q1::A1
             let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
                 noteFile,
                 folderTopicPath,
+                true,
             );
             expect(questionList.length).toEqual(1);
-            expect(questionList[0].topicPath).toEqual(expectedPath);
+            expect(questionList[0].topicPathList.formatPsv()).toEqual("#flashcards/science");
             expect(questionList[0].cards.length).toEqual(1);
             expect(questionList[0].cards[0].front).toEqual("Q5");
         });
diff --git a/tests/unit/SampleItems.ts b/tests/unit/SampleItems.ts
index 8c5434f..da071b5 100644
--- a/tests/unit/SampleItems.ts
+++ b/tests/unit/SampleItems.ts
@@ -6,8 +6,9 @@ import { NoteQuestionParser } from "src/NoteQuestionParser";
 import { CardType, Question } from "src/Question";
 import { CardFrontBack, CardFrontBackUtil } from "src/QuestionType";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
-import { UnitTestSRFile } from "src/SRFile";
 import { TopicPath } from "src/TopicPath";
+import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
+import { CardOrder, DeckOrder, DeckTreeIterator } from "src/DeckTreeIterator";
 
 export function createTest_NoteQuestionParser(settings: SRSettings): NoteQuestionParser {
     let questionParser: NoteQuestionParser = new NoteQuestionParser(settings);
@@ -38,17 +39,36 @@ Q3::A3`;
         return deck;
     }
 
-    static async createDeckFromText(text: string, folderTopicPath: TopicPath): Promise<Deck> {
+    static async createDeckFromText(
+        text: string,
+        folderTopicPath: TopicPath = TopicPath.emptyPath,
+    ): Promise<Deck> {
         let file: UnitTestSRFile = new UnitTestSRFile(text);
         return await this.createDeckFromFile(file, folderTopicPath);
     }
 
+    static async createDeckAndIteratorFromText(
+        text: string,
+        folderTopicPath: TopicPath,
+        cardOrder: CardOrder,
+        deckOrder: DeckOrder,
+    ): Promise<[Deck, DeckTreeIterator]> {
+        let deck: Deck = await SampleItemDecks.createDeckFromText(text, folderTopicPath);
+        let iterator: DeckTreeIterator = new DeckTreeIterator(
+            {
+                cardOrder,
+                deckOrder,
+            },
+            deck,
+        );
+        return [deck, iterator];
+    }
+
     static async createDeckFromFile(
         file: UnitTestSRFile,
-        folderTopicPath: TopicPath,
+        folderTopicPath: TopicPath = TopicPath.emptyPath,
     ): Promise<Deck> {
         let deck: Deck = new Deck("Root", null);
-        let topicPath: TopicPath = TopicPath.emptyPath;
         let noteParser: NoteParser = createTest_NoteParser();
         let note: Note = await noteParser.parse(file, folderTopicPath);
         note.appendCardsToDeck(deck);
diff --git a/tests/unit/TopicPath.test.ts b/tests/unit/TopicPath.test.ts
index e6bb2ab..af17047 100644
--- a/tests/unit/TopicPath.test.ts
+++ b/tests/unit/TopicPath.test.ts
@@ -1,6 +1,7 @@
-import { ISRFile, UnitTestSRFile } from "src/SRFile";
-import { TopicPath } from "src/TopicPath";
+import { ISRFile } from "src/SRFile";
+import { TopicPath, TopicPathList } from "src/TopicPath";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
+import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
 
 describe("Constructor exception handling", () => {
     test("Constructor rejects null path", () => {
@@ -238,54 +239,3 @@ describe("isValidTag", () => {
         expect(TopicPath.isValidTag("#flashcards")).toEqual(true);
     });
 });
-
-describe("getTopicPathOfFile", () => {
-    describe("convertFoldersToDecks: false", () => {
-        test("Mixture of irrelevant tags and relevant ones", () => {
-            let content: string = `
-            #ignored Q1::A1
-            #ignored Q2::A2 <!--SR:!2023-09-02,4,270-->
-            #also-Ignored Q3::A3
-            #flashcards/science Q4::A4 <!--SR:!2023-09-02,4,270-->
-            #flashcards/science/physics Q5::A5 <!--SR:!2023-09-02,4,270-->
-            #flashcards/math Q6::A6`;
-            let file: ISRFile = new UnitTestSRFile(content);
-            let expected = ["flashcards", "science"];
-
-            expect(TopicPath.getTopicPathOfFile(file, DEFAULT_SETTINGS).path).toEqual(expected);
-        });
-
-        test("No relevant tags", () => {
-            let content: string = `
-            #ignored Q1::A1
-            #ignored Q2::A2 <!--SR:!2023-09-02,4,270-->
-            #also-Ignored Q3::A3
-            Q4::A4 <!--SR:!2023-09-02,4,270-->
-            #ignored/science/physics Q5::A5 <!--SR:!2023-09-02,4,270-->
-            Q6::A6`;
-            let file: ISRFile = new UnitTestSRFile(content);
-
-            expect(TopicPath.getTopicPathOfFile(file, DEFAULT_SETTINGS).isEmptyPath).toEqual(true);
-        });
-    });
-
-    describe("convertFoldersToDecks: true", () => {
-        let settings_ConvertFoldersToDecks: SRSettings = { ...DEFAULT_SETTINGS };
-        settings_ConvertFoldersToDecks.convertFoldersToDecks = true;
-        test("Mixture of irrelevant tags and relevant ones", () => {
-            let ignoredContent: string = `
-            #ignored Q1::A1
-            #ignored Q2::A2 <!--SR:!2023-09-02,4,270-->
-            #also-Ignored Q3::A3
-            #flashcards/science Q4::A4 <!--SR:!2023-09-02,4,270-->
-            #flashcards/science/physics Q5::A5 <!--SR:!2023-09-02,4,270-->
-            #flashcards/math Q6::A6`;
-
-            let fakeFilePath: string = "history/modern/Greek.md";
-            let file: ISRFile = new UnitTestSRFile(ignoredContent, fakeFilePath);
-            let expected = ["history", "modern"];
-            let actual = TopicPath.getTopicPathOfFile(file, settings_ConvertFoldersToDecks);
-            expect(actual.path).toEqual(expected);
-        });
-    });
-});
diff --git a/tests/unit/deck.test.ts b/tests/unit/deck.test.ts
index 1fe0960..25e305c 100644
--- a/tests/unit/deck.test.ts
+++ b/tests/unit/deck.test.ts
@@ -93,6 +93,86 @@ describe("getOrCreateDeck()", () => {
     });
 });
 
+describe("getDistinctCardCount()", () => {
+    test("Single deck", async () => {
+        let text: string = `#flashcards
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4 <!--SR:!2023-09-02,4,270-->`;
+        const deck: Deck = await SampleItemDecks.createDeckFromText(text);
+        const flashcardDeck: Deck = deck.getDeckByTopicTag("#flashcards");
+        expect(flashcardDeck.getDistinctCardCount(CardListType.NewCard, false)).toEqual(3);
+        expect(flashcardDeck.getDistinctCardCount(CardListType.DueCard, false)).toEqual(1);
+        expect(flashcardDeck.getDistinctCardCount(CardListType.All, false)).toEqual(4);
+    });
+
+    test("Deck hierarchy - no duplicate cards", async () => {
+        let text: string = `#flashcards
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4 <!--SR:!2023-09-02,4,270-->
+
+#flashcards/folder1
+Q11::A11
+Q12::A12
+Q13::A13
+Q14::A14 <!--SR:!2023-09-02,4,270
+Q15::A15 <!--SR:!2023-09-02,4,270
+
+
+`;
+        const deck: Deck = await SampleItemDecks.createDeckFromText(text);
+        const flashcardDeck: Deck = deck.getDeckByTopicTag("#flashcards");
+        // Just #flashcards, no subdeck
+        expect(flashcardDeck.getDistinctCardCount(CardListType.NewCard, false)).toEqual(3);
+        expect(flashcardDeck.getDistinctCardCount(CardListType.DueCard, false)).toEqual(1);
+        expect(flashcardDeck.getDistinctCardCount(CardListType.All, false)).toEqual(4);
+
+        // #flashcards, and subdeck
+        expect(flashcardDeck.getDistinctCardCount(CardListType.NewCard, true)).toEqual(6);
+        expect(flashcardDeck.getDistinctCardCount(CardListType.DueCard, true)).toEqual(3);
+        expect(flashcardDeck.getDistinctCardCount(CardListType.All, true)).toEqual(9);
+    });
+
+    test("Deck hierarchy - with duplicate cards", async () => {
+        let text: string = `#flashcards
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4 <!--SR:!2023-09-02,4,270-->
+
+#flashcards/folder1 #flashcards/folder2
+Q11::A11
+Q12::A12
+Q13::A13
+Q14::A14 <!--SR:!2023-09-02,4,270
+Q15::A15 <!--SR:!2023-09-02,4,270
+
+#flashcards/folder2
+Q21::A21
+
+`;
+        const deck: Deck = await SampleItemDecks.createDeckFromText(text);
+
+        // #flashcards/folder1
+        let subdeck: Deck = deck.getDeckByTopicTag("#flashcards/folder1");
+        expect(subdeck.getDistinctCardCount(CardListType.NewCard, true)).toEqual(3);
+        expect(subdeck.getDistinctCardCount(CardListType.DueCard, true)).toEqual(2);
+
+        // #flashcards/folder2
+        subdeck = deck.getDeckByTopicTag("#flashcards/folder2");
+        expect(subdeck.getDistinctCardCount(CardListType.NewCard, true)).toEqual(4);
+        expect(subdeck.getDistinctCardCount(CardListType.DueCard, true)).toEqual(2);
+
+        // #flashcards and subdecks
+        subdeck = deck.getDeckByTopicTag("#flashcards");
+        expect(subdeck.getDistinctCardCount(CardListType.NewCard, true)).toEqual(3 + 3 + 1);
+        expect(subdeck.getDistinctCardCount(CardListType.DueCard, true)).toEqual(1 + 2 + 0);
+    });
+});
+
 describe("getTopicPath()", () => {
     test("Empty topic path", () => {
         let deck: Deck = new Deck("Root", null);
@@ -194,30 +274,32 @@ describe("copyWithCardFilter()", () => {
         });
 
         test("With new cards", async () => {
-            let text: string = `
+            let text: string = `#flashcards
 Q1::A1
 Q2::A2
 Q3::A3`;
             let original: Deck = await SampleItemDecks.createDeckFromText(
                 text,
-                new TopicPath(["Root"]),
+                TopicPath.emptyPath,
             );
             let copy: Deck = original.copyWithCardFilter((card) => card.front.includes("2"));
+            copy = copy.getDeckByTopicTag("#flashcards");
 
             expect(copy.newFlashcards.length).toEqual(1);
             expect(copy.newFlashcards[0].front).toEqual("Q2");
         });
 
         test("With scheduled cards", async () => {
-            let text: string = `
+            let text: string = `#flashcards
 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"]),
+                TopicPath.emptyPath,
             );
             let copy: Deck = original.copyWithCardFilter((card) => !card.front.includes("2"));
+            copy = copy.getDeck(TopicPath.getTopicPathFromTag("#flashcards"));
 
             expect(copy.newFlashcards.length).toEqual(0);
             expect(copy.dueFlashcards.length).toEqual(2);
@@ -284,3 +366,54 @@ Q3::A3 <!--SR:!2023-09-02,4,270-->`;
         });
     });
 });
+
+describe("deleteCardFromAllDecks()", () => {
+    test("Single deck", async () => {
+        let text: string = `#flashcards
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4 <!--SR:!2023-09-02,4,270-->`;
+        const deck: Deck = await SampleItemDecks.createDeckFromText(text);
+        const flashcardDeck: Deck = deck.getDeckByTopicTag("#flashcards");
+        expect(flashcardDeck.getCardCount(CardListType.All, false)).toEqual(4);
+
+        // We have to call on the base deck (i.e. "deck", not "flashcardDeck")
+        deck.deleteCardFromAllDecks(flashcardDeck.newFlashcards[1], true);
+        expect(flashcardDeck.getCardCount(CardListType.All, false)).toEqual(3);
+        expect(flashcardDeck.newFlashcards[0].front).toEqual("Q1");
+        expect(flashcardDeck.newFlashcards[1].front).toEqual("Q3");
+        expect(flashcardDeck.dueFlashcards[0].front).toEqual("Q4");
+    });
+
+    test("Deck hierarchy - with duplicate cards", async () => {
+        let text: string = `#flashcards
+Q1::A1
+Q2::A2
+Q3::A3
+Q4::A4 <!--SR:!2023-09-02,4,270-->
+
+#flashcards/folder1 #flashcards/folder2
+Q11::A11
+Q12::A12
+Q13::A13
+Q14::A14 <!--SR:!2023-09-02,4,270
+Q15::A15 <!--SR:!2023-09-02,4,270
+
+#flashcards/folder2
+Q21::A21
+
+`;
+        const deck: Deck = await SampleItemDecks.createDeckFromText(text);
+
+        // Delete card from #flashcards/folder1, deletes from #flashcards/folder as well
+        const folder1: Deck = deck.getDeckByTopicTag("#flashcards/folder1");
+        const folder2: Deck = deck.getDeckByTopicTag("#flashcards/folder2");
+        let card: Card = folder1.newFlashcards[1];
+        expect(folder1.getCardCount(CardListType.NewCard, false)).toEqual(3);
+        expect(folder2.getCardCount(CardListType.NewCard, false)).toEqual(3 + 1);
+        deck.deleteCardFromAllDecks(card, true);
+        expect(folder1.getCardCount(CardListType.NewCard, false)).toEqual(2);
+        expect(folder2.getCardCount(CardListType.NewCard, false)).toEqual(2 + 1);
+    });
+});
diff --git a/tests/unit/helpers/UnitTestHelper.test.ts b/tests/unit/helpers/UnitTestHelper.test.ts
new file mode 100644
index 0000000..519c501
--- /dev/null
+++ b/tests/unit/helpers/UnitTestHelper.test.ts
@@ -0,0 +1,65 @@
+import { TagCache } from "obsidian";
+import { unitTest_GetAllTagsFromTextEx } from "./UnitTestHelper";
+
+describe("unitTest_GetAllTagsFromTextEx", () => {
+    describe("Without frontmatter", () => {
+        test("Tags on multiple lines", () => {
+            // The next line is numbered as line 0, therefore #review is line 2
+            const text: string = `
+
+#review
+
+----
+#flashcards/science/chemistry
+
+# Questions
+
+Chemistry Question from file underelephant 4A::goodby
+<!--SR:!2023-11-02,17,290-->
+Chemistry Question from file underdog 4B::goodby
+<!--SR:!2023-12-18,57,310-->
+Chemistry Question from file underdog 4C::goodby
+<!--SR:!2023-10-25,3,210-->
+This single {{question}} turns into {{3 separate}} {{cards}}
+<!--SR:!2023-10-20,1,241!2023-10-25,3,254!2023-10-23,1,221-->
+
+#flashcards/science/misc
+
+    `;
+            const actual: TagCache[] = unitTest_GetAllTagsFromTextEx(text);
+            const expected: TagCache[] = [
+                createTagCacheObj("#review", 2),
+                createTagCacheObj("#flashcards/science/chemistry", 5),
+                createTagCacheObj("#flashcards/science/misc", 18),
+            ];
+            expect(actual).toEqual(expected);
+        });
+
+        test("Multiple tags on same line", () => {
+            // The next line is numbered as line 0, therefore #review is line 2
+            const text: string = `
+
+#flashcards/science/chemistry #flashcards/science/misc
+
+
+
+    `;
+            const actual: TagCache[] = unitTest_GetAllTagsFromTextEx(text);
+            const expected: TagCache[] = [
+                createTagCacheObj("#flashcards/science/chemistry", 2),
+                createTagCacheObj("#flashcards/science/misc", 2),
+            ];
+            expect(actual).toEqual(expected);
+        });
+    });
+});
+
+function createTagCacheObj(tag: string, line: number): any {
+    return {
+        tag: tag,
+        position: {
+            start: { line: line, col: null, offset: null },
+            end: { line: line, col: null, offset: null },
+        },
+    };
+}
diff --git a/tests/unit/helpers/UnitTestHelper.ts b/tests/unit/helpers/UnitTestHelper.ts
new file mode 100644
index 0000000..e87c842
--- /dev/null
+++ b/tests/unit/helpers/UnitTestHelper.ts
@@ -0,0 +1,64 @@
+import { TagCache } from "obsidian";
+import { extractFrontmatter, splitTextIntoLineArray } from "src/util/utils";
+
+export function unitTest_CreateTagCache(tag: string, lineNum: number): TagCache {
+    return {
+        tag,
+        position: {
+            start: { line: lineNum, col: null, offset: null },
+            end: { line: lineNum, col: null, offset: null },
+        },
+    };
+}
+
+export function unitTest_GetAllTagsFromTextEx(text: string): TagCache[] {
+    const [frontmatter, content] = extractFrontmatter(text);
+    const result = [] as TagCache[];
+    let lines: string[];
+
+    if (frontmatter) {
+        const dataPrefix: string = "  - ";
+        lines = splitTextIntoLineArray(frontmatter);
+        let foundTagHeading: boolean = false;
+        for (let i = 0; i < lines.length; i++) {
+            const line: string = lines[i];
+            if (foundTagHeading) {
+                if (line.startsWith(dataPrefix)) {
+                    const tagStr: string = line.substring(dataPrefix.length);
+                    result.push(unitTest_CreateTagCache("#" + tagStr, i));
+                } else {
+                    break;
+                }
+            } else {
+                if (line.startsWith("tags:")) {
+                    foundTagHeading = true;
+                }
+            }
+        }
+    }
+    lines = splitTextIntoLineArray(text);
+    for (let i = 0; i < lines.length; i++) {
+        const tagRegex = /#[^\s#]+/gi;
+        const matchList: RegExpMatchArray = lines[i].match(tagRegex);
+        if (matchList) {
+            for (const match of matchList) {
+                const tag: TagCache = {
+                    tag: match,
+                    position: {
+                        start: { line: i, col: null, offset: null },
+                        end: { line: i, col: null, offset: null },
+                    },
+                };
+                result.push(tag);
+            }
+        }
+    }
+    return result;
+}
+
+export function unitTest_GetAllTagsFromText(text: string): string[] {
+    const tagRegex = /#[^\s#]+/gi;
+    const result: RegExpMatchArray = text.match(tagRegex);
+    if (!result) return [];
+    return result;
+}
diff --git a/tests/unit/helpers/UnitTestSRFile.ts b/tests/unit/helpers/UnitTestSRFile.ts
new file mode 100644
index 0000000..3110f89
--- /dev/null
+++ b/tests/unit/helpers/UnitTestSRFile.ts
@@ -0,0 +1,38 @@
+import { TagCache } from "obsidian";
+import { ISRFile } from "src/SRFile";
+import { unitTest_GetAllTagsFromTextEx } from "./UnitTestHelper";
+
+export class UnitTestSRFile implements ISRFile {
+    content: string;
+    _path: string;
+
+    constructor(content: string, path: string = null) {
+        this.content = content;
+        this._path = path;
+    }
+
+    get path(): string {
+        return this._path;
+    }
+
+    get basename(): string {
+        return "";
+    }
+
+    getAllTagsFromText(): TagCache[] {
+        return unitTest_GetAllTagsFromTextEx(this.content);
+    }
+
+    // eslint-disable-next-line  @typescript-eslint/no-unused-vars
+    getQuestionContext(cardLine: number): string[] {
+        return [];
+    }
+
+    async read(): Promise<string> {
+        return this.content;
+    }
+
+    async write(content: string): Promise<void> {
+        this.content = content;
+    }
+}
diff --git a/tests/unit/parser.test.ts b/tests/unit/parser.test.ts
index 183dc67..9d2c7b6 100644
--- a/tests/unit/parser.test.ts
+++ b/tests/unit/parser.test.ts
@@ -1,4 +1,4 @@
-import { parse } from "src/parser";
+import { parseEx, ParsedQuestionInfo } from "src/parser";
 import { CardType } from "src/Question";
 
 const defaultArgs: [string, string, string, string, boolean, boolean, boolean] = [
@@ -11,83 +11,125 @@ const defaultArgs: [string, string, string, string, boolean, boolean, boolean] =
     true,
 ];
 
+/**
+ * This function is a small wrapper around parseEx used for testing only.
+ * 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,
+    singlelineCardSeparator: string,
+    singlelineReversedCardSeparator: string,
+    multilineCardSeparator: string,
+    multilineReversedCardSeparator: string,
+    convertHighlightsToClozes: boolean,
+    convertBoldTextToClozes: boolean,
+    convertCurlyBracketsToClozes: boolean,
+): [CardType, string, number, number][] {
+    const list: ParsedQuestionInfo[] = parseEx(
+        text,
+        singlelineCardSeparator,
+        singlelineReversedCardSeparator,
+        multilineCardSeparator,
+        multilineReversedCardSeparator,
+        convertHighlightsToClozes,
+        convertBoldTextToClozes,
+        convertCurlyBracketsToClozes,
+    );
+    const result: [CardType, string, number, number][] = [];
+    for (const item of list) {
+        result.push([item.cardType, item.text, item.firstLineNum, item.lastLineNum]);
+    }
+    return result;
+}
+
 test("Test parsing of single line basic cards", () => {
     expect(parse("Question::Answer", ...defaultArgs)).toEqual([
-        [CardType.SingleLineBasic, "Question::Answer", 0],
+        [CardType.SingleLineBasic, "Question::Answer", 0, 0],
     ]);
     expect(parse("Question::Answer\n<!--SR:!2021-08-11,4,270-->", ...defaultArgs)).toEqual([
-        [CardType.SingleLineBasic, "Question::Answer\n<!--SR:!2021-08-11,4,270-->", 0],
+        [CardType.SingleLineBasic, "Question::Answer\n<!--SR:!2021-08-11,4,270-->", 0, 1],
     ]);
     expect(parse("Question::Answer <!--SR:2021-08-11,4,270-->", ...defaultArgs)).toEqual([
-        [CardType.SingleLineBasic, "Question::Answer <!--SR:2021-08-11,4,270-->", 0],
+        [CardType.SingleLineBasic, "Question::Answer <!--SR:2021-08-11,4,270-->", 0, 0],
     ]);
     expect(parse("Some text before\nQuestion ::Answer", ...defaultArgs)).toEqual([
-        [CardType.SingleLineBasic, "Question ::Answer", 1],
+        [CardType.SingleLineBasic, "Question ::Answer", 1, 1],
     ]);
     expect(parse("#Title\n\nQ1::A1\nQ2:: A2", ...defaultArgs)).toEqual([
-        [CardType.SingleLineBasic, "Q1::A1", 2],
-        [CardType.SingleLineBasic, "Q2:: A2", 3],
+        [CardType.SingleLineBasic, "Q1::A1", 2, 2],
+        [CardType.SingleLineBasic, "Q2:: A2", 3, 3],
     ]);
     expect(parse("#flashcards/science Question ::Answer", ...defaultArgs)).toEqual([
-        [CardType.SingleLineBasic, "#flashcards/science Question ::Answer", 0],
+        [CardType.SingleLineBasic, "#flashcards/science Question ::Answer", 0, 0],
     ]);
 });
 
 test("Test parsing of single line reversed cards", () => {
     expect(parse("Question:::Answer", ...defaultArgs)).toEqual([
-        [CardType.SingleLineReversed, "Question:::Answer", 0],
+        [CardType.SingleLineReversed, "Question:::Answer", 0, 0],
     ]);
     expect(parse("Some text before\nQuestion :::Answer", ...defaultArgs)).toEqual([
-        [CardType.SingleLineReversed, "Question :::Answer", 1],
+        [CardType.SingleLineReversed, "Question :::Answer", 1, 1],
     ]);
     expect(parse("#Title\n\nQ1:::A1\nQ2::: A2", ...defaultArgs)).toEqual([
-        [CardType.SingleLineReversed, "Q1:::A1", 2],
-        [CardType.SingleLineReversed, "Q2::: A2", 3],
+        [CardType.SingleLineReversed, "Q1:::A1", 2, 2],
+        [CardType.SingleLineReversed, "Q2::: A2", 3, 3],
     ]);
 });
 
 test("Test parsing of multi line basic cards", () => {
     expect(parse("Question\n?\nAnswer", ...defaultArgs)).toEqual([
-        [CardType.MultiLineBasic, "Question\n?\nAnswer", 1],
+        [CardType.MultiLineBasic, "Question\n?\nAnswer", 0, 2],
     ]);
     expect(parse("Question\n? \nAnswer", ...defaultArgs)).toEqual([
-        [CardType.MultiLineBasic, "Question\n?\nAnswer", 1],
+        [CardType.MultiLineBasic, "Question\n?\nAnswer", 0, 2],
     ]);
     expect(parse("Question\n?\nAnswer <!--SR:!2021-08-11,4,270-->", ...defaultArgs)).toEqual([
-        [CardType.MultiLineBasic, "Question\n?\nAnswer <!--SR:!2021-08-11,4,270-->", 1],
+        [CardType.MultiLineBasic, "Question\n?\nAnswer <!--SR:!2021-08-11,4,270-->", 0, 2],
     ]);
     expect(parse("Question\n?\nAnswer\n<!--SR:2021-08-11,4,270-->", ...defaultArgs)).toEqual([
-        [CardType.MultiLineBasic, "Question\n?\nAnswer\n<!--SR:2021-08-11,4,270-->", 1],
+        [CardType.MultiLineBasic, "Question\n?\nAnswer\n<!--SR:2021-08-11,4,270-->", 0, 3],
     ]);
-    expect(parse("Some text before\nQuestion\n?\nAnswer", ...defaultArgs)).toEqual([
-        [CardType.MultiLineBasic, "Some text before\nQuestion\n?\nAnswer", 2],
+    expect(parse("Question line 1\nQuestion line 2\n?\nAnswer", ...defaultArgs)).toEqual([
+        [CardType.MultiLineBasic, "Question line 1\nQuestion line 2\n?\nAnswer", 0, 3],
     ]);
-    expect(parse("Question\n?\nAnswer\nSome text after!", ...defaultArgs)).toEqual([
-        [CardType.MultiLineBasic, "Question\n?\nAnswer\nSome text after!", 1],
+    expect(parse("Question\n?\nAnswer line 1\nAnswer line 2", ...defaultArgs)).toEqual([
+        [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", ...defaultArgs)).toEqual([
-        [CardType.MultiLineBasic, "Line0\nQ1\n?\nA1\nAnswerExtra", 4],
-        [CardType.MultiLineBasic, "Q2\n?\nA2", 9],
+        [
+            CardType.MultiLineBasic,
+            "Line0\nQ1\n?\nA1\nAnswerExtra",
+            /* Line0 */ 2,
+            /* AnswerExtra */ 6,
+        ],
+        [CardType.MultiLineBasic, "Q2\n?\nA2", 8, 10],
     ]);
     expect(parse("#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", ...defaultArgs)).toEqual([
-        [CardType.MultiLineBasic, "#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", 2],
+        [CardType.MultiLineBasic, "#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", 0, 3],
     ]);
 });
 
 test("Test parsing of multi line reversed cards", () => {
     expect(parse("Question\n??\nAnswer", ...defaultArgs)).toEqual([
-        [CardType.MultiLineReversed, "Question\n??\nAnswer", 1],
+        [CardType.MultiLineReversed, "Question\n??\nAnswer", 0, 2],
     ]);
-    expect(parse("Some text before\nQuestion\n??\nAnswer", ...defaultArgs)).toEqual([
-        [CardType.MultiLineReversed, "Some text before\nQuestion\n??\nAnswer", 2],
+    expect(parse("Question line 1\nQuestion line 2\n??\nAnswer", ...defaultArgs)).toEqual([
+        [CardType.MultiLineReversed, "Question line 1\nQuestion line 2\n??\nAnswer", 0, 3],
     ]);
-    expect(parse("Question\n??\nAnswer\nSome text after!", ...defaultArgs)).toEqual([
-        [CardType.MultiLineReversed, "Question\n??\nAnswer\nSome text after!", 1],
+    expect(parse("Question\n??\nAnswer line 1\nAnswer line 2", ...defaultArgs)).toEqual([
+        [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", ...defaultArgs)).toEqual(
         [
-            [CardType.MultiLineReversed, "Line0\nQ1\n??\nA1\nAnswerExtra", 4],
-            [CardType.MultiLineReversed, "Q2\n??\nA2", 9],
+            [
+                CardType.MultiLineReversed,
+                "Line0\nQ1\n??\nA1\nAnswerExtra",
+                /* Line0 */ 2,
+                /* AnswerExtra */ 6,
+            ],
+            [CardType.MultiLineReversed, "Q2\n??\nA2", 8, 10],
         ],
     );
 });
@@ -95,16 +137,16 @@ test("Test parsing of multi line reversed cards", () => {
 test("Test parsing of cloze cards", () => {
     // ==highlights==
     expect(parse("cloze ==deletion== test", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "cloze ==deletion== test", 0],
+        [CardType.Cloze, "cloze ==deletion== test", 0, 0],
     ]);
     expect(parse("cloze ==deletion== test\n<!--SR:2021-08-11,4,270-->", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "cloze ==deletion== test\n<!--SR:2021-08-11,4,270-->", 0],
+        [CardType.Cloze, "cloze ==deletion== test\n<!--SR:2021-08-11,4,270-->", 0, 1],
     ]);
     expect(parse("cloze ==deletion== test <!--SR:2021-08-11,4,270-->", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "cloze ==deletion== test <!--SR:2021-08-11,4,270-->", 0],
+        [CardType.Cloze, "cloze ==deletion== test <!--SR:2021-08-11,4,270-->", 0, 0],
     ]);
     expect(parse("==this== is a ==deletion==\n", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "==this== is a ==deletion==", 0],
+        [CardType.Cloze, "==this== is a ==deletion==", 0, 0],
     ]);
     expect(
         parse(
@@ -113,8 +155,8 @@ test("Test parsing of cloze cards", () => {
             ...defaultArgs,
         ),
     ).toEqual([
-        [CardType.Cloze, "a deletion on\nsuch ==wow==", 3],
-        [CardType.Cloze, "many text\nsuch surprise ==wow== more ==text==\nsome text after", 6],
+        [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 ==", ...defaultArgs)).toEqual([]);
     expect(parse("lorem ipsum ==p\ndolor won==", ...defaultArgs)).toEqual([]);
@@ -126,16 +168,16 @@ test("Test parsing of cloze cards", () => {
 
     // **bolded**
     expect(parse("cloze **deletion** test", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "cloze **deletion** test", 0],
+        [CardType.Cloze, "cloze **deletion** test", 0, 0],
     ]);
     expect(parse("cloze **deletion** test\n<!--SR:2021-08-11,4,270-->", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "cloze **deletion** test\n<!--SR:2021-08-11,4,270-->", 0],
+        [CardType.Cloze, "cloze **deletion** test\n<!--SR:2021-08-11,4,270-->", 0, 1],
     ]);
     expect(parse("cloze **deletion** test <!--SR:2021-08-11,4,270-->", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "cloze **deletion** test <!--SR:2021-08-11,4,270-->", 0],
+        [CardType.Cloze, "cloze **deletion** test <!--SR:2021-08-11,4,270-->", 0, 0],
     ]);
     expect(parse("**this** is a **deletion**\n", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "**this** is a **deletion**", 0],
+        [CardType.Cloze, "**this** is a **deletion**", 0, 0],
     ]);
     expect(
         parse(
@@ -144,8 +186,8 @@ test("Test parsing of cloze cards", () => {
             ...defaultArgs,
         ),
     ).toEqual([
-        [CardType.Cloze, "a deletion on\nsuch **wow**", 3],
-        [CardType.Cloze, "many text\nsuch surprise **wow** more **text**\nsome text after", 6],
+        [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 **", ...defaultArgs)).toEqual([]);
     expect(parse("lorem ipsum **p\ndolor won**", ...defaultArgs)).toEqual([]);
@@ -157,7 +199,7 @@ test("Test parsing of cloze cards", () => {
 
     // both
     expect(parse("cloze **deletion** test ==another deletion==!", ...defaultArgs)).toEqual([
-        [CardType.Cloze, "cloze **deletion** test ==another deletion==!", 0],
+        [CardType.Cloze, "cloze **deletion** test ==another deletion==!", 0, 0],
     ]);
 });
 
@@ -177,12 +219,14 @@ test("Test parsing of a mix of card types", () => {
                 "Duis magna arcu, eleifend rhoncus ==euismod non,==\n" +
                 "laoreet vitae enim.",
             2,
+            4,
         ],
-        [CardType.SingleLineBasic, "Fusce placerat::velit in pharetra gravida", 6],
+        [CardType.SingleLineBasic, "Fusce placerat::velit in pharetra gravida", 6, 6],
         [
             CardType.MultiLineReversed,
             "Donec dapibus ullamcorper aliquam.\n??\nDonec dapibus ullamcorper aliquam.\n<!--SR:2021-08-11,4,270-->",
-            9,
+            8,
+            11 /* <!--SR:2021-08-11,4,270--> */,
         ],
     ]);
 });
@@ -200,7 +244,8 @@ test("Test codeblocks", () => {
             CardType.MultiLineBasic,
             "How do you ... Python?\n?\n" +
                 "```\nprint('Hello World!')\nprint('Howdy?')\nlambda x: x[0]\n```",
-            1,
+            0,
+            6 /* ``` */,
         ],
     ]);
 
@@ -216,7 +261,8 @@ test("Test codeblocks", () => {
             CardType.MultiLineBasic,
             "How do you ... Python?\n?\n" +
                 "```\nprint('Hello World!')\n\n\nprint('Howdy?')\n\nlambda x: x[0]\n```",
-            1,
+            0,
+            9 /* ``` */,
         ],
     ]);
 
@@ -248,7 +294,8 @@ test("Test codeblocks", () => {
                 "print('hello world')\n" +
                 "~~~\n" +
                 "````",
-            1,
+            0,
+            12 /* ``` */,
         ],
     ]);
 });
diff --git a/tests/unit/util/utils.test.ts b/tests/unit/util/utils.test.ts
index 36a797e..b4b79e6 100644
--- a/tests/unit/util/utils.test.ts
+++ b/tests/unit/util/utils.test.ts
@@ -1,5 +1,9 @@
 import { YAML_FRONT_MATTER_REGEX } from "src/constants";
-import { findLineIndexOfSearchStringIgnoringWs, literalStringReplace } from "src/util/utils";
+import {
+    extractFrontmatter,
+    findLineIndexOfSearchStringIgnoringWs,
+    literalStringReplace,
+} from "src/util/utils";
 
 describe("literalStringReplace", () => {
     test("Replacement string doesn't have any dollar signs", async () => {
@@ -92,6 +96,71 @@ describe("YAML_FRONT_MATTER_REGEX", () => {
     });
 });
 
+describe("extractFrontmatter", () => {
+    test("No frontmatter", () => {
+        let text: string = `Hello
+Goodbye`;
+        let frontmatter: string;
+        let content: string;
+        [frontmatter, content] = extractFrontmatter(text);
+        expect(frontmatter).toEqual("");
+        expect(content).toEqual(text);
+
+        text = `---
+Goodbye`;
+        [frontmatter, content] = extractFrontmatter(text);
+        expect(frontmatter).toEqual("");
+        expect(content).toEqual(text);
+    });
+
+    test("With frontmatter (and nothing else)", () => {
+        let frontmatter: string = `---
+sr-due: 2024-01-17
+sr-interval: 16
+sr-ease: 278
+tags:
+  - flashcards/aws
+  - flashcards/datascience
+---`;
+        const text: string = frontmatter;
+        let content: string;
+        [frontmatter, content] = extractFrontmatter(text);
+        expect(frontmatter).toEqual(text);
+        expect(content).toEqual("");
+    });
+
+    test("With frontmatter (and content)", () => {
+        let frontmatter: string = `---
+sr-due: 2024-01-17
+sr-interval: 16
+sr-ease: 278
+tags:
+  - flashcards/aws
+  - flashcards/datascience
+---`;
+        const content: string = `#flashcards/science/chemistry
+
+# Questions
+
+Chemistry Question from file underelephant 4A::goodby
+<!--SR:!2023-11-02,17,290-->
+Chemistry Question from file underdog 4B::goodby
+<!--SR:!2023-12-18,57,310-->
+Chemistry Question from file underdog 4C::goodby
+<!--SR:!2023-10-25,3,210-->
+This single {{question}} turns into {{3 separate}} {{cards}}
+<!--SR:!2023-10-20,1,241!2023-10-25,3,254!2023-10-23,1,221-->
+
+`;
+        const text: string = `${frontmatter}
+${content}`;
+
+        const [f, c] = extractFrontmatter(text);
+        expect(f).toEqual(frontmatter);
+        expect(c).toEqual(content);
+    });
+});
+
 describe("findLineIndexOfSearchStringIgnoringWs", () => {
     const space: string = " ";
     test("Search string not present", () => {