diff --git a/docs/changelog.md b/docs/changelog.md
index a740e670..e6ca5609 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
+#### [Unreleased]
+
+- 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)
+- Feat 878 Refactor code to support diff methods of storing the scheduling info, and diff SR algorithms https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1006
+
#### [1.12.4](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.12.3...1.12.4)
- chore: fix package manager issue in CI [`#939`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/939)
diff --git a/docs/en/flashcards.md b/docs/en/flashcards.md
index afabfdc2..8bfc5b1e 100644
--- a/docs/en/flashcards.md
+++ b/docs/en/flashcards.md
@@ -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.
diff --git a/docs/zh/algorithms.md b/docs/zh/algorithms.md
new file mode 100644
index 00000000..81552691
--- /dev/null
+++ b/docs/zh/algorithms.md
@@ -0,0 +1,30 @@
+# 算法
+
+## SM-2
+
+!!! 警告
+
+ 该条目长时间未更新,
+ 请注意阅读 [源代码](https://github.com/st3v3nmw/obsidian-spaced-repetition/blob/master/src/scheduling.ts).
+
+(除 PageRanks 之外,卡片复习采用相同规划算法)
+
+- 该算法为 [Anki](https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html) 所采用的基于 [SM-2 算法](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2) 的变种。
+- 使用三级打分制,即在复习阶段自评对某个概念的掌握程度为`困难`,`记得`或`简单`。
+- 初始熟练度会根据链接笔记的平均熟练度、当前笔记的重要性和基本熟练度进行加权(使用 最大外链因子)。
+ - `当存在外链时: 初始熟练度 = (1 - 链接加权) * 基础熟练度 + 链接加权 * 外链平均熟练度`
+ - `链接加权 = 最大外链因子 * min(1.0, log(外链数目 + 0.5) / log(64))` (以自适应不同情况)
+ - 不同概念/笔记的优先级由 PageRank 算法设定(笔记之间存在轻重缓急)
+ - 大多数情况下基础概念/笔记具有更高优先级
+- 当用户对某个概念/笔记的自评为:
+ - 简单, 熟练度增加 `20` 复习间隔更新为 `原复习间隔 * 更新后熟练度 / 100 * 1.3` (1.3 是简单奖励)
+ - 记得, 熟练度不变,复习间隔更新为 `原复习间隔 * old_ease / 100`
+ - 困难, 熟练度降低 `20`,复习间隔更新为 `原复习间隔 * 0.5`
+ - `0.5` 可在设置中更改
+ - `最小熟练度 = 130`
+ - 当复习间隔不小于 `8` 天时
+ - `间隔 += 随机取值({-扰动, 0, +扰动})`
+ - 设定 `扰动 = 向上取整(0.05 * 间隔)`
+ - [Anki 文档](https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html):
+ > "[...] Anki 还会加入少量的随机扰动,以防止同时出现且评级相同的卡片获得相同的复习周期,导致其它们是在同一天被复习。"
+- 复习规划信息将被存储于笔记的yaml front matter部分
diff --git a/docs/zh/contributing.md b/docs/zh/contributing.md
new file mode 100644
index 00000000..bc89ca07
--- /dev/null
+++ b/docs/zh/contributing.md
@@ -0,0 +1,156 @@
+# Contributing
+
+First off, thanks for wanting to contribute to the Spaced Repetition plugin!
+
+## Bug Reports & Feature Requests
+
+- Check the [roadmap](https://github.com/st3v3nmw/obsidian-spaced-repetition/projects/2/) for upcoming features & fixes.
+- Raise an issue [here](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/) if you have a feature request or a bug report.
+- Visit the [discussions](https://github.com/st3v3nmw/obsidian-spaced-repetition/discussions/) section for Q&A help, feedback, and general discussion.
+
+## Translating
+
+### Steps
+
+To help translate the plugin to your language:
+
+1. Fork the [repository](https://github.com/st3v3nmw/obsidian-spaced-repetition).
+2. Copy the entries from `src/lang/locale/en.ts` to the proper file in `src/lang/locale/` (i.e. `fr.ts` for French, or `sw.ts` for Swahili). The locale codes are [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag).
+3. Translate,
+4. Then open a pull request,
+
+### Example
+
+Sample `en.ts` file:
+
+```typescript
+// English
+
+export default {
+ EASY: "Easy",
+ SHOW_ANSWER: "Show Answer",
+ DAYS_STR_IVL: "${interval} days",
+ CHECK_ALGORITHM_WIKI:
+ 'For more information, check the algorithm implementation.',
+};
+```
+
+Equivalent `sw.ts` file:
+
+```typescript
+// Swahili
+
+export default {
+ EASY: "Rahisi",
+ SHOW_ANSWER: "Onyesha Jibu",
+ DAYS_STR_IVL: "Siku ${interval}",
+ CHECK_ALGORITHM_WIKI:
+ 'Kwa habari zaidi, angalia utekelezaji wa algorithm.',
+};
+```
+
+A part of that last one is uhh, Google translated, I have a working understanding of Swahili but not enough to write computerese lol.
+
+Please note that:
+
+1. Only the strings(templates) on the right of the key should be translated.
+2. Text inside `${}` isn't translated. This is used to replace variables in code. For instance, if interval = 4, it becomes `4 days` in English & `Siku 4` in Swahili. Quite nifty if you ask me.
+
+## Code
+
+1. Make your changes.
+2. Run `pnpm dev` to test the changes inside Obsidian.
+3. You could create symbolic links between the build files and the Obsidian vault, example:
+
+ ```bash
+ # remove existing files in the Obsidian vault
+ rm ~/notes/.obsidian/plugins/obsidian-spaced-repetition/main.js ~/notes/.obsidian/plugins/obsidian-spaced-repetition/manifest.json ~/notes/.obsidian/plugins/obsidian-spaced-repetition/styles.css
+ # use absolute paths
+ ln -s /home/stephen/obsidian-spaced-repetition/build/main.js /home/stephen/notes/.obsidian/plugins/obsidian-spaced-repetition
+ ln -s /home/stephen/obsidian-spaced-repetition/manifest.json /home/stephen/notes/.obsidian/plugins/obsidian-spaced-repetition
+ ln -s /home/stephen/obsidian-spaced-repetition/styles.css /home/stephen/notes/.obsidian/plugins/obsidian-spaced-repetition
+ ```
+
+ - This can be coupled with the [Hot Reload plugin](https://github.com/pjeby/hot-reload)
+
+4. Document the "user-facing" changes e.g. new feature, UI change, etc.
+5. If your "business logic" is properly decoupled from Obsidian APIs, write some unit tests.
+ - This project uses [jest](https://jestjs.io/), tests are stored in `tests/`.
+ - `pnpm test`
+6. Add your change to the `[Unreleased]` section of the changelog (`docs/changelog.md`).
+ - The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), TL;DR:
+ - `Added` for new features.
+ - `Changed` for changes in existing functionality.
+ - `Deprecated` for soon-to-be removed features.
+ - `Removed` for now removed features.
+ - `Fixed` for any bug fixes.
+ - `Security` in case of vulnerabilities.
+ - You can also append a link to your GitHub profile, example:
+ - `Make flashcard text selectable [@st3v3nmw](https://github.com/st3v3nmw)`
+7. Before pushing your changes, run the linter: `pnpm lint`
+ - Format the code in case any warnings are raised: `pnpm format`
+8. Open the pull request.
+
+## Documentation
+
+The documentation consists of Markdown files which [MkDocs](https://www.mkdocs.org/) converts to static web pages.
+Specifically, this project uses [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/).
+
+These files reside in `docs/` in the respective language's folder. For instance, English docs are located in `docs/en/`.
+
+The docs are served on [https://www.stephenmwangi.com/obsidian-spaced-repetition/](https://www.stephenmwangi.com/obsidian-spaced-repetition/).
+
+For small changes, you can simply open an pull request for merging (against the `master` branch).
+The changes will be live once a new [release](https://github.com/st3v3nmw/obsidian-spaced-repetition/releases) is made.
+
+For larger diffs, it's important that you check how your docs look like as explained below.
+
+### Viewing Docs Locally
+
+#### Initial Setup
+
+1. Create a virtual environment: `python3 -m venv venv`
+2. Activate it: `. venv/bin/activate`
+3. Install the required dependencies: `pip install -r requirements.txt`
+
+#### Viewing
+
+1. Activate the virtual environment: `. venv/bin/activate`
+2. Serve the docs: `mkdocs serve`
+3. View your documentation locally on [http://127.0.0.1:8000/obsidian-spaced-repetition/](http://127.0.0.1:8000/obsidian-spaced-repetition/), any changes you make will reflect on the browser instantly.
+
+### Translating Documentation
+
+1. Create a folder for your language in `docs/` if it doesn't exist. Use the language codes provided [here](https://squidfunk.github.io/mkdocs-material/setup/changing-the-language/#site-language).
+2. Add the code from (1) to the MkDocs configuration (`mkdocs.yml` - `plugins.i18n.languages`).
+3. Copy the files from the English (`en`) folder into the new folder.
+4. Translate then open a pull request.
+
+## Maintenance
+
+### Releases
+
+Example using `v1.9.2`:
+
+1. Create a new branch: `git switch -c release-v1.9.2`
+2. Bump the plugin version in `manifest.json` and `package.json` (following [Semantic Versioning](https://semver.org/spec/v2.0.0.html)).
+ - Semantic Versioning TL;DR, given a version number `MAJOR.MINOR.PATCH`, increment the:
+ - `MAJOR` version when you make incompatible API changes
+ - `MINOR` version when you add functionality in a backwards compatible manner
+ - `PATCH` version when you make backwards compatible bug fixes
+ - If the new version uses new Obsidian APIs, update `minAppVersion` and `versions.json` to reflect this.
+3. Run `pnpm changelog` to update the CHANGELOG.
+4. Commit and push the changes:
+
+ ```bash
+ git add .
+ git commit -m "Bump version to v1.9.2"
+ git push --set-upstream origin release-v1.9.2
+ ```
+
+5. Open and merge the PR into `master`.
+6. Locally, switch back to `master` and pull the changes: `git switch master && git pull`
+7. Create a git tag with the version: `git tag 1.9.2`
+8. Push the tag: `git push --tags`.
You're all set! [This GitHub action](https://github.com/st3v3nmw/obsidian-spaced-repetition/blob/master/.github/workflows/release.yml) should pick it up, create a release, publish it, and update the live documentation.
+
+[^1]: Check the Obsidian Tasks project which has [excellent contribution guidelines](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md).
diff --git a/docs/zh/flashcards.md b/docs/zh/flashcards.md
new file mode 100644
index 00000000..273293f8
--- /dev/null
+++ b/docs/zh/flashcards.md
@@ -0,0 +1,240 @@
+# 卡片
+
+## 新建卡片
+
+[Piotr Wozniak的知识标准化二十守则](https://supermemo.guru/wiki/20_rules_of_knowledge_formulation) 是创建复习卡片时的良好入门指南。
+
+### 单行基础卡片 (Remnote风格)
+
+问题和答案以 `::` 分隔(可在设置中更改)。
+
+```markdown
+这是问题::这是答案
+```
+
+### 单行双向卡片
+
+创建 `正:::反` 与其反向卡片 `反:::正`.
+
+问题和答案以 `:::` 分隔(可在设置中更改)。
+
+```markdown
+这是问题:::这是答案
+```
+
+注意:初次复习时插件会同时展示正向和反向卡片。
+如果打开 **将关联卡片隐藏至下一天?** 将仅展示正向卡片。
+
+### 多行基础卡片
+
+卡片的正反面以 `?` 分隔(可在设置中更改)。
+
+```markdown
+多行卡片的正面
+?
+多行卡片的反面
+```
+
+只要正反面字段都在 `?` 的作用域内,卡片内容可以跨越多行:
+
+```markdown
+顾名思义
+多行卡片的内容
+可以跨越多行
+?
+这也包括
+卡片的反面
+```
+
+### 多行双向卡片
+
+创建 `正??反` 与其反向卡片 `反??正`.
+
+卡片的正反面以 `??` 分隔(可在设置中更改)。
+
+```markdown
+多行卡片的正面
+??
+多行卡片的反面
+```
+
+只要正反面字段都在 `??` 的作用域内,卡片内容可以跨越多行:
+
+```markdown
+顾名思义
+多行卡片的内容
+可以跨越多行
+??
+这也包括
+卡片的反面
+```
+
+注意:其隐藏机制同单行双向卡片
+
+### 填空卡片
+
+你可以轻松使用 `==高亮==` ,`**加粗**` ,或 `{{花括号}}` 创建挖空卡片.
+
+该特性可在设置中开关。
+
+暂不支持 Anki 风格的 `{{c1:This text}} would {{c2:generate}} {{c1:2 cards}}` 挖空语法。该特性正在 [计划中](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/93/)。
+
+## 卡组
+
+![Screenshot from 2021-06-05 19-28-24](https://user-images.githubusercontent.com/43380836/120922211-78603400-c6d0-11eb-9d09-bdd5df1c9112.png)
+
+卡组名称右边的绿色和蓝色数字分别表示到期卡片和新卡片数目。
+
+### 使用 Obsidian 标签
+
+1. 在设置中设定制卡标签 (默认为 `#flashcards`)。
+2. 将您想要制卡的笔记打上该标签。
+
+#### 标签层级
+
+注意 `#flashcards` 可以匹配嵌套标签例如 `#flashcards/subdeck/subdeck`.
+
+#### 单个文件包含多个标签
+
+单一文件中可以包含不同卡组的多个卡片内容。
+
+这是因为一个标签的作用域直到下一个标签出现才会结束。
+
+例如:
+
+```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
+```
+
+#### 多个卡组包含同一卡片
+
+通常情况下一张卡片只会出现在一个卡组。然而某些时候,一张卡片无法被恰当地归入单一卡组的层级结构中。
+
+这种情况下,卡片可以被标记为归属为多个卡组。比如下面这张卡片属于三个卡组。
+
+```markdown
+#flashcards/language/words #flashcards/trivia #flashcards/learned-from-tv
+A group of cats is called a::clowder
+```
+
+注意,在上面的例子中所有标签必须位于一行并以空格隔开。
+
+#### 作用于特定问答的卡片
+
+位于卡片内容同一行开头处的标签是「仅限当前问答」的。
+
+例如:
+
+```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
+```
+
+此处 `Question6` 将出现在 `deckA` 但不会出现于 `deckB` 因 `deckB` 是仅作用于 `Question4` 的标签。
+
+### 使用目录结构
+
+插件将自动遍历目录结构并依次创建卡组和子卡组,例如 `Folder/sub-folder/sub-sub-folder` ⇔ `Deck/sub-deck/sub-sub-deck`。
+
+这是使用标签指定卡组的替代方案,可以在设置中打开。
+
+## 复习
+
+制卡完成后即可在左边栏中点击图标开始复习。当一张卡片被复习后,将会被附上一个包含下一次复习时间、复习间隔和熟练度的HTML注释。
+
+```
+
+```
+
+HTML注释在笔记预览页面中不可见。对于单行卡片,你可以在设置中选择让这个标签位于同一行还是另起一行。放置在同一行可以防止破坏markdown的列表渲染效果。
+
+注意您可以按 `S` 跳过一张卡片(大小写不敏感)。
+
+!!! 提示
+
+ 如果您在移动设备上遇到了悬浮框尺寸的问题,进入设置并将 _Flashcard Height Percentage_ 和 _Flashcard Width Percentage_
+ 设为 100% 以适应屏幕。
+
+### 快速复习
+
+你可以在快速复习中使用如下快捷键:
+
+- `Space/Enter` => 显示答案
+- `0` => 重置进度 (等价于 Anki 中的 `Again`)
+- `1` => 标记为 `Hard`
+- `2` 或 `Space` => 标记为 `Good`
+- `3` => 标记为 `Easy`
+
+### 上下文
+
+如用于制卡的部分位于笔记标题之下,则卡片中会附加一个上下文标题。
+
+例如:
+
+```markdown
+#flashcards
+
+# Trivia
+
+## Capitals
+
+### Africa
+
+Kenya::Nairobi
+
+### North America
+
+Canada::Ottawa
+```
+
+卡片 `Kenya::Nairobi` 将会被附上 `Trivia > Capitals > Africa` 作为上下文标题而卡片 `Canada::Ottawa` 将会被附上 `Trivia > Capitals > North America` 作为上下文标题。
+
+### 删除卡片
+
+要删除一个卡片,只需删除复习规划标签和卡片相关文本。
+
+### 忽略卡片
+
+你可以使用诸如 ` -->` 的HTML标签来将其从复习队列中移除。你可以随时移除该标签。
+
+## 集中复习
+
+当前仅支持使用 集中复习此笔记中的卡片 命令。将复习所有卡组中来自该笔记的卡片。
+
+## 数据统计
+
+统计页面可以使用 `View Statistics` 命令打开。
+
+### 预估
+
+计算将要到期的卡片数量。
+
+
+
+### 复习间隔
+
+统计卡片再次出现的时间间隔。
+
+### 熟练度
+
+统计卡片熟练度。
+
+### 卡片类型
+
+统计卡片类型:新卡片,较新卡片, 熟悉卡片(复习间隔超过一个月)。
diff --git a/docs/zh/index.md b/docs/zh/index.md
new file mode 100644
index 00000000..d27ab167
--- /dev/null
+++ b/docs/zh/index.md
@@ -0,0 +1,53 @@
+# Obsidian Spaced Repetition
+
+
+
+Fight the forgetting curve & note aging by reviewing flashcards & notes using spaced repetition on Obsidian.md
+
+- 阅读 [文档](https://www.stephenmwangi.com/obsidian-spaced-repetition/).
+- 查看新特性和故障修复 [规划](https://github.com/st3v3nmw/obsidian-spaced-repetition/projects/2/)
+- 如果您有新建议或故障报告,请提出 [请求](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/)
+- 访问 [讨论](https://github.com/st3v3nmw/obsidian-spaced-repetition/discussions/) 板块以获取问答帮助,意见反馈
+- 感谢Obsidian社区 😄 的贡献,本插件已支持 _Arabic / العربية, Chinese (Simplified) / 简体中文, Chinese (Traditional) / 繁體中文, Czech / čeština, German / Deutsch, Italian / Italiano, Korean / 한국어, Japanese / 日本語, Polish / Polski, Portuguese (Brazil) / Português do Brasil, Spanish / Español, and Russian / русский_
+ - 如果您愿意提供翻译上的帮助,请阅读 [翻译指南](https://www.stephenmwangi.com/obsidian-spaced-repetition/contributing/#translating_1).
+
+## 视频演示
+
+
+
+## 安装
+
+在Obsidian搜索社区插件-`Spaced Repetition`.
+
+### 手动安装
+
+在您的Obsidian仓库中的 `.obsidian/plugins` 下创建 `obsidian-spaced-repetition` 目录。将 [最新发布的软件包](https://github.com/st3v3nmw/obsidian-spaced-repetition/releases) 解压并移动 `main.js`, `manifest.json` 和 `styles.css` 到该目录下。
+
+## 相关资源
+
+### YouTube 教程
+
+#### 卡片
+
+- [PRODUCTIVELY Learning New Things Using Obsidian by @FromSergio](https://youtu.be/DwSNZEW6jCU)
+
+#### 笔记
+
+##### 渐进式写作
+
+- [Obsidian: inbox review with spaced repetition by @aviskase](https://youtu.be/zG5r7QIY_TM)
+- [Разгребатель инбокса заметок как у Andy Matuschak в Obsidian by @YuliyaBagriy_ru](https://youtu.be/CF6SSHB74cs)
+
+### 间隔重复系统
+
+- [How to Remember Anything Forever-Ish by Nicky Case](https://ncase.me/remember/)
+- [Spaced Repetition for Efficient Learning by Gwern](https://www.gwern.net/Spaced-repetition/)
+- [20 rules of knowledge formulation by Dr. Piotr Wozniak](https://supermemo.guru/wiki/20_rules_of_knowledge_formulation)
+
+### 赞助
+
+
+
+
diff --git a/docs/zh/notes.md b/docs/zh/notes.md
new file mode 100644
index 00000000..fb9ad808
--- /dev/null
+++ b/docs/zh/notes.md
@@ -0,0 +1,71 @@
+# 笔记
+
+- 笔记应当具有原子性:说清楚**一个**概念;
+- 笔记之间应当高度关联;
+- 先理解,后复习;
+- 善用 [费曼学习法](https://fs.blog/2021/02/feynman-learning-technique/)
+
+## 开始使用
+
+为需要复习的笔记添加 `#review` 标签。你可以在插件设置中修改此默认标签(也可以使用多个标签)
+
+## 新笔记
+
+新笔记将展示在右栏的 `新` (复习序列)中,如图:
+
+
+
+## 复习
+
+打开笔记即可复习。在菜单中选择 `复习: 简单`,`复习: 记得` 或 `复习: 较难`。 选择 `简单`,`记得` 还是 `较难` 取决于你对复习材料的理解程度。
+
+
+
+在文件上右击可以调出相同选项:
+
+
+
+笔记将被添加到复习队列中:
+
+
+
+### 快速复习
+
+我们提供快速进入复习模式的命令。你可以在 `设置 -> 快捷键` 中定制快捷键。这可以使您直接开始复习。
+
+### 复习设置
+
+可供定制的选项包括:
+
+- 随机打开笔记或按照优先级排序
+- 在复习完成后是否自动打开下一个笔记
+
+## 复习计划
+
+位于状态栏底部的 `复习: N 卡片已到期` 显示您今天需要复习的卡片数目(今日卡片 + 逾期卡片)。点击可打开一张卡片开始复习。
+
+您也可以使用 `打开一个笔记开始复习` 命令。
+
+## 复习序列
+
+- 每日复习条目将按照优先级排序 (PageRank)
+
+## 渐进式写作
+
+阅读 `@aviskase` 的 [介绍](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/15)
+
+视频资源:
+
+- 英文: [Obsidian: inbox review with spaced repetition](https://youtu.be/zG5r7QIY_TM)
+- 俄文: [Yuliya Bagriy - Разгребатель инбокса заметок как у Andy Matuschak в Obsidian](https://www.youtube.com/watch?v=CF6SSHB74cs)
+
+### 概要
+
+Andy Matuschak 在 [写作素材库中引入间隔重复系统](https://notes.andymatuschak.org/z7iCjRziX6V6unNWL81yc2dJicpRw2Cpp9MfQ).
+
+简而言之,可以进行四种操作 (此处 `x < y`):
+
+- 跳过笔记 (增加 `x` 天的复习间隔) == 标记为 `记得`
+- 已阅,觉得有用 (降低复习间隔) == 标记为 `较难`
+- 已阅,觉得没用 (增加 `y` 天的复习间隔) == 标记为 `简单`
+- 转换为 evergreen 笔记 (中止使用间隔重复系统)
diff --git a/mkdocs.yml b/mkdocs.yml
index 375c0f56..6537cdbd 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -42,6 +42,7 @@ plugins:
docs_structure: folder
languages:
en: English
+ zh: 简体中文
default_language: en
markdown_extensions:
diff --git a/src/NoteFileLoader.ts b/src/NoteFileLoader.ts
index 252e863b..f63e0670 100644
--- a/src/NoteFileLoader.ts
+++ b/src/NoteFileLoader.ts
@@ -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;
@@ -16,7 +17,11 @@ export class NoteFileLoader {
this.settings = settings;
}
- async load(noteFile: ISRFile, folderTopicPath: TopicPath): Promise {
+ async load(
+ noteFile: ISRFile,
+ defaultTextDirection: TextDirection,
+ folderTopicPath: TopicPath,
+ ): Promise {
this.noteFile = noteFile;
const questionParser: NoteQuestionParser = new NoteQuestionParser(this.settings);
@@ -24,6 +29,7 @@ export class NoteFileLoader {
const onlyKeepQuestionsWithTopicPath: boolean = true;
const questionList: Question[] = await questionParser.createQuestionList(
noteFile,
+ defaultTextDirection,
folderTopicPath,
onlyKeepQuestionsWithTopicPath,
);
diff --git a/src/NoteParser.ts b/src/NoteParser.ts
index 15b7fdbe..f6368954 100644
--- a/src/NoteParser.ts
+++ b/src/NoteParser.ts
@@ -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;
@@ -12,9 +13,18 @@ export class NoteParser {
this.settings = settings;
}
- async parse(noteFile: ISRFile, folderTopicPath: TopicPath): Promise {
+ async parse(
+ noteFile: ISRFile,
+ defaultTextDirection: TextDirection,
+ folderTopicPath: TopicPath,
+ ): Promise {
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;
diff --git a/src/NoteQuestionParser.ts b/src/NoteQuestionParser.ts
index c9b545f8..b53c8208 100644
--- a/src/NoteQuestionParser.ts
+++ b/src/NoteQuestionParser.ts
@@ -9,6 +9,7 @@ import { SRSettings, SettingsUtil } from "./settings";
import { ISRFile, frontmatterTagPseudoLineNum } from "./SRFile";
import { TopicPath, TopicPathList } from "./TopicPath";
import { splitNoteIntoFrontmatterAndContent, splitTextIntoLineArray } from "./util/utils";
+import { TextDirection } from "./util/TextDirection";
export class NoteQuestionParser {
settings: SRSettings;
@@ -41,6 +42,7 @@ export class NoteQuestionParser {
async createQuestionList(
noteFile: ISRFile,
+ defaultTextDirection: TextDirection,
folderTopicPath: TopicPath,
onlyKeepQuestionsWithTopicPath: boolean,
): Promise {
@@ -65,8 +67,11 @@ export class NoteQuestionParser {
[this.frontmatterText, this.contentText] = splitNoteIntoFrontmatterAndContent(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,
);
@@ -90,6 +95,7 @@ export class NoteQuestionParser {
private doCreateQuestionList(
noteText: string,
+ textDirection: TextDirection,
folderTopicPath: TopicPath,
tagCacheList: TagCache[],
): Question[] {
@@ -101,7 +107,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(
@@ -148,7 +154,10 @@ 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,
);
@@ -156,6 +165,7 @@ export class NoteQuestionParser {
this.settings,
parsedQuestionInfo,
null, // We haven't worked out the TopicPathList yet
+ textDirection,
questionContext,
);
return result;
diff --git a/src/OsrCore.ts b/src/OsrCore.ts
index 4a804d70..ebdb2ad6 100644
--- a/src/OsrCore.ts
+++ b/src/OsrCore.ts
@@ -18,12 +18,14 @@ import { ReviewResponse } from "./algorithms/base/RepetitionItem";
import { IOsrVaultNoteLinkInfoFinder } from "./algorithms/osr/ObsidianVaultNoteLinkInfoFinder";
import { CardDueDateHistogram, NoteDueDateHistogram } from "./DueDateHistogram";
import { globalDateProvider } from "./util/DateProvider";
+import { TextDirection } from "./util/TextDirection";
export interface IOsrVaultEvents {
dataChanged: () => void;
}
export class OsrCore {
+ public defaultTextDirection: TextDirection;
protected settings: SRSettings;
private dataChangedHandler: () => void;
protected osrNoteGraph: OsrNoteGraph;
@@ -222,7 +224,7 @@ export class OsrCore {
async loadNote(noteFile: ISRFile, topicPath: TopicPath): Promise {
const loader: NoteFileLoader = new NoteFileLoader(this.settings);
- const note: Note = await loader.load(noteFile, topicPath);
+ const note: Note = await loader.load(noteFile, this.defaultTextDirection, topicPath);
if (note.hasChanged) {
await note.writeNoteFile(this.settings);
}
diff --git a/src/Question.ts b/src/Question.ts
index 2c634235..57656017 100644
--- a/src/Question.ts
+++ b/src/Question.ts
@@ -7,6 +7,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 {
@@ -83,6 +84,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
@@ -98,11 +102,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
@@ -113,10 +119,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] {
@@ -245,7 +255,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(
@@ -274,10 +289,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) {
diff --git a/src/SRFile.ts b/src/SRFile.ts
index 067683a6..3d37a8c2 100644
--- a/src/SRFile.ts
+++ b/src/SRFile.ts
@@ -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
@@ -18,6 +19,7 @@ export interface ISRFile {
getAllTagsFromCache(): string[];
getAllTagsFromText(): TagCache[];
getQuestionContext(cardLine: number): string[];
+ getTextDirection(): TextDirection;
read(): Promise;
write(content: string): Promise;
}
@@ -131,6 +133,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 {
return await this.vault.read(this.file);
}
diff --git a/src/gui/EditModal.tsx b/src/gui/EditModal.tsx
index 1feb19d5..35c9a7cd 100644
--- a/src/gui/EditModal.tsx
+++ b/src/gui/EditModal.tsx
@@ -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 {
@@ -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 {
- const newPromptModal = new FlashcardEditModal(app, placeholder);
+ private textDirection: TextDirection;
+
+ public static Prompt(
+ app: App,
+ placeholder: string,
+ textDirection: TextDirection,
+ ): Promise {
+ 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((resolve, reject) => {
this.resolvePromise = resolve;
@@ -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);
}
diff --git a/src/gui/FlashcardModal.tsx b/src/gui/FlashcardModal.tsx
index 9ff4b372..ddadb7d3 100644
--- a/src/gui/FlashcardModal.tsx
+++ b/src/gui/FlashcardModal.tsx
@@ -48,7 +48,9 @@ export class FlashcardModal extends Modal {
// Setup base containers
this.modalEl.style.height = this.settings.flashcardHeightPercentage + "%";
+ this.modalEl.style.maxHeight = this.settings.flashcardHeightPercentage + "%";
this.modalEl.style.width = this.settings.flashcardWidthPercentage + "%";
+ this.modalEl.style.maxWidth = this.settings.flashcardWidthPercentage + "%";
this.modalEl.setAttribute("id", "sr-modal");
this.contentEl.addClass("sr-modal-content");
@@ -118,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);
diff --git a/src/gui/FlashcardReviewView.tsx b/src/gui/FlashcardReviewView.tsx
index a64e57ee..f081d3be 100644
--- a/src/gui/FlashcardReviewView.tsx
+++ b/src/gui/FlashcardReviewView.tsx
@@ -137,7 +137,13 @@ 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;
// Setup response buttons
this._resetResponseButtons();
@@ -291,7 +297,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");
@@ -340,9 +350,17 @@ export class FlashcardReviewView {
private _formatQuestionContextText(questionContext: string[]): string {
const separator: string = " > ";
let result = this._currentNote.file.basename;
- if (questionContext.length > 0) {
- result += separator + questionContext.join(separator);
- }
+ questionContext.forEach((context) => {
+ // Check for links trim [[ ]]
+ if (context.startsWith("[[") && context.endsWith("]]")) {
+ context = context.replace("[[", "").replace("]]", "");
+ // Use replacement text if any
+ if (context.contains("|")) {
+ context = context.split("|")[1];
+ }
+ }
+ result += separator + context;
+ });
return result + separator + "...";
}
diff --git a/src/gui/ReviewQueueListView.ts b/src/gui/ReviewQueueListView.ts
index 09e26ab9..7b1affb3 100644
--- a/src/gui/ReviewQueueListView.ts
+++ b/src/gui/ReviewQueueListView.ts
@@ -54,8 +54,8 @@ export class ReviewQueueListView extends ItemView {
public redraw(): void {
const activeFile: TFile | null = this.app.workspace.getActiveFile();
- const rootEl: HTMLElement = createDiv("nav-folder mod-root");
- const childrenEl: HTMLElement = rootEl.createDiv("nav-folder-children");
+ const rootEl: HTMLElement = createDiv();
+ const childrenEl: HTMLElement = rootEl;
for (const [deckKey, deck] of this.noteReviewQueue.reviewDecks) {
const deckCollapsed = !deck.activeFolders.has(deck.deckName);
@@ -66,7 +66,7 @@ export class ReviewQueueListView extends ItemView {
deckCollapsed,
false,
deck,
- ).getElementsByClassName("nav-folder-children")[0] as HTMLElement;
+ ).getElementsByClassName("tree-item-children")[0] as HTMLElement;
if (deck.newNotes.length > 0) {
const newNotesFolderEl: HTMLElement = this.createRightPaneFolder(
@@ -161,11 +161,11 @@ export class ReviewQueueListView extends ItemView {
hidden: boolean,
deck: NoteReviewDeck,
): HTMLElement {
- const folderEl: HTMLDivElement = parentEl.createDiv("nav-folder");
- const folderTitleEl: HTMLDivElement = folderEl.createDiv("nav-folder-title");
- const childrenEl: HTMLDivElement = folderEl.createDiv("nav-folder-children");
+ const folderEl: HTMLDivElement = parentEl.createDiv("tree-item");
+ const folderTitleEl: HTMLDivElement = folderEl.createDiv("tree-item-self");
+ const childrenEl: HTMLDivElement = folderEl.createDiv("tree-item-children");
const collapseIconEl: HTMLDivElement = folderTitleEl.createDiv(
- "nav-folder-collapse-indicator collapse-icon",
+ "tree-item-collapse-indicator collapse-icon",
);
collapseIconEl.innerHTML = COLLAPSE_ICON;
@@ -173,7 +173,7 @@ export class ReviewQueueListView extends ItemView {
(collapseIconEl.childNodes[0] as HTMLElement).style.transform = "rotate(-90deg)";
}
- folderTitleEl.createDiv("nav-folder-title-content").setText(folderTitle);
+ folderTitleEl.createDiv("tree-item-content").setText(folderTitle);
if (hidden) {
folderEl.style.display = "none";
@@ -205,18 +205,18 @@ export class ReviewQueueListView extends ItemView {
deck: NoteReviewDeck,
): void {
const navFileEl: HTMLElement = folderEl
- .getElementsByClassName("nav-folder-children")[0]
- .createDiv("nav-file");
+ .getElementsByClassName("tree-item-children")[0]
+ .createDiv("tree-item");
if (hidden) {
navFileEl.style.display = "none";
}
- const navFileTitle: HTMLElement = navFileEl.createDiv("nav-file-title");
+ const navFileTitle: HTMLElement = navFileEl.createDiv("tree-item-self");
if (fileElActive) {
navFileTitle.addClass("is-active");
}
- navFileTitle.createDiv("nav-file-title-content").setText(file.basename);
+ navFileTitle.createDiv("tree-item-content").setText(file.basename);
navFileTitle.addEventListener(
"click",
async (event: MouseEvent) => {
@@ -244,7 +244,7 @@ export class ReviewQueueListView extends ItemView {
}
private changeFolderIconToExpanded(folderEl: HTMLElement): void {
- const collapseIconEl = folderEl.find("div.nav-folder-collapse-indicator");
+ const collapseIconEl = folderEl.find("div.tree-item-collapse-indicator");
(collapseIconEl.childNodes[0] as HTMLElement).style.transform = "";
}
}
diff --git a/src/main.ts b/src/main.ts
index f303a749..fe4df048 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -27,6 +27,8 @@ import { Note } from "./Note";
import { NoteFileLoader } from "./NoteFileLoader";
import { ISRFile, SrTFile as SrTFile } from "./SRFile";
import { QuestionPostponementList } from "./QuestionPostponementList";
+import { TextDirection } from "./util/TextDirection";
+import { convertToStringOrEmpty } from "./util/utils";
import { ReviewResponse } from "./algorithms/base/RepetitionItem";
import { SrsAlgorithm } from "./algorithms/base/SrsAlgorithm";
import { DataStore } from "./dataStore/base/DataStore";
@@ -50,7 +52,7 @@ export default class SRPlugin extends Plugin {
private nextNoteReviewHandler: NextNoteReviewHandler;
async onload(): Promise {
- console.log("onload: Branch: feat-878-support-multiple-sched, Date: 2024-07-16");
+ console.log("onload: Branch: feat-878-support-multiple-sched, Date: 2024-07-24");
await this.loadPluginData();
this.initLogicClasses();
@@ -350,6 +352,7 @@ export default class SRPlugin extends Plugin {
}
const now = window.moment(Date.now());
+ this.osrAppCore.defaultTextDirection = this.getObsidianRtlSetting();
await this.osrAppCore.loadVault();
@@ -386,13 +389,24 @@ export default class SRPlugin extends Plugin {
this.data.settings,
);
- const note: Note = await loader.load(this.createSrTFile(noteFile), folderTopicPath);
+ const note: Note = await loader.load(
+ this.createSrTFile(noteFile),
+ this.getObsidianRtlSetting(),
+ folderTopicPath,
+ );
if (note.hasChanged) {
note.writeNoteFile(this.data.settings);
}
return note;
}
+ private getObsidianRtlSetting(): TextDirection {
+ // Get the direction with Obsidian's own setting
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const v: any = (this.app.vault as any).getConfig("rightToLeft");
+ return convertToStringOrEmpty(v) == "true" ? TextDirection.Rtl : TextDirection.Ltr;
+ }
+
async saveNoteReviewResponse(note: TFile, response: ReviewResponse): Promise {
const noteSrTFile: ISRFile = this.createSrTFile(note);
diff --git a/src/util/RenderMarkdownWrapper.ts b/src/util/RenderMarkdownWrapper.ts
index 8b82f763..8422b8c6 100644
--- a/src/util/RenderMarkdownWrapper.ts
+++ b/src/util/RenderMarkdownWrapper.ts
@@ -6,6 +6,7 @@ import {
NON_LETTER_SYMBOLS_REGEX,
} from "../constants";
import SRPlugin from "../main";
+import { TextDirection } from "./TextDirection";
export class RenderMarkdownWrapper {
private app: App;
@@ -23,13 +24,19 @@ export class RenderMarkdownWrapper {
async renderMarkdownWrapper(
markdownString: string,
containerEl: HTMLElement,
+ textDirection: TextDirection,
recursiveDepth = 0,
): Promise {
if (recursiveDepth > 4) return;
- MarkdownRenderer.renderMarkdown(markdownString, containerEl, this.notePath, this.plugin);
+ let el: HTMLElement;
+ if (textDirection == TextDirection.Rtl) {
+ el = containerEl.createDiv();
+ el.setAttribute("dir", "rtl");
+ } else el = containerEl;
+ MarkdownRenderer.render(this.app, markdownString, el, this.notePath, this.plugin);
- containerEl.findAll(".internal-embed").forEach((el) => {
+ el.findAll(".internal-embed").forEach((el) => {
const link = this.parseLink(el.getAttribute("src"));
// file does not exist, display dead link
@@ -145,6 +152,9 @@ export class RenderMarkdownWrapper {
blockText = text;
}
- this.renderMarkdownWrapper(blockText, el, recursiveDepth + 1);
+ // We are operating here within the parent container.
+ // It already has the rtl div if necessary.
+ // We don't need another rtl div, so we can set direction to Unspecified
+ this.renderMarkdownWrapper(blockText, el, TextDirection.Unspecified, recursiveDepth + 1);
}
}
diff --git a/src/util/TextDirection.ts b/src/util/TextDirection.ts
new file mode 100644
index 00000000..a49807cf
--- /dev/null
+++ b/src/util/TextDirection.ts
@@ -0,0 +1,5 @@
+export enum TextDirection {
+ Unspecified,
+ Ltr,
+ Rtl,
+}
diff --git a/src/util/utils.ts b/src/util/utils.ts
index 5a2d32c6..dfa413cd 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -1,6 +1,7 @@
import moment from "moment";
import { Moment } from "moment";
-import { PREFERRED_DATE_FORMAT, YAML_FRONT_MATTER_REGEX } from "src/constants";
+import { normalize, sep } from "path";
+import { PREFERRED_DATE_FORMAT } from "src/constants";
type Hex = number;
@@ -96,6 +97,47 @@ export function stringTrimStart(str: string): [string, string] {
return [ws, trimmed];
}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function convertToStringOrEmpty(v: any): string {
+ let result: string = "";
+ if (v != null && v != undefined) {
+ result = v + "";
+ }
+ return result;
+}
+
+/**
+ * Checks a path is equal or a subpath of the other rootPath
+ *
+ * @param toCheck The path to check it is equal or a subpath of path.
+ * @param rootPath The ref path to check the other is equal to or a subpath of this.
+ * @tutorial
+ * rootPath = "root/sub/sub2"
+ * if toCheck = "notRoot/..." -> false
+ * if toCheck = "root" -> true
+ * if toCheck = "root/sub" -> true
+ * if toCheck = "root/s" -> false
+ */
+export function isEqualOrSubPath(toCheck: string, rootPath: string): boolean {
+ const rootPathSections = normalize(rootPath.toLowerCase())
+ .replaceAll(/(\\|\/)/g, sep)
+ .split(sep)
+ .filter((p) => p !== "");
+ const pathSections = normalize(toCheck.toLowerCase())
+ .replaceAll(/(\\|\/)/g, sep)
+ .split(sep)
+ .filter((p) => p !== "");
+ if (pathSections.length < rootPathSections.length) {
+ return false;
+ }
+ for (let i = 0; i < rootPathSections.length; i++) {
+ if (rootPathSections[i] !== pathSections[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
//
// This returns [frontmatter, content]
//
@@ -109,32 +151,32 @@ export function stringTrimStart(str: string): [string, string] {
// e.g. for calls to getQuestionContext(cardLine: number)
//
export function splitNoteIntoFrontmatterAndContent(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;
- }
+ const lines = splitTextIntoLineArray(str);
+ let lineIndex = 0;
+ let hasFrontmatter = false;
+ do {
+ // Starts file with '---'
+ if (lineIndex === 0 && lines[lineIndex] === "---") {
+ hasFrontmatter = true;
}
-
- if (frontmatterEndLineNum) {
- const frontmatterStartLineNum: number = 0;
- const frontmatterLines: string[] = [];
- for (let i = frontmatterStartLineNum; i <= frontmatterEndLineNum; i++) {
- frontmatterLines.push(lines[i]);
- lines[i] = "";
- }
- frontmatter = frontmatterLines.join("\n");
- content = lines.join("\n");
+ // Line is end of front matter
+ else if (hasFrontmatter && lines[lineIndex] === "---") {
+ hasFrontmatter = false;
+ lineIndex++;
+ }
+ if (hasFrontmatter) {
+ lineIndex++;
}
+ } while (hasFrontmatter && lineIndex < lines.length);
+ // No end of Frontmatter found
+ if (hasFrontmatter) {
+ lineIndex = 0;
}
- if (frontmatter.length == 0) content = str;
+
+ const frontmatter: string = lines.slice(0, lineIndex).join("\n");
+ const emptyLines: string[] = lineIndex > 0 ? Array(lineIndex).join(".").split(".") : [];
+ const content: string = emptyLines.concat(lines.slice(lineIndex)).join("\n");
+
return [frontmatter, content];
}
diff --git a/styles.css b/styles.css
index af8e7a1d..f532c01c 100644
--- a/styles.css
+++ b/styles.css
@@ -1,8 +1,40 @@
-.is-mobile #sr-modal {
- --top-space: calc(var(--safe-area-inset-top) + var(--header-height) + var(--size-4-2));
- width: 100vw !important;
- height: calc(100vh - var(--top-space)) !important;
- margin-top: var(--top-space);
+@media only screen and (orientation: landscape) {
+ .is-mobile .sr-flashcard {
+ flex-direction: row;
+ }
+
+ .is-mobile .sr-header {
+ flex-direction: column;
+ flex: 0 1 0;
+ }
+
+ .is-mobile .sr-content {
+ flex: 1 0 0;
+ }
+
+ .is-mobile .sr-response {
+ flex-direction: column;
+ flex: 0 1 0;
+ }
+
+ .is-mobile .sr-controls {
+ flex-direction: column;
+ }
+
+ .is-mobile .sr-title {
+ display: none;
+ }
+
+ .is-mobile .sr-response-button {
+ writing-mode: vertical-lr;
+ }
+}
+
+#sr-modal {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
}
#sr-modal .modal-title {
diff --git a/tests/unit/Note.test.ts b/tests/unit/Note.test.ts
index 8c3abed4..984999a4 100644
--- a/tests/unit/Note.test.ts
+++ b/tests/unit/Note.test.ts
@@ -5,6 +5,7 @@ import { Note } from "src/Note";
import { Question } from "src/Question";
import { DEFAULT_SETTINGS } from "src/settings";
import { NoteFileLoader } from "src/NoteFileLoader";
+import { TextDirection } from "src/util/TextDirection";
import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
import { NoteEaseList } from "src/NoteEaseList";
import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
@@ -25,7 +26,7 @@ Q3::A3
`;
let file: UnitTestSRFile = new UnitTestSRFile(noteText);
let folderTopicPath = TopicPath.emptyPath;
- let note: Note = await parser.parse(file, folderTopicPath);
+ let note: Note = await parser.parse(file, TextDirection.Ltr, folderTopicPath);
let deck: Deck = Deck.emptyDeck;
note.appendCardsToDeck(deck);
let subdeck: Deck = deck.getDeck(new TopicPath(["flashcards", "test"]));
@@ -43,7 +44,7 @@ Q3:::A3
`;
let file: UnitTestSRFile = new UnitTestSRFile(noteText);
let folderTopicPath = TopicPath.emptyPath;
- let note: Note = await parser.parse(file, folderTopicPath);
+ let note: Note = await parser.parse(file, TextDirection.Ltr, folderTopicPath);
let deck: Deck = Deck.emptyDeck;
note.appendCardsToDeck(deck);
let subdeck: Deck = deck.getDeck(new TopicPath(["flashcards", "test"]));
@@ -65,7 +66,7 @@ Q3:::A3
`;
let file: UnitTestSRFile = new UnitTestSRFile(originalText);
- let note: Note = await noteFileLoader.load(file, TopicPath.emptyPath);
+ let note: Note = await noteFileLoader.load(file, TextDirection.Ltr, TopicPath.emptyPath);
await note.writeNoteFile(DEFAULT_SETTINGS);
let updatedText: string = file.content;
diff --git a/tests/unit/NoteFileLoader.test.ts b/tests/unit/NoteFileLoader.test.ts
index b9a646d3..83dbd2d0 100644
--- a/tests/unit/NoteFileLoader.test.ts
+++ b/tests/unit/NoteFileLoader.test.ts
@@ -2,6 +2,7 @@ import { Note } from "src/Note";
import { NoteFileLoader } from "src/NoteFileLoader";
import { TopicPath } from "src/TopicPath";
import { DEFAULT_SETTINGS } from "src/settings";
+import { TextDirection } from "src/util/TextDirection";
import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
import { NoteEaseList } from "src/NoteEaseList";
import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
@@ -22,7 +23,7 @@ Q3:::A3
`;
let file: UnitTestSRFile = new UnitTestSRFile(noteText);
- let note: Note = await noteFileLoader.load(file, TopicPath.emptyPath);
+ let note: Note = await noteFileLoader.load(file, TextDirection.Ltr, TopicPath.emptyPath);
expect(note.hasChanged).toEqual(false);
});
@@ -35,7 +36,7 @@ Q3:::A3
`;
let file: UnitTestSRFile = new UnitTestSRFile(noteText);
- let note: Note = await noteFileLoader.load(file, TopicPath.emptyPath);
+ let note: Note = await noteFileLoader.load(file, TextDirection.Ltr, TopicPath.emptyPath);
expect(note.hasChanged).toEqual(true);
});
});
diff --git a/tests/unit/NoteParser.test.ts b/tests/unit/NoteParser.test.ts
index a867f9df..5808e837 100644
--- a/tests/unit/NoteParser.test.ts
+++ b/tests/unit/NoteParser.test.ts
@@ -4,6 +4,7 @@ import { Note } from "src/Note";
import { Question } from "src/Question";
import { DEFAULT_SETTINGS } from "src/settings";
import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
+import { TextDirection } from "src/util/TextDirection";
import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
import { NoteEaseList } from "src/NoteEaseList";
@@ -24,7 +25,7 @@ Q3::A3
`;
let file: UnitTestSRFile = new UnitTestSRFile(noteText);
let folderTopicPath = TopicPath.emptyPath;
- let note: Note = await parser.parse(file, folderTopicPath);
+ let note: Note = await parser.parse(file, TextDirection.Ltr, folderTopicPath);
let questionList = note.questionList;
expect(questionList.length).toEqual(3);
});
diff --git a/tests/unit/NoteQuestionParser.test.ts b/tests/unit/NoteQuestionParser.test.ts
index 6efdd918..6edb3770 100644
--- a/tests/unit/NoteQuestionParser.test.ts
+++ b/tests/unit/NoteQuestionParser.test.ts
@@ -6,6 +6,7 @@ import { TopicPath, TopicPathList } from "src/TopicPath";
import { createTest_NoteQuestionParser } from "./SampleItems";
import { ISRFile, frontmatterTagPseudoLineNum } from "src/SRFile";
import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
+import { TextDirection } from "src/util/TextDirection";
import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/RepItemScheduleInfo_Osr";
@@ -32,7 +33,12 @@ describe("No flashcard questions", () => {
let noteFile: ISRFile = new UnitTestSRFile(noteText);
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toEqual([]);
});
@@ -42,7 +48,12 @@ describe("No flashcard questions", () => {
let noteFile: ISRFile = new UnitTestSRFile(noteText);
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toEqual([]);
});
});
@@ -74,7 +85,12 @@ A::B
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -110,7 +126,12 @@ A::B
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -132,7 +153,12 @@ A::B
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -161,7 +187,12 @@ In computer-science, a *heap* is a tree-based data-structure, that satisfies the
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
});
@@ -200,7 +231,12 @@ A::B ^d7cee0
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -243,7 +279,12 @@ A::B ^d7cee0
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -283,7 +324,12 @@ A::B ^d7cee0
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -322,7 +368,12 @@ A::B ^d7cee0
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
});
@@ -337,6 +388,7 @@ Q2::A2
let folderTopicPath: TopicPath = TopicPath.emptyPath;
let questionList: Question[] = await parser_ConvertFoldersToDecks.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -354,6 +406,7 @@ Q3::A3
let folderTopicPath: TopicPath = new TopicPath(["flashcards", "science"]);
let questionList: Question[] = await parser_ConvertFoldersToDecks.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -380,6 +433,7 @@ Q3::A3
let folderTopicPath: TopicPath = TopicPath.emptyPath;
let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -409,6 +463,7 @@ Multiline answer2
let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
noteFile,
+ TextDirection.Ltr,
TopicPath.emptyPath,
true,
);
@@ -455,6 +510,7 @@ describe("Handling tags within note", () => {
let folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]);
let questionList: Question[] = await parser2.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -472,6 +528,7 @@ Q1::A1
let folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]);
let questionList: Question[] = await parser2.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -491,6 +548,7 @@ Q1::A1
let folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]);
let questionList: Question[] = await parser2.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -514,6 +572,7 @@ Q1::A1
let folderTopicPath: TopicPath = TopicPath.emptyPath;
let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -551,7 +610,12 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -566,6 +630,7 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу
let folderTopicPath: TopicPath = TopicPath.emptyPath;
let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -588,6 +653,7 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу
let folderTopicPath: TopicPath = TopicPath.emptyPath;
let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -609,6 +675,7 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу
let folderTopicPath: TopicPath = TopicPath.emptyPath;
let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -632,6 +699,7 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу
let folderTopicPath: TopicPath = TopicPath.emptyPath;
let questionList: Question[] = await parserWithDefaultSettings.createQuestionList(
noteFile,
+ TextDirection.Ltr,
folderTopicPath,
true,
);
@@ -673,7 +741,12 @@ What year was the Taliban Emirate founded?::1996 #flashcards
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
});
@@ -710,7 +783,12 @@ In computer-science, a *heap* is a tree-based data-structure, that satisfies the
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -741,7 +819,12 @@ In computer-science, a *heap* is a tree-based data-structure, that satisfies the
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
@@ -781,7 +864,12 @@ A::B
},
];
expect(
- await parserWithDefaultSettings.createQuestionList(noteFile, folderTopicPath, true),
+ await parserWithDefaultSettings.createQuestionList(
+ noteFile,
+ TextDirection.Ltr,
+ folderTopicPath,
+ true,
+ ),
).toMatchObject(expected);
});
});
diff --git a/tests/unit/Question.test.ts b/tests/unit/Question.test.ts
index 8ba2928f..e6c65c9b 100644
--- a/tests/unit/Question.test.ts
+++ b/tests/unit/Question.test.ts
@@ -1,6 +1,7 @@
import { TopicPath } from "src/TopicPath";
import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
import { Question, QuestionText } from "src/Question";
+import { TextDirection } from "src/util/TextDirection";
let settings_cardCommentOnSameLine: SRSettings = { ...DEFAULT_SETTINGS };
settings_cardCommentOnSameLine.cardCommentOnSameLine = true;
@@ -13,7 +14,7 @@ describe("Question", () => {
"```\nprint('Hello World!')\nprint('Howdy?')\nlambda x: x[0]\n```";
let question: Question = new Question({
- questionText: new QuestionText(text, null, text, null),
+ questionText: new QuestionText(text, null, text, TextDirection.Ltr, null),
});
expect(question.getHtmlCommentSeparator(DEFAULT_SETTINGS)).toEqual("\n");
@@ -24,7 +25,7 @@ describe("Question", () => {
let text: string = "Q1::A1";
let question: Question = new Question({
- questionText: new QuestionText(text, null, text, null),
+ questionText: new QuestionText(text, null, text, TextDirection.Ltr, null),
});
expect(question.getHtmlCommentSeparator(DEFAULT_SETTINGS)).toEqual("\n");
diff --git a/tests/unit/SampleItems.ts b/tests/unit/SampleItems.ts
index da071b5b..b928032c 100644
--- a/tests/unit/SampleItems.ts
+++ b/tests/unit/SampleItems.ts
@@ -7,6 +7,7 @@ import { CardType, Question } from "src/Question";
import { CardFrontBack, CardFrontBackUtil } from "src/QuestionType";
import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
import { TopicPath } from "src/TopicPath";
+import { TextDirection } from "src/util/TextDirection";
import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
import { CardOrder, DeckOrder, DeckTreeIterator } from "src/DeckTreeIterator";
@@ -70,7 +71,7 @@ Q3::A3`;
): Promise {
let deck: Deck = new Deck("Root", null);
let noteParser: NoteParser = createTest_NoteParser();
- let note: Note = await noteParser.parse(file, folderTopicPath);
+ let note: Note = await noteParser.parse(file, TextDirection.Ltr, folderTopicPath);
note.appendCardsToDeck(deck);
return deck;
}
diff --git a/tests/unit/helpers/UnitTestSRFile.ts b/tests/unit/helpers/UnitTestSRFile.ts
index af404ff5..3c8af9f0 100644
--- a/tests/unit/helpers/UnitTestSRFile.ts
+++ b/tests/unit/helpers/UnitTestSRFile.ts
@@ -1,11 +1,11 @@
import * as fs from "fs";
import {
unitTest_BasicFrontmatterParser,
- unitTest_BasicFrontmatterParserEx,
unitTest_GetAllTagsFromTextEx,
} from "./UnitTestHelper";
import { TFile, TagCache } from "obsidian";
import { ISRFile } from "src/SRFile";
+import { TextDirection } from "src/util/TextDirection";
export class UnitTestSRFile implements ISRFile {
content: string;
@@ -45,6 +45,10 @@ export class UnitTestSRFile implements ISRFile {
return [];
}
+ getTextDirection(): TextDirection {
+ return TextDirection.Unspecified;
+ }
+
async read(): Promise {
return this.content;
}
diff --git a/tests/unit/util/utils.test.ts b/tests/unit/util/utils.test.ts
index 9424617a..f39931e1 100644
--- a/tests/unit/util/utils.test.ts
+++ b/tests/unit/util/utils.test.ts
@@ -1,8 +1,10 @@
import { YAML_FRONT_MATTER_REGEX } from "src/constants";
import {
- splitNoteIntoFrontmatterAndContent,
+ convertToStringOrEmpty,
findLineIndexOfSearchStringIgnoringWs,
+ isEqualOrSubPath,
literalStringReplace,
+ splitNoteIntoFrontmatterAndContent,
} from "src/util/utils";
describe("literalStringReplace", () => {
@@ -78,6 +80,28 @@ $$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$
});
});
+describe("convertToStringOrEmpty", () => {
+ test("undefined returns empty string", () => {
+ expect(convertToStringOrEmpty(undefined)).toEqual("");
+ });
+
+ test("null returns empty string", () => {
+ expect(convertToStringOrEmpty(null)).toEqual("");
+ });
+
+ test("empty string returns empty string", () => {
+ expect(convertToStringOrEmpty("")).toEqual("");
+ });
+
+ test("string returned unchanged", () => {
+ expect(convertToStringOrEmpty("Hello")).toEqual("Hello");
+ });
+
+ test("number is converted to string", () => {
+ expect(convertToStringOrEmpty(5)).toEqual("5");
+ });
+});
+
function createTestStr1(sep: string): string {
return `---${sep}sr-due: 2024-08-10${sep}sr-interval: 273${sep}sr-ease: 309${sep}---`;
}
@@ -177,6 +201,176 @@ ${content}`;
${content}`;
expect(c).toEqual(expectedContent);
});
+
+ test("With frontmatter and content (Horizontal line)", () => {
+ const frontmatter: string = `---
+sr-due: 2024-01-17
+sr-interval: 16
+sr-ease: 278
+tags:
+ - flashcards/aws
+ - flashcards/datascience
+---`;
+ const frontmatterBlankedOut: string = `
+
+
+
+
+
+
+`;
+ const content: string = `#flashcards/science/chemistry
+
+
+---
+# Questions
+---
+
+
+Chemistry Question from file underelephant 4A::goodby
+
+
+
+Chemistry Question from file underdog 4B::goodby
+
+
+
+---
+
+Chemistry Question from file underdog 4C::goodby
+
+
+
+This single {{question}} turns into {{3 separate}} {{cards}}
+
+
+
+---`;
+
+ const text: string = `${frontmatter}
+${content}`;
+ const expectedContent: string = `${frontmatterBlankedOut}
+${content}`;
+
+ const [f, c] = splitNoteIntoFrontmatterAndContent(text);
+ expect(f).toEqual(frontmatter);
+ expect(c).toEqual(expectedContent);
+ });
+
+ test("With frontmatter and content (Horizontal line newLine)", () => {
+ const frontmatter: string = `---
+sr-due: 2024-01-17
+sr-interval: 16
+sr-ease: 278
+tags:
+ - flashcards/aws
+ - flashcards/datascience
+---`;
+ const frontmatterBlankedOut: string = `
+
+
+
+
+
+
+`;
+ const content: string = `#flashcards/science/chemistry
+
+
+---
+# Questions
+---
+
+
+Chemistry Question from file underelephant 4A::goodby
+
+
+
+Chemistry Question from file underdog 4B::goodby
+
+
+
+---
+
+Chemistry Question from file underdog 4C::goodby
+
+
+
+This single {{question}} turns into {{3 separate}} {{cards}}
+
+
+
+---
+`;
+
+ const text: string = `${frontmatter}
+${content}`;
+ const expectedContent: string = `${frontmatterBlankedOut}
+${content}`;
+
+ const [f, c] = splitNoteIntoFrontmatterAndContent(text);
+ expect(f).toEqual(frontmatter);
+ expect(c).toEqual(expectedContent);
+ });
+
+ test("With frontmatter and content (Horizontal line codeblock)", () => {
+ const frontmatter: string = `---
+sr-due: 2024-01-17
+sr-interval: 16
+sr-ease: 278
+tags:
+ - flashcards/aws
+ - flashcards/datascience
+---`;
+ const frontmatterBlankedOut: string = `
+
+
+
+
+
+
+`;
+ const content: string = [
+ "```",
+ "---",
+ "```",
+ "#flashcards/science/chemistry",
+ "# Questions",
+ " ",
+ "",
+ "Chemistry Question from file underelephant 4A::goodby",
+ "",
+ "",
+ "",
+ "Chemistry Question from file underdog 4B::goodby",
+ "",
+ "",
+ "```",
+ "---",
+ "```",
+ "",
+ "Chemistry Question from file underdog 4C::goodby",
+ "",
+ "",
+ "",
+ "This single {{question}} turns into {{3 separate}} {{cards}}",
+ "",
+ "",
+ "",
+ "```",
+ "---",
+ "```",
+ ].join("\n");
+
+ const text: string = `${frontmatter}
+${content}`;
+ const expectedContent: string = `${frontmatterBlankedOut}
+${content}`;
+
+ const [f, c] = splitNoteIntoFrontmatterAndContent(text);
+ expect(f).toEqual(frontmatter);
+ expect(c).toEqual(expectedContent);
+ });
});
describe("findLineIndexOfSearchStringIgnoringWs", () => {
@@ -243,3 +437,113 @@ describe("findLineIndexOfSearchStringIgnoringWs", () => {
expect(findLineIndexOfSearchStringIgnoringWs(lines, "??")).toEqual(2);
});
});
+
+describe("isEqualOrSubPath", () => {
+ const winSep = "\\";
+ const linSep = "/";
+ const root = "root";
+ const sub_1 = "plugins";
+ const sub_2 = "obsidian-spaced-repetition";
+ const sub_3 = "data";
+ const noMatch = "notRoot";
+ const caseMatch = "Root";
+
+ describe("Windows", () => {
+ const sep = winSep;
+ const rootPath = root + sep + sub_1;
+
+ test("Upper and lower case letters", () => {
+ expect(isEqualOrSubPath(caseMatch, root)).toBe(true);
+ expect(isEqualOrSubPath(caseMatch.toUpperCase(), root)).toBe(true);
+ });
+
+ test("Seperator auto correction", () => {
+ expect(isEqualOrSubPath(root + winSep + sub_1, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + winSep + sub_1 + winSep, rootPath)).toBe(true);
+
+ expect(isEqualOrSubPath(root + linSep + sub_1, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + linSep + sub_1 + linSep, rootPath)).toBe(true);
+ });
+
+ test("Differnent path", () => {
+ expect(isEqualOrSubPath(noMatch, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(noMatch + sep, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(noMatch + sep + sub_1, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(noMatch + sep + sub_1 + sep + sub_2, rootPath)).toBe(false);
+ });
+
+ test("Partially Match path", () => {
+ expect(isEqualOrSubPath("roo", rootPath)).toBe(false);
+ expect(isEqualOrSubPath("roo" + sep, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(root + sep + "plug", rootPath)).toBe(false);
+ expect(isEqualOrSubPath(root + sep + "plug" + sep, rootPath)).toBe(false);
+ });
+
+ test("Same path", () => {
+ expect(isEqualOrSubPath(rootPath, rootPath)).toBe(true);
+ });
+
+ test("Subpath", () => {
+ expect(isEqualOrSubPath(root, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(root + sep, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(root + sep + sub_1, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(rootPath, rootPath + sep)).toBe(true);
+ expect(isEqualOrSubPath(rootPath + sep, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + sep + sub_1 + sep, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + sep + sub_1 + sep + sub_2, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + sep + sub_1 + sep + sub_2 + sep, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + sep + sub_1 + sep + sub_2 + sep + sub_3, rootPath)).toBe(
+ true,
+ );
+ });
+ });
+ describe("Linux", () => {
+ const sep = linSep;
+ const rootPath = root + sep + sub_1;
+
+ test("Upper and lower case letters", () => {
+ expect(isEqualOrSubPath(caseMatch, root)).toBe(true);
+ expect(isEqualOrSubPath(caseMatch.toUpperCase(), root)).toBe(true);
+ });
+
+ test("Seperator auto correction", () => {
+ expect(isEqualOrSubPath(root + winSep + sub_1, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + winSep + sub_1 + winSep, rootPath)).toBe(true);
+
+ expect(isEqualOrSubPath(root + linSep + sub_1, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + linSep + sub_1 + linSep, rootPath)).toBe(true);
+ });
+
+ test("Differnent path", () => {
+ expect(isEqualOrSubPath(noMatch, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(noMatch + sep, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(noMatch + sep + sub_1, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(noMatch + sep + sub_1 + sep + sub_2, rootPath)).toBe(false);
+ });
+
+ test("Partially Match path", () => {
+ expect(isEqualOrSubPath("roo", rootPath)).toBe(false);
+ expect(isEqualOrSubPath("roo" + sep, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(root + sep + "plug", rootPath)).toBe(false);
+ expect(isEqualOrSubPath(root + sep + "plug" + sep, rootPath)).toBe(false);
+ });
+
+ test("Same path", () => {
+ expect(isEqualOrSubPath(rootPath, rootPath)).toBe(true);
+ });
+
+ test("Subpath", () => {
+ expect(isEqualOrSubPath(root, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(root + sep, rootPath)).toBe(false);
+ expect(isEqualOrSubPath(root + sep + sub_1, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(rootPath, rootPath + sep)).toBe(true);
+ expect(isEqualOrSubPath(rootPath + sep, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + sep + sub_1 + sep, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + sep + sub_1 + sep + sub_2, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + sep + sub_1 + sep + sub_2 + sep, rootPath)).toBe(true);
+ expect(isEqualOrSubPath(root + sep + sub_1 + sep + sub_2 + sep + sub_3, rootPath)).toBe(
+ true,
+ );
+ });
+ });
+});