Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multi-deck cards #495, and multiple decks in a single note #705 #834

Merged
merged 22 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions docs/en/flashcards.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
110 changes: 102 additions & 8 deletions src/Deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ 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,
DueCard,
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[];
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading