Repository: st3v3nmw/obsidian-spaced-repetition (ts)

Issue:
[BUG] Folder ignore sorts all folder starting with string
**Describe the bug**
I recently discovered that if you have brunche of  folders there are starting with same text e.g. ``Test-1`` ``Test-2`` and you put ``Test-`` to ignore list both get ignored.

**To Reproduce**
Steps to reproduce the behavior:

1. Create 2 folder ``./Test-1`` ``./Test-2``
2. Create in each a file with test flashcard
3. Add to folder ignore list ``Test-``
4. See error

**Expected behavior**
Only exact match of the name should be ignored

**Versions (please complete the following information):**

-   OS: Win10
-   Obsidian version: 1.5.12
-   Plugin version: 1.12.4
-   If on desktop, Installer version: 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/1.4.16 Chrome/114.0.5735.289 Electron/25.8.1 Safari/537.36

**Additional context**
[TestFolders.zip](https://github.com/st3v3nmw/obsidian-spaced-repetition/files/15421853/Test.zip)


Pull request:
[FIX] Folder ignore sorts all folder starting with string
Fixes #969 

Implementation of better comparison 
Implementation of Unit Test 

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index b8afae5..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-name: Bug report
-about: Create a bug report to help us improve the plugin.
-title: "[BUG]"
-labels: bug
-assignees: ""
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain the bug. This could be the logs in the developer console (`CTRL` + `SHIFT` + `i`).
-
-**Versions (please complete the following information):**
-
--   OS: [e.g. iOS]
--   Obsidian version: [e.g. v0.12.4]
--   Plugin version: [e.g. v1.4.9]
--   If on desktop, Installer version: [run `this.navigator.appVersion` on the developer console, e.g. `5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/0.12.3 Chrome/89.0.4389.128 Electron/12.0.6 Safari/537.36"`]
-
-**Additional context**
-Add any other context about the problem here (e.g. the markdown producing the error).
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..c5fefeb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,91 @@
+name: Bug report
+description: Create a bug report to help us improve the plugin.
+title: "[Bug]: "
+labels: ["bug"]
+
+body:
+    - type: textarea
+      id: bug-description
+      attributes:
+          label: Describe the bug
+          description: A clear and concise description of what the bug is.
+          placeholder: Tell us what happened that you didn't expect [...]
+      validations:
+          required: true
+    - type: textarea
+      id: bug-reproduce-steps
+      attributes:
+          label: To Reproduce
+          description: Steps to reproduce the behavior
+          placeholder: |
+              1. Go to '...'
+              2. Click on '...' 
+              3. Scroll down to '...' 
+              4. See error"
+      validations:
+          required: true
+    - type: textarea
+      id: expected-behavior
+      attributes:
+          label: Expected behavior
+          description: A clear and concise description of what you expected to happen.
+          placeholder: Tell us what you think should have happened...
+      validations:
+          required: true
+    - type: textarea
+      id: screenshots
+      attributes:
+          label: Screenshots
+          description: If applicable, add screenshots to help explain the bug. This could be the logs in the developer console (`CTRL` + `SHIFT` + `i`).
+          placeholder: Paste image here
+      validations:
+          required: false
+    - type: input
+      id: operating-system
+      attributes:
+          label: OS of your device
+          placeholder: e.g. Win10
+      validations:
+          required: true
+    - type: input
+      id: obsidian-version
+      attributes:
+          label: Obsidian version
+          placeholder: e.g. v0.12.4
+      validations:
+          required: true
+    - type: input
+      id: plugin-version
+      attributes:
+          label: Plugin version
+          placeholder: "e.g. v1.4.9"
+      validations:
+          required: true
+    - type: input
+      id: installer-version
+      attributes:
+          label: Installer version
+          description: If on desktop [run `this.navigator.appVersion` on the developer console, e.g. `5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) obsidian/0.12.3 Chrome/89.0.4389.128 Electron/12.0.6 Safari/537.36"`]
+          placeholder: "e.g. v1.4.9"
+      validations:
+          required: false
+    - type: textarea
+      id: context
+      attributes:
+          label: Additional context
+          description: Add any other context about the problem here (e.g. the markdown producing the error).
+          placeholder: Explain any additional context [...]
+      validations:
+          required: false
+    - type: textarea
+      id: config-files
+      attributes:
+          label: Config file
+          description: |
+              Drag & drop your config file here [VAULT/.obsidian/plugins/obsidian-spaced-repetition/data.json]
+              Drag & drop your violated file containing your error here
+          placeholder: |
+              data.json
+              violated_file.md
+      validations:
+          required: false
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 4f153ba..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this plugin.
-title: "[FEAT]"
-labels: enhancement
-assignees: ""
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..79a8dfa
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,30 @@
+name: Feature request
+description: Suggest an idea for this plugin.
+title: "[FEAT]: "
+labels: ["enhancement"]
+
+body:
+    - type: textarea
+      id: problem-description
+      attributes:
+          label: Is your feature request related to a problem? Please describe.
+          description: A clear and concise description of what the problem is.
+          placeholder: I'm always frustrated when [...]
+      validations:
+          required: false
+    - type: textarea
+      id: solution-description
+      attributes:
+          label: Describe the solution you'd like
+          description: A clear and concise description of what you want to happen.
+          placeholder: I wanted that [...]
+      validations:
+          required: true
+    - type: textarea
+      id: context
+      attributes:
+          label: Additional context
+          description: Add any other context or screenshots about the feature request here.
+          placeholder: Explain any additional context [...]
+      validations:
+          required: false
diff --git a/docs/zh/algorithms.md b/docs/zh/algorithms.md
new file mode 100644
index 0000000..8155269
--- /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 0000000..bc89ca0
--- /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 <a href="${algo_url}">algorithm implementation</a>.',
+};
+```
+
+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 <a href="${algo_url}">utekelezaji wa algorithm</a>.',
+};
+```
+
+<sub><sup>A part of that last one is uhh, Google translated, I have a working understanding of Swahili but not enough to write computerese lol.</sup></sub>
+
+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`. <br> 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 0000000..273293f
--- /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注释。
+
+```
+<!--SR:!2021-08-20,13,290-->
+```
+
+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` 作为上下文标题。
+
+### 删除卡片
+
+要删除一个卡片，只需删除复习规划标签和卡片相关文本。
+
+### 忽略卡片
+
+你可以使用诸如 `<!--Card text <!--SR:2021-08-20,13,290--> -->` 的HTML标签来将其从复习队列中移除。你可以随时移除该标签。
+
+## 集中复习
+
+当前仅支持使用 集中复习此笔记中的卡片 命令。将复习所有卡组中来自该笔记的卡片。
+
+## 数据统计
+
+统计页面可以使用 `View Statistics` 命令打开。
+
+### 预估
+
+计算将要到期的卡片数量。
+
+<img src="https://raw.githubusercontent.com/st3v3nmw/obsidian-spaced-repetition/master/assets/stats_forecast.png" />
+
+### 复习间隔
+
+统计卡片再次出现的时间间隔。
+
+### 熟练度
+
+统计卡片熟练度。
+
+### 卡片类型
+
+统计卡片类型：新卡片，较新卡片, 熟悉卡片（复习间隔超过一个月）。
diff --git a/docs/zh/index.md b/docs/zh/index.md
new file mode 100644
index 0000000..d27ab16
--- /dev/null
+++ b/docs/zh/index.md
@@ -0,0 +1,53 @@
+# Obsidian Spaced Repetition
+
+<img src="https://img.shields.io/github/downloads/st3v3nmw/obsidian-spaced-repetition/total" /> <img src="https://img.shields.io/github/downloads/st3v3nmw/obsidian-spaced-repetition/latest/total?style=flat-square" /> <img src="https://img.shields.io/github/manifest-json/v/st3v3nmw/obsidian-spaced-repetition?style=flat-square" />
+
+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).
+
+## 视频演示
+
+<video controls>
+  <source src="https://user-images.githubusercontent.com/43380836/115256965-5d455f00-a138-11eb-988f-27ba29f328a0.mp4" type="video/mp4">
+</video>
+
+## 安装
+
+在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)
+
+### 赞助
+
+<a href='https://ko-fi.com/M4M44DEN6' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://cdn.ko-fi.com/cdn/kofi3.png?v=2' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
+
+<a href="https://jb.gg/OpenSourceSupport" target="_blank"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" height='128' style='border:0px;height:128px;' alt="JetBrains Logo (Main) logo."></a>
diff --git a/docs/zh/notes.md b/docs/zh/notes.md
new file mode 100644
index 0000000..fb9ad80
--- /dev/null
+++ b/docs/zh/notes.md
@@ -0,0 +1,71 @@
+# 笔记
+
+-   笔记应当具有原子性：说清楚**一个**概念；
+-   笔记之间应当高度关联；
+-   先理解，后复习；
+-   善用 [费曼学习法](https://fs.blog/2021/02/feynman-learning-technique/)
+
+## 开始使用
+
+为需要复习的笔记添加 `#review` 标签。你可以在插件设置中修改此默认标签（也可以使用多个标签）
+
+## 新笔记
+
+新笔记将展示在右栏的 `新` （复习序列）中，如图：
+
+<img src="https://raw.githubusercontent.com/st3v3nmw/obsidian-spaced-repetition/master/assets/new_notes.png" />
+
+## 复习
+
+打开笔记即可复习。在菜单中选择 `复习: 简单`，`复习: 记得` 或 `复习: 较难`。 选择 `简单`，`记得` 还是 `较难` 取决于你对复习材料的理解程度。
+
+<img src="https://raw.githubusercontent.com/st3v3nmw/obsidian-spaced-repetition/master/assets/more_options.png" />
+
+在文件上右击可以调出相同选项：
+
+<img src="https://raw.githubusercontent.com/st3v3nmw/obsidian-spaced-repetition/master/assets/file_context_menu.png" />
+
+笔记将被添加到复习队列中：
+
+<img src="https://raw.githubusercontent.com/st3v3nmw/obsidian-spaced-repetition/master/assets/scheduled.png" />
+
+### 快速复习
+
+我们提供快速进入复习模式的命令。你可以在 `设置 -> 快捷键` 中定制快捷键。这可以使您直接开始复习。
+
+### 复习设置
+
+可供定制的选项包括：
+
+-   随机打开笔记或按照优先级排序
+-   在复习完成后是否自动打开下一个笔记
+
+## 复习计划
+
+位于状态栏底部的 `复习: 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 375c0f5..6537cdb 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/main.ts b/src/main.ts
index a8c3c27..fac5237 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -41,6 +41,7 @@ import { NoteEaseCalculator } from "./NoteEaseCalculator";
 import { DeckTreeStatsCalculator } from "./DeckTreeStatsCalculator";
 import { NoteEaseList } from "./NoteEaseList";
 import { QuestionPostponementList } from "./QuestionPostponementList";
+import { isEqualOrSubPath } from "./util/utils";
 
 interface PluginData {
     settings: SRSettings;
@@ -379,7 +380,7 @@ export default class SRPlugin extends Plugin {
         for (const noteFile of notes) {
             if (
                 this.data.settings.noteFoldersToIgnore.some((folder) =>
-                    noteFile.path.startsWith(folder),
+                    isEqualOrSubPath(noteFile.path, folder),
                 )
             ) {
                 continue;
@@ -567,7 +568,11 @@ export default class SRPlugin extends Plugin {
             fileCachedData.frontmatter || {};
 
         const tags = getAllTags(fileCachedData) || [];
-        if (this.data.settings.noteFoldersToIgnore.some((folder) => note.path.startsWith(folder))) {
+        if (
+            this.data.settings.noteFoldersToIgnore.some((folder) =>
+                isEqualOrSubPath(note.path, folder),
+            )
+        ) {
             new Notice(t("NOTE_IN_IGNORED_FOLDER"));
             return;
         }
diff --git a/src/util/utils.ts b/src/util/utils.ts
index 6e4bfef..6882c5e 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -1,5 +1,6 @@
 import moment from "moment";
 import { Moment } from "moment";
+import { normalize, sep } from "path";
 import { PREFERRED_DATE_FORMAT, YAML_FRONT_MATTER_REGEX } from "src/constants";
 
 type Hex = number;
@@ -96,6 +97,38 @@ export function stringTrimStart(str: string): [string, string] {
     return [ws, trimmed];
 }
 
+/**
+ * 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]
 //


Test patch:
diff --git a/tests/unit/util/utils.test.ts b/tests/unit/util/utils.test.ts
index bcfcf46..89dd3c3 100644
--- a/tests/unit/util/utils.test.ts
+++ b/tests/unit/util/utils.test.ts
@@ -2,6 +2,7 @@ import { YAML_FRONT_MATTER_REGEX } from "src/constants";
 import {
     extractFrontmatter,
     findLineIndexOfSearchStringIgnoringWs,
+    isEqualOrSubPath,
     literalStringReplace,
 } from "src/util/utils";
 
@@ -243,3 +244,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,
+            );
+        });
+    });
+});