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 RTL flashcards specified by frontmatter "direction" attribute #935

Merged
merged 16 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ All notable changes to this project will be documented in this file. Dates are d

Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).

[Unreleased]
#### [Unreleased]

- RTL support https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/335
- Fixed notes selection when all notes are reviewed. [`#548`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/548)

#### [1.12.4](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.12.3...1.12.4)
Expand Down
18 changes: 18 additions & 0 deletions docs/en/flashcards.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ The plugin will automatically search for folders that contain flashcards & use t

This is an alternative to the tagging option and can be enabled in settings.

## RTL Support

There are two ways that the plugin can be used with RTL languages, such as Arabic, Hebrew, Persian (Farsi).

If all cards are in a RTL language, then simply enable the global Obsidian option `Editor → Right-to-left (RTL)`.

If all cards within a single note have the same LTR/RTL direction, then frontmatter can be used to specify the text direction. For example:

```
---
direction: rtl
---
```

This is the same way text direction is specified to the `RTL Support` plugin.

Note that there is no current support for cards with different text directions within the same note.

## Reviewing

Once done creating cards, click on the flashcards button on the left ribbon to start reviewing the flashcards. After a card is reviewed, a HTML comment is added containing the next review day, the interval, and the card's ease.
Expand Down
8 changes: 7 additions & 1 deletion src/NoteFileLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Question } from "./Question";
import { TopicPath } from "./TopicPath";
import { NoteQuestionParser } from "./NoteQuestionParser";
import { SRSettings } from "./settings";
import { TextDirection } from "./util/TextDirection";

export class NoteFileLoader {
fileText: string;
Expand All @@ -16,14 +17,19 @@ export class NoteFileLoader {
this.settings = settings;
}

async load(noteFile: ISRFile, folderTopicPath: TopicPath): Promise<Note | null> {
async load(
noteFile: ISRFile,
defaultTextDirection: TextDirection,
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,
defaultTextDirection,
folderTopicPath,
onlyKeepQuestionsWithTopicPath,
);
Expand Down
14 changes: 12 additions & 2 deletions src/NoteParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ISRFile } from "./SRFile";
import { Note } from "./Note";
import { SRSettings } from "./settings";
import { TopicPath } from "./TopicPath";
import { TextDirection } from "./util/TextDirection";

export class NoteParser {
settings: SRSettings;
Expand All @@ -12,9 +13,18 @@ export class NoteParser {
this.settings = settings;
}

async parse(noteFile: ISRFile, folderTopicPath: TopicPath): Promise<Note> {
async parse(
noteFile: ISRFile,
defaultTextDirection: TextDirection,
folderTopicPath: TopicPath,
): Promise<Note> {
const questionParser: NoteQuestionParser = new NoteQuestionParser(this.settings);
const questions = await questionParser.createQuestionList(noteFile, folderTopicPath, true);
const questions = await questionParser.createQuestionList(
noteFile,
defaultTextDirection,
folderTopicPath,
true,
);

const result: Note = new Note(noteFile, questions);
return result;
Expand Down
14 changes: 12 additions & 2 deletions src/NoteQuestionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CardFrontBack, CardFrontBackUtil } from "./QuestionType";
import { SRSettings, SettingsUtil } from "./settings";
import { ISRFile, frontmatterTagPseudoLineNum } from "./SRFile";
import { TopicPath, TopicPathList } from "./TopicPath";
import { TextDirection } from "./util/TextDirection";
import { extractFrontmatter, splitTextIntoLineArray } from "./util/utils";

export class NoteQuestionParser {
Expand Down Expand Up @@ -40,6 +41,7 @@ export class NoteQuestionParser {

async createQuestionList(
noteFile: ISRFile,
defaultTextDirection: TextDirection,
folderTopicPath: TopicPath,
onlyKeepQuestionsWithTopicPath: boolean,
): Promise<Question[]> {
Expand All @@ -64,8 +66,11 @@ export class NoteQuestionParser {
[this.frontmatterText, this.contentText] = extractFrontmatter(noteText);

// Create the question list
let textDirection: TextDirection = noteFile.getTextDirection();
if (textDirection == TextDirection.Unspecified) textDirection = defaultTextDirection;
this.questionList = this.doCreateQuestionList(
noteText,
textDirection,
folderTopicPath,
this.tagCacheList,
);
Expand All @@ -89,6 +94,7 @@ export class NoteQuestionParser {

private doCreateQuestionList(
noteText: string,
textDirection: TextDirection,
folderTopicPath: TopicPath,
tagCacheList: TagCache[],
): Question[] {
Expand All @@ -100,7 +106,7 @@ export class NoteQuestionParser {
const result: Question[] = [];
const parsedQuestionInfoList: ParsedQuestionInfo[] = this.parseQuestions();
for (const parsedQuestionInfo of parsedQuestionInfoList) {
const question: Question = this.createQuestionObject(parsedQuestionInfo);
const question: Question = this.createQuestionObject(parsedQuestionInfo, textDirection);

// Each rawCardText can turn into multiple CardFrontBack's (e.g. CardType.Cloze, CardType.SingleLineReversed)
const cardFrontBackList: CardFrontBack[] = CardFrontBackUtil.expand(
Expand Down Expand Up @@ -144,14 +150,18 @@ export class NoteQuestionParser {
return result;
}

private createQuestionObject(parsedQuestionInfo: ParsedQuestionInfo): Question {
private createQuestionObject(
parsedQuestionInfo: ParsedQuestionInfo,
textDirection: TextDirection,
): Question {
const questionContext: string[] = this.noteFile.getQuestionContext(
parsedQuestionInfo.firstLineNum,
);
const result = Question.Create(
this.settings,
parsedQuestionInfo,
null, // We haven't worked out the TopicPathList yet
textDirection,
questionContext,
);
return result;
Expand Down
28 changes: 24 additions & 4 deletions src/Question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ParsedQuestionInfo } from "./parser";
import { SRSettings } from "./settings";
import { TopicPath, TopicPathList, TopicPathWithWs } from "./TopicPath";
import { MultiLineTextFinder } from "./util/MultiLineTextFinder";
import { TextDirection } from "./util/TextDirection";
import { cyrb53, stringTrimStart } from "./util/utils";

export enum CardType {
Expand Down Expand Up @@ -87,6 +88,9 @@ export class QuestionText {
// The question text, e.g. "Q1::A1" with leading/trailing whitespace as described above
actualQuestion: string;

// Either LTR or RTL
textDirection: TextDirection;

// The block identifier (optional), e.g. "^quote-of-the-day"
// Format of block identifiers:
// https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note
Expand All @@ -102,11 +106,13 @@ export class QuestionText {
original: string,
topicPathWithWs: TopicPathWithWs,
actualQuestion: string,
textDirection: TextDirection,
blockId: string,
) {
this.original = original;
this.topicPathWithWs = topicPathWithWs;
this.actualQuestion = actualQuestion;
this.textDirection = textDirection;
this.obsidianBlockId = blockId;

// The hash is generated based on the topic and question, explicitly not the schedule or obsidian block ID
Expand All @@ -117,10 +123,14 @@ export class QuestionText {
return this.actualQuestion.endsWith("```");
}

static create(original: string, settings: SRSettings): QuestionText {
static create(
original: string,
textDirection: TextDirection,
settings: SRSettings,
): QuestionText {
const [topicPathWithWs, actualQuestion, blockId] = this.splitText(original, settings);

return new QuestionText(original, topicPathWithWs, actualQuestion, blockId);
return new QuestionText(original, topicPathWithWs, actualQuestion, textDirection, blockId);
}

static splitText(original: string, settings: SRSettings): [TopicPathWithWs, string, string] {
Expand Down Expand Up @@ -264,7 +274,12 @@ export class Question {

let newText = MultiLineTextFinder.findAndReplace(noteText, originalText, replacementText);
if (newText) {
this.questionText = QuestionText.create(replacementText, settings);
// Don't support changing the textDirection setting
this.questionText = QuestionText.create(
replacementText,
this.questionText.textDirection,
settings,
);
} else {
console.error(
`updateQuestionText: Text not found: ${originalText.substring(
Expand Down Expand Up @@ -293,10 +308,15 @@ export class Question {
settings: SRSettings,
parsedQuestionInfo: ParsedQuestionInfo,
noteTopicPathList: TopicPathList,
textDirection: TextDirection,
context: string[],
): Question {
const hasEditLaterTag = parsedQuestionInfo.text.includes(settings.editLaterTag);
const questionText: QuestionText = QuestionText.create(parsedQuestionInfo.text, settings);
const questionText: QuestionText = QuestionText.create(
parsedQuestionInfo.text,
textDirection,
settings,
);

let topicPathList: TopicPathList = noteTopicPathList;
if (questionText.topicPathWithWs) {
Expand Down
20 changes: 19 additions & 1 deletion src/SRFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import {
MetadataCache,
TFile,
Vault,
HeadingCache,
getAllTags as ObsidianGetAllTags,
HeadingCache,
TagCache,
FrontMatterCache,
} from "obsidian";
import { TextDirection } from "./util/TextDirection";
import { parseObsidianFrontmatterTag } from "./util/utils";

// NOTE: Line numbers are zero based
Expand All @@ -16,6 +17,7 @@ export interface ISRFile {
getAllTagsFromCache(): string[];
getAllTagsFromText(): TagCache[];
getQuestionContext(cardLine: number): string[];
getTextDirection(): TextDirection;
read(): Promise<string>;
write(content: string): Promise<void>;
}
Expand Down Expand Up @@ -111,6 +113,22 @@ export class SrTFile implements ISRFile {
return result;
}

getTextDirection(): TextDirection {
let result: TextDirection = TextDirection.Unspecified;
const fileCache = this.metadataCache.getFileCache(this.file);
const frontMatter = fileCache?.frontmatter;
if (frontMatter && frontMatter?.direction) {
// Don't know why the try/catch is needed; but copied from Obsidian RTL plug-in getFrontMatterDirection()
try {
const str: string = (frontMatter.direction + "").toLowerCase();
result = str == "rtl" ? TextDirection.Rtl : TextDirection.Ltr;
} catch (error) {
// continue regardless of error
}
}
return result;
}

async read(): Promise<string> {
return await this.vault.read(this.file);
}
Expand Down
18 changes: 14 additions & 4 deletions src/gui/EditModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { App, Modal } from "obsidian";
import { t } from "src/lang/helpers";
import { TextDirection } from "src/util/TextDirection";

// from https://github.com/chhoumann/quickadd/blob/bce0b4cdac44b867854d6233796e3406dfd163c6/src/gui/GenericInputPrompt/GenericInputPrompt.ts#L5
export class FlashcardEditModal extends Modal {
Expand All @@ -17,17 +18,23 @@ export class FlashcardEditModal extends Modal {
private rejectPromise: (reason?: any) => void;
private didSaveChanges = false;
private readonly modalText: string;

public static Prompt(app: App, placeholder: string): Promise<string> {
const newPromptModal = new FlashcardEditModal(app, placeholder);
private textDirection: TextDirection;

public static Prompt(
app: App,
placeholder: string,
textDirection: TextDirection,
): Promise<string> {
const newPromptModal = new FlashcardEditModal(app, placeholder, textDirection);
return newPromptModal.waitForClose;
}

constructor(app: App, existingText: string) {
constructor(app: App, existingText: string, textDirection: TextDirection) {
super(app);

this.modalText = existingText;
this.changedText = existingText;
this.textDirection = textDirection;

this.waitForClose = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
Expand Down Expand Up @@ -56,6 +63,9 @@ export class FlashcardEditModal extends Modal {
this.textArea.addClass("sr-input");
this.textArea.setText(this.modalText ?? "");
this.textArea.addEventListener("keydown", this.saveOnEnterCallback);
if (this.textDirection == TextDirection.Rtl) {
this.textArea.setAttribute("dir", "rtl");
}

this._createResponse(this.contentEl);
}
Expand Down
6 changes: 5 additions & 1 deletion src/gui/FlashcardModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ export class FlashcardModal extends Modal {
// Just the question/answer text; without any preceding topic tag
const textPrompt = currentQ.questionText.actualQuestion;

const editModal = FlashcardEditModal.Prompt(this.app, textPrompt);
const editModal = FlashcardEditModal.Prompt(
this.app,
textPrompt,
currentQ.questionText.textDirection,
);
editModal
.then(async (modifiedCardText) => {
this.reviewSequencer.updateCurrentQuestionText(modifiedCardText);
Expand Down
12 changes: 10 additions & 2 deletions src/gui/FlashcardReviewView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,11 @@ export class FlashcardReviewView {
this.plugin,
this._currentNote.filePath,
);
await wrapper.renderMarkdownWrapper(this._currentCard.front, this.content);
await wrapper.renderMarkdownWrapper(
this._currentCard.front,
this.content,
this._currentQuestion.questionText.textDirection,
);
// Set scroll position back to top
this.content.scrollTop = 0;

Expand Down Expand Up @@ -292,7 +296,11 @@ export class FlashcardReviewView {
this.plugin,
this._currentNote.filePath,
);
wrapper.renderMarkdownWrapper(this._currentCard.back, this.content);
wrapper.renderMarkdownWrapper(
this._currentCard.back,
this.content,
this._currentQuestion.questionText.textDirection,
);

// Show response buttons
this.answerButton.addClass("sr-is-hidden");
Expand Down
Loading
Loading