diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index d19677c0..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "linebreak-style": 0, - "quotes": ["warn", "double", "avoid-escape"], - "semi": ["error", "always"], - "@typescript-eslint/no-unused-vars": [ - "warn", - { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } - ] - } -} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index bd3c98fb..00000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,33 +0,0 @@ -# Note that this job currently doesn't work properly - -name: End to End Tests - -on: workflow_dispatch - -env: - OBSIDIAN_VERSION: 1.2.8 - COREPACK_ENABLE_STRICT: 0 - -jobs: - e2e: - runs-on: ubuntu-latest - - steps: - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "20" - - - uses: actions/checkout@v3 - - - name: Install Obsidian - run: > - sudo apt-get install libnotify4 && - curl -L https://github.com/obsidianmd/obsidian-releases/releases/download/v${{ env.OBSIDIAN_VERSION }}/obsidian_${{ env.OBSIDIAN_VERSION }}_amd64.deb -o Obsidian.deb && - sudo dpkg -i Obsidian.deb - - - name: Install NPM Dependencies - run: npm install -g pnpm && pnpm install - - - name: Test - run: pnpm e2e diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c899559a..62211b90 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,29 +1,21 @@ -name: Lint and Test +name: Lint PR on: - pull_request: - branches: [master] + pull_request_target: + types: + - opened + - edited + - synchronize + - reopened -env: - COREPACK_ENABLE_STRICT: 0 +permissions: + pull-requests: read jobs: - lint_and_test: + lint_pr: runs-on: ubuntu-latest steps: - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "20" - - - uses: actions/checkout@v3 - - - name: Install Dependencies - run: npm install -g pnpm && pnpm install - - - name: Lint - run: pnpm lint - - - name: Test - run: pnpm test + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04efc099..c6e0e03e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: id: extract-release-notes uses: ffurrer2/extract-release-notes@v1 with: - changelog_file: docs/changelog.md + changelog_file: docs/docs/changelog.md - name: Create Release id: create-release diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..1c5c4b86 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Lint and Test Code + +on: + pull_request: + branches: [master] + +env: + COREPACK_ENABLE_STRICT: 0 + +jobs: + lint_and_test_code: + runs-on: ubuntu-latest + + steps: + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - uses: actions/checkout@v3 + + - name: Install Dependencies + run: npm install -g pnpm && pnpm install + + - name: Lint + run: pnpm lint + + - name: Test + run: pnpm test diff --git a/.gitignore b/.gitignore index ddfd9dd8..5c8086e3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,12 +23,15 @@ build # testing coverage -tests/e2e/vault/ +tests/vaults/*/.obsidian # mkdocs site/ venv/ .venv/ +docs/docs/en/media/~* +docs/vaults/*/.obsidian +.obsidian/plugins/obsidian-spaced-repetition-beta/* # env env.sh diff --git a/.npmrc b/.npmrc index 4ae152d7..86efb405 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -use-node-version=20.1.0 +use-node-version=20.17.0 diff --git a/.prettierignore b/.prettierignore index 29152bc6..89f1e65c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,8 +1,6 @@ build coverage node_modules -.yarn -.pnp.cjs -.pnp.loader.mjs pnpm-lock.yaml -tests/e2e/vault/ +docs/vaults/ +tests/vaults/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bed66b3..1fcfbd40 120000 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -docs/changelog.md \ No newline at end of file +docs/docs/changelog.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index fe48d755..43cbfd70 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -st3v3n.mw@gmail.com. +mail@stephenmwangi.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 651dc17d..f6af8f1f 120000 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1 @@ -docs/en/contributing.md \ No newline at end of file +docs/docs/en/contributing.md diff --git a/Makefile b/Makefile deleted file mode 100644 index 3b122d07..00000000 --- a/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -.PHONY: setup_e2e -setup_e2e: - rm -rf tests/e2e/vault - mkdir --parents tests/e2e/vault/.obsidian/plugins/obsidian-spaced-repetition/ - pnpm build - cp build/main.js tests/e2e/vault/.obsidian/plugins/obsidian-spaced-repetition/ - cp styles.css tests/e2e/vault/.obsidian/plugins/obsidian-spaced-repetition/ - cp manifest.json tests/e2e/vault/.obsidian/plugins/obsidian-spaced-repetition/ diff --git a/README.md b/README.md index d6e555ff..f6b1e818 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Fight the forgetting curve & note aging by reviewing flashcards & notes using sp - Check the [roadmap](https://github.com/st3v3nmw/obsidian-spaced-repetition/projects/3/) 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. -- The plugin has been translated into _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 / русский_ by the Obsidian community 😄. +- The plugin has been translated into _Arabic, Chinese, Czech, French, German, Italian, Korean, Japanese, Polish, Portuguese, Spanish, Russian, and Turkish_ by the Obsidian community 😄. - To help translate this plugin to your language, check the [translation guide here](https://www.stephenmwangi.com/obsidian-spaced-repetition/contributing/#translating_1). ## Features @@ -35,8 +35,4 @@ Fight the forgetting curve & note aging by reviewing flashcards & notes using sp Check the [docs](https://www.stephenmwangi.com/obsidian-spaced-repetition/) for more details. -## Supported By - Buy Me a Coffee at ko-fi.com - -JetBrains Logo (Main) logo. diff --git a/docs/changelog.md b/docs/docs/changelog.md similarity index 93% rename from docs/changelog.md rename to docs/docs/changelog.md index 2b9f2326..33b474d1 100644 --- a/docs/changelog.md +++ b/docs/docs/changelog.md @@ -4,8 +4,44 @@ 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 + +Extended card title functionality partly as described in issue [`#851`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/851) + +#### [1.12.7](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.12.6...1.12.7) + +- fix: parsing code blocks & custom separators [`#1081`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1081) +- chore: remove e2e testing [`#1083`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1083) +- update french translation [`#1082`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1082) +- French translation [`#1080`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1080) +- chore: update dependencies, linting, & tests [`#1056`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1056) +- Added options to show/hide (1) ribbon icon, (2) status bar, (3) file menu options [`#1066`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1066) +- Translate to Turkish [`#1078`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1078) +- fix: parsing code blocks & custom separators (#1081) [`#1072`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/1072) [`#1077`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/1077) +- chore: update dependencies, linting, & tests (#1056) [`#548`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/548) [`#1000`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/1000) + +#### [1.12.6](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.12.5...1.12.6) + +> 15 September 2024 + +- Bump version to v1.12.6 [`#1071`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1071) +- Bug 1041 bad transclusion render (format & lint) [`#1062`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1062) +- Fixed [`#1061`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1061) +- Fixed docs references for: Major reworking of the documentation [`#1051`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1051) +- feat: refactor code to support diff methods of storing the scheduling info, and diff SR algorithms [`#1006`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1006) +- Added new option allowing for multiline cards with empty lines [`#1012`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1012) +- fix: arrows in note review panel don't move [`#962`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/962) +- [FIX] isEqualOrSubPath function [`#1048`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1048) +- Major reworking of the documentation [`#1032`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1032) +- fix: add language switcher to docs [`#1043`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1043) +- [FEAT] Split the long list of options into categories within a tab control [`#1021`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1021) +- feat: refactor code to support diff methods of storing the scheduling info, and diff SR algorithms (#1006) [`#548`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/548) [`#1000`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/1000) + #### [1.12.5](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.12.4...1.12.5) +> 1 August 2024 + +- Bump version to v1.12.5 [`#1031`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/1031) - Support RTL flashcards specified by frontmatter "direction" attribute [`#935`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/935) - FEAT-990 Mobile landscape mode and functional size sliders [`#998`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/998) - [FIX] Cards missing when horizontal rule present in document [`#970`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/970) diff --git a/docs/docs/en/algorithms.md b/docs/docs/en/algorithms.md new file mode 100644 index 00000000..efc8bb72 --- /dev/null +++ b/docs/docs/en/algorithms.md @@ -0,0 +1,61 @@ +# Learning Algorithms + +A learning algorithm is a formula that determines when a note or flashcard should next be reviewed. + +| Algorithm | Status | +| --------------------------------------------------- | ----------- | +| [SM-2-OSR](#sm-2-osr) | Implemented | +| [FSRS](#fsrs) | Planned | +| [User Defined Intervals](#user-specified-intervals) | Planned | + +## SM-2-OSR + +- The `SM-2-OSR` algorithm is a variant of [Anki's algorithm](https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html) which is based on the [SM-2 algorithm](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2). +- It supports ternary reviews i.e. a concept is either hard, good, or easy at the time of review. +- initial ease is weighted (using max_link_factor) depending on the average ease of linked notes, note importance, and the base ease. +- Anki also applies a small amount of random “fuzz” to prevent cards that were introduced at the same time and given the same ratings from sticking together and always coming up for review on the same day." +- The algorithm is essentially the same for both notes and flashcards - apart from the PageRanks + +### Algorithm Details + +!!! warning + + Note that this hasn't been updated in a while, + please see the [code](https://github.com/st3v3nmw/obsidian-spaced-repetition/blob/master/src/algorithms/osr/srs-algorithm-osr.ts). + +- `if link_count > 0: initial_ease = (1 - link_contribution) * base_ease + link_contribution * average_ease` - `link_contribution = max_link_factor * min(1.0, log(link_count + 0.5) / log(64))` (cater for uncertainty) + - The importance of the different concepts/notes is determined using the PageRank algorithm (not all notes are created equal xD) + - On most occasions, the most fundamental concepts/notes have higher importance +- If the user reviews a concept/note as: + - easy, the ease increases by `20` and the interval changes to `old_interval * new_ease / 100 * 1.3` (the 1.3 is the easy bonus) + - good, the ease remains unchanged and the interval changes to `old_interval * old_ease / 100` + - hard, the ease decreases by `20` and the interval changes to `old_interval * 0.5` + - The `0.5` can be modified in settings + - `minimum ease = 130` + - For `8` or more days: + - `interval += random_choice({-fuzz, 0, +fuzz})` + - where `fuzz = ceil(0.05 * interval)` + - [Anki docs](https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html): + > "[...] Anki also applies a small amount of random “fuzz” to prevent cards that were introduced at the same time and given the same ratings from sticking together and always coming up for review on the same day." +- The scheduling information is stored in the YAML front matter + +--- + +## FSRS + +The algorithm is detailed at: +[fsrs4anki](https://github.com/open-spaced-repetition/fsrs4anki/wiki) + +Incorporation of the FSRS algorithm into this plugin has not yet occurred. For progress see: +[ [FEAT] sm-2 is outdated, can you please replace it with the fsrs algorithm? #748 ](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/748) + +--- + +## User Specified Intervals + +This is the simplest "algorithm" possible. There are fixed intervals configured by the user for each of the possible review outcomes. + +For example, `hard` might be configured for an interval of 1 day. + +Implementation of this technique has not yet occurred. For progress see: +[ [FEAT] user defined "Easy, Good, Hard" values instead of or in addition to the algorithm defined one. #741 ](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/741) diff --git a/docs/zh/contributing.md b/docs/docs/en/contributing.md similarity index 87% rename from docs/zh/contributing.md rename to docs/docs/en/contributing.md index a7c67fea..14e49989 100644 --- a/docs/zh/contributing.md +++ b/docs/docs/en/contributing.md @@ -10,6 +10,23 @@ First off, thanks for wanting to contribute to the Spaced Repetition plugin! ## Translating +The plugin has been translated into the following languages by the Obsidian community 😄. + +- Arabic / العربية +- Chinese (Simplified) / 简体中文 +- Chinese (Traditional) / 繁體中文 +- Czech / čeština +- French / français +- German / Deutsch +- Italian / Italiano +- Korean / 한국어 +- Japanese / 日本語 +- Polish / Polski +- Portuguese (Brazil) / Português do Brasil +- Spanish / Español +- Russian / русский +- Turkish / Türkçe + ### Steps To help translate the plugin to your language: @@ -56,6 +73,8 @@ 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. @@ -77,7 +96,7 @@ Please note that: 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`). +6. Add your change to the `[Unreleased]` section of the changelog (`docs/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. @@ -91,12 +110,14 @@ Please note that: - 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/`. +These files reside in `docs/docs/` in the respective language's folder. For instance, English docs are located in `docs/docs/en/`. The docs are served on [https://www.stephenmwangi.com/obsidian-spaced-repetition/](https://www.stephenmwangi.com/obsidian-spaced-repetition/). @@ -121,11 +142,13 @@ For larger diffs, it's important that you check how your docs look like as expla ### 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). +1. Create a folder for your language in `docs/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 @@ -150,5 +173,5 @@ Example using `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` +7. Create a git tag with the version: `git tag -a 1.9.2 -m "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. diff --git a/docs/docs/en/data-storage.md b/docs/docs/en/data-storage.md new file mode 100644 index 00000000..2c2c8621 --- /dev/null +++ b/docs/docs/en/data-storage.md @@ -0,0 +1,64 @@ +# Data Storage + +## Scheduling Information + +### Individual Markdown Files + +This is the original method used for storing the scheduling information for cards and notes. + +For cards this is stored in an HTML comment for that card. For example with the card: + +``` +The RCU and WCU limits for a single partition key value::3000 RCU, 1000 WCU +``` + +When the card is reviewed, an HTML comment will be added after the card's text, such as: + +``` + +``` + +By default, the comment is stored on the line following the card text. +Alternatively, it can be stored on the same line by enabling the +[Save scheduling comment on the same line as the flashcard's last line?](user-options.md#storage-of-scheduling-data) option. + +Scheduling information for the note is kept at the beginning of the file, in YAML format within the frontmatter section. +For example: + +![note-frontmatter](https://github.com/user-attachments/assets/b9744f50-c897-46ad-ab34-1bbc55796b57) + +!!! note "Raw text format" + + --- + sr-due: 2024-07-01 + sr-interval: 3 + sr-ease: 269 + --- + +### Single Scheduling File + +The scheduling information for all cards and notes is kept in a single dedicated file. + +Implementation of this has not yet occurred. For progress see: + +[[FEAT] Stop using YAML; Move plugin info and data to separate file #162](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/162) + +--- + +## User Options + +All user [options](user-options.md) are stored in `data.json` in the plugin folder. + +--- + +## Card Postponement List + +This records a list of cards reviewed today that have sibling cards that shouldn't be reviewed until tomorrow. + +Cards are only added to this list if the [Bury sibling cards until the next day](user-options.md#flashcard-review) setting is turned on. + +This information is also kept in the `data.json` file. + +!!! note + + To minimise the space required for this, a copy of the card is not stored. Rather a small numeric hash code ("fingerprint") is kept. diff --git a/docs/docs/en/flashcards/basic-cloze-cards.md b/docs/docs/en/flashcards/basic-cloze-cards.md new file mode 100644 index 00000000..8ecc37ee --- /dev/null +++ b/docs/docs/en/flashcards/basic-cloze-cards.md @@ -0,0 +1,113 @@ +# Basic Cloze Cards + +With [Single & Multiline Cards](../flashcards/q-and-a-cards.md) the text of both the front and back of each card is specified. + +With `cloze` cards a single text is specified, together with an identification of which parts of the text should be obscured. + +The front of the card is displayed as the text with (one or more) `cloze deletions` obscured. + +## Cloze Deletions + +A part of the card text is identified as a cloze deletion by surrounding it with the `cloze delimiter`. + +### Single Cloze Deletion + +By default, the cloze delimiter is `==`, and a simple cloze card would be: + +``` +The first female prime minister of Australia was ==Julia Gillard== +``` + +!!! note "Displayed when reviewed" + +
+ + !!! tip "Initial View" + + The first female prime minister of Australia was [...] + + !!! tip "After `Show Answer` Clicked" + + The first female prime minister of Australia was Julia Gillard + +
+ +### Multiple Cloze Deletions + +If the card text identifies multiple parts as cloze deletions, then multiple cards will be shown for review, each one occluding one deletion, while leaving the other deletions visible. + +For instance, the following note: + +``` +The first female ==prime minister== of Australia was ==Julia Gillard== +``` + +!!! note "" + +
+ + !!! tip "Card 1 Initial View" + + The first female [...] of Australia was Julia Gillard + + !!! tip "Card 2 Initial View" + + The first female prime minister of Australia was [...] + +
+ +!!! tip "After `Show Answer` Clicked (same for both cards)" + + The first female prime minister of Australia was Julia Gillard + +These two cards are considered sibling cards. See [sibling cards](flashcards-overview.md#sibling-cards) regarding the +[Bury sibling cards until the next day](../user-options.md#flashcard-review) scheduling option. + +## Cloze Delimiter + +The cloze delimiter can be modified in [settings](../user-options.md#flashcard-review), e.g. to `**`, or curly braces `{{text in curly braces}}`. + + + +## Anki style + +!!! warning + + Anki style `{{c1:This text}} would {{c2:generate}} {{c1:2 cards}}` cloze deletions are not currently supported. This feature is being tracked [here](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/93/). diff --git a/docs/docs/en/flashcards/cards-with-blank-lines.md b/docs/docs/en/flashcards/cards-with-blank-lines.md new file mode 100644 index 00000000..384ca7da --- /dev/null +++ b/docs/docs/en/flashcards/cards-with-blank-lines.md @@ -0,0 +1,61 @@ +# Including Blank Lines in Flashcards + +By default, [Multi-line Basic](q-and-a-cards.md#multi-line-basic), [Multi-line Bidirectional](q-and-a-cards.md#multi-line-bidirectional) +and [Cloze](basic-cloze-cards.md) type flashcards recognize a blank line as the end of the flashcard text. +This means that blank lines can not be included within the text. + +If blank lines need to be included (e.g. on a card containing a markdown table), the +`Characters denoting the end of clozes and multiline flashcards` [setting](../user-options.md#flashcard-separators) +needs to be changed. + +For example, it could be changed to `+++`. + +!!! warning "Global Edit Required" + + Note that after changing this you have to manually edit any flashcards you already have. + +## Including a Table in the Flashcard Answer + +!!! note "Obsidian requires a blank line before a table for it to be displayed correctly." + + Without it, Obsidian displays it just as text and not correctly formatted. + + ![table-with-no-preceding-blank-line](https://github.com/user-attachments/assets/daed1309-3b38-4d14-bb42-b302efda96df) + + And with a blank line after the `?` and before the table, it is displays correctly. + ![table-with-preceding-blank-line](https://github.com/user-attachments/assets/beef90b7-324e-4876-b10b-055a4d23d41f) + +However, by default a blank line signifies the end of the multiline card. + +To include the blank line, the +`Characters denoting the end of clozes and multiline flashcards` [setting](../user-options.md#flashcard-separators) +needs to be changed. Then that character sequence added as a line after the end of the flashcard text. For example: + +![table-with-preceding-blank-line+++](https://github.com/user-attachments/assets/954fd7fc-6d5f-4315-b40e-2192664c3962) + +Now the card is displayed correctly during a review. + +![table-with-preceding-blank-line-review](https://github.com/user-attachments/assets/3bff8d25-f91f-4bc0-b922-7471d6b60869) + +## Including Blank Lines in a Cloze Flashcard + +With `Convert ==highlights== to clozes` enabled in [settings](../user-options.md#flashcard-separators) +and the `Characters denoting the end of clozes and multiline flashcards` set to `+++`, +we can have blank lines in a cloze flashcard. E.g. + +![cloze-with-blank-lines](https://github.com/user-attachments/assets/f9d6f123-3378-41cb-9c93-2b061856c81d) + +As there are 3 clozes defined, three separate cards will be generated for review. +One card, for example is: + +![cloze-with-blank-lines-front1](https://github.com/user-attachments/assets/6b939d46-b93a-4a67-96d4-6985ccafb76e) + +And after `Show Answer` is clicked, the following is displayed: + +![cloze-with-blank-lines-answer](https://github.com/user-attachments/assets/225abd90-20a4-4e29-abb3-36beb61388d7) + +## Limitation + +### Blank Lines in Answer Side Only + +Blank lines are only supported in the answer side of a multiline flashcard, and not in the question side. diff --git a/docs/docs/en/flashcards/decks.md b/docs/docs/en/flashcards/decks.md new file mode 100644 index 00000000..44e63b79 --- /dev/null +++ b/docs/docs/en/flashcards/decks.md @@ -0,0 +1,76 @@ +# Organizing into Decks + +![flashcard-decks-1](https://github.com/user-attachments/assets/a207b0f6-b064-443c-9c55-540681b10891) + +## Using Obsidian Tags + +1. Specify flashcard tags in settings (`#flashcards` is the default). +2. Tag any notes that you'd like to put flashcards using said tags. + +### Hierarchical Tags + +Note that `#flashcards` will match nested tags like `#flashcards/subdeck/subdeck`. + +### Multiple Tags Within a Single File + +A single file can contain cards for multiple different decks. + +This is possible because a tag pertains to all subsequent cards in a file until any subsequent tag. + +For example: + +```markdown +#flashcards/deckA +Question1 (in deckA)::Answer1 +Question2 (also in deckA)::Answer2 +Question3 (also in deckA)::Answer3 + +#flashcards/deckB +Question4 (in deckB)::Answer4 +Question5 (also in deckB)::Answer5 + +#flashcards/deckC +Question6 (in deckC)::Answer6 +``` + +### A Single Card Within Multiple Decks + +Usually the content of a card is only relevant to a single deck. However, sometimes content doesn't fall neatly into a single deck of the hierarchy. + +In these cases, a card can be tagged as being part of multiple decks. The following card is specified as being in the three different decks listed. + +```markdown +#flashcards/language/words #flashcards/trivia #flashcards/learned-from-tv +A group of cats is called a::clowder +``` + +Note that as shown in the above example, all tags must be placed on the same line, separated by spaces. + +### Question Specific Tags + +A tag that is present at the start of the first line of a card is "question specific", and applies only to that card. + +For example: + +```markdown +#flashcards/deckA +Question1 (in deckA)::Answer1 +Question2 (also in deckA)::Answer2 +Question3 (also in deckA)::Answer3 + +#flashcards/deckB Question4 (in deckB)::Answer4 + +Question6 (in deckA)::Answer6 +``` + +Here `Question6` will be part of `deckA` and not `deckB` as `deckB` is specific to `Question4` only. + +--- + +## Using Folder Structure + +The plugin will automatically search for folders that contain flashcards & use their paths to create decks & sub-decks + +e.g. `Folder/sub-folder/sub-sub-folder` ⇔ `Deck/sub-deck/sub-sub-deck`. + +This is an alternative to the tagging option and can be enabled in [settings](../user-options.md#tags-folders). diff --git a/docs/docs/en/flashcards/flashcards-overview.md b/docs/docs/en/flashcards/flashcards-overview.md new file mode 100644 index 00000000..499622f2 --- /dev/null +++ b/docs/docs/en/flashcards/flashcards-overview.md @@ -0,0 +1,100 @@ +# Flashcard Introduction + +Flashcards are defined within standard Obsidian markdown files. + +A markdown file containing flashcards must identify the [deck](decks.md) (or decks) into which the flashcards are placed. +However, the file does not need to be tagged as a [note](../notes.md) for it to have flashcards defined. + +Two types of flashcards are supported: + +
+ +!!! note "Question & Answer" + + [Question & Answer](q-and-a-cards.md) flashcards are ones where the flashcard text contains both the question text and answer text. + +
+ ![flashcard-qanda-example](https://github.com/user-attachments/assets/65639d80-b249-4b16-ae40-c2af011c6aab) + +!!! note "Cloze" + + [Cloze](basic-cloze-cards.md) flashcards are ones where the flashcard text identifies parts of the text (e.g. a word or phrase) that is hidden + when the front of the card is shown.
+ The hidden text is known as a `cloze deletion`. +
+ ![flashcard-cloze-example](https://github.com/user-attachments/assets/9fb12f2e-9b81-45d9-9097-7f1e3d97ae5a) + +
+ +!!! tip + + For guidelines on how to write and structure flashcards, see [Spaced Repetition Guides](../resources.md#flashcards) + +--- + +## Flashcard Text, Flashcards and Cards + +!!! note + + For simplicity `flashcard text` is sometimes written just as `flashcard` + +The `flashcard text` is text that defines the type and content of a card (or a set of related, `sibling` cards). + +### Single flashcard, multiple cards + +For some flashcard types, the flashcard text defines a single card. For other flashcard types, multiple +cards are defined. + +| Flashcard Type | Cards Defined | +| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| [Single-line Basic](q-and-a-cards.md#single-line-basic) | flashcard defines the front and back of a single card. | +| [Single-line Bidirectional](q-and-a-cards.md#single-line-bidirectional) | flashcard defines two cards. | +| [Multi-line Basic](q-and-a-cards.md#multi-line-basic) | flashcard defines the front and back of a single card. | +| [Multi-line Bidirectional](q-and-a-cards.md#multi-line-bidirectional) | flashcard defines two cards. | +| [Cloze](basic-cloze-cards.md) | flashcard defines multiple cards, the number of cards based on the number of cloze deletions. | + +### Sibling Cards + +If there are multiple cards defined by a single flashcard, those cards are known as `sibling` cards. + +A special scheduling option is available for the review of sibling cards. If the [Bury sibling cards until the next day](../user-options.md#flashcard-review) setting is turned on, +only one sibling card is available for review on a single day. + +### Including Blank Lines within Multiline and Cloze Flashcards + +By default, the end of a multiline flashcard is denoted by a blank line at the end of the flashcard text. +This means that blank lines can not be included within the text. + +See [Cards with Blank Lines](cards-with-blank-lines.md) if blank lines need to be included. + +--- + +## 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. + +--- + +## Card Maintenance + +### Deleting cards + +To delete a card, simply delete the scheduling information & the card text. + +### Ignoring cards + +You can wrap flashcards in HTML comments e.g. ` -->` to prevent it from showing up in your review queues. You can always remove the wrapping comment later. diff --git a/docs/docs/en/flashcards/q-and-a-cards.md b/docs/docs/en/flashcards/q-and-a-cards.md new file mode 100644 index 00000000..a3e091c2 --- /dev/null +++ b/docs/docs/en/flashcards/q-and-a-cards.md @@ -0,0 +1,168 @@ +# Question & Answer Cards + +!!! note + + Cards must be assigned to a deck, either using an Obsidian tag such as `#flashcard` or by using the + folder structure within the vault. + + See [Decks](decks.md) for further details. + +## Single-line Basic + +The prompt and the answer are separated by `::` (this can be configured in [settings](../user-options.md#flashcard-separators)). + +```markdown +the question goes on this side::answer goes here! +``` + +!!! note "Displayed when reviewed" + +
+ + !!! tip "Card Front" + + the question goes on this side + + !!! tip "Card Back" + + answer goes here! + +
+ +## Single-line Bidirectional + +Two cards are created from the single flashcard text. + +The two parts are separated by `:::` (this can be configured in [settings](../user-options.md#flashcard-separators)). + +For example: + +```markdown +info 1:::info 2 +``` + +!!! note "Card 1" + +
+ + !!! tip "Front" + + info 1 + + !!! tip "Back" + + info 2 + +
+ +!!! note "Card 2" + +
+ + !!! tip "Front" + + info 2 + + !!! tip "Back" + + info 1 + +
+ +These two cards are considered sibling cards. See [sibling cards](flashcards-overview.md#sibling-cards) regarding the +[Bury sibling cards until the next day](../user-options.md#flashcard-review) scheduling option. + +--- + +## Multi-line Basic + +The front and the back of the card are separated by `?` (this can be configured in [settings](../user-options.md#flashcard-separators)). + +```markdown +As per the definition +of "multiline" the prompt +can be on multiple lines +? +same goes for +the answer +``` + +!!! note "Displayed when reviewed" + +
+ + !!! tip "Card Front" + + As per the definition
+ of "multiline" the prompt
+ can be on multiple lines + + !!! tip "Card Back" + + same goes for
+ the answer + +
+ +These can also span over multiple lines as long as both sides "touch" the `?`. + +See [Cards with Blank Lines](cards-with-blank-lines.md) if blank lines need to be included. + +--- + +## Multi-line Bidirectional + +Two cards are created from the single flashcard text. + +The two parts are separated by `??` (this can be configured in [settings](../user-options.md#flashcard-separators)). + +For example: + +```markdown +info 1A +info 1B +info 1C +?? +info 2A +info 2B +``` + +These can also span over multiple lines as long as both sides "touch" the `??`: +To include blank lines, see the section below. + +!!! note "Card 1" + +
+ + !!! tip "Front" + + info 1A
+ info 1B
+ info 1C + + !!! tip "Back" + + info 2A
+ info 2B + +
+ +!!! note "Card 2" + +
+ + !!! tip "Front" + + info 2A
+ info 2B + + !!! tip "Back" + + info 1A
+ info 1B
+ info 1C + +
+ +These two cards are considered sibling cards. See [sibling cards](flashcards-overview.md#sibling-cards) regarding the +[Bury sibling cards until the next day](../user-options.md#flashcard-review) scheduling option. diff --git a/docs/docs/en/flashcards/reviewing.md b/docs/docs/en/flashcards/reviewing.md new file mode 100644 index 00000000..0f13f9d1 --- /dev/null +++ b/docs/docs/en/flashcards/reviewing.md @@ -0,0 +1,110 @@ +# Reviewing & Cramming + +A key part of spaced repetition learning is being shown the front of cards to test whether or not you recall the information on the back. There are two similar functions that perform this – [reviewing](#reviewing) & [cramming](#cramming). + +
+ +!!! tip "Reviewing" + + For the selected deck, you are only shown
+ :material-circle-medium: new cards (i.e. ones that have never been reviewed before) as well as
+ :material-circle-medium: due cards (those that the [algorithm](../algorithms.md) has decided it's time to review) + +!!! tip "Cramming" + + You are shown every single card in the selected deck/note, even those that have recently been reviewed. + +
+ +## Common Features of Reviewing & Cramming + +### Deck Selection + +Although you may want to review or cram all cards across all decks, you often may wish to do so on only a subset of decks. + +![flashcard-decks-1](https://github.com/user-attachments/assets/a207b0f6-b064-443c-9c55-540681b10891) + +!!! note "All subdecks included" + + For example, clicking on the `course` deck will also include all cards within the `aws` + and `developer-associate` decks. + +### Operation + +![review-operation](https://github.com/user-attachments/assets/d8f438dc-f1f0-43c4-a752-a5eeb64346e4) + +!!! note "" # | Name | Description - | - | - +1 | Edit | Edit the flashcard text +2 | Reset | Reset the review schedule information - the review interval is set to 1 day, and the ease is set to the default value +3 | Info | Shows the scheduling information for the card +4 | Skip | Skip the current card without reviewing + +### Context + +If the parent note has heading(s), the flashcard will have a title containing the context. + +Taking the following note: + +```markdown +#flashcards + +# Trivia + +## Capitals + +### Africa + +Kenya::Nairobi + +### North America + +Canada::Ottawa +``` + +!!! tip "Context displayed" +![reviewing-context](https://github.com/user-attachments/assets/2ccfc23a-a106-4133-91ec-8bd0efd0e372) + +!!! note +Context is only shown if enabled in [UI Preferences](../user-options.md#ui-preferences) + +### Keyboard shortcuts + +To review faster, use the following keyboard shortcuts: + +- `Space/Enter` => Show answer +- `0` => Reset card's progress (Sorta like `Again` in Anki) +- `1` => Review as `Hard` +- `2` => Review as `Good` +- `3` => Review as `Easy` + +--- + +## 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. + +``` + +``` + +Wrapping in a HTML comment makes the scheduling information not visible in the notes preview. For single-line cards, you can choose whether you want the HTML comment on the same line or on a separate line in the settings. Putting them on the same line prevents breaking of list structures in the preview or after auto-formatting. + +Note that you can skip a card by simply pressing `S` (case doesn't matter). + +!!! tip + + If you're experiencing issues with the size of the modal on mobile devices, + go to [settings](../user-options.md#ui-preferences) and set the _Flashcard Height Percentage_ and _Flashcard Width Percentage_ + to 100% to maximize it. + +--- + +## Cramming + +You are shown every single card, even those that have recently been reviewed. +By using the appropriate [command](../plugin-commands.md) have the choice of cramming cards: + +| Cards | Command | +| -------------------------------------- | ------------------------------------------------- | +| Within a single note | `Spaced Repetition: Cram flashcards in this note` | +| Within a deck (including all subdecks) | `Spaced Repetition: Select a deck to cram note` | diff --git a/docs/docs/en/flashcards/statistics.md b/docs/docs/en/flashcards/statistics.md new file mode 100644 index 00000000..130330dc --- /dev/null +++ b/docs/docs/en/flashcards/statistics.md @@ -0,0 +1,21 @@ +## Statistics + +The statistics section can be accessed using the `View Statistics` command. + +### Forecast + +Stats on the number of cards due in the future. + + + +### Intervals + +Stats on delays until cards are shown again. + +### Eases + +Stats on card eases. + +### Card Types + +Stats on card types: New, Young, Mature (Have intervals more than 1 month). diff --git a/docs/docs/en/index.md b/docs/docs/en/index.md new file mode 100644 index 00000000..d0afa47f --- /dev/null +++ b/docs/docs/en/index.md @@ -0,0 +1,71 @@ +# Obsidian Spaced Repetition + + + +Fight the forgetting curve by reviewing flashcards & notes using spaced repetition on Obsidian.md + +
+ +!!! tip "Getting started" + + :material-circle-medium: View the [quick demo](index.md#quick-demo) below
+ :material-circle-medium: [Plugin installation](index.md#installation)
+ :material-circle-medium: General [guidelines & tips](resources.md) about spaced repetition learning. + +!!! tip "Features" + + :material-circle-medium: [Flashcards](flashcards/flashcards-overview.md)     :material-circle-medium: [Notes](notes.md)
+ :material-circle-medium: [User Options](user-options.md)     :material-circle-medium: [Commands](plugin-commands.md) +
+ :material-circle-medium: [Repetition Algorithms](algorithms.md)     :material-circle-medium: [Data Storage](data-storage.md) + +!!! tip "Help & Support" + + :material-circle-medium: Visit the [discussions](https://github.com/st3v3nmw/obsidian-spaced-repetition/discussions/) section for Q&A help, feedback, and general discussion.
+ :material-circle-medium: Raise an issue [here](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/) if you have a feature request or a bug-report. + +!!! tip "Contributing" + + :material-circle-medium: The plugin has been translated into over [10 languages](contributing.md#translating) by the Obsidian community 😄. To help translate this plugin to your language, check the [translation guide here](contributing.md#translating).
+ :material-circle-medium: Software developers can contribute [feature enhancements and bug fixes](contributing.md#code) + +
+ +--- + +## Quick Demo + +![user-interface-overview](https://github.com/user-attachments/assets/977bab30-cc5e-4b5c-849e-3881d82b3f8e) + +!!! note "" + + 1. Display the [Note Review Queue](notes.md#note-review-queue)
+ 2. Note review queue
+ 3. Display the Obsidian command dialog to access the plugin [commands](plugin-commands.md)
+ 4. `Flashcard Review Icon` Select a flashcard [deck](flashcards/reviewing.md#deck-selection) to [review](flashcards/reviewing.md#reviewing)
+ 5. Identify that flashcards within this note are in the `#flashcards/science/physics` [deck](flashcards/decks.md#using-obsidian-tags)
+ 6. A [single line question](flashcards/q-and-a-cards.md#single-line-basic) (identified by the `::` separating the question and answer)
+ 7. The plugin stores scheduling info within this [HTML comment](data-storage.md#individual-markdown-files)
+ 8. `Spaced Repetition Status Area` The number of notes and flashcards currently due for review. Click to [open a note for review](notes.md#selecting-a-note-for-review). + + + +--- + +## Installation + +You can easily install the plugin from Obsidian's community plugin section in the Obsidian app (Search for `Spaced Repetition`). + +### Manual Installation + +!!! note "Advanced" + + Create an `obsidian-spaced-repetition` folder under `.obsidian/plugins` in your vault. Add the `main.js`, `manifest.json`, and the `styles.css` files from the [latest release](https://github.com/st3v3nmw/obsidian-spaced-repetition/releases) to the folder. + +--- + +## Support + +Buy Me a Coffee at ko-fi.com diff --git a/docs/docs/en/notes.md b/docs/docs/en/notes.md new file mode 100644 index 00000000..142eee8e --- /dev/null +++ b/docs/docs/en/notes.md @@ -0,0 +1,83 @@ +# Notes + +!!! tip + + For guidelines on how to write and structure notes, see [Spaced Repetition Guides](resources.md#notes) + +## Getting started + +Tag any markdown files for review with the `note review` tag, which by default is `#review`. + +The note will appear under `New` in the `Note Review Queue` in the right pane. + +!!! note + + When you tag a note with `#review` the note doesn't immediately appear in the review queue. + You will need to first click on the `Flashcard Review Icon` or the `Spaced Repetition Status Area` for the queue to update + +## Note Review Queue + +![note-review-queue](https://github.com/user-attachments/assets/c0e1d09c-610f-4775-b532-ab78369b117a) + +!!! note "" + + 1. The Note Review Queue
+ 2. The `Note Review Deck` for tag `#review`
+ 3. The new notes - i.e. those tagged with `#review` but have yet to be reviewed
+ 4. The notes scheduled for review on August 1
+ 5. The notes scheduled for review on August 31
+ +### Displaying the Note Review Queue + +By default, the Note Review Queue is displayed when the plugin starts. This can be changed with +the `Enable note review pane on startup` [setting](user-options.md#note-settings). + +The Note Review Queue can also be shown by using the `Open Notes Review Queue in sidebar` +[command](plugin-commands.md). + +## Reviewing + +Open a file, read & review it. Once done, choose either the `Review: Easy`, `Review: Good`, or the `Review: Hard` option on the file menu (the three dots). Select `Easy`, `Good`, or `Hard` depend on how well you comprehend the material being reviewed. + +![file-three-dots-menu](https://github.com/user-attachments/assets/5f37ab88-30f9-477d-b39c-eb86ba15abdb) + +Alternatively, you can right click on the file within the note review queue, and access the same options: + +![note-review-queue-context-menu](https://github.com/user-attachments/assets/d4affa19-5126-45f8-bf3c-0079d2a8a597) + +The note will then be scheduled appropriately by the [learning algorithm](algorithms.md), and the markdown file updated: + +![note-frontmatter](https://github.com/user-attachments/assets/b9744f50-c897-46ad-ab34-1bbc55796b57) + +### Keyboard Shortcuts + +The `Easy`, `Good`, and `Hard` review result can also be selected from the plugin's [command list](plugin-commands.md). + +This is less practical than the methods described above, but does enable the definition of keyboard shortcuts. +You can create custom hotkeys for the review result in `Settings -> HotKeys`. + +### Selecting a Note for Review + +There are a few ways to open a note for review: + +- Open a note via the standard Obsidian features +- Double click on a note title from the Note Review Queue +- Click on the `Spaced Repetition Status Area` in the status bar at the bottom of the screen +- Select the command [Open a note to review review](plugin-commands.md) + +There are also the following relevant options: + +- [Open a random note for review](user-options.md) +- [Open next note automatically after a review](user-options.md) + +## Multiple Note Review Decks + +By default, there is a single review deck called `#review`. + +This default tag can be changed in the [settings](user-options.md#note-settings). Multiple review decks can also be specified. + +## Spaced Repetition Status Area + +`Review: N note(s)` on the status bar at the bottom of the screen shows how many notes one has to review today (Today's notes + overdue notes). + +Clicking on that opens one of the notes for review. diff --git a/docs/docs/en/plugin-commands.md b/docs/docs/en/plugin-commands.md new file mode 100644 index 00000000..869a961a --- /dev/null +++ b/docs/docs/en/plugin-commands.md @@ -0,0 +1,3 @@ +# Plugin Commands + +![plugin-commands](https://github.com/user-attachments/assets/4838812c-121b-4bd1-82b3-46138b2ae67f) diff --git a/docs/docs/en/resources.md b/docs/docs/en/resources.md new file mode 100644 index 00000000..be7d7790 --- /dev/null +++ b/docs/docs/en/resources.md @@ -0,0 +1,48 @@ +# Spaced Repetition Resources + +## General + +- [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) is a great introduction on proper flashcard creation. + +--- + +## Flashcards + +- [PRODUCTIVELY Learning New Things Using Obsidian by @FromSergio :fontawesome-brands-youtube:{ .youtube } ](https://youtu.be/DwSNZEW6jCU) + +--- + +## Notes + +- Notes should be atomic i.e. focus on a single concept. +- Notes should be highly linked. +- Reviews should start only after properly understanding a concept. +- Reviews should be [Feynman-technique](https://fs.blog/2021/02/feynman-learning-technique/)-esque. + +### Incremental Writing + +- English: [Obsidian: inbox review with spaced repetition by @aviskase :fontawesome-brands-youtube:{ .youtube } ](https://youtu.be/zG5r7QIY_TM) +- Russian / русский: [Разгребатель инбокса заметок как у Andy Matuschak в Obsidian by @YuliyaBagriy_ru :fontawesome-brands-youtube:{ .youtube } ](https://youtu.be/CF6SSHB74cs) + +!!! bug "Improvement Needed" + + Extract anything important from the reference and include here instead + +This was introduced [here](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/15) by `@aviskase`. + +### Brief summary + +Andy Matuschak uses [spaced repetition system for working on writing inbox](https://notes.andymatuschak.org/z7iCjRziX6V6unNWL81yc2dJicpRw2Cpp9MfQ). + +!!! bug "Improvement Needed" + + What does this mean? + +In short, there are four possible actions (where `x < y`): + +- skip note (increase interval for `x`) == mark as `good` +- work on it, mark as fruitful work (decrease interval) == mark as `hard` +- work on it, mark as unfruitful work (increase interval for `y`) == mark as `easy` +- convert to evergreen note (stop using the space-repetition prompts) diff --git a/docs/docs/en/user-options.md b/docs/docs/en/user-options.md new file mode 100644 index 00000000..b6920dbe --- /dev/null +++ b/docs/docs/en/user-options.md @@ -0,0 +1,31 @@ +# User Options + +## Flashcard Settings + +### Tags & Folders + +![flashcard-settings-tag-folders.jpg](https://github.com/user-attachments/assets/34baba63-8439-4f31-b07b-e8f62671b621) + +### Flashcard Review + +![flashcard-settings-review.jpg](https://github.com/user-attachments/assets/fe81f6a8-e333-4894-b6cd-db68e1ae6f86) + +### Flashcard Separators + +![flashcard-settings-separators](https://github.com/user-attachments/assets/744aea85-fdb3-4508-b532-7a551253f97d) + +### Storage of Scheduling Data + +![flashcard-settings-scheduling-data](https://github.com/user-attachments/assets/200bb976-c631-4d73-82a5-12ba7e140339) + +--- + +## Note Settings + +![settings-notes](https://github.com/user-attachments/assets/75f7a55a-933f-436f-868a-efb622cc0f9c) + +--- + +## UI Preferences + +![settings-ui-preferences](https://github.com/user-attachments/assets/c0740fa0-02b5-4db9-9d81-94f0ae29ab6c) diff --git a/docs/docs/extra.css b/docs/docs/extra.css new file mode 100644 index 00000000..a7b38dc8 --- /dev/null +++ b/docs/docs/extra.css @@ -0,0 +1,14 @@ +.youtube { + color: #ee0f0f; +} + +hr { + border: none !important; + height: 4px; + background-color: #333; +} + +.thin { + border: none !important; + height: 1px; +} diff --git a/docs/assets/favicon.ico b/docs/docs/favicon.ico similarity index 100% rename from docs/assets/favicon.ico rename to docs/docs/favicon.ico diff --git a/docs/license.md b/docs/docs/license.md similarity index 100% rename from docs/license.md rename to docs/docs/license.md diff --git a/docs/zh/algorithms.md b/docs/docs/zh/algorithms.md similarity index 96% rename from docs/zh/algorithms.md rename to docs/docs/zh/algorithms.md index 81552691..a9f87a81 100644 --- a/docs/zh/algorithms.md +++ b/docs/docs/zh/algorithms.md @@ -5,7 +5,7 @@ !!! 警告 该条目长时间未更新, - 请注意阅读 [源代码](https://github.com/st3v3nmw/obsidian-spaced-repetition/blob/master/src/scheduling.ts). + 请注意阅读 [源代码](https://github.com/st3v3nmw/obsidian-spaced-repetition/blob/master/src/algorithms/osr/srs-algorithm-osr.ts). (除 PageRanks 之外,卡片复习采用相同规划算法) diff --git a/docs/en/contributing.md b/docs/docs/zh/contributing.md similarity index 94% rename from docs/en/contributing.md rename to docs/docs/zh/contributing.md index a7c67fea..ef16dda5 100644 --- a/docs/en/contributing.md +++ b/docs/docs/zh/contributing.md @@ -77,7 +77,7 @@ Please note that: 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`). +6. Add your change to the `[Unreleased]` section of the changelog (`docs/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. @@ -96,7 +96,7 @@ Please note that: 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/`. +These files reside in `docs/docs/` in the respective language's folder. For instance, English docs are located in `docs/docs/en/`. The docs are served on [https://www.stephenmwangi.com/obsidian-spaced-repetition/](https://www.stephenmwangi.com/obsidian-spaced-repetition/). @@ -121,7 +121,7 @@ For larger diffs, it's important that you check how your docs look like as expla ### 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). +1. Create a folder for your language in `docs/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. diff --git a/docs/zh/flashcards.md b/docs/docs/zh/flashcards.md similarity index 99% rename from docs/zh/flashcards.md rename to docs/docs/zh/flashcards.md index 273293f8..d75cc865 100644 --- a/docs/zh/flashcards.md +++ b/docs/docs/zh/flashcards.md @@ -225,7 +225,7 @@ Canada::Ottawa 计算将要到期的卡片数量。 - + ### 复习间隔 diff --git a/docs/zh/index.md b/docs/docs/zh/index.md similarity index 92% rename from docs/zh/index.md rename to docs/docs/zh/index.md index d27ab167..b6a6d024 100644 --- a/docs/zh/index.md +++ b/docs/docs/zh/index.md @@ -49,5 +49,3 @@ Fight the forgetting curve & note aging by reviewing flashcards & notes using sp ### 赞助 Buy Me a Coffee at ko-fi.com - -JetBrains Logo (Main) logo. diff --git a/docs/zh/notes.md b/docs/docs/zh/notes.md similarity index 91% rename from docs/zh/notes.md rename to docs/docs/zh/notes.md index fb9ad808..1a5ed788 100644 --- a/docs/zh/notes.md +++ b/docs/docs/zh/notes.md @@ -13,21 +13,21 @@ 新笔记将展示在右栏的 `新` (复习序列)中,如图: - + ## 复习 打开笔记即可复习。在菜单中选择 `复习: 简单`,`复习: 记得` 或 `复习: 较难`。 选择 `简单`,`记得` 还是 `较难` 取决于你对复习材料的理解程度。 - + 在文件上右击可以调出相同选项: - + 笔记将被添加到复习队列中: - + ### 快速复习 diff --git a/docs/en/algorithms.md b/docs/en/algorithms.md deleted file mode 100644 index e2fcc8e9..00000000 --- a/docs/en/algorithms.md +++ /dev/null @@ -1,30 +0,0 @@ -# Algorithms - -## SM-2 - -!!! warning - - Note that this hasn't been updated in a while, - please see the [code](https://github.com/st3v3nmw/obsidian-spaced-repetition/blob/master/src/scheduling.ts). - -(It's the same as that used for flashcards - apart from the PageRanks) - -- The algorithm is a variant of [Anki's algorithm](https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html) which is based on the [SM-2 algorithm](https://www.supermemo.com/en/archives1990-2015/english/ol/sm2). -- It supports ternary reviews i.e. a concept is either hard, good, or easy at the time of review. -- initial ease is weighted (using max_link_factor) depending on the average ease of linked notes, note importance, and the base ease. - - `if link_count > 0: initial_ease = (1 - link_contribution) * base_ease + link_contribution * average_ease` - - `link_contribution = max_link_factor * min(1.0, log(link_count + 0.5) / log(64))` (cater for uncertainty) - - The importance of the different concepts/notes is determined using the PageRank algorithm (not all notes are created equal xD) - - On most occasions, the most fundamental concepts/notes have higher importance -- If the user reviews a concept/note as: - - easy, the ease increases by `20` and the interval changes to `old_interval * new_ease / 100 * 1.3` (the 1.3 is the easy bonus) - - good, the ease remains unchanged and the interval changes to `old_interval * old_ease / 100` - - hard, the ease decreases by `20` and the interval changes to `old_interval * 0.5` - - The `0.5` can be modified in settings - - `minimum ease = 130` - - For `8` or more days: - - `interval += random_choice({-fuzz, 0, +fuzz})` - - where `fuzz = ceil(0.05 * interval)` - - [Anki docs](https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html): - > "[...] Anki also applies a small amount of random “fuzz” to prevent cards that were introduced at the same time and given the same ratings from sticking together and always coming up for review on the same day." -- The scheduling information is stored in the YAML front matter diff --git a/docs/en/flashcards.md b/docs/en/flashcards.md deleted file mode 100644 index 8bfc5b1e..00000000 --- a/docs/en/flashcards.md +++ /dev/null @@ -1,259 +0,0 @@ -# Flashcards - -## Creating - -[Piotr Wozniak's 20 rules of knowledge formulation](https://supermemo.guru/wiki/20_rules_of_knowledge_formulation) is a great introduction on proper flashcard creation. - -### Single-line Basic (Remnote style) - -The prompt and the answer are separated by `::` (this can be configured in settings). - -```markdown -the question goes on this side::answer goes here! -``` - -### Single-line Reversed - -Creates two cards `side1:::side2` & the reversed version `side2:::side1`. - -The prompt and the answer are separated by `:::` (this can be configured in settings). - -```markdown -the question goes on this side:::answer goes here! -``` - -Note: In the first review, the plugin will show non-reversed card and reversed card. -If **Bury sibling cards until the next day?** turn on, only non-reversed card will appear. - -### Multi-line Basic - -The front and the back of the card are separated by `?` (this can be configured in settings). - -```markdown -Front of multiline -? -Backside of multiline card -``` - -These can also span over multiple lines as long as both sides "touch" the `?`: - -```markdown -As per the definition -of "multiline" the prompt -can be on multiple lines -? -same goes for -the answer -``` - -### Multi-line Reversed - -Creates two cards `side1??side2` & the reversed version `side2??side1`. - -The front and the back of the card are separated by `??` (this can be configured in settings). - -```markdown -Front of multiline -?? -Backside of multiline card -``` - -These can also span over multiple lines as long as both sides "touch" the `??`: - -```markdown -As per the definition -of "multiline" the prompt -can be on multiple lines -?? -same goes for -the answer -``` - -Note: The behaviour is same as single line reversed. - -### Cloze cards - -You can easily add cloze deletion cards using `==highlights==`, `**bolded text**`, or `{{text in curly braces}}`. - -These can be turned on or off in settings. - -Anki style `{{c1:This text}} would {{c2:generate}} {{c1:2 cards}}` cloze deletions are not currently supported. This feature is being tracked [here](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/93/). - -## Decks - -![Screenshot from 2021-06-05 19-28-24](https://user-images.githubusercontent.com/43380836/120922211-78603400-c6d0-11eb-9d09-bdd5df1c9112.png) - -The green and blue counts on the right of each deck name represent due and new cards respectively. - -### Using Obsidian Tags - -1. Specify flashcard tags in settings (`#flashcards` is the default). -2. Tag any notes that you'd like to put flashcards using said tags. - -#### Hierarchical Tags - -Note that `#flashcards` will match nested tags like `#flashcards/subdeck/subdeck`. - -#### Multiple Tags Within a Single File - -A single file can contain cards for multiple different decks. - -This is possible because a tag pertains to all subsequent cards in a file until any subsequent tag. - -For example: - -```markdown -#flashcards/deckA -Question1 (in deckA)::Answer1 -Question2 (also in deckA)::Answer2 -Question3 (also in deckA)::Answer3 - -#flashcards/deckB -Question4 (in deckB)::Answer4 -Question5 (also in deckB)::Answer5 - -#flashcards/deckC -Question6 (in deckC)::Answer6 -``` - -#### A Single Card Within Multiple Decks - -Usually the content of a card is only relevant to a single deck. However, sometimes content doesn't fall neatly into a single deck of the hierarchy. - -In these cases, a card can be tagged as being part of multiple decks. The following card is specified as being in the three different decks listed. - -```markdown -#flashcards/language/words #flashcards/trivia #flashcards/learned-from-tv -A group of cats is called a::clowder -``` - -Note that as shown in the above example, all tags must be placed on the same line, separated by spaces. - -#### Question Specific Tags - -A tag that is present at the start of the first line of a card is "question specific", and applies only to that card. - -For example: - -```markdown -#flashcards/deckA -Question1 (in deckA)::Answer1 -Question2 (also in deckA)::Answer2 -Question3 (also in deckA)::Answer3 - -#flashcards/deckB Question4 (in deckB)::Answer4 - -Question6 (in deckA)::Answer6 -``` - -Here `Question6` will be part of `deckA` and not `deckB` as `deckB` is specific to `Question4` only. - -### Using Folder Structure - -The plugin will automatically search for folders that contain flashcards & use their paths to create decks & sub-decks i.e. `Folder/sub-folder/sub-sub-folder` ⇔ `Deck/sub-deck/sub-sub-deck`. - -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. - -``` - -``` - -Wrapping in a HTML comment makes the scheduling information not visible in the notes preview. For single-line cards, you can choose whether you want the HTML comment on the same line or on a separate line in the settings. Putting them on the same line prevents breaking of list structures in the preview or after auto-formatting. - -Note that you can skip a card by simply pressing `S` (case doesn't matter). - -!!! tip - - If you're experiencing issues with the size of the modal on mobile devices, - go to settings and set the _Flashcard Height Percentage_ and _Flashcard Width Percentage_ - to 100% to maximize it. - -### Faster Review - -To review faster, use the following keyboard shortcuts: - -- `Space/Enter` => Show answer -- `0` => Reset card's progress (Sorta like `Again` in Anki) -- `1` => Review as `Hard` -- `2` or `Space` => Review as `Good` -- `3` => Review as `Easy` - -### Context - -If the parent note has heading(s), the flashcard will have a title containing the context. - -Taking the following note: - -```markdown -#flashcards - -# Trivia - -## Capitals - -### Africa - -Kenya::Nairobi - -### North America - -Canada::Ottawa -``` - -The flashcard for `Kenya::Nairobi` will have `Trivia > Capitals > Africa` as the context/title whereas the flashcard for `Canada::Ottawa` will have `Trivia > Capitals > North America` as the context/title. - -### Deleting cards - -To delete a card, simply delete the scheduling information & the card text. - -### Ignoring cards - -You can wrap flashcards in HTML comments e.g. ` -->` to prevent it from showing up in your review queues. You can always remove the wrapping comment later. - -## Cramming - -Currently, the only supported method is "cramming" all cards in a note using the Cram flashcards in this note command. Will work on a per-deck across-all-notes method. - -## Statistics - -The statistics section can be accessed using the `View Statistics` command. - -### Forecast - -Stats on the number of cards due in the future. - - - -### Intervals - -Stats on delays until cards are shown again. - -### Eases - -Stats on card eases. - -### Card Types - -Stats on card types: New, Young, Mature (Have intervals more than 1 month). diff --git a/docs/en/index.md b/docs/en/index.md deleted file mode 100644 index 87dbd1f0..00000000 --- a/docs/en/index.md +++ /dev/null @@ -1,53 +0,0 @@ -# Obsidian Spaced Repetition - - - -Fight the forgetting curve & note aging by reviewing flashcards & notes using spaced repetition on Obsidian.md - -- Check the documentation [here](https://www.stephenmwangi.com/obsidian-spaced-repetition/). -- 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. -- The plugin has been translated into _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 / русский_ by the Obsidian community 😄. - - To help translate this plugin to your language, check the [translation guide here](https://www.stephenmwangi.com/obsidian-spaced-repetition/contributing/#translating_1). - -## Quick Demo - - - -## Installation - -You can easily install the plugin from Obsidian's community plugin section in the Obsidian app (Search for `Spaced Repetition`). - -### Manual Installation - -Create an `obsidian-spaced-repetition` folder under `.obsidian/plugins` in your vault. Add the `main.js`, `manifest.json`, and the `styles.css` files from the [latest release](https://github.com/st3v3nmw/obsidian-spaced-repetition/releases) to the folder. - -## Resources - -### YouTube Tutorials - -#### Flashcards - -- [PRODUCTIVELY Learning New Things Using Obsidian by @FromSergio](https://youtu.be/DwSNZEW6jCU) - -#### Notes - -##### Incremental Writing - -- [Obsidian: inbox review with spaced repetition by @aviskase](https://youtu.be/zG5r7QIY_TM) -- [Разгребатель инбокса заметок как у Andy Matuschak в Obsidian by @YuliyaBagriy_ru](https://youtu.be/CF6SSHB74cs) - -### On Spaced Repetition - -- [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) - -### Supported By - -Buy Me a Coffee at ko-fi.com - -JetBrains Logo (Main) logo. diff --git a/docs/en/notes.md b/docs/en/notes.md deleted file mode 100644 index 4e383e8c..00000000 --- a/docs/en/notes.md +++ /dev/null @@ -1,71 +0,0 @@ -# Notes - -- Notes should be atomic i.e. focus on a single concept. -- Notes should be highly linked. -- Reviews should start only after properly understanding a concept. -- Reviews should be [Feynman-technique](https://fs.blog/2021/02/feynman-learning-technique/)-esque. - -## Getting started - -Tag any notes that you'd like to review as `#review`. This default tag can be changed in the settings. (You can also use multiple tags) - -## New Notes - -All "new" notes are listed under `New` on the right pane (Review Queue). Like so: - - - -## Reviewing - -Open the file & review it. Once done, choose either the `Review: Easy`, `Review: Good`, or the `Review: Hard` option on the file menu (the three dots). The `Easy`, `Good`, or `Hard` depend on how well you comprehend the material being reviewed. - - - -Alternatively, you can right click on the file and access the same options: - - - -The note will then be scheduled appropriately: - - - -### Faster Review - -Commands to open a note for review, and making review responses are provided. You can create custom hotkeys for them in `Settings -> HotKeys`. This allows for much faster review. - -### Review Settings - -Available settings are: - -- Choosing whether to open a note at random or the most important note -- Choosing whether to open the next note automatically after reviewing another - -## Scheduled notes - -`Review: N due` on the status bar at the bottom of the screen shows how many notes one has to review today (Today's notes + overdue notes). Clicking on that opens one of the notes for review. - -Alternatively, one can use the `Open a note for review` command. - -## Review Queue - -- Daily review entries are sorted by importance (PageRank) - -## Incremental Writing - -This was introduced [here](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/15) by `@aviskase`. - -Here are the YouTube videos: - -- English: [Obsidian: inbox review with spaced repetition](https://youtu.be/zG5r7QIY_TM) -- Russian / русский: [Yuliya Bagriy - Разгребатель инбокса заметок как у Andy Matuschak в Obsidian](https://www.youtube.com/watch?v=CF6SSHB74cs) - -### Brief summary - -Andy Matuschak uses [spaced repetition system for working on writing inbox](https://notes.andymatuschak.org/z7iCjRziX6V6unNWL81yc2dJicpRw2Cpp9MfQ). - -In short, there are four possible actions (where `x < y`): - -- skip note (increase interval for `x`) == mark as `good` -- work on it, mark as fruitful work (decrease interval) == mark as `hard` -- work on it, mark as unfruitful work (increase interval for `y`) == mark as `easy` -- convert to evergreen note (stop using the space-repetition prompts) diff --git a/docs/media/en/cloze-with-blank-lines-answer.jpg b/docs/media/en/cloze-with-blank-lines-answer.jpg new file mode 100644 index 00000000..bcf3594c Binary files /dev/null and b/docs/media/en/cloze-with-blank-lines-answer.jpg differ diff --git a/docs/media/en/cloze-with-blank-lines-front1.jpg b/docs/media/en/cloze-with-blank-lines-front1.jpg new file mode 100644 index 00000000..b4ded5e6 Binary files /dev/null and b/docs/media/en/cloze-with-blank-lines-front1.jpg differ diff --git a/docs/media/en/cloze-with-blank-lines.jpg b/docs/media/en/cloze-with-blank-lines.jpg new file mode 100644 index 00000000..42665314 Binary files /dev/null and b/docs/media/en/cloze-with-blank-lines.jpg differ diff --git a/docs/media/en/file-three-dots-menu.jpg b/docs/media/en/file-three-dots-menu.jpg new file mode 100644 index 00000000..a5ea69b7 Binary files /dev/null and b/docs/media/en/file-three-dots-menu.jpg differ diff --git a/assets/file_context_menu.png b/docs/media/en/file_context_menu.png similarity index 100% rename from assets/file_context_menu.png rename to docs/media/en/file_context_menu.png diff --git a/docs/media/en/flashcard-cloze-example.jpg b/docs/media/en/flashcard-cloze-example.jpg new file mode 100644 index 00000000..8652a74e Binary files /dev/null and b/docs/media/en/flashcard-cloze-example.jpg differ diff --git a/docs/media/en/flashcard-decks-1.jpg b/docs/media/en/flashcard-decks-1.jpg new file mode 100644 index 00000000..e94740a8 Binary files /dev/null and b/docs/media/en/flashcard-decks-1.jpg differ diff --git a/docs/media/en/flashcard-qanda-example.jpg b/docs/media/en/flashcard-qanda-example.jpg new file mode 100644 index 00000000..590e6bb0 Binary files /dev/null and b/docs/media/en/flashcard-qanda-example.jpg differ diff --git a/docs/media/en/flashcard-settings-review.jpg b/docs/media/en/flashcard-settings-review.jpg new file mode 100644 index 00000000..e28f9552 Binary files /dev/null and b/docs/media/en/flashcard-settings-review.jpg differ diff --git a/docs/media/en/flashcard-settings-scheduling-data.jpg b/docs/media/en/flashcard-settings-scheduling-data.jpg new file mode 100644 index 00000000..6d4765a0 Binary files /dev/null and b/docs/media/en/flashcard-settings-scheduling-data.jpg differ diff --git a/docs/media/en/flashcard-settings-separators.jpg b/docs/media/en/flashcard-settings-separators.jpg new file mode 100644 index 00000000..43792f78 Binary files /dev/null and b/docs/media/en/flashcard-settings-separators.jpg differ diff --git a/docs/media/en/flashcard-settings-tag-folders.jpg b/docs/media/en/flashcard-settings-tag-folders.jpg new file mode 100644 index 00000000..967e563c Binary files /dev/null and b/docs/media/en/flashcard-settings-tag-folders.jpg differ diff --git a/docs/media/en/image-annotation.docx b/docs/media/en/image-annotation.docx new file mode 100644 index 00000000..d781941e Binary files /dev/null and b/docs/media/en/image-annotation.docx differ diff --git a/assets/more_options.png b/docs/media/en/more_options.png similarity index 100% rename from assets/more_options.png rename to docs/media/en/more_options.png diff --git a/assets/new_notes.png b/docs/media/en/new_notes.png similarity index 100% rename from assets/new_notes.png rename to docs/media/en/new_notes.png diff --git a/docs/media/en/note-frontmatter.jpg b/docs/media/en/note-frontmatter.jpg new file mode 100644 index 00000000..470f2a35 Binary files /dev/null and b/docs/media/en/note-frontmatter.jpg differ diff --git a/docs/media/en/note-review-queue-context-menu.jpg b/docs/media/en/note-review-queue-context-menu.jpg new file mode 100644 index 00000000..bc6e872f Binary files /dev/null and b/docs/media/en/note-review-queue-context-menu.jpg differ diff --git a/docs/media/en/note-review-queue.jpg b/docs/media/en/note-review-queue.jpg new file mode 100644 index 00000000..deed81dc Binary files /dev/null and b/docs/media/en/note-review-queue.jpg differ diff --git a/docs/media/en/plugin-commands.jpg b/docs/media/en/plugin-commands.jpg new file mode 100644 index 00000000..b63fc827 Binary files /dev/null and b/docs/media/en/plugin-commands.jpg differ diff --git a/docs/media/en/review-deck.jpg b/docs/media/en/review-deck.jpg new file mode 100644 index 00000000..ab244ac8 Binary files /dev/null and b/docs/media/en/review-deck.jpg differ diff --git a/docs/media/en/review-operation.jpg b/docs/media/en/review-operation.jpg new file mode 100644 index 00000000..53db66f6 Binary files /dev/null and b/docs/media/en/review-operation.jpg differ diff --git a/docs/media/en/reviewing-context.jpg b/docs/media/en/reviewing-context.jpg new file mode 100644 index 00000000..3ecffdda Binary files /dev/null and b/docs/media/en/reviewing-context.jpg differ diff --git a/assets/scheduled.png b/docs/media/en/scheduled.png similarity index 100% rename from assets/scheduled.png rename to docs/media/en/scheduled.png diff --git a/docs/media/en/settings-notes.jpg b/docs/media/en/settings-notes.jpg new file mode 100644 index 00000000..6632b2ff Binary files /dev/null and b/docs/media/en/settings-notes.jpg differ diff --git a/docs/media/en/settings-ui-preferences.jpg b/docs/media/en/settings-ui-preferences.jpg new file mode 100644 index 00000000..536af0be Binary files /dev/null and b/docs/media/en/settings-ui-preferences.jpg differ diff --git a/assets/stats_forecast.png b/docs/media/en/stats_forecast.png similarity index 100% rename from assets/stats_forecast.png rename to docs/media/en/stats_forecast.png diff --git a/docs/media/en/table-with-no-preceding-blank-line.jpg b/docs/media/en/table-with-no-preceding-blank-line.jpg new file mode 100644 index 00000000..f971bf0b Binary files /dev/null and b/docs/media/en/table-with-no-preceding-blank-line.jpg differ diff --git a/docs/media/en/table-with-preceding-blank-line+++.jpg b/docs/media/en/table-with-preceding-blank-line+++.jpg new file mode 100644 index 00000000..70310c2e Binary files /dev/null and b/docs/media/en/table-with-preceding-blank-line+++.jpg differ diff --git a/docs/media/en/table-with-preceding-blank-line-review.jpg b/docs/media/en/table-with-preceding-blank-line-review.jpg new file mode 100644 index 00000000..ba044367 Binary files /dev/null and b/docs/media/en/table-with-preceding-blank-line-review.jpg differ diff --git a/docs/media/en/table-with-preceding-blank-line.jpg b/docs/media/en/table-with-preceding-blank-line.jpg new file mode 100644 index 00000000..5b9a63e7 Binary files /dev/null and b/docs/media/en/table-with-preceding-blank-line.jpg differ diff --git a/docs/media/en/user-interface-overview.jpg b/docs/media/en/user-interface-overview.jpg new file mode 100644 index 00000000..d8fa4c25 Binary files /dev/null and b/docs/media/en/user-interface-overview.jpg differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Accelerator (DAX).md b/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Accelerator (DAX).md new file mode 100644 index 00000000..5b367f1b --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Accelerator (DAX).md @@ -0,0 +1,49 @@ +#course/aws/developer-associate + +DynamoDB accelerator, also known as DAX, is a way that we can reduce latency and increase performance for our DynamoDB table. DAX is a managed service that provides in-memory acceleration for our DynamoDB tables. It improves performance from milliseconds to microseconds even at really high rates like millions of requests per second. So remember those keywords millisecond and microsecond. You get millisecond latency with DynamoDB but you get microsecond latency if you put the accelerator in front of DynamoDB. It provides managed cache invalidation, data population, and cluster management. And DAX is used to improve read performance but not writes. So this is about improving reads and not writes to your database. + +You don't need to modify the application logic because DAX is compatible with DynamoDB API calls. So it's really simple to put DAX in front of your DynamoDB table. So let's look at how it looks in an architectural diagram. + +We've got DynamoDB, then we've got our application instance. And what we do is we put our cache in front of DynamoDB. Now note that the Cache is within the VPC because it runs on EC2 two instances. + +![[Pasted image 20240508132354.png]] + +Now to make this work, we do need some permissions: +- The IAM role for the DAX instance must have permissions to access DynamoDB. And +- the IAM role for the EC2 instance requires permissions to access both DynamoDB and DAX + +If we have a security group we'll need these rules. So in this case we'll need inbound rules: +- So we'll need TCP 8000 and TCP 8111. +Those are for DynamoDB and DAX. And from we can specify from the actual security group ID of the security group itself because our instance and our accelerator are both within the security group. That can be enabled really easily through the management console or using the SDK. + +And as with DynamoDB, you only pay for the capacity you actually provision. It's provisions through clusters and charged by the node. So it actually runs on EC2 instances so you pay by the node based on the instance type you use. Pricing is per node-hour consumed and is dependent on the instance type that you select. + +Now, just a quick comparison with ElastiCache another in memory caching solution. So DAX is more optimized for DynamoDB. Obviously it can call the APIs directly so it's much simpler to put in front of a DynamoDB table. With ElastiCache, you also have more management overhead for things like invalidation of items in the cache. With ElastiCache you would also need to modify the application code to point to the cache. That being said, ElastiCache does support more data stores than DAX. + +---- + +# Questions + +Does DynamoDB DAX improve latency for both read and write operations::No - just reads + + +What is DynamoDB DAX::a managed service that provides in-memory acceleration for our DynamoDB tables + + +How much of an improvement does DAX provide on read operations::From milliseconds to microseconds + + +On what compute platform does DynamoDB DAX run::EC2 + + +What security mechanisms are required for using DAX +? +(1) The IAM role for the DAX instance must have permissions to access DynamoDB. +(2) the IAM role for the EC2 instance requires permissions to access both DynamoDB and DAX +(3) the security group needs to allow TCP 8000 and TCP 8111. + +Why is it easier to use DAX for speeding up access to DynamoDB rather than Elasticache::DAX accepts the same api as DynamoDB itself, therefore no code change required + + + + diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Global Tables.md b/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Global Tables.md new file mode 100644 index 00000000..8c23000d --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Global Tables.md @@ -0,0 +1,20 @@ +#course/aws/developer-associate + +Welcome to this lesson. In this lesson, I'm going to cover DynamoDB Global Tables. + +[[Global Tables|Global tables]] is a fully managed solution for deploying a multi-region and multi-master database. When you create a global table, you specify the regions you want the table to be available in. And DynamoDB performs all the necessary tasks to create identical tables and propagate any ongoing changes to all of those tables. And changes can be made in any of those tables in any region that your global table is replicated to. So let's have a look at a diagram. + +We've got region A and region B and also region C here. We've got DynamoDB and our application server here and it's reading and writing data in the table. We can then have a replica of the DynamoDB table in each of the other regions as well. And we can perform read and writes to all of those tables and the changes will be replicated across the tables in each of these three regions. + +![[Pasted image 20240508134406.png]] + +Global tables uses asynchronous replication. Global tables is therefore a multi-region, multi-master database. Each replica table stores the same set of data items. And you can use logic in the application layer to failover to a replica region as well. So that gives you really high availability and fault tolerance. + +---- + + +# Questions + +In the context of AWS DynamoDB global tables, what does "multi master" mean?::Each replica of the DynamoDB table (in different AWS Regions) can serve both **read** and **write** requests independently. + + diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Streams.md b/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Streams.md new file mode 100644 index 00000000..f3968aa7 --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB Streams.md @@ -0,0 +1,39 @@ +#course/aws/developer-associate + +Hi guys. In this lesson, I'm going to cover [[Amazon DynamoDB Streams|DynamoDB streams]]. So what is streams? Well, let's have a look at an example. So we have an application here, a DynamoDB table and then DynamoDB streams. Our application is writing items to the table. Maybe it's inserting, updating, or deleting an item. So we can see that as the first operation here. + +A record is written to the DynamoDB stream. So after a change is made to the DynamoDB table, all changes are then projected into the DynamoDB stream. We might then have a lambda function being triggered and it will do something like process the information in the stream and write something to CloudWatch Logs. So just an example of using a stream. + +![[Pasted image 20240507224507.png]] + +So streams captures a time-ordered sequence of item level modifications in a DynamoDB table. And the information is stored in a log for up to 24 hours. Applications can access that log and view the data items as they appeared before and after they were modified in near real time. You can also use the create table or update table API operations to enable or modify a stream. + +#### StreamSpecification + +The stream specification parameter determines how the stream is configured. + +So stream enabled specifies that a stream is enabled if it's true or disabled if it's false. + +Stream view type specifies the information that will be written to the stream whenever data in the table is modified. So what we can do is we can decide what information we want to replicate into streams: +- Either keys only, that means only the key attributes of the modified item. +- New image. So the entire item as it appears after it was modified +- Old image. The entire item as it appeared before it was modified. Or +- new and old images and that's both the new and old images of the item + +So this is a great way to record the information that's changed in your table. Maybe you want to take the old items so you can archive it in some way. So you've got a record of the items before they were modified. Or maybe you want to take the new information and process that outside of DynamoDB by using a Lambda function or some other processing element to read from the stream. + +---- +# Questions + +What is the maximum time that information is stored in a DynamoDB stream:: 24 hours + + +Is there any order to the items in a DynamoDB stream:: yes - they are a time ordered sequence + + +Are DynamoDB streams considered (1) real time (2) near real time (3) batch::Near real time + + +What options are there over the content of items in a DynamoDB stream:: (1) the keys only (2) only the old values of the item (3) only the new values (4) both old and new + + diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB.md b/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB.md new file mode 100644 index 00000000..1ca6a811 --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/Amazon DynamoDB.md @@ -0,0 +1,9 @@ +--- +sr-due: 2024-08-03 +sr-interval: 4 +sr-ease: 270 +--- + + #review + +Hi guys in this lesson, we're going to go over some of the fundamentals of Amazon DynamoDB. So what is DynamoDB. Well, it's a fully managed NoSQL database service. So it's more of an unstructured database compared to SQL which has more of a rigid structure. It's a key value store and also a document store. It's non-relational, key value. It's fully serverless and you get push button scaling. So that means it's very easy to adjust how your database scales by adjusting the throughput, which will look at in this section. So you have a DynamoDB table and it's essentially scaling horizontally as you give it more throughput. And on the backend that's happening across various partitions in the Amazon data center. So data is stored in partitions and they are replicated across multiple AZs within a single region. DynamoDB provides low latency. So it's in the range of milliseconds. If you need lower latency, like microsecond and these are keywords to look for in exam questions then you would need to use DynamoDB accelerator which we'll also cover in this section. All data gets stored on SSD storage. So it's solid state drives which are high performance. Data gets replicated as we mentioned in the previous slide across multiple AZs within a region. And there's a feature called Global Tables and that will synchronize your tables across regions if you need to have replication across regions. Maybe you're running an application in another region or you might be using it in a DR HA set up. Now, let's look at some of the features of DynamoDB. So as I mentioned, it is fully serverless, it's fully managed, and it's fault tolerant. It's highly available with four nines availability and five nines if you use Global Tables. It's a NoSQL type of database with a name/value structure, has a flexible schemer, which is good for when your data is not well structured or it's unpredictable. Scaling is horizontal by adjusting the throughput. And then AWS takes care of how it actually scales your database across partitions on the backend. And you can also use Auto Scaling. DynamoDB streams is a feature that allows you to capture a time-ordered sequence of item level modifications in a table and it stores that information for up to 24 hours. DynamoDB accelerator is a fully managed in-memory cache for DynamoDB. So that reduces the latency to microseconds. And that runs on EC2 instances. There are various transaction options, including strongly consistent and eventually consistent reads and support for ACID transactions. We'll go into more detail about that. For backup, you get point in time recovery down to the second in the last 35 days and also on-demand backup and restore. And global tables is a fully managed multi-region, multi-master solution. So with global tables you can make changes in each of the regions your table is replicated to. So let's look at the core components of DynamoDB. Firstly we have a table. So everything you see here would constitute the contents of a table. Then we have items. An item is essentially a row in the DynamoDB table. And then we have attributes. The attribute is the information that's associated with each of the items in the database. For the exam it's worth understanding some of the API actions. Now all operations are categorized as control plane or data plane. So let's have a look at some control plane. API actions. For instance create table to create a new table, describe table to get information about an existing table, and list tables will return the names of all your tables in a list. Update table is where you're able to modify the settings of a table or its indexes. And then delete table will delete the table and all the contents. Data plane API actions can be performed using PartiQL, which is SQL compatible or classic DynamoDB, create, read, update, delete or CRUD APIs. So let's have a look at some examples. You've got PutItem to write a single item into a table. BatchWriteItem so you can actually write up to 25 items to a table. So that's more efficient. And then GetItem which will retrieve a single item from a table. BatchGetItem retrieves up to 100 items from one or more tables. So the batch options give you more efficiency when you have large reads or large writes. Update item will modify one or more attributes in an item. And we have delete item to delete a single item from a table. Now let's have a look at some of the supported data types. DynamoDB does support many data types and they're categorized as follows. We've got scalar. A scalar type can represent exactly one value. And those are number, string, binary, boolean, and null. We've then got document types. Those are list and map. And we've got set types. A set type represents multiple scalar values and those are set, number set, and binary set. Now there are a couple of classes of table we can use. We've got the standard which is the default and it's recommended for most workloads. We've then got DynamoDB Standard Infrequent Access or DynamoDB Standard IA. This is lower cost storage for tables that store infrequently accessed data. For example, application logs, old social media posts, E-commerce order history, or past gaming achievements. Let's move on to access control. Access control is managed using IAM. So its identity-based policies that we're using to control access to DynamoDB. You can attach a permissions policy to a user or a group in your account and you can apply a permissions policy to a role. And you can grant cross-account permissions through that option as well. DynamoDB does not support resource-based policies. you can use a special IAM condition to restrict user access to only their own records. The primary DynamoDB resources are tables but it also supports additional resource types, indexes, and streams. You can create indexes and streams only in the context of an existing table. So there are several resources of the actual DynamoDB table. The resources and sub resources will have unique ARNs of their own. And we can see in the table here what the format is. So we've got a table, an index, and a stream and you can see the format of the ARN. Of course where it's highlighted in red that's that's where you would actually replace these values with your region, your account ID, and then your table name or your stream label. Now let's have a look at a couple of example policies. The following example policy grants permissions for one DynamoDB action. That's DynamoDB list tables. So we can see the policy here. The effect is allow. The action is DynamoDB list tables. The resource in this case is *, so any DynamoDB table. The resource is * so that means any table. So it's not specifying a specific ARN. Let's look at another policy. This one grants permissions for three DynamoDB actions. And we can see those are DynamoDB describe table, query, and scan and in this case we're actually specifying the ARN of one individual table. That table is named Books. So the actions that are allowed through this policy will only be allowed on that specific table. So that's it for some of the core fundamentals of DynamoDB. We've got lots more to get on with, and I'll see you in the next lesson. \ No newline at end of file diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Capacity Units (RCU-WCU).md b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Capacity Units (RCU-WCU).md new file mode 100644 index 00000000..a229538b --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Capacity Units (RCU-WCU).md @@ -0,0 +1,212 @@ +#course/aws/developer-associate + +Hi guys, in this lesson I'm going to cover DynamoDB capacity units, RCUs and WCUs. For the exam you need to know what these are and also how to calculate them. + +So there's a couple of [[DynamoDB Capacity Modes|modes]] that we can apply to our table. + +# Provisioned Capacity + +The first is known as [[DynamoDB Provisioned|provision capacity]] and this is the default setting. In this case you specify the reads and writes per second. You can also enable Auto Scaling for dynamic adjustments. And the capacity is specified using both read capacity units and write capacity units. + +So in the example here we can see we've got read capacity and write capacity, we've got Auto Scaling on, and we've specified the minimum and the maximum and also a target utilization, so what is the utilization that we want to target? + +![[Pasted image 20240506143842.png]] + +## RCUs + +So let's look into some more detail of RCUs, read capacity units. + +- Each API called to read data from your table is a read request. +- Read requests can be strongly consistent, eventually consistent, or transactional. + +Now think of these as credit. So an RCU is essentially a credit that gives you the ability to use some of the capacity of DynamoDB. Now for items up to 4 KB in size one RCU will equal: +- one strongly consistent read per second, +- two eventually consistent reads per second or +- 0.5 transactional read requests per second. + +For items that are larger than 4 KB, these will require additional RCUs. Now you do need to understand how to calculate RCUs for a given use case. So let's have a look. On the left, we have the requirements and on the right the RCUs needed. So based on the information on the previous slide, we know how to calculate. + +- So in this case we've got 10 strongly consistent reads per second of 4 KB each. So that's 10 * 4 KB / 4 KB = 10 RCUs. And we know this is correct because one RCU equals one strongly consistent read. +- In the next example we require 10 strongly consistent reads per second of 11 KB each. Now we need to round 11 up to 12 because one RCU is up to 4 KB. So we've got 10 * 12 and then we're dividing it by 4 KB. So we need 30 RCUs. +- In the next example we need 20 eventually consistent reads per second of 12 KB each. Now in this case the calculation is 20/2 * 12/4 and that also equals 30 RCUs. Remember that one RCU equals two eventually consistent reads per second. +- In the next example we have 36 eventually consistent reads per second of 16 KB each. So in this case it's 36/2 * 16/4 and we need 72 RCU. + +![[Pasted image 20240506145524.png]] + +Now there is a nice tool in the console where you can put in your requirements and it tells you the RCU or WCUs you need. So we'll look at that in the hands-on. + +## WCUs + +Now let's move on to write capacity units. Each API call to write data is a write request. For items up to 1 KB in size one WCU can perform: +- one standard write requests per second or +- 0.5 transactional write requests per second. + +Items larger than 1 KB require additional WCUs. So let's look at some examples. + +- In the first requirement we need 10 standard writes per second of 4 KB each. That's simply 10 * 4=40 WCUs. +- 12 standard writes per second of 9.5 KB each gets rounded up to 10. So we've got 12 * 10 which is simply 120 WCUs. It's a bit easier with WCUs. +- Next we have 12 transactional writes per second of four KB each. In this case it's 10 * 2 * 4= 80 WCU. And that's because one WCU equals 0.5 transactional write requests per second. +- In the last example we have 12 transactional write requests per second of 9.5 KB so that will get rounded up to 10. That's 12 * 2 * 10=240 WCU. + +![[Pasted image 20240506150447.png]] +# On-demand Capacity + +If we don't want to use provision capacity, we can also use on-demand capacity. With on-demand, you don't need to specify your requirements. DynamoDB instantly scales up and down based on the activity of your application. So this is really good for unpredictable or spiky workloads or new workloads where you don't really understand the resource requirements well. You pay for what you use and that's per request. And you can see here where you can specify whether you want to use on-demand or provisioned and you can change between these. + +---- + +# Questions + +What is the block size for DynamoDB RCU/WCU accounting:: 4KB/1KB + + +What are the DynamoDB read types (consistency models etc), and how many RCUs does it take to read 4KB item +? +(1) one strongly consistent read per second +(2) two eventually consistent reads per second or +(3) 0.5 transactional read requests per second. + +How many RCUs are needed for 10 strongly consistent reads per second of 11 KB each::Now we need to round 11 up to 12 because one RCU is up to 4 KB. So we've got 10 * 12 and then we're dividing it by 4 KB. So we need 30 RCUs. + + +What are the DynamoDB write types, and how many WCUs does it take to write 1KB item +? +(1) one standard write per second +(2) 0.5 transactional write requests per second. + + +How many WCUs are needed for 12 transactional write requests per second of 9.5 KB::so that will get rounded up to 10. That's 12 * 2 * 10=240 WCU. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Consistency Models and Transactions.md b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Consistency Models and Transactions.md new file mode 100644 index 00000000..dbe80ba1 --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Consistency Models and Transactions.md @@ -0,0 +1,57 @@ +#course/aws/developer-associate + +In this lesson, I'm going to cover [[DynamoDB Consistency Models|DynamoDB consistency models]] and transactions. + +# Consistency Models + +DynamoDB supports eventually consistent as well as strongly consistent reads. + +Now, [[Eventually Consistent Read|eventually consistent reads]] means that when you read data from a table, the response might not reflect the results of a recently completed write operation. The response might include some stale data. And if you repeat your read request after a short time, the response should return the latest data. + +We then have the [[Strongly Consistent Read|strongly consistent read]]. In this case, a response is returned with the most up to date data reflecting the updates from all prior write operations that were successful. + +A strongly consistent read may not be available if there is a network delay or outage. In that case you might get a server error with an HTTP 500 code. + +Strongly consistent reads also have higher latency than eventually consistent reads. Strongly consistent reads are not supported on [[Global Secondary Index (GSI)|global secondary indexes]] and they use more throughput capacity than eventually consistent reads and we'll be able to calculate that soon. + +So let's look at a diagram to visualize what I'm talking about. Here we have an application, our DynamoDB table, and the actual backend of the table is spread across three availability zones within a region. + +![[Pasted image 20240504223518.png]] + +So the application puts an item into the table. That item is then written into the table and it's going to get replicated. Now let's say you immediately perform a get item straight after the put item. And in this case DynamoDB has sent the read request to a partition over in table A here or a partition that doesn't have the replicated data in it at this point because we can see the full sync has not happened. Now in this case, it's an eventually consistent read. So we actually get a failure. + +Now a short while later the replication continues. That's all complete. And if we try and read again then we get a success. With a strongly consistent read, data will always be returned when reading after a successful write and eventually consistent reads are the default. + +Now, how do you configure strongly consistent reads? Well, you can do so with the API when you issue get item query and scan APIs. And you need to set the consistent read to true. + +# Transactions + +Next we have something called [[DynamoDB Transactions|DynamoDB transactions]]. In this case, the table will make coordinated all or nothing changes to multiple items within and across tables. + +Transactions provide ACID compliance. So that's atomicity, consistency, isolation, and durability. + +It enables reading and writing of multiple items across multiple tables as an all or nothing operation. So in other words it either succeeds everywhere or nowhere. + +DynamoDB checks for a prerequisite condition before writing to the table. With the transaction write API, you can group multiple Put, Update, Delete, and ConditionCheck actions. And you can submit them as a single TransactWriteItems operation that either succeeds or it fails. The same is true for multiple Get actions which you can group and submit as a single TransactGetItems operation. There's no additional cost to enable transactions for DynamoDB tables and you only pay for the reads or writes that are part of your transaction. + +DynamoDB performs two underlying reads or writes of every item in the transaction. That's one to prepare the transaction and then one to commit the transaction. + +So let's see it in action. We have an Amazon EC2 instance here. And we have two different tables across two different accounts account A and account B. Now we used to transact write items API to try and write to both tables simultaneously. Now the write in account A fails. That means the write in account B will fail as well because it's an all or nothing transaction. In another case, we rerun the TransactWriteItems and in this case it succeeds in both locations. So because it succeeds in both locations, all is good. If one fails, both fails, so remember that. Everything has to succeed all or nothing. And that's it for this lesson. + +---- +# Questions + +What are the two consistency models supported by DynamoDB::Eventually consistent read, strongly consistent read + + +Are strongly consistent reads supported for reads involving (1) local secondary indexes (2) global secondary indexes (3) neither (4) both:: local secondary indexes only + + +With DynamoDB where is the consistency model specified::for individual query and scan operations + + +What does the acronym ACID mean::atomicity, consistency, isolation, and durability + + +What types of APIs can be grouped together within a single DynamoDB transaction:: (1) multiple Put, Update, Delete, and ConditionCheck actions (2) multiple get actions. + \ No newline at end of file diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB LSI and GSI.md b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB LSI and GSI.md new file mode 100644 index 00000000..f8f248a8 --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB LSI and GSI.md @@ -0,0 +1,56 @@ +--- +sr-due: 2024-08-01 +sr-interval: 3 +sr-ease: 269 +--- + +#course/aws/developer-associate #review + +In this lesson, I'm going to cover what are called [[Local Secondary Index (LSI)|Local Secondary Indexes]] and [[Global Secondary Index (GSI)|Global Secondary Indexes]], LSIs and GSIs. + +# Local Secondary Indexes + +So firstly let's look at the LSIs. These provide an alternative sort key to use for scans and queries. + +You can create up to five LSIs per table. And they must be created at the creation time for your DynamoDB table. You cannot add, remove, or modify them later on. + +The LSI has the same partition key as your original table, but it has a different sort key. So this helps you being able to perform scans and queries that you can't do in your primary table. Essentially it gives you a different view of your data organized by the alternative sort key. + +Any queries based on the sort key are much faster using the index than the main table. So let's have a look at the diagram. We have our primary table. And I'm not showing the attributes. I'm just showing the partition key and the sort key here. + +So we have client ID and then we have created. Now what we might want to do is create an index from our primary table. And this LSI has a partition key and the partition key is always going to be the same, so it's client ID. But in this case the sort key is the SKU. + +![[Pasted image 20240507170722.png]] + +Attributes can be optionally [[Attribute Projection|projected]] and that means that they will be actually put into the LSI as well. So it really depends what exactly you're searching for. So remember the sort key is different on the LSI, but the partition key is always the same. + +Now let's have a look at a couple of examples of querying. So in this example, querying the main table we must use the partition key client ID and the sort key created. But in another example with an LSI, we can query the index for any orders made by a certain user with the SKU because we can have a different sort key than the main table. + +# GSI + +Next we have the Global Secondary Index. This is used to speed up queries on non-key attributes. So those are not part of the partition key. You can create these when you create your table or at any time. And you can specify a different partition key as well as a different sort key. It gives a completely different view of the data. And it speeds up any queries relating to this alternative partition key and sort key. + +So again, let's have a look at an example. We have a primary table here with a partition key or client ID. And a sort key is created. The index is created again from the primary table. And with the GSI, in this case we've got a completely different partition key and a different sort key. And again optionally, we can project attributes into the GSI if we wish to. So let's look at an example. And in this one we can query the index for orders of the SKU where the quantity is greater than one. + +And that's because we have a different partition key and a different sort key. + +---- + +# Questions + +What are 3 limitations of DynamoDB LSIs +? +(1) must have the same partition key as the table itself +(2) a maximum of five LSIs per table +(3) the LSIs must be specified at the time that the table is created, can't be changed afterwards + + +What is DynamoDB attribute projection::Attributes that are actually stored within the LSI + + +Which is more flexible - DynamoDB LSIs or GSIs and give 2 reasons +? +GSIs are more flexible - +(1) they can be created at any time, not just when the table is created +(2) they can have a different partition key than the base table + diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Optimistic Locking and Conditional Updates.md b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Optimistic Locking and Conditional Updates.md new file mode 100644 index 00000000..84fb3166 --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Optimistic Locking and Conditional Updates.md @@ -0,0 +1,34 @@ +#course/aws/developer-associate + +In this lesson, we're going to look at two different strategies for protecting the consistency of our data in DynamoDB. One's called optimistic locking and then we'll talk about conditional updates. + +# Optimistic Locking + +So first [[Optimistic Locking|optimistic locking]]. This is a strategy to ensure that the client-side item you're updating or deleting is the same as the item in DynamoDB. And it protects the database writes from being overwritten by the writes of others or vice versa. + +So best to understand by using an example. So we have a DynamoDB table and we can see some of the items in that table. And here we have a SKU and a price. Now what's happening is application instance here on the left wants to update SKU 1 from 1,299 to 1,499. And that's successful. But then application instance two here then tries to update the SKU 1 to 1,399 and that's also successful. Now perhaps the application on the right hand side here was supposed to write that update before the subsequent update to 1499 but maybe there was some kind of delay in processing. + +So now we've got the wrong price in the table. Now, a way to resolve that is now what we do with optimistic locking is we specify update SKU 1 to 1,499 if the item version equals one. So it gets updated. Then the other application instance comes along, but it was also given instructions to update SKU 1 to 1399 if item version is one. + +And that fails because the item was already updated. So that's optimistic locking. + +![[Pasted image 20240507215621.png]] + +# Conditional Updates + +Next we have [[Conditional Write|conditional updates]]. To manipulate data in DynamoDB tables you use the put item, update item, and delete item API operations. You can optionally specify a condition expression to determine which items should be modified. And if the condition expression evaluates to true, the operation succeeds. Otherwise the operation will fail. So let's have a look at an example. + +This CLI command allows the right to proceed only if the item in question does not already have the same key. In another example, the CLI command uses attribute not exist to delete a product only if it does not have a price attribute. So in both cases we've got attribute not exists, here we've got ID, and here we've got price. + +![[Pasted image 20240507215928.png]] + +In another example, the CLI command only deletes an item if the product category is either sporting goods or gardening supplies and the price is between 500 and 600. So we've got multiple conditions here, product category in cat one and cat two and price between the low and the high that we specify. + +---- + +# Questions + +What is "optimistic" about DynamoDB's optimistic locking +? +At the start of the read - modify - write process, the item is simply read and no resource wasted to lock the record from use by other applications + \ No newline at end of file diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Partitions and Primary Keys.md b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Partitions and Primary Keys.md new file mode 100644 index 00000000..672d6b59 --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Partitions and Primary Keys.md @@ -0,0 +1,116 @@ +#course/aws/dynamo-db + + Hi guys. In this lesson, we're going to cover DynamoDB Partitions and Primary Keys. And this stuff is really important to understand so you can design your table for throughput and also searchability. + + DynamoDB stores data in partitions. And a partition is an allocation of storage for a table that's automatically replicated across multiple AZs within the region. DynamoDB manages the partitions fully for you. DynamoDB will always allocate sufficient partitions to support the provision throughput requirements. + + So when you specify the throughput you need on the frontend, and we'll see how to do that in this section, then DynamoDB will automatically take care of spreading your data across the relevant partitions. DynamoDB allocates additional partitions to a table in various situations. So if you increase the table's provisioned throughput settings beyond what the existing partitions can support, or if an existing partition fills to capacity and more storage is actually required. + + Now, for primary keys, there are two different types of primary key. We've got the partition keys and composite keys. + + A partition key must be a unique attribute such as a user ID. So every user will have their own individual ID. The value of the partition key is input to an internal hash function and that determines the partition or physical location on which the data is stored. If you're using the partition key as your primary key, we'll see what all this means in a moment, then no two items can have the same partition key. + + So let's look at an example table. On the left hand side here, we have a partition key. Now, in this case this is using a post ID, so maybe it's something like a forum. And there's a unique ID for each entry in the forum. And then on the right we have the attributes. And those are the information associated with each of these entries in the table. + + + You can also have something called a composite key. That is a partition key plus a {{sort key}} in combination. + + + So an example is a user posting to a forum. The partition key would be the user ID and the sort key would be the timestamp of the post. The two together mean that you can have multiple items in the table with the same partition key but they're going to have a different sort key and that creates uniqueness. Two items may have the same partition key but they must have a different sort key. + + All items with the same partition key are stored together and then they're sorted according to the sort key value. So that's why I said it's important to understand how this works in terms of performance and searchability. Using a composite key allows you to store multiple items with the same partition key. If you don't have a composite key with a sort key then you can only ever have one item in the table for each partition key entry. So let's have a look at an example. We've got a partition key here which is the client ID. We've then got a sort key which is the created timestamp. And together they form the primary key or composite key as it has both a partition key and a sort key together. + + So then we can have entries in the table such as this, where we have the email address of the user under the client ID and then some kind of timestamp in the created field. So we've got multiple items in the table. And we can of course in this situation with a composite key have multiple items from the same client ID, the same partition key. + + Then we might have our various attributes. And in this case we've got a whole range of different attributes. Now notice that the data structure here can be unpredictable. So for some items there are no attributes assigned. And that's because this is a store. We've got the SKU, S-K-U and then we've got the category, size, color and weight. And some of those don't apply to some entries in the table. DynamoDB evenly distributes provisions throughput using what's called a read capacity unit and a write capacity unit. + + So these are values you can specify. If your access pattern exceeds 3000 RCU or 1000 WCU for a single partition key value, your requests may be throttled. And reading or writing above the limit can be caused by a variety of issues including: + - uneven distribution of data due to the wrong choice of partition key, + - frequent access of the same key in a partition, the most popular item which is sometimes known as a hot key, or + - a request rate greater than the provision throughput. + + Let's go over some best practices for partition keys: + - Use [[High Cardinality|high cardinality]] attributes, things like mail ID, employee number, customer ID, session ID, and so on. So these are things which should be completely unique. So that gives you high cardinality. + - Use composite attributes so your customer ID plus your products ID plus your country code giving even more uniqueness. And then you might have the order date as the sort key. + - Cache popular items using DynamoDB accelerator for caching reads. And that will offload some of the impacts on your database. + - Add random numbers or digits from a predetermined range for write heavy use cases. For example, you might add a random suffix to an invoice number such as this one. So we've got the invoice number and then in red we have some random value that we're going to add to it. That will help with spreading the rights across different partitions. + + That's it for this lesson. I will see you in the next one. + +---- + +# Questions + +What is needed to have DynamoDB partitions replicated across multiple AZs::Nothing - happens automatically + + +DynamoDB allocates additional partitions to a table in which 2 situations +? +(1) So if you increase the table's provisioned throughput settings beyond what the existing partitions can support, or +(2) if an existing partition fills to capacity and more storage is actually required. + + +What are the two different types of primary key::We've got the partition keys and composite keys. + + +In what situation must the partition key be unique within a table::If the primary key is configured as a partition key and not a composite key + + +The elements present in a composite key are::Partition key and sort key + + +How can you add an item into a DynamoDB table with the same partition key/sort key combination as an existing item::Can't be done - the combination must be unique + + +What configuration of a DynamoDB table is necessary so that items with the same partition key can be spread over multiple partitions +? +Can't be done - All items with the same partition key are stored together + + +What are the RCU and WCU limits for a single partition key value::3000 RCU, 1000 WCU + +What are 3 causes of reading or writing above the RCU/WCU limits +? +(1) uneven distribution of data due to the wrong choice of partition key, +(2) frequent access of the same key in a partition, the most popular item which is sometimes known as a hot key, or +(3) a request rate greater than the provision throughput. + + +What is the definition of high cardinality::A data field that contains many distinct values + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Performance and Throttling.md b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Performance and Throttling.md new file mode 100644 index 00000000..c9f43bd0 --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Performance and Throttling.md @@ -0,0 +1,33 @@ +#course/aws/developer-associate + +In this lesson, I'm going to cover DynamoDB performance and throttling. + +# Throttling + +With throttling, this will occur when the configured RCU or WCU is exceeded. And you can get this error. So it's a `ProvisionedThroughputExceededException` error. and that indicates that the request rate is too high for the read/write capacity that's provisioned for the table. + +The AWS SDKs for DynamoDB will automatically retry requests that exceed this exception. The request is eventually successful unless the retry queue is too large to finish. Let's have a look at some possible causes of performance issues. + +- Firstly we've got hot keys. That simply means that one partition key is being read too often. +- We can also have hot partitions where our data access patterns are imbalanced. So we're trying to get a higher amount of read and writes going to one particular partition. And this comes back to how we designed our partition keys and are sort keys and our composite keys which we looked at earlier on in this section. +- Also the items may be too large and they're consuming more RCUs and WCUs because of the size. + +So what are the potential resolutions?: +- Well, you can reduce the frequency of requests and use exponential backoff. +- You can try and design your application for uniform activity across all logical partition keys in the table and any secondary indexes. + +# Burst Capacity + +You can use [[Burst Capacity|burst capacity]] effectively. And this is because DynamoDB will retain up to five minutes, so 300 seconds of unused read and write capacity which can be consumed in a short period of time. Say, if your workload is bursty rather than consistently high, that might save you. + +You should also ensure that adaptive capacity is enabled and it is the default. This feature will minimize throttling due to throughput exceptions. Now, that's it for this lesson. Just a few tips on performance and throttling. + +---- + +What is a benefit of using the AWS SDK when accessing DynamoDB tables configured for provisioned capacity mode::In the event of a ProvisionedThroughputExceededException, the SDK will automatically retry + + +Apart from price reduction, what is another benefit of minimizing data still in DynamoDB tables configured for provisioned capacity::Less data can mean less RCUs and WCUs (if it leads to a reduction in the number of blocks) + + +Over what duration will DynamoDB retained unused read and write capacity which can be consumed as burst capacity:: 5 minutes \ No newline at end of file diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Scan and Query API.md b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Scan and Query API.md new file mode 100644 index 00000000..fec2c5e9 --- /dev/null +++ b/docs/vaults/en/Computing/AWS/DynamoDB/DynamoDB Scan and Query API.md @@ -0,0 +1,52 @@ +#course/aws/developer-associate + +When searching for data in our DynamoDB tables we can use the scan API and the query API. And these come up in exam questions where you need to know the benefits and drawbacks of each and when to use them. So let's start with the scan API. + +# Scan + +The [[scan|scan]] operation returns one or more items and attributes by accessing every item in a table or a secondary index. + +To have DynamoDB return fewer items, you can provide a filter expression operation. Now note that accessing every item in a table or secondary index does incur costs because we're actually reading those items. A single scan operation reads up to the maximum number of items set if you use the limit parameter or a maximum of one megabyte. + +Scan APIs can use a lot of RCUs as they access every single item in your table. Scan operations proceed sequentially. + +Applications can request parallel scans using the segment and total segments parameters. The scan uses eventually consistent reads when accessing the data in a table. If you need a consistent copy of the data as of the time the scam begins you can set the consistent read parameter to true. + +So let's see what happens with a scan. In this case, the developer has sent a scan API request. And in this case there's only a few items in our table but all of them are actually accessed. Of course in a big table that's going to be a lot of RCUs. And then the results are returned. Now in this case, all items and all attributes are returned. + +![[Pasted image 20240506155555.png]] + +You can use a scan with a [[Projection Expression|projection expression]]. In this example, the scan returns all posts in the forum that were posted within a date range and they have more than 50 replies. So this is the scan. You can see here, we're doing this through the console. We're choosing a scan, sorting by post ID, filtering by replies and last post time. And we've got a date range here and a max number of replies. So let's see what a scan API with a projection expression looks like. So we issue the scan API with our projection expression. + +In this case, all of the items in the table are accessed using lots of RCUs. But in the results, select attributes are returned. + +# Query + +Next we have the query API. A [[query|query]] operation finds items in your table based on the primary key attribute and a distinct value to search for. + +So for example, you might search for the user ID and then all attributes related to that item would be returned. You can optionally use a sort key name and value to refine the results. + +So for example, if you're sort key is a timestamp, you can refine the query to only select items with a timestamp within the last seven days. All attributes are returned for the items by default. You can also use the projection expression parameter as we did with the scan API to just return the select attributes we need. + +By default, queries are eventually consistent. To use strongly consistent you need to explicitly set this in the query. + +So let's look at using a query API. A developer uses a query API request with a projection expression. Now in this case, select items with select attributes to access, consuming far fewer RCUs. And then those results are returned. In another example, the query returns only items with the client ID of chris@example.com that were created within a certain date range and that are in the category of pen. So here we can see how we're creating the query through the console. We've chosen the product orders table with client ID and created, we've specified as the string here chrisexample.com and then between a specific date range. We've also specified a filter for the category which is a string and it's equal to pen. So that's it for this lesson. Make sure you fully understand the differences between the scan and the query API before you sit the exam. + +---- + +# Questions + +When performing a DynamoDB scan operation, what limits are there on the amount of returned data::(1) the item limit specified in the scan request (2) 1 megabyte + + +What is the benefit of using a projection expression in a DynamoDB scan request given that it still reads all attributes and therefore no reduction in RCUs +? +It only returns the requested attributes, meaning less data transfer, less data to process by the client. + + +What is the disadvantage of the DynamoDB query operation compared with scan::It can only retrieve items with the specified partition key + + +How does DynamoDB scan and query compare regarding the projection expression::Both only return the requested attributes, but scan still reads all, whilst query only reads those requested. + + diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240504223518.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240504223518.png new file mode 100644 index 00000000..1d9dba43 Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240504223518.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506143842.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506143842.png new file mode 100644 index 00000000..f6ddf523 Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506143842.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506145524.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506145524.png new file mode 100644 index 00000000..88fe47ff Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506145524.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506150447.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506150447.png new file mode 100644 index 00000000..36b64006 Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506150447.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506155555.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506155555.png new file mode 100644 index 00000000..619eda67 Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240506155555.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507170722.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507170722.png new file mode 100644 index 00000000..3421babf Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507170722.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507215621.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507215621.png new file mode 100644 index 00000000..896bcb3a Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507215621.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507215928.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507215928.png new file mode 100644 index 00000000..b3d4b4aa Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507215928.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507224507.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507224507.png new file mode 100644 index 00000000..ca2e0cfc Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240507224507.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240508132354.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240508132354.png new file mode 100644 index 00000000..cb15fb8d Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240508132354.png differ diff --git a/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240508134406.png b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240508134406.png new file mode 100644 index 00000000..bcc6eb00 Binary files /dev/null and b/docs/vaults/en/Computing/AWS/DynamoDB/files/Pasted image 20240508134406.png differ diff --git a/docs/vaults/en/Math/Ellipse/Ellipse Equation.md b/docs/vaults/en/Math/Ellipse/Ellipse Equation.md new file mode 100644 index 00000000..685b3748 --- /dev/null +++ b/docs/vaults/en/Math/Ellipse/Ellipse Equation.md @@ -0,0 +1,29 @@ +--- +sr-due: 2024-08-31 +sr-interval: 3 +sr-ease: 266 +--- + +#review + + +The general equation of an ellipse is used to algebraically represent an ellipse in the coordinate plane. The equation of an ellipse can be given as, + +$\large \frac {x ^2} {a ^2}+\frac {y ^2} {b ^2}=1$ + +Where + +- 'a' represents the [[Semi-Major Axis|semi-major axis]] (half of the length of the major axis) +- 'b' represents the [[Semi-Minor Axis|semi-minor axis]] (half of the length of the minor axis) + +![[Pasted image 20230828144739.png]] + +---- +#flashcards/math +# Questions + +Given lengths a and b (half the length of the major and minor axes respectively), and a coordinate system with origin half way between an ellipse's foci, what is the ellipse's equation +? +$\large \frac {x ^2} {a ^2}+\frac {y ^2} {b ^2}=1$ + + diff --git a/docs/vaults/en/Math/Ellipse/Ellipse.md b/docs/vaults/en/Math/Ellipse/Ellipse.md new file mode 100644 index 00000000..c913f34f --- /dev/null +++ b/docs/vaults/en/Math/Ellipse/Ellipse.md @@ -0,0 +1,22 @@ +https://en.wikipedia.org/wiki/Ellipse + + +In mathematics, an ellipse is a: +- closed curve that +- is symmetric with respect to two perpendicular axes. +- It can be defined as the set of all points in a plane, such that the sum of the distances from any point on the curve to two fixed points (called the [[Focus|foci]]) is constant. +- The fixed points are called the foci and are denoted by F and F'. + +# See Also + +[[Ellipse Equation]] + +---- +#flashcards/math +# Questions + +In terms of the two foci of an ellipse, define an ellipse::It can be defined as the set of all points in a plane, such that the sum of the distances from any point on the curve to two fixed points is constant. + + + + diff --git a/docs/vaults/en/Math/Ellipse/Focus.md b/docs/vaults/en/Math/Ellipse/Focus.md new file mode 100644 index 00000000..a8d572b9 --- /dev/null +++ b/docs/vaults/en/Math/Ellipse/Focus.md @@ -0,0 +1,7 @@ +The ellipse has two foci and their coordinates are F(c, o), and F'(-c, 0). The distance between the foci is thus equal to 2c. + +---- +#flashcards/math +# Questions + +How many foci does an ellipse have:: 2 \ No newline at end of file diff --git a/docs/vaults/en/Math/Ellipse/Major Axis.md b/docs/vaults/en/Math/Ellipse/Major Axis.md new file mode 100644 index 00000000..2080b6a8 --- /dev/null +++ b/docs/vaults/en/Math/Ellipse/Major Axis.md @@ -0,0 +1,3 @@ +Major axis is the longest diameter of an ellipse. + +The length of the major axis of the ellipse is 2a units, and the end vertices of this major axis is (a, 0), (-a, 0) respectively. diff --git a/docs/vaults/en/Math/Ellipse/Minor Axis.md b/docs/vaults/en/Math/Ellipse/Minor Axis.md new file mode 100644 index 00000000..a189939a --- /dev/null +++ b/docs/vaults/en/Math/Ellipse/Minor Axis.md @@ -0,0 +1 @@ +Minor axis is the shortest diameter of an ellipse. The length of the minor axis of the ellipse is 2b units and the end vertices of the minor axis is (0, b), and (0, -b) respectively diff --git a/docs/vaults/en/Math/Ellipse/Semi-Major Axis.md b/docs/vaults/en/Math/Ellipse/Semi-Major Axis.md new file mode 100644 index 00000000..7fc649cd --- /dev/null +++ b/docs/vaults/en/Math/Ellipse/Semi-Major Axis.md @@ -0,0 +1 @@ +half of the length of the [[major axis]] \ No newline at end of file diff --git a/docs/vaults/en/Math/Ellipse/Semi-Minor Axis.md b/docs/vaults/en/Math/Ellipse/Semi-Minor Axis.md new file mode 100644 index 00000000..dd8b8476 --- /dev/null +++ b/docs/vaults/en/Math/Ellipse/Semi-Minor Axis.md @@ -0,0 +1 @@ +half of the length of the [[minor axis]] diff --git a/docs/vaults/en/Math/Ellipse/files/Pasted image 20230828144739.png b/docs/vaults/en/Math/Ellipse/files/Pasted image 20230828144739.png new file mode 100644 index 00000000..3fedf711 Binary files /dev/null and b/docs/vaults/en/Math/Ellipse/files/Pasted image 20230828144739.png differ diff --git a/docs/vaults/en/Math/Singular Matrix.md b/docs/vaults/en/Math/Singular Matrix.md new file mode 100644 index 00000000..e220c1ab --- /dev/null +++ b/docs/vaults/en/Math/Singular Matrix.md @@ -0,0 +1,18 @@ +https://www.coursera.org/learn/machine-learning-course/lecture/FuSWY/inverse-and-transpose + +A [[Matrix]] for which there is no [[Matrix Inverse]]. + +For example, the following does not have an inverse: + +![[Pasted image 20221018122853.png]] + +Similar to how with real numbers, 0 does not have an inverse. + +---- +#flashcards/math +# Questions + +What is a matrix that doesn't have an inverse called::Singular matrix + +What is a singular matrix::A matrix that doesn't have an inverse + \ No newline at end of file diff --git a/docs/vaults/en/Physics/Electric Field/Electric Field Diagram.md b/docs/vaults/en/Physics/Electric Field/Electric Field Diagram.md new file mode 100644 index 00000000..01f2287b --- /dev/null +++ b/docs/vaults/en/Physics/Electric Field/Electric Field Diagram.md @@ -0,0 +1,10 @@ +https://www.khanacademy.org/science/ap-physics-2/ap-2-electric-charge-electric-force-and-voltage/electric-field-ap2/v/electric-field-definition + +![[Pasted image 20230828112605.png]] + +---- +#flashcards/science/physics +# Questions + +On an electric field diagram, what is one way that the relative field strength at different parts is represented::By the length of the arrow + \ No newline at end of file diff --git a/docs/vaults/en/Physics/Electric Field/Electric Field Simulation.md b/docs/vaults/en/Physics/Electric Field/Electric Field Simulation.md new file mode 100644 index 00000000..e26525ad --- /dev/null +++ b/docs/vaults/en/Physics/Electric Field/Electric Field Simulation.md @@ -0,0 +1,10 @@ +--- +sr-due: 2024-08-31 +sr-interval: 1 +sr-ease: 230 +--- + + #review + +[[charges-and-fields_en.html]] + diff --git a/docs/vaults/en/Physics/Electric Field/Electric Field Strength and Charge Equation.md b/docs/vaults/en/Physics/Electric Field/Electric Field Strength and Charge Equation.md new file mode 100644 index 00000000..5f2ec91b --- /dev/null +++ b/docs/vaults/en/Physics/Electric Field/Electric Field Strength and Charge Equation.md @@ -0,0 +1,12 @@ +https://youtu.be/-LtvW5783zE + +![[Pasted image 20230919225731.png]] + + $\huge E = \frac {k \cdot Q} {r^2}$ + +---- +#flashcards/science/physics +# Questions + +In the equation $\huge E = \frac {k \cdot Q} {r^2}$, what is $\large k$ called and what is its value::Coulomb's constant, approximately $\large 8.99 \times 10 ^ 9$ + diff --git a/docs/vaults/en/Physics/Electric Field/Electric Field Strength and Force Equation.md b/docs/vaults/en/Physics/Electric Field/Electric Field Strength and Force Equation.md new file mode 100644 index 00000000..5bb6e4f9 --- /dev/null +++ b/docs/vaults/en/Physics/Electric Field/Electric Field Strength and Force Equation.md @@ -0,0 +1,18 @@ + + + +The field is denoted by E, and as it is a vector $\large \vec E$ it is given by the formula: + + $\huge \vec E = \frac {\vec F_e} C$ + + +---- +#flashcards/science/physics +# Questions + + +In terms of the field strength $\large \vec E$ at a particular point, what is the force experienced by a particle with charge $\large C$:: $\large \vec F_e = \vec E \times C$ + + +In the equation $\large \vec F_e = \vec E \times C$, both $\large \vec F_e$ and $\large \vec E$ are vectors. What is the relationship of the direction of the two?::For a positive charge, the force is in the same direction as the field, for a negative charge the force is in the opposite direction + diff --git a/docs/vaults/en/Physics/Electric Field/Electric Field.md b/docs/vaults/en/Physics/Electric Field/Electric Field.md new file mode 100644 index 00000000..38f7b187 --- /dev/null +++ b/docs/vaults/en/Physics/Electric Field/Electric Field.md @@ -0,0 +1,27 @@ +https://www.physicsclassroom.com/Class/estatics/u8l4a.cfm + +An electric field is a type of [[Field]]. + +Charges can either repel or attract when held some distance apart. + +The electric field is present around a charge regardless of whether or not there is another charge in the vicinity. + + + +![[Pasted image 20230828112824.png]] + +---- +#flashcards/science/physics +# Questions + +What symbol represents an electric field:: $\large \vec E$ + + +At a particular distance from charge $\large Q_1$ there is an electric field strength of $\large \vec E_1$. How is the field strength $\large \vec E_1$ altered by the placement of another charge $\large Q_2$ at that same position + +In what direction is the electric field around a positive charge::Radially away from the charge + + +What type of electric charge has an electric field pointing radially towards it::A negative charge + + diff --git a/tests/e2e/flashcards.test.js b/docs/vaults/en/Physics/Electric Field/Obsidian Testing.md similarity index 100% rename from tests/e2e/flashcards.test.js rename to docs/vaults/en/Physics/Electric Field/Obsidian Testing.md diff --git a/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230828112605.png b/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230828112605.png new file mode 100644 index 00000000..5d08600f Binary files /dev/null and b/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230828112605.png differ diff --git a/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230828112824.png b/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230828112824.png new file mode 100644 index 00000000..173a5232 Binary files /dev/null and b/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230828112824.png differ diff --git a/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230919225731.png b/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230919225731.png new file mode 100644 index 00000000..3aa8e151 Binary files /dev/null and b/docs/vaults/en/Physics/Electric Field/files/Pasted image 20230919225731.png differ diff --git a/docs/vaults/en/Physics/Electric Field/files/charges-and-fields_en.html b/docs/vaults/en/Physics/Electric Field/files/charges-and-fields_en.html new file mode 100644 index 00000000..41963f12 --- /dev/null +++ b/docs/vaults/en/Physics/Electric Field/files/charges-and-fields_en.html @@ -0,0 +1,672 @@ + + + + + + + + + + + + + + + + + ‪Charges and Fields‬ 1.0.58 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/vaults/en/User Guide/Cloze with blank lines.md b/docs/vaults/en/User Guide/Cloze with blank lines.md new file mode 100644 index 00000000..23f9ec11 --- /dev/null +++ b/docs/vaults/en/User Guide/Cloze with blank lines.md @@ -0,0 +1,9 @@ +#flashcards + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. ==Nam efficitur et nulla vel porttitor. Nam a neque in massa egestas rutrum.== Maecenas in nulla ipsum. Donec a lobortis justo. Donec tincidunt lorem dui. Praesent nulla nisi, facilisis vitae tincidunt nec, mollis id eros. + +Duis eget volutpat magna. Nam sit amet est ac diam venenatis sollicitudin. Aliquam finibus ante quis bibendum dignissim. ==Maecenas quis semper risus==. Nam condimentum a tellus et vestibulum. Nullam porta arcu in orci efficitur, sed suscipit urna imperdiet. + +Phasellus sodales dictum erat sit amet posuere. ==Vestibulum volutpat, turpis at bibendum mollis, turpis leo elementum lacus, ac finibus mi orci ac enim.== Quisque porta eleifend diam, sit amet sagittis odio pulvinar sed. Aenean tristique enim eu dui pulvinar, eget ornare ante scelerisque. + ++++ diff --git a/docs/vaults/en/User Guide/Cloze with table using+++.md b/docs/vaults/en/User Guide/Cloze with table using+++.md new file mode 100644 index 00000000..ed5fbb34 --- /dev/null +++ b/docs/vaults/en/User Guide/Cloze with table using+++.md @@ -0,0 +1,13 @@ + +#flashcards + + +| Name | Description | +| ------------------------ | ------------------------------------------------------------------------------------ | +| ==[[Amazon Inspector]]== | ==Automated and continual vulnerability management at scale (within EC2 instances)== | +| [[AWS GuardDuty]] | Protect your AWS accounts with intelligent threat detection | +| [[AWS Security Hub]] | ==Automate AWS security checks and centralize security alerts== | +| [[AWS Shield]] | Maximize application availability and responsiveness with managed DDoS protection | + ++++ + diff --git a/docs/vaults/en/User Guide/Context.md b/docs/vaults/en/User Guide/Context.md new file mode 100644 index 00000000..80436b6a --- /dev/null +++ b/docs/vaults/en/User Guide/Context.md @@ -0,0 +1,15 @@ + +#flashcards + +# Trivia + +## Capitals + +### Africa + +Kenya::Nairobi + + +### North America + +Canada::Ottawa diff --git a/docs/vaults/en/User Guide/Multiline with blank lines in answer.md b/docs/vaults/en/User Guide/Multiline with blank lines in answer.md new file mode 100644 index 00000000..ab91ca9e --- /dev/null +++ b/docs/vaults/en/User Guide/Multiline with blank lines in answer.md @@ -0,0 +1,28 @@ +#flashcards + +# Table with no preceding blank line + +List 4 AWS security related services +? +| Name | Description | +| -------------------- | --------------------------------------------------------------------------------- | +| [[Amazon Inspector]] | Automated and continual vulnerability management at scale (within EC2 instances) | +| [[AWS GuardDuty]] | Protect your AWS accounts with intelligent threat detection | +| [[AWS Security Hub]] | Automate AWS security checks and centralize security alerts | +| [[AWS Shield]] | Maximize application availability and responsiveness with managed DDoS protection | + +# Table with preceding blank line + +List 4 AWS security related services +? + +| Name | Description | +| -------------------- | --------------------------------------------------------------------------------- | +| [[Amazon Inspector]] | Automated and continual vulnerability management at scale (within EC2 instances) | +| [[AWS GuardDuty]] | Protect your AWS accounts with intelligent threat detection | +| [[AWS Security Hub]] | Automate AWS security checks and centralize security alerts | +| [[AWS Shield]] | Maximize application availability and responsiveness with managed DDoS protection | + + + + diff --git a/docs/vaults/en/User Guide/Table with preceding blank line and+++.md b/docs/vaults/en/User Guide/Table with preceding blank line and+++.md new file mode 100644 index 00000000..72088ce7 --- /dev/null +++ b/docs/vaults/en/User Guide/Table with preceding blank line and+++.md @@ -0,0 +1,14 @@ +#flashcards + +List 4 AWS security related services +? + +| Name | Description | +| -------------------- | --------------------------------------------------------------------------------- | +| [[Amazon Inspector]] | Automated and continual vulnerability management at scale (within EC2 instances) | +| [[AWS GuardDuty]] | Protect your AWS accounts with intelligent threat detection | +| [[AWS Security Hub]] | Automate AWS security checks and centralize security alerts | +| [[AWS Shield]] | Maximize application availability and responsiveness with managed DDoS protection | + ++++ + diff --git a/docs/vaults/en/Welcome.md b/docs/vaults/en/Welcome.md new file mode 100644 index 00000000..1fb12f5e --- /dev/null +++ b/docs/vaults/en/Welcome.md @@ -0,0 +1,5 @@ +This is your new *vault*. + +Make a note of something, [[create a link]], or try [the Importer](https://help.obsidian.md/Plugins/Importer)! + +When you're ready, delete this note and make the vault your own. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..0ebc2eaf --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,61 @@ +import eslint from "@eslint/js"; +import eslintPluginUnicorn from "eslint-plugin-unicorn"; +import simpleImportSort from "eslint-plugin-simple-import-sort"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + plugins: { + "simple-import-sort": simpleImportSort, + unicorn: eslintPluginUnicorn, + }, + rules: { + "linebreak-style": 0, + quotes: ["warn", "double", "avoid-escape"], + semi: ["error", "always"], + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "unicorn/filename-case": [ + "error", + { + case: "kebabCase", + }, + ], + "simple-import-sort/imports": [ + "error", + { + groups: [["^"], ["^src"], ["^\\.", "^tests"]], + }, + ], + }, + }, + { + files: ["tests/**"], + rules: { + "@typescript-eslint/no-require-imports": "off", + }, + }, + { + files: ["src/**"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["./", "../"], + message: "Relative imports are not allowed in src/.", + }, + ], + }, + ], + }, + }, +); diff --git a/jest.config.js b/jest.config.js index 63690224..23a31f1b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,32 +9,38 @@ module.exports = { }, moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json", "node", "d.ts"], roots: ["/src/", "/tests/unit/"], - collectCoverageFrom: [ - "src/**/lang/*.ts", - "src/NoteEaseList.ts", - "src/NoteFileLoader.ts", - "src/NoteParser.ts", - "src/NoteQuestionParser.ts", - "src/TopicParser.ts", - "src/parser.ts", - "src/scheduling.ts", - "utils.ts", - ], + collectCoverageFrom: ["src/**"], coveragePathIgnorePatterns: [ - "/node_modules/", - "src/lang/locale/", - "src/constants", - "src/icons", + // node modules & build output + "build/", + "node_modules/", + + // GUI & Obsidian coupled code + "src/app-core.ts", + "src/sr-file.ts", + "src/gui/", + "src/icons/", + "src/main.ts", + "src/next-note-review-handler.ts", + "src/plugin-data.ts", + "src/settings.ts", + "src/utils/renderers.ts", + + // debugging utils + "src/utils/debug.ts", + + // don't include in results "src/declarations.d.ts", - "build", + "src/lang/locale/", ], coverageDirectory: "coverage", collectCoverage: true, coverageProvider: "v8", coverageThreshold: { global: { - statements: 100, - branches: 100, + // TODO: Bring coverage back up to 98%+ + statements: 93, + branches: 89, }, }, }; diff --git a/manifest.json b/manifest.json index 5701fb06..5f2f64dc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "id": "obsidian-spaced-repetition", "name": "Spaced Repetition", - "version": "1.12.5", - "minAppVersion": "0.15.4", + "version": "1.12.7", + "minAppVersion": "1.2.8", "description": "Fight the forgetting curve by reviewing flashcards & entire notes.", "author": "Stephen Mwangi", "authorUrl": "https://github.com/st3v3nmw", diff --git a/mkdocs.yml b/mkdocs.yml index 8f58b9c4..9e3b7018 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,11 +7,12 @@ site_description: >- repo_name: st3v3nmw/obsidian-spaced-repetition repo_url: https://github.com/st3v3nmw/obsidian-spaced-repetition/ edit_uri: "" +docs_dir: docs/docs theme: name: material - logo: assets/favicon.ico - favicon: assets/favicon.ico + logo: favicon.ico + favicon: favicon.ico icon: repo: fontawesome/brands/github palette: @@ -25,16 +26,31 @@ theme: name: Switch to light mode features: - navigation.top + - navigation.expand - toc.follow nav: - Index: index.md - - Flashcards: flashcards.md + - Flashcards: + - Flashcards Overview: flashcards/flashcards-overview.md + - Question & Answer Cards: flashcards/q-and-a-cards.md + - Cloze Cards: flashcards/basic-cloze-cards.md + - Cards with Blank Lines: flashcards/cards-with-blank-lines.md + - Organizing into Decks: flashcards/decks.md + - Reviewing & Cramming: flashcards/reviewing.md + - Statistics: flashcards/statistics.md - Notes: notes.md - - Contributing: contributing.md - - Algorithms: algorithms.md - - Changelog: changelog.md - - License: license.md + - User Options & Commands: + - User Options: user-options.md + - Commands: plugin-commands.md + - Algorithms & Data Storage: + - Repetition Algorithms: algorithms.md + - Data Storage: data-storage.md + - Additional: + - Spaced Repetition Guides: resources.md + - Contributing: contributing.md + - Changelog: changelog.md + - License: license.md plugins: - search @@ -61,7 +77,11 @@ markdown_extensions: - footnotes - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - md_in_html + +extra_css: + - extra.css remote_branch: gh-pages remote_name: origin diff --git a/package.json b/package.json index f6db1263..269a553f 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,55 @@ { "name": "obsidian-spaced-repetition", - "version": "1.12.5", + "version": "1.12.7", "description": "Fight the forgetting curve by reviewing flashcards & entire notes.", "main": "main.js", "scripts": { "build": "node esbuild.config.mjs production", "dev": "node esbuild.config.mjs", - "format": "npx prettier --write .", - "lint": "npx prettier --check . && npx eslint src/", - "test": "jest", - "changelog": "auto-changelog --template=compact --package && npx prettier --write docs/changelog.md", - "e2e": "make setup_e2e && wdio run ./wdio.conf.js" + "format": "pnpm prettier --write . && pnpm eslint src/ tests/ --fix", + "lint": "pnpm prettier --check . && pnpm eslint src/ tests/", + "test": "pnpm jest", + "changelog": "pnpm auto-changelog --template=compact --package && pnpm prettier --write docs/docs/changelog.md" }, "keywords": [ "obsidian", - "spaced-repetition", + "spaced repetition", + "flashcard", "flashcards" ], "author": "Stephen Mwangi", "license": "MIT", "devDependencies": { - "@types/jest": "^29.5.12", - "@types/node": "^20.11.24", + "@eslint/js": "^9.11.0", + "@types/eslint__js": "^8.42.3", + "@types/jest": "^29.5.13", + "@types/node": "^22.5.5", "@types/vhtml": "^2.2.9", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "@wdio/cli": "^8.32.3", - "@wdio/local-runner": "^8.32.3", - "@wdio/mocha-framework": "^8.32.3", - "@wdio/spec-reporter": "^8.32.2", - "auto-changelog": "^2.4.0", - "builtin-modules": "^3.3.0", - "chai": "^4.4.1", - "esbuild": "~0.19.12", - "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.6.0", + "@typescript-eslint/parser": "^8.6.0", + "auto-changelog": "^2.5.0", + "builtin-modules": "^4.0.0", + "esbuild": "^0.23.1", + "eslint": "^9.11.0", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-unicorn": "^55.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-expect-message": "^1.1.3", "moment": "^2.30.1", - "obsidian": "^1.5.7", - "prettier": "^3.2.5", - "ts-jest": "^29.1.2", - "tslib": "2.6.1", - "typescript": "5.1.6", - "vhtml": "^2.2.0", - "wdio-chromedriver-service": "^8.1.1" + "obsidian": "^1.7.2", + "prettier": "^3.3.3", + "ts-jest": "^29.2.5", + "tslib": "^2.7.0", + "typescript": "~5.5.4", + "typescript-eslint": "^8.6.0", + "vhtml": "^2.2.0" }, "dependencies": { - "chart.js": "^4.4.2", - "pagerank.js": "^1.0.2" + "chart.js": "^4.4.4", + "minimatch": "^10.0.1", + "pagerank.js": "^1.0.2", + "peggy": "^4.0.3" }, - "packageManager": "^pnpm@8.15.4" + "packageManager": "^pnpm@9.10.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f08b92ce..bab79c50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,58 +6,61 @@ settings: dependencies: chart.js: - specifier: ^4.4.2 - version: 4.4.2 + specifier: ^4.4.4 + version: 4.4.4 + minimatch: + specifier: ^10.0.1 + version: 10.0.1 pagerank.js: specifier: ^1.0.2 version: 1.0.2 + peggy: + specifier: ^4.0.3 + version: 4.0.3 devDependencies: + '@eslint/js': + specifier: ^9.11.0 + version: 9.11.0 + '@types/eslint__js': + specifier: ^8.42.3 + version: 8.42.3 '@types/jest': - specifier: ^29.5.12 - version: 29.5.12 + specifier: ^29.5.13 + version: 29.5.13 '@types/node': - specifier: ^20.11.24 - version: 20.11.24 + specifier: ^22.5.5 + version: 22.5.5 '@types/vhtml': specifier: ^2.2.9 version: 2.2.9 '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.1.6) + specifier: ^8.6.0 + version: 8.6.0(@typescript-eslint/parser@8.6.0)(eslint@9.11.0)(typescript@5.5.4) '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.0)(typescript@5.1.6) - '@wdio/cli': - specifier: ^8.32.3 - version: 8.32.3(typescript@5.1.6) - '@wdio/local-runner': - specifier: ^8.32.3 - version: 8.32.3(typescript@5.1.6) - '@wdio/mocha-framework': - specifier: ^8.32.3 - version: 8.32.3 - '@wdio/spec-reporter': - specifier: ^8.32.2 - version: 8.32.2 + specifier: ^8.6.0 + version: 8.6.0(eslint@9.11.0)(typescript@5.5.4) auto-changelog: - specifier: ^2.4.0 - version: 2.4.0 + specifier: ^2.5.0 + version: 2.5.0 builtin-modules: - specifier: ^3.3.0 - version: 3.3.0 - chai: - specifier: ^4.4.1 - version: 4.4.1 + specifier: ^4.0.0 + version: 4.0.0 esbuild: - specifier: ~0.19.12 - version: 0.19.12 + specifier: ^0.23.1 + version: 0.23.1 eslint: - specifier: ^8.57.0 - version: 8.57.0 + specifier: ^9.11.0 + version: 9.11.0 + eslint-plugin-simple-import-sort: + specifier: ^12.1.1 + version: 12.1.1(eslint@9.11.0) + eslint-plugin-unicorn: + specifier: ^55.0.0 + version: 55.0.0(eslint@9.11.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.11.24) + version: 29.7.0(@types/node@22.5.5) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 @@ -68,34 +71,29 @@ devDependencies: specifier: ^2.30.1 version: 2.30.1 obsidian: - specifier: ^1.5.7 - version: 1.5.7(@codemirror/state@6.4.1)(@codemirror/view@6.24.1) + specifier: ^1.7.2 + version: 1.7.2(@codemirror/state@6.4.1)(@codemirror/view@6.33.0) prettier: - specifier: ^3.2.5 - version: 3.2.5 + specifier: ^3.3.3 + version: 3.3.3 ts-jest: - specifier: ^29.1.2 - version: 29.1.2(@babel/core@7.24.0)(esbuild@0.19.12)(jest@29.7.0)(typescript@5.1.6) + specifier: ^29.2.5 + version: 29.2.5(@babel/core@7.25.2)(esbuild@0.23.1)(jest@29.7.0)(typescript@5.5.4) tslib: - specifier: 2.6.1 - version: 2.6.1 + specifier: ^2.7.0 + version: 2.7.0 typescript: - specifier: 5.1.6 - version: 5.1.6 + specifier: ~5.5.4 + version: 5.5.4 + typescript-eslint: + specifier: ^8.6.0 + version: 8.6.0(eslint@9.11.0)(typescript@5.5.4) vhtml: specifier: ^2.2.0 version: 2.2.0 - wdio-chromedriver-service: - specifier: ^8.1.1 - version: 8.1.1(webdriverio@8.32.3) packages: - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true - /@ampproject/remapping@2.3.0: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -104,35 +102,35 @@ packages: '@jridgewell/trace-mapping': 0.3.25 dev: true - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + /@babel/code-frame@7.24.7: + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 + '@babel/highlight': 7.24.7 + picocolors: 1.1.0 dev: true - /@babel/compat-data@7.23.5: - resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + /@babel/compat-data@7.25.4: + resolution: {integrity: sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/core@7.24.0: - resolution: {integrity: sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==} + /@babel/core@7.25.2: + resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) - '@babel/helpers': 7.24.0 - '@babel/parser': 7.24.0 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0 - '@babel/types': 7.24.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.6 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helpers': 7.25.6 + '@babel/parser': 7.25.6 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -140,292 +138,297 @@ packages: - supports-color dev: true - /@babel/generator@7.23.6: - resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + /@babel/generator@7.25.6: + resolution: {integrity: sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.25.6 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 dev: true - /@babel/helper-compilation-targets@7.23.6: - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + /@babel/helper-compilation-targets@7.25.2: + resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/compat-data': 7.23.5 - '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 + '@babel/compat-data': 7.25.4 + '@babel/helper-validator-option': 7.24.8 + browserslist: 4.23.3 lru-cache: 5.1.1 semver: 6.3.1 dev: true - /@babel/helper-environment-visitor@7.22.20: - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-function-name@7.23.0: - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.0 - dev: true - - /@babel/helper-hoist-variables@7.22.5: - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.0 - dev: true - - /@babel/helper-module-imports@7.22.15: - resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + /@babel/helper-module-imports@7.24.7: + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 + transitivePeerDependencies: + - supports-color dev: true - /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.0): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + /@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2): + resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 - dev: true - - /@babel/helper-plugin-utils@7.24.0: - resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} - engines: {node: '>=6.9.0'} + '@babel/core': 7.25.2 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.6 + transitivePeerDependencies: + - supports-color dev: true - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + /@babel/helper-plugin-utils@7.24.8: + resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.0 dev: true - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + /@babel/helper-simple-access@7.24.7: + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 + transitivePeerDependencies: + - supports-color dev: true - /@babel/helper-string-parser@7.23.4: - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + /@babel/helper-string-parser@7.24.8: + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-option@7.23.5: - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + /@babel/helper-validator-option@7.24.8: + resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} engines: {node: '>=6.9.0'} dev: true - /@babel/helpers@7.24.0: - resolution: {integrity: sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==} + /@babel/helpers@7.25.6: + resolution: {integrity: sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0 - '@babel/types': 7.24.0 - transitivePeerDependencies: - - supports-color + '@babel/template': 7.25.0 + '@babel/types': 7.25.6 dev: true - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + /@babel/highlight@7.24.7: + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 + picocolors: 1.1.0 dev: true - /@babel/parser@7.24.0: - resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} + /@babel/parser@7.25.6: + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.25.6 dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.0): + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.2): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.0): + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.0): + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.2): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.2): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + dev: true + + /@babel/plugin-syntax-import-attributes@7.25.6(@babel/core@7.25.2): + resolution: {integrity: sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.0): + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.2): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.0): + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.0): - resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} + /@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.25.2): + resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.0): + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.2): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.0): + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.0): + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.2): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.0): + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.0): + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.0): + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.2): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.2): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.0): + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.2): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.24.0): - resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} + /@babel/plugin-syntax-typescript@7.25.4(@babel/core@7.25.2): + resolution: {integrity: sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 dev: true - /@babel/template@7.24.0: - resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} + /@babel/template@7.25.0: + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.23.5 - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 dev: true - /@babel/traverse@7.24.0: - resolution: {integrity: sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==} + /@babel/traverse@7.25.6: + resolution: {integrity: sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 - debug: 4.3.4(supports-color@8.1.1) + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.6 + '@babel/parser': 7.25.6 + '@babel/template': 7.25.0 + '@babel/types': 7.25.6 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types@7.24.0: - resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + /@babel/types@7.25.6: + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.23.4 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 dev: true @@ -437,245 +440,265 @@ packages: resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==} dev: true - /@codemirror/view@6.24.1: - resolution: {integrity: sha512-sBfP4rniPBRQzNakwuQEqjEuiJDWJyF2kqLLqij4WXRoVwPPJfjx966Eq3F7+OPQxDtMt/Q9MWLoZLWjeveBlg==} + /@codemirror/view@6.33.0: + resolution: {integrity: sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==} dependencies: '@codemirror/state': 6.4.1 style-mod: 4.1.2 w3c-keyname: 2.2.8 dev: true - /@esbuild/aix-ppc64@0.19.12: - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} + /@esbuild/aix-ppc64@0.23.1: + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] requiresBuild: true dev: true optional: true - /@esbuild/android-arm64@0.19.12: - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} + /@esbuild/android-arm64@0.23.1: + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@esbuild/android-arm@0.19.12: - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} + /@esbuild/android-arm@0.23.1: + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@esbuild/android-x64@0.19.12: - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} + /@esbuild/android-x64@0.23.1: + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} cpu: [x64] os: [android] requiresBuild: true dev: true optional: true - /@esbuild/darwin-arm64@0.19.12: - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} + /@esbuild/darwin-arm64@0.23.1: + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@esbuild/darwin-x64@0.19.12: - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} + /@esbuild/darwin-x64@0.23.1: + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@esbuild/freebsd-arm64@0.19.12: - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} + /@esbuild/freebsd-arm64@0.23.1: + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] requiresBuild: true dev: true optional: true - /@esbuild/freebsd-x64@0.19.12: - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} + /@esbuild/freebsd-x64@0.23.1: + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] requiresBuild: true dev: true optional: true - /@esbuild/linux-arm64@0.19.12: - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} + /@esbuild/linux-arm64@0.23.1: + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-arm@0.19.12: - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} + /@esbuild/linux-arm@0.23.1: + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-ia32@0.19.12: - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} + /@esbuild/linux-ia32@0.23.1: + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-loong64@0.19.12: - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} + /@esbuild/linux-loong64@0.23.1: + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-mips64el@0.19.12: - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} + /@esbuild/linux-mips64el@0.23.1: + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-ppc64@0.19.12: - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} + /@esbuild/linux-ppc64@0.23.1: + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-riscv64@0.19.12: - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} + /@esbuild/linux-riscv64@0.23.1: + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-s390x@0.19.12: - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} + /@esbuild/linux-s390x@0.23.1: + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-x64@0.19.12: - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} + /@esbuild/linux-x64@0.23.1: + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/netbsd-x64@0.19.12: - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} + /@esbuild/netbsd-x64@0.23.1: + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] requiresBuild: true dev: true optional: true - /@esbuild/openbsd-x64@0.19.12: - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} + /@esbuild/openbsd-arm64@0.23.1: + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.23.1: + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] requiresBuild: true dev: true optional: true - /@esbuild/sunos-x64@0.19.12: - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} + /@esbuild/sunos-x64@0.23.1: + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] requiresBuild: true dev: true optional: true - /@esbuild/win32-arm64@0.19.12: - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} + /@esbuild/win32-arm64@0.23.1: + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@esbuild/win32-ia32@0.19.12: - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} + /@esbuild/win32-ia32@0.23.1: + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@esbuild/win32-x64@0.19.12: - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} + /@esbuild/win32-x64@0.23.1: + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): + /@eslint-community/eslint-utils@4.4.0(eslint@9.11.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.57.0 + eslint: 9.11.0 eslint-visitor-keys: 3.4.3 dev: true - /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + /@eslint-community/regexpp@4.11.1: + resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc@2.1.4: - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@eslint/config-array@0.18.0: + resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/eslintrc@3.1.0: + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.1 + debug: 4.3.7 + espree: 10.1.0 + globals: 14.0.0 + ignore: 5.3.2 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -684,20 +707,21 @@ packages: - supports-color dev: true - /@eslint/js@8.57.0: - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@eslint/js@9.11.0: + resolution: {integrity: sha512-LPkkenkDqyzTFauZLLAPhIb48fj6drrfMvRGSL9tS3AcZBSVTllemLSNyCvHNNL2t797S/6DJNSIwRwXgMO/eQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /@eslint/object-schema@2.1.4: + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: true - /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} + /@eslint/plugin-kit@0.2.0: + resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4(supports-color@8.1.1) - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + levn: 0.4.1 dev: true /@humanwhocodes/module-importer@1.0.1: @@ -705,20 +729,9 @@ packages: engines: {node: '>=12.22'} dev: true - /@humanwhocodes/object-schema@2.0.2: - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} - dev: true - - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 + /@humanwhocodes/retry@0.3.0: + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} dev: true /@istanbuljs/load-nyc-config@1.1.0: @@ -742,7 +755,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -763,14 +776,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.11.24) + jest-config: 29.7.0(@types/node@22.5.5) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -782,7 +795,7 @@ packages: jest-util: 29.7.0 jest-validate: 29.7.0 jest-watcher: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 strip-ansi: 6.0.1 @@ -798,7 +811,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 jest-mock: 29.7.0 dev: true @@ -825,7 +838,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.11.24 + '@types/node': 22.5.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -858,14 +871,14 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.11.24 + '@types/node': 22.5.5 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 glob: 7.2.3 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.2 + istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.7 @@ -875,7 +888,7 @@ packages: slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 - v8-to-istanbul: 9.2.0 + v8-to-istanbul: 9.3.0 transitivePeerDependencies: - supports-color dev: true @@ -920,7 +933,7 @@ packages: resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.25.2 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 @@ -931,7 +944,7 @@ packages: jest-haste-map: 29.7.0 jest-regex-util: 29.6.3 jest-util: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 pirates: 4.0.6 slash: 3.0.0 write-file-atomic: 4.0.2 @@ -946,8 +959,8 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.11.24 - '@types/yargs': 17.0.32 + '@types/node': 22.5.5 + '@types/yargs': 17.0.33 chalk: 4.1.2 dev: true @@ -956,7 +969,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 dev: true @@ -970,28 +983,21 @@ packages: engines: {node: '>=6.0.0'} dev: true - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} dev: true /@jridgewell/trace-mapping@0.3.25: resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 dev: true /@kurkle/color@0.3.2: resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} dev: false - /@ljharb/through@2.3.12: - resolution: {integrity: sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - dev: true - /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1013,60 +1019,17 @@ packages: fastq: 1.17.1 dev: true - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: true - optional: true - - /@puppeteer/browsers@1.4.6(typescript@5.1.6): - resolution: {integrity: sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==} - engines: {node: '>=16.3.0'} - hasBin: true - peerDependencies: - typescript: '>= 4.7.4' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - debug: 4.3.4(supports-color@8.1.1) - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.3.0 - tar-fs: 3.0.4 - typescript: 5.1.6 - unbzip2-stream: 1.4.3 - yargs: 17.7.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@puppeteer/browsers@1.9.1: - resolution: {integrity: sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==} - engines: {node: '>=16.3.0'} - hasBin: true + /@peggyjs/from-mem@1.3.0: + resolution: {integrity: sha512-kzGoIRJjkg3KuGI4bopz9UvF3KguzfxalHRDEIdqEZUe45xezsQ6cx30e0RKuxPUexojQRBfu89Okn7f4/QXsw==} + engines: {node: '>=18'} dependencies: - debug: 4.3.4(supports-color@8.1.1) - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.3.1 - tar-fs: 3.0.4 - unbzip2-stream: 1.4.3 - yargs: 17.7.2 - transitivePeerDependencies: - - supports-color - dev: true + semver: 7.6.0 + dev: false /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true - /@sindresorhus/is@5.6.0: - resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} - engines: {node: '>=14.16'} - dev: true - /@sinonjs/commons@3.0.1: resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} dependencies: @@ -1079,49 +1042,38 @@ packages: '@sinonjs/commons': 3.0.1 dev: true - /@szmarczak/http-timer@5.0.1: - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} - dependencies: - defer-to-connect: 2.0.1 - dev: true - /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} dev: true - /@tootallnate/quickjs-emscripten@0.23.0: - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - dev: true - /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.5 + '@types/babel__traverse': 7.20.6 dev: true /@types/babel__generator@7.6.8: resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.25.6 dev: true /@types/babel__template@7.4.4: resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} dependencies: - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 dev: true - /@types/babel__traverse@7.20.5: - resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + /@types/babel__traverse@7.20.6: + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.25.6 dev: true /@types/codemirror@5.60.8: @@ -1130,18 +1082,27 @@ packages: '@types/tern': 0.23.9 dev: true - /@types/estree@1.0.5: - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + /@types/eslint@9.6.1: + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 dev: true - /@types/graceful-fs@4.1.9: - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + /@types/eslint__js@8.42.3: + resolution: {integrity: sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==} dependencies: - '@types/node': 20.11.24 + '@types/eslint': 9.6.1 + dev: true + + /@types/estree@1.0.6: + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} dev: true - /@types/http-cache-semantics@4.0.4: - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 22.5.5 dev: true /@types/istanbul-lib-coverage@2.0.6: @@ -1160,8 +1121,8 @@ packages: '@types/istanbul-lib-report': 3.0.3 dev: true - /@types/jest@29.5.12: - resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + /@types/jest@29.5.13: + resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==} dependencies: expect: 29.7.0 pretty-format: 29.7.0 @@ -1170,7 +1131,7 @@ packages: /@types/jsdom@20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 20.11.24 + '@types/node': 22.5.5 '@types/tough-cookie': 4.0.5 parse5: 7.1.2 dev: true @@ -1179,24 +1140,16 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true - /@types/mocha@10.0.6: - resolution: {integrity: sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==} - dev: true - - /@types/node@20.11.24: - resolution: {integrity: sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==} + /@types/node@22.5.5: + resolution: {integrity: sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==} dependencies: - undici-types: 5.26.5 + undici-types: 6.19.8 dev: true /@types/normalize-package-data@2.4.4: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} dev: true - /@types/semver@7.5.8: - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - dev: true - /@types/stack-utils@2.0.3: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: true @@ -1204,7 +1157,7 @@ packages: /@types/tern@0.23.9: resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==} dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 dev: true /@types/tough-cookie@4.0.5: @@ -1215,376 +1168,142 @@ packages: resolution: {integrity: sha512-maEIRJb+PdK2FWDORl0a0aNUSGlniMl8pN+7WbanLzx1Gry4gLfsT0C9O/6JucPPBHCNrqDSImr2QcsUENLKUg==} dev: true - /@types/which@2.0.2: - resolution: {integrity: sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==} - dev: true - - /@types/ws@8.5.10: - resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} - dependencies: - '@types/node': 20.11.24 - dev: true - /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} dev: true - /@types/yargs@17.0.32: - resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + /@types/yargs@17.0.33: + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} dependencies: '@types/yargs-parser': 21.0.3 dev: true - /@types/yauzl@2.10.3: - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - requiresBuild: true - dependencies: - '@types/node': 20.11.24 - dev: true - optional: true - - /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.1.6): - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0)(eslint@9.11.0)(typescript@5.5.4): + resolution: {integrity: sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.1.6) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + '@eslint-community/regexpp': 4.11.1 + '@typescript-eslint/parser': 8.6.0(eslint@9.11.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.6.0 + '@typescript-eslint/type-utils': 8.6.0(eslint@9.11.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.6.0(eslint@9.11.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.6.0 + eslint: 9.11.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 natural-compare: 1.4.0 - semver: 7.6.0 - ts-api-utils: 1.2.1(typescript@5.1.6) - typescript: 5.1.6 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.1.6): - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/parser@8.6.0(eslint@9.11.0)(typescript@5.5.4): + resolution: {integrity: sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 - typescript: 5.1.6 + '@typescript-eslint/scope-manager': 8.6.0 + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.6.0 + debug: 4.3.7 + eslint: 9.11.0 + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@6.21.0: - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/scope-manager@8.6.0: + resolution: {integrity: sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/visitor-keys': 8.6.0 dev: true - /@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.1.6): - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/type-utils@8.6.0(eslint@9.11.0)(typescript@5.5.4): + resolution: {integrity: sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 - ts-api-utils: 1.2.1(typescript@5.1.6) - typescript: 5.1.6 + '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.6.0(eslint@9.11.0)(typescript@5.5.4) + debug: 4.3.7 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: + - eslint - supports-color dev: true - /@typescript-eslint/types@6.21.0: - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/types@8.6.0: + resolution: {integrity: sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: true - /@typescript-eslint/typescript-estree@6.21.0(typescript@5.1.6): - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/typescript-estree@8.6.0(typescript@5.5.4): + resolution: {integrity: sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) - globby: 11.1.0 + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/visitor-keys': 8.6.0 + debug: 4.3.7 + fast-glob: 3.3.2 is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.6.0 - ts-api-utils: 1.2.1(typescript@5.1.6) - typescript: 5.1.6 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.1.6): - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/utils@8.6.0(eslint@9.11.0)(typescript@5.5.4): + resolution: {integrity: sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) - eslint: 8.57.0 - semver: 7.6.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0) + '@typescript-eslint/scope-manager': 8.6.0 + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.5.4) + eslint: 9.11.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys@6.21.0: - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} + /@typescript-eslint/visitor-keys@8.6.0: + resolution: {integrity: sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/types': 8.6.0 eslint-visitor-keys: 3.4.3 dev: true - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true - - /@vitest/snapshot@1.3.1: - resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==} - dependencies: - magic-string: 0.30.7 - pathe: 1.1.2 - pretty-format: 29.7.0 - dev: true - - /@wdio/cli@8.32.3(typescript@5.1.6): - resolution: {integrity: sha512-qjR1MFKQM547iCooceBHyggJRNguD7fhgF4Q7L2r7psG3AQFWzdCN/8rulRGIxTz4PJlIqks9AH9kUJlVAf44A==} - engines: {node: ^16.13 || >=18} - hasBin: true - dependencies: - '@types/node': 20.11.24 - '@vitest/snapshot': 1.3.1 - '@wdio/config': 8.32.3 - '@wdio/globals': 8.32.3(typescript@5.1.6) - '@wdio/logger': 8.28.0 - '@wdio/protocols': 8.32.0 - '@wdio/types': 8.32.2 - '@wdio/utils': 8.32.3 - async-exit-hook: 2.0.1 - chalk: 5.3.0 - chokidar: 3.6.0 - cli-spinners: 2.9.2 - dotenv: 16.4.5 - ejs: 3.1.9 - execa: 8.0.1 - import-meta-resolve: 4.0.0 - inquirer: 9.2.12 - lodash.flattendeep: 4.4.0 - lodash.pickby: 4.6.0 - lodash.union: 4.6.0 - read-pkg-up: 10.0.0 - recursive-readdir: 2.2.3 - webdriverio: 8.32.3(typescript@5.1.6) - yargs: 17.7.2 - transitivePeerDependencies: - - bufferutil - - devtools - - encoding - - supports-color - - typescript - - utf-8-validate - dev: true - - /@wdio/config@8.32.3: - resolution: {integrity: sha512-hZkaz5Fd8830uniQvRgPus8yp9rp50MAsHa5kZ2Jt8y++Rp330FyJpQZE5oPjTATuz35G5Anprk2Q3fmFd0Ifw==} - engines: {node: ^16.13 || >=18} - dependencies: - '@wdio/logger': 8.28.0 - '@wdio/types': 8.32.2 - '@wdio/utils': 8.32.3 - decamelize: 6.0.0 - deepmerge-ts: 5.1.0 - glob: 10.3.10 - import-meta-resolve: 4.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@wdio/globals@8.32.3(typescript@5.1.6): - resolution: {integrity: sha512-jyK89GvWaOYQT9pfZ6HNwkFANgai9eBVfeDPw5yFLXfk6js9GSWbvqMJg/PFi1dQ7xbnbuuf5eYVc65bfAt9KQ==} - engines: {node: ^16.13 || >=18} - optionalDependencies: - expect-webdriverio: 4.11.9(typescript@5.1.6) - webdriverio: 8.32.3(typescript@5.1.6) - transitivePeerDependencies: - - bufferutil - - devtools - - encoding - - supports-color - - typescript - - utf-8-validate - dev: true - - /@wdio/local-runner@8.32.3(typescript@5.1.6): - resolution: {integrity: sha512-YgqYkKarx2m9OjA8WO4NqQPCfFNLmZHnuEWQ6P2LqUeYFsdXRd3wR3UTo9XrI23VSQo+kcpqInsR5vLOYDd1zg==} - engines: {node: ^16.13 || >=18} - dependencies: - '@types/node': 20.11.24 - '@wdio/logger': 8.28.0 - '@wdio/repl': 8.24.12 - '@wdio/runner': 8.32.3(typescript@5.1.6) - '@wdio/types': 8.32.2 - async-exit-hook: 2.0.1 - split2: 4.2.0 - stream-buffers: 3.0.2 - transitivePeerDependencies: - - bufferutil - - devtools - - encoding - - supports-color - - typescript - - utf-8-validate - dev: true - - /@wdio/logger@8.28.0: - resolution: {integrity: sha512-/s6zNCqwy1hoc+K4SJypis0Ud0dlJ+urOelJFO1x0G0rwDRWyFiUP6ijTaCcFxAm29jYEcEPWijl2xkVIHwOyA==} - engines: {node: ^16.13 || >=18} - dependencies: - chalk: 5.3.0 - loglevel: 1.9.1 - loglevel-plugin-prefix: 0.8.4 - strip-ansi: 7.1.0 - dev: true - - /@wdio/mocha-framework@8.32.3: - resolution: {integrity: sha512-wwQ6rDd6TMPqwGfkwvtcBmcirYZUi9GUiwH2OsHvMJ4i+YY7H2dLyZon1ghcIan7r4ufr8KlljbwyerCpUzvcw==} - engines: {node: ^16.13 || >=18} - dependencies: - '@types/mocha': 10.0.6 - '@types/node': 20.11.24 - '@wdio/logger': 8.28.0 - '@wdio/types': 8.32.2 - '@wdio/utils': 8.32.3 - mocha: 10.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@wdio/protocols@8.32.0: - resolution: {integrity: sha512-inLJRrtIGdTz/YPbcsvpSvPlYQFTVtF3OYBwAXhG2FiP1ZwE1CQNLP/xgRGye1ymdGCypGkexRqIx3KBGm801Q==} - dev: true - - /@wdio/repl@8.24.12: - resolution: {integrity: sha512-321F3sWafnlw93uRTSjEBVuvWCxTkWNDs7ektQS15drrroL3TMeFOynu4rDrIz0jXD9Vas0HCD2Tq/P0uxFLdw==} - engines: {node: ^16.13 || >=18} - dependencies: - '@types/node': 20.11.24 - dev: true - - /@wdio/reporter@8.32.2: - resolution: {integrity: sha512-BZdReAFfRCtgtYbyhkKgSGqqoIn/yG5/Z4COjvRvon9NJkz+eA4PiHCKdEP7+ekBIjud7H8Gy+6mPBDEu+wllw==} - engines: {node: ^16.13 || >=18} - dependencies: - '@types/node': 20.11.24 - '@wdio/logger': 8.28.0 - '@wdio/types': 8.32.2 - diff: 5.2.0 - object-inspect: 1.13.1 - dev: true - - /@wdio/runner@8.32.3(typescript@5.1.6): - resolution: {integrity: sha512-HlhdQ4lJ07seL7/x0UQPDnK+o5a0okyjd8ukFYqDL+g9+d3KlW/oM3NvFfX7pb9liIYNEpmoNMwKFp+5XPUE7w==} - engines: {node: ^16.13 || >=18} - dependencies: - '@types/node': 20.11.24 - '@wdio/config': 8.32.3 - '@wdio/globals': 8.32.3(typescript@5.1.6) - '@wdio/logger': 8.28.0 - '@wdio/types': 8.32.2 - '@wdio/utils': 8.32.3 - deepmerge-ts: 5.1.0 - expect-webdriverio: 4.11.9(typescript@5.1.6) - gaze: 1.1.3 - webdriver: 8.32.3 - webdriverio: 8.32.3(typescript@5.1.6) - transitivePeerDependencies: - - bufferutil - - devtools - - encoding - - supports-color - - typescript - - utf-8-validate - dev: true - - /@wdio/spec-reporter@8.32.2: - resolution: {integrity: sha512-3hUXpE+idC4KOXofJnpubdDDCE8X0OTd6ykypiaXMI2hJTA2nIZcHtpRQnAE0E4JT9OzLnPWODcMq54GGBDRkg==} - engines: {node: ^16.13 || >=18} - dependencies: - '@wdio/reporter': 8.32.2 - '@wdio/types': 8.32.2 - chalk: 5.3.0 - easy-table: 1.2.0 - pretty-ms: 7.0.1 - dev: true - - /@wdio/types@8.32.2: - resolution: {integrity: sha512-jq8LcBBQpBP9ZF5kECKEpXv8hN7irCGCjLFAN0Bd5ScRR6qu6MGWvwkDkau2sFPr0b++sKDCEaMzQlwrGFjZXg==} - engines: {node: ^16.13 || >=18} - dependencies: - '@types/node': 20.11.24 - dev: true - - /@wdio/utils@8.32.3: - resolution: {integrity: sha512-UnR9rPpR1W9U5wz2TU+6BQ2rlxtuK/e3fvdaiWIMZKleB/OCcEQFGiGPAGGVi4ShfaTPwz6hK1cTTgj1OtMXkg==} - engines: {node: ^16.13 || >=18} - dependencies: - '@puppeteer/browsers': 1.9.1 - '@wdio/logger': 8.28.0 - '@wdio/types': 8.32.2 - decamelize: 6.0.0 - deepmerge-ts: 5.1.0 - edgedriver: 5.3.10 - geckodriver: 4.3.3 - get-port: 7.0.0 - import-meta-resolve: 4.0.0 - locate-app: 2.2.20 - safaridriver: 0.1.2 - split2: 4.2.0 - wait-port: 1.1.0 - transitivePeerDependencies: - - supports-color - dev: true - /abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead @@ -1593,25 +1312,27 @@ packages: /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: - acorn: 8.11.3 - acorn-walk: 8.3.2 + acorn: 8.12.1 + acorn-walk: 8.3.4 dev: true - /acorn-jsx@5.3.2(acorn@8.11.3): + /acorn-jsx@5.3.2(acorn@8.12.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.11.3 + acorn: 8.12.1 dev: true - /acorn-walk@8.3.2: - resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + /acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.12.1 dev: true - /acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + /acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} hasBin: true dev: true @@ -1620,16 +1341,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - dev: true - - /agent-base@7.1.0: - resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} - engines: {node: '>= 14'} - dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color dev: true @@ -1643,11 +1355,6 @@ packages: uri-js: 4.4.1 dev: true - /ansi-colors@4.1.1: - resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} - engines: {node: '>=6'} - dev: true - /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -1660,11 +1367,6 @@ packages: engines: {node: '>=8'} dev: true - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: true - /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -1684,11 +1386,6 @@ packages: engines: {node: '>=10'} dev: true - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - dev: true - /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1697,31 +1394,6 @@ packages: picomatch: 2.3.1 dev: true - /archiver-utils@4.0.1: - resolution: {integrity: sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==} - engines: {node: '>= 12.0.0'} - dependencies: - glob: 8.1.0 - graceful-fs: 4.2.11 - lazystream: 1.0.1 - lodash: 4.17.21 - normalize-path: 3.0.0 - readable-stream: 3.6.2 - dev: true - - /archiver@6.0.2: - resolution: {integrity: sha512-UQ/2nW7NMl1G+1UnrLypQw1VdT9XZg/ECcKPq7l+STzStrSivFIXIp34D8M5zeNGW5NoOupdYCHv6VySCPNNlw==} - engines: {node: '>= 12.0.0'} - dependencies: - archiver-utils: 4.0.1 - async: 3.2.5 - buffer-crc32: 0.2.13 - readable-stream: 3.6.2 - readdir-glob: 1.1.3 - tar-stream: 3.1.7 - zip-stream: 5.0.2 - dev: true - /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -1732,70 +1404,40 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true - /aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - dependencies: - dequal: 2.0.3 + /async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} dev: true - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true - - /ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} - dependencies: - tslib: 2.6.1 - dev: true - - /async-exit-hook@2.0.1: - resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} - engines: {node: '>=0.12.0'} - dev: true - - /async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} - dev: true - - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true - - /auto-changelog@2.4.0: - resolution: {integrity: sha512-vh17hko1c0ItsEcw6m7qPRf3m45u+XK5QyCrrBFViElZ8jnKrPC1roSznrd1fIB/0vR/zawdECCRJtTuqIXaJw==} + /auto-changelog@2.5.0: + resolution: {integrity: sha512-UTnLjT7I9U2U/xkCUH5buDlp8C7g0SGChfib+iDrJkamcj5kaMqNKHNfbKJw1kthJUq8sUo3i3q2S6FzO/l/wA==} engines: {node: '>=8.3'} hasBin: true dependencies: commander: 7.2.0 handlebars: 4.7.8 + import-cwd: 3.0.0 node-fetch: 2.7.0 - parse-github-url: 1.0.2 - semver: 7.6.0 + parse-github-url: 1.0.3 + semver: 7.6.3 transitivePeerDependencies: - encoding dev: true - /b4a@1.6.6: - resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} - dev: true - - /babel-jest@29.7.0(@babel/core@7.24.0): + /babel-jest@29.7.0(@babel/core@7.25.2): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.25.2 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.24.0) + babel-preset-jest: 29.6.3(@babel/core@7.25.2) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -1807,7 +1449,7 @@ packages: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} dependencies: - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.8 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -1820,115 +1462,48 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.0 + '@babel/template': 7.25.0 + '@babel/types': 7.25.6 '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.20.5 + '@types/babel__traverse': 7.20.6 dev: true - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.0): - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + /babel-preset-current-node-syntax@1.1.0(@babel/core@7.25.2): + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.0) - dev: true - - /babel-preset-jest@29.6.3(@babel/core@7.24.0): + '@babel/core': 7.25.2 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.25.6(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.25.2): resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.25.2 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.0) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.2) dev: true /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /bare-events@2.2.1: - resolution: {integrity: sha512-9GYPpsPFvrWBkelIhOhTWtkeZxVxZOdb3VnFTCzlOo3OjvmTvzLoZFUT8kNFACx0vJej6QPney1Cf9BvzCNE/A==} - requiresBuild: true - dev: true - optional: true - - /bare-fs@2.2.1: - resolution: {integrity: sha512-+CjmZANQDFZWy4PGbVdmALIwmt33aJg8qTkVjClU6X4WmZkTPBDxRHiBn7fpqEWEfF3AC2io++erpViAIQbSjg==} - requiresBuild: true - dependencies: - bare-events: 2.2.1 - bare-os: 2.2.0 - bare-path: 2.1.0 - streamx: 2.16.1 - dev: true - optional: true - - /bare-os@2.2.0: - resolution: {integrity: sha512-hD0rOPfYWOMpVirTACt4/nK8mC55La12K5fY1ij8HAdfQakD62M+H4o4tpfKzVGLgRDTuk3vjA4GqGXXCeFbag==} - requiresBuild: true - dev: true - optional: true - - /bare-path@2.1.0: - resolution: {integrity: sha512-DIIg7ts8bdRKwJRJrUMy/PICEaQZaPGZ26lsSx9MJSwIhSrcdHn7/C8W+XmnG/rKi6BaRcz+JO00CjZteybDtw==} - requiresBuild: true - dependencies: - bare-os: 2.2.0 - dev: true - optional: true - - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true - - /basic-ftp@5.0.5: - resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} - engines: {node: '>=10.0.0'} - dev: true - - /big-integer@1.6.52: - resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} - engines: {node: '>=0.6'} - dev: true - - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: true - - /binary@0.3.0: - resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} - dependencies: - buffers: 0.1.1 - chainsaw: 0.1.0 - dev: true - - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: true - - /bluebird@3.4.7: - resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} - dev: true /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1941,28 +1516,23 @@ packages: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 - dev: true - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} dependencies: - fill-range: 7.0.1 - dev: true - - /browser-stdout@1.3.1: - resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + fill-range: 7.1.1 dev: true - /browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} + /browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001591 - electron-to-chromium: 1.4.690 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) + caniuse-lite: 1.0.30001662 + electron-to-chromium: 1.5.27 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) dev: true /bs-logger@0.2.6: @@ -1978,63 +1548,18 @@ packages: node-int64: 0.4.0 dev: true - /buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - dev: true - /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true - /buffer-indexof-polyfill@1.0.2: - resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} - engines: {node: '>=0.10'} - dev: true - - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: true - - /buffers@0.1.1: - resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} - engines: {node: '>=0.2.0'} - dev: true - /builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} dev: true - /cacheable-lookup@7.0.0: - resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} - engines: {node: '>=14.16'} - dev: true - - /cacheable-request@10.2.14: - resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} - engines: {node: '>=14.16'} - dependencies: - '@types/http-cache-semantics': 4.0.4 - get-stream: 6.0.1 - http-cache-semantics: 4.1.1 - keyv: 4.5.4 - mimic-response: 4.0.0 - normalize-url: 8.0.0 - responselike: 3.0.0 - dev: true - - /call-bind@1.0.7: - resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} - engines: {node: '>= 0.4'} - dependencies: - es-define-property: 1.0.0 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - set-function-length: 1.2.1 + /builtin-modules@4.0.0: + resolution: {integrity: sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==} + engines: {node: '>=18.20'} dev: true /callsites@3.1.0: @@ -2052,27 +1577,8 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001591: - resolution: {integrity: sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==} - dev: true - - /chai@4.4.1: - resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} - engines: {node: '>=4'} - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.3 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.0.8 - dev: true - - /chainsaw@0.1.0: - resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} - dependencies: - traverse: 0.3.9 + /caniuse-lite@1.0.30001662: + resolution: {integrity: sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==} dev: true /chalk@2.4.2: @@ -2092,104 +1598,37 @@ packages: supports-color: 7.2.0 dev: true - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true - /char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} dev: true - /chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true - - /chart.js@4.4.2: - resolution: {integrity: sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==} + /chart.js@4.4.4: + resolution: {integrity: sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==} engines: {pnpm: '>=8'} dependencies: '@kurkle/color': 0.3.2 dev: false - /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - dependencies: - get-func-name: 2.0.2 - dev: true - - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /chromium-bidi@0.4.16(devtools-protocol@0.0.1147663): - resolution: {integrity: sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==} - peerDependencies: - devtools-protocol: '*' - dependencies: - devtools-protocol: 0.0.1147663 - mitt: 3.0.0 - dev: true - /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} dev: true - /cjs-module-lexer@1.2.3: - resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} - dev: true - - /cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + /ci-info@4.0.0: + resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} engines: {node: '>=8'} - dependencies: - restore-cursor: 3.1.0 dev: true - /cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - dev: true - - /cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} + /cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} dev: true - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + /clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 + escape-string-regexp: 1.0.5 dev: true /cliui@8.0.1: @@ -2201,12 +1640,6 @@ packages: wrap-ansi: 7.0.0 dev: true - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - requiresBuild: true - dev: true - /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -2244,26 +1677,16 @@ packages: delayed-stream: 1.0.0 dev: true + /commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + dev: false + /commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} dev: true - /commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - dev: true - - /compress-commons@5.0.3: - resolution: {integrity: sha512-/UIcLWvwAQyVibgpQDPtfNM3SvqN7G9elAPAV7GM0L53EbNWwWiCsWtK8Fwed/APEbptPHXs5PuW+y8Bq8lFTA==} - engines: {node: '>= 12.0.0'} - dependencies: - crc-32: 1.2.2 - crc32-stream: 5.0.1 - normalize-path: 3.0.0 - readable-stream: 3.6.2 - dev: true - /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -2272,25 +1695,13 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: true - - /crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - dev: true - - /crc32-stream@5.0.1: - resolution: {integrity: sha512-lO1dFui+CEUh/ztYIpgpKItKW9Bb4NWakCRJrnqAbFIYD+OZAwb2VfD5T5eXMw2FNcsDHkQcNl/Wh3iVXYwU6g==} - engines: {node: '>= 12.0.0'} + /core-js-compat@3.38.1: + resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==} dependencies: - crc-32: 1.2.2 - readable-stream: 3.6.2 + browserslist: 4.23.3 dev: true - /create-jest@29.7.0(@types/node@20.11.24): + /create-jest@29.7.0(@types/node@22.5.5): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -2299,7 +1710,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.11.24) + jest-config: 29.7.0(@types/node@22.5.5) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2309,14 +1720,6 @@ packages: - ts-node dev: true - /cross-fetch@4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - dev: true - /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2326,14 +1729,6 @@ packages: which: 2.0.2 dev: true - /css-shorthand-properties@1.1.1: - resolution: {integrity: sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==} - dev: true - - /css-value@0.0.1: - resolution: {integrity: sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==} - dev: true - /cssom@0.3.8: resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} dev: true @@ -2349,16 +1744,6 @@ packages: cssom: 0.3.8 dev: true - /data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - dev: true - - /data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - dev: true - /data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -2368,8 +1753,8 @@ packages: whatwg-url: 11.0.0 dev: true - /debug@4.3.1: - resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==} + /debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2377,45 +1762,15 @@ packages: supports-color: optional: true dependencies: - ms: 2.1.2 - dev: true - - /debug@4.3.4(supports-color@8.1.1): - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - supports-color: 8.1.1 - dev: true - - /decamelize@4.0.0: - resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} - engines: {node: '>=10'} - dev: true - - /decamelize@6.0.0: - resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + ms: 2.1.3 dev: true /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: true - /decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - dependencies: - mimic-response: 3.1.0 - dev: true - - /dedent@1.5.1: - resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + /dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -2423,109 +1778,30 @@ packages: optional: true dev: true - /deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} - dependencies: - type-detect: 4.0.8 - dev: true - /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true - /deepmerge-ts@5.1.0: - resolution: {integrity: sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==} - engines: {node: '>=16.0.0'} - dev: true - /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} dev: true - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - requiresBuild: true - dependencies: - clone: 1.0.4 - dev: true - - /defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - dev: true - - /define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - dependencies: - es-define-property: 1.0.0 - es-errors: 1.3.0 - gopd: 1.0.1 - dev: true - - /degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 - dev: true - /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} dev: true - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: true - /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} dev: true - /devtools-protocol@0.0.1147663: - resolution: {integrity: sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==} - dev: true - - /devtools-protocol@0.0.1262051: - resolution: {integrity: sha512-YJe4CT5SA8on3Spa+UDtNhEqtuV6Epwz3OZ4HQVLhlRccpZ9/PAYk0/cy/oKxFKRrZPBUPyxympQci4yWNWZ9g==} - dev: true - /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /diff@5.0.0: - resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} - engines: {node: '>=0.3.1'} - dev: true - - /diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - dev: true - - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true - - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - /domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -2534,60 +1810,16 @@ packages: webidl-conversions: 7.0.0 dev: true - /dotenv@16.4.5: - resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} - engines: {node: '>=12'} - dev: true - - /duplexer2@0.1.4: - resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} - dependencies: - readable-stream: 2.3.8 - dev: true - - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true - - /easy-table@1.2.0: - resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} - dependencies: - ansi-regex: 5.0.1 - optionalDependencies: - wcwidth: 1.0.1 - dev: true - - /edge-paths@3.0.5: - resolution: {integrity: sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==} - engines: {node: '>=14.0.0'} - dependencies: - '@types/which': 2.0.2 - which: 2.0.2 - dev: true - - /edgedriver@5.3.10: - resolution: {integrity: sha512-RFSHYMNtcF1PjaGZCA2rdQQ8hSTLPZgcYgeY1V6dC+tR4NhZXwFAku+8hCbRYh7ZlwKKrTbVu9FwknjFddIuuw==} - hasBin: true - requiresBuild: true - dependencies: - '@wdio/logger': 8.28.0 - decamelize: 6.0.0 - edge-paths: 3.0.5 - node-fetch: 3.3.2 - unzipper: 0.10.14 - which: 4.0.0 - dev: true - - /ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + /ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} hasBin: true dependencies: - jake: 10.8.7 + jake: 10.9.2 dev: true - /electron-to-chromium@1.4.690: - resolution: {integrity: sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==} + /electron-to-chromium@1.5.27: + resolution: {integrity: sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==} dev: true /emittery@0.13.1: @@ -2599,16 +1831,6 @@ packages: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: true - - /end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - dependencies: - once: 1.4.0 - dev: true - /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2620,51 +1842,40 @@ packages: is-arrayish: 0.2.1 dev: true - /es-define-property@1.0.0: - resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.4 - dev: true - - /es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - dev: true - - /esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} + /esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - dev: true - - /escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + dev: true + + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} dev: true @@ -2683,11 +1894,6 @@ packages: engines: {node: '>=10'} dev: true - /escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - dev: true - /escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -2700,9 +1906,42 @@ packages: source-map: 0.6.1 dev: true - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /eslint-plugin-simple-import-sort@12.1.1(eslint@9.11.0): + resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} + peerDependencies: + eslint: '>=5.0.0' + dependencies: + eslint: 9.11.0 + dev: true + + /eslint-plugin-unicorn@55.0.0(eslint@9.11.0): + resolution: {integrity: sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==} + engines: {node: '>=18.18'} + peerDependencies: + eslint: '>=8.56.0' + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0) + ci-info: 4.0.0 + clean-regexp: 1.0.0 + core-js-compat: 3.38.1 + eslint: 9.11.0 + esquery: 1.6.0 + globals: 15.9.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + jsesc: 3.0.2 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.27 + regjsparser: 0.10.0 + semver: 7.6.3 + strip-indent: 3.0.0 + dev: true + + /eslint-scope@8.0.2: + resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 @@ -2713,60 +1952,66 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /eslint@9.11.0: + resolution: {integrity: sha512-yVS6XODx+tMFMDFcG4+Hlh+qG7RM6cCJXtQhCKLSsr3XkLvWggHjCqjfh0XsPPnt1c56oaT6PMgW9XWQQjdHXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.0) + '@eslint-community/regexpp': 4.11.1 + '@eslint/config-array': 0.18.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.11.0 + '@eslint/plugin-kit': 0.2.0 '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) - doctrine: 3.0.0 + debug: 4.3.7 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.5.0 + eslint-scope: 8.0.2 + eslint-visitor-keys: 4.0.0 + espree: 10.1.0 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color dev: true - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) - eslint-visitor-keys: 3.4.3 + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 4.0.0 dev: true /esprima@4.0.1: @@ -2775,8 +2020,8 @@ packages: hasBin: true dev: true - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} dependencies: estraverse: 5.3.0 @@ -2814,47 +2059,11 @@ packages: strip-final-newline: 2.0.0 dev: true - /execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - dev: true - /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} dev: true - /expect-webdriverio@4.11.9(typescript@5.1.6): - resolution: {integrity: sha512-nHVLoC4W8wuVAyfpitJ07iDMLjeQ2OeYVjrKEb7dMeG4fqlegzN1SGYnnsKay+KWrte9KzuW1pZ7h5Nmbm/hAQ==} - engines: {node: '>=16 || >=18 || >=20'} - dependencies: - '@vitest/snapshot': 1.3.1 - expect: 29.7.0 - jest-matcher-utils: 29.7.0 - lodash.isequal: 4.5.0 - optionalDependencies: - '@wdio/globals': 8.32.3(typescript@5.1.6) - '@wdio/logger': 8.28.0 - webdriverio: 8.32.3(typescript@5.1.6) - transitivePeerDependencies: - - bufferutil - - devtools - - encoding - - supports-color - - typescript - - utf-8-validate - dev: true - /expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2866,41 +2075,10 @@ packages: jest-util: 29.7.0 dev: true - /external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - dev: true - - /extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - dependencies: - debug: 4.3.4(supports-color@8.1.1) - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - dev: true - - /fast-deep-equal@2.0.1: - resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} - dev: true - /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true - /fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - dev: true - /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -2909,7 +2087,7 @@ packages: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 + micromatch: 4.0.8 dev: true /fast-json-stable-stringify@2.1.0: @@ -2932,33 +2110,11 @@ packages: bser: 2.1.1 dev: true - /fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - dependencies: - pend: 1.2.0 - dev: true - - /fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - dev: true - - /figures@5.0.0: - resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} - engines: {node: '>=14'} - dependencies: - escape-string-regexp: 5.0.0 - is-unicode-supported: 1.3.0 - dev: true - - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + /file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 dev: true /filelist@1.0.4: @@ -2967,8 +2123,8 @@ packages: minimatch: 5.1.6 dev: true - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 @@ -2990,45 +2146,18 @@ packages: path-exists: 4.0.0 dev: true - /find-up@6.3.0: - resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - dev: true - - /flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + /flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} dependencies: flatted: 3.3.1 keyv: 4.5.4 - rimraf: 3.0.2 - dev: true - - /flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true dev: true /flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} dev: true - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - dev: true - - /form-data-encoder@2.1.4: - resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} - engines: {node: '>= 14.17'} - dev: true - /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -3038,22 +2167,6 @@ packages: mime-types: 2.1.35 dev: true - /formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - dependencies: - fetch-blob: 3.2.0 - dev: true - - /fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - dev: true - /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -3066,45 +2179,10 @@ packages: dev: true optional: true - /fstream@1.0.12: - resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} - engines: {node: '>=0.6'} - dependencies: - graceful-fs: 4.2.11 - inherits: 2.0.4 - mkdirp: 0.5.6 - rimraf: 2.7.1 - dev: true - /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true - /gaze@1.1.3: - resolution: {integrity: sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==} - engines: {node: '>= 4.0.0'} - dependencies: - globule: 1.3.4 - dev: true - - /geckodriver@4.3.3: - resolution: {integrity: sha512-we2c2COgxFkLVuoknJNx+ioP+7VDq0sr6SCqWHTzlA4kzIbzR0EQ1Pps34s8WrsOnQqPC8a4sZV9dRPROOrkSg==} - engines: {node: ^16.13 || >=18 || >=20} - hasBin: true - requiresBuild: true - dependencies: - '@wdio/logger': 8.28.0 - decamelize: 6.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 - node-fetch: 3.3.2 - tar-fs: 3.0.5 - unzipper: 0.10.14 - which: 4.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3115,60 +2193,16 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true - - /get-intrinsic@1.2.4: - resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - has-proto: 1.0.3 - has-symbols: 1.0.3 - hasown: 2.0.1 - dev: true - /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} dev: true - /get-port@7.0.0: - resolution: {integrity: sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==} - engines: {node: '>=16'} - dev: true - - /get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - dependencies: - pump: 3.0.0 - dev: true - /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} dev: true - /get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - dev: true - - /get-uri@6.0.3: - resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} - engines: {node: '>= 14'} - dependencies: - basic-ftp: 5.0.5 - data-uri-to-buffer: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) - fs-extra: 11.2.0 - transitivePeerDependencies: - - supports-color - dev: true - /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3183,31 +2217,9 @@ packages: is-glob: 4.0.3 dev: true - /glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 - path-scurry: 1.10.1 - dev: true - - /glob@7.1.7: - resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.0.8 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -3217,81 +2229,25 @@ packages: path-is-absolute: 1.0.1 dev: true - /glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.0.1 - once: 1.4.0 - dev: true - /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} dev: true - /globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.1 - merge2: 1.4.1 - slash: 3.0.0 - dev: true - - /globule@1.3.4: - resolution: {integrity: sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==} - engines: {node: '>= 0.10'} - dependencies: - glob: 7.1.7 - lodash: 4.17.21 - minimatch: 3.0.8 - dev: true - - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - dependencies: - get-intrinsic: 1.2.4 + /globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} dev: true - /got@12.6.1: - resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} - engines: {node: '>=14.16'} - dependencies: - '@sindresorhus/is': 5.6.0 - '@szmarczak/http-timer': 5.0.1 - cacheable-lookup: 7.0.0 - cacheable-request: 10.2.14 - decompress-response: 6.0.0 - form-data-encoder: 2.1.4 - get-stream: 6.0.1 - http2-wrapper: 2.2.1 - lowercase-keys: 3.0.0 - p-cancelable: 3.0.0 - responselike: 3.0.0 + /globals@15.9.0: + resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} + engines: {node: '>=18'} dev: true /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true - /grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - dev: true - /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true @@ -3306,7 +2262,7 @@ packages: source-map: 0.6.1 wordwrap: 1.0.0 optionalDependencies: - uglify-js: 3.17.4 + uglify-js: 3.19.3 dev: true /has-flag@3.0.0: @@ -3319,39 +2275,15 @@ packages: engines: {node: '>=8'} dev: true - /has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - dependencies: - es-define-property: 1.0.0 - dev: true - - /has-proto@1.0.3: - resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} - engines: {node: '>= 0.4'} - dev: true - - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - dev: true - - /hasown@2.0.1: - resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 dev: true - /he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - dev: true - - /hosted-git-info@7.0.1: - resolution: {integrity: sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - lru-cache: 10.2.0 + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true /html-encoding-sniffer@3.0.0: @@ -3365,55 +2297,23 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true - /http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - dev: true - /http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - dev: true - - /http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color dev: true - /http2-wrapper@2.2.1: - resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} - engines: {node: '>=10.19.0'} - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - dev: true - /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - dev: true - - /https-proxy-agent@7.0.4: - resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color dev: true @@ -3423,18 +2323,6 @@ packages: engines: {node: '>=10.17.0'} dev: true - /human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - dev: true - - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true - /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -3442,13 +2330,16 @@ packages: safer-buffer: 2.1.2 dev: true - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} dev: true - /ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} + /import-cwd@3.0.0: + resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} + engines: {node: '>=8'} + dependencies: + import-from: 3.0.0 dev: true /import-fresh@3.3.0: @@ -3459,8 +2350,15 @@ packages: resolve-from: 4.0.0 dev: true - /import-local@3.1.0: - resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + /import-from@3.0.0: + resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + + /import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} hasBin: true dependencies: @@ -3468,17 +2366,19 @@ packages: resolve-cwd: 3.0.0 dev: true - /import-meta-resolve@4.0.0: - resolution: {integrity: sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==} - dev: true - /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} dev: true + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. dependencies: once: 1.4.0 wrappy: 1.0.2 @@ -3488,55 +2388,22 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true - /inquirer@9.2.12: - resolution: {integrity: sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==} - engines: {node: '>=14.18.0'} - dependencies: - '@ljharb/through': 2.3.12 - ansi-escapes: 4.3.2 - chalk: 5.3.0 - cli-cursor: 3.1.0 - cli-width: 4.1.0 - external-editor: 3.1.0 - figures: 5.0.0 - lodash: 4.17.21 - mute-stream: 1.0.0 - ora: 5.4.1 - run-async: 3.0.0 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - dev: true - - /ip-address@9.0.5: - resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} - engines: {node: '>= 12'} - dependencies: - jsbn: 1.1.0 - sprintf-js: 1.1.3 - dev: true - - /ip-regex@4.3.0: - resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} - engines: {node: '>=8'} - dev: true - /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} dependencies: - binary-extensions: 2.2.0 + builtin-modules: 3.3.0 dev: true - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + /is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} dependencies: - hasown: 2.0.1 + hasown: 2.0.2 dev: true /is-extglob@2.1.1: @@ -3561,11 +2428,6 @@ packages: is-extglob: 2.1.1 dev: true - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: true - /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -3576,16 +2438,6 @@ packages: engines: {node: '>=8'} dev: true - /is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - dev: true - - /is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - dev: true - /is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} dev: true @@ -3595,47 +2447,10 @@ packages: engines: {node: '>=8'} dev: true - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: true - - /is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - dev: true - - /is-url@1.2.4: - resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} - dev: true - - /is2@2.0.9: - resolution: {integrity: sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==} - engines: {node: '>=v0.10.0'} - dependencies: - deep-is: 0.1.4 - ip-regex: 4.3.0 - is-url: 1.2.4 - dev: true - - /isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: true - /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - dev: true - /istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -3645,8 +2460,8 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.24.0 - '@babel/parser': 7.24.0 + '@babel/core': 7.25.2 + '@babel/parser': 7.25.6 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -3654,15 +2469,15 @@ packages: - supports-color dev: true - /istanbul-lib-instrument@6.0.2: - resolution: {integrity: sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==} + /istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} engines: {node: '>=10'} dependencies: - '@babel/core': 7.24.0 - '@babel/parser': 7.24.0 + '@babel/core': 7.25.2 + '@babel/parser': 7.25.6 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.6.0 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: true @@ -3680,7 +2495,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -3695,21 +2510,12 @@ packages: istanbul-lib-report: 3.0.1 dev: true - /jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - dev: true - - /jake@10.8.7: - resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} + /jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} engines: {node: '>=10'} hasBin: true dependencies: - async: 3.2.5 + async: 3.2.6 chalk: 4.1.2 filelist: 1.0.4 minimatch: 3.1.2 @@ -3732,10 +2538,10 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 chalk: 4.1.2 co: 4.6.0 - dedent: 1.5.1 + dedent: 1.5.3 is-generator-fn: 2.1.0 jest-each: 29.7.0 jest-matcher-utils: 29.7.0 @@ -3745,7 +2551,7 @@ packages: jest-util: 29.7.0 p-limit: 3.1.0 pretty-format: 29.7.0 - pure-rand: 6.0.4 + pure-rand: 6.1.0 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: @@ -3753,7 +2559,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@20.11.24): + /jest-cli@29.7.0(@types/node@22.5.5): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -3767,10 +2573,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.11.24) + create-jest: 29.7.0(@types/node@22.5.5) exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.11.24) + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.5.5) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -3781,7 +2587,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.11.24): + /jest-config@29.7.0(@types/node@22.5.5): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -3793,11 +2599,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.24 - babel-jest: 29.7.0(@babel/core@7.24.0) + '@types/node': 22.5.5 + babel-jest: 29.7.0(@babel/core@7.25.2) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -3811,7 +2617,7 @@ packages: jest-runner: 29.7.0 jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 parse-json: 5.2.0 pretty-format: 29.7.0 slash: 3.0.0 @@ -3862,7 +2668,7 @@ packages: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 20.11.24 + '@types/node': 22.5.5 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -3879,7 +2685,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -3899,14 +2705,14 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.11.24 + '@types/node': 22.5.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 jest-regex-util: 29.6.3 jest-util: 29.7.0 jest-worker: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 @@ -3934,12 +2740,12 @@ packages: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 @@ -3950,7 +2756,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 jest-util: 29.7.0 dev: true @@ -4005,7 +2811,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -4036,9 +2842,9 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 chalk: 4.1.2 - cjs-module-lexer: 1.2.3 + cjs-module-lexer: 1.4.1 collect-v8-coverage: 1.0.2 glob: 7.2.3 graceful-fs: 4.2.11 @@ -4059,15 +2865,15 @@ packages: resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.24.0 - '@babel/generator': 7.23.6 - '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.0) - '@babel/types': 7.24.0 + '@babel/core': 7.25.2 + '@babel/generator': 7.25.6 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-typescript': 7.25.4(@babel/core@7.25.2) + '@babel/types': 7.25.6 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.0) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.2) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -4078,7 +2884,7 @@ packages: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.0 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: true @@ -4088,7 +2894,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -4113,7 +2919,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.24 + '@types/node': 22.5.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -4125,13 +2931,13 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.11.24 + '@types/node': 22.5.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@20.11.24): + /jest@29.7.0(@types/node@22.5.5): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -4143,8 +2949,8 @@ packages: dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.11.24) + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.5.5) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -4171,10 +2977,6 @@ packages: argparse: 2.0.1 dev: true - /jsbn@1.1.0: - resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - dev: true - /jsdom@20.0.3: resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} engines: {node: '>=14'} @@ -4185,7 +2987,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.11.3 + acorn: 8.12.1 acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 @@ -4198,17 +3000,17 @@ packages: http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.7 + nwsapi: 2.2.12 parse5: 7.1.2 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 4.1.3 + tough-cookie: 4.1.4 w3c-xmlserializer: 4.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.16.0 + ws: 8.18.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -4216,12 +3018,23 @@ packages: - utf-8-validate dev: true + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true dev: true + /jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + dev: true + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true @@ -4230,11 +3043,6 @@ packages: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true - /json-parse-even-better-errors@3.0.1: - resolution: {integrity: sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -4249,14 +3057,6 @@ packages: hasBin: true dev: true - /jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - dev: true - /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -4268,18 +3068,6 @@ packages: engines: {node: '>=6'} dev: true - /ky@0.33.3: - resolution: {integrity: sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==} - engines: {node: '>=14.16'} - dev: true - - /lazystream@1.0.1: - resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} - engines: {node: '>= 0.6.3'} - dependencies: - readable-stream: 2.3.8 - dev: true - /leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -4297,23 +3085,6 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true - /lines-and-columns@2.0.4: - resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /listenercount@1.0.1: - resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} - dev: true - - /locate-app@2.2.20: - resolution: {integrity: sha512-TOCp8H9l75GhNtd+BgyUnLMNzR+IpYge7cWjxELsyDlqH+MyYWxq+NfyjQ+o6oRAORzOs3IfMM6KAR6q3JNfhg==} - dependencies: - n12: 1.8.23 - type-fest: 2.13.0 - userhome: 1.0.0 - dev: true - /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -4328,26 +3099,6 @@ packages: p-locate: 5.0.0 dev: true - /locate-path@7.2.0: - resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - p-locate: 6.0.0 - dev: true - - /lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - dev: true - - /lodash.flattendeep@4.4.0: - resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} - dev: true - - /lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - requiresBuild: true - dev: true - /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} dev: true @@ -4356,55 +3107,6 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true - /lodash.pickby@4.6.0: - resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} - dev: true - - /lodash.union@4.6.0: - resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} - dev: true - - /lodash.zip@4.2.0: - resolution: {integrity: sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==} - dev: true - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true - - /log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - dev: true - - /loglevel-plugin-prefix@0.8.4: - resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} - dev: true - - /loglevel@1.9.1: - resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==} - engines: {node: '>= 0.6.0'} - dev: true - - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - dependencies: - get-func-name: 2.0.2 - dev: true - - /lowercase-keys@3.0.0: - resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /lru-cache@10.2.0: - resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} - engines: {node: 14 || >=16.14} - dev: true - /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -4416,25 +3118,13 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true - - /lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - dev: true - - /magic-string@0.30.7: - resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + dev: false /make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: - semver: 7.6.0 + semver: 7.6.3 dev: true /make-error@1.3.6: @@ -4456,11 +3146,11 @@ packages: engines: {node: '>= 8'} dev: true - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 dev: true @@ -4481,26 +3171,17 @@ packages: engines: {node: '>=6'} dev: true - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true - - /mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - dev: true - - /mimic-response@4.0.0: - resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} dev: true - /minimatch@3.0.8: - resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + /minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} dependencies: - brace-expansion: 1.1.11 - dev: true + brace-expansion: 2.0.1 + dev: false /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4508,13 +3189,6 @@ packages: brace-expansion: 1.1.11 dev: true - /minimatch@5.0.1: - resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} - engines: {node: '>=10'} - dependencies: - brace-expansion: 2.0.1 - dev: true - /minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -4522,8 +3196,8 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 @@ -4533,53 +3207,6 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true - /minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} - engines: {node: '>=16 || 14 >=14.17'} - dev: true - - /mitt@3.0.0: - resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==} - dev: true - - /mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - dev: true - - /mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: true - - /mocha@10.3.0: - resolution: {integrity: sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==} - engines: {node: '>= 14.0.0'} - hasBin: true - dependencies: - ansi-colors: 4.1.1 - browser-stdout: 1.3.1 - chokidar: 3.5.3 - debug: 4.3.4(supports-color@8.1.1) - diff: 5.0.0 - escape-string-regexp: 4.0.0 - find-up: 5.0.0 - glob: 8.1.0 - he: 1.2.0 - js-yaml: 4.1.0 - log-symbols: 4.1.0 - minimatch: 5.0.1 - ms: 2.1.3 - serialize-javascript: 6.0.0 - strip-json-comments: 3.1.1 - supports-color: 8.1.1 - workerpool: 6.2.1 - yargs: 16.2.0 - yargs-parser: 20.2.4 - yargs-unparser: 2.0.0 - dev: true - /moment@2.29.4: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} dev: true @@ -4588,23 +3215,10 @@ packages: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} dev: true - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true - /mute-stream@1.0.0: - resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - - /n12@1.8.23: - resolution: {integrity: sha512-kQITb5LlO0Gk8rmbMAkfbmhs+QlXZ5SRHsx6YcG++3yc57iolbiQuo5rsfu3dkB7Qw3jKCqntsZvNNgvdfotkA==} - dev: true - /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -4613,16 +3227,6 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true - /netmask@2.0.2: - resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} - engines: {node: '>= 0.4.0'} - dev: true - - /node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - dev: true - /node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -4635,30 +3239,20 @@ packages: whatwg-url: 5.0.0 dev: true - /node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - dev: true - /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true - /node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + /node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} dev: true - /normalize-package-data@6.0.0: - resolution: {integrity: sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==} - engines: {node: ^16.14.0 || >=18.0.0} + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: - hosted-git-info: 7.0.1 - is-core-module: 2.13.1 - semver: 7.6.0 + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true @@ -4667,11 +3261,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /normalize-url@8.0.0: - resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} - engines: {node: '>=14.16'} - dev: true - /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -4679,29 +3268,18 @@ packages: path-key: 3.1.1 dev: true - /npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true - - /nwsapi@2.2.7: - resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} - dev: true - - /object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + /nwsapi@2.2.12: + resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} dev: true - /obsidian@1.5.7(@codemirror/state@6.4.1)(@codemirror/view@6.24.1): - resolution: {integrity: sha512-DNcvQJ6TvMflHZqWfO9cLGbOUbKTy2KBi6B6vjo5RG8XsftKZZq1zS/OQFhII2BnXK/DWan/lUcb2JYxfM3p5A==} + /obsidian@1.7.2(@codemirror/state@6.4.1)(@codemirror/view@6.33.0): + resolution: {integrity: sha512-k9hN9brdknJC+afKr5FQzDRuEFGDKbDjfCazJwpgibwCAoZNYHYV8p/s3mM8I6AsnKrPKNXf8xGuMZ4enWelZQ==} peerDependencies: '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 dependencies: '@codemirror/state': 6.4.1 - '@codemirror/view': 6.24.1 + '@codemirror/view': 6.33.0 '@types/codemirror': 5.60.8 moment: 2.29.4 dev: true @@ -4719,48 +3297,16 @@ packages: mimic-fn: 2.1.0 dev: true - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true - - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true - - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - dev: true - - /os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - dev: true - - /p-cancelable@3.0.0: - resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} - engines: {node: '>=12.20'} + word-wrap: 1.2.5 dev: true /p-limit@2.3.0: @@ -4777,13 +3323,6 @@ packages: yocto-queue: 0.1.0 dev: true - /p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - yocto-queue: 1.0.0 - dev: true - /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -4798,42 +3337,11 @@ packages: p-limit: 3.1.0 dev: true - /p-locate@6.0.0: - resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - p-limit: 4.0.0 - dev: true - /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} dev: true - /pac-proxy-agent@7.0.1: - resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} - engines: {node: '>= 14'} - dependencies: - '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) - get-uri: 6.0.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 - pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.2 - transitivePeerDependencies: - - supports-color - dev: true - - /pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} - dependencies: - degenerator: 5.0.1 - netmask: 2.0.2 - dev: true - /pagerank.js@1.0.2: resolution: {integrity: sha512-IinWDOC9kVC40s9jV4ifniiY9aJFa46r+GufsFVc6yTrCr1tHDQgVqsoYEPfCs9oJTTG8qzAePcJcCfvBGQBRw==} dev: false @@ -4845,9 +3353,9 @@ packages: callsites: 3.1.0 dev: true - /parse-github-url@1.0.2: - resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==} - engines: {node: '>=0.10.0'} + /parse-github-url@1.0.3: + resolution: {integrity: sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==} + engines: {node: '>= 0.10'} hasBin: true dev: true @@ -4855,28 +3363,12 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 dev: true - /parse-json@7.1.1: - resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} - engines: {node: '>=16'} - dependencies: - '@babel/code-frame': 7.23.5 - error-ex: 1.3.2 - json-parse-even-better-errors: 3.0.1 - lines-and-columns: 2.0.4 - type-fest: 3.13.1 - dev: true - - /parse-ms@2.1.0: - resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} - engines: {node: '>=6'} - dev: true - /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: @@ -4888,11 +3380,6 @@ packages: engines: {node: '>=8'} dev: true - /path-exists@5.0.0: - resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -4903,42 +3390,22 @@ packages: engines: {node: '>=8'} dev: true - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} + /peggy@4.0.3: + resolution: {integrity: sha512-v7/Pt6kGYsfXsCrfb52q7/yg5jaAwiVaUMAPLPvy4DJJU6Wwr72t6nDIqIDkGfzd1B4zeVuTnQT0RGeOhe/uSA==} + engines: {node: '>=18'} + hasBin: true dependencies: - lru-cache: 10.2.0 - minipass: 7.0.4 - dev: true - - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true - - /pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - dev: true - - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true - - /pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - dev: true + '@peggyjs/from-mem': 1.3.0 + commander: 12.1.0 + source-map-generator: 0.8.0 + dev: false - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + /picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} dev: true /picomatch@2.3.1: @@ -4958,13 +3425,18 @@ packages: find-up: 4.1.0 dev: true + /pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} dev: true - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + /prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} hasBin: true dev: true @@ -4975,23 +3447,7 @@ packages: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 18.2.0 - dev: true - - /pretty-ms@7.0.1: - resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} - engines: {node: '>=10'} - dependencies: - parse-ms: 2.1.0 - dev: true - - /process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: true - - /progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} + react-is: 18.3.1 dev: true /prompts@2.4.2: @@ -5002,87 +3458,17 @@ packages: sisteransi: 1.0.5 dev: true - /proxy-agent@6.3.0: - resolution: {integrity: sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 - lru-cache: 7.18.3 - pac-proxy-agent: 7.0.1 - proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.2 - transitivePeerDependencies: - - supports-color - dev: true - - /proxy-agent@6.3.1: - resolution: {integrity: sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 - lru-cache: 7.18.3 - pac-proxy-agent: 7.0.1 - proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.2 - transitivePeerDependencies: - - supports-color - dev: true - - /proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: true - /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: true - /pump@3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - dev: true - /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} dev: true - /puppeteer-core@20.9.0(typescript@5.1.6): - resolution: {integrity: sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==} - engines: {node: '>=16.3.0'} - peerDependencies: - typescript: '>= 4.7.4' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@puppeteer/browsers': 1.4.6(typescript@5.1.6) - chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) - cross-fetch: 4.0.0 - debug: 4.3.4(supports-color@8.1.1) - devtools-protocol: 0.0.1147663 - typescript: 5.1.6 - ws: 8.13.0 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - dev: true - - /pure-rand@6.0.4: - resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} - dev: true - - /query-selector-shadow-dom@1.0.1: - resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + /pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} dev: true /querystringify@2.2.0: @@ -5093,83 +3479,39 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true - /queue-tick@1.0.1: - resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} dev: true - /quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - dev: true - - /randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - dependencies: - safe-buffer: 5.2.1 - dev: true - - /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true - - /read-pkg-up@10.0.0: - resolution: {integrity: sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==} - engines: {node: '>=16'} + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} dependencies: - find-up: 6.3.0 - read-pkg: 8.1.0 - type-fest: 3.13.1 + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 dev: true - /read-pkg@8.1.0: - resolution: {integrity: sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==} - engines: {node: '>=16'} + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} dependencies: '@types/normalize-package-data': 2.4.4 - normalize-package-data: 6.0.0 - parse-json: 7.1.1 - type-fest: 4.10.3 - dev: true - - /readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - dev: true - - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - dev: true - - /readdir-glob@1.1.3: - resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - dependencies: - minimatch: 5.1.6 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 dev: true - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 + /regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true dev: true - /recursive-readdir@2.2.3: - resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} - engines: {node: '>=6.0.0'} + /regjsparser@0.10.0: + resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} + hasBin: true dependencies: - minimatch: 3.1.2 + jsesc: 0.5.0 dev: true /require-directory@2.1.1: @@ -5181,10 +3523,6 @@ packages: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true - /resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - dev: true - /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -5211,84 +3549,22 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true dependencies: - is-core-module: 2.13.1 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true - /responselike@3.0.0: - resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} - engines: {node: '>=14.16'} - dependencies: - lowercase-keys: 3.0.0 - dev: true - - /resq@1.11.0: - resolution: {integrity: sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==} - dependencies: - fast-deep-equal: 2.0.1 - dev: true - - /restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: true - /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true - /rgb2hex@0.2.5: - resolution: {integrity: sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==} - dev: true - - /rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - - /run-async@3.0.0: - resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} - engines: {node: '>=0.12.0'} - dev: true - /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 dev: true - /rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - dependencies: - tslib: 2.6.1 - dev: true - - /safaridriver@0.1.2: - resolution: {integrity: sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==} - dev: true - - /safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: true - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true - /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true @@ -5300,6 +3576,11 @@ packages: xmlchars: 2.2.0 dev: true + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + dev: true + /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -5311,35 +3592,12 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true - - /serialize-error@11.0.3: - resolution: {integrity: sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==} - engines: {node: '>=14.16'} - dependencies: - type-fest: 2.19.0 - dev: true - - /serialize-javascript@6.0.0: - resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} - dependencies: - randombytes: 2.1.0 - dev: true - - /set-function-length@1.2.1: - resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - gopd: 1.0.1 - has-property-descriptors: 1.0.2 - dev: true + dev: false - /setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true dev: true /shebang-command@2.0.0: @@ -5358,11 +3616,6 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true - /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -5372,29 +3625,10 @@ packages: engines: {node: '>=8'} dev: true - /smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - dev: true - - /socks-proxy-agent@8.0.2: - resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) - socks: 2.8.1 - transitivePeerDependencies: - - supports-color - dev: true - - /socks@2.8.1: - resolution: {integrity: sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - dependencies: - ip-address: 9.0.5 - smart-buffer: 4.2.0 - dev: true + /source-map-generator@0.8.0: + resolution: {integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==} + engines: {node: '>= 10'} + dev: false /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -5412,7 +3646,7 @@ packages: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.17 + spdx-license-ids: 3.0.20 dev: true /spdx-exceptions@2.5.0: @@ -5423,26 +3657,17 @@ packages: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.17 - dev: true - - /spdx-license-ids@3.0.17: - resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} + spdx-license-ids: 3.0.20 dev: true - /split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} + /spdx-license-ids@3.0.20: + resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} dev: true /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true - /sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - dev: true - /stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -5450,20 +3675,6 @@ packages: escape-string-regexp: 2.0.0 dev: true - /stream-buffers@3.0.2: - resolution: {integrity: sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==} - engines: {node: '>= 0.10.0'} - dev: true - - /streamx@2.16.1: - resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} - dependencies: - fast-fifo: 1.3.2 - queue-tick: 1.0.1 - optionalDependencies: - bare-events: 2.2.1 - dev: true - /string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -5481,27 +3692,6 @@ packages: strip-ansi: 6.0.1 dev: true - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - dev: true - - /string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - dependencies: - safe-buffer: 5.1.2 - dev: true - - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - dependencies: - safe-buffer: 5.2.1 - dev: true - /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5509,13 +3699,6 @@ packages: ansi-regex: 5.0.1 dev: true - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - dev: true - /strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -5526,9 +3709,11 @@ packages: engines: {node: '>=6'} dev: true - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 dev: true /strip-json-comments@3.1.1: @@ -5570,41 +3755,6 @@ packages: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true - /tar-fs@3.0.4: - resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} - dependencies: - mkdirp-classic: 0.5.3 - pump: 3.0.0 - tar-stream: 3.1.7 - dev: true - - /tar-fs@3.0.5: - resolution: {integrity: sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==} - dependencies: - pump: 3.0.0 - tar-stream: 3.1.7 - optionalDependencies: - bare-fs: 2.2.1 - bare-path: 2.1.0 - dev: true - - /tar-stream@3.1.7: - resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - dependencies: - b4a: 1.6.6 - fast-fifo: 1.3.2 - streamx: 2.16.1 - dev: true - - /tcp-port-used@1.0.2: - resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==} - dependencies: - debug: 4.3.1 - is2: 2.0.9 - transitivePeerDependencies: - - supports-color - dev: true - /test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -5618,17 +3768,6 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true - /through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true - - /tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - dependencies: - os-tmpdir: 1.0.2 - dev: true - /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true @@ -5645,8 +3784,8 @@ packages: is-number: 7.0.0 dev: true - /tough-cookie@4.1.3: - resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} dependencies: psl: 1.9.0 @@ -5666,25 +3805,22 @@ packages: punycode: 2.3.1 dev: true - /traverse@0.3.9: - resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} - dev: true - - /ts-api-utils@1.2.1(typescript@5.1.6): - resolution: {integrity: sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==} + /ts-api-utils@1.3.0(typescript@5.5.4): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.1.6 + typescript: 5.5.4 dev: true - /ts-jest@29.1.2(@babel/core@7.24.0)(esbuild@0.19.12)(jest@29.7.0)(typescript@5.1.6): - resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} - engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} + /ts-jest@29.2.5(@babel/core@7.25.2)(esbuild@0.23.1)(jest@29.7.0)(typescript@5.5.4): + resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 '@jest/types': ^29.0.0 babel-jest: ^29.0.0 esbuild: '*' @@ -5693,6 +3829,8 @@ packages: peerDependenciesMeta: '@babel/core': optional: true + '@jest/transform': + optional: true '@jest/types': optional: true babel-jest: @@ -5700,22 +3838,23 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.25.2 bs-logger: 0.2.6 - esbuild: 0.19.12 + ejs: 3.1.10 + esbuild: 0.23.1 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.11.24) + jest: 29.7.0(@types/node@22.5.5) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.6.0 - typescript: 5.1.6 + semver: 7.6.3 + typescript: 5.5.4 yargs-parser: 21.1.1 dev: true - /tslib@2.6.1: - resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} + /tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} dev: true /type-check@0.4.0: @@ -5730,59 +3869,55 @@ packages: engines: {node: '>=4'} dev: true - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} dev: true - /type-fest@2.13.0: - resolution: {integrity: sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==} - engines: {node: '>=12.20'} - dev: true - - /type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} dev: true - /type-fest@3.13.1: - resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} - engines: {node: '>=14.16'} + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} dev: true - /type-fest@4.10.3: - resolution: {integrity: sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==} - engines: {node: '>=16'} + /typescript-eslint@8.6.0(eslint@9.11.0)(typescript@5.5.4): + resolution: {integrity: sha512-eEhhlxCEpCd4helh3AO1hk0UP2MvbRi9CtIAJTVPQjuSXOOO2jsEacNi4UdcJzZJbeuVg1gMhtZ8UYb+NFYPrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 8.6.0(@typescript-eslint/parser@8.6.0)(eslint@9.11.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.6.0(eslint@9.11.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.6.0(eslint@9.11.0)(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color dev: true - /typescript@5.1.6: - resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + /typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true dev: true - /uglify-js@3.17.4: - resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + /uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} hasBin: true requiresBuild: true dev: true optional: true - /unbzip2-stream@1.4.3: - resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - dependencies: - buffer: 5.7.1 - through: 2.3.8 - dev: true - - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} dev: true /universalify@0.2.0: @@ -5790,35 +3925,15 @@ packages: engines: {node: '>= 4.0.0'} dev: true - /universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - dev: true - - /unzipper@0.10.14: - resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} - dependencies: - big-integer: 1.6.52 - binary: 0.3.0 - bluebird: 3.4.7 - buffer-indexof-polyfill: 1.0.2 - duplexer2: 0.1.4 - fstream: 1.0.12 - graceful-fs: 4.2.11 - listenercount: 1.0.1 - readable-stream: 2.3.8 - setimmediate: 1.0.5 - dev: true - - /update-browserslist-db@1.0.13(browserslist@4.23.0): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + /update-browserslist-db@1.1.0(browserslist@4.23.3): + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.23.0 - escalade: 3.1.2 - picocolors: 1.0.0 + browserslist: 4.23.3 + escalade: 3.2.0 + picocolors: 1.1.0 dev: true /uri-js@4.4.1: @@ -5834,17 +3949,8 @@ packages: requires-port: 1.0.0 dev: true - /userhome@1.0.0: - resolution: {integrity: sha512-ayFKY3H+Pwfy4W98yPdtH1VqH4psDeyW8lYYFzfecR9d6hqLpqhecktvYR3SEEXt7vG0S1JEpciI3g94pMErig==} - engines: {node: '>= 0.8.0'} - dev: true - - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true - - /v8-to-istanbul@9.2.0: - resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + /v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -5874,119 +3980,12 @@ packages: xml-name-validator: 4.0.0 dev: true - /wait-port@1.1.0: - resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} - engines: {node: '>=10'} - hasBin: true - dependencies: - chalk: 4.1.2 - commander: 9.5.0 - debug: 4.3.4(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - dev: true - /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: makeerror: 1.0.12 dev: true - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - dependencies: - defaults: 1.0.4 - dev: true - - /wdio-chromedriver-service@8.1.1(webdriverio@8.32.3): - resolution: {integrity: sha512-pN3GiOkTIMnalfq4PJAHdX95pDp1orHnTY8W1fIbd6ok81ba97UjerTgS7lUDRUh1p0MAm35Ww0uc0/9wzB7SA==} - engines: {node: ^16.13 || >=18} - peerDependencies: - '@wdio/types': ^7.0.0 || ^8.0.0-alpha.219 - chromedriver: '*' - webdriverio: ^7.0.0 || ^8.0.0-alpha.219 - peerDependenciesMeta: - '@wdio/types': - optional: true - chromedriver: - optional: true - dependencies: - '@wdio/logger': 8.28.0 - fs-extra: 11.2.0 - split2: 4.2.0 - tcp-port-used: 1.0.2 - webdriverio: 8.32.3(typescript@5.1.6) - transitivePeerDependencies: - - supports-color - dev: true - - /web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - dev: true - - /webdriver@8.32.3: - resolution: {integrity: sha512-1/kpZvuftt59oikHs+6FvWXNfOM5tgMMMAk3LnMe7D938dVOoNGAe46fq0oL/xsxxPicbMRTRgIy1OifLiglaA==} - engines: {node: ^16.13 || >=18} - dependencies: - '@types/node': 20.11.24 - '@types/ws': 8.5.10 - '@wdio/config': 8.32.3 - '@wdio/logger': 8.28.0 - '@wdio/protocols': 8.32.0 - '@wdio/types': 8.32.2 - '@wdio/utils': 8.32.3 - deepmerge-ts: 5.1.0 - got: 12.6.1 - ky: 0.33.3 - ws: 8.16.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - - /webdriverio@8.32.3(typescript@5.1.6): - resolution: {integrity: sha512-SupbQKMtUZHSH7lmF5xaJPgxsn8sIBNAjs1CyPI33u30eY9VcVQ4CJQ818ZS3FLxR0q2XdWk9lsQNyhZwlN3RA==} - engines: {node: ^16.13 || >=18} - peerDependencies: - devtools: ^8.14.0 - peerDependenciesMeta: - devtools: - optional: true - dependencies: - '@types/node': 20.11.24 - '@wdio/config': 8.32.3 - '@wdio/logger': 8.28.0 - '@wdio/protocols': 8.32.0 - '@wdio/repl': 8.24.12 - '@wdio/types': 8.32.2 - '@wdio/utils': 8.32.3 - archiver: 6.0.2 - aria-query: 5.3.0 - css-shorthand-properties: 1.1.1 - css-value: 0.0.1 - devtools-protocol: 0.0.1262051 - grapheme-splitter: 1.0.4 - import-meta-resolve: 4.0.0 - is-plain-obj: 4.1.0 - lodash.clonedeep: 4.5.0 - lodash.zip: 4.2.0 - minimatch: 9.0.3 - puppeteer-core: 20.9.0(typescript@5.1.6) - query-selector-shadow-dom: 1.0.1 - resq: 1.11.0 - rgb2hex: 0.2.5 - serialize-error: 11.0.3 - webdriver: 8.32.3 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - typescript - - utf-8-validate - dev: true - /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true @@ -6031,31 +4030,15 @@ packages: isexe: 2.0.0 dev: true - /which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - dependencies: - isexe: 3.1.1 + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} dev: true /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: true - /workerpool@6.2.1: - resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} - dev: true - - /wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -6065,15 +4048,6 @@ packages: strip-ansi: 6.0.1 dev: true - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - dev: true - /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true @@ -6086,21 +4060,8 @@ packages: signal-exit: 3.0.7 dev: true - /ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - - /ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -6132,60 +4093,19 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - - /yargs-parser@20.2.4: - resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} - engines: {node: '>=10'} - dev: true + dev: false /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} dev: true - /yargs-unparser@2.0.0: - resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} - engines: {node: '>=10'} - dependencies: - camelcase: 6.3.0 - decamelize: 4.0.0 - flat: 5.0.2 - is-plain-obj: 2.1.0 - dev: true - - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - dependencies: - cliui: 7.0.4 - escalade: 3.1.2 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.4 - dev: true - - /yargs@17.7.1: - resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} - engines: {node: '>=12'} - dependencies: - cliui: 8.0.1 - escalade: 3.1.2 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - dev: true - /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} dependencies: cliui: 8.0.1 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 @@ -6193,28 +4113,7 @@ packages: yargs-parser: 21.1.1 dev: true - /yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - dev: true - /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true - - /yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - dev: true - - /zip-stream@5.0.2: - resolution: {integrity: sha512-LfOdrUvPB8ZoXtvOBz6DlNClfvi//b5d56mSWyJi7XbH/HfhOHfUhOqxhT/rUiR7yiktlunqRo+jY6y/cWC/5g==} - engines: {node: '>= 12.0.0'} - dependencies: - archiver-utils: 4.0.1 - compress-commons: 5.0.3 - readable-stream: 3.6.2 - dev: true diff --git a/requirements.txt b/requirements.txt index 87e32081..de65e019 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ +mkdocs==1.6.0 mkdocs-material==9.5.31 mkdocs-static-i18n==1.2.3 +pymdown-extensions==10.9 diff --git a/src/Card.ts b/src/Card.ts deleted file mode 100644 index 2c4b2599..00000000 --- a/src/Card.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Question } from "./Question"; -import { CardScheduleInfo } from "./CardSchedule"; -import { CardListType } from "./Deck"; - -export class Card { - question: Question; - cardIdx: number; - - // scheduling - get hasSchedule(): boolean { - return this.scheduleInfo != null; - } - scheduleInfo?: CardScheduleInfo; - - // visuals - front: string; - back: string; - - constructor(init?: Partial) { - Object.assign(this, init); - } - - get cardListType(): CardListType { - return this.isNew ? CardListType.NewCard : CardListType.DueCard; - } - - get isNew(): boolean { - return !this.hasSchedule || this.scheduleInfo.isDummyScheduleForNewCard(); - } - - get isDue(): boolean { - return this.hasSchedule && this.scheduleInfo.isDue(); - } - - formatSchedule(): string { - let result: string = ""; - if (this.hasSchedule) result = this.scheduleInfo.formatSchedule(); - else result = "New"; - return result; - } -} diff --git a/src/CardSchedule.ts b/src/CardSchedule.ts deleted file mode 100644 index bfda1571..00000000 --- a/src/CardSchedule.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Moment } from "moment"; -import { - LEGACY_SCHEDULING_EXTRACTOR, - MULTI_SCHEDULING_EXTRACTOR, - TICKS_PER_DAY, -} from "./constants"; -import { INoteEaseList } from "./NoteEaseList"; -import { ReviewResponse, schedule } from "./scheduling"; -import { SRSettings } from "./settings"; -import { formatDate_YYYY_MM_DD } from "./util/utils"; -import { DateUtil, globalDateProvider } from "./util/DateProvider"; - -export class CardScheduleInfo { - dueDate: Moment; - interval: number; - ease: number; - delayBeforeReviewTicks: number; - - // A question can have multiple cards. The schedule info for all sibling cards are formatted together - // in a single comment, such as: - // - // - // However, not all sibling cards may have been reviewed. Therefore we need a method of indicating that a particular card - // has not been reviewed, and should be considered "new" - // This is done by using this magic value for the date - private static dummyDueDateForNewCard: string = "2000-01-01"; - - constructor(dueDate: Moment, interval: number, ease: number, delayBeforeReviewTicks: number) { - this.dueDate = dueDate; - this.interval = interval; - this.ease = ease; - this.delayBeforeReviewTicks = delayBeforeReviewTicks; - } - - get delayBeforeReviewDaysInt(): number { - return Math.ceil(this.delayBeforeReviewTicks / TICKS_PER_DAY); - } - - isDue(): boolean { - return this.dueDate.isSameOrBefore(globalDateProvider.today); - } - - isDummyScheduleForNewCard(): boolean { - return this.formatDueDate() == CardScheduleInfo.dummyDueDateForNewCard; - } - - static getDummyScheduleForNewCard(settings: SRSettings): CardScheduleInfo { - return CardScheduleInfo.fromDueDateStr( - CardScheduleInfo.dummyDueDateForNewCard, - CardScheduleInfo.initialInterval, - settings.baseEase, - 0, - ); - } - - static fromDueDateStr( - dueDateStr: string, - interval: number, - ease: number, - delayBeforeReviewTicks: number, - ) { - const dueDateTicks: Moment = DateUtil.dateStrToMoment(dueDateStr); - return new CardScheduleInfo(dueDateTicks, interval, ease, delayBeforeReviewTicks); - } - - static fromDueDateMoment( - dueDateTicks: Moment, - interval: number, - ease: number, - delayBeforeReviewTicks: number, - ) { - return new CardScheduleInfo(dueDateTicks, interval, ease, delayBeforeReviewTicks); - } - - static get initialInterval(): number { - return 1.0; - } - - formatDueDate(): string { - return formatDate_YYYY_MM_DD(this.dueDate); - } - - formatSchedule() { - return `!${this.formatDueDate()},${this.interval},${this.ease}`; - } -} - -export interface ICardScheduleCalculator { - getResetCardSchedule(): CardScheduleInfo; - getNewCardSchedule(response: ReviewResponse, notePath: string): CardScheduleInfo; - calcUpdatedSchedule(response: ReviewResponse, schedule: CardScheduleInfo): CardScheduleInfo; -} - -export class CardScheduleCalculator { - settings: SRSettings; - noteEaseList: INoteEaseList; - dueDatesFlashcards: Record = {}; // Record<# of days in future, due count> - - constructor(settings: SRSettings, noteEaseList: INoteEaseList) { - this.settings = settings; - this.noteEaseList = noteEaseList; - } - - getResetCardSchedule(): CardScheduleInfo { - const interval = CardScheduleInfo.initialInterval; - const ease = this.settings.baseEase; - const dueDate = globalDateProvider.today.add(interval, "d"); - const delayBeforeReview = 0; - return CardScheduleInfo.fromDueDateMoment(dueDate, interval, ease, delayBeforeReview); - } - - getNewCardSchedule(response: ReviewResponse, notePath: string): CardScheduleInfo { - let initial_ease: number = this.settings.baseEase; - if (this.noteEaseList.hasEaseForPath(notePath)) { - initial_ease = Math.round(this.noteEaseList.getEaseByPath(notePath)); - } - const delayBeforeReview = 0; - - const schedObj: Record = schedule( - response, - CardScheduleInfo.initialInterval, - initial_ease, - delayBeforeReview, - this.settings, - this.dueDatesFlashcards, - ); - - const interval = schedObj.interval; - const ease = schedObj.ease; - const dueDate = globalDateProvider.today.add(interval, "d"); - return CardScheduleInfo.fromDueDateMoment(dueDate, interval, ease, delayBeforeReview); - } - - calcUpdatedSchedule( - response: ReviewResponse, - cardSchedule: CardScheduleInfo, - ): CardScheduleInfo { - const schedObj: Record = schedule( - response, - cardSchedule.interval, - cardSchedule.ease, - cardSchedule.delayBeforeReviewTicks, - this.settings, - this.dueDatesFlashcards, - ); - const interval = schedObj.interval; - const ease = schedObj.ease; - const dueDate = globalDateProvider.today.add(interval, "d"); - const delayBeforeReview = 0; - return CardScheduleInfo.fromDueDateMoment(dueDate, interval, ease, delayBeforeReview); - } -} - -export class NoteCardScheduleParser { - static createCardScheduleInfoList(questionText: string): CardScheduleInfo[] { - let scheduling: RegExpMatchArray[] = [...questionText.matchAll(MULTI_SCHEDULING_EXTRACTOR)]; - if (scheduling.length === 0) - scheduling = [...questionText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)]; - - const result: CardScheduleInfo[] = []; - for (let i = 0; i < scheduling.length; i++) { - const match: RegExpMatchArray = scheduling[i]; - const dueDateStr = match[1]; - const interval = parseInt(match[2]); - const ease = parseInt(match[3]); - const dueDate: Moment = DateUtil.dateStrToMoment(dueDateStr); - const delayBeforeReviewTicks: number = - dueDate.valueOf() - globalDateProvider.today.valueOf(); - - const info: CardScheduleInfo = new CardScheduleInfo( - dueDate, - interval, - ease, - delayBeforeReviewTicks, - ); - result.push(info); - } - return result; - } - - static removeCardScheduleInfo(questionText: string): string { - return questionText.replace(//gm, ""); - } -} diff --git a/src/NoteEaseCalculator.ts b/src/NoteEaseCalculator.ts deleted file mode 100644 index 24cc1bee..00000000 --- a/src/NoteEaseCalculator.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Note } from "./Note"; -import { SRSettings } from "./settings"; - -export class NoteEaseCalculator { - static Calculate(note: Note, settings: SRSettings): number { - let totalEase: number = 0; - let scheduledCount: number = 0; - - note.questionList.forEach((question) => { - question.cards - .filter((card) => card.hasSchedule) - .forEach((card) => { - totalEase += card.scheduleInfo.ease; - scheduledCount++; - }); - }); - - let result: number = 0; - if (scheduledCount > 0) { - const flashcardsInNoteAvgEase: number = totalEase / scheduledCount; - const flashcardContribution: number = Math.min( - 1.0, - Math.log(scheduledCount + 0.5) / Math.log(64), - ); - result = - flashcardsInNoteAvgEase * flashcardContribution + - settings.baseEase * (1.0 - flashcardContribution); - } - return result; - } -} diff --git a/src/ReviewDeck.ts b/src/ReviewDeck.ts deleted file mode 100644 index b35fbb29..00000000 --- a/src/ReviewDeck.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { App, FuzzySuggestModal, TFile } from "obsidian"; - -import { SchedNote } from "src/main"; -import { t } from "src/lang/helpers"; - -export class ReviewDeck { - public deckName: string; - public newNotes: TFile[] = []; - public scheduledNotes: SchedNote[] = []; - public activeFolders: Set; - public dueNotesCount = 0; - - constructor(name: string) { - this.deckName = name; - this.activeFolders = new Set([this.deckName, t("TODAY")]); - } - - public sortNotes(pageranks: Record): void { - // sort new notes by importance - this.newNotes = this.newNotes.sort( - (a: TFile, b: TFile) => (pageranks[b.path] || 0) - (pageranks[a.path] || 0), - ); - - // sort scheduled notes by date & within those days, sort them by importance - this.scheduledNotes = this.scheduledNotes.sort((a: SchedNote, b: SchedNote) => { - const result = a.dueUnix - b.dueUnix; - if (result != 0) { - return result; - } - return (pageranks[b.note.path] || 0) - (pageranks[a.note.path] || 0); - }); - } -} - -export class ReviewDeckSelectionModal extends FuzzySuggestModal { - public deckKeys: string[] = []; - public submitCallback: (deckKey: string) => void; - - constructor(app: App, deckKeys: string[]) { - super(app); - this.deckKeys = deckKeys; - } - - getItems(): string[] { - return this.deckKeys; - } - - getItemText(item: string): string { - return item; - } - - onChooseItem(deckKey: string, _: MouseEvent | KeyboardEvent): void { - this.close(); - this.submitCallback(deckKey); - } -} diff --git a/src/algorithms/base/isrs-algorithm.ts b/src/algorithms/base/isrs-algorithm.ts new file mode 100644 index 00000000..7cc867be --- /dev/null +++ b/src/algorithms/base/isrs-algorithm.ts @@ -0,0 +1,33 @@ +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { OsrNoteGraph } from "src/algorithms/osr/osr-note-graph"; +import { DueDateHistogram } from "src/due-date-histogram"; +import { Note } from "src/note"; + +export interface ISrsAlgorithm { + noteOnLoadedNote(path: string, note: Note, noteEase: number): void; + noteCalcNewSchedule( + notePath: string, + osrNoteGraph: OsrNoteGraph, + response: ReviewResponse, + dueDateNoteHistogram: DueDateHistogram, + ): RepItemScheduleInfo; + noteCalcUpdatedSchedule( + notePath: string, + noteSchedule: RepItemScheduleInfo, + response: ReviewResponse, + dueDateNoteHistogram: DueDateHistogram, + ): RepItemScheduleInfo; + + cardGetResetSchedule(): RepItemScheduleInfo; + cardGetNewSchedule( + response: ReviewResponse, + notePath: string, + dueDateFlashcardHistogram: DueDateHistogram, + ): RepItemScheduleInfo; + cardCalcUpdatedSchedule( + response: ReviewResponse, + schedule: RepItemScheduleInfo, + dueDateFlashcardHistogram: DueDateHistogram, + ): RepItemScheduleInfo; +} diff --git a/src/algorithms/base/rep-item-schedule-info.ts b/src/algorithms/base/rep-item-schedule-info.ts new file mode 100644 index 00000000..8571411d --- /dev/null +++ b/src/algorithms/base/rep-item-schedule-info.ts @@ -0,0 +1,29 @@ +import { Moment } from "moment"; + +import { TICKS_PER_DAY } from "src/constants"; +import { formatDate_YYYY_MM_DD, globalDateProvider } from "src/utils/dates"; + +export abstract class RepItemScheduleInfo { + dueDate: Moment; + latestEase: number; + interval: number; + delayedBeforeReviewTicks: number; + + get dueDateAsUnix(): number { + return this.dueDate.valueOf(); + } + + isDue(): boolean { + return this.dueDate && this.dueDate.isSameOrBefore(globalDateProvider.today); + } + + formatDueDate(): string { + return formatDate_YYYY_MM_DD(this.dueDate); + } + + delayedBeforeReviewDaysInt(): number { + return Math.max(0, Math.floor(this.delayedBeforeReviewTicks / TICKS_PER_DAY)); + } + + abstract formatCardScheduleForHtmlComment(): string; +} diff --git a/src/algorithms/base/repetition-item.ts b/src/algorithms/base/repetition-item.ts new file mode 100644 index 00000000..e716de00 --- /dev/null +++ b/src/algorithms/base/repetition-item.ts @@ -0,0 +1,33 @@ +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { RepItemStorageInfo } from "src/data-stores/base/rep-item-storage-info"; + +export enum ReviewResponse { + Easy, + Good, + Hard, + Reset, +} +export enum RepetitionPhase { + New, + Review, +} + +export class RepetitionItem { + repetitionPhase: RepetitionPhase; + + scheduleInfo: RepItemScheduleInfo; + storageInfo: RepItemStorageInfo; + + // scheduling + get hasSchedule(): boolean { + return this.scheduleInfo != null; + } + + get isNew(): boolean { + return !this.hasSchedule; + } + + get isDue(): boolean { + return this.hasSchedule && this.scheduleInfo.isDue(); + } +} diff --git a/src/algorithms/base/srs-algorithm.ts b/src/algorithms/base/srs-algorithm.ts new file mode 100644 index 00000000..4e375ae6 --- /dev/null +++ b/src/algorithms/base/srs-algorithm.ts @@ -0,0 +1,12 @@ +import { ISrsAlgorithm } from "src/algorithms/base/isrs-algorithm"; + +export class SrsAlgorithm { + static instance: ISrsAlgorithm; + + public static getInstance(): ISrsAlgorithm { + if (!SrsAlgorithm.instance) { + throw new Error("there is no SrsAlgorithm instance."); + } + return SrsAlgorithm.instance; + } +} diff --git a/src/algorithms/osr/note-scheduling.ts b/src/algorithms/osr/note-scheduling.ts new file mode 100644 index 00000000..aa0146a9 --- /dev/null +++ b/src/algorithms/osr/note-scheduling.ts @@ -0,0 +1,74 @@ +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { TICKS_PER_DAY } from "src/constants"; +import { DueDateHistogram } from "src/due-date-histogram"; +import { t } from "src/lang/helpers"; +import { SRSettings } from "src/settings"; + +// Note that if dueDateHistogram is provided, then it is just used to assist with fuzzing. +// (Unlike earlier versions, it is not updated based on the calculated schedule. The +// caller needs to do that if needed. + +export function osrSchedule( + response: ReviewResponse, + originalInterval: number, + ease: number, + delayedBeforeReview: number, + settingsObj: SRSettings, + dueDateHistogram?: DueDateHistogram, +): Record { + const delayedBeforeReviewDays = Math.max(0, Math.floor(delayedBeforeReview / TICKS_PER_DAY)); + let interval: number = originalInterval; + + if (response === ReviewResponse.Easy) { + ease += 20; + interval = ((interval + delayedBeforeReviewDays) * ease) / 100; + interval *= settingsObj.easyBonus; + } else if (response === ReviewResponse.Good) { + interval = ((interval + delayedBeforeReviewDays / 2) * ease) / 100; + } else if (response === ReviewResponse.Hard) { + ease = Math.max(130, ease - 20); + interval = Math.max( + 1, + (interval + delayedBeforeReviewDays / 4) * settingsObj.lapsesIntervalChange, + ); + } + + // replaces random fuzz with load balancing over the fuzz interval + if (dueDateHistogram !== undefined) { + interval = Math.round(interval); + // disable fuzzing for small intervals + if (interval > 4) { + let fuzz = 0; + if (interval < 7) fuzz = 1; + else if (interval < 30) fuzz = Math.max(2, Math.floor(interval * 0.15)); + else fuzz = Math.max(4, Math.floor(interval * 0.05)); + + const fuzzedInterval = dueDateHistogram.findLeastUsedIntervalOverRange(interval, fuzz); + interval = fuzzedInterval; + } + } + + interval = Math.min(interval, settingsObj.maximumInterval); + interval = Math.round(interval * 10) / 10; + + return { interval, ease }; +} + +export function textInterval(interval: number, isMobile: boolean): string { + if (interval === undefined) { + return t("NEW"); + } + + const m: number = Math.round(interval / 3.04375) / 10, + y: number = Math.round(interval / 36.525) / 10; + + if (isMobile) { + if (m < 1.0) return t("DAYS_STR_IVL_MOBILE", { interval }); + else if (y < 1.0) return t("MONTHS_STR_IVL_MOBILE", { interval: m }); + else return t("YEARS_STR_IVL_MOBILE", { interval: y }); + } else { + if (m < 1.0) return t("DAYS_STR_IVL", { interval }); + else if (y < 1.0) return t("MONTHS_STR_IVL", { interval: m }); + else return t("YEARS_STR_IVL", { interval: y }); + } +} diff --git a/src/algorithms/osr/obsidian-vault-notelink-info-finder.ts b/src/algorithms/osr/obsidian-vault-notelink-info-finder.ts new file mode 100644 index 00000000..077df131 --- /dev/null +++ b/src/algorithms/osr/obsidian-vault-notelink-info-finder.ts @@ -0,0 +1,17 @@ +import { MetadataCache } from "obsidian"; + +export interface IOsrVaultNoteLinkInfoFinder { + getResolvedTargetLinksForNotePath(sourcePath: string): Record; +} + +export class ObsidianVaultNoteLinkInfoFinder implements IOsrVaultNoteLinkInfoFinder { + private metadataCache: MetadataCache; + + constructor(metadataCache: MetadataCache) { + this.metadataCache = metadataCache; + } + + getResolvedTargetLinksForNotePath(path: string): Record { + return this.metadataCache.resolvedLinks[path]; + } +} diff --git a/src/algorithms/osr/osr-note-graph.ts b/src/algorithms/osr/osr-note-graph.ts new file mode 100644 index 00000000..d1231eaa --- /dev/null +++ b/src/algorithms/osr/osr-note-graph.ts @@ -0,0 +1,98 @@ +import * as graph from "pagerank.js"; + +import { IOsrVaultNoteLinkInfoFinder } from "src/algorithms/osr/obsidian-vault-notelink-info-finder"; +import { INoteEaseList } from "src/note-ease-list"; +import { isSupportedFileType } from "src/utils/fs"; + +export interface LinkStat { + sourcePath: string; + linkCount: number; +} + +export interface NoteLinkStat { + linkTotal: number; + linkPGTotal: number; + totalLinkCount: number; +} + +export class OsrNoteGraph { + private vaultNoteLinkInfoFinder: IOsrVaultNoteLinkInfoFinder; + // Key: targetFilename + // Value: Map + // This is the number of links from sourceFilename to targetFilename + // For simplicity, we just store the filename without the directory or filename extension + incomingLinks: Record = {}; + pageranks: Record = {}; + + constructor(vaultNoteLinkInfoFinder: IOsrVaultNoteLinkInfoFinder) { + this.vaultNoteLinkInfoFinder = vaultNoteLinkInfoFinder; + this.reset(); + } + + reset() { + this.incomingLinks = {}; + this.pageranks = {}; + graph.reset(); + } + + processLinks(path: string) { + if (this.incomingLinks[path] === undefined) { + this.incomingLinks[path] = []; + } + + const targetLinks = + this.vaultNoteLinkInfoFinder.getResolvedTargetLinksForNotePath(path) || + /* c8 ignore next */ {}; + for (const targetPath in targetLinks) { + if (this.incomingLinks[targetPath] === undefined) this.incomingLinks[targetPath] = []; + + // markdown files only + if (isSupportedFileType(targetPath)) { + const linkCount: number = targetLinks[targetPath]; + this.incomingLinks[targetPath].push({ + sourcePath: path, + linkCount, + }); + + graph.link(path, targetPath, linkCount); + } + } + } + + calcNoteLinkStat(notePath: string, noteEaseList: INoteEaseList): NoteLinkStat { + let linkTotal = 0, + linkPGTotal = 0, + totalLinkCount = 0; + + for (const statObj of this.incomingLinks[notePath] || /* c8 ignore next */ []) { + const ease: number = noteEaseList.getEaseByPath(statObj.sourcePath); + if (ease) { + linkTotal += statObj.linkCount * this.pageranks[statObj.sourcePath] * ease; + linkPGTotal += this.pageranks[statObj.sourcePath] * statObj.linkCount; + totalLinkCount += statObj.linkCount; + } + } + + const outgoingLinks = + this.vaultNoteLinkInfoFinder.getResolvedTargetLinksForNotePath(notePath) || + /* c8 ignore next */ {}; + for (const outgoingLink in outgoingLinks) { + const ease: number = noteEaseList.getEaseByPath(outgoingLink); + const linkCount: number = outgoingLinks[outgoingLink]; + const pageRank: number = this.pageranks[outgoingLink]; + if (ease) { + linkTotal += linkCount * pageRank * ease; + linkPGTotal += pageRank * linkCount; + totalLinkCount += linkCount; + } + } + + return { linkTotal, linkPGTotal, totalLinkCount }; + } + + generatePageRanks() { + graph.rank(0.85, 0.000001, (node: string, rank: number) => { + this.pageranks[node] = rank * 10000; + }); + } +} diff --git a/src/algorithms/osr/rep-item-schedule-info-osr.ts b/src/algorithms/osr/rep-item-schedule-info-osr.ts new file mode 100644 index 00000000..2e863f39 --- /dev/null +++ b/src/algorithms/osr/rep-item-schedule-info-osr.ts @@ -0,0 +1,62 @@ +import { Moment } from "moment"; + +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { SRSettings } from "src/settings"; +import { DateUtil, globalDateProvider } from "src/utils/dates"; + +export class RepItemScheduleInfo_Osr extends RepItemScheduleInfo { + // A question can have multiple cards. The schedule info for all sibling cards are formatted together + // in a single comment, such as: + // + // + // However, not all sibling cards may have been reviewed. Therefore we need a method of indicating that a particular card + // has not been reviewed, and should be considered "new" + // This is done by using this magic value for the date + public static dummyDueDateForNewCard: string = "2000-01-01"; + + constructor( + dueDate: Moment, + interval: number, + latestEase: number, + delayedBeforeReviewTicks: number | null = null, + ) { + super(); + this.dueDate = dueDate; + this.interval = Math.round(interval); + this.latestEase = latestEase; + this.delayedBeforeReviewTicks = delayedBeforeReviewTicks; + if (dueDate && delayedBeforeReviewTicks == null) { + this.delayedBeforeReviewTicks = globalDateProvider.today.valueOf() - dueDate.valueOf(); + } + } + + formatCardScheduleForHtmlComment(): string { + // We always want the correct schedule format, so we use the dummy due date if there is no schedule for a card + const dateStr: string = this.dueDate + ? this.formatDueDate() + : RepItemScheduleInfo_Osr.dummyDueDateForNewCard; + return `!${dateStr},${this.interval},${this.latestEase}`; + } + + static get initialInterval(): number { + return 1.0; + } + + static getDummyScheduleForNewCard(settings: SRSettings): RepItemScheduleInfo_Osr { + return RepItemScheduleInfo_Osr.fromDueDateStr( + RepItemScheduleInfo_Osr.dummyDueDateForNewCard, + RepItemScheduleInfo_Osr.initialInterval, + settings.baseEase, + ); + } + + static fromDueDateStr( + dueDateStr: string, + interval: number, + ease: number, + delayedBeforeReviewTicks: number | null = null, + ) { + const dueDate: Moment = DateUtil.dateStrToMoment(dueDateStr); + return new RepItemScheduleInfo_Osr(dueDate, interval, ease, delayedBeforeReviewTicks); + } +} diff --git a/src/algorithms/osr/srs-algorithm-osr.ts b/src/algorithms/osr/srs-algorithm-osr.ts new file mode 100644 index 00000000..55123aab --- /dev/null +++ b/src/algorithms/osr/srs-algorithm-osr.ts @@ -0,0 +1,210 @@ +import moment, { Moment } from "moment"; + +import { ISrsAlgorithm } from "src/algorithms/base/isrs-algorithm"; +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { osrSchedule } from "src/algorithms/osr/note-scheduling"; +import { NoteLinkStat, OsrNoteGraph } from "src/algorithms/osr/osr-note-graph"; +import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/rep-item-schedule-info-osr"; +import { DueDateHistogram } from "src/due-date-histogram"; +import { Note } from "src/note"; +import { INoteEaseList, NoteEaseList } from "src/note-ease-list"; +import { Question } from "src/question"; +import { SRSettings } from "src/settings"; +import { globalDateProvider } from "src/utils/dates"; + +export class SrsAlgorithm_Osr implements ISrsAlgorithm { + private settings: SRSettings; + private noteEaseList: INoteEaseList; + + constructor(settings: SRSettings) { + this.settings = settings; + this.noteEaseList = new NoteEaseList(settings); + } + + static get initialInterval(): number { + return 1.0; + } + + noteCalcNewSchedule( + notePath: string, + osrNoteGraph: OsrNoteGraph, + response: ReviewResponse, + dueDateNoteHistogram: DueDateHistogram, + ): RepItemScheduleInfo { + const noteLinkStat: NoteLinkStat = osrNoteGraph.calcNoteLinkStat( + notePath, + this.noteEaseList, + ); + + const linkContribution: number = + this.settings.maxLinkFactor * + Math.min(1.0, Math.log(noteLinkStat.totalLinkCount + 0.5) / Math.log(64)); + let ease: number = + (1.0 - linkContribution) * this.settings.baseEase + + (noteLinkStat.totalLinkCount > 0 + ? (linkContribution * noteLinkStat.linkTotal) / noteLinkStat.linkPGTotal + : linkContribution * this.settings.baseEase); + + // add note's average flashcard ease if available + /* c8 ignore next 3 */ + if (this.noteEaseList.hasEaseForPath(notePath)) { + ease = (ease + this.noteEaseList.getEaseByPath(notePath)) / 2; + } + + // Don't know the due date until we know the calculated interval + const dueDate: Moment = null; + const interval: number = SrsAlgorithm_Osr.initialInterval; + ease = Math.round(ease); + const temp: RepItemScheduleInfo_Osr = new RepItemScheduleInfo_Osr(dueDate, interval, ease); + + const result: RepItemScheduleInfo_Osr = this.calcSchedule( + temp, + response, + dueDateNoteHistogram, + ); + + // Calculate the due date now that we know the interval + result.dueDate = moment(globalDateProvider.today.add(result.interval, "d")); + return result; + } + + noteOnLoadedNote(path: string, note: Note, noteEase: number): void { + let flashcardsInNoteAvgEase: number = null; + if (note) { + flashcardsInNoteAvgEase = SrsAlgorithm_Osr.calculateFlashcardAvgEase( + note.questionList, + this.settings, + ); + } + let ease: number = null; + if (flashcardsInNoteAvgEase && noteEase) { + ease = (flashcardsInNoteAvgEase + noteEase) / 2; + } else { + ease = flashcardsInNoteAvgEase ? flashcardsInNoteAvgEase : noteEase; + } + + if (ease) { + this.noteEaseList.setEaseForPath(path, ease); + } + } + + static calculateFlashcardAvgEase(questionList: Question[], settings: SRSettings): number { + let totalEase: number = 0; + let scheduledCount: number = 0; + + questionList.forEach((question) => { + question.cards + .filter((card) => card.hasSchedule) + .forEach((card) => { + totalEase += card.scheduleInfo.latestEase; + scheduledCount++; + }); + }); + + let result: number = 0; + if (scheduledCount > 0) { + const flashcardsInNoteAvgEase: number = totalEase / scheduledCount; + const flashcardContribution: number = Math.min( + 1.0, + Math.log(scheduledCount + 0.5) / Math.log(64), + ); + result = + flashcardsInNoteAvgEase * flashcardContribution + + settings.baseEase * (1.0 - flashcardContribution); + } + return result; + } + + noteCalcUpdatedSchedule( + notePath: string, + noteSchedule: RepItemScheduleInfo, + response: ReviewResponse, + dueDateNoteHistogram: DueDateHistogram, + ): RepItemScheduleInfo { + const noteScheduleOsr: RepItemScheduleInfo_Osr = noteSchedule as RepItemScheduleInfo_Osr; + const temp: RepItemScheduleInfo_Osr = this.calcSchedule( + noteScheduleOsr, + response, + dueDateNoteHistogram, + ); + const interval: number = temp.interval; + const ease: number = temp.latestEase; + + const dueDate: Moment = moment(globalDateProvider.today.add(interval, "d")); + this.noteEaseList.setEaseForPath(notePath, ease); + return new RepItemScheduleInfo_Osr(dueDate, interval, ease); + } + + private calcSchedule( + schedule: RepItemScheduleInfo_Osr, + response: ReviewResponse, + dueDateHistogram: DueDateHistogram, + ): RepItemScheduleInfo_Osr { + const temp: Record = osrSchedule( + response, + schedule.interval, + schedule.latestEase, + schedule.delayedBeforeReviewTicks, + this.settings, + dueDateHistogram, + ); + + return new RepItemScheduleInfo_Osr(globalDateProvider.today, temp.interval, temp.ease); + } + + cardGetResetSchedule(): RepItemScheduleInfo { + const interval = SrsAlgorithm_Osr.initialInterval; + const ease = this.settings.baseEase; + const dueDate = globalDateProvider.today.add(interval, "d"); + return new RepItemScheduleInfo_Osr(dueDate, interval, ease); + } + + cardGetNewSchedule( + response: ReviewResponse, + notePath: string, + dueDateFlashcardHistogram: DueDateHistogram, + ): RepItemScheduleInfo { + let initial_ease: number = this.settings.baseEase; + /* c8 ignore next 3 */ + if (this.noteEaseList.hasEaseForPath(notePath)) { + initial_ease = Math.round(this.noteEaseList.getEaseByPath(notePath)); + } + const delayBeforeReview = 0; + + const schedObj: Record = osrSchedule( + response, + SrsAlgorithm_Osr.initialInterval, + initial_ease, + delayBeforeReview, + this.settings, + dueDateFlashcardHistogram, + ); + + const interval = schedObj.interval; + const ease = schedObj.ease; + const dueDate = globalDateProvider.today.add(interval, "d"); + return new RepItemScheduleInfo_Osr(dueDate, interval, ease, delayBeforeReview); + } + + cardCalcUpdatedSchedule( + response: ReviewResponse, + cardSchedule: RepItemScheduleInfo, + dueDateFlashcardHistogram: DueDateHistogram, + ): RepItemScheduleInfo { + const cardScheduleOsr: RepItemScheduleInfo_Osr = cardSchedule as RepItemScheduleInfo_Osr; + const schedObj: Record = osrSchedule( + response, + cardScheduleOsr.interval, + cardSchedule.latestEase, + cardSchedule.delayedBeforeReviewTicks, + this.settings, + dueDateFlashcardHistogram, + ); + const interval = schedObj.interval; + const ease = schedObj.ease; + const dueDate = globalDateProvider.today.add(interval, "d"); + const delayBeforeReview = 0; + return new RepItemScheduleInfo_Osr(dueDate, interval, ease, delayBeforeReview); + } +} diff --git a/src/app-core.ts b/src/app-core.ts new file mode 100644 index 00000000..b8d00389 --- /dev/null +++ b/src/app-core.ts @@ -0,0 +1,48 @@ +import { App, TFile } from "obsidian"; + +import { OsrCore } from "src/core"; +import { SettingsUtil } from "src/settings"; +import { SrTFile } from "src/sr-file"; + +export class OsrAppCore extends OsrCore { + private app: App; + private _syncLock = false; + + get syncLock(): boolean { + return this._syncLock; + } + + constructor(app: App) { + super(); + this.app = app; + } + + async loadVault(): Promise { + if (this._syncLock) { + return; + } + this._syncLock = true; + + try { + this.loadInit(); + + const notes: TFile[] = this.app.vault.getMarkdownFiles(); + for (const noteFile of notes) { + if (SettingsUtil.isPathInNoteIgnoreFolder(this.settings, noteFile.path)) { + continue; + } + + const file: SrTFile = this.createSrTFile(noteFile); + await this.processFile(file); + } + + this.finaliseLoad(); + } finally { + this._syncLock = false; + } + } + + createSrTFile(note: TFile): SrTFile { + return new SrTFile(this.app.vault, this.app.metadataCache, note); + } +} diff --git a/src/card.ts b/src/card.ts new file mode 100644 index 00000000..89f28841 --- /dev/null +++ b/src/card.ts @@ -0,0 +1,28 @@ +import { RepetitionItem } from "src/algorithms/base/repetition-item"; +import { CardListType } from "src/deck"; +import { Question } from "src/question"; + +export class Card extends RepetitionItem { + question: Question; + cardIdx: number; + + // visuals + front: string; + back: string; + + constructor(init?: Partial) { + super(); + Object.assign(this, init); + } + + get cardListType(): CardListType { + return this.isNew ? CardListType.NewCard : CardListType.DueCard; + } + + formatSchedule(): string { + let result: string = ""; + if (this.hasSchedule) result = this.scheduleInfo.formatCardScheduleForHtmlComment(); + else result = "New"; + return result; + } +} diff --git a/src/constants.ts b/src/constants.ts index 6319a83b..136d3794 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,7 +3,6 @@ export const SCHEDULING_INFO_REGEX = /^---\r?\n((?:.*\r?\n)*)sr-due: (.+)\r?\nsr-interval: (\d+)\r?\nsr-ease: (\d+)\r?\n((?:.*\r?\n)?)---/; export const YAML_FRONT_MATTER_REGEX = /^---\r?\n((?:.*\r?\n)*?)---/; -export const NON_LETTER_SYMBOLS_REGEX = /[!-/:-@[-`{-~}\s]/g; export const MULTI_SCHEDULING_EXTRACTOR = /!([\d-]+),(\d+),(\d+)/gm; export const LEGACY_SCHEDULING_EXTRACTOR = //gm; @@ -36,7 +35,7 @@ export const AUDIO_FORMATS = ["mp3", "webm", "m4a", "wav", "ogg"]; export const VIDEO_FORMATS = ["mp4", "mkv", "avi", "mov"]; export const COLLAPSE_ICON = - ''; + ''; export const TICKS_PER_DAY = 24 * 3600 * 1000; diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 00000000..5b4feaef --- /dev/null +++ b/src/core.ts @@ -0,0 +1,237 @@ +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { SrsAlgorithm } from "src/algorithms/base/srs-algorithm"; +import { IOsrVaultNoteLinkInfoFinder } from "src/algorithms/osr/obsidian-vault-notelink-info-finder"; +import { OsrNoteGraph } from "src/algorithms/osr/osr-note-graph"; +import { DataStoreAlgorithm } from "src/data-store-algorithm/data-store-algorithm"; +import { Deck, DeckTreeFilter } from "src/deck"; +import { DeckTreeStatsCalculator } from "src/deck-tree-stats-calculator"; +import { CardDueDateHistogram, NoteDueDateHistogram } from "src/due-date-histogram"; +import { FlashcardReviewMode } from "src/flashcard-review-sequencer"; +import { Note } from "src/note"; +import { NoteEaseList } from "src/note-ease-list"; +import { NoteFileLoader } from "src/note-file-loader"; +import { NoteReviewQueue } from "src/note-review-queue"; +import { QuestionPostponementList } from "src/question-postponement-list"; +import { SettingsUtil, SRSettings } from "src/settings"; +import { ISRFile } from "src/sr-file"; +import { Stats } from "src/stats"; +import { TopicPath } from "src/topic-path"; +import { globalDateProvider } from "src/utils/dates"; +import { TextDirection } from "src/utils/strings"; + +export interface IOsrVaultEvents { + dataChanged: () => void; +} + +export class OsrCore { + public defaultTextDirection: TextDirection; + protected settings: SRSettings; + private dataChangedHandler: () => void; + protected osrNoteGraph: OsrNoteGraph; + private osrNoteLinkInfoFinder: IOsrVaultNoteLinkInfoFinder; + private _easeByPath: NoteEaseList; + private _questionPostponementList: QuestionPostponementList; + private _noteReviewQueue: NoteReviewQueue; + + private fullDeckTree: Deck; + private _reviewableDeckTree: Deck = new Deck("root", null); + private _remainingDeckTree: Deck; + private _cardStats: Stats; + private _dueDateFlashcardHistogram: CardDueDateHistogram; + private _dueDateNoteHistogram: NoteDueDateHistogram; + + get noteReviewQueue(): NoteReviewQueue { + return this._noteReviewQueue; + } + + get remainingDeckTree(): Deck { + return this._remainingDeckTree; + } + + get reviewableDeckTree(): Deck { + return this._reviewableDeckTree; + } + + get questionPostponementList(): QuestionPostponementList { + return this._questionPostponementList; + } + + /* c8 ignore start */ + get dueDateFlashcardHistogram(): CardDueDateHistogram { + return this._dueDateFlashcardHistogram; + } + + get dueDateNoteHistogram(): NoteDueDateHistogram { + return this._dueDateNoteHistogram; + } + + get easeByPath(): NoteEaseList { + return this._easeByPath; + } + + get cardStats(): Stats { + return this._cardStats; + } + /* c8 ignore stop */ + + init( + questionPostponementList: QuestionPostponementList, + osrNoteLinkInfoFinder: IOsrVaultNoteLinkInfoFinder, + settings: SRSettings, + dataChangedHandler: () => void, + ): void { + this.settings = settings; + this.osrNoteLinkInfoFinder = osrNoteLinkInfoFinder; + this.dataChangedHandler = dataChangedHandler; + this._noteReviewQueue = new NoteReviewQueue(); + this._questionPostponementList = questionPostponementList; + this._dueDateFlashcardHistogram = new CardDueDateHistogram(); + this._dueDateNoteHistogram = new NoteDueDateHistogram(); + } + + protected loadInit(): void { + // reset notes stuff + this.osrNoteGraph = new OsrNoteGraph(this.osrNoteLinkInfoFinder); + this._noteReviewQueue.init(); + + // reset flashcards stuff + this.fullDeckTree = new Deck("root", null); + } + + protected async processFile(noteFile: ISRFile): Promise { + const schedule: RepItemScheduleInfo = + await DataStoreAlgorithm.getInstance().noteGetSchedule(noteFile); + let note: Note = null; + + // Update the graph of links between notes + // (Performance note: This only requires accessing Obsidian's metadata cache and not loading the file) + this.osrNoteGraph.processLinks(noteFile.path); + + // Does the note contain any tags that are specified as flashcard tags in the settings + // (Doing this check first saves us from loading and parsing the note if not necessary) + const topicPath: TopicPath = this.findTopicPath(noteFile); + if (topicPath.hasPath) { + note = await this.loadNote(noteFile, topicPath); + note.appendCardsToDeck(this.fullDeckTree); + } + + // Give the algorithm a chance to do something with the loaded note + // e.g. OSR - calculate the average ease across all the questions within the note + // TODO: should this move to this.loadNote + SrsAlgorithm.getInstance().noteOnLoadedNote(noteFile.path, note, schedule?.latestEase); + + const tags = noteFile.getAllTagsFromCache(); + + const matchedNoteTags = SettingsUtil.filterForNoteReviewTag(this.settings, tags); + if (matchedNoteTags.length == 0) { + return; + } + const noteSchedule: RepItemScheduleInfo = + await DataStoreAlgorithm.getInstance().noteGetSchedule(noteFile); + this._noteReviewQueue.addNoteToQueue(noteFile, noteSchedule, matchedNoteTags); + } + + protected finaliseLoad(): void { + this.osrNoteGraph.generatePageRanks(); + + // Reviewable cards are all except those with the "edit later" tag + this._reviewableDeckTree = DeckTreeFilter.filterForReviewableCards(this.fullDeckTree); + + // sort the deck names + this._reviewableDeckTree.sortSubdecksList(); + this._remainingDeckTree = DeckTreeFilter.filterForRemainingCards( + this._questionPostponementList, + this._reviewableDeckTree, + FlashcardReviewMode.Review, + ); + const calc: DeckTreeStatsCalculator = new DeckTreeStatsCalculator(); + this._cardStats = calc.calculate(this._reviewableDeckTree); + + // Generate the histogram for the due dates for (1) all the notes (2) all the cards + this.calculateDerivedInfo(); + this._dueDateFlashcardHistogram.calculateFromDeckTree(this._reviewableDeckTree); + + // Tell the interested party that the data has changed + if (this.dataChangedHandler) this.dataChangedHandler(); + } + + async saveNoteReviewResponse( + noteFile: ISRFile, + response: ReviewResponse, + settings: SRSettings, + ): Promise { + // Get the current schedule for the note (null if new note) + const originalNoteSchedule: RepItemScheduleInfo = + await DataStoreAlgorithm.getInstance().noteGetSchedule(noteFile); + + // Calculate the new/updated schedule + let noteSchedule: RepItemScheduleInfo; + if (originalNoteSchedule == null) { + noteSchedule = SrsAlgorithm.getInstance().noteCalcNewSchedule( + noteFile.path, + this.osrNoteGraph, + response, + this._dueDateNoteHistogram, + ); + } else { + noteSchedule = SrsAlgorithm.getInstance().noteCalcUpdatedSchedule( + noteFile.path, + originalNoteSchedule, + response, + this._dueDateNoteHistogram, + ); + } + + // Store away the new schedule info + await DataStoreAlgorithm.getInstance().noteSetSchedule(noteFile, noteSchedule); + + // Generate the histogram for the due dates for all the notes + // (This could be optimized to make the small adjustments to the histogram, but simpler to implement + // by recalculating from scratch) + this._noteReviewQueue.updateScheduleInfo(noteFile, noteSchedule); + this.calculateDerivedInfo(); + + // If configured in the settings, bury all cards within the note + await this.buryAllCardsInNote(settings, noteFile); + + // Tell the interested party that the data has changed + if (this.dataChangedHandler) this.dataChangedHandler(); + } + + private calculateDerivedInfo(): void { + const todayUnix: number = globalDateProvider.today.valueOf(); + this.noteReviewQueue.calcDueNotesCount(todayUnix); + this._dueDateNoteHistogram.calculateFromReviewDecksAndSort( + this.noteReviewQueue.reviewDecks, + this.osrNoteGraph, + ); + } + + private async buryAllCardsInNote(settings: SRSettings, noteFile: ISRFile): Promise { + if (settings.burySiblingCards) { + const topicPath: TopicPath = this.findTopicPath(noteFile); + const noteX: Note = await this.loadNote(noteFile, topicPath); + + if (noteX.questionList.length > 0) { + for (const question of noteX.questionList) { + this._questionPostponementList.add(question); + } + await this._questionPostponementList.write(); + } + } + } + + async loadNote(noteFile: ISRFile, topicPath: TopicPath): Promise { + const loader: NoteFileLoader = new NoteFileLoader(this.settings); + const note: Note = await loader.load(noteFile, this.defaultTextDirection, topicPath); + if (note.hasChanged) { + await note.writeNoteFile(this.settings); + } + return note; + } + + private findTopicPath(note: ISRFile): TopicPath { + return TopicPath.getTopicPathOfFile(note, this.settings); + } +} diff --git a/src/data-store-algorithm/data-store-algorithm.ts b/src/data-store-algorithm/data-store-algorithm.ts new file mode 100644 index 00000000..339c7472 --- /dev/null +++ b/src/data-store-algorithm/data-store-algorithm.ts @@ -0,0 +1,12 @@ +import { IDataStoreAlgorithm } from "src/data-store-algorithm/idata-store-algorithm"; + +export class DataStoreAlgorithm { + static instance: IDataStoreAlgorithm; + + public static getInstance(): IDataStoreAlgorithm { + if (!DataStoreAlgorithm.instance) { + throw new Error("there is no DataStoreAlgorithm instance."); + } + return DataStoreAlgorithm.instance; + } +} diff --git a/src/data-store-algorithm/data-store-in-note-algorithm-osr.ts b/src/data-store-algorithm/data-store-in-note-algorithm-osr.ts new file mode 100644 index 00000000..55a505cb --- /dev/null +++ b/src/data-store-algorithm/data-store-in-note-algorithm-osr.ts @@ -0,0 +1,109 @@ +import { Moment } from "moment"; +import moment from "moment"; + +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/rep-item-schedule-info-osr"; +import { Card } from "src/card"; +import { + ALLOWED_DATE_FORMATS, + SCHEDULING_INFO_REGEX, + SR_HTML_COMMENT_BEGIN, + SR_HTML_COMMENT_END, + YAML_FRONT_MATTER_REGEX, +} from "src/constants"; +import { IDataStoreAlgorithm } from "src/data-store-algorithm/idata-store-algorithm"; +import { Question } from "src/question"; +import { SRSettings } from "src/settings"; +import { ISRFile } from "src/sr-file"; +import { formatDate_YYYY_MM_DD } from "src/utils/dates"; + +// +// Algorithm: The original OSR algorithm +// (RZ: Perhaps not the original algorithm, but the only one available in 2023/early 2024) +// +// Data Store: With data stored in the note's markdown file +// +export class DataStoreInNote_AlgorithmOsr implements IDataStoreAlgorithm { + private settings: SRSettings; + + constructor(settings: SRSettings) { + this.settings = settings; + } + + async noteGetSchedule(note: ISRFile): Promise { + let result: RepItemScheduleInfo = null; + const frontmatter: Map = await note.getFrontmatter(); + + if ( + frontmatter && + frontmatter.has("sr-due") && + frontmatter.has("sr-interval") && + frontmatter.has("sr-ease") + ) { + const dueDate: Moment = moment(frontmatter.get("sr-due"), ALLOWED_DATE_FORMATS); + const interval: number = parseFloat(frontmatter.get("sr-interval")); + const ease: number = parseFloat(frontmatter.get("sr-ease")); + result = new RepItemScheduleInfo_Osr(dueDate, interval, ease); + } + return result; + } + + async noteSetSchedule(note: ISRFile, repItemScheduleInfo: RepItemScheduleInfo): Promise { + let fileText: string = await note.read(); + + const schedInfo: RepItemScheduleInfo_Osr = repItemScheduleInfo as RepItemScheduleInfo_Osr; + const dueString: string = formatDate_YYYY_MM_DD(schedInfo.dueDate); + const interval: number = schedInfo.interval; + const ease: number = schedInfo.latestEase; + + // check if scheduling info exists + if (SCHEDULING_INFO_REGEX.test(fileText)) { + const schedulingInfo = SCHEDULING_INFO_REGEX.exec(fileText); + fileText = fileText.replace( + SCHEDULING_INFO_REGEX, + `---\n${schedulingInfo[1]}sr-due: ${dueString}\n` + + `sr-interval: ${interval}\nsr-ease: ${ease}\n` + + `${schedulingInfo[5]}---`, + ); + } else if (YAML_FRONT_MATTER_REGEX.test(fileText)) { + // new note with existing YAML front matter + const existingYaml = YAML_FRONT_MATTER_REGEX.exec(fileText); + fileText = fileText.replace( + YAML_FRONT_MATTER_REGEX, + `---\n${existingYaml[1]}sr-due: ${dueString}\n` + + `sr-interval: ${interval}\nsr-ease: ${ease}\n---`, + ); + } else { + fileText = + `---\nsr-due: ${dueString}\nsr-interval: ${interval}\n` + + `sr-ease: ${ease}\n---\n\n${fileText}`; + } + + await note.write(fileText); + } + + questionFormatScheduleAsHtmlComment(question: Question): string { + let result: string = SR_HTML_COMMENT_BEGIN; + + for (let i = 0; i < question.cards.length; i++) { + const card: Card = question.cards[i]; + result += this.formatCardSchedule(card); + } + result += SR_HTML_COMMENT_END; + return result; + } + + formatCardSchedule(card: Card) { + let result: string; + if (card.hasSchedule) { + const schedule = card.scheduleInfo as RepItemScheduleInfo_Osr; + const dateStr = schedule.dueDate + ? formatDate_YYYY_MM_DD(schedule.dueDate) + : RepItemScheduleInfo_Osr.dummyDueDateForNewCard; + result = `!${dateStr},${schedule.interval},${schedule.latestEase}`; + } else { + result = `!${RepItemScheduleInfo_Osr.dummyDueDateForNewCard},${RepItemScheduleInfo_Osr.initialInterval},${this.settings.baseEase}`; + } + return result; + } +} diff --git a/src/data-store-algorithm/idata-store-algorithm.ts b/src/data-store-algorithm/idata-store-algorithm.ts new file mode 100644 index 00000000..9fe6c1b6 --- /dev/null +++ b/src/data-store-algorithm/idata-store-algorithm.ts @@ -0,0 +1,9 @@ +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { Question } from "src/question"; +import { ISRFile } from "src/sr-file"; + +export interface IDataStoreAlgorithm { + noteGetSchedule(note: ISRFile): Promise; + noteSetSchedule(note: ISRFile, scheduleInfo: RepItemScheduleInfo): Promise; + questionFormatScheduleAsHtmlComment(question: Question): string; +} diff --git a/src/data-stores/base/data-store.ts b/src/data-stores/base/data-store.ts new file mode 100644 index 00000000..9fb5c7c5 --- /dev/null +++ b/src/data-stores/base/data-store.ts @@ -0,0 +1,24 @@ +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { RepItemStorageInfo } from "src/data-stores/base/rep-item-storage-info"; +import { Question } from "src/question"; + +export interface IDataStore { + questionCreateSchedule( + originalQuestionText: string, + storageInfo: RepItemStorageInfo, + ): RepItemScheduleInfo[]; + questionRemoveScheduleInfo(questionText: string): string; + questionWrite(question: Question): Promise; + questionWriteSchedule(question: Question): Promise; +} + +export class DataStore { + static instance: IDataStore; + + public static getInstance(): IDataStore { + if (!DataStore.instance) { + throw new Error("there is no DataStore instance."); + } + return DataStore.instance; + } +} diff --git a/src/data-stores/base/rep-item-storage-info.ts b/src/data-stores/base/rep-item-storage-info.ts new file mode 100644 index 00000000..b8e53b42 --- /dev/null +++ b/src/data-stores/base/rep-item-storage-info.ts @@ -0,0 +1 @@ +export class RepItemStorageInfo {} diff --git a/src/data-stores/store-in-note/note.ts b/src/data-stores/store-in-note/note.ts new file mode 100644 index 00000000..cfc99896 --- /dev/null +++ b/src/data-stores/store-in-note/note.ts @@ -0,0 +1,72 @@ +import { Moment } from "moment"; +import { App } from "obsidian"; + +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/rep-item-schedule-info-osr"; +import { LEGACY_SCHEDULING_EXTRACTOR, MULTI_SCHEDULING_EXTRACTOR } from "src/constants"; +import { IDataStore } from "src/data-stores/base/data-store"; +import { RepItemStorageInfo } from "src/data-stores/base/rep-item-storage-info"; +import { NoteEaseList } from "src/note-ease-list"; +import { Question } from "src/question"; +import { SRSettings } from "src/settings"; +import { DateUtil, formatDate_YYYY_MM_DD, globalDateProvider } from "src/utils/dates"; + +export class StoreInNote implements IDataStore { + private settings: SRSettings; + app: App; + easeByPath: NoteEaseList; + + constructor(settings: SRSettings) { + this.settings = settings; + } + + questionCreateSchedule( + originalQuestionText: string, + _: RepItemStorageInfo, + ): RepItemScheduleInfo[] { + let scheduling: RegExpMatchArray[] = [ + ...originalQuestionText.matchAll(MULTI_SCHEDULING_EXTRACTOR), + ]; + if (scheduling.length === 0) + scheduling = [...originalQuestionText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)]; + + const result: RepItemScheduleInfo[] = []; + for (let i = 0; i < scheduling.length; i++) { + const match: RegExpMatchArray = scheduling[i]; + const dueDateStr = match[1]; + const interval = parseInt(match[2]); + const ease = parseInt(match[3]); + const dueDate: Moment = DateUtil.dateStrToMoment(dueDateStr); + let info: RepItemScheduleInfo; + if ( + dueDate == null || + formatDate_YYYY_MM_DD(dueDate) == RepItemScheduleInfo_Osr.dummyDueDateForNewCard + ) { + info = null; + } else { + const delayBeforeReviewTicks: number = + dueDate.valueOf() - globalDateProvider.today.valueOf(); + + info = new RepItemScheduleInfo_Osr(dueDate, interval, ease, delayBeforeReviewTicks); + } + result.push(info); + } + return result; + } + + questionRemoveScheduleInfo(questionText: string): string { + return questionText.replace(//gm, ""); + } + + async questionWriteSchedule(question: Question): Promise { + await this.questionWrite(question); + } + + async questionWrite(question: Question): Promise { + const fileText: string = await question.note.file.read(); + + const newText: string = question.updateQuestionWithinNoteText(fileText, this.settings); + await question.note.file.write(newText); + question.hasChanged = false; + } +} diff --git a/src/DeckTreeIterator.ts b/src/deck-tree-iterator.ts similarity index 98% rename from src/DeckTreeIterator.ts rename to src/deck-tree-iterator.ts index 1c5d0ae4..b4c08225 100644 --- a/src/DeckTreeIterator.ts +++ b/src/deck-tree-iterator.ts @@ -1,8 +1,8 @@ -import { Card } from "./Card"; -import { CardListType, Deck } from "./Deck"; -import { Question } from "./Question"; -import { TopicPath } from "./TopicPath"; -import { WeightedRandomNumber, globalRandomNumberProvider } from "./util/RandomNumberProvider"; +import { Card } from "src/card"; +import { CardListType, Deck } from "src/deck"; +import { Question } from "src/question"; +import { TopicPath } from "src/topic-path"; +import { globalRandomNumberProvider, WeightedRandomNumber } from "src/utils/numbers"; export enum CardOrder { NewFirstSequential, diff --git a/src/DeckTreeStatsCalculator.ts b/src/deck-tree-stats-calculator.ts similarity index 64% rename from src/DeckTreeStatsCalculator.ts rename to src/deck-tree-stats-calculator.ts index 2375cd71..55a1c797 100644 --- a/src/DeckTreeStatsCalculator.ts +++ b/src/deck-tree-stats-calculator.ts @@ -1,19 +1,17 @@ -import { Deck } from "./Deck"; +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { Card } from "src/card"; +import { Deck } from "src/deck"; import { CardOrder, DeckOrder, DeckTreeIterator, IDeckTreeIterator, IIteratorOrder, -} from "./DeckTreeIterator"; -import { Card } from "./Card"; -import { Stats } from "./stats"; -import { CardScheduleInfo } from "./CardSchedule"; -import { TopicPath } from "./TopicPath"; +} from "src/deck-tree-iterator"; +import { Stats } from "src/stats"; +import { TopicPath } from "src/topic-path"; export class DeckTreeStatsCalculator { - private deckTree: Deck; - calculate(deckTree: Deck): Stats { // Order doesn't matter as long as we iterate over everything const iteratorOrder: IIteratorOrder = { @@ -27,8 +25,12 @@ export class DeckTreeStatsCalculator { while (iterator.nextCard()) { const card: Card = iterator.currentCard; if (card.hasSchedule) { - const schedule: CardScheduleInfo = card.scheduleInfo; - result.update(schedule.delayBeforeReviewDaysInt, schedule.interval, schedule.ease); + const schedule: RepItemScheduleInfo = card.scheduleInfo; + result.update( + schedule.delayedBeforeReviewDaysInt(), + schedule.interval, + schedule.latestEase, + ); } else { result.incrementNew(); } diff --git a/src/Deck.ts b/src/deck.ts similarity index 97% rename from src/Deck.ts rename to src/deck.ts index 70d0935d..0d4f2357 100644 --- a/src/Deck.ts +++ b/src/deck.ts @@ -1,8 +1,8 @@ -import { Card } from "./Card"; -import { FlashcardReviewMode } from "./FlashcardReviewSequencer"; -import { Question } from "./Question"; -import { IQuestionPostponementList } from "./QuestionPostponementList"; -import { TopicPath, TopicPathList } from "./TopicPath"; +import { Card } from "src/card"; +import { FlashcardReviewMode } from "src/flashcard-review-sequencer"; +import { Question } from "src/question"; +import { IQuestionPostponementList } from "src/question-postponement-list"; +import { TopicPath, TopicPathList } from "src/topic-path"; export enum CardListType { NewCard, diff --git a/src/due-date-histogram.ts b/src/due-date-histogram.ts new file mode 100644 index 00000000..977a0d2d --- /dev/null +++ b/src/due-date-histogram.ts @@ -0,0 +1,137 @@ +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { OsrNoteGraph } from "src/algorithms/osr/osr-note-graph"; +import { Card } from "src/card"; +import { TICKS_PER_DAY } from "src/constants"; +import { Deck } from "src/deck"; +import { + CardOrder, + DeckOrder, + DeckTreeIterator, + IDeckTreeIterator, + IIteratorOrder, +} from "src/deck-tree-iterator"; +import { NoteReviewDeck, SchedNote } from "src/note-review-deck"; +import { TopicPath } from "src/topic-path"; +import { globalDateProvider } from "src/utils/dates"; + +export class DueDateHistogram { + // The key for dueDatesNotes is the number of days after today + // therefore the key to lookup how many cards are due today is 0 + public static dueNowNDays: number = 0; + + // Key - # of days in future + // Value - Count of notes due + dueDatesMap: Map = new Map(); + + constructor(rec: Record = null) { + this.dueDatesMap = new Map(); + if (rec != null) { + Object.entries(rec).forEach(([key, value]) => { + this.dueDatesMap.set(Number(key), value); + }); + } + } + + get dueNotesCount(): number { + let result: number = 0; + if (this.dueDatesMap.has(DueDateHistogram.dueNowNDays)) + result = this.dueDatesMap.get(DueDateHistogram.dueNowNDays); + return result; + } + + hasEntryForDays(days: number): boolean { + return this.dueDatesMap.has(days); + } + + set(days: number, value: number): void { + this.dueDatesMap.set(days, value); + } + + get(days: number): number { + return this.dueDatesMap.get(days); + } + + increment(days: number): void { + let value: number = 0; + if (this.dueDatesMap.has(days)) { + value = this.dueDatesMap.get(days); + } + this.dueDatesMap.set(days, value + 1); + } + + decrement(days: number): void { + let value: number = 0; + if (this.dueDatesMap.has(days)) value = this.dueDatesMap.get(days); + if (value > 0) { + this.dueDatesMap.set(days, value - 1); + } + } + + findLeastUsedIntervalOverRange(originalInterval: number, fuzz: number): number { + if (!this.hasEntryForDays(originalInterval)) { + // There are no entries for the interval originalInterval - can't get a better result + return originalInterval; + } + let interval: number = originalInterval; + outer: for (let i = 1; i <= fuzz; i++) { + for (const ivl of [originalInterval - i, originalInterval + i]) { + if (!this.hasEntryForDays(ivl)) { + // There are no entries for the interval ivl - can't get a better result + interval = ivl; + break outer; + } + + // We've found a better result, but keep searching + if (this.dueDatesMap.get(ivl) < this.dueDatesMap.get(interval)) interval = ivl; + } + } + return interval; + } +} + +export class NoteDueDateHistogram extends DueDateHistogram { + calculateFromReviewDecksAndSort( + reviewDecks: Map, + osrNoteGraph: OsrNoteGraph, + ): void { + this.dueDatesMap = new Map(); + + const today: number = globalDateProvider.today.valueOf(); + reviewDecks.forEach((reviewDeck: NoteReviewDeck) => { + reviewDeck.scheduledNotes.forEach((scheduledNote: SchedNote) => { + const nDays: number = Math.ceil((scheduledNote.dueUnix - today) / TICKS_PER_DAY); + this.increment(nDays); + }); + + reviewDeck.sortNotesByDateAndImportance(osrNoteGraph.pageranks); + }); + } +} + +export class CardDueDateHistogram extends DueDateHistogram { + calculateFromDeckTree(deckTree: Deck): void { + this.dueDatesMap = new Map(); + + // Order doesn't matter as long as we iterate over everything + const iteratorOrder: IIteratorOrder = { + deckOrder: DeckOrder.PrevDeckComplete_Sequential, + cardOrder: CardOrder.DueFirstSequential, + }; + + // Iteration is a destructive operation on the supplied tree, so we first take a copy + const today: number = globalDateProvider.today.valueOf(); + const iterator: IDeckTreeIterator = new DeckTreeIterator(iteratorOrder, deckTree.clone()); + iterator.setIteratorTopicPath(TopicPath.emptyPath); + while (iterator.nextCard()) { + const card: Card = iterator.currentCard; + if (card.hasSchedule) { + const scheduledCard: RepItemScheduleInfo = card.scheduleInfo; + + const nDays: number = Math.ceil( + (scheduledCard.dueDateAsUnix - today) / TICKS_PER_DAY, + ); + this.increment(nDays); + } + } + } +} diff --git a/src/FlashcardReviewSequencer.ts b/src/flashcard-review-sequencer.ts similarity index 80% rename from src/FlashcardReviewSequencer.ts rename to src/flashcard-review-sequencer.ts index 5ef74668..3162c9ea 100644 --- a/src/FlashcardReviewSequencer.ts +++ b/src/flashcard-review-sequencer.ts @@ -1,13 +1,16 @@ -import { Card } from "./Card"; -import { CardListType, Deck } from "./Deck"; -import { Question, QuestionText } from "./Question"; -import { ReviewResponse } from "./scheduling"; -import { SRSettings } from "./settings"; -import { TopicPath } from "./TopicPath"; -import { CardScheduleInfo, ICardScheduleCalculator } from "./CardSchedule"; -import { Note } from "./Note"; -import { IDeckTreeIterator } from "./DeckTreeIterator"; -import { IQuestionPostponementList } from "./QuestionPostponementList"; +import { ISrsAlgorithm } from "src/algorithms/base/isrs-algorithm"; +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { Card } from "src/card"; +import { DataStore } from "src/data-stores/base/data-store"; +import { CardListType, Deck } from "src/deck"; +import { IDeckTreeIterator } from "src/deck-tree-iterator"; +import { DueDateHistogram } from "src/due-date-histogram"; +import { Note } from "src/note"; +import { Question, QuestionText } from "src/question"; +import { IQuestionPostponementList } from "src/question-postponement-list"; +import { SRSettings } from "src/settings"; +import { TopicPath } from "src/topic-path"; export interface IFlashcardReviewSequencer { get hasCurrentCard(): boolean; @@ -21,7 +24,7 @@ export interface IFlashcardReviewSequencer { setCurrentDeck(topicPath: TopicPath): void; getDeckStats(topicPath: TopicPath): DeckStats; skipCurrentCard(): void; - determineCardSchedule(response: ReviewResponse, card: Card): CardScheduleInfo; + determineCardSchedule(response: ReviewResponse, card: Card): RepItemScheduleInfo; processReview(response: ReviewResponse): Promise; updateCurrentQuestionText(text: string): Promise; } @@ -53,21 +56,24 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer { private reviewMode: FlashcardReviewMode; private cardSequencer: IDeckTreeIterator; private settings: SRSettings; - private cardScheduleCalculator: ICardScheduleCalculator; + private srsAlgorithm: ISrsAlgorithm; private questionPostponementList: IQuestionPostponementList; + private dueDateFlashcardHistogram: DueDateHistogram; constructor( reviewMode: FlashcardReviewMode, cardSequencer: IDeckTreeIterator, settings: SRSettings, - cardScheduleCalculator: ICardScheduleCalculator, + srsAlgorithm: ISrsAlgorithm, questionPostponementList: IQuestionPostponementList, + dueDateFlashcardHistogram: DueDateHistogram, ) { this.reviewMode = reviewMode; this.cardSequencer = cardSequencer; this.settings = settings; - this.cardScheduleCalculator = cardScheduleCalculator; + this.srsAlgorithm = srsAlgorithm; this.questionPostponementList = questionPostponementList; + this.dueDateFlashcardHistogram = dueDateFlashcardHistogram; } get hasCurrentCard(): boolean { @@ -147,7 +153,7 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer { this.currentCard.scheduleInfo = this.determineCardSchedule(response, this.currentCard); // Update the source file with the updated schedule - await this.currentQuestion.writeQuestion(this.settings); + await DataStore.getInstance().questionWriteSchedule(this.currentQuestion); } // Move/delete the card @@ -183,24 +189,26 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer { } } - determineCardSchedule(response: ReviewResponse, card: Card): CardScheduleInfo { - let result: CardScheduleInfo; + determineCardSchedule(response: ReviewResponse, card: Card): RepItemScheduleInfo { + let result: RepItemScheduleInfo; if (response == ReviewResponse.Reset) { // Resetting the card schedule - result = this.cardScheduleCalculator.getResetCardSchedule(); + result = this.srsAlgorithm.cardGetResetSchedule(); } else { // scheduled card if (card.hasSchedule) { - result = this.cardScheduleCalculator.calcUpdatedSchedule( + result = this.srsAlgorithm.cardCalcUpdatedSchedule( response, card.scheduleInfo, + this.dueDateFlashcardHistogram, ); } else { const currentNote: Note = card.question.note; - result = this.cardScheduleCalculator.getNewCardSchedule( + result = this.srsAlgorithm.cardGetNewSchedule( response, currentNote.filePath, + this.dueDateFlashcardHistogram, ); } } @@ -212,6 +220,6 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer { q.actualQuestion = text; - await this.currentQuestion.writeQuestion(this.settings); + await DataStore.getInstance().questionWrite(this.currentQuestion); } } diff --git a/src/gui/DeckListView.tsx b/src/gui/deck-list-view.tsx similarity index 97% rename from src/gui/DeckListView.tsx rename to src/gui/deck-list-view.tsx index d1bf0b1f..6db5971e 100644 --- a/src/gui/DeckListView.tsx +++ b/src/gui/deck-list-view.tsx @@ -1,17 +1,17 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars import h from "vhtml"; -import type SRPlugin from "src/main"; -import { SRSettings } from "src/settings"; import { COLLAPSE_ICON } from "src/constants"; -import { t } from "src/lang/helpers"; -import { Deck } from "../Deck"; +import { Deck } from "src/deck"; import { DeckStats, IFlashcardReviewSequencer as IFlashcardReviewSequencer, -} from "src/FlashcardReviewSequencer"; -import { TopicPath } from "src/TopicPath"; -import { FlashcardModalMode } from "./FlashcardModal"; +} from "src/flashcard-review-sequencer"; +import { FlashcardModalMode } from "src/gui/flashcard-modal"; +import { t } from "src/lang/helpers"; +import type SRPlugin from "src/main"; +import { SRSettings } from "src/settings"; +import { TopicPath } from "src/topic-path"; export class DeckListView { public plugin: SRPlugin; diff --git a/src/gui/EditModal.tsx b/src/gui/edit-modal.tsx similarity index 98% rename from src/gui/EditModal.tsx rename to src/gui/edit-modal.tsx index 35c9a7cd..fd5facff 100644 --- a/src/gui/EditModal.tsx +++ b/src/gui/edit-modal.tsx @@ -1,6 +1,7 @@ import { App, Modal } from "obsidian"; + import { t } from "src/lang/helpers"; -import { TextDirection } from "src/util/TextDirection"; +import { TextDirection } from "src/utils/strings"; // from https://github.com/chhoumann/quickadd/blob/bce0b4cdac44b867854d6233796e3406dfd163c6/src/gui/GenericInputPrompt/GenericInputPrompt.ts#L5 export class FlashcardEditModal extends Modal { diff --git a/src/gui/FlashcardModal.tsx b/src/gui/flashcard-modal.tsx similarity index 87% rename from src/gui/FlashcardModal.tsx rename to src/gui/flashcard-modal.tsx index ddadb7d3..447b17a2 100644 --- a/src/gui/FlashcardModal.tsx +++ b/src/gui/flashcard-modal.tsx @@ -1,19 +1,16 @@ -import { Modal, App } from "obsidian"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import h from "vhtml"; +import { App, Modal } from "obsidian"; -import type SRPlugin from "src/main"; -import { SRSettings } from "src/settings"; - -import { Deck } from "../Deck"; -import { Question } from "../Question"; +import { Deck } from "src/deck"; import { FlashcardReviewMode, IFlashcardReviewSequencer as IFlashcardReviewSequencer, -} from "src/FlashcardReviewSequencer"; -import { FlashcardEditModal } from "./EditModal"; -import { DeckListView } from "./DeckListView"; -import { FlashcardReviewView } from "./FlashcardReviewView"; +} from "src/flashcard-review-sequencer"; +import { DeckListView } from "src/gui/deck-list-view"; +import { FlashcardEditModal } from "src/gui/edit-modal"; +import { FlashcardReviewView } from "src/gui/flashcard-review-view"; +import type SRPlugin from "src/main"; +import { Question } from "src/question"; +import { SRSettings } from "src/settings"; export enum FlashcardModalMode { DecksList, @@ -82,9 +79,9 @@ export class FlashcardModal extends Modal { } onClose(): void { + this.mode = FlashcardModalMode.Closed; this.deckView.close(); this.flashcardView.close(); - this.mode = FlashcardModalMode.Closed; } private _showDecksList(): void { @@ -96,9 +93,9 @@ export class FlashcardModal extends Modal { this.deckView.hide(); } - private _showFlashcard(): void { + private _showFlashcard(deck: Deck): void { this._hideDecksList(); - this.flashcardView.show(); + this.flashcardView.show(deck); } private _hideFlashcard(): void { @@ -108,7 +105,7 @@ export class FlashcardModal extends Modal { private _startReviewOfDeck(deck: Deck) { this.reviewSequencer.setCurrentDeck(deck.getTopicPath()); if (this.reviewSequencer.hasCurrentCard) { - this._showFlashcard(); + this._showFlashcard(deck); } else { this._showDecksList(); } diff --git a/src/gui/FlashcardReviewView.tsx b/src/gui/flashcard-review-view.tsx similarity index 84% rename from src/gui/FlashcardReviewView.tsx rename to src/gui/flashcard-review-view.tsx index 16054442..a4f97de6 100644 --- a/src/gui/FlashcardReviewView.tsx +++ b/src/gui/flashcard-review-view.tsx @@ -1,22 +1,21 @@ import { App, Notice, Platform, setIcon } from "obsidian"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type SRPlugin from "src/main"; -import { SRSettings } from "src/settings"; -import { textInterval, ReviewResponse } from "src/scheduling"; -import { t } from "src/lang/helpers"; -import { Card } from "../Card"; -import { CardListType, Deck } from "../Deck"; -import { CardType, Question } from "../Question"; +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { textInterval } from "src/algorithms/osr/note-scheduling"; +import { Card } from "src/card"; +import { CardListType, Deck } from "src/deck"; import { FlashcardReviewMode, IFlashcardReviewSequencer as IFlashcardReviewSequencer, -} from "src/FlashcardReviewSequencer"; -import { Note } from "src/Note"; -import { RenderMarkdownWrapper } from "src/util/RenderMarkdownWrapper"; -import { CardScheduleInfo } from "src/CardSchedule"; -import { FlashcardModalMode } from "./FlashcardModal"; -import { now } from "moment"; +} from "src/flashcard-review-sequencer"; +import { FlashcardModalMode } from "src/gui/flashcard-modal"; +import { t } from "src/lang/helpers"; +import type SRPlugin from "src/main"; +import { Note } from "src/note"; +import { CardType, Question } from "src/question"; +import { SRSettings } from "src/settings"; +import { RenderMarkdownWrapper } from "src/utils/renderers"; export class FlashcardReviewView { public app: App; @@ -28,7 +27,9 @@ export class FlashcardReviewView { public view: HTMLDivElement; public header: HTMLDivElement; + public titleWrapper: HTMLDivElement; public title: HTMLDivElement; + public subTitle: HTMLDivElement; public backButton: HTMLDivElement; public controls: HTMLDivElement; @@ -45,8 +46,8 @@ export class FlashcardReviewView { public goodButton: HTMLButtonElement; public easyButton: HTMLButtonElement; public answerButton: HTMLButtonElement; - public lastPressed: number; + private chosenDeck: Deck | null; private reviewSequencer: IFlashcardReviewSequencer; private settings: SRSettings; private reviewMode: FlashcardReviewMode; @@ -74,6 +75,7 @@ export class FlashcardReviewView { this.editClickHandler = editClickHandler; this.modalContentEl = contentEl; this.modalEl = modalEl; + this.chosenDeck = null; // Build ui this.init(); @@ -83,17 +85,23 @@ export class FlashcardReviewView { * Initializes all static elements in the FlashcardView */ init() { + this._createBackButton(); + this.view = this.modalContentEl.createDiv(); this.view.addClasses(["sr-flashcard", "sr-is-hidden"]); this.header = this.view.createDiv(); this.header.addClass("sr-header"); - this._createBackButton(); + this.titleWrapper = this.header.createDiv(); + this.titleWrapper.addClass("sr-title-wrapper"); - this.title = this.header.createDiv(); + this.title = this.titleWrapper.createDiv(); this.title.addClass("sr-title"); + this.subTitle = this.titleWrapper.createDiv(); + this.subTitle.addClasses(["sr-sub-title", "sr-is-hidden"]); + this.controls = this.header.createDiv(); this.controls.addClass("sr-controls"); @@ -114,14 +122,60 @@ export class FlashcardReviewView { } /** - * Shows the FlashcardView & rerenders all dynamic elements + * Shows the FlashcardView if it is hidden + */ + async show(chosenDeck: Deck) { + if (!this.view.hasClass("sr-is-hidden")) { + return; + } + this.chosenDeck = chosenDeck; + + await this._drawContent(); + + // Prevents the following code, from running if this show is just a redraw and not an unhide + this.view.removeClass("sr-is-hidden"); + this.backButton.removeClass("sr-is-hidden"); + document.addEventListener("keydown", this._keydownHandler); + } + + /** + * Refreshes all dynamic elements + */ + async refresh() { + await this._drawContent(); + } + + /** + * Hides the FlashcardView if it is visible */ - async show() { + hide() { + // Prevents the following code, from running if this was executed multiple times after one another + if (this.view.hasClass("sr-is-hidden")) { + return; + } + + document.removeEventListener("keydown", this._keydownHandler); + this.view.addClass("sr-is-hidden"); + this.backButton.addClass("sr-is-hidden"); + } + + /** + * Closes the FlashcardView + */ + close() { + this.hide(); + document.removeEventListener("keydown", this._keydownHandler); + } + + // #region -> Functions & helpers + + private async _drawContent() { this.mode = FlashcardModalMode.Front; - const deck: Deck = this.reviewSequencer.currentDeck; + const currentDeck: Deck = this.reviewSequencer.currentDeck; // Setup title - this._setTitle(deck); + this._setTitle(this.chosenDeck); + this._setSubTitle(this.chosenDeck, currentDeck); this.resetButton.disabled = true; // Setup context @@ -148,39 +202,8 @@ export class FlashcardReviewView { // Setup response buttons this._resetResponseButtons(); - - // Prevents the following code, from running if this show is just a redraw and not an unhide - if (!this.view.hasClass("sr-is-hidden")) { - return; - } - this.view.removeClass("sr-is-hidden"); - this.backButton.removeClass("sr-is-hidden"); - document.addEventListener("keydown", this._keydownHandler); - } - - /** - * Hides the FlashcardView - */ - hide() { - // Prevents the following code, from running if this was executed multiple times after one another - if (this.view.hasClass("sr-is-hidden")) { - return; - } - this.view.addClass("sr-is-hidden"); - this.backButton.addClass("sr-is-hidden"); - document.removeEventListener("keydown", this._keydownHandler); - } - - /** - * Closes the FlashcardView - */ - close() { - document.removeEventListener("keydown", this._keydownHandler); - this.hide(); } - // -> Functions & helpers - private _keydownHandler = (e: KeyboardEvent) => { // Prevents any input, if the edit modal is open if ( @@ -257,7 +280,7 @@ export class FlashcardReviewView { private _displayCurrentCardInfoNotice() { const schedule = this._currentCard.scheduleInfo; - const currentEaseStr = t("CURRENT_EASE_HELP_TEXT") + (schedule?.ease ?? t("NEW")); + const currentEaseStr = t("CURRENT_EASE_HELP_TEXT") + (schedule?.latestEase ?? t("NEW")); const currentIntervalStr = t("CURRENT_INTERVAL_HELP_TEXT") + textInterval(schedule?.interval, false); const generatedFromStr = t("CARD_GENERATED_FROM", { @@ -280,15 +303,6 @@ export class FlashcardReviewView { } private _showAnswer(): void { - const timeNow = now(); - if ( - this.lastPressed && - timeNow - this.lastPressed < this.plugin.data.settings.reviewButtonDelay - ) { - return; - } - this.lastPressed = timeNow; - this.mode = FlashcardModalMode.Back; this.resetButton.disabled = false; @@ -343,15 +357,6 @@ export class FlashcardReviewView { } private async _processReview(response: ReviewResponse): Promise { - const timeNow = now(); - if ( - this.lastPressed && - timeNow - this.lastPressed < this.plugin.data.settings.reviewButtonDelay - ) { - return; - } - this.lastPressed = timeNow; - await this.reviewSequencer.processReview(response); await this._handleSkipCard(); } @@ -362,7 +367,7 @@ export class FlashcardReviewView { } private async _handleSkipCard(): Promise { - if (this._currentCard != null) await this.show(); + if (this._currentCard != null) await this.refresh(); else this.backClickHandler(); } @@ -380,7 +385,7 @@ export class FlashcardReviewView { } result += separator + context; }); - return result + separator + "..."; + return result; } // -> Header @@ -391,13 +396,46 @@ export class FlashcardReviewView { setIcon(this.backButton, "arrow-left"); this.backButton.setAttribute("aria-label", t("BACK")); this.backButton.addEventListener("click", () => { - /* this.plugin.data.historyDeck = ""; */ this.backClickHandler(); }); } private _setTitle(deck: Deck) { - this.title.setText(`${deck.deckName}: ${deck.getCardCount(CardListType.All, true)}`); + let text = deck.deckName; + + const deckStats = this.reviewSequencer.getDeckStats(deck.getTopicPath()); + const cardsInQueue = deckStats.dueCount + deckStats.newCount; + text += `: ${cardsInQueue}`; + + this.title.setText(text); + } + + private _setSubTitle(chosenDeck: Deck, currentDeck: Deck) { + if (chosenDeck.subdecks.length === 0) { + if (!this.subTitle.hasClass("sr-is-hidden")) { + this.subTitle.addClass("sr-is-hidden"); + } + return; + } + + if (this.subTitle.hasClass("sr-is-hidden")) { + this.subTitle.removeClass("sr-is-hidden"); + } + + let text = `${currentDeck.deckName}`; + + const isRandomMode = this.settings.flashcardCardOrder === "EveryCardRandomDeckAndCard"; + if (!isRandomMode) { + const subDecksWithCardsInQueue = chosenDeck.subdecks.filter((subDeck) => { + const deckStats = this.reviewSequencer.getDeckStats(subDeck.getTopicPath()); + return deckStats.dueCount + deckStats.newCount > 0; + }); + + text = `${t("DECKS")}: ${subDecksWithCardsInQueue.length} | ${text}`; + text += `: ${currentDeck.getCardCount(CardListType.All, false)}`; + } + + this.subTitle.setText(text); } // -> Controls @@ -522,7 +560,7 @@ export class FlashcardReviewView { buttonName: string, reviewResponse: ReviewResponse, ) { - const schedule: CardScheduleInfo = this.reviewSequencer.determineCardSchedule( + const schedule: RepItemScheduleInfo = this.reviewSequencer.determineCardSchedule( reviewResponse, this._currentCard, ); diff --git a/src/gui/review-deck-selection-modal.tsx b/src/gui/review-deck-selection-modal.tsx new file mode 100644 index 00000000..86d88f64 --- /dev/null +++ b/src/gui/review-deck-selection-modal.tsx @@ -0,0 +1,24 @@ +import { App, FuzzySuggestModal } from "obsidian"; + +export class ReviewDeckSelectionModal extends FuzzySuggestModal { + public deckKeys: string[] = []; + public submitCallback: (deckKey: string) => void; + + constructor(app: App, deckKeys: string[]) { + super(app); + this.deckKeys = deckKeys; + } + + getItems(): string[] { + return this.deckKeys; + } + + getItemText(item: string): string { + return item; + } + + onChooseItem(deckKey: string, _: MouseEvent | KeyboardEvent): void { + this.close(); + this.submitCallback(deckKey); + } +} diff --git a/src/gui/Sidebar.tsx b/src/gui/review-queue-list-view.tsx similarity index 65% rename from src/gui/Sidebar.tsx rename to src/gui/review-queue-list-view.tsx index 9e878844..a605b763 100644 --- a/src/gui/Sidebar.tsx +++ b/src/gui/review-queue-list-view.tsx @@ -1,19 +1,31 @@ -import { ItemView, WorkspaceLeaf, Menu, TFile } from "obsidian"; +import { App, ItemView, Menu, TFile, WorkspaceLeaf } from "obsidian"; -import type SRPlugin from "src/main"; -import { COLLAPSE_ICON } from "src/constants"; -import { ReviewDeck } from "src/ReviewDeck"; +import { COLLAPSE_ICON, TICKS_PER_DAY } from "src/constants"; import { t } from "src/lang/helpers"; +import { NextNoteReviewHandler } from "src/next-note-review-handler"; +import { NoteReviewDeck } from "src/note-review-deck"; +import { NoteReviewQueue } from "src/note-review-queue"; +import { SRSettings } from "src/settings"; export const REVIEW_QUEUE_VIEW_TYPE = "review-queue-list-view"; export class ReviewQueueListView extends ItemView { - private plugin: SRPlugin; - - constructor(leaf: WorkspaceLeaf, plugin: SRPlugin) { + private get noteReviewQueue(): NoteReviewQueue { + return this.nextNoteReviewHandler.noteReviewQueue; + } + private settings: SRSettings; + private nextNoteReviewHandler: NextNoteReviewHandler; + + constructor( + leaf: WorkspaceLeaf, + app: App, + nextNoteReviewHandler: NextNoteReviewHandler, + settings: SRSettings, + ) { super(leaf); - this.plugin = plugin; + this.nextNoteReviewHandler = nextNoteReviewHandler; + this.settings = settings; this.registerEvent(this.app.workspace.on("file-open", () => this.redraw())); this.registerEvent(this.app.vault.on("rename", () => this.redraw())); } @@ -43,12 +55,10 @@ export class ReviewQueueListView extends ItemView { public redraw(): void { const activeFile: TFile | null = this.app.workspace.getActiveFile(); - const rootEl: HTMLElement = createDiv(); - const childrenEl: HTMLElement = rootEl; - - for (const deckKey in this.plugin.reviewDecks) { - const deck: ReviewDeck = this.plugin.reviewDecks[deckKey]; + const rootEl: HTMLElement = createDiv("tree-item nav-folder mod-root"); + const childrenEl: HTMLElement = rootEl.createDiv("tree-item-children nav-folder-children"); + for (const [deckKey, deck] of this.noteReviewQueue.reviewDecks) { const deckCollapsed = !deck.activeFolders.has(deck.deckName); const deckFolderEl: HTMLElement = this.createRightPaneFolder( @@ -57,7 +67,7 @@ export class ReviewQueueListView extends ItemView { deckCollapsed, false, deck, - ).getElementsByClassName("tree-item-children")[0] as HTMLElement; + ).getElementsByClassName("tree-item-children nav-folder-children")[0] as HTMLElement; if (deck.newNotes.length > 0) { const newNotesFolderEl: HTMLElement = this.createRightPaneFolder( @@ -73,16 +83,15 @@ export class ReviewQueueListView extends ItemView { if (fileIsOpen) { deck.activeFolders.add(deck.deckName); deck.activeFolders.add(t("NEW")); - this.changeFolderIconToExpanded(newNotesFolderEl); - this.changeFolderIconToExpanded(deckFolderEl); + this.changeFolderFolding(newNotesFolderEl); + this.changeFolderFolding(deckFolderEl); } this.createRightPaneFile( newNotesFolderEl, - newFile, + newFile.tfile, fileIsOpen, !deck.activeFolders.has(t("NEW")), deck, - this.plugin, ); } } @@ -92,11 +101,11 @@ export class ReviewQueueListView extends ItemView { let currUnix = -1; let schedFolderEl: HTMLElement | null = null, folderTitle = ""; - const maxDaysToRender: number = this.plugin.data.settings.maxNDaysNotesReviewQueue; + const maxDaysToRender: number = this.settings.maxNDaysNotesReviewQueue; for (const sNote of deck.scheduledNotes) { if (sNote.dueUnix != currUnix) { - const nDays: number = Math.ceil((sNote.dueUnix - now) / (24 * 3600 * 1000)); + const nDays: number = Math.ceil((sNote.dueUnix - now) / TICKS_PER_DAY); if (nDays > maxDaysToRender) { break; @@ -126,17 +135,16 @@ export class ReviewQueueListView extends ItemView { if (fileIsOpen) { deck.activeFolders.add(deck.deckName); deck.activeFolders.add(folderTitle); - this.changeFolderIconToExpanded(schedFolderEl); - this.changeFolderIconToExpanded(deckFolderEl); + this.changeFolderFolding(schedFolderEl); + this.changeFolderFolding(deckFolderEl); } this.createRightPaneFile( schedFolderEl, - sNote.note, + sNote.note.tfile, fileIsOpen, !deck.activeFolders.has(folderTitle), deck, - this.plugin, ); } } @@ -152,38 +160,34 @@ export class ReviewQueueListView extends ItemView { folderTitle: string, collapsed: boolean, hidden: boolean, - deck: ReviewDeck, + deck: NoteReviewDeck, ): HTMLElement { - const folderEl: HTMLDivElement = parentEl.createDiv("tree-item"); - const folderTitleEl: HTMLDivElement = folderEl.createDiv("tree-item-self"); - const childrenEl: HTMLDivElement = folderEl.createDiv("tree-item-children"); + const folderEl: HTMLDivElement = parentEl.createDiv("tree-item nav-folder"); + const folderTitleEl: HTMLDivElement = folderEl.createDiv("tree-item-self nav-folder-title"); + const childrenEl: HTMLDivElement = folderEl.createDiv( + "tree-item-children nav-folder-children", + ); const collapseIconEl: HTMLDivElement = folderTitleEl.createDiv( - "tree-item-collapse-indicator collapse-icon", + "tree-item-icon collapse-icon nav-folder-collapse-indicator", ); collapseIconEl.innerHTML = COLLAPSE_ICON; - if (collapsed) { - (collapseIconEl.childNodes[0] as HTMLElement).style.transform = "rotate(-90deg)"; - } + this.changeFolderFolding(folderEl, collapsed); - folderTitleEl.createDiv("tree-item-content").setText(folderTitle); + folderTitleEl.createDiv("tree-item-inner nav-folder-title-content").setText(folderTitle); if (hidden) { folderEl.style.display = "none"; } folderTitleEl.onClickEvent(() => { - for (const child of childrenEl.childNodes as NodeListOf) { - if (child.style.display === "block" || child.style.display === "") { - child.style.display = "none"; - (collapseIconEl.childNodes[0] as HTMLElement).style.transform = - "rotate(-90deg)"; - deck.activeFolders.delete(folderTitle); - } else { - child.style.display = "block"; - (collapseIconEl.childNodes[0] as HTMLElement).style.transform = ""; - deck.activeFolders.add(folderTitle); - } + this.changeFolderFolding(folderEl, !folderEl.hasClass("is-collapsed")); + childrenEl.style.display = !folderEl.hasClass("is-collapsed") ? "block" : "none"; + + if (!folderEl.hasClass("is-collapsed")) { + deck.activeFolders.delete(folderTitle); + } else { + deck.activeFolders.add(folderTitle); } }); @@ -195,28 +199,26 @@ export class ReviewQueueListView extends ItemView { file: TFile, fileElActive: boolean, hidden: boolean, - deck: ReviewDeck, - plugin: SRPlugin, + deck: NoteReviewDeck, ): void { const navFileEl: HTMLElement = folderEl - .getElementsByClassName("tree-item-children")[0] - .createDiv("tree-item"); + .getElementsByClassName("tree-item-children nav-folder-children")[0] + .createDiv("nav-file"); if (hidden) { navFileEl.style.display = "none"; } - const navFileTitle: HTMLElement = navFileEl.createDiv("tree-item-self"); + const navFileTitle: HTMLElement = navFileEl.createDiv("tree-item-self nav-file-title"); if (fileElActive) { navFileTitle.addClass("is-active"); } - navFileTitle.createDiv("tree-item-content").setText(file.basename); + navFileTitle.createDiv("tree-item-inner nav-file-title-content").setText(file.basename); navFileTitle.addEventListener( "click", async (event: MouseEvent) => { event.preventDefault(); - plugin.lastSelectedReviewDeck = deck.deckName; - await this.app.workspace.getLeaf().openFile(file); + await this.nextNoteReviewHandler.openNote(deck.deckName, file); return false; }, false, @@ -238,8 +240,15 @@ export class ReviewQueueListView extends ItemView { ); } - private changeFolderIconToExpanded(folderEl: HTMLElement): void { - const collapseIconEl = folderEl.find("div.tree-item-collapse-indicator"); - (collapseIconEl.childNodes[0] as HTMLElement).style.transform = ""; + private changeFolderFolding(folderEl: HTMLElement, collapsed = false): void { + if (collapsed) { + folderEl.addClass("is-collapsed"); + const collapseIconEl = folderEl.find("div.nav-folder-collapse-indicator"); + collapseIconEl.addClass("is-collapsed"); + } else { + folderEl.removeClass("is-collapsed"); + const collapseIconEl = folderEl.find("div.nav-folder-collapse-indicator"); + collapseIconEl.removeClass("is-collapsed"); + } } } diff --git a/src/gui/sidebar.tsx b/src/gui/sidebar.tsx new file mode 100644 index 00000000..1f3d399d --- /dev/null +++ b/src/gui/sidebar.tsx @@ -0,0 +1,76 @@ +import { App, Plugin, WorkspaceLeaf } from "obsidian"; + +import { REVIEW_QUEUE_VIEW_TYPE, ReviewQueueListView } from "src/gui/review-queue-list-view"; +import { NextNoteReviewHandler } from "src/next-note-review-handler"; +import { SRSettings } from "src/settings"; + +export class OsrSidebar { + private plugin: Plugin; + private settings: SRSettings; + private nextNoteReviewHandler: NextNoteReviewHandler; + private reviewQueueListView: ReviewQueueListView; + + private get app(): App { + return this.plugin.app; + } + + constructor( + plugin: Plugin, + settings: SRSettings, + nextNoteReviewHandler: NextNoteReviewHandler, + ) { + this.plugin = plugin; + this.settings = settings; + this.nextNoteReviewHandler = nextNoteReviewHandler; + } + + redraw(): void { + if (this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE)) this.reviewQueueListView.redraw(); + } + + private getActiveLeaf(type: string): WorkspaceLeaf | null { + const leaves = this.app.workspace.getLeavesOfType(type); + if (leaves.length == 0) { + return null; + } + + return leaves[0]; + } + + async init(): Promise { + this.plugin.registerView(REVIEW_QUEUE_VIEW_TYPE, (leaf) => { + return (this.reviewQueueListView = new ReviewQueueListView( + leaf, + this.app, + this.nextNoteReviewHandler, + this.settings, + )); + }); + + if ( + this.settings.enableNoteReviewPaneOnStartup && + this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE) == null + ) { + await this.activateReviewQueueViewPanel(); + } + } + + private async activateReviewQueueViewPanel(): Promise { + await this.app.workspace.getRightLeaf(false).setViewState({ + type: REVIEW_QUEUE_VIEW_TYPE, + active: true, + }); + } + + async openReviewQueueView(): Promise { + let reviewQueueLeaf = this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE); + if (reviewQueueLeaf == null) { + await this.activateReviewQueueViewPanel(); + reviewQueueLeaf = this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE); + } + + if (reviewQueueLeaf !== null) { + this.app.workspace.revealLeaf(reviewQueueLeaf); + } + } +} diff --git a/src/gui/StatsModal.tsx b/src/gui/stats-modal.tsx similarity index 93% rename from src/gui/StatsModal.tsx rename to src/gui/stats-modal.tsx index 06072586..1381ca57 100644 --- a/src/gui/StatsModal.tsx +++ b/src/gui/stats-modal.tsx @@ -1,27 +1,27 @@ -import { Modal, App, Platform } from "obsidian"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import h from "vhtml"; import { - Chart, - BarElement, + ArcElement, BarController, - Legend, - Title, - Tooltip, - SubTitle, - ChartTypeRegistry, + BarElement, CategoryScale, + Chart, + ChartTypeRegistry, + Legend, LinearScale, PieController, - ArcElement, + SubTitle, + Title, + Tooltip, } from "chart.js"; +import { App, Modal, Platform } from "obsidian"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import h from "vhtml"; -import type SRPlugin from "src/main"; -import { getKeysPreserveType, getTypedObjectEntries } from "src/util/utils"; -import { textInterval } from "src/scheduling"; +import { textInterval } from "src/algorithms/osr/note-scheduling"; +import { OsrCore } from "src/core"; +import { CardListType } from "src/deck"; import { t } from "src/lang/helpers"; -import { Stats } from "../stats"; -import { CardListType } from "src/Deck"; +import { Stats } from "src/stats"; +import { getKeysPreserveType, getTypedObjectEntries } from "src/utils/types"; Chart.register( BarElement, @@ -37,12 +37,12 @@ Chart.register( ); export class StatsModal extends Modal { - private plugin: SRPlugin; + private osrCore: OsrCore; - constructor(app: App, plugin: SRPlugin) { + constructor(app: App, osrCore: OsrCore) { super(app); - this.plugin = plugin; + this.osrCore = osrCore; this.titleEl.setText(`${t("STATS_TITLE")} `); this.titleEl.addClass("sr-centered"); @@ -70,7 +70,7 @@ export class StatsModal extends Modal { contentEl.style.textAlign = "center"; // Add forecast - const cardStats: Stats = this.plugin.cardStats; + const cardStats: Stats = this.osrCore.cardStats; let maxN: number = cardStats.delayedDays.getMaxValue(); for (let dueOffset = 0; dueOffset <= maxN; dueOffset++) { cardStats.delayedDays.clearCountIfMissing(dueOffset); @@ -142,7 +142,10 @@ export class StatsModal extends Modal { t("INTERVALS_DESC"), Object.keys(cardStats.intervals.dict), Object.values(cardStats.intervals.dict), - t("INTERVALS_SUMMARY", { avg: average_interval, longest: longest_interval }), + t("INTERVALS_SUMMARY", { + avg: average_interval, + longest: longest_interval, + }), t("COUNT"), t("DAYS"), t("NUMBER_OF_CARDS"), @@ -170,7 +173,7 @@ export class StatsModal extends Modal { ); // Add card types - const totalCardsCount: number = this.plugin.deckTree.getDistinctCardCount( + const totalCardsCount: number = this.osrCore.reviewableDeckTree.getDistinctCardCount( CardListType.All, true, ); diff --git a/src/gui/Tabs.ts b/src/gui/tabs.tsx similarity index 98% rename from src/gui/Tabs.ts rename to src/gui/tabs.tsx index 06fab85b..a4d29ae2 100644 --- a/src/gui/Tabs.ts +++ b/src/gui/tabs.tsx @@ -56,7 +56,9 @@ export function createTabs( tabs: Tabs, activateTabId: string, ): TabStructure { - const tab_header = container_element.createEl("div", { attr: { class: "sr-tab-header" } }); + const tab_header = container_element.createEl("div", { + attr: { class: "sr-tab-header" }, + }); const tab_content_containers: TabContentContainers = {}; const tab_buttons: TabButtons = {}; const tab_structure: TabStructure = { diff --git a/src/icons/appicon.ts b/src/icons/app-icon.ts similarity index 100% rename from src/icons/appicon.ts rename to src/icons/app-icon.ts diff --git a/src/lang/helpers.ts b/src/lang/helpers.ts index 02e7b43d..70c5b7f8 100644 --- a/src/lang/helpers.ts +++ b/src/lang/helpers.ts @@ -1,38 +1,40 @@ // https://github.com/mgmeyers/obsidian-kanban/blob/93014c2512507fde9eafd241e8d4368a8dfdf853/src/lang/helpers.ts import { moment } from "obsidian"; -import af from "./locale/af"; -import ar from "./locale/ar"; -import cz from "./locale/cz"; -import bn from "./locale/bn"; -import da from "./locale/da"; -import de from "./locale/de"; -import en from "./locale/en"; -import enGB from "./locale/en-gb"; -import es from "./locale/es"; -import fr from "./locale/fr"; -import hi from "./locale/hi"; -import id from "./locale/id"; -import it from "./locale/it"; -import ja from "./locale/ja"; -import ko from "./locale/ko"; -import mr from "./locale/mr"; -import nl from "./locale/nl"; -import no from "./locale/no"; -import pl from "./locale/pl"; -import pt from "./locale/pt"; -import ptBR from "./locale/pt-br"; -import ro from "./locale/ro"; -import ru from "./locale/ru"; -import ta from "./locale/ta"; -import te from "./locale/te"; -import th from "./locale/th"; -import tr from "./locale/tr"; -import uk from "./locale/uk"; -import ur from "./locale/ur"; -import vi from "./locale/vi"; -import zhCN from "./locale/zh-cn"; -import zhTW from "./locale/zh-tw"; + +import af from "src/lang/locale/af"; +import ar from "src/lang/locale/ar"; +import bn from "src/lang/locale/bn"; +import cz from "src/lang/locale/cz"; +import da from "src/lang/locale/da"; +import de from "src/lang/locale/de"; +import en from "src/lang/locale/en"; +import enGB from "src/lang/locale/en-gb"; +import es from "src/lang/locale/es"; +import fr from "src/lang/locale/fr"; +import hi from "src/lang/locale/hi"; +import id from "src/lang/locale/id"; +import it from "src/lang/locale/it"; +import ja from "src/lang/locale/ja"; +import ko from "src/lang/locale/ko"; +import mr from "src/lang/locale/mr"; +import nl from "src/lang/locale/nl"; +import no from "src/lang/locale/no"; +import pl from "src/lang/locale/pl"; +import pt from "src/lang/locale/pt"; +import ptBR from "src/lang/locale/pt-br"; +import ro from "src/lang/locale/ro"; +import ru from "src/lang/locale/ru"; +import sw from "src/lang/locale/sw"; +import ta from "src/lang/locale/ta"; +import te from "src/lang/locale/te"; +import th from "src/lang/locale/th"; +import tr from "src/lang/locale/tr"; +import uk from "src/lang/locale/uk"; +import ur from "src/lang/locale/ur"; +import vi from "src/lang/locale/vi"; +import zhCN from "src/lang/locale/zh-cn"; +import zhTW from "src/lang/locale/zh-tw"; export const localeMap: { [k: string]: Partial } = { af, @@ -58,6 +60,7 @@ export const localeMap: { [k: string]: Partial } = { "pt-br": ptBR, ro, ru, + sw, ta, te, th, diff --git a/src/lang/locale/ar.ts b/src/lang/locale/ar.ts index 6157efc4..ced95f1b 100644 --- a/src/lang/locale/ar.ts +++ b/src/lang/locale/ar.ts @@ -71,8 +71,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "مجلدات لتجاهلها", - FOLDERS_TO_IGNORE_DESC: `Templates Meta/Scripts. -Note that this setting is common to both Flashcards and Notes. : أدخل مسارات المجلد مفصولة بواسطة سطور جديدة,مثال`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "البطاقات", FLASHCARD_EASY_LABEL: "نص الزر سهل", FLASHCARD_GOOD_LABEL: "نص الزر جيد", @@ -122,6 +123,7 @@ Note that this setting is common to both Flashcards and Notes. : أدخل مسا INLINE_REVERSED_CARDS_SEPARATOR: "فاصل من أجل البطاقات العكسية المضمنة", MULTILINE_CARDS_SEPARATOR: "فاصل من أجل البطاقات المتعددة", MULTILINE_REVERSED_CARDS_SEPARATOR: "فاصل من أجل البطاقات العكسية المتعددة", + MULTILINE_CARDS_END_MARKER: "الأحرف التي تدل على نهاية الكلوزات وبطاقات التعلم المتعددة الأسطر", NOTES: "ملاحظات", REVIEW_PANE_ON_STARTUP: "تمكين جزء مراجعة الملاحظات عند بدء التشغيل", TAGS_TO_REVIEW: "وسوم للمراجعة", @@ -129,14 +131,19 @@ Note that this setting is common to both Flashcards and Notes. : أدخل مسا OPEN_RANDOM_NOTE: "افتح ملاحظة عشوائية للمراجعة", OPEN_RANDOM_NOTE_DESC: "(Pagerank) عند تعطيل هذا الخيار ،الملاحظات سيتم ترتيبُها حسب الأهمية", AUTO_NEXT_NOTE: "افتح الملاحظة التالية تلقائيًا بعد المراجعة", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "تعطيل خيارات المراجعة في قائمة الملفات ، أي المراجعة:السهل الصعب الجيد", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "عند تغيير هذا الخيار Obsidian أعد تشغيل , command hotkeys. بعد التعطيل ، يمكنك المراجعة باستخدام", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "فعّل خيارات المراجعة في قائمة الملف (مثال: مراجعة: سهل، جيد، صعب)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "إذا قمت بتعطيل خيارات المراجعة في قائمة الملف، يمكنك مراجعة ملاحظاتك باستخدام أوامر الإضافة وإذا كنت قد حددتها، باستخدام مفاتيح الاختصار المرتبطة.", MAX_N_DAYS_REVIEW_QUEUE: "الحد الأقصى لعدد الأيام التي يجب عرضها على اللوحة اليمنى", MIN_ONE_DAY: "يجب أن يكون عدد الأيام 1 على الأقل", VALID_NUMBER_WARNING: "يرجى تقديم رقم صالح", UI_PREFERENCES: "تفضيلات واجهة المستخدم", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "يجب أن يكون العرض الشجري للرُزم موسع بحيث تطهر الملفات الفرعية كلها", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: @@ -159,7 +166,9 @@ Note that this setting is common to both Flashcards and Notes. : أدخل مسا MAX_LINK_CONTRIB: "أقصى مساهمة ارتباط", MAX_LINK_CONTRIB_DESC: "أقصى مساهمة للسهولة المرجحة للملاحظات المرتبطة بالسهولة الأولية.", LOGGING: "تسجيل", - DISPLAY_DEBUG_INFO: "عرض معلومات التصحيح على وحدة تحكم المطور؟", + DISPLAY_DEBUG_INFO: "عرض معلومات التصحيح على وحدة تحكم المطور", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "ملاحظات قائمة المراجعة", diff --git a/src/lang/locale/cz.ts b/src/lang/locale/cz.ts index b886e98a..4415d663 100644 --- a/src/lang/locale/cz.ts +++ b/src/lang/locale/cz.ts @@ -10,7 +10,7 @@ export default { SKIP: "Skip", EDIT_CARD: "Edit Card", RESET_CARD_PROGRESS: "Vynulovat pokrok kartičky", - HARD: "Težké", + HARD: "Těžké", GOOD: "Dobré", EASY: "Jednoduché", SHOW_ANSWER: "Ukázat odpověď", @@ -71,8 +71,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "Ignorované složky", - FOLDERS_TO_IGNORE_DESC: `Zadejte cesty ke složkám oddělené odřádkováním napříkad. Šablony Meta/Scripts. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "Kartičky", FLASHCARD_EASY_LABEL: "Easy Button Text", FLASHCARD_GOOD_LABEL: "Good Button Text", @@ -125,6 +126,7 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "Oddělovač pro otočené inline kartičky", MULTILINE_CARDS_SEPARATOR: "Oddělovač pro víceřádkové kartičky", MULTILINE_REVERSED_CARDS_SEPARATOR: "Oddělovač pro víceřádkove otočené kartičky", + MULTILINE_CARDS_END_MARKER: "Znaky označující konec clozes a víceřádkových flash karet", NOTES: "Poznámky", REVIEW_PANE_ON_STARTUP: "Enable note review pane on startup", TAGS_TO_REVIEW: "Tag pro revizi", @@ -133,14 +135,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE: "Otevřít náhodnou poznámku pro revizi", OPEN_RANDOM_NOTE_DESC: "Pokud toto vypnete, poznámky budou řazeny dle důležitosti (PageRank).", AUTO_NEXT_NOTE: "Otevřít automaticky další poznámku po dokončení revize", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "Vypnout volby revize v menu souboru například 'Revize: Jednoduché'", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "Po vypnutí můžete používat klávesové zkratky. Restartujte Obsidian po změně nastavení.", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Povolte možnosti revize v nabídce souboru (např. Revize: Jednoduché, Dobré, Těžké)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "Pokud v nabídce souboru zakážete možnosti revize, můžete své poznámky revidovat pomocí příkazů pluginu a, pokud jste je definovali, pomocí přiřazených klávesových zkratek.", MAX_N_DAYS_REVIEW_QUEUE: "Maximální počet dní zobrazených v pravém panelu", MIN_ONE_DAY: "Počet dní musí být minimálně 1.", VALID_NUMBER_WARNING: "Prosím zadejte validní číslo.", UI_PREFERENCES: "Předvolby uživatelského rozhraní", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "Stromy balíčky by měly být zpočátku zobrazeny jako rozbalené", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: @@ -163,7 +170,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "Maximální příspěvek vážené složitosti prolinkovaných poznámek použitý pro určení počáteční složitosti.", LOGGING: "Zaznamenávám", - DISPLAY_DEBUG_INFO: "Zobrazit informace pro ladění na vývojářské konzoli?", + DISPLAY_DEBUG_INFO: "Zobrazit informace pro ladění na vývojářské konzoli", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "Fronta poznámek k revizi", diff --git a/src/lang/locale/de.ts b/src/lang/locale/de.ts index ea9a5591..598c5897 100644 --- a/src/lang/locale/de.ts +++ b/src/lang/locale/de.ts @@ -1,6 +1,6 @@ // Deutsch -// Obsidian specific names (folder, note, tag, etc.) are consistent with the german translation: +// Obsidian specific names (folder, note, tag, etc.) are consistent with the German translation: // https://github.com/obsidianmd/obsidian-translations/blob/master/de.json export default { @@ -28,8 +28,8 @@ export default { // main.ts OPEN_NOTE_FOR_REVIEW: "Notiz zur Wiederholung öffnen", REVIEW_CARDS: "Lernkarten wiederholen", - REVIEW_DIFFICULTY_FILE_MENU: "Notiz abschliessen als: ${difficulty}", - REVIEW_NOTE_DIFFICULTY_CMD: "Notiz abschliessen als: ${difficulty}", + REVIEW_DIFFICULTY_FILE_MENU: "Notizen wiederholen als: ${difficulty}", + REVIEW_NOTE_DIFFICULTY_CMD: "Notizen wiederholen als: ${difficulty}", REVIEW_ALL_CARDS: "Alle Lernkarten wiederholen", CRAM_ALL_CARDS: "Wähle ein Stapel zum pauken", REVIEW_CARDS_IN_NOTE: "Lernkarten in dieser Notiz wiederholen", @@ -77,8 +77,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "Ausgeschlossene Ordner", - FOLDERS_TO_IGNORE_DESC: `Mehrere Ordner mit Zeilenumbrüchen getrennt angeben. Bsp. OrdnerA[Zeilenumbruch]OrdnerB/Unterordner. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "Lernkarten", FLASHCARD_EASY_LABEL: "Einfach Knopf Text", FLASHCARD_GOOD_LABEL: "Gut Knopf Text", @@ -137,6 +138,8 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "Trennzeichen für einzeilige beidseitige Lernkarten", MULTILINE_CARDS_SEPARATOR: "Trennzeichen für mehrzeilige Lernkarten", MULTILINE_REVERSED_CARDS_SEPARATOR: "Trennzeichen für mehrzeilige beidseitige Lernkarten", + MULTILINE_CARDS_END_MARKER: + "Zeichen, die das Ende von Lückentexten und mehrzeiligen Flashcards kennzeichnen", NOTES: "Notizen", REVIEW_PANE_ON_STARTUP: "Öffne Überprüfungswarteschlage beim start", TAGS_TO_REVIEW: "Zu wiederholende Tags", @@ -146,15 +149,20 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE_DESC: "Wenn dies deaktiviert wird, dann werden die Notizen nach Wichtigkeit wiederholt (PageRank).", AUTO_NEXT_NOTE: "Nach einer Wiederholung automatisch die nächste Karte öffnen", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "Optionen zur Wiederholung im Menü einer Datei deaktivieren. Bsp. Wiederholen: Einfach Gut Schwer", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "Nach dem Deaktivieren können die Tastenkürzel zur Wiederholung verwendet werden. Obsidian muss nach einer Änderung neu geladen weren.", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Aktiviere die Überprüfungsoptionen im Dateimenü (z. B. Notizen wiederholen als: Einfach, Gut, Schwer)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "Wenn du die Überprüfungsoptionen im Dateimenü deaktivierst, kannst du deine Notizen mit den Plugin-Befehlen und, falls definiert, den zugehörigen Tastenkombinationen überprüfen.", MAX_N_DAYS_REVIEW_QUEUE: "Maximale Anzahl anstehender Notizen, die im rechten Fensterbereich angezeigt werden", MIN_ONE_DAY: "Anzahl der Tage muss mindestens 1 sein.", VALID_NUMBER_WARNING: "Bitte eine gültige Zahl eingeben.", UI_PREFERENCES: "Einstellungen der Benutzeroberfläche", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "Stapelverzeichnis soll beim öffnen erweitert angezeigt werden", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: @@ -180,7 +188,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "Maximaler Einfluss der Einfachheiten verlinkter Notizen zur gewichteten initialen Einfachheit einer neuen Lernkarte.", LOGGING: "Protokollierung", - DISPLAY_DEBUG_INFO: "Informationen zum Debugging in der Entwicklerkonsole anzeigen?", + DISPLAY_DEBUG_INFO: "Informationen zum Debugging in der Entwicklerkonsole anzeigen", + DISPLAY_PARSER_DEBUG_INFO: + "Informationen zum parser Debugging in der Entwicklerkonsole anzeigen", // sidebar.ts NOTES_REVIEW_QUEUE: "Anstehende Notizen zur Wiederholung", diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 10d0d637..e7833693 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -71,8 +71,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "Folders to ignore", - FOLDERS_TO_IGNORE_DESC: `Enter folder paths separated by newlines e.g. Templates Meta/Scripts. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "Flashcards", FLASHCARD_EASY_LABEL: "Easy Button Text", FLASHCARD_GOOD_LABEL: "Good Button Text", @@ -125,6 +126,7 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "Separator for inline reversed flashcards", MULTILINE_CARDS_SEPARATOR: "Separator for multiline flashcards", MULTILINE_REVERSED_CARDS_SEPARATOR: "Separator for multiline reversed flashcards", + MULTILINE_CARDS_END_MARKER: "Characters denoting the end of clozes and multiline flashcards", NOTES: "Notes", REVIEW_PANE_ON_STARTUP: "Enable note review pane on startup", TAGS_TO_REVIEW: "Tags to review", @@ -132,14 +134,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE: "Open a random note for review", OPEN_RANDOM_NOTE_DESC: "When you turn this off, notes are ordered by importance (PageRank).", AUTO_NEXT_NOTE: "Open next note automatically after a review", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "Disable review options in the file menu i.e. Review: Easy Good Hard", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "After disabling, you can review using the command hotkeys. Reload Obsidian after changing this.", MAX_N_DAYS_REVIEW_QUEUE: "Maximum number of days to display on right panel", MIN_ONE_DAY: "The number of days must be at least 1.", VALID_NUMBER_WARNING: "Please provide a valid number.", UI_PREFERENCES: "UI Preferences", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Enable the review options in the file menu (e.g. Review: Easy, Good, Hard)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "If you disable the review options in the file menu, you can review your notes using the plugin commands and, if you defined them, the associated command hotkeys.", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "Deck trees should be initially displayed as expanded", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: "Turn this off to collapse nested decks in the same card. Useful if you have cards which belong to many decks in the same file.", @@ -162,7 +169,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "Maximum contribution of the weighted ease of linked notes to the initial ease.", LOGGING: "Logging", - DISPLAY_DEBUG_INFO: "Display debugging information on the developer console?", + DISPLAY_DEBUG_INFO: "Display debugging information on the developer console", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "Notes Review Queue", diff --git a/src/lang/locale/es.ts b/src/lang/locale/es.ts index 672f3e39..7c39835e 100644 --- a/src/lang/locale/es.ts +++ b/src/lang/locale/es.ts @@ -71,8 +71,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "Directorios a ignorar", - FOLDERS_TO_IGNORE_DESC: `Escriba las rutas de los directorios separadas por saltos de línea, por ejemplo, Plantillas Extra/Guiones. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "Tarjetas de Memorización", FLASHCARD_EASY_LABEL: "Texto del botón: Fácil", FLASHCARD_GOOD_LABEL: "Texto del botón: Bien", @@ -127,6 +128,8 @@ Note that this setting is common to both Flashcards and Notes.`, MULTILINE_CARDS_SEPARATOR: "Separador para tarjetas de memorización multilínea", MULTILINE_REVERSED_CARDS_SEPARATOR: "Separador para tarjetas de memorización multilínea invertidas", + MULTILINE_CARDS_END_MARKER: + "Caracteres que denotan el fin de los clozes y tarjetas didácticas de varias líneas", NOTES: "Notes", REVIEW_PANE_ON_STARTUP: "Activar panel de revisión de notas al arrancar", TAGS_TO_REVIEW: "Etiquetas a revisar", @@ -136,14 +139,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE_DESC: "Cuando deshabilita esto, las notas son ordenadas por importancia (Algoritmo PageRank).", AUTO_NEXT_NOTE: "Abrir la siguiente nota automáticamente después de una revisión", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "Deshabilitar opciones de revisión en el menú de archivo, por ejemplo, Revisión: Fácil Bien Difícil", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "Después de deshabilitarlo, puede hacer las revisiones utilizando atajos de teclado. Reinicie Obsidian después de cambiar esto.", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Habilita las opciones de revisión en el menú Archivo (por ejemplo: Revisar: Fácil, Bien, Difícil)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "Si desactivas las opciones de revisión en el menú Archivo, puedes revisar tus notas usando los comandos del plugin y, si los definiste, las teclas rápidas asociadas.", MAX_N_DAYS_REVIEW_QUEUE: "Número máximo de días a mostrar en el panel derecho.", MIN_ONE_DAY: "El número de días debe ser al menos uno.", VALID_NUMBER_WARNING: "Por favor especifique un número válido.", UI_PREFERENCES: "Preferencias de la interfaz de usuario.", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "Los árboles de mazos deberían ser expandidos al inicio.", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: "Desactiva esto para contraer mazos anidados en la misma tarjeta. Útil si tienes tarjetas que pertenecen a muchos mazos en el mismo archivo.", @@ -168,7 +176,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "Contribución máxima de la facilidad ponderada de las notas vinculadas a la facilidad inicial.", LOGGING: "Registro", - DISPLAY_DEBUG_INFO: "¿Mostrar información de depuración en la consola de desarrollador?", + DISPLAY_DEBUG_INFO: "¿Mostrar información de depuración en la consola de desarrollador", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "Cola de notas a revisar", diff --git a/src/lang/locale/fr.ts b/src/lang/locale/fr.ts index b05317b7..f6f32487 100644 --- a/src/lang/locale/fr.ts +++ b/src/lang/locale/fr.ts @@ -1,3 +1,212 @@ // français -export default {}; +export default { + // flashcard-modal.tsx + DECKS: "Paquets", + DUE_CARDS: "Cartes dues", + NEW_CARDS: "Nouvelles cartes", + TOTAL_CARDS: "Total de cartes", + BACK: "Précédent", + SKIP: "Sauter", + EDIT_CARD: "Modifier la carte", + RESET_CARD_PROGRESS: "Remettre à zéro le niveau de cette carte", + HARD: "Difficile", + GOOD: "Correct", + EASY: "Facile", + SHOW_ANSWER: "Montrer la réponse", + CARD_PROGRESS_RESET: "Le niveau de la carte a été remis à zéro.", + SAVE: "Sauvegarder", + CANCEL: "Annuler", + NO_INPUT: "Pas de contenu.", + CURRENT_EASE_HELP_TEXT: "Facilité actuelle : ", + CURRENT_INTERVAL_HELP_TEXT: "Intervalle actuel : ", + CARD_GENERATED_FROM: "Généré depuis : ${notePath}", + + // main.ts + OPEN_NOTE_FOR_REVIEW: "Ouvrir une note à apprendre", + REVIEW_CARDS: "Apprendre les flashcards", + REVIEW_DIFFICULTY_FILE_MENU: "Apprentissage : ${difficulty}", + REVIEW_NOTE_DIFFICULTY_CMD: "Difficulté de la révision : ${difficulty}", + CRAM_ALL_CARDS: "Choisir un deck à réviser", + REVIEW_ALL_CARDS: "Apprendre les flashcards dans toutes les notes", + REVIEW_CARDS_IN_NOTE: "Apprendre les flashcards dans cette note", + CRAM_CARDS_IN_NOTE: "Réviser les flashcards dans cette note", + VIEW_STATS: "Voir les statistiques", + OPEN_REVIEW_QUEUE_VIEW: + "Ouvrir la file d'attente des notes à apprendre dans la barre verticale", + STATUS_BAR: "Apprentissage : ${dueNotesCount} note(s), ${dueFlashcardsCount} carte(s) dues", + SYNC_TIME_TAKEN: "Synchronisé en ${t}ms", + NOTE_IN_IGNORED_FOLDER: "La note est dans un dossier ignoré (voir paramètres).", + PLEASE_TAG_NOTE: "Ajoutez le bon tag à la note pour l'apprendre (dans les paramètres).", + RESPONSE_RECEIVED: "Réponse enregistrée.", + NO_DECK_EXISTS: "Pas de paquet sous le nom ${deckName}", + ALL_CAUGHT_UP: "Bravo, vous êtes à jour !", + + // scheduling.ts + DAYS_STR_IVL: "${interval} jour(s)", + MONTHS_STR_IVL: "${interval} mois(s)", + YEARS_STR_IVL: "${interval} an(s)", + DAYS_STR_IVL_MOBILE: "${interval}j", + MONTHS_STR_IVL_MOBILE: "${interval}m", + YEARS_STR_IVL_MOBILE: "${interval}a", + + // settings.ts + SETTINGS_HEADER: "Spaced Repetition - Paramètres", + GROUP_TAGS_FOLDERS: "Tags & Dossiers", + GROUP_FLASHCARD_REVIEW: "Apprentissage des flashcards", + GROUP_FLASHCARD_SEPARATORS: "Séparateurs de flashcards", + GROUP_DATA_STORAGE: "Stockage des informations de planification", + GROUP_FLASHCARDS_NOTES: "Flashcards & Notes", + GROUP_CONTRIBUTING: "Contribuer", + CHECK_WIKI: 'Pour plus d\'informations, visitez le wiki.', + GITHUB_DISCUSSIONS: + 'Visitez les discussions pour des questions-réponses, des retours ou une discussion généraliste.', + GITHUB_ISSUES: + 'Créez un ticket sur GitHub si vous trouvez un bug ou voulez demander une fonctionnalité.', + GITHUB_SOURCE_CODE: + 'Code source du projet disponible sur GitHub', + CODE_CONTRIBUTION_INFO: + 'Information sur les contributions au code', + TRANSLATION_CONTRIBUTION_INFO: + 'Informations sur la traduction du plugin dans votre langue', + PROJECT_CONTRIBUTIONS: + 'Créez un ticket sur GitHub si vous trouvez un bug ou voulez demander une fonctionnalité', + FOLDERS_TO_IGNORE: "Dossiers à ignorer", + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", + FLASHCARDS: "Flashcards", + FLASHCARD_EASY_LABEL: "Bouton Facile", + FLASHCARD_GOOD_LABEL: "Bouton Correct", + FLASHCARD_HARD_LABEL: "Bouton Difficile", + FLASHCARD_EASY_DESC: "Changez le texte du bouton Facile", + FLASHCARD_GOOD_DESC: "Changez le texte du bouton Correct", + FLASHCARD_HARD_DESC: "Changez le texte du bouton Difficile", + FLASHCARD_TAGS: "Tags des flashcards", + FLASHCARD_TAGS_DESC: + "Entrez les tags séparés par un espace ou une ligne i.e. #flashcards #paquet2 #paquet3.", + CONVERT_FOLDERS_TO_DECKS: "Convertir les dossiers en paquets et sous-paquets ?", + CONVERT_FOLDERS_TO_DECKS_DESC: + "Ceci est une alternative aux tags de flashcards présentés ci-dessus.", + INLINE_SCHEDULING_COMMENTS: + "Sauvegarder le commentaire de planification dans la dernière ligne de la flashcard ?", + INLINE_SCHEDULING_COMMENTS_DESC: + "Activer ceci empêche les commentaires HTML de casser la mise en page des listes.", + BURY_SIBLINGS_TILL_NEXT_DAY: "Enterrer les cartes sœurs jusqu'au lendemain ?", + BURY_SIBLINGS_TILL_NEXT_DAY_DESC: + "Les cartes sœurs sont les cartes générées depuis le même texte, par exemple pour les textes à trous", + SHOW_CARD_CONTEXT: "Montrer le contexte dans les cartes ?", + SHOW_CARD_CONTEXT_DESC: "ex. Titre de la note > Titre 1 > Sous-titre > ... > Sous-titre", + CARD_MODAL_HEIGHT_PERCENT: "Pourcentage de hauteur de la flashcard", + CARD_MODAL_SIZE_PERCENT_DESC: "Devrait être 100% sur mobile ou en cas de grandes images", + RESET_DEFAULT: "Réinitialiser les paramètres", + CARD_MODAL_WIDTH_PERCENT: "Pourcentage de largeur de la flashcard", + RANDOMIZE_CARD_ORDER: "Apprendre les cartes dans un ordre aléatoire ?", + REVIEW_CARD_ORDER_WITHIN_DECK: "Ordre d'affichage des cartes d'un paquet pendant les révisions", + REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Dans l'ordre du paquet (Nouvelles cartes d'abord)", + REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Dans l'ordre du paquet (Cartes dues d'abord)", + REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Aléatoirement dans le paquet (Nouvelles cartes d'abord)", + REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Aléatoirement dans le paquet (Cartes dues d'abord)", + REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Carte au hasard dans un paquet au hasard", + REVIEW_DECK_ORDER: "Ordre d'affichage des paquets pendant les révisions", + REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL: + "Séquentiel (quand toutes les cartes du paquet précédent sont apprises)", + REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM: + "Aléatoire (quand toutes les cartes du paquet précédent sont apprises)", + REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Carte au hasard dans un paquet au hasard", + DISABLE_CLOZE_CARDS: "Désactiver les textes à trous ?", + CONVERT_HIGHLIGHTS_TO_CLOZES: "Convertir ==soulignages== en trous ?", + CONVERT_BOLD_TEXT_TO_CLOZES: "Convertir **gras** en trous ?", + CONVERT_CURLY_BRACKETS_TO_CLOZES: "Convertir {{crochets}} en trous ?", + INLINE_CARDS_SEPARATOR: "Séparateur pour flashcards en une ligne", + FIX_SEPARATORS_MANUALLY_WARNING: + "Après avoir changé ce réglage, vous devrez manuellement mettre à jour toutes vos flashcards.", + INLINE_REVERSED_CARDS_SEPARATOR: "Séparateur pour flashcards inversées en une ligne", + MULTILINE_CARDS_SEPARATOR: "Séparateur pour flashcards en plusieurs lignes", + MULTILINE_REVERSED_CARDS_SEPARATOR: "Séparateur pour flashcards inversées en plusieurs lignes", + MULTILINE_CARDS_END_MARKER: + "Caractères de fin de textes à trous ou de flashcards en plusieurs lignes", + NOTES: "Notes", + REVIEW_PANE_ON_STARTUP: "Montrer le module d'apprentissage des notes au démarrage", + TAGS_TO_REVIEW: "Tags à apprendre", + TAGS_TO_REVIEW_DESC: + "Entrez les tags séparés par un espace ou une ligne i.e. #review #tag2 #tag3.", + OPEN_RANDOM_NOTE: "Ouvrir une note à apprendre au hasard", + OPEN_RANDOM_NOTE_DESC: + "Si vous désactivez cette option, les notes sont triées par importance (PageRank).", + AUTO_NEXT_NOTE: "Ouvrir la prochaine note automatiquement après un apprentissage", + MAX_N_DAYS_REVIEW_QUEUE: "Jours maximum affichés dans la barre de droite", + MIN_ONE_DAY: "Le nombre de jours doit être au moins 1.", + VALID_NUMBER_WARNING: "Entrez un nombre valide.", + UI_PREFERENCES: "Préférences UI", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Enable the review options in the file menu (e.g. Review: Easy, Good, Hard)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "If you disable the review options in the file menu, you can review your notes using the plugin commands and, if you defined them, the associated command hotkeys.", + INITIALLY_EXPAND_SUBDECKS_IN_TREE: + "Les dossiers de paquets devraient initialement tous être ouverts", + INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: + "Désactivez pour réduire les paquets dans la même carte. Ce réglage est utile si vous avez des cartes qui appartiennent à beaucoup de paquets à la fois.", + ALGORITHM: "Algorithme", + CHECK_ALGORITHM_WIKI: + "Pour en savoir plus, lisez l'implémentation de l'algorithme.", + BASE_EASE: "Facilité de base", + BASE_EASE_DESC: "minimum = 130, recommandé = vers 250.", + BASE_EASE_MIN_WARNING: "La facilité de base doit être supérieure à 130.", + LAPSE_INTERVAL_CHANGE: + "Changement d'intervalle quand vous indiquez qu'une flashcard/note a été difficile", + LAPSE_INTERVAL_CHANGE_DESC: "nouvelIntervalle = ancienIntervalle * changementIntervalle / 100.", + EASY_BONUS: "Bonus Facile", + EASY_BONUS_DESC: + "Le bonus Facile vous permet d'augmenter l'intervalle entre une réponse Correct et une réponse Facile sur une flashcard/note (minimum = 100%).", + EASY_BONUS_MIN_WARNING: "Le bonus Facile doit être au moins 100.", + MAX_INTERVAL: "Intervalle maximum (en jours)", + MAX_INTERVAL_DESC: + "Vous permet de mettre une limite maximale sur l'intervalle (par défaut, 100 ans).", + MAX_INTERVAL_MIN_WARNING: "L'intervalle maximum doit être au moins 1 jour.", + MAX_LINK_CONTRIB: "Contribution maximum des liens", + MAX_LINK_CONTRIB_DESC: + "Contribution maximum de la facilité pondérée des notes liées à la facilité initiale.", + LOGGING: "Logging", + DISPLAY_DEBUG_INFO: "Afficher les informations de débogage dans la console de développement", + DISPLAY_PARSER_DEBUG_INFO: + "Afficher les informations de débogage pour le parser dans la console de développement", + + // sidebar.ts + NOTES_REVIEW_QUEUE: "Cartes à apprendre", + CLOSE: "Fermer", + NEW: "Nouveau", + YESTERDAY: "Hier", + TODAY: "Aujourd'hui", + TOMORROW: "Demain", + + // stats-modal.tsx + STATS_TITLE: "Statistiques", + MONTH: "Mois", + QUARTER: "Trimestre", + YEAR: "Année", + LIFETIME: "Toujours", + FORECAST: "Prévisions", + FORECAST_DESC: "Le nombre de cartes dues dans le futur", + SCHEDULED: "Planifié", + DAYS: "Jours", + NUMBER_OF_CARDS: "Nombre de cartes", + REVIEWS_PER_DAY: "Moyenne : ${avg} apprentissages / jour", + INTERVALS: "Intervalles", + INTERVALS_DESC: "Durée avant de remontrer une carte", + COUNT: "Total", + INTERVALS_SUMMARY: "Intervalle moyen : ${avg}. Intervalle maximum: ${longest}", + EASES: "Facilité", + EASES_SUMMARY: "Facilité moyenne : ${avgEase}", + CARD_TYPES: "Types de cartes", + CARD_TYPES_DESC: "Ceci inclut les cartes enterrées, s'il y en a", + CARD_TYPE_NEW: "Nouvelles", + CARD_TYPE_YOUNG: "En cours d'apprentissage", + CARD_TYPE_MATURE: "Matures", + CARD_TYPES_SUMMARY: "Total de cartes : ${totalCardsCount}", +}; diff --git a/src/lang/locale/it.ts b/src/lang/locale/it.ts index ef75a8ef..ba6d687a 100644 --- a/src/lang/locale/it.ts +++ b/src/lang/locale/it.ts @@ -72,8 +72,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "Cartelle da ignorare", - FOLDERS_TO_IGNORE_DESC: `Inserisci i percorsi delle cartelle separati da a capo, per esempio, Templates Meta/Scripts. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "Schede", FLASHCARD_EASY_LABEL: "Testo del bottone facile", FLASHCARD_GOOD_LABEL: "Testo del bottone buono", @@ -130,6 +131,8 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "Separatore per schede all'incontrario sulla stessa riga", MULTILINE_CARDS_SEPARATOR: "Separatore per schede su più righe", MULTILINE_REVERSED_CARDS_SEPARATOR: "Separatore per schede all'incontrario su più righe", + MULTILINE_CARDS_END_MARKER: + "Caratteri che denotano la fine di carte con spazi da riempiere e carte multilinea", NOTES: "Note", REVIEW_PANE_ON_STARTUP: "Abilita il pannello di revisione note all'avvio", TAGS_TO_REVIEW: "Etichette da rivedere", @@ -139,14 +142,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE_DESC: "Quando lo disabiliti, le note saranno ordinate per importanza (PageRank).", AUTO_NEXT_NOTE: "Apri la prossima nota automaticamente dopo la revisione", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "Disabilita le opzioni di revisioni nel menù di file, per esempio Revisione: Facile Buono Difficile", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "Dopo avermi disattivato, puoi iniziare una revisione con le combinazioni di testi per il comando. Riavvia Obsidian dopo avermi cambiato.", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Abilita le opzioni di revisione nel menu File (es.: Rivisita: Facile, Buono, Difficile)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "Se disabiliti le opzioni di revisione nel menu File, puoi rivedere le tue note utilizzando i comandi del plugin e, se li hai definiti, le scorciatoie da tastiera associate.", MAX_N_DAYS_REVIEW_QUEUE: "Numero di giorni massimi da visualizzare nel pannello di destra", MIN_ONE_DAY: "Il numero di giorni deve essere almeno 1.", VALID_NUMBER_WARNING: "Per favore, mettere un numero valido.", UI_PREFERENCES: "Preferenze di interfaccia", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "Alberti di mazzi dovrebbero essere inizialmente visualizzate come espansi", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: @@ -171,7 +179,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "Contributo massimo della difficoltà pasata delle note collegate alla difficoltà iniziale.", LOGGING: "Registrando", - DISPLAY_DEBUG_INFO: "Visualizza informazione di debug sulla console per sviluppatori?", + DISPLAY_DEBUG_INFO: "Visualizza informazione di debug sulla console per sviluppatori", + DISPLAY_PARSER_DEBUG_INFO: + "Visualizza informazione di debug riguardanti il parser sulla console per sviluppatori", // sidebar.ts NOTES_REVIEW_QUEUE: "Coda di note da rivedere", diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index 2f0c6645..cec9c902 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -72,8 +72,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "無視するフォルダ", - FOLDERS_TO_IGNORE_DESC: `フォルダパスを改行で区切って入力してください。"Templates Meta/Scripts" のようなスペースによる区切りでの書き方は無効です。. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "フラッシュカード", FLASHCARD_EASY_LABEL: "Easy Button Text", FLASHCARD_GOOD_LABEL: "Good Button Text", @@ -128,6 +129,7 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "インラインの表裏反転フラッシュカードに使用するセパレーター", MULTILINE_CARDS_SEPARATOR: "複数行のフラッシュカードに使用するセパレーター", MULTILINE_REVERSED_CARDS_SEPARATOR: "複数行の表裏反転フラッシュカードに使用するセパレーター", + MULTILINE_CARDS_END_MARKER: "クローズと複数行フラッシュカードの終わりを示す文字", NOTES: "ノート", REVIEW_PANE_ON_STARTUP: "Enable note review pane on startup", TAGS_TO_REVIEW: "レビューに使用するタグ", @@ -137,14 +139,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE_DESC: "このオプションが無効化されている状態では、ノートは重要度(ページランク)による順番で表示されます。", AUTO_NEXT_NOTE: "レビュー後に次のノートを自動的に開く", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "ファイルメニューでのレビューオプションを無効化(「レビュー: Easy」等の項目を非表示にする)", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "無効化した後、コマンドホットキーを使ってレビューすることが可能になります。このオプションを変更した場合にはObsidianをリロードしてください。", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "ファイルメニューでレビューオプションを有効にしてください(例: Easy, Good, Hard)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "ファイルメニューでレビューオプションを無効にした場合、プラグインコマンドや、設定している場合は対応するホットキーを使用してメモをレビューできます。", MAX_N_DAYS_REVIEW_QUEUE: "右パネルに表示する最大の日数", MIN_ONE_DAY: "日数には1以上の数字を指定してください。", VALID_NUMBER_WARNING: "有効な数字を入力してください。", UI_PREFERENCES: "ユーザー インターフェイスの設定", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "デッキ ツリーは最初は展開して表示する必要があります", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: "これをオフにすると、同じカード内のネストされたデッキが折りたたまれます。同じファイルに多くのデッキに属するカードがある場合に便利です。", @@ -167,7 +174,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "最初の易しさに対して、リンクされたノートの重み付けされた易しさが寄与する最大値を指定してください。", LOGGING: "ログ管理", - DISPLAY_DEBUG_INFO: "デベロッパーコンソールにてデバッグ情報を表示しますか?", + DISPLAY_DEBUG_INFO: "デベロッパーコンソールにてデバッグ情報を表示しますか", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "ノートレビューのキュー", diff --git a/src/lang/locale/ko.ts b/src/lang/locale/ko.ts index 07946003..2ae735cf 100644 --- a/src/lang/locale/ko.ts +++ b/src/lang/locale/ko.ts @@ -71,8 +71,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "무시할 폴더들", - FOLDERS_TO_IGNORE_DESC: `폴더 경로를 빈 줄로 구분해서 입력해주세요. 'Templates Meta/Scripts' 와 같이 입력하는 것은 유효하지 않습니다. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "플래시카드", FLASHCARD_EASY_LABEL: "Easy Button Text", FLASHCARD_GOOD_LABEL: "Good Button Text", @@ -126,6 +127,7 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "인라인 반전 플래시카드 구분자", MULTILINE_CARDS_SEPARATOR: "여러 줄 플래시카드 구분자", MULTILINE_REVERSED_CARDS_SEPARATOR: "여러 줄 반전 플래시카드 구분자", + MULTILINE_CARDS_END_MARKER: "클로즈와 다중 행 플래시카드의 끝을 나타내는 문자", NOTES: "노트", REVIEW_PANE_ON_STARTUP: "Enable note review pane on startup", TAGS_TO_REVIEW: "리뷰에 사용할 태그", @@ -134,14 +136,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE: "리뷰를 위해 랜덤 노트를 엽니다.", OPEN_RANDOM_NOTE_DESC: "이 옵션이 꺼져있으면, 노트는 중요도(페이지 랭크)에 따라 정렬됩니다.", AUTO_NEXT_NOTE: "리뷰 후에 다음 노트를 자동으로 엽니다.", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "파일 메뉴에서의 리뷰 옵션을 비활성화 합니다. 예) 리뷰: Easy Good Hard", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "이 옵션을 비활성화 한 후, 명령 단축키를 이용해 리뷰하실 수 있습니다. 이 옵션을 변경한 후에 옵시디언을 새로고침 하십시오.", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "파일 메뉴에서 검토 옵션을 활성화하세요 (예: 검토: 쉬움, 좋음, 어려움)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "파일 메뉴에서 검토 옵션을 비활성화하면 플러그인 명령을 사용해 노트를 검토할 수 있으며, 정의된 경우에는 관련된 단축키도 사용할 수 있습니다.", MAX_N_DAYS_REVIEW_QUEUE: "오른쪽 패널에 표시할 최대 일수", MIN_ONE_DAY: "적어도 1이상이어야 합니다.", VALID_NUMBER_WARNING: "유효한 숫자를 입력해주세요.", UI_PREFERENCES: "사용자 인터페이스 기본 설정", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "덱 트리는 처음에 확장된 것으로 표시되어야 합니다.", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: "같은 카드에 중첩된 덱을 접으려면 이 옵션을 끄십시오. 같은 파일에 여러 덱에 속한 카드가 있는 경우 유용합니다.", @@ -164,7 +171,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "링크된 노트의 초기 ease에 대한 가중치가 적용된 ease의 최대 기여도입니다.", LOGGING: "로깅", - DISPLAY_DEBUG_INFO: "디버깅 정보를 개발자 콘솔에 표시하시겠습니까?", + DISPLAY_DEBUG_INFO: "디버깅 정보를 개발자 콘솔에 표시하시겠습니까", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "리뷰할 노트 대기열", diff --git a/src/lang/locale/pl.ts b/src/lang/locale/pl.ts index e1b6f8d9..c8f5913c 100644 --- a/src/lang/locale/pl.ts +++ b/src/lang/locale/pl.ts @@ -71,8 +71,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "Foldery do zignorowania", - FOLDERS_TO_IGNORE_DESC: `Wprowadź ścieżki folderów oddzielone nowymi liniami, np. Szablony Meta/Scripts. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "Fiszki", FLASHCARD_EASY_LABEL: "Tekst przycisku Łatwe", FLASHCARD_GOOD_LABEL: "Tekst przycisku Średnio trudne", @@ -129,6 +130,7 @@ Note that this setting is common to both Flashcards and Notes.`, MULTILINE_CARDS_SEPARATOR: "Separator dla kart zamaskowanych wieloliniowych", MULTILINE_REVERSED_CARDS_SEPARATOR: "Separator dla kart zamaskowanych odwróconych wieloliniowych", + MULTILINE_CARDS_END_MARKER: "Caracteres que denotam o fim de clozes e flashcards multilineares", NOTES: "Notatki", REVIEW_PANE_ON_STARTUP: "Włączyć panel przeglądu notatek przy starcie", TAGS_TO_REVIEW: "Tagi do przeglądu", @@ -138,14 +140,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE_DESC: "Po wyłączeniu tej opcji notatki są uporządkowane według istotności (PageRank).", AUTO_NEXT_NOTE: "Automatycznie otwierać następną notatkę po przeglądzie", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "Wyłączyć opcje przeglądu w menu pliku, tj. Przegląd: Łatwe Dobrze Trudne", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "Po wyłączeniu możesz przeglądać za pomocą skrótów klawiszowych. Po zmianie tej opcji konieczne jest ponowne załadowanie Obsidian.", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Wyłączyć opcje przeglądu w menu pliku, tj. Przeglądaj: Łatwe Dobrze Trudne", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "Jeśli wyłączysz opcje przeglądu w menu Plik, możesz przeglądać swoje notatki za pomocą poleceń wtyczki i, jeśli je zdefiniowałeś, przypisanych skrótów klawiszowych.", MAX_N_DAYS_REVIEW_QUEUE: "Maksymalna liczba dni do wyświetlenia w panelu prawym", MIN_ONE_DAY: "Liczba dni musi wynosić co najmniej 1.", VALID_NUMBER_WARNING: "Podaj prawidłową liczbę.", UI_PREFERENCES: "Preferencje interfejsu użytkownika", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "Podtalie powinny być początkowo wyświetlane rozszerzone", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: "Wyłącz to, aby zwinąć zagnieżdżone talie w tej samej karcie. Przydatne, jeśli karty należą do wielu talii w tym samym pliku.", @@ -168,7 +175,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "Maksymalny wkład ważonej łatwości połączonych notatek do początkowej łatwości.", LOGGING: "Logowanie", - DISPLAY_DEBUG_INFO: "Wyświetl informacje debugowania w konsoli deweloperskiej?", // sidebar.ts + DISPLAY_DEBUG_INFO: "Wyświetl informacje debugowania w konsoli deweloperskiej", // sidebar.ts + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", //sidebar.ts NOTES_REVIEW_QUEUE: "Kolejka przeglądu notatek", diff --git a/src/lang/locale/pt-br.ts b/src/lang/locale/pt-br.ts index 9f22b148..38103173 100644 --- a/src/lang/locale/pt-br.ts +++ b/src/lang/locale/pt-br.ts @@ -73,8 +73,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "Pastas para ignorar", - FOLDERS_TO_IGNORE_DESC: `Insira o caminho das pastas separado por quebras de linha ex: Templates Meta/Scripts. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "Flashcards", FLASHCARD_EASY_LABEL: "Texto do Botão de Fácil", FLASHCARD_GOOD_LABEL: "Texto do Botão de OK", @@ -128,6 +129,7 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "Separador para flashcards inline reversos", MULTILINE_CARDS_SEPARATOR: "Separador para flashcards de múltiplas linhas", MULTILINE_REVERSED_CARDS_SEPARATOR: "Separador para flashcards de múltiplas linhas reversos", + MULTILINE_CARDS_END_MARKER: "Caracteres que denotam o fim de clozes e flashcards multilinha", NOTES: "Notas", REVIEW_PANE_ON_STARTUP: "Habilitar painel de revisão de notas na inicialização", TAGS_TO_REVIEW: "Etiquetas para revisar", @@ -137,14 +139,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE_DESC: "Quando você desabilitar isso, as notas vão ser ordenadas por importância (PageRank).", AUTO_NEXT_NOTE: "Abrir a próxima nota automaticamente depois de uma revisão", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "Desabilitar opções de revisão no menu de arquivos ex: Revisão: Fácil OK Difícil", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "Depois de desabilitar, você pode revisar usando os atalhos de comando. Reinicie Obsidian depois de mudar isso.", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Ative as opções de revisão no menu Arquivo (ex.: Revisão: Fácil, OK, Difícil)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "Se você desativar as opções de revisão no menu Arquivo, poderá revisar suas anotações usando os comandos do plugin e, se os tiver definido, as teclas de atalho associadas.", MAX_N_DAYS_REVIEW_QUEUE: "Número máximo de dias para exibir no painel direito", MIN_ONE_DAY: "O número de dias deve ser pelo menos 1.", VALID_NUMBER_WARNING: "Por favor Insira um número válido.", UI_PREFERENCES: "Preferências de UI", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "Árvores de baralhos devem inicialmente ser exibidas como expandidas", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: @@ -170,7 +177,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: "Contribuição máxima da facilidade ponderada das notas linkadas à facilidade inicial.", LOGGING: "Logging", - DISPLAY_DEBUG_INFO: "Mostrar informação de debugging no console de desenvolvimento?", + DISPLAY_DEBUG_INFO: "Mostrar informação de debugging no console de desenvolvimento", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "Fila de Notas para Revisar", diff --git a/src/lang/locale/ru.ts b/src/lang/locale/ru.ts index 7acc51c1..0d5297da 100644 --- a/src/lang/locale/ru.ts +++ b/src/lang/locale/ru.ts @@ -80,8 +80,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "Игнорируемые папки", - FOLDERS_TO_IGNORE_DESC: `Укажите пути папок, каждый на своей строке, например: Templates Meta/Scripts. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "Карточки", FLASHCARD_EASY_LABEL: 'Текст кнопки "Легко"', FLASHCARD_GOOD_LABEL: 'Текст кнопки "Нормально"', @@ -136,6 +137,7 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "Разделитель для обратных внутристрочных карточек", MULTILINE_CARDS_SEPARATOR: "Разделитель для многострочных карточек", MULTILINE_REVERSED_CARDS_SEPARATOR: "Разделитель для обратных многострочных карточек", + MULTILINE_CARDS_END_MARKER: "Символы, обозначающие конец клозов и многострочных карточек", NOTES: "Заметки", REVIEW_PANE_ON_STARTUP: "Включить панель изучения карточек при запуске программы", TAGS_TO_REVIEW: "Теги для изучения", @@ -144,14 +146,19 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE: "Открыть случайную заметку для изучения", OPEN_RANDOM_NOTE_DESC: "Если выключить, то заметки будут отсортированы по важности (PageRank).", AUTO_NEXT_NOTE: "После изучения автоматически открывать следующую заметку", - DISABLE_FILE_MENU_REVIEW_OPTIONS: - "Выключить выбор сложности изучения в меню файла, т.е.: Изучение: Легко Нормально Сложно", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "После выключения вы сможете изучать карточки при помощи горячих клавиш. Перезагрузите Obsidian после изменения этой настройки.", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Включите параметры обзора в меню Файл (т.е.: Изучение: Легко, Нормально, Сложно)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "Если вы отключите параметры обзора в меню Файл, вы сможете просматривать свои заметки с помощью команд плагина и, если вы их задали, соответствующих горячих клавиш.", MAX_N_DAYS_REVIEW_QUEUE: "Наибольшее количество дней для отображение на панели справа", MIN_ONE_DAY: "Количество дней не меньше 1.", VALID_NUMBER_WARNING: "Пожалуйста, введите подходящее число.", UI_PREFERENCES: "Пользовательский интерфейс - Настройки", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "Деревья колод должны изначально отображаться как развернутые", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: @@ -177,7 +184,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB_DESC: 'Максимальный вклад среднего значения "Лёгкости" связанных заметок в начальную "Лёгкость".', LOGGING: "Журналирование", - DISPLAY_DEBUG_INFO: "Отображать отладочную информацию в консоли разработчика?", + DISPLAY_DEBUG_INFO: "Отображать отладочную информацию в консоли разработчика", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "Очередь заметок на повторение", diff --git a/src/lang/locale/sw.ts b/src/lang/locale/sw.ts index 81f55517..aa948731 100644 --- a/src/lang/locale/sw.ts +++ b/src/lang/locale/sw.ts @@ -1,3 +1,3 @@ -// Swahili +// Kiswahili export default {}; diff --git a/src/lang/locale/tr.ts b/src/lang/locale/tr.ts index d1db79b3..bd6e75c1 100644 --- a/src/lang/locale/tr.ts +++ b/src/lang/locale/tr.ts @@ -1,3 +1,208 @@ // Türkçe -export default {}; +export default { + // flashcard-modal.tsx + DECKS: "Desteler", + DUE_CARDS: "Güncel Kartlar", + NEW_CARDS: "Yeni Kartlar", + TOTAL_CARDS: "Toplam Kartlar", + BACK: "Geri", + SKIP: "Atla", + EDIT_CARD: "Kartı Düzenle", + RESET_CARD_PROGRESS: "Kartın ilerlemesini sıfırla", + HARD: "Zor", + GOOD: "Orta", + EASY: "Kolay", + SHOW_ANSWER: "Cevabı Göster", + CARD_PROGRESS_RESET: "Kartın ilerlemesi sıfırlandı.", + SAVE: "Kaydet", + CANCEL: "İptal", + NO_INPUT: "Girdi sağlanmadı.", + CURRENT_EASE_HELP_TEXT: "Mevcut Kolaylık: ", + CURRENT_INTERVAL_HELP_TEXT: "Mevcut Aralık: ", + CARD_GENERATED_FROM: "${notePath} kaynağından oluşturuldu.", + + // main.ts + OPEN_NOTE_FOR_REVIEW: "Gözden geçirmek için bir not aç", + REVIEW_CARDS: "Flash kartları gözden geçir", + REVIEW_DIFFICULTY_FILE_MENU: "Gözden Geçir: ${difficulty}", + REVIEW_NOTE_DIFFICULTY_CMD: "Notu ${difficulty} derecesiyle gözden geçir", + CRAM_ALL_CARDS: "Tüm destelerden yoğun tekrar yap", + REVIEW_ALL_CARDS: "Tüm notlardaki flash kartları gözden geçir", + REVIEW_CARDS_IN_NOTE: "Bu nottaki flash kartları gözden geçir", + CRAM_CARDS_IN_NOTE: "Bu nottaki flash kartları yoğun tekrar yap", + VIEW_STATS: "İstatistikleri görüntüle", + OPEN_REVIEW_QUEUE_VIEW: "Kenar çubuğunda Not Gözden Geçirme Sırasını aç", + STATUS_BAR: "Gözden Geçir: ${dueNotesCount} not, ${dueFlashcardsCount} kart güncel", + SYNC_TIME_TAKEN: "Senkronizasyon ${t}ms sürdü", + NOTE_IN_IGNORED_FOLDER: "Not, dışlanan klasörde kayıtlı (ayarları kontrol edin).", + PLEASE_TAG_NOTE: "Lütfen gözden geçirmek için notu uygun şekilde etiketleyin (ayarlar içinde).", + RESPONSE_RECEIVED: "Yanıt alındı.", + NO_DECK_EXISTS: "${deckName} adında bir deste yok", + ALL_CAUGHT_UP: "🏆 Şampiyon gibi bitirdin! 😄", + + // scheduling.ts + DAYS_STR_IVL: "${interval} gün", + MONTHS_STR_IVL: "${interval} ay", + YEARS_STR_IVL: "${interval} yıl", + DAYS_STR_IVL_MOBILE: "${interval}g", + MONTHS_STR_IVL_MOBILE: "${interval}a", + YEARS_STR_IVL_MOBILE: "${interval}y", + + // settings.ts + SETTINGS_HEADER: "Aralıklı Tekrar - Ayarlar", + GROUP_TAGS_FOLDERS: "Etiketler ve Klasörler", + GROUP_FLASHCARD_REVIEW: "Flash Kartları Gözden Geçirme", + GROUP_FLASHCARD_SEPARATORS: "Flash Kart Ayırıcıları", + GROUP_DATA_STORAGE: "Planlama Verilerinin Saklanması", + GROUP_FLASHCARDS_NOTES: "Flash Kartlar ve Notlar", + GROUP_CONTRIBUTING: "Katkıda Bulunma", + CHECK_WIKI: 'Daha fazla bilgi için wiki sayfasına göz atın.', + GITHUB_DISCUSSIONS: + 'Soru-cevap, geri bildirim ve genel tartışmalar için tartışmalar bölümüne göz atın.', + GITHUB_ISSUES: + 'Bir özellik isteğiniz ya da hata bildiriminiz varsa buradan bildirin.', + GITHUB_SOURCE_CODE: + 'Proje kaynak koduna GitHub üzerinden ulaşabilirsiniz.', + CODE_CONTRIBUTION_INFO: + 'Kod katkıları hakkında bilgi alın.', + TRANSLATION_CONTRIBUTION_INFO: + 'Eklentiyi kendi dilinize çevirmek hakkında bilgi için çeviri katkıları sayfasını ziyaret edin.', + PROJECT_CONTRIBUTIONS: + 'Bir özellik isteğiniz ya da hata bildiriminiz varsa buradan bildirin.', + FOLDERS_TO_IGNORE: "Yoksayılan Klasörler", + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", + FLASHCARDS: "Flash Kartlar", + FLASHCARD_EASY_LABEL: "Kolay Butonu Metni", + FLASHCARD_GOOD_LABEL: "Orta Butonu Metni", + FLASHCARD_HARD_LABEL: "Zor Butonu Metni", + FLASHCARD_EASY_DESC: '"Kolay" butonunun metnini özelleştirin', + FLASHCARD_GOOD_DESC: '"Orta" butonunun metnini özelleştirin', + FLASHCARD_HARD_DESC: '"Zor" butonunun metnini özelleştirin', + FLASHCARD_TAGS: "Flash Kart Etiketleri", + FLASHCARD_TAGS_DESC: + "Etiketleri boşluklar veya yeni satırlarla ayırarak girin, örneğin: #flashcards #deck2 #deck3.", + CONVERT_FOLDERS_TO_DECKS: "Klasörleri destelere ve alt destelere dönüştür?", + CONVERT_FOLDERS_TO_DECKS_DESC: + "Bu, yukarıdaki Flash Kart etiketleri seçeneğine bir alternatiftir.", + INLINE_SCHEDULING_COMMENTS: "Planlama yorumunu flash kartın son satırıyla aynı satıra kaydet?", + INLINE_SCHEDULING_COMMENTS_DESC: + "Bunu açmak, HTML yorumlarının liste biçimlendirmesini bozmamasını sağlar.", + BURY_SIBLINGS_TILL_NEXT_DAY: "Kardeş kartları bir sonraki güne kadar gizle?", + BURY_SIBLINGS_TILL_NEXT_DAY_DESC: + "Kardeş kartlar, aynı kart metninden üretilen kartlardır (örneğin gizlemeler).", + SHOW_CARD_CONTEXT: "Kartlarda bağlamı göster?", + SHOW_CARD_CONTEXT_DESC: "Örneğin: Başlık > Başlık 1 > Alt Başlık > ... > Alt Başlık", + CARD_MODAL_HEIGHT_PERCENT: "Flash Kart Yükseklik Yüzdesi", + CARD_MODAL_SIZE_PERCENT_DESC: + "Mobilde veya çok büyük resimleriniz varsa %100 olarak ayarlayın.", + RESET_DEFAULT: "Varsayılana sıfırla", + CARD_MODAL_WIDTH_PERCENT: "Flash Kart Genişlik Yüzdesi", + RANDOMIZE_CARD_ORDER: "İnceleme sırasında kart sırasını rastgele yap?", + REVIEW_CARD_ORDER_WITHIN_DECK: "İnceleme sırasında bir destede kartların görüntülenme sırası", + REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sıralı olarak (önce tüm yeni kartlar)", + REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sıralı olarak (önce tüm güncel kartlar)", + REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Rastgele olarak (önce tüm yeni kartlar)", + REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Rastgele olarak (önce tüm güncel kartlar)", + REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Rastgele desteden rastgele kart", + REVIEW_DECK_ORDER: "İnceleme sırasında destelerin görüntülenme sırası", + REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL: + "Sıralı olarak (Önceki destedeki tüm kartlar gözden geçirildikten sonra)", + REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM: + "Rastgele olarak (Önceki destedeki tüm kartlar gözden geçirildikten sonra)", + REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Rastgele desteden rastgele kart", + DISABLE_CLOZE_CARDS: "Gizli kartları devre dışı bırak?", + CONVERT_HIGHLIGHTS_TO_CLOZES: "==Vurgulanan== metni gizli kartlara dönüştür?", + CONVERT_BOLD_TEXT_TO_CLOZES: "**Kalın metni** gizli kartlara dönüştür?", + CONVERT_CURLY_BRACKETS_TO_CLOZES: "{{Kıvırcık parantezleri}} gizli kartlara dönüştür?", + INLINE_CARDS_SEPARATOR: "Satır içi flash kartlar için ayırıcı", + FIX_SEPARATORS_MANUALLY_WARNING: + "Bunu değiştirdikten sonra mevcut flash kartlarınızı manuel olarak düzenlemeniz gerektiğini unutmayın.", + INLINE_REVERSED_CARDS_SEPARATOR: "Satır içi ters flash kartlar için ayırıcı", + MULTILINE_CARDS_SEPARATOR: "Çok satırlı flash kartlar için ayırıcı", + MULTILINE_REVERSED_CARDS_SEPARATOR: "Çok satırlı ters flash kartlar için ayırıcı", + MULTILINE_CARDS_END_MARKER: + "Gizli kartlar ve çok satırlı flash kartların sonunu belirten karakterler", + NOTES: "Notlar", + REVIEW_PANE_ON_STARTUP: "Başlangıçta not inceleme panelini etkinleştir", + TAGS_TO_REVIEW: "Gözden geçirilecek etiketler", + TAGS_TO_REVIEW_DESC: + "Etiketleri boşluklar veya yeni satırlarla ayırarak girin, örneğin: #review #tag2 #tag3.", + OPEN_RANDOM_NOTE: "Gözden geçirmek için rastgele bir not aç", + OPEN_RANDOM_NOTE_DESC: "Bunu kapattığınızda, notlar önem sırasına göre sıralanır (PageRank).", + AUTO_NEXT_NOTE: "Bir incelemeden sonra otomatik olarak bir sonraki notu aç", + MAX_N_DAYS_REVIEW_QUEUE: "Sağ panelde gösterilecek maksimum gün sayısı", + MIN_ONE_DAY: "Gün sayısı en az 1 olmalıdır.", + VALID_NUMBER_WARNING: "Lütfen geçerli bir sayı girin.", + UI_PREFERENCES: "Kullanıcı Arayüzü Tercihleri", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", + ENABLE_FILE_MENU_REVIEW_OPTIONS: + "Enable the review options in the file menu (e.g. Review: Easy, Good, Hard)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "If you disable the review options in the file menu, you can review your notes using the plugin commands and, if you defined them, the associated command hotkeys.", + INITIALLY_EXPAND_SUBDECKS_IN_TREE: + "Deste ağaçları başlangıçta genişletilmiş olarak gösterilmeli mi", + INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: + "Bunu kapatın, aynı dosyada birçok desteye ait kartlarınız varsa iç içe desteleri daraltmak için kullanışlıdır.", + ALGORITHM: "Algoritma", + CHECK_ALGORITHM_WIKI: + 'Daha fazla bilgi için algoritma uygulamasına göz atın.', + BASE_EASE: "Temel kolaylık", + BASE_EASE_DESC: "minimum = 130, tercihen yaklaşık 250.", + BASE_EASE_MIN_WARNING: "Temel kolaylık en az 130 olmalıdır.", + LAPSE_INTERVAL_CHANGE: "Bir flash kartı/notu zor olarak incelediğinizde aralık değişikliği", + LAPSE_INTERVAL_CHANGE_DESC: "yeniAralık = eskiAralık * aralıkDeğişikliği / 100.", + EASY_BONUS: "Kolaylık Bonusu", + EASY_BONUS_DESC: + "Kolaylık bonusu, bir flash kartı/notu İyi ve Kolay yanıtladığınızda aralıklardaki farkı ayarlamanıza olanak tanır (minimum = %100).", + EASY_BONUS_MIN_WARNING: "Kolaylık bonusu en az %100 olmalıdır.", + MAX_INTERVAL: "Maksimum aralık (gün)", + MAX_INTERVAL_DESC: "Aralığa bir üst sınır koymanıza olanak tanır (varsayılan = 100 yıl).", + MAX_INTERVAL_MIN_WARNING: "Maksimum aralık en az 1 gün olmalıdır.", + MAX_LINK_CONTRIB: "Maksimum bağlantı katkısı", + MAX_LINK_CONTRIB_DESC: + "Bağlantılı notların ağırlıklı kolaylık değerinin başlangıç kolaylığına maksimum katkısı.", + LOGGING: "Kayıt tutma", + DISPLAY_DEBUG_INFO: "Geliştirici konsolunda hata ayıklama bilgilerini göster", + DISPLAY_PARSER_DEBUG_INFO: + "Ayrıştırıcı için hata ayıklama bilgilerini geliştirici konsolunda göster", + + // sidebar.ts + NOTES_REVIEW_QUEUE: "Not İnceleme Sırası", + CLOSE: "Kapat", + NEW: "Yeni", + YESTERDAY: "Dün", + TODAY: "Bugün", + TOMORROW: "Yarın", + + // stats-modal.tsx + STATS_TITLE: "İstatistikler", + MONTH: "Ay", + QUARTER: "Çeyrek", + YEAR: "Yıl", + LIFETIME: "Ömür Boyu", + FORECAST: "Tahmin", + FORECAST_DESC: "Gelecekte incelemeye alınacak kartların sayısı", + SCHEDULED: "Planlanmış", + DAYS: "Günler", + NUMBER_OF_CARDS: "Kart Sayısı", + REVIEWS_PER_DAY: "Ortalama: ${avg} inceleme/gün", + INTERVALS: "Aralıklar", + INTERVALS_DESC: "İncelemelerin tekrar gösterilme gecikmeleri", + COUNT: "Sayı", + INTERVALS_SUMMARY: "Ortalama aralık: ${avg}, En uzun aralık: ${longest}", + EASES: "Kolaylıklar", + EASES_SUMMARY: "Ortalama kolaylık: ${avgEase}", + CARD_TYPES: "Kart Türleri", + CARD_TYPES_DESC: "Bu, gömülü kartları da içerir (varsa)", + CARD_TYPE_NEW: "Yeni", + CARD_TYPE_YOUNG: "Genç", + CARD_TYPE_MATURE: "Olgun", + CARD_TYPES_SUMMARY: "Toplam kart: ${totalCardsCount}", +}; diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 7ecf389b..96635495 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -71,8 +71,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "忽略此文件夹", - FOLDERS_TO_IGNORE_DESC: `输入文件夹路径,用新建行分隔,例如:Templates Meta/Scripts. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "卡片", FLASHCARD_EASY_LABEL: "“简单”按钮文本", FLASHCARD_GOOD_LABEL: "“记得”按钮文本", @@ -117,6 +118,7 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "单行翻转卡片的分隔符", MULTILINE_CARDS_SEPARATOR: "多行卡片的分隔符", MULTILINE_REVERSED_CARDS_SEPARATOR: "多行翻转卡片的分隔符", + MULTILINE_CARDS_END_MARKER: "表示填空和多行闪卡结束的字符", NOTES: "笔记", REVIEW_PANE_ON_STARTUP: "启动时开启笔记复习窗格", TAGS_TO_REVIEW: "复习标签", @@ -124,13 +126,18 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE: "复习随机笔记", OPEN_RANDOM_NOTE_DESC: "关闭此选项,笔记将以重要度(PageRank)排序。", AUTO_NEXT_NOTE: "复习后自动打开下一个笔记", - DISABLE_FILE_MENU_REVIEW_OPTIONS: "关闭文件选单中的复习选项 例如:复习:简单 记得 较难", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: - "关闭此选项后你可以使用快捷键开始复习。重新启动Obsidian使本选项生效。", + ENABLE_FILE_MENU_REVIEW_OPTIONS: "请在文件菜单中启用复习选项(例如:复习:简单、良好、困难", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "如果您在文件菜单中禁用复习选项,您可以使用插件命令来复习笔记,如果您定义了相关快捷键,也可以使用它们。", MAX_N_DAYS_REVIEW_QUEUE: "右边栏中显示的最大天数", MIN_ONE_DAY: "天数最小值为1", VALID_NUMBER_WARNING: "请输入有效的数字。", UI_PREFERENCES: "用户界面首选项", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "甲板树最初应显示为展开", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: "关闭此选项可折叠同一张卡片中的嵌套牌组。如果您的卡片属于同一文件中的许多套牌,则很有用。", @@ -150,7 +157,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB: "最大链接收益", MAX_LINK_CONTRIB_DESC: "链接笔记的加权掌握程度对原始掌握程度的最大贡献。", LOGGING: "记录中", - DISPLAY_DEBUG_INFO: "在开发者控制台中显示调试信息?", + DISPLAY_DEBUG_INFO: "在开发者控制台中显示调试信息", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "笔记复习序列", diff --git a/src/lang/locale/zh-tw.ts b/src/lang/locale/zh-tw.ts index 4f001082..f2a219e3 100644 --- a/src/lang/locale/zh-tw.ts +++ b/src/lang/locale/zh-tw.ts @@ -71,8 +71,9 @@ export default { PROJECT_CONTRIBUTIONS: 'Raise an issue here if you have a feature request or a bug-report', FOLDERS_TO_IGNORE: "忽略此資料夾", - FOLDERS_TO_IGNORE_DESC: `輸入資料夾路徑(用換行字元分隔),例如:Templates Meta/Scripts. -Note that this setting is common to both Flashcards and Notes.`, + FOLDERS_TO_IGNORE_DESC: + "Enter folder paths or glob patterns on separate lines e.g. Templates Meta/Scripts or **/*.excalidraw.md. This setting is common to both flashcards and notes.", + OBSIDIAN_INTEGRATION: "Integration into Obsidian", FLASHCARDS: "卡片", FLASHCARD_EASY_LABEL: "簡單按鈕文字", FLASHCARD_GOOD_LABEL: "記得按鈕文字", @@ -117,6 +118,7 @@ Note that this setting is common to both Flashcards and Notes.`, INLINE_REVERSED_CARDS_SEPARATOR: "單行反轉卡片的分隔字元", MULTILINE_CARDS_SEPARATOR: "多行卡片的分隔字元", MULTILINE_REVERSED_CARDS_SEPARATOR: "多行翻轉卡片的分隔字元", + MULTILINE_CARDS_END_MARKER: "表示填空和多行闪卡结束的字符", NOTES: "筆記", REVIEW_PANE_ON_STARTUP: "啟動時開啟筆記復習窗格", TAGS_TO_REVIEW: "復習標籤", @@ -124,12 +126,18 @@ Note that this setting is common to both Flashcards and Notes.`, OPEN_RANDOM_NOTE: "復習隨機筆記", OPEN_RANDOM_NOTE_DESC: "關閉此選項,筆記將以重要度(PageRank)排序。", AUTO_NEXT_NOTE: "復習後自動打開下一個筆記", - DISABLE_FILE_MENU_REVIEW_OPTIONS: "關閉檔案選單中的復習選項 例如:復習:簡單 記得 較難", - DISABLE_FILE_MENU_REVIEW_OPTIONS_DESC: "關閉檔案選單的復習選項,例如:復習: 簡單 記得 較難。", + ENABLE_FILE_MENU_REVIEW_OPTIONS: "請在檔案選單中啟用檢視選項(例如:檢視:簡單、記得、較難)", + ENABLE_FILE_MENU_REVIEW_OPTIONS_DESC: + "如果您在檔案選單中停用檢視選項,您可以使用插件指令檢視筆記,如果有設定,也可以使用相關的快捷鍵。", MAX_N_DAYS_REVIEW_QUEUE: "右邊面板顯示的最大天數", MIN_ONE_DAY: "天數最小值為1", VALID_NUMBER_WARNING: "請輸入有效的數字。", UI_PREFERENCES: "用戶介面首選項", + SHOW_STATUS_BAR: "Show status bar", + SHOW_STATUS_BAR_DESC: + "Turn this off to hide the flashcard's review status in Obsidian's status bar", + SHOW_RIBBON_ICON: "Show icon in the ribbon bar", + SHOW_RIBBON_ICON_DESC: "Turn this off to hide the plugin icon from Obsidian's ribbon bar", INITIALLY_EXPAND_SUBDECKS_IN_TREE: "牌組樹最初應顯示為展開", INITIALLY_EXPAND_SUBDECKS_IN_TREE_DESC: "關閉此選項可摺疊同一張卡片中的巢狀牌組。如果您的卡片屬於同一檔案中的許多套牌,則很有用。", @@ -149,7 +157,9 @@ Note that this setting is common to both Flashcards and Notes.`, MAX_LINK_CONTRIB: "最大鏈接貢獻", MAX_LINK_CONTRIB_DESC: "鏈接筆記的加權掌握程度對原始掌握程度的最大貢獻。", LOGGING: "記錄中", - DISPLAY_DEBUG_INFO: "在開發者控制台中顯示除錯資訊?", + DISPLAY_DEBUG_INFO: "在開發者控制台中顯示除錯資訊", + DISPLAY_PARSER_DEBUG_INFO: + "Display debugging information for the parser on the developer console", // sidebar.ts NOTES_REVIEW_QUEUE: "筆記復習序列", diff --git a/src/main.ts b/src/main.ts index 43f33549..faa40bb1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,181 +1,183 @@ -import { - Notice, - Plugin, - TAbstractFile, - TFile, - getAllTags, - FrontMatterCache, - WorkspaceLeaf, -} from "obsidian"; -import * as graph from "pagerank.js"; - -import { SRSettingTab, SRSettings, DEFAULT_SETTINGS, upgradeSettings } from "src/settings"; -import { FlashcardModal } from "src/gui/FlashcardModal"; -import { StatsModal } from "src/gui/StatsModal"; -import { ReviewQueueListView, REVIEW_QUEUE_VIEW_TYPE } from "src/gui/Sidebar"; -import { ReviewResponse, schedule } from "src/scheduling"; -import { YAML_FRONT_MATTER_REGEX, SCHEDULING_INFO_REGEX } from "src/constants"; -import { ReviewDeck, ReviewDeckSelectionModal } from "src/ReviewDeck"; -import { t } from "src/lang/helpers"; -import { appIcon } from "src/icons/appicon"; -import { TopicPath } from "./TopicPath"; -import { CardListType, Deck, DeckTreeFilter } from "./Deck"; -import { Stats } from "./stats"; -import { - FlashcardReviewMode, - FlashcardReviewSequencer as FlashcardReviewSequencer, - IFlashcardReviewSequencer as IFlashcardReviewSequencer, -} from "./FlashcardReviewSequencer"; +import { Menu, Notice, Plugin, TAbstractFile, TFile, WorkspaceLeaf } from "obsidian"; + +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { SrsAlgorithm } from "src/algorithms/base/srs-algorithm"; +import { ObsidianVaultNoteLinkInfoFinder } from "src/algorithms/osr/obsidian-vault-notelink-info-finder"; +import { SrsAlgorithm_Osr } from "src/algorithms/osr/srs-algorithm-osr"; +import { OsrAppCore } from "src/app-core"; +import { DataStoreAlgorithm } from "src/data-store-algorithm/data-store-algorithm"; +import { DataStoreInNote_AlgorithmOsr } from "src/data-store-algorithm/data-store-in-note-algorithm-osr"; +import { DataStore } from "src/data-stores/base/data-store"; +import { StoreInNote } from "src/data-stores/store-in-note/note"; +import { CardListType, Deck, DeckTreeFilter } from "src/deck"; import { CardOrder, + DeckOrder, DeckTreeIterator, IDeckTreeIterator, IIteratorOrder, - DeckOrder, -} from "./DeckTreeIterator"; -import { CardScheduleCalculator } from "./CardSchedule"; -import { Note } from "./Note"; -import { NoteFileLoader } from "./NoteFileLoader"; -import { ISRFile, SrTFile as SrTFile } from "./SRFile"; -import { NoteEaseCalculator } from "./NoteEaseCalculator"; -import { DeckTreeStatsCalculator } from "./DeckTreeStatsCalculator"; -import { NoteEaseList } from "./NoteEaseList"; -import { QuestionPostponementList } from "./QuestionPostponementList"; -import { TextDirection } from "./util/TextDirection"; -import { convertToStringOrEmpty } from "./util/utils"; -import { isEqualOrSubPath } from "./util/utils"; - -interface PluginData { - settings: SRSettings; - buryDate: string; - // hashes of card texts - // should work as long as user doesn't modify card's text - // which covers most of the cases - buryList: string[]; - historyDeck: string | null; -} - -const DEFAULT_DATA: PluginData = { - settings: DEFAULT_SETTINGS, - buryDate: "", - buryList: [], - historyDeck: null, -}; - -export interface SchedNote { - note: TFile; - dueUnix: number; -} - -export interface LinkStat { - sourcePath: string; - linkCount: number; -} +} from "src/deck-tree-iterator"; +import { + FlashcardReviewMode, + FlashcardReviewSequencer, + IFlashcardReviewSequencer, +} from "src/flashcard-review-sequencer"; +import { FlashcardModal } from "src/gui/flashcard-modal"; +import { REVIEW_QUEUE_VIEW_TYPE } from "src/gui/review-queue-list-view"; +import { OsrSidebar } from "src/gui/sidebar"; +import { StatsModal } from "src/gui/stats-modal"; +import { appIcon } from "src/icons/app-icon"; +import { t } from "src/lang/helpers"; +import { NextNoteReviewHandler } from "src/next-note-review-handler"; +import { Note } from "src/note"; +import { NoteFileLoader } from "src/note-file-loader"; +import { generateParser, setDebugParser } from "src/parser"; +import { DEFAULT_DATA, PluginData } from "src/plugin-data"; +import { QuestionPostponementList } from "src/question-postponement-list"; +import { + DEFAULT_SETTINGS, + SettingsUtil, + SRSettings, + SRSettingTab, + upgradeSettings, +} from "src/settings"; +import { ISRFile, SrTFile as SrTFile } from "src/sr-file"; +import { TopicPath } from "src/topic-path"; +import { convertToStringOrEmpty, TextDirection } from "src/utils/strings"; export default class SRPlugin extends Plugin { - private statusBar: HTMLElement; - private reviewQueueView: ReviewQueueListView; public data: PluginData; - public syncLock = false; + private osrAppCore: OsrAppCore; + private osrSidebar: OsrSidebar; + private nextNoteReviewHandler: NextNoteReviewHandler; - public reviewDecks: { [deckKey: string]: ReviewDeck } = {}; - public lastSelectedReviewDeck: string; + private debouncedGenerateParserTimeout: number | null = null; - public easeByPath: NoteEaseList; - private questionPostponementList: QuestionPostponementList; - private incomingLinks: Record = {}; - private pageranks: Record = {}; - private dueNotesCount = 0; - public dueDatesNotes: Record = {}; // Record<# of days in future, due count> - - public deckTree: Deck = new Deck("root", null); - private remainingDeckTree: Deck; - public cardStats: Stats; + private ribbonIcon: HTMLElement | null = null; + private statusBar: HTMLElement | null = null; + private fileMenuHandler: ( + menu: Menu, + file: TAbstractFile, + source: string, + leaf?: WorkspaceLeaf, + ) => void; async onload(): Promise { await this.loadPluginData(); - this.easeByPath = new NoteEaseList(this.data.settings); - this.questionPostponementList = new QuestionPostponementList( + + this.initLogicClasses(); + + this.initGuiItems(); + } + + private initLogicClasses() { + const questionPostponementList: QuestionPostponementList = new QuestionPostponementList( this, this.data.settings, this.data.buryList, ); + const osrNoteLinkInfoFinder: ObsidianVaultNoteLinkInfoFinder = + new ObsidianVaultNoteLinkInfoFinder(this.app.metadataCache); + + this.osrAppCore = new OsrAppCore(this.app); + this.osrAppCore.init( + questionPostponementList, + osrNoteLinkInfoFinder, + this.data.settings, + this.onOsrVaultDataChanged.bind(this), + ); + } + + private initGuiItems() { + this.nextNoteReviewHandler = new NextNoteReviewHandler( + this.app, + this.data.settings, + this.app.workspace, + this.osrAppCore.noteReviewQueue, + ); appIcon(); - this.statusBar = this.addStatusBarItem(); - this.statusBar.classList.add("mod-clickable"); - this.statusBar.setAttribute("aria-label", t("OPEN_NOTE_FOR_REVIEW")); - this.statusBar.setAttribute("aria-label-position", "top"); - this.statusBar.addEventListener("click", async () => { - if (!this.syncLock) { - await this.sync(); - this.reviewNextNoteModal(); - } - }); + this.showStatusBar(this.data.settings.showStatusBar); - this.addRibbonIcon("SpacedRepIcon", t("REVIEW_CARDS"), async () => { - if (!this.syncLock) { - await this.sync(); - this.openFlashcardModal( - this.deckTree, - this.remainingDeckTree, - FlashcardReviewMode.Review, - ); - } + this.showRibbonIcon(this.data.settings.showRibbonIcon); + + this.showFileMenuItems(!this.data.settings.disableFileMenuReviewOptions); + + this.addPluginCommands(); + + this.addSettingTab(new SRSettingTab(this.app, this)); + + this.osrSidebar = new OsrSidebar(this, this.data.settings, this.nextNoteReviewHandler); + this.app.workspace.onLayoutReady(async () => { + await this.osrSidebar.init(); + setTimeout(async () => { + if (!this.osrAppCore.syncLock) { + await this.sync(); + } + }, 2000); }); + } - if (!this.data.settings.disableFileMenuReviewOptions) { - this.registerEvent( - this.app.workspace.on("file-menu", (menu, fileish: TAbstractFile) => { - if (fileish instanceof TFile && fileish.extension === "md") { - menu.addItem((item) => { - item.setTitle( - t("REVIEW_DIFFICULTY_FILE_MENU", { - difficulty: this.data.settings.flashcardEasyText, - }), - ) - .setIcon("SpacedRepIcon") - .onClick(() => { - this.saveReviewResponse(fileish, ReviewResponse.Easy); - }); - }); - - menu.addItem((item) => { - item.setTitle( - t("REVIEW_DIFFICULTY_FILE_MENU", { - difficulty: this.data.settings.flashcardGoodText, - }), - ) - .setIcon("SpacedRepIcon") - .onClick(() => { - this.saveReviewResponse(fileish, ReviewResponse.Good); - }); - }); - - menu.addItem((item) => { - item.setTitle( - t("REVIEW_DIFFICULTY_FILE_MENU", { - difficulty: this.data.settings.flashcardHardText, - }), - ) - .setIcon("SpacedRepIcon") - .onClick(() => { - this.saveReviewResponse(fileish, ReviewResponse.Hard); - }); - }); - } - }), - ); + showFileMenuItems(status: boolean) { + // define the handler if it was not defined yet + if (this.fileMenuHandler === undefined) { + this.fileMenuHandler = (menu, fileish: TAbstractFile) => { + if (fileish instanceof TFile && fileish.extension === "md") { + menu.addItem((item) => { + item.setTitle( + t("REVIEW_DIFFICULTY_FILE_MENU", { + difficulty: this.data.settings.flashcardEasyText, + }), + ) + .setIcon("SpacedRepIcon") + .onClick(() => { + this.saveNoteReviewResponse(fileish, ReviewResponse.Easy); + }); + }); + + menu.addItem((item) => { + item.setTitle( + t("REVIEW_DIFFICULTY_FILE_MENU", { + difficulty: this.data.settings.flashcardGoodText, + }), + ) + .setIcon("SpacedRepIcon") + .onClick(() => { + this.saveNoteReviewResponse(fileish, ReviewResponse.Good); + }); + }); + + menu.addItem((item) => { + item.setTitle( + t("REVIEW_DIFFICULTY_FILE_MENU", { + difficulty: this.data.settings.flashcardHardText, + }), + ) + .setIcon("SpacedRepIcon") + .onClick(() => { + this.saveNoteReviewResponse(fileish, ReviewResponse.Hard); + }); + }); + } + }; } + if (status) { + this.registerEvent(this.app.workspace.on("file-menu", this.fileMenuHandler)); + } else { + this.app.workspace.off("file-menu", this.fileMenuHandler); + } + } + + private addPluginCommands() { this.addCommand({ id: "srs-note-review-open-note", name: t("OPEN_NOTE_FOR_REVIEW"), callback: async () => { - if (!this.syncLock) { + if (!this.osrAppCore.syncLock) { await this.sync(); - this.reviewNextNoteModal(); + this.nextNoteReviewHandler.reviewNextNoteModal(); } }, }); @@ -188,7 +190,7 @@ export default class SRPlugin extends Plugin { callback: () => { const openFile: TFile | null = this.app.workspace.getActiveFile(); if (openFile && openFile.extension === "md") { - this.saveReviewResponse(openFile, ReviewResponse.Easy); + this.saveNoteReviewResponse(openFile, ReviewResponse.Easy); } }, }); @@ -201,7 +203,7 @@ export default class SRPlugin extends Plugin { callback: () => { const openFile: TFile | null = this.app.workspace.getActiveFile(); if (openFile && openFile.extension === "md") { - this.saveReviewResponse(openFile, ReviewResponse.Good); + this.saveNoteReviewResponse(openFile, ReviewResponse.Good); } }, }); @@ -214,7 +216,7 @@ export default class SRPlugin extends Plugin { callback: () => { const openFile: TFile | null = this.app.workspace.getActiveFile(); if (openFile && openFile.extension === "md") { - this.saveReviewResponse(openFile, ReviewResponse.Hard); + this.saveNoteReviewResponse(openFile, ReviewResponse.Hard); } }, }); @@ -223,11 +225,11 @@ export default class SRPlugin extends Plugin { id: "srs-review-flashcards", name: t("REVIEW_ALL_CARDS"), callback: async () => { - if (!this.syncLock) { + if (!this.osrAppCore.syncLock) { await this.sync(); this.openFlashcardModal( - this.deckTree, - this.remainingDeckTree, + this.osrAppCore.reviewableDeckTree, + this.osrAppCore.remainingDeckTree, FlashcardReviewMode.Review, ); } @@ -239,7 +241,11 @@ export default class SRPlugin extends Plugin { name: t("CRAM_ALL_CARDS"), callback: async () => { await this.sync(); - this.openFlashcardModal(this.deckTree, this.deckTree, FlashcardReviewMode.Cram); + this.openFlashcardModal( + this.osrAppCore.reviewableDeckTree, + this.osrAppCore.reviewableDeckTree, + FlashcardReviewMode.Cram, + ); }, }); @@ -269,9 +275,9 @@ export default class SRPlugin extends Plugin { id: "srs-view-stats", name: t("VIEW_STATS"), callback: async () => { - if (!this.syncLock) { + if (!this.osrAppCore.syncLock) { await this.sync(); - new StatsModal(this.app, this).open(); + new StatsModal(this.app, this.osrAppCore).open(); } }, }); @@ -280,20 +286,9 @@ export default class SRPlugin extends Plugin { id: "srs-open-review-queue-view", name: t("OPEN_REVIEW_QUEUE_VIEW"), callback: async () => { - await this.openReviewQueueView(); + await this.osrSidebar.openReviewQueueView(); }, }); - - this.addSettingTab(new SRSettingTab(this.app, this)); - - this.app.workspace.onLayoutReady(async () => { - await this.initReviewQueueView(); - setTimeout(async () => { - if (!this.syncLock) { - await this.sync(); - } - }, 2000); - }); } onunload(): void { @@ -309,7 +304,7 @@ export default class SRPlugin extends Plugin { const deckTree = new Deck("root", null); note.appendCardsToDeck(deckTree); const remainingDeckTree = DeckTreeFilter.filterForRemainingCards( - this.questionPostponementList, + this.osrAppCore.questionPostponementList, deckTree, reviewMode, ); @@ -321,24 +316,21 @@ export default class SRPlugin extends Plugin { remainingDeckTree: Deck, reviewMode: FlashcardReviewMode, ): void { - const deckIterator = SRPlugin.createDeckTreeIterator(this.data.settings, remainingDeckTree); - const cardScheduleCalculator = new CardScheduleCalculator( - this.data.settings, - this.easeByPath, - ); + const deckIterator = SRPlugin.createDeckTreeIterator(this.data.settings); const reviewSequencer: IFlashcardReviewSequencer = new FlashcardReviewSequencer( reviewMode, deckIterator, this.data.settings, - cardScheduleCalculator, - this.questionPostponementList, + SrsAlgorithm.getInstance(), + this.osrAppCore.questionPostponementList, + this.osrAppCore.dueDateFlashcardHistogram, ); reviewSequencer.setDeckTree(fullDeckTree, remainingDeckTree); new FlashcardModal(this.app, this, this.data.settings, reviewSequencer, reviewMode).open(); } - private static createDeckTreeIterator(settings: SRSettings, baseDeck: Deck): IDeckTreeIterator { + private static createDeckTreeIterator(settings: SRSettings): IDeckTreeIterator { let cardOrder: CardOrder = CardOrder[settings.flashcardCardOrder as keyof typeof CardOrder]; if (cardOrder === undefined) cardOrder = CardOrder.DueFirstSequential; let deckOrder: DeckOrder = DeckOrder[settings.flashcardDeckOrder as keyof typeof DeckOrder]; @@ -348,156 +340,21 @@ export default class SRPlugin extends Plugin { deckOrder, cardOrder, }; - return new DeckTreeIterator(iteratorOrder, baseDeck); + return new DeckTreeIterator(iteratorOrder, null); } async sync(): Promise { - if (this.syncLock) { + if (this.osrAppCore.syncLock) { return; } - this.syncLock = true; - - // reset notes stuff - graph.reset(); - this.easeByPath = new NoteEaseList(this.data.settings); - this.incomingLinks = {}; - this.pageranks = {}; - this.reviewDecks = {}; - - // reset flashcards stuff - const fullDeckTree = new Deck("root", null); const now = window.moment(Date.now()); - const todayDate: string = now.format("YYYY-MM-DD"); - // clear bury list if we've changed dates - if (todayDate !== this.data.buryDate) { - this.data.buryDate = todayDate; - this.questionPostponementList.clear(); - - // The following isn't needed for plug-in functionality; but can aid during debugging - await this.savePluginData(); - } - - const notes: TFile[] = this.app.vault.getMarkdownFiles(); - for (const noteFile of notes) { - if ( - this.data.settings.noteFoldersToIgnore.some((folder) => - isEqualOrSubPath(noteFile.path, folder), - ) - ) { - continue; - } - - if (this.incomingLinks[noteFile.path] === undefined) { - this.incomingLinks[noteFile.path] = []; - } - - const links = this.app.metadataCache.resolvedLinks[noteFile.path] || {}; - for (const targetPath in links) { - if (this.incomingLinks[targetPath] === undefined) - this.incomingLinks[targetPath] = []; - - // markdown files only - if (targetPath.split(".").pop().toLowerCase() === "md") { - this.incomingLinks[targetPath].push({ - sourcePath: noteFile.path, - linkCount: links[targetPath], - }); - - graph.link(noteFile.path, targetPath, links[targetPath]); - } - } - - const note: Note = await this.loadNote(noteFile); - if (note.questionList.length > 0) { - const flashcardsInNoteAvgEase: number = NoteEaseCalculator.Calculate( - note, - this.data.settings, - ); - note.appendCardsToDeck(fullDeckTree); - - if (flashcardsInNoteAvgEase > 0) { - this.easeByPath.setEaseForPath(note.filePath, flashcardsInNoteAvgEase); - } - } - const fileCachedData = this.app.metadataCache.getFileCache(noteFile) || {}; - - const frontmatter: FrontMatterCache | Record = - fileCachedData.frontmatter || {}; - const tags = getAllTags(fileCachedData) || []; - - let shouldIgnore = true; - const matchedNoteTags = []; - - for (const tagToReview of this.data.settings.tagsToReview) { - if (tags.some((tag) => tag === tagToReview || tag.startsWith(tagToReview + "/"))) { - if (!Object.prototype.hasOwnProperty.call(this.reviewDecks, tagToReview)) { - this.reviewDecks[tagToReview] = new ReviewDeck(tagToReview); - } - matchedNoteTags.push(tagToReview); - shouldIgnore = false; - break; - } - } - if (shouldIgnore) { - continue; - } - - // file has no scheduling information - if ( - !( - Object.prototype.hasOwnProperty.call(frontmatter, "sr-due") && - Object.prototype.hasOwnProperty.call(frontmatter, "sr-interval") && - Object.prototype.hasOwnProperty.call(frontmatter, "sr-ease") - ) - ) { - for (const matchedNoteTag of matchedNoteTags) { - this.reviewDecks[matchedNoteTag].newNotes.push(noteFile); - } - continue; - } - - const dueUnix: number = window - .moment(frontmatter["sr-due"], ["YYYY-MM-DD", "DD-MM-YYYY", "ddd MMM DD YYYY"]) - .valueOf(); - - let ease: number; - if (this.easeByPath.hasEaseForPath(noteFile.path)) { - ease = (this.easeByPath.getEaseByPath(noteFile.path) + frontmatter["sr-ease"]) / 2; - } else { - ease = frontmatter["sr-ease"]; - } - this.easeByPath.setEaseForPath(noteFile.path, ease); - - // schedule the note - for (const matchedNoteTag of matchedNoteTags) { - this.reviewDecks[matchedNoteTag].scheduledNotes.push({ note: noteFile, dueUnix }); - } - } - - graph.rank(0.85, 0.000001, (node: string, rank: number) => { - this.pageranks[node] = rank * 10000; - }); + this.osrAppCore.defaultTextDirection = this.getObsidianRtlSetting(); - // Reviewable cards are all except those with the "edit later" tag - this.deckTree = DeckTreeFilter.filterForReviewableCards(fullDeckTree); - - // sort the deck names - this.deckTree.sortSubdecksList(); - this.remainingDeckTree = DeckTreeFilter.filterForRemainingCards( - this.questionPostponementList, - this.deckTree, - FlashcardReviewMode.Review, - ); - const calc: DeckTreeStatsCalculator = new DeckTreeStatsCalculator(); - this.cardStats = calc.calculate(this.deckTree); - - if (this.data.settings.showDebugMessages) { - console.log(`SR: ${t("EASES")}`, this.easeByPath.dict); - console.log(`SR: ${t("DECKS")}`, this.deckTree); - } + await this.osrAppCore.loadVault(); if (this.data.settings.showDebugMessages) { + console.log(`SR: ${t("DECKS")}`, this.osrAppCore.reviewableDeckTree); console.log( "SR: " + t("SYNC_TIME_TAKEN", { @@ -505,48 +362,19 @@ export default class SRPlugin extends Plugin { }), ); } - - this.updateAndSortDueNotes(); - - this.syncLock = false; } - private updateAndSortDueNotes() { - this.dueNotesCount = 0; - this.dueDatesNotes = {}; - - const now = window.moment(Date.now()); - Object.values(this.reviewDecks).forEach((reviewDeck: ReviewDeck) => { - reviewDeck.dueNotesCount = 0; - reviewDeck.scheduledNotes.forEach((scheduledNote: SchedNote) => { - if (scheduledNote.dueUnix <= now.valueOf()) { - reviewDeck.dueNotesCount++; - this.dueNotesCount++; - } - - const nDays: number = Math.ceil( - (scheduledNote.dueUnix - now.valueOf()) / (24 * 3600 * 1000), - ); - if (!Object.prototype.hasOwnProperty.call(this.dueDatesNotes, nDays)) { - this.dueDatesNotes[nDays] = 0; - } - this.dueDatesNotes[nDays]++; - }); - - reviewDeck.sortNotes(this.pageranks); - }); - + private onOsrVaultDataChanged() { this.statusBar.setText( t("STATUS_BAR", { - dueNotesCount: this.dueNotesCount, - dueFlashcardsCount: this.remainingDeckTree.getDistinctCardCount( + dueNotesCount: this.osrAppCore.noteReviewQueue.dueNotesCount, + dueFlashcardsCount: this.osrAppCore.remainingDeckTree.getCardCount( CardListType.All, true, ), }), ); - - if (this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE)) this.reviewQueueView.redraw(); + this.osrSidebar.redraw(); } async loadNote(noteFile: TFile): Promise { @@ -575,225 +403,28 @@ export default class SRPlugin extends Plugin { return convertToStringOrEmpty(v) == "true" ? TextDirection.Rtl : TextDirection.Ltr; } - async saveReviewResponse(note: TFile, response: ReviewResponse): Promise { - const fileCachedData = this.app.metadataCache.getFileCache(note) || {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const frontmatter: FrontMatterCache | Record = - fileCachedData.frontmatter || {}; - - const tags = getAllTags(fileCachedData) || []; - if ( - this.data.settings.noteFoldersToIgnore.some((folder) => - isEqualOrSubPath(note.path, folder), - ) - ) { + async saveNoteReviewResponse(note: TFile, response: ReviewResponse): Promise { + const noteSrTFile: ISRFile = this.createSrTFile(note); + + if (SettingsUtil.isPathInNoteIgnoreFolder(this.data.settings, note.path)) { new Notice(t("NOTE_IN_IGNORED_FOLDER")); return; } - let shouldIgnore = true; - for (const tag of tags) { - if ( - this.data.settings.tagsToReview.some( - (tagToReview) => tag === tagToReview || tag.startsWith(tagToReview + "/"), - ) - ) { - shouldIgnore = false; - break; - } - } - - if (shouldIgnore) { + const tags = noteSrTFile.getAllTagsFromCache(); + if (!SettingsUtil.isAnyTagANoteReviewTag(this.data.settings, tags)) { new Notice(t("PLEASE_TAG_NOTE")); return; } - let fileText: string = await this.app.vault.read(note); - let ease: number, interval: number, delayBeforeReview: number; - const now: number = Date.now(); - // new note - if ( - !( - Object.prototype.hasOwnProperty.call(frontmatter, "sr-due") && - Object.prototype.hasOwnProperty.call(frontmatter, "sr-interval") && - Object.prototype.hasOwnProperty.call(frontmatter, "sr-ease") - ) - ) { - let linkTotal = 0, - linkPGTotal = 0, - totalLinkCount = 0; - - for (const statObj of this.incomingLinks[note.path] || []) { - const ease: number = this.easeByPath.getEaseByPath(statObj.sourcePath); - if (ease) { - linkTotal += statObj.linkCount * this.pageranks[statObj.sourcePath] * ease; - linkPGTotal += this.pageranks[statObj.sourcePath] * statObj.linkCount; - totalLinkCount += statObj.linkCount; - } - } - - const outgoingLinks = this.app.metadataCache.resolvedLinks[note.path] || {}; - for (const linkedFilePath in outgoingLinks) { - const ease: number = this.easeByPath.getEaseByPath(linkedFilePath); - if (ease) { - linkTotal += - outgoingLinks[linkedFilePath] * this.pageranks[linkedFilePath] * ease; - linkPGTotal += this.pageranks[linkedFilePath] * outgoingLinks[linkedFilePath]; - totalLinkCount += outgoingLinks[linkedFilePath]; - } - } - - const linkContribution: number = - this.data.settings.maxLinkFactor * - Math.min(1.0, Math.log(totalLinkCount + 0.5) / Math.log(64)); - ease = - (1.0 - linkContribution) * this.data.settings.baseEase + - (totalLinkCount > 0 - ? (linkContribution * linkTotal) / linkPGTotal - : linkContribution * this.data.settings.baseEase); - // add note's average flashcard ease if available - if (this.easeByPath.hasEaseForPath(note.path)) { - ease = (ease + this.easeByPath.getEaseByPath(note.path)) / 2; - } - ease = Math.round(ease); - interval = 1.0; - delayBeforeReview = 0; - } else { - interval = frontmatter["sr-interval"]; - ease = frontmatter["sr-ease"]; - delayBeforeReview = - now - - window - .moment(frontmatter["sr-due"], ["YYYY-MM-DD", "DD-MM-YYYY", "ddd MMM DD YYYY"]) - .valueOf(); - } - - const schedObj: Record = schedule( - response, - interval, - ease, - delayBeforeReview, - this.data.settings, - this.dueDatesNotes, - ); - interval = schedObj.interval; - ease = schedObj.ease; - - const due = window.moment(now + interval * 24 * 3600 * 1000); - const dueString: string = due.format("YYYY-MM-DD"); - - // check if scheduling info exists - if (SCHEDULING_INFO_REGEX.test(fileText)) { - const schedulingInfo = SCHEDULING_INFO_REGEX.exec(fileText); - fileText = fileText.replace( - SCHEDULING_INFO_REGEX, - `---\n${schedulingInfo[1]}sr-due: ${dueString}\n` + - `sr-interval: ${interval}\nsr-ease: ${ease}\n` + - `${schedulingInfo[5]}---`, - ); - } else if (YAML_FRONT_MATTER_REGEX.test(fileText)) { - // new note with existing YAML front matter - const existingYaml = YAML_FRONT_MATTER_REGEX.exec(fileText); - fileText = fileText.replace( - YAML_FRONT_MATTER_REGEX, - `---\n${existingYaml[1]}sr-due: ${dueString}\n` + - `sr-interval: ${interval}\nsr-ease: ${ease}\n---`, - ); - } else { - fileText = - `---\nsr-due: ${dueString}\nsr-interval: ${interval}\n` + - `sr-ease: ${ease}\n---\n\n${fileText}`; - } - - if (this.data.settings.burySiblingCards) { - const noteX: Note = await this.loadNote(note); - for (const question of noteX.questionList) { - this.data.buryList.push(question.questionText.textHash); - } - await this.savePluginData(); - } - await this.app.vault.modify(note, fileText); - - // Update note's properties to update our due notes. - this.easeByPath.setEaseForPath(note.path, ease); - - Object.values(this.reviewDecks).forEach((reviewDeck: ReviewDeck) => { - let wasDueInDeck = false; - for (const scheduledNote of reviewDeck.scheduledNotes) { - if (scheduledNote.note.path === note.path) { - scheduledNote.dueUnix = due.valueOf(); - wasDueInDeck = true; - break; - } - } - - // It was a new note, remove it from the new notes and schedule it. - if (!wasDueInDeck) { - reviewDeck.newNotes.splice( - reviewDeck.newNotes.findIndex((newNote: TFile) => newNote.path === note.path), - 1, - ); - reviewDeck.scheduledNotes.push({ note, dueUnix: due.valueOf() }); - } - }); - - this.updateAndSortDueNotes(); + // + await this.osrAppCore.saveNoteReviewResponse(noteSrTFile, response, this.data.settings); new Notice(t("RESPONSE_RECEIVED")); if (this.data.settings.autoNextNote) { - if (!this.lastSelectedReviewDeck) { - const reviewDeckKeys: string[] = Object.keys(this.reviewDecks); - if (reviewDeckKeys.length > 0) this.lastSelectedReviewDeck = reviewDeckKeys[0]; - else { - new Notice(t("ALL_CAUGHT_UP")); - return; - } - } - this.reviewNextNote(this.lastSelectedReviewDeck); - } - } - - async reviewNextNoteModal(): Promise { - const reviewDeckNames: string[] = Object.keys(this.reviewDecks); - - if (reviewDeckNames.length === 1) { - this.reviewNextNote(reviewDeckNames[0]); - } else { - const deckSelectionModal = new ReviewDeckSelectionModal(this.app, reviewDeckNames); - deckSelectionModal.submitCallback = (deckKey: string) => this.reviewNextNote(deckKey); - deckSelectionModal.open(); - } - } - - async reviewNextNote(deckKey: string): Promise { - if (!Object.prototype.hasOwnProperty.call(this.reviewDecks, deckKey)) { - new Notice(t("NO_DECK_EXISTS", { deckName: deckKey })); - return; - } - - this.lastSelectedReviewDeck = deckKey; - const deck = this.reviewDecks[deckKey]; - - const nowUnix = Date.now(); - const dueNotes = deck.scheduledNotes.filter((note) => note.dueUnix <= nowUnix); - if (dueNotes.length > 0) { - const index = this.data.settings.openRandomNote - ? Math.floor(Math.random() * dueNotes.length) - : 0; - await this.app.workspace.getLeaf().openFile(dueNotes[index].note); - return; + this.nextNoteReviewHandler.autoReviewNextNote(); } - - if (deck.newNotes.length > 0) { - const index = this.data.settings.openRandomNote - ? Math.floor(Math.random() * deck.newNotes.length) - : 0; - this.app.workspace.getLeaf().openFile(deck.newNotes[index]); - return; - } - - new Notice(t("ALL_CAUGHT_UP")); } createSrTFile(note: TFile): SrTFile { @@ -805,52 +436,83 @@ export default class SRPlugin extends Plugin { if (loadedData?.settings) upgradeSettings(loadedData.settings); this.data = Object.assign({}, DEFAULT_DATA, loadedData); this.data.settings = Object.assign({}, DEFAULT_SETTINGS, this.data.settings); + setDebugParser(this.data.settings.showPaserDebugMessages); + + this.setupDataStoreAndAlgorithmInstances(this.data.settings); + } + + setupDataStoreAndAlgorithmInstances(settings: SRSettings) { + // For now we can hardcoded as we only support the one data store and one algorithm + DataStore.instance = new StoreInNote(settings); + SrsAlgorithm.instance = new SrsAlgorithm_Osr(settings); + DataStoreAlgorithm.instance = new DataStoreInNote_AlgorithmOsr(settings); } async savePluginData(): Promise { await this.saveData(this.data); } - private getActiveLeaf(type: string): WorkspaceLeaf | null { - const leaves = this.app.workspace.getLeavesOfType(type); - if (leaves.length == 0) { - return null; + async debouncedGenerateParser(timeout_ms = 250) { + if (this.debouncedGenerateParserTimeout) { + clearTimeout(this.debouncedGenerateParserTimeout); } - return leaves[0]; + this.debouncedGenerateParserTimeout = window.setTimeout(async () => { + const parserOptions = { + singleLineCardSeparator: this.data.settings.singleLineCardSeparator, + singleLineReversedCardSeparator: this.data.settings.singleLineReversedCardSeparator, + multilineCardSeparator: this.data.settings.multilineCardSeparator, + multilineReversedCardSeparator: this.data.settings.multilineReversedCardSeparator, + multilineCardEndMarker: this.data.settings.multilineCardEndMarker, + convertHighlightsToClozes: this.data.settings.convertHighlightsToClozes, + convertBoldTextToClozes: this.data.settings.convertBoldTextToClozes, + convertCurlyBracketsToClozes: this.data.settings.convertCurlyBracketsToClozes, + }; + generateParser(parserOptions); + this.debouncedGenerateParserTimeout = null; + }, timeout_ms); } - private async initReviewQueueView() { - this.registerView( - REVIEW_QUEUE_VIEW_TYPE, - (leaf) => (this.reviewQueueView = new ReviewQueueListView(leaf, this)), - ); - - if ( - this.data.settings.enableNoteReviewPaneOnStartup && - this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE) == null - ) { - await this.activateReviewQueueViewPanel(); + showRibbonIcon(status: boolean) { + // if it does not exit, we create it + if (!this.ribbonIcon) { + this.ribbonIcon = this.addRibbonIcon("SpacedRepIcon", t("REVIEW_CARDS"), async () => { + if (!this.osrAppCore.syncLock) { + await this.sync(); + this.openFlashcardModal( + this.osrAppCore.reviewableDeckTree, + this.osrAppCore.remainingDeckTree, + FlashcardReviewMode.Review, + ); + } + }); + } + if (status) { + this.ribbonIcon.style.display = ""; + } else { + this.ribbonIcon.style.display = "none"; } } - private async activateReviewQueueViewPanel() { - await this.app.workspace.getRightLeaf(false).setViewState({ - type: REVIEW_QUEUE_VIEW_TYPE, - active: true, - }); - } - - private async openReviewQueueView() { - let reviewQueueLeaf = this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE); - if (reviewQueueLeaf == null) { - await this.activateReviewQueueViewPanel(); - reviewQueueLeaf = this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE); + showStatusBar(status: boolean) { + // if it does not exit, we create it + if (!this.statusBar) { + this.statusBar = this.addStatusBarItem(); + this.statusBar.classList.add("mod-clickable"); + this.statusBar.setAttribute("aria-label", t("OPEN_NOTE_FOR_REVIEW")); + this.statusBar.setAttribute("aria-label-position", "top"); + this.statusBar.addEventListener("click", async () => { + if (!this.osrAppCore.syncLock) { + await this.sync(); + this.nextNoteReviewHandler.reviewNextNoteModal(); + } + }); } - if (reviewQueueLeaf !== null) { - this.app.workspace.revealLeaf(reviewQueueLeaf); - this.updateAndSortDueNotes(); + if (status) { + this.statusBar.style.display = ""; + } else { + this.statusBar.style.display = "none"; } } } diff --git a/src/next-note-review-handler.ts b/src/next-note-review-handler.ts new file mode 100644 index 00000000..beccad2f --- /dev/null +++ b/src/next-note-review-handler.ts @@ -0,0 +1,84 @@ +import { App, Notice, TFile, Workspace } from "obsidian"; + +import { ReviewDeckSelectionModal } from "src/gui/review-deck-selection-modal"; +import { t } from "src/lang/helpers"; +import { NoteReviewQueue } from "src/note-review-queue"; +import { SRSettings } from "src/settings"; + +export class NextNoteReviewHandler { + private app: App; + private settings: SRSettings; + private workspace: Workspace; + private _noteReviewQueue: NoteReviewQueue; + private _lastSelectedReviewDeck: string; + + get lastSelectedReviewDeck(): string { + return this._lastSelectedReviewDeck; + } + + get noteReviewQueue(): NoteReviewQueue { + return this._noteReviewQueue; + } + + constructor( + app: App, + settings: SRSettings, + workspace: Workspace, + noteReviewQueue: NoteReviewQueue, + ) { + this.app = app; + this.settings = settings; + this.workspace = workspace; + this._noteReviewQueue = noteReviewQueue; + } + + async autoReviewNextNote(): Promise { + if (this.settings.autoNextNote) { + if (!this._lastSelectedReviewDeck) { + const reviewDeckKeys: string[] = this._noteReviewQueue.reviewDeckNameList; + if (reviewDeckKeys.length > 0) this._lastSelectedReviewDeck = reviewDeckKeys[0]; + else { + // 2024-07-05 existing functionality: Code doesn't look at other decks + new Notice(t("ALL_CAUGHT_UP")); + return; + } + } + this.reviewNextNote(this._lastSelectedReviewDeck); + } + } + + async reviewNextNoteModal(): Promise { + const reviewDeckNames: string[] = this._noteReviewQueue.reviewDeckNameList; + + if (reviewDeckNames.length === 1) { + // There is only one deck, so no need to ask the user to make a selection + this.reviewNextNote(reviewDeckNames[0]); + } else { + const deckSelectionModal = new ReviewDeckSelectionModal(this.app, reviewDeckNames); + deckSelectionModal.submitCallback = (deckKey: string) => this.reviewNextNote(deckKey); + deckSelectionModal.open(); + } + } + + async reviewNextNote(deckKey: string): Promise { + if (!this._noteReviewQueue.reviewDeckNameList.contains(deckKey)) { + new Notice(t("NO_DECK_EXISTS", { deckName: deckKey })); + return; + } + + this._lastSelectedReviewDeck = deckKey; + const deck = this._noteReviewQueue.reviewDecks.get(deckKey); + const notefile = deck.determineNextNote(this.settings.openRandomNote); + + if (notefile) { + await this.openNote(deckKey, notefile.tfile); + } else { + new Notice(t("ALL_CAUGHT_UP")); + } + } + + async openNote(deckName: string, file: TFile): Promise { + this._lastSelectedReviewDeck = deckName; + await this.app.workspace.getLeaf().openFile(file); + } +} diff --git a/src/NoteEaseList.ts b/src/note-ease-list.ts similarity index 95% rename from src/NoteEaseList.ts rename to src/note-ease-list.ts index 0424f3c7..2f9942d2 100644 --- a/src/NoteEaseList.ts +++ b/src/note-ease-list.ts @@ -1,4 +1,4 @@ -import { SRSettings } from "./settings"; +import { SRSettings } from "src/settings"; export interface INoteEaseList { hasEaseForPath(path: string): boolean; diff --git a/src/NoteFileLoader.ts b/src/note-file-loader.ts similarity index 74% rename from src/NoteFileLoader.ts rename to src/note-file-loader.ts index f63e0670..e6df2344 100644 --- a/src/NoteFileLoader.ts +++ b/src/note-file-loader.ts @@ -1,10 +1,10 @@ -import { ISRFile } from "./SRFile"; -import { Note } from "./Note"; -import { Question } from "./Question"; -import { TopicPath } from "./TopicPath"; -import { NoteQuestionParser } from "./NoteQuestionParser"; -import { SRSettings } from "./settings"; -import { TextDirection } from "./util/TextDirection"; +import { Note } from "src/note"; +import { NoteQuestionParser } from "src/note-question-parser"; +import { Question } from "src/question"; +import { SRSettings } from "src/settings"; +import { ISRFile } from "src/sr-file"; +import { TopicPath } from "src/topic-path"; +import { TextDirection } from "src/utils/strings"; export class NoteFileLoader { fileText: string; diff --git a/src/NoteParser.ts b/src/note-parser.ts similarity index 70% rename from src/NoteParser.ts rename to src/note-parser.ts index f6368954..55181b91 100644 --- a/src/NoteParser.ts +++ b/src/note-parser.ts @@ -1,9 +1,9 @@ -import { NoteQuestionParser } from "./NoteQuestionParser"; -import { ISRFile } from "./SRFile"; -import { Note } from "./Note"; -import { SRSettings } from "./settings"; -import { TopicPath } from "./TopicPath"; -import { TextDirection } from "./util/TextDirection"; +import { Note } from "src/note"; +import { NoteQuestionParser } from "src/note-question-parser"; +import { SRSettings } from "src/settings"; +import { ISRFile } from "src/sr-file"; +import { TopicPath } from "src/topic-path"; +import { TextDirection } from "src/utils/strings"; export class NoteParser { settings: SRSettings; diff --git a/src/NoteQuestionParser.ts b/src/note-question-parser.ts similarity index 87% rename from src/NoteQuestionParser.ts rename to src/note-question-parser.ts index acb668cf..342da19b 100644 --- a/src/NoteQuestionParser.ts +++ b/src/note-question-parser.ts @@ -1,14 +1,19 @@ import { TagCache } from "obsidian"; -import { Card } from "./Card"; -import { CardScheduleInfo, NoteCardScheduleParser } from "./CardSchedule"; -import { parseEx, ParsedQuestionInfo } from "./parser"; -import { Question, QuestionText } from "./Question"; -import { CardFrontBack, CardFrontBackUtil } from "./QuestionType"; -import { SRSettings, SettingsUtil } from "./settings"; -import { ISRFile, frontmatterTagPseudoLineNum } from "./SRFile"; -import { TopicPath, TopicPathList } from "./TopicPath"; -import { TextDirection } from "./util/TextDirection"; -import { extractFrontmatter, splitTextIntoLineArray } from "./util/utils"; + +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { Card } from "src/card"; +import { DataStore } from "src/data-stores/base/data-store"; +import { ParsedQuestionInfo, parseEx, ParserOptions } from "src/parser"; +import { Question, QuestionText } from "src/question"; +import { CardFrontBack, CardFrontBackUtil } from "src/question-type"; +import { SettingsUtil, SRSettings } from "src/settings"; +import { frontmatterTagPseudoLineNum, ISRFile } from "src/sr-file"; +import { TopicPath, TopicPathList } from "src/topic-path"; +import { + splitNoteIntoFrontmatterAndContent, + splitTextIntoLineArray, + TextDirection, +} from "src/utils/strings"; export class NoteQuestionParser { settings: SRSettings; @@ -63,7 +68,7 @@ export class NoteQuestionParser { // The following analysis can require fair computation. // There is no point doing it if there aren't any topic paths - [this.frontmatterText, this.contentText] = extractFrontmatter(noteText); + [this.frontmatterText, this.contentText] = splitNoteIntoFrontmatterAndContent(noteText); // Create the question list let textDirection: TextDirection = noteFile.getTextDirection(); @@ -116,8 +121,11 @@ export class NoteQuestionParser { ); // And if the card has been reviewed, then scheduling info as well - let cardScheduleInfoList: CardScheduleInfo[] = - NoteCardScheduleParser.createCardScheduleInfoList(question.questionText.original); + let cardScheduleInfoList: RepItemScheduleInfo[] = + DataStore.getInstance().questionCreateSchedule( + question.questionText.original, + null, + ); // we have some extra scheduling dates to delete const correctLength = cardFrontBackList.length; @@ -136,17 +144,18 @@ export class NoteQuestionParser { private parseQuestions(): ParsedQuestionInfo[] { // We pass contentText which has the frontmatter blanked out; see extractFrontmatter for reasoning - const settings: SRSettings = this.settings; - const result: ParsedQuestionInfo[] = parseEx( - this.contentText, - settings.singleLineCardSeparator, - settings.singleLineReversedCardSeparator, - settings.multilineCardSeparator, - settings.multilineReversedCardSeparator, - settings.convertHighlightsToClozes, - settings.convertBoldTextToClozes, - settings.convertCurlyBracketsToClozes, - ); + const parserOptions: ParserOptions = { + singleLineCardSeparator: this.settings.singleLineCardSeparator, + singleLineReversedCardSeparator: this.settings.singleLineReversedCardSeparator, + multilineCardSeparator: this.settings.multilineCardSeparator, + multilineReversedCardSeparator: this.settings.multilineReversedCardSeparator, + multilineCardEndMarker: this.settings.multilineCardEndMarker, + convertHighlightsToClozes: this.settings.convertHighlightsToClozes, + convertBoldTextToClozes: this.settings.convertBoldTextToClozes, + convertCurlyBracketsToClozes: this.settings.convertCurlyBracketsToClozes, + }; + + const result: ParsedQuestionInfo[] = parseEx(this.contentText, parserOptions); return result; } @@ -169,7 +178,7 @@ export class NoteQuestionParser { private createCardList( cardFrontBackList: CardFrontBack[], - cardScheduleInfoList: CardScheduleInfo[], + cardScheduleInfoList: RepItemScheduleInfo[], ): Card[] { const siblings: Card[] = []; @@ -178,15 +187,15 @@ export class NoteQuestionParser { const { front, back } = cardFrontBackList[i]; const hasScheduleInfo: boolean = i < cardScheduleInfoList.length; - const schedule: CardScheduleInfo = cardScheduleInfoList[i]; + const schedule: RepItemScheduleInfo = cardScheduleInfoList[i]; const cardObj: Card = new Card({ front, back, cardIdx: i, }); - cardObj.scheduleInfo = - hasScheduleInfo && !schedule.isDummyScheduleForNewCard() ? schedule : null; + + cardObj.scheduleInfo = hasScheduleInfo ? schedule : null; siblings.push(cardObj); } diff --git a/src/note-review-deck.ts b/src/note-review-deck.ts new file mode 100644 index 00000000..6644f5ab --- /dev/null +++ b/src/note-review-deck.ts @@ -0,0 +1,99 @@ +import { t } from "src/lang/helpers"; +import { ISRFile } from "src/sr-file"; +import { globalRandomNumberProvider } from "src/utils/numbers"; + +export class SchedNote { + note: ISRFile; + dueUnix: number; + + constructor(note: ISRFile, dueUnix: number) { + this.note = note; + this.dueUnix = dueUnix; + } + + isDue(todayUnix: number): boolean { + return this.dueUnix <= todayUnix; + } +} + +export class NoteReviewDeck { + // Deck name such as the default "#review" + private _deckName: string; + + private _newNotes: ISRFile[] = []; + private _scheduledNotes: SchedNote[] = []; + private _dueNotesCount = 0; + + // This stores the collapsed/expanded state of each folder (folder names being things like + // "TODAY", "NEW" or formatted dates). + private _activeFolders: Set; + + get deckName(): string { + return this._deckName; + } + + get newNotes(): ISRFile[] { + return this._newNotes; + } + + get scheduledNotes(): SchedNote[] { + return this._scheduledNotes; + } + + get dueNotesCount(): number { + return this._dueNotesCount; + } + + get activeFolders(): Set { + return this._activeFolders; + } + + constructor(name: string) { + this._deckName = name; + this._activeFolders = new Set([this._deckName, t("TODAY")]); + } + + public calcDueNotesCount(todayUnix: number): void { + this._dueNotesCount = 0; + this.scheduledNotes.forEach((scheduledNote: SchedNote) => { + if (scheduledNote.isDue(todayUnix)) { + this._dueNotesCount++; + } + }); + } + + public sortNotesByDateAndImportance(pageranks: Record): void { + // sort new notes by importance + this._newNotes = this.newNotes.sort( + (a: ISRFile, b: ISRFile) => (pageranks[b.path] || 0) - (pageranks[a.path] || 0), + ); + + // sort scheduled notes by date & within those days, sort them by importance + this._scheduledNotes = this.scheduledNotes.sort((a: SchedNote, b: SchedNote) => { + const result = a.dueUnix - b.dueUnix; + if (result != 0) { + return result; + } + return (pageranks[b.note.path] || 0) - (pageranks[a.note.path] || 0); + }); + } + + determineNextNote(openRandomNote: boolean): ISRFile { + // Review due notes before new ones + if (this.dueNotesCount > 0) { + const index = openRandomNote + ? globalRandomNumberProvider.getInteger(0, this.dueNotesCount - 1) + : 0; + return this.scheduledNotes[index].note; + } + + if (this.newNotes.length > 0) { + const index = openRandomNote + ? globalRandomNumberProvider.getInteger(0, this.newNotes.length - 1) + : 0; + return this.newNotes[index]; + } + + return null; + } +} diff --git a/src/note-review-queue.ts b/src/note-review-queue.ts new file mode 100644 index 00000000..2d8b8d86 --- /dev/null +++ b/src/note-review-queue.ts @@ -0,0 +1,78 @@ +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { NoteReviewDeck, SchedNote } from "src/note-review-deck"; +import { ISRFile } from "src/sr-file"; + +export class NoteReviewQueue { + private _reviewDecks: Map; + private _dueNotesCount: number; + + get reviewDecks(): Map { + return this._reviewDecks; + } + + get dueNotesCount(): number { + return this._dueNotesCount; + } + + get reviewDeckNameList(): string[] { + return [...this._reviewDecks.keys()]; + } + + init(): void { + this._reviewDecks = new Map(); + } + + public calcDueNotesCount(todayUnix: number): void { + this._dueNotesCount = 0; + this._reviewDecks.forEach((reviewDeck: NoteReviewDeck) => { + reviewDeck.calcDueNotesCount(todayUnix); + this._dueNotesCount += reviewDeck.dueNotesCount; + }); + } + + addNoteToQueue( + noteFile: ISRFile, + noteSchedule: RepItemScheduleInfo, + matchedNoteTags: string[], + ): void { + for (const matchedNoteTag of matchedNoteTags) { + if (!this.reviewDecks.has(matchedNoteTag)) { + this.reviewDecks.set(matchedNoteTag, new NoteReviewDeck(matchedNoteTag)); + } + } + if (noteSchedule == null) { + for (const matchedNoteTag of matchedNoteTags) { + this.reviewDecks.get(matchedNoteTag).newNotes.push(noteFile); + } + } else { + // schedule the note + for (const matchedNoteTag of matchedNoteTags) { + this.reviewDecks + .get(matchedNoteTag) + .scheduledNotes.push(new SchedNote(noteFile, noteSchedule.dueDateAsUnix)); + } + } + } + + updateScheduleInfo(note: ISRFile, scheduleInfo: RepItemScheduleInfo): void { + this.reviewDecks.forEach((reviewDeck: NoteReviewDeck) => { + let wasDueInDeck = false; + for (const scheduledNote of reviewDeck.scheduledNotes) { + if (scheduledNote.note.path === note.path) { + scheduledNote.dueUnix = scheduleInfo.dueDate.valueOf(); + wasDueInDeck = true; + break; + } + } + + // It was a new note, remove it from the new notes and schedule it. + if (!wasDueInDeck) { + reviewDeck.newNotes.splice( + reviewDeck.newNotes.findIndex((newNote: ISRFile) => newNote.path === note.path), + 1, + ); + reviewDeck.scheduledNotes.push(new SchedNote(note, scheduleInfo.dueDate.valueOf())); + } + }); + } +} diff --git a/src/Note.ts b/src/note.ts similarity index 86% rename from src/Note.ts rename to src/note.ts index 15466939..55da8c72 100644 --- a/src/Note.ts +++ b/src/note.ts @@ -1,7 +1,7 @@ -import { SRSettings } from "./settings"; -import { Deck } from "./Deck"; -import { Question } from "./Question"; -import { ISRFile } from "./SRFile"; +import { Deck } from "src/deck"; +import { Question } from "src/question"; +import { SRSettings } from "src/settings"; +import { ISRFile } from "src/sr-file"; export class Note { file: ISRFile; @@ -44,7 +44,7 @@ export class Note { let fileText: string = await this.file.read(); for (const question of this.questionList) { if (question.hasChanged) { - fileText = question.updateQuestionText(fileText, settings); + fileText = question.updateQuestionWithinNoteText(fileText, settings); } } await this.file.write(fileText); diff --git a/src/parser.ts b/src/parser.ts index 3cc06542..5423ab0e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,4 +1,339 @@ -import { CardType } from "./Question"; +import { generate, Parser } from "peggy"; + +import { CardType } from "src/question"; + +let parser: Parser | null = null; +let oldOptions: ParserOptions; +export let debugParser = false; + +export interface ParserOptions { + singleLineCardSeparator: string; + singleLineReversedCardSeparator: string; + multilineCardSeparator: string; + multilineReversedCardSeparator: string; + multilineCardEndMarker: string; + convertHighlightsToClozes: boolean; + convertBoldTextToClozes: boolean; + convertCurlyBracketsToClozes: boolean; +} + +function areParserOptionsEqual(options1: ParserOptions, options2: ParserOptions): boolean { + return ( + options1.singleLineCardSeparator === options2.singleLineCardSeparator && + options1.singleLineReversedCardSeparator === options2.singleLineReversedCardSeparator && + options1.multilineCardSeparator === options2.multilineCardSeparator && + options1.multilineReversedCardSeparator === options2.multilineReversedCardSeparator && + options1.multilineCardEndMarker === options2.multilineCardEndMarker && + options1.convertHighlightsToClozes === options2.convertHighlightsToClozes && + options1.convertBoldTextToClozes === options2.convertBoldTextToClozes && + options1.convertCurlyBracketsToClozes === options2.convertCurlyBracketsToClozes + ); +} + +export function generateParser(options: ParserOptions): Parser { + let grammar: string | null = null; + + // Debug the grammar before generating the parser `generate(grammar)` from the grammar. + if (debugParser) { + if (grammar === null) { + grammar = generateGrammar(options); + } + console.log( + "The parsers grammar is provided below. You can test it with https://peggyjs.org/online.html.", + ); + console.log({ + info: "Copy the grammar by right-clicking on the property grammar and copying it as a string. Then, paste it in https://peggyjs.org/online.html.", + grammar: grammar, + }); + } + + // If the parser did not already exist or if the parser options changed since last the last + // parser was generated, we generate a new parser. Otherwise, we skip the block to save + // some execution time. + if (parser === null || !areParserOptionsEqual(options, oldOptions)) { + /* GENERATE A NEW PARSER */ + + oldOptions = Object.assign({}, options); + + grammar = generateGrammar(options); + + if (debugParser) { + const t0 = Date.now(); + parser = generate(grammar); + const t1 = Date.now(); + console.log("New parser generated in " + (t1 - t0) + " milliseconds."); + } else { + parser = generate(grammar); + } + } else { + if (debugParser) { + console.log("Parser already exists. No need to generate a new parser."); + } + } + + return parser; +} + +function generateGrammar(options: ParserOptions): string { + // Contains the grammar for cloze cards + let clozes_grammar = ""; + + // An array contianing the types of cards enabled by the user + const card_rules_list: string[] = ["html_comment", "tilde_code", "backprime_code"]; + + // Include reversed inline flashcards rule only if the user provided a non-empty marker for reversed inline flashcards + if (options.singleLineCardSeparator.trim() !== "") card_rules_list.push("inline_rev_card"); + + // Include inline flashcards rule only if the user provided a non-empty marker for inline flashcards + if (options.singleLineCardSeparator.trim() !== "") card_rules_list.push("inline_card"); + + // Include reversed multiline flashcards rule only if the user provided a non-empty marker for reversed multiline flashcards + if (options.multilineReversedCardSeparator.trim() !== "") + card_rules_list.push("multiline_rev_card"); + + // Include multiline flashcards rule only if the user provided a non-empty marker for multiline flashcards + if (options.multilineCardSeparator.trim() !== "") card_rules_list.push("multiline_card"); + + const cloze_rules_list: string[] = []; + if (options.convertHighlightsToClozes) cloze_rules_list.push("cloze_equal"); + if (options.convertBoldTextToClozes) cloze_rules_list.push("cloze_star"); + if (options.convertCurlyBracketsToClozes) cloze_rules_list.push("cloze_bracket"); + + // Include cloze cards only if the user enabled at least one type of cloze cards + if (cloze_rules_list.length > 0) { + card_rules_list.push("cloze_card"); + const cloze_rules = cloze_rules_list.join(" / "); + clozes_grammar = ` +cloze_card += $(multiline_before_cloze? cloze_line (multiline_after_cloze)? (newline annotation)?) { + return createParsedQuestionInfo(CardType.Cloze,text().trimEnd(),location().start.line-1,location().end.line-1); +} + +cloze_line += ((!cloze_text (inline_code / non_newline))* cloze_text) text_line_nonterminated? + +multiline_before_cloze += (!cloze_line nonempty_text_line)+ + +multiline_after_cloze += e:(!(newline separator_line) text_line1)+ + +cloze_text += ${cloze_rules} + +cloze_equal += cloze_mark_equal (!cloze_mark_equal non_newline)+ cloze_mark_equal + +cloze_mark_equal += "==" + +cloze_star += cloze_mark_star (!cloze_mark_star non_newline)+ cloze_mark_star + +cloze_mark_star += "**" + +cloze_bracket += cloze_mark_bracket_open (!cloze_mark_bracket_close non_newline)+ cloze_mark_bracket_close + +cloze_mark_bracket_open += "{{" + +cloze_mark_bracket_close += "}}" +`; + } + + // Important: we need to include `loose_line` rule to detect any other loose line. + // Otherwise, we get a syntax error because the parser is likely not able to reach the end + // of the file, as it may encounter loose lines, which it would not know how to handle. + card_rules_list.push("loose_line"); + + const card_rules = card_rules_list.join(" / "); + + return `{ + // The fallback case is important if we want to test the rules with https://peggyjs.org/online.html + const CardTypeFallBack = { + SingleLineBasic: 0, + SingleLineReversed: 1, + MultiLineBasic: 2, + MultiLineReversed: 3, + Cloze: 4, + }; + + // The fallback case is important if we want to test the rules with https://peggyjs.org/online.html + const createParsedQuestionInfoFallBack = (cardType, text, firstLineNum, lastLineNum) => { + return {cardType, text, firstLineNum, lastLineNum}; + }; + + const CardType = options.CardType ? options.CardType : CardTypeFallBack; + const createParsedQuestionInfo = options.createParsedQuestionInfo ? options.createParsedQuestionInfo : createParsedQuestionInfoFallBack; + + function filterBlocks(b) { + return b.filter( (d) => d !== null ) + } +} + +main += blocks:block* { return filterBlocks(blocks); } + +/* The input text to the parser contains arbitrary text, not just card definitions. +Hence we fallback to matching on loose_line. The result from loose_line is filtered out by filterBlocks() */ +block += ${card_rules} + +html_comment += $("" (html_comment / .))* "-->" newline?) { + return null; +} + +/* Obsidian tag definition: https://help.obsidian.md/Editing+and+formatting/Tags#Tag+format */ +tag += $("#" + name:([a-zA-Z/\\-_] { return 1; } / [0-9]{ return 0;})+ &{ + // check if it is a valid Obsidian tag - (Tags must contain at least one non-numerical character) + return name.includes(1); +}) + +inline_card += e:inline newline? { return e; } + +inline += $(left:(!inline_mark (inline_code / non_newline))+ inline_mark right:text_till_newline (newline annotation)?) { + return createParsedQuestionInfo(CardType.SingleLineBasic,text(),location().start.line-1,location().end.line-1); +} + +inline_rev_card += e:inline_rev newline? { return e; } + +inline_rev += left:(!inline_rev_mark (inline_code / non_newline))+ inline_rev_mark right:text_till_newline (newline annotation)? { + return createParsedQuestionInfo(CardType.SingleLineReversed,text(),location().start.line-1,location().end.line-1); +} + +multiline_card += c:multiline separator_line { + return c; +} + +multiline += arg1:multiline_before multiline_mark arg2:multiline_after { + return createParsedQuestionInfo(CardType.MultiLineBasic,(arg1+"${options.multilineCardSeparator}\\n"+arg2.trimEnd()),location().start.line-1,location().end.line-2); +} + +multiline_before += $(!multiline_mark nonempty_text_line)+ + +multiline_after += $(!separator_line (tilde_code / backprime_code / text_line))+ + +inline_code += $("\`" (!"\`" .)* "\`") + +tilde_code += $( + " "* left:$tilde_marker text_line + (!(middle:$tilde_marker &{ return left.length===middle.length;}) (tilde_code / text_line))* + (right:$tilde_marker &{ return left.length===right.length; }) + newline +) { return null; } + +tilde_marker += "~~~" "~"* + +backprime_code += $( + " "* left:$backprime_marker text_line + (!(middle:$backprime_marker &{ return left.length===middle.length;}) (backprime_code / text_line))* + (right:$backprime_marker &{ return left.length===right.length; }) + newline +) { return null; } + +backprime_marker += "\`\`\`" "\`"* + +multiline_rev_card += @multiline_rev separator_line + +multiline_rev += arg1:multiline_rev_before multiline_rev_mark arg2:multiline_rev_after { + return createParsedQuestionInfo(CardType.MultiLineReversed,(arg1+"${options.multilineReversedCardSeparator}\\n"+arg2.trimEnd()),location().start.line-1,location().end.line-2); +} + +multiline_rev_before += $(!multiline_rev_mark nonempty_text_line)+ + +multiline_rev_after += $(!separator_line text_line)+ + +${clozes_grammar} + +inline_mark += "${options.singleLineCardSeparator}" + +inline_rev_mark += "${options.singleLineReversedCardSeparator}" + +multiline_mark += optional_whitespaces "${options.multilineCardSeparator}" optional_whitespaces newline + +multiline_rev_mark += optional_whitespaces "${options.multilineReversedCardSeparator}" optional_whitespaces newline + +end_card_mark += "${options.multilineCardEndMarker}" + +separator_line += end_card_mark optional_whitespaces newline + +text_line_nonterminated += $nonempty_text_till_newline + +nonempty_text_line += nonempty_text_till_newline newline + +text_line += @$text_till_newline newline + +// very likely, it is possible to homogeneize/modify the rules to use only either 'text_line1' or 'text_line' +text_line1 += newline @$text_till_newline + +loose_line += $((text_till_newline newline) / nonempty_text_till_newline) { + return null; + } + +annotation += $("" .)+ "-->") + +nonempty_text_till_newline += $(inline_code / non_newline)+ + +text_till_newline += $non_newline* + +non_newline += [^\\n] + +newline += $[\\n] + +empty_line += $(whitespace_char* [\\n]) + +nonemptyspace += [^ \\f\\t\\v\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff] + +optional_whitespaces += whitespace_char* + +whitespace_char = ([ \\f\\t\\v\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]) +`; +} + +export function setDebugParser(value: boolean) { + debugParser = value; +} export class ParsedQuestionInfo { cardType: CardType; @@ -10,7 +345,7 @@ export class ParsedQuestionInfo { constructor(cardType: CardType, text: string, firstLineNum: number, lastLineNum: number) { this.cardType = cardType; - this.text = text; + this.text = text; // text.replace(/\s*$/gm, ""); // reproduce the same old behavior as when adding new lines with trimEnd. It is not clear why we need it in real life. However, it is needed to pass the tests. this.firstLineNum = firstLineNum; this.lastLineNum = lastLineNum; } @@ -25,107 +360,50 @@ export class ParsedQuestionInfo { * * It is best that the text does not contain frontmatter, see extractFrontmatter for reasoning * - * Multi-line question with blank lines user workaround: - * As of 3/04/2024 there is no support for including blank lines within multi-line questions - * As a workaround, one user uses a zero width Unicode character - U+200B - * https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/915#issuecomment-2031003092 + * EXCEPTIONS: The underlying peggy parser can throw an exception if the input it receives does + * not conform to the grammar it was built with. However, the grammar used in generating this + * parser, see generateParser(), intentionally matches all input text and therefore + * this function should not throw an exception. * * @param text - The text to extract flashcards from - * @param singlelineCardSeparator - Separator for inline basic cards - * @param singlelineReversedCardSeparator - Separator for inline reversed cards - * @param multilineCardSeparator - Separator for multiline basic cards - * @param multilineReversedCardSeparator - Separator for multiline basic card + * @param options - Plugin's settings * @returns An array of [CardType, card text, line number] tuples */ -export function parseEx( - text: string, - singlelineCardSeparator: string, - singlelineReversedCardSeparator: string, - multilineCardSeparator: string, - multilineReversedCardSeparator: string, - convertHighlightsToClozes: boolean, - convertBoldTextToClozes: boolean, - convertCurlyBracketsToClozes: boolean, -): ParsedQuestionInfo[] { - let cardText = ""; - const cards: ParsedQuestionInfo[] = []; - let cardType: CardType | null = null; - let firstLineNo = 0; - let lastLineNo = 0; - - const lines: string[] = text.replaceAll("\r\n", "\n").split("\n"); - for (let i = 0; i < lines.length; i++) { - const currentLine = lines[i]; - if (currentLine.length === 0) { - if (cardType) { - lastLineNo = i - 1; - cards.push(new ParsedQuestionInfo(cardType, cardText, firstLineNo, lastLineNo)); - cardType = null; - } - - cardText = ""; - continue; - } else if (currentLine.startsWith("")) i++; - i++; - continue; - } +export function parseEx(text: string, options: ParserOptions): ParsedQuestionInfo[] { + if (debugParser) { + console.log("Text to parse:\n<<<" + text + ">>>"); + } - if (cardText.length > 0) { - cardText += "\n"; - } else if (cardText.length === 0) { - // This could be the first line of a multi line question - firstLineNo = i; - } - cardText += currentLine.trimEnd(); - - if ( - currentLine.includes(singlelineReversedCardSeparator) || - currentLine.includes(singlelineCardSeparator) - ) { - cardType = lines[i].includes(singlelineReversedCardSeparator) - ? CardType.SingleLineReversed - : CardType.SingleLineBasic; - cardText = lines[i]; - firstLineNo = i; - if (i + 1 < lines.length && lines[i + 1].startsWith(" -Q3:::A3 - -`; - let file: UnitTestSRFile = new UnitTestSRFile(originalText); - let note: Note = await noteFileLoader.load(file, TextDirection.Ltr, TopicPath.emptyPath); - - await note.writeNoteFile(DEFAULT_SETTINGS); - let updatedText: string = file.content; - - let expectedText: string = `#flashcards/test -Q1::A1 -#flashcards Q2::A2 - -Q3:::A3 - -`; - expect(updatedText).toEqual(expectedText); - }); -}); diff --git a/tests/unit/NoteCardScheduleParser.test.ts b/tests/unit/NoteCardScheduleParser.test.ts deleted file mode 100644 index 49f37df4..00000000 --- a/tests/unit/NoteCardScheduleParser.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CardScheduleInfo, NoteCardScheduleParser } from "src/CardSchedule"; -import { TICKS_PER_DAY } from "src/constants"; -import { setupStaticDateProvider_20230906 } from "src/util/DateProvider"; - -beforeAll(() => { - setupStaticDateProvider_20230906(); -}); - -test("No schedule info for question", () => { - expect(NoteCardScheduleParser.createCardScheduleInfoList("A::B")).toEqual([]); -}); - -test("Single schedule info for question (on separate line)", () => { - let actual: CardScheduleInfo[] = - NoteCardScheduleParser.createCardScheduleInfoList(`What symbol represents an electric field:: $\\large \\vec E$ -`); - - expect(actual).toEqual([ - CardScheduleInfo.fromDueDateStr("2023-09-02", 4, 270, -4 * TICKS_PER_DAY), - ]); -}); - -test("Single schedule info for question (on same line)", () => { - let actual: CardScheduleInfo[] = NoteCardScheduleParser.createCardScheduleInfoList( - `What symbol represents an electric field:: $\\large \\vec E$`, - ); - - expect(actual).toEqual([ - CardScheduleInfo.fromDueDateStr("2023-09-02", 4, 270, -4 * TICKS_PER_DAY), - ]); -}); - -test("Multiple schedule info for question (on separate line)", () => { - let actual: CardScheduleInfo[] = - NoteCardScheduleParser.createCardScheduleInfoList(`This is a really very ==interesting== and ==fascinating== and ==great== test - `); - - expect(actual).toEqual([ - CardScheduleInfo.fromDueDateStr("2023-09-03", 1, 230, -3 * TICKS_PER_DAY), - CardScheduleInfo.fromDueDateStr("2023-09-05", 3, 250, -1 * TICKS_PER_DAY), - CardScheduleInfo.fromDueDateStr("2023-09-06", 4, 270, 0), - ]); -}); diff --git a/tests/unit/NoteFileLoader.test.ts b/tests/unit/NoteFileLoader.test.ts deleted file mode 100644 index c60db472..00000000 --- a/tests/unit/NoteFileLoader.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -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"; - -var noteFileLoader: NoteFileLoader = new NoteFileLoader(DEFAULT_SETTINGS); - -describe("load", () => { - test("Multiple questions, none with too many schedule details", async () => { - let noteText: string = `#flashcards/test -Q1::A1 -#flashcards Q2::A2 - -Q3:::A3 - -`; - let file: UnitTestSRFile = new UnitTestSRFile(noteText); - let note: Note = await noteFileLoader.load(file, TextDirection.Ltr, TopicPath.emptyPath); - expect(note.hasChanged).toEqual(false); - }); - - test("Multiple questions, some with too many schedule details", async () => { - let noteText: string = `#flashcards/test -Q1::A1 -#flashcards Q2::A2 - -Q3:::A3 - -`; - let file: UnitTestSRFile = new UnitTestSRFile(noteText); - 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 deleted file mode 100644 index 3f62bbc4..00000000 --- a/tests/unit/NoteParser.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NoteParser } from "src/NoteParser"; -import { TopicPath } from "src/TopicPath"; -import { Note } from "src/Note"; -import { Question } from "src/Question"; -import { DEFAULT_SETTINGS } from "src/settings"; -import { setupStaticDateProvider_20230906 } from "src/util/DateProvider"; -import { TextDirection } from "src/util/TextDirection"; -import { UnitTestSRFile } from "./helpers/UnitTestSRFile"; - -let parser: NoteParser = new NoteParser(DEFAULT_SETTINGS); - -beforeAll(() => { - setupStaticDateProvider_20230906(); -}); - -describe("Multiple questions in the text", () => { - test("SingleLineBasic: No schedule info", async () => { - let noteText: string = `#flashcards/test -Q1::A1 -Q2::A2 -Q3::A3 -`; - let file: UnitTestSRFile = new UnitTestSRFile(noteText); - let folderTopicPath = TopicPath.emptyPath; - let note: Note = await parser.parse(file, TextDirection.Ltr, folderTopicPath); - let questionList = note.questionList; - expect(questionList.length).toEqual(3); - }); -}); diff --git a/tests/unit/__mocks__/obsidian.js b/tests/unit/__mocks__/obsidian.js index 379d6354..1ef5c5b8 100644 --- a/tests/unit/__mocks__/obsidian.js +++ b/tests/unit/__mocks__/obsidian.js @@ -1,3 +1,6 @@ +/* eslint-disable no-undef */ +/* eslint-disable getter-return */ + module.exports = { moment: { locale: jest.fn(() => "en"), diff --git a/tests/unit/algorithms/osr/rep-item-schedule-info-osr.test.ts b/tests/unit/algorithms/osr/rep-item-schedule-info-osr.test.ts new file mode 100644 index 00000000..578566df --- /dev/null +++ b/tests/unit/algorithms/osr/rep-item-schedule-info-osr.test.ts @@ -0,0 +1,29 @@ +import moment from "moment"; + +import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/rep-item-schedule-info-osr"; +import { DEFAULT_SETTINGS } from "src/settings"; + +describe("formatCardScheduleForHtmlComment", () => { + test("With due date", () => { + const repItem: RepItemScheduleInfo_Osr = RepItemScheduleInfo_Osr.fromDueDateStr( + "2023-09-02", + 4, + 270, + null, + ); + expect(repItem.formatCardScheduleForHtmlComment()).toEqual("!2023-09-02,4,270"); + }); + + test("Without due date", () => { + const repItem: RepItemScheduleInfo_Osr = new RepItemScheduleInfo_Osr(null, 5, 290, null); + expect(repItem.formatCardScheduleForHtmlComment()).toEqual("!2000-01-01,5,290"); + }); +}); + +test("getDummyScheduleForNewCard", () => { + const repItem: RepItemScheduleInfo_Osr = + RepItemScheduleInfo_Osr.getDummyScheduleForNewCard(DEFAULT_SETTINGS); + expect(repItem.interval).toEqual(1); + expect(repItem.latestEase).toEqual(250); + expect(repItem.dueDate.valueOf).toEqual(moment("2000-01-01").valueOf); +}); diff --git a/tests/unit/card.test.ts b/tests/unit/card.test.ts new file mode 100644 index 00000000..54ddcc54 --- /dev/null +++ b/tests/unit/card.test.ts @@ -0,0 +1,26 @@ +import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/rep-item-schedule-info-osr"; +import { Card } from "src/card"; +import { TICKS_PER_DAY } from "src/constants"; + +describe("Card", () => { + test("format Schedule", () => { + expect( + new Card({ + front: "What year did Aegon's Conquest start?", + back: "2BC #flashcards", + }).formatSchedule(), + ).toBe("New"); + expect( + new Card({ + front: "What year did Aegon's Conquest start?", + back: "2BC #flashcards", + scheduleInfo: RepItemScheduleInfo_Osr.fromDueDateStr( + "2023-09-03", + 1, + 230, + 3 * TICKS_PER_DAY, + ), + }).formatSchedule(), + ).toBe("!2023-09-03,1,230"); + }); +}); diff --git a/tests/unit/constants.test.ts b/tests/unit/constants.test.ts new file mode 100644 index 00000000..d6dbd604 --- /dev/null +++ b/tests/unit/constants.test.ts @@ -0,0 +1,19 @@ +import { YAML_FRONT_MATTER_REGEX } from "src/constants"; + +describe("YAML_FRONT_MATTER_REGEX", () => { + function createTestStr1(sep: string): string { + return `---${sep}sr-due: 2024-08-10${sep}sr-interval: 273${sep}sr-ease: 309${sep}---`; + } + + test("New line is line feed", async () => { + const sep: string = String.fromCharCode(10); + const text: string = createTestStr1(sep); + expect(YAML_FRONT_MATTER_REGEX.test(text)).toEqual(true); + }); + + test("New line is carriage return line feed", async () => { + const sep: string = String.fromCharCode(13, 10); + const text: string = createTestStr1(sep); + expect(YAML_FRONT_MATTER_REGEX.test(text)).toEqual(true); + }); +}); diff --git a/tests/unit/core.test.ts b/tests/unit/core.test.ts new file mode 100644 index 00000000..7150753e --- /dev/null +++ b/tests/unit/core.test.ts @@ -0,0 +1,380 @@ +import moment from "moment"; + +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { CardListType } from "src/deck"; +import { NoteDueDateHistogram } from "src/due-date-histogram"; +import { NoteReviewDeck, SchedNote } from "src/note-review-deck"; +import { DEFAULT_SETTINGS, SRSettings } from "src/settings"; +import { ISRFile } from "src/sr-file"; +import { formatDate_YYYY_MM_DD, setupStaticDateProvider_20230906 } from "src/utils/dates"; + +import { UnitTestOsrCore } from "./helpers/unit-test-core"; +import { unitTest_CheckNoteFrontmatter } from "./helpers/unit-test-helper"; +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; + +function checkDeckTreeCounts( + osrCore: UnitTestOsrCore, + expectedReviewableCount: number, + expectedRemainingCount: number, +): void { + expect(osrCore.reviewableDeckTree.getCardCount(CardListType.All, true)).toEqual( + expectedReviewableCount, + ); + expect(osrCore.remainingDeckTree.getCardCount(CardListType.All, true)).toEqual( + expectedRemainingCount, + ); +} + +function checkNoteReviewDeck_Basic( + actual: NoteReviewDeck, + expected: { + deckName: string; + dueNotesCount: number; + newNotesLength: number; + scheduledNotesLength: number; + }, +): void { + expect(actual.deckName).toEqual(expected.deckName); + expect(actual.dueNotesCount).toEqual(expected.dueNotesCount); + expect(actual.newNotes.length).toEqual(expected.newNotesLength); + expect(actual.scheduledNotes.length).toEqual(expected.scheduledNotesLength); +} + +function checkScheduledNote( + actual: SchedNote, + expected: { filename: string; dueDate: string }, +): void { + expect(actual.note.path.endsWith(expected.filename)).toBeTruthy(); + expect(formatDate_YYYY_MM_DD(moment(actual.dueUnix))).toEqual(expected.dueDate); +} + +beforeAll(() => { + setupStaticDateProvider_20230906(); + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); +}); + +test("No questions in the text; no files tagged as notes", async () => { + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS); + await osrCore.loadTestVault("filesButNoQuestions"); + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + expect(osrCore.noteReviewQueue.reviewDecks.size).toEqual(0); + checkDeckTreeCounts(osrCore, 0, 0); + expect(osrCore.questionPostponementList.list.length).toEqual(0); +}); + +describe("Notes", () => { + describe("Testing code that loads from test vault", () => { + test("Tagged as note, but no OSR frontmatter", async () => { + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS); + await osrCore.loadTestVault("notes1"); + + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + expect(osrCore.noteReviewQueue.reviewDecks.size).toEqual(1); + + // Single deck "#review", with single new note "Computation Graph.md" + const actual: NoteReviewDeck = osrCore.noteReviewQueue.reviewDecks.get("#review"); + checkNoteReviewDeck_Basic(actual, { + deckName: "#review", + dueNotesCount: 0, + newNotesLength: 1, + scheduledNotesLength: 0, + }); + expect(actual.newNotes[0].path.endsWith("Computation Graph.md")).toBeTruthy(); + }); + + test("Tagged as note, and includes OSR frontmatter", async () => { + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS); + await osrCore.loadTestVault("notes2"); + + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + expect(osrCore.noteReviewQueue.reviewDecks.size).toEqual(1); + + // Single deck "#review", with single scheduled note "Triboelectric Effect.md", + const actual: NoteReviewDeck = osrCore.noteReviewQueue.reviewDecks.get("#review"); + checkNoteReviewDeck_Basic(actual, { + deckName: "#review", + dueNotesCount: 0, + newNotesLength: 0, + scheduledNotesLength: 1, + }); + checkScheduledNote(actual.scheduledNotes[0], { + filename: "Triboelectric Effect.md", + dueDate: "2025-02-21", + }); + }); + }); + + describe("Review New note (i.e. not previously reviewed); no questions present", () => { + test("New note without any backlinks", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + await osrCore.loadTestVault("notes1"); + + // Review the note + const file = osrCore.getFileByNoteName("Computation Graph"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // Check note frontmatter - 4 days after the simulated test date of 2023-09-06 + const expectedDueDate: string = "2023-09-10"; + unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 270); + }); + + // The notes that have links to [[A]] themselves haven't been reviewed, + // So the expected post-review schedule is the same as if no files had links to [[A]] + test("Review note with some backlinks (source files without reviews)", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + await osrCore.loadTestVault("notes3"); + + // Review the note + const file = osrCore.getFileByNoteName("A"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // Check note frontmatter - 4 days after the simulated test date of 2023-09-06 + const expectedDueDate: string = "2023-09-10"; + unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 270); + }); + + test("Review note with a backlink (one source file already reviewed)", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + + // See: tests\vaults\readme.md + await osrCore.loadTestVault("notes4"); + + // Review note B + const file = osrCore.getFileByNoteName("B"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // Check note frontmatter - 4 days after the simulated test date of 2023-09-06 + const expectedDueDate: string = "2023-09-10"; + unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 272); + }); + }); + + describe("Review Old note (i.e. previously reviewed); no questions present", () => { + test("Review note with a backlink - Good", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + + // See: tests/vaults/readme.md + // See: tests/vaults/notes4/readme.md + await osrCore.loadTestVault("notes4"); + + // Initial histogram + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + let expectedHistogram: NoteDueDateHistogram = new NoteDueDateHistogram({ + 4: 1, + }); + expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram); + + // Review note A + const file = osrCore.getFileByNoteName("A"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Good, settings); + + // Check note frontmatter - 11 days after the simulated test date of 2023-09-06 + const expectedDueDate: string = "2023-09-17"; + unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 11, 270); + + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + expectedHistogram = new NoteDueDateHistogram({ + 11: 1, + }); + expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram); + }); + + test("Review note with a backlink - Hard", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + + // See: tests/vaults/readme.md + // See: tests/vaults/notes4/readme.md + await osrCore.loadTestVault("notes4"); + + // Review note A + const file = osrCore.getFileByNoteName("A"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Hard, settings); + + // Check note frontmatter - 2 days after the simulated test date of 2023-09-06 + const expectedDueDate: string = "2023-09-08"; + unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 2, 250); + }); + }); + + describe("Review New note (i.e. not previously reviewed); questions present and some previously reviewed", () => { + test("New note without any backlinks", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + await osrCore.loadTestVault("notes1"); + + // Review the note + const file = osrCore.getFileByNoteName("Computation Graph"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // Check note frontmatter - 4 days after the simulated test date of 2023-09-06 + const expectedDueDate: string = "2023-09-10"; + unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 270); + }); + + // The notes that have links to [[A]] themselves haven't been reviewed, + // So the expected post-review schedule is the same as if no files had links to [[A]] + test("Review note with some backlinks (source files without reviews)", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + await osrCore.loadTestVault("notes3"); + + // Review the note + const file = osrCore.getFileByNoteName("A"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // Check note frontmatter - 4 days after the simulated test date of 2023-09-06 + const expectedDueDate: string = "2023-09-10"; + unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 270); + }); + + test("Review note with a backlink (one source file already reviewed)", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + + // See: tests\vaults\readme.md + await osrCore.loadTestVault("notes4"); + + // Review note B + const file = osrCore.getFileByNoteName("B"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // Check note frontmatter - 4 days after the simulated test date of 2023-09-06 + const expectedDueDate: string = "2023-09-10"; + unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 272); + }); + }); + + describe("loadNote", () => { + test("There is schedule info for 3 cards, but only 2 cards in the question", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + settings.cardCommentOnSameLine = true; + settings.convertCurlyBracketsToClozes = true; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + await osrCore.loadTestVault("notes6"); + + /* +A {{question}} with multiple parts {{Navevo part}} + +The final schedule info "!2033-03-03,3,333" has been deleted + */ + const file = osrCore.getFileByNoteName("A"); + expect(file.content).toContain(""); + }); + }); +}); + +describe("Note Due Date Histogram", () => { + test("New note", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + await osrCore.loadTestVault("notes1"); + + // Initial status + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + + // Review the note + const file = osrCore.getFileByNoteName("Computation Graph"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // Check histogram - in 4 days there is one card due + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + const expectedHistogram: NoteDueDateHistogram = new NoteDueDateHistogram({ + 4: 1, + }); + expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram); + }); + + test("Review old note - Good", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + + // See: tests/vaults/readme.md + // See: tests/vaults/notes4/readme.md + await osrCore.loadTestVault("notes4"); + + // Initial histogram + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + let expectedHistogram: NoteDueDateHistogram = new NoteDueDateHistogram({ + 4: 1, + }); + expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram); + + // Review note A + const file = osrCore.getFileByNoteName("A"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Good, settings); + + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + expectedHistogram = new NoteDueDateHistogram({ + 11: 1, + }); + expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram); + }); + + test("Review multiple notes", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + + // See: tests/vaults/readme.md + // See: tests/vaults/notes4/readme.md + await osrCore.loadTestVault("notes4"); + + // Review all the notes + let file: ISRFile = osrCore.getFileByNoteName("A"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Good, settings); + file = osrCore.getFileByNoteName("B"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Hard, settings); + file = osrCore.getFileByNoteName("C"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Hard, settings); + file = osrCore.getFileByNoteName("D"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Good, settings); + + expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0); + const expectedHistogram: NoteDueDateHistogram = new NoteDueDateHistogram({ + 1: 2, + 3: 1, + 11: 1, + }); + expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram); + }); +}); + +describe("Note review - bury all flashcards", () => { + test("burySiblingCards - false", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + settings.burySiblingCards = false; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + await osrCore.loadTestVault("notes5"); + + // Nothing initially on the postponement list + expect(osrCore.questionPostponementList.list.length).toEqual(0); + + // Review the note + const file = osrCore.getFileByNoteName("D"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // Because burySiblingCards is false, nothing has been added to the postponement list + expect(osrCore.questionPostponementList.list.length).toEqual(0); + }); + + test("burySiblingCards - true", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + settings.burySiblingCards = true; + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings); + await osrCore.loadTestVault("notes5"); + + // Nothing initially on the postponement list + expect(osrCore.questionPostponementList.list.length).toEqual(0); + + // Review the note + const file = osrCore.getFileByNoteName("D"); + await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings); + + // The two cards in note D have been added to the postponement list + expect(osrCore.questionPostponementList.list.length).toEqual(2); + }); +}); diff --git a/tests/unit/data-store-algorithm/data-store-in-note-algorithm-osr.test.ts b/tests/unit/data-store-algorithm/data-store-in-note-algorithm-osr.test.ts new file mode 100644 index 00000000..45905947 --- /dev/null +++ b/tests/unit/data-store-algorithm/data-store-in-note-algorithm-osr.test.ts @@ -0,0 +1,74 @@ +import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/rep-item-schedule-info-osr"; +import { Card } from "src/card"; +import { DataStoreInNote_AlgorithmOsr } from "src/data-store-algorithm/data-store-in-note-algorithm-osr"; +import { DEFAULT_SETTINGS, SRSettings } from "src/settings"; +import { setupStaticDateProvider_20230906 } from "src/utils/dates"; + +import { UnitTestSRFile } from "../helpers/unit-test-file"; + +beforeAll(() => { + setupStaticDateProvider_20230906(); +}); + +describe("noteSetSchedule", () => { + test("File originally has frontmatter (but not OSR note scheduling frontmatter)", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const instance: DataStoreInNote_AlgorithmOsr = new DataStoreInNote_AlgorithmOsr(settings); + + const noteText: string = `--- +created: 2024-01-17 +--- +A very interesting note +`; + const file: UnitTestSRFile = new UnitTestSRFile(noteText); + const scheduleInfo: RepItemScheduleInfo_Osr = RepItemScheduleInfo_Osr.fromDueDateStr( + "2023-10-06", + 25, + 263, + ); + await instance.noteSetSchedule(file, scheduleInfo); + + const expectedText: string = `--- +created: 2024-01-17 +sr-due: 2023-10-06 +sr-interval: 25 +sr-ease: 263 +--- +A very interesting note +`; + expect(file.content).toEqual(expectedText); + }); +}); + +describe("formatCardSchedule", () => { + test("Has schedule, with due date", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const instance: DataStoreInNote_AlgorithmOsr = new DataStoreInNote_AlgorithmOsr(settings); + + const scheduleInfo: RepItemScheduleInfo_Osr = RepItemScheduleInfo_Osr.fromDueDateStr( + "2023-10-06", + 25, + 263, + ); + const card: Card = new Card({ + scheduleInfo, + }); + expect(instance.formatCardSchedule(card)).toEqual("!2023-10-06,25,263"); + }); + + test("Has schedule, but no due date", async () => { + const settings: SRSettings = { ...DEFAULT_SETTINGS }; + const instance: DataStoreInNote_AlgorithmOsr = new DataStoreInNote_AlgorithmOsr(settings); + + const scheduleInfo: RepItemScheduleInfo_Osr = new RepItemScheduleInfo_Osr( + null, + 25, + 303, + null, + ); + const card: Card = new Card({ + scheduleInfo, + }); + expect(instance.formatCardSchedule(card)).toEqual("!2000-01-01,25,303"); + }); +}); diff --git a/tests/unit/data-stores/data-store.test.ts b/tests/unit/data-stores/data-store.test.ts new file mode 100644 index 00000000..ad1e05aa --- /dev/null +++ b/tests/unit/data-stores/data-store.test.ts @@ -0,0 +1,8 @@ +import { DataStore } from "src/data-stores/base/data-store"; + +test("getInstance() not initialised exception", () => { + const t = () => { + DataStore.getInstance(); + }; + expect(t).toThrow(Error); +}); diff --git a/tests/unit/data-stores/rep-item-storage-info.test.ts b/tests/unit/data-stores/rep-item-storage-info.test.ts new file mode 100644 index 00000000..7146e962 --- /dev/null +++ b/tests/unit/data-stores/rep-item-storage-info.test.ts @@ -0,0 +1,5 @@ +import { RepItemStorageInfo } from "src/data-stores/base/rep-item-storage-info"; + +test("Just to make code coverage analysis happy", () => { + new RepItemStorageInfo(); +}); diff --git a/tests/unit/DeckTreeIterator.test.ts b/tests/unit/deck-tree-iterator.test.ts similarity index 74% rename from tests/unit/DeckTreeIterator.test.ts rename to tests/unit/deck-tree-iterator.test.ts index 742bb413..3dcc999d 100644 --- a/tests/unit/DeckTreeIterator.test.ts +++ b/tests/unit/deck-tree-iterator.test.ts @@ -1,39 +1,27 @@ -import { NoteQuestionParser } from "src/NoteQuestionParser"; -import { CardListType, Deck } from "src/Deck"; +import { CardListType, Deck } from "src/deck"; +import { CardOrder, DeckOrder, DeckTreeIterator } from "src/deck-tree-iterator"; import { DEFAULT_SETTINGS } from "src/settings"; -import { SampleItemDecks } from "./SampleItems"; -import { TopicPath } from "src/TopicPath"; -import { CardOrder, DeckTreeIterator, IIteratorOrder, DeckOrder } from "src/DeckTreeIterator"; -import { - StaticDateProvider, - globalDateProvider, - setupStaticDateProvider_20230906, -} from "src/util/DateProvider"; -import { - setupNextRandomNumber, - setupStaticRandomNumberProvider, -} from "src/util/RandomNumberProvider"; - -let order_DueFirst_Sequential: IIteratorOrder = { - cardOrder: CardOrder.DueFirstSequential, - deckOrder: DeckOrder.PrevDeckComplete_Sequential, -}; - -var iterator: DeckTreeIterator; +import { TopicPath } from "src/topic-path"; +import { setupStaticDateProvider_20230906 } from "src/utils/dates"; +import { setupNextRandomNumber, setupStaticRandomNumberProvider } from "src/utils/numbers"; + +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; +import { SampleItemDecks } from "./sample-items"; beforeAll(() => { setupStaticDateProvider_20230906(); setupStaticRandomNumberProvider(); + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); }); describe("setDeck", () => { test("currentDeck null immediately after setDeck", async () => { - let text: string = ` + const text: string = ` Q1::A1 Q2::A2 Q3::A3`; - let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"])); - let iterator: DeckTreeIterator = new DeckTreeIterator( + const deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"])); + const iterator: DeckTreeIterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -49,15 +37,15 @@ describe("nextCard - Cards only present in a single deck", () => { describe("DeckOrder.PrevDeckComplete_Sequential; Sequential card ordering", () => { describe("Due cards before new cards", () => { test("Single topic, new cards only", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3`; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - let iterator: DeckTreeIterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.DueFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -82,18 +70,18 @@ Q3::A3`; describe("Single topic, mixture of new and scheduled cards", () => { test("Get the scheduled cards first", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 Q4::A4 Q5::A5 Q6::A6`; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - iterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.DueFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -103,14 +91,14 @@ Q6::A6`; iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards")); // Scheduled cards first - nextCardThenCheck("Q2"); - nextCardThenCheck("Q4"); - nextCardThenCheck("Q5"); + nextCardThenCheck(iterator, "Q2"); + nextCardThenCheck(iterator, "Q4"); + nextCardThenCheck(iterator, "Q5"); // New cards next - nextCardThenCheck("Q1"); - nextCardThenCheck("Q3"); - nextCardThenCheck("Q6"); + nextCardThenCheck(iterator, "Q1"); + nextCardThenCheck(iterator, "Q3"); + nextCardThenCheck(iterator, "Q6"); // Check no more expect(iterator.nextCard()).toEqual(false); @@ -119,7 +107,7 @@ Q6::A6`; describe("Multiple topics, mixture of new and scheduled cards", () => { test("Get the ancestor deck's cards first, then descendants", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3 @@ -129,14 +117,14 @@ Q6::A6`; #flashcards/science/physics Q6::A6 #flashcards/science/physics Q7::A7 - + #flashcards/science/chemistry Q8::A8 `; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - iterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.DueFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -146,22 +134,22 @@ Q6::A6`; iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards")); // Due root deck's cards first - nextCardThenCheck("Q2"); + nextCardThenCheck(iterator, "Q2"); // Then the new cards - nextCardThenCheck("Q1"); - nextCardThenCheck("Q3"); + nextCardThenCheck(iterator, "Q1"); + nextCardThenCheck(iterator, "Q3"); // Then subdeck #flashcards/science (due then new) - nextCardThenCheck("Q4"); - nextCardThenCheck("Q5"); + nextCardThenCheck(iterator, "Q4"); + nextCardThenCheck(iterator, "Q5"); // Then subdeck #flashcards/science/physics - nextCardThenCheck("Q6"); - nextCardThenCheck("Q7"); + nextCardThenCheck(iterator, "Q6"); + nextCardThenCheck(iterator, "Q7"); // Then subdeck #flashcards/science/chemistry - nextCardThenCheck("Q8"); + nextCardThenCheck(iterator, "Q8"); // Check no more expect(iterator.nextCard()).toEqual(false); @@ -171,15 +159,15 @@ Q6::A6`; describe("New cards before due cards", () => { test("Single topic, new cards only", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3`; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - let iterator: DeckTreeIterator = new DeckTreeIterator( + const iterator: DeckTreeIterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -203,18 +191,18 @@ Q3::A3`; describe("Single topic, mixture of new and scheduled cards", () => { test("Get the new cards first", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 Q4::A4 Q5::A5 Q6::A6`; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - iterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -224,32 +212,32 @@ Q6::A6`; iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards")); // New cards first - nextCardThenCheck("Q1"); - nextCardThenCheck("Q3"); - nextCardThenCheck("Q6"); + nextCardThenCheck(iterator, "Q1"); + nextCardThenCheck(iterator, "Q3"); + nextCardThenCheck(iterator, "Q6"); // Scheduled cards next - nextCardThenCheck("Q2"); - nextCardThenCheck("Q4"); - nextCardThenCheck("Q5"); + nextCardThenCheck(iterator, "Q2"); + nextCardThenCheck(iterator, "Q4"); + nextCardThenCheck(iterator, "Q5"); // Check no more expect(iterator.nextCard()).toEqual(false); }); test("Get the scheduled cards first", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 Q4::A4 Q5::A5 Q6::A6`; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - iterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.DueFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -259,14 +247,14 @@ Q6::A6`; iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards")); // Scheduled cards first - nextCardThenCheck("Q2"); - nextCardThenCheck("Q4"); - nextCardThenCheck("Q5"); + nextCardThenCheck(iterator, "Q2"); + nextCardThenCheck(iterator, "Q4"); + nextCardThenCheck(iterator, "Q5"); // New cards next - nextCardThenCheck("Q1"); - nextCardThenCheck("Q3"); - nextCardThenCheck("Q6"); + nextCardThenCheck(iterator, "Q1"); + nextCardThenCheck(iterator, "Q3"); + nextCardThenCheck(iterator, "Q6"); // Check no more expect(iterator.nextCard()).toEqual(false); @@ -275,7 +263,7 @@ Q6::A6`; describe("Multiple topics, mixture of new and scheduled cards", () => { test("Get the ancestor deck's cards first, then descendants", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3 @@ -285,14 +273,14 @@ Q6::A6`; #flashcards/science/physics Q6::A6 #flashcards/science/physics Q7::A7 - + #flashcards/science/chemistry Q8::A8 `; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - iterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -302,20 +290,20 @@ Q6::A6`; iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards")); // New root deck's cards first - nextCardThenCheck("Q1"); - nextCardThenCheck("Q3"); - nextCardThenCheck("Q2"); + nextCardThenCheck(iterator, "Q1"); + nextCardThenCheck(iterator, "Q3"); + nextCardThenCheck(iterator, "Q2"); // Then subdeck #flashcards/science - nextCardThenCheck("Q5"); - nextCardThenCheck("Q4"); + nextCardThenCheck(iterator, "Q5"); + nextCardThenCheck(iterator, "Q4"); // Then subdeck #flashcards/science/physics - nextCardThenCheck("Q6"); - nextCardThenCheck("Q7"); + nextCardThenCheck(iterator, "Q6"); + nextCardThenCheck(iterator, "Q7"); // Then subdeck #flashcards/science/chemistry - nextCardThenCheck("Q8"); + nextCardThenCheck(iterator, "Q8"); // Check no more expect(iterator.nextCard()).toEqual(false); @@ -327,7 +315,7 @@ Q6::A6`; describe("DeckOrder.PrevDeckComplete_Sequential; Random card ordering", () => { describe("Due cards before new cards", () => { test("All new cards", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q0::A0 Q1::A1 Q2::A2 @@ -335,11 +323,11 @@ Q3::A3 Q4::A4 Q5::A5 Q6::A6`; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - iterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.DueFirstRandom, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -350,32 +338,32 @@ Q6::A6`; // [0, 1, 2, 3, 4, 5, 6] setupNextRandomNumber({ lower: 0, upper: 6, next: 5 }); - nextCardThenCheck("Q5"); + nextCardThenCheck(iterator, "Q5"); // [0, 1, 2, 3, 4, 6] setupNextRandomNumber({ lower: 0, upper: 5, next: 5 }); - nextCardThenCheck("Q6"); + nextCardThenCheck(iterator, "Q6"); // [0, 1, 2, 3, 4] setupNextRandomNumber({ lower: 0, upper: 4, next: 1 }); - nextCardThenCheck("Q1"); + nextCardThenCheck(iterator, "Q1"); // [0, 2, 3, 4] setupNextRandomNumber({ lower: 0, upper: 3, next: 3 }); - nextCardThenCheck("Q4"); + nextCardThenCheck(iterator, "Q4"); // [0, 2, 3] setupNextRandomNumber({ lower: 0, upper: 2, next: 1 }); - nextCardThenCheck("Q2"); + nextCardThenCheck(iterator, "Q2"); // [0, 3] setupNextRandomNumber({ lower: 0, upper: 1, next: 1 }); - nextCardThenCheck("Q3"); + nextCardThenCheck(iterator, "Q3"); // [0] setupNextRandomNumber({ lower: 0, upper: 0, next: 0 }); - nextCardThenCheck("Q0"); + nextCardThenCheck(iterator, "Q0"); // Check no more expect(iterator.nextCard()).toEqual(false); }); test("Mixture new/scheduled", async () => { - let text: string = `#flashcards + const text: string = `#flashcards QN0::A QS0::A QN1::A @@ -384,11 +372,11 @@ QS2::A QN2::A QN3::A QS3::Q `; - let deck: Deck = await SampleItemDecks.createDeckFromText( + const deck: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); - iterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.DueFirstRandom, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -400,36 +388,36 @@ QS3::Q `; // Scheduled cards first // [QN0, QN1, QN2, QN3], [QS0, QS1, QS2, QS3] setupNextRandomNumber({ lower: 0, upper: 3, next: 3 }); - nextCardThenCheck("QS3"); + nextCardThenCheck(iterator, "QS3"); // [QN0, QN1, QN2, QN3], [QS0, QS1, QS2] setupNextRandomNumber({ lower: 0, upper: 2, next: 1 }); - nextCardThenCheck("QS1"); + nextCardThenCheck(iterator, "QS1"); // [QN0, QN1, QN2, QN3], [QS0, QS2] setupNextRandomNumber({ lower: 0, upper: 1, next: 0 }); - nextCardThenCheck("QS0"); + nextCardThenCheck(iterator, "QS0"); // [QN0, QN1, QN2, QN3], [QS2] setupNextRandomNumber({ lower: 0, upper: 0, next: 0 }); - nextCardThenCheck("QS2"); + nextCardThenCheck(iterator, "QS2"); // New cards next // [QN0, QN1, QN2, QN3] setupNextRandomNumber({ lower: 0, upper: 3, next: 2 }); - nextCardThenCheck("QN2"); + nextCardThenCheck(iterator, "QN2"); // [QN0, QN1, QN3] setupNextRandomNumber({ lower: 0, upper: 2, next: 2 }); - nextCardThenCheck("QN3"); + nextCardThenCheck(iterator, "QN3"); // [QN0, QN1] setupNextRandomNumber({ lower: 0, upper: 1, next: 0 }); - nextCardThenCheck("QN0"); + nextCardThenCheck(iterator, "QN0"); // [QN1] setupNextRandomNumber({ lower: 0, upper: 0, next: 0 }); - nextCardThenCheck("QN1"); + nextCardThenCheck(iterator, "QN1"); // Check no more expect(iterator.nextCard()).toEqual(false); @@ -439,7 +427,7 @@ QS3::Q `; describe("DeckOrder.PrevDeckComplete_Random", () => { test("CardOrder.NewFirstSequential", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3 @@ -452,8 +440,8 @@ QS3::Q `; #flashcards/science/chemistry Q8::A8 `; - let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); - iterator = new DeckTreeIterator( + const deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Random, @@ -464,28 +452,28 @@ QS3::Q `; // New root deck's cards first Q1/Q3, then due cards - Q2 setupNextRandomNumber({ lower: 0, upper: 3, next: 0 }); - nextCardThenCheck("Q1"); - nextCardThenCheck("Q3"); - nextCardThenCheck("Q2"); + nextCardThenCheck(iterator, "Q1"); + nextCardThenCheck(iterator, "Q3"); + nextCardThenCheck(iterator, "Q2"); // 3 decks with cards present to choose from (hence we expect the random number provider to be asked // for a random number 0... 2): // [0=#flashcards/science, 1=#flashcards/science/physics, 2=#flashcards/science/chemistry] // Have the random number provider return 1 when next asked; deck 1 corresponds to - #flashcards/science/physics setupNextRandomNumber({ lower: 0, upper: 2, next: 1 }); - nextCardThenCheck("Q6"); - nextCardThenCheck("Q7"); + nextCardThenCheck(iterator, "Q6"); + nextCardThenCheck(iterator, "Q7"); // 2 decks to choose from [#flashcards/science, #flashcards/science/chemistry] // Then random deck - #flashcards/science/chemistry setupNextRandomNumber({ lower: 0, upper: 1, next: 1 }); - nextCardThenCheck("Q8"); + nextCardThenCheck(iterator, "Q8"); // 1 deck to choose from [#flashcards/science] // Then subdeck #flashcards/science/chemistry setupNextRandomNumber({ lower: 0, upper: 0, next: 0 }); - nextCardThenCheck("Q5"); - nextCardThenCheck("Q4"); + nextCardThenCheck(iterator, "Q5"); + nextCardThenCheck(iterator, "Q4"); // Check no more expect(iterator.nextCard()).toEqual(false); @@ -494,7 +482,7 @@ QS3::Q `; describe("DeckOrder.EveryCardRandomDeckAndCard", () => { test("Simple test", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3 @@ -507,8 +495,8 @@ QS3::Q `; #flashcards/science/chemistry Q8::A8 `; - let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); - iterator = new DeckTreeIterator( + const deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.EveryCardRandomDeckAndCard, deckOrder: null, @@ -522,35 +510,35 @@ QS3::Q `; // [0=Q1, 1=Q3, 2=Q2, 3=Q5, 4=Q4, 5=Q6, 6=Q7, 7=Q8] // Have the random number provider return 5 when next asked; 5 corresponds to Q6 setupNextRandomNumber({ lower: 0, upper: 7, next: 5 }); - nextCardThenCheck("Q6"); + nextCardThenCheck(iterator, "Q6"); // [0=Q1, 1=Q3, 2=Q2, 3=Q5, 4=Q4, 5=Q7, 6=Q8] setupNextRandomNumber({ lower: 0, upper: 6, next: 3 }); - nextCardThenCheck("Q5"); + nextCardThenCheck(iterator, "Q5"); // [0=Q1, 1=Q3, 2=Q2, 3=Q4, 4=Q7, 5=Q8] setupNextRandomNumber({ lower: 0, upper: 5, next: 1 }); - nextCardThenCheck("Q3"); + nextCardThenCheck(iterator, "Q3"); // [0=Q1, 1=Q2, 2=Q4, 3=Q7, 4=Q8] setupNextRandomNumber({ lower: 0, upper: 4, next: 0 }); - nextCardThenCheck("Q1"); + nextCardThenCheck(iterator, "Q1"); // [0=Q2, 1=Q4, 2=Q7, 3=Q8] setupNextRandomNumber({ lower: 0, upper: 3, next: 3 }); - nextCardThenCheck("Q8"); + nextCardThenCheck(iterator, "Q8"); // [0=Q2, 1=Q4, 2=Q7] setupNextRandomNumber({ lower: 0, upper: 2, next: 1 }); - nextCardThenCheck("Q4"); + nextCardThenCheck(iterator, "Q4"); // [0=Q2, 1=Q7] setupNextRandomNumber({ lower: 0, upper: 1, next: 1 }); - nextCardThenCheck("Q7"); + nextCardThenCheck(iterator, "Q7"); // [0=Q2] setupNextRandomNumber({ lower: 0, upper: 0, next: 0 }); - nextCardThenCheck("Q2"); + nextCardThenCheck(iterator, "Q2"); // Check no more expect(iterator.nextCard()).toEqual(false); @@ -561,7 +549,7 @@ QS3::Q `; describe("nextCard - Some cards present in multiple decks", () => { describe("DeckOrder.PrevDeckComplete_Sequential; Sequential card ordering", () => { test("Iterating over complete deck tree", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 #flashcards/folder1 @@ -574,7 +562,7 @@ Q31::A31 Q11::A11 Q12::A12 `; - const [deck, iterator] = await SampleItemDecks.createDeckAndIteratorFromText( + const [_, iterator] = await SampleItemDecks.createDeckAndIteratorFromText( text, TopicPath.emptyPath, CardOrder.DueFirstSequential, @@ -605,7 +593,7 @@ Q12::A12 }); test("Iterating over portion of deck tree still deletes hard-linked cards in non-iterated portion of the deck", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 #flashcards/folder1 @@ -649,12 +637,12 @@ Q12::A12 describe("hasCurrentCard", () => { test("false immediately after setDeck", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3`; - let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); - let iterator: DeckTreeIterator = new DeckTreeIterator( + const deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); + const iterator: DeckTreeIterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -667,12 +655,12 @@ describe("hasCurrentCard", () => { }); test("true immediately after nextCard", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3`; - let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); - let iterator: DeckTreeIterator = new DeckTreeIterator( + const deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); + const iterator: DeckTreeIterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -688,12 +676,12 @@ describe("hasCurrentCard", () => { describe("deleteCurrentCard", () => { test("Delete after all cards iterated - exception throw", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3`; - let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); - iterator = new DeckTreeIterator( + const deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -714,14 +702,14 @@ Q3::A3`; }); test("Delete card, with single card remaining after it", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3`; - let deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); + const deck: Deck = await SampleItemDecks.createDeckFromText(text, TopicPath.emptyPath); const flashcardDeck: Deck = deck.getDeckByTopicTag("#flashcards"); expect(flashcardDeck.newFlashcards.length).toEqual(3); - iterator = new DeckTreeIterator( + const iterator = new DeckTreeIterator( { cardOrder: CardOrder.NewFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, @@ -730,15 +718,15 @@ Q3::A3`; ); iterator.setIteratorTopicPath(TopicPath.getTopicPathFromTag("#flashcards")); - nextCardThenCheck("Q1"); - nextCardThenCheck("Q2"); + nextCardThenCheck(iterator, "Q1"); + nextCardThenCheck(iterator, "Q2"); expect(iterator.deleteCurrentCardFromAllDecks()).toEqual(true); expect(iterator.currentCard.front).toEqual("Q3"); expect(iterator.deleteCurrentCardFromAllDecks()).toEqual(false); }); }); -function nextCardThenCheck(expectedFront: string): void { +function nextCardThenCheck(iterator: DeckTreeIterator, expectedFront: string): void { expect(iterator.nextCard()).toEqual(true); expect(iterator.currentCard.front).toEqual(expectedFront); } diff --git a/tests/unit/deck.test.ts b/tests/unit/deck.test.ts index 25e305c5..0562579b 100644 --- a/tests/unit/deck.test.ts +++ b/tests/unit/deck.test.ts @@ -1,11 +1,18 @@ -import { CardListType, Deck } from "src/Deck"; -import { TopicPath } from "src/TopicPath"; -import { SampleItemDecks } from "./SampleItems"; -import { Card } from "src/Card"; +import { Card } from "src/card"; +import { CardListType, Deck } from "src/deck"; +import { DEFAULT_SETTINGS } from "src/settings"; +import { TopicPath } from "src/topic-path"; + +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; +import { SampleItemDecks } from "./sample-items"; + +beforeAll(() => { + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); +}); describe("constructor", () => { test("Deck name", () => { - let actual: Deck = new Deck("Great Name", null); + const actual: Deck = new Deck("Great Name", null); expect(actual.deckName).toEqual("Great Name"); }); @@ -13,7 +20,7 @@ describe("constructor", () => { describe("getOrCreateDeck()", () => { test("Empty topic path", () => { - let deck: Deck = new Deck("Great Name", null); + const deck: Deck = new Deck("Great Name", null); deck.getOrCreateDeck(TopicPath.emptyPath); expect(deck.deckName).toEqual("Great Name"); @@ -21,9 +28,9 @@ describe("getOrCreateDeck()", () => { }); test("Create single subdeck on empty deck", () => { - let deck: Deck = new Deck("Root", null); - let path: TopicPath = new TopicPath(["Level1"]); - let subdeck: Deck = deck.getOrCreateDeck(path); + const deck: Deck = new Deck("Root", null); + const path: TopicPath = new TopicPath(["Level1"]); + const subdeck: Deck = deck.getOrCreateDeck(path); expect(deck.deckName).toEqual("Root"); expect(deck.subdecks.length).toEqual(1); @@ -32,10 +39,10 @@ describe("getOrCreateDeck()", () => { }); test("Create multiple subdecks under single deck", () => { - let deck: Deck = new Deck("Root", null); - let subdeckA: Deck = deck.getOrCreateDeck(new TopicPath(["Level1A"])); - let subdeckB: Deck = deck.getOrCreateDeck(new TopicPath(["Level1B"])); - let subdeckC: Deck = deck.getOrCreateDeck(new TopicPath(["Level1C"])); + const deck: Deck = new Deck("Root", null); + const subdeckA: Deck = deck.getOrCreateDeck(new TopicPath(["Level1A"])); + const subdeckB: Deck = deck.getOrCreateDeck(new TopicPath(["Level1B"])); + const subdeckC: Deck = deck.getOrCreateDeck(new TopicPath(["Level1C"])); expect(deck.deckName).toEqual("Root"); expect(deck.subdecks.length).toEqual(3); @@ -51,10 +58,10 @@ describe("getOrCreateDeck()", () => { }); test("Create multi-level deck in separate steps", () => { - let deck: Deck = new Deck("Root", null); - let subdeck1: Deck = deck.getOrCreateDeck(new TopicPath(["Level1"])); - let subdeck2: Deck = subdeck1.getOrCreateDeck(new TopicPath(["Level2"])); - let subdeck3: Deck = subdeck2.getOrCreateDeck(new TopicPath(["Level3"])); + const deck: Deck = new Deck("Root", null); + const subdeck1: Deck = deck.getOrCreateDeck(new TopicPath(["Level1"])); + const subdeck2: Deck = subdeck1.getOrCreateDeck(new TopicPath(["Level2"])); + const subdeck3: Deck = subdeck2.getOrCreateDeck(new TopicPath(["Level3"])); expect(deck.deckName).toEqual("Root"); expect(deck.subdecks.length).toEqual(1); @@ -73,17 +80,17 @@ describe("getOrCreateDeck()", () => { }); test("Create multi-level deck in single step", () => { - let deck: Deck = new Deck("Root", null); - let subdeck3: Deck = deck.getOrCreateDeck(new TopicPath(["Level1", "Level2", "Level3"])); + const deck: Deck = new Deck("Root", null); + const subdeck3: Deck = deck.getOrCreateDeck(new TopicPath(["Level1", "Level2", "Level3"])); expect(deck.deckName).toEqual("Root"); expect(deck.subdecks.length).toEqual(1); - let subdeck1: Deck = deck.subdecks[0]; + const subdeck1: Deck = deck.subdecks[0]; expect(subdeck1.deckName).toEqual("Level1"); expect(subdeck1.subdecks.length).toEqual(1); - let subdeck2: Deck = subdeck1.subdecks[0]; + const subdeck2: Deck = subdeck1.subdecks[0]; expect(subdeck2.deckName).toEqual("Level2"); expect(subdeck2.subdecks.length).toEqual(1); @@ -95,7 +102,7 @@ describe("getOrCreateDeck()", () => { describe("getDistinctCardCount()", () => { test("Single deck", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 @@ -108,7 +115,7 @@ Q4::A4 `; }); test("Deck hierarchy - no duplicate cards", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 @@ -137,7 +144,7 @@ Q15::A15 Q2::A2 Q3::A3 `; - let original: Deck = await SampleItemDecks.createDeckFromText( + const original: Deck = await SampleItemDecks.createDeckFromText( text, TopicPath.emptyPath, ); @@ -310,45 +317,43 @@ Q3::A3 `; describe("Multi level tree", () => { test("No change in original deck after copy", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3 - + #flashcards/science Q4::A4 #flashcards/science Q5::A5 - + #flashcards/science/physics Q6::A6`; - let original: Deck = await SampleItemDecks.createDeckFromText( + const original: Deck = await SampleItemDecks.createDeckFromText( text, new TopicPath(["Root"]), ); - let originalCountPreCopy: number = original.getCardCount(CardListType.All, true); + const originalCountPreCopy: number = original.getCardCount(CardListType.All, true); expect(originalCountPreCopy).toEqual(6); - let copy: Deck = original.copyWithCardFilter( - (card) => parseInt(card.front[1]) % 2 == 1, - ); - let originalCountPostCopy: number = original.getCardCount(CardListType.All, true); + original.copyWithCardFilter((card) => parseInt(card.front[1]) % 2 == 1); + const originalCountPostCopy: number = original.getCardCount(CardListType.All, true); expect(originalCountPreCopy).toEqual(originalCountPostCopy); }); test("With new cards", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3 - + #flashcards/science Q4::A4 #flashcards/science Q5::A5 - + #flashcards/science/physics Q6::A6`; - let original: Deck = await SampleItemDecks.createDeckFromText( + const original: Deck = await SampleItemDecks.createDeckFromText( text, new TopicPath(["Root"]), ); - let copy: Deck = original.copyWithCardFilter( + const copy: Deck = original.copyWithCardFilter( (card) => parseInt(card.front[1]) % 2 == 1, ); @@ -369,7 +374,7 @@ Q3::A3 `; describe("deleteCardFromAllDecks()", () => { test("Single deck", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 @@ -387,7 +392,7 @@ Q4::A4 `; }); test("Deck hierarchy - with duplicate cards", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 @@ -409,7 +414,7 @@ Q21::A21 // Delete card from #flashcards/folder1, deletes from #flashcards/folder as well const folder1: Deck = deck.getDeckByTopicTag("#flashcards/folder1"); const folder2: Deck = deck.getDeckByTopicTag("#flashcards/folder2"); - let card: Card = folder1.newFlashcards[1]; + const card: Card = folder1.newFlashcards[1]; expect(folder1.getCardCount(CardListType.NewCard, false)).toEqual(3); expect(folder2.getCardCount(CardListType.NewCard, false)).toEqual(3 + 1); deck.deleteCardFromAllDecks(card, true); diff --git a/tests/unit/FlashcardReviewSequencer.test.ts b/tests/unit/flashcard-review-sequencer.test.ts similarity index 76% rename from tests/unit/FlashcardReviewSequencer.test.ts rename to tests/unit/flashcard-review-sequencer.test.ts index 7d67a93f..370694bc 100644 --- a/tests/unit/FlashcardReviewSequencer.test.ts +++ b/tests/unit/flashcard-review-sequencer.test.ts @@ -1,51 +1,52 @@ -import { CardScheduleCalculator } from "src/CardSchedule"; +import moment from "moment"; + +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { SrsAlgorithm } from "src/algorithms/base/srs-algorithm"; +import { CardListType, Deck, DeckTreeFilter } from "src/deck"; import { CardOrder, DeckOrder, DeckTreeIterator, IDeckTreeIterator, IIteratorOrder, -} from "src/DeckTreeIterator"; +} from "src/deck-tree-iterator"; +import { CardDueDateHistogram } from "src/due-date-histogram"; import { DeckStats, FlashcardReviewMode, FlashcardReviewSequencer, IFlashcardReviewSequencer, -} from "src/FlashcardReviewSequencer"; -import { TopicPath } from "src/TopicPath"; -import { CardListType, Deck, DeckTreeFilter } from "src/Deck"; +} from "src/flashcard-review-sequencer"; +import { QuestionPostponementList } from "src/question-postponement-list"; import { DEFAULT_SETTINGS, SRSettings } from "src/settings"; -import { SampleItemDecks } from "./SampleItems"; -import { ReviewResponse } from "src/scheduling"; +import { TopicPath } from "src/topic-path"; import { - setupStaticDateProvider, setupStaticDateProvider_20230906, setupStaticDateProvider_OriginDatePlusDays, -} from "src/util/DateProvider"; -import moment from "moment"; -import { INoteEaseList, NoteEaseList } from "src/NoteEaseList"; -import { QuestionPostponementList, IQuestionPostponementList } from "src/QuestionPostponementList"; -import { UnitTestSRFile } from "./helpers/UnitTestSRFile"; +} from "src/utils/dates"; -let order_DueFirst_Sequential: IIteratorOrder = { +import { UnitTestSRFile } from "./helpers/unit-test-file"; +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; +import { SampleItemDecks } from "./sample-items"; + +const order_DueFirst_Sequential: IIteratorOrder = { cardOrder: CardOrder.DueFirstSequential, deckOrder: DeckOrder.PrevDeckComplete_Sequential, }; -let clozeQuestion1: string = "This single ==question== turns into ==3 separate== ==cards=="; -let clozeQuestion1Card1: RegExp = /This single.+\.\.\..+turns into 3 separate cards/; -let clozeQuestion1Card2: RegExp = /This single question turns into.+\.\.\..+cards/; -let clozeQuestion1Card3: RegExp = /This single question turns into 3 separate.+\.\.\./; +const clozeQuestion1: string = "This single ==question== turns into ==3 separate== ==cards=="; +const clozeQuestion1Card1: RegExp = /This single.+\.\.\..+turns into 3 separate cards/; +const clozeQuestion1Card2: RegExp = /This single question turns into.+\.\.\..+cards/; +const clozeQuestion1Card3: RegExp = /This single question turns into 3 separate.+\.\.\./; class TestContext { settings: SRSettings; reviewMode: FlashcardReviewMode; iteratorOrder: IIteratorOrder; cardSequencer: IDeckTreeIterator; - noteEaseList: INoteEaseList; - cardScheduleCalculator: CardScheduleCalculator; - reviewSequencer: FlashcardReviewSequencer; + reviewSequencer: IFlashcardReviewSequencer; questionPostponementList: QuestionPostponementList; + dueDateFlashcardHistogram: CardDueDateHistogram; file: UnitTestSRFile; originalText: string; fakeFilePath: string; @@ -57,13 +58,14 @@ class TestContext { async resetContext(text: string, daysAfterOrigin: number): Promise { this.originalText = text; this.file.content = text; - let cardSequencer: IDeckTreeIterator = new DeckTreeIterator(this.iteratorOrder, null); - let reviewSequencer: FlashcardReviewSequencer = new FlashcardReviewSequencer( + const cardSequencer: IDeckTreeIterator = new DeckTreeIterator(this.iteratorOrder, null); + new FlashcardReviewSequencer( this.reviewMode, cardSequencer, this.settings, - this.cardScheduleCalculator, + SrsAlgorithm.getInstance(), this.questionPostponementList, + this.dueDateFlashcardHistogram, ); setupStaticDateProvider_OriginDatePlusDays(daysAfterOrigin); @@ -77,7 +79,7 @@ class TestContext { } async setSequencerDeckTreeFromOriginalText(): Promise { - let deckTree: Deck = await SampleItemDecks.createDeckFromFile( + const deckTree: Deck = await SampleItemDecks.createDeckFromFile( this.file, new TopicPath(["Root"]), ); @@ -101,33 +103,30 @@ class TestContext { text: string, fakeFilePath?: string, ): TestContext { - let cardSequencer: IDeckTreeIterator = new DeckTreeIterator(iteratorOrder, null); - let noteEaseList = new NoteEaseList(settings); - let cardScheduleCalculator: CardScheduleCalculator = new CardScheduleCalculator( - settings, - noteEaseList, - ); - let cardPostponementList: QuestionPostponementList = new QuestionPostponementList( + const settingsClone: SRSettings = { ...settings }; + const cardSequencer: IDeckTreeIterator = new DeckTreeIterator(iteratorOrder, null); + unitTestSetup_StandardDataStoreAlgorithm(settingsClone); + const cardPostponementList: QuestionPostponementList = new QuestionPostponementList( null, - settings, + settingsClone, [], ); - let reviewSequencer: FlashcardReviewSequencer = new FlashcardReviewSequencer( + const dueDateFlashcardHistogram: CardDueDateHistogram = new CardDueDateHistogram(); + const reviewSequencer: FlashcardReviewSequencer = new FlashcardReviewSequencer( reviewMode, cardSequencer, - settings, - cardScheduleCalculator, + settingsClone, + SrsAlgorithm.getInstance(), cardPostponementList, + dueDateFlashcardHistogram, ); - var file: UnitTestSRFile = new UnitTestSRFile(text, fakeFilePath); + const file: UnitTestSRFile = new UnitTestSRFile(text, fakeFilePath); - let result: TestContext = new TestContext({ - settings, + const result: TestContext = new TestContext({ + settings: settingsClone, reviewMode, iteratorOrder, cardSequencer, - noteEaseList, - cardScheduleCalculator, reviewSequencer, questionPostponementList: cardPostponementList, file, @@ -150,26 +149,26 @@ async function checkReviewResponse_ReviewMode( reviewResponse: ReviewResponse, info: Info1, ): Promise { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3`; - let fakeFilePath: string = moment().millisecond().toString(); - let c: TestContext = TestContext.Create( + const fakeFilePath: string = moment().millisecond().toString(); + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, text, fakeFilePath, ); - let deck: Deck = await c.setSequencerDeckTreeFromOriginalText(); + await c.setSequencerDeckTreeFromOriginalText(); // State before calling processReview - let card = c.reviewSequencer.currentCard; + const card = c.reviewSequencer.currentCard; expect(card.front).toEqual("Q2"); expect(card.scheduleInfo).toMatchObject({ - ease: 270, + latestEase: 270, interval: 4, }); @@ -178,12 +177,12 @@ async function checkReviewResponse_ReviewMode( expect(c.reviewSequencer.currentCard.front).toEqual("Q1"); // Schedule for the reviewed card has been updated - expect(card.scheduleInfo.ease).toEqual(info.cardQ2_PostReviewEase); + expect(card.scheduleInfo.latestEase).toEqual(info.cardQ2_PostReviewEase); expect(card.scheduleInfo.interval).toEqual(info.cardQ2_PostReviewInterval); expect(card.scheduleInfo.dueDate.unix).toEqual(moment(info.cardQ2_PostReviewDueDate).unix); // Note text has been updated - let expectedText: string = c.originalText.replace( + const expectedText: string = c.originalText.replace( info.cardQ2_PreReviewText, info.cardQ2_PostReviewText, ); @@ -191,27 +190,27 @@ async function checkReviewResponse_ReviewMode( } async function checkReviewResponse_CramMode(reviewResponse: ReviewResponse): Promise { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3 #flashcards Q4::A4 `; - let str: string = moment().millisecond().toString(); - let c: TestContext = TestContext.Create( + const str: string = moment().millisecond().toString(); + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Cram, DEFAULT_SETTINGS, text, str, ); - let deck: Deck = await c.setSequencerDeckTreeFromOriginalText(); + await c.setSequencerDeckTreeFromOriginalText(); // State before calling processReview - let card = c.reviewSequencer.currentCard; + const card = c.reviewSequencer.currentCard; expect(card.front).toEqual("Q1"); - let expectInfo = { - ease: 270, + const expectInfo = { + latestEase: 270, interval: 4, }; expect(card.scheduleInfo).toMatchObject(expectInfo); @@ -225,7 +224,7 @@ async function checkReviewResponse_CramMode(reviewResponse: ReviewResponse): Pro expect(card.scheduleInfo.dueDate.unix).toEqual(moment("2023-09-02").unix); // Note text remains the same - let expectedText: string = c.originalText; + const expectedText: string = c.originalText; expect(await c.file.read()).toEqual(expectedText); return c; @@ -235,7 +234,7 @@ async function setupSample1( reviewMode: FlashcardReviewMode, settings: SRSettings, ): Promise { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 @@ -246,30 +245,10 @@ async function setupSample1( #flashcards/science/physics Q5::A5 #flashcards/math Q6::A6`; - let c: TestContext = TestContext.Create(order_DueFirst_Sequential, reviewMode, settings, text); - await c.setSequencerDeckTreeFromOriginalText(); - return c; -} - -async function setupSample2(reviewMode: FlashcardReviewMode): Promise { - let text: string = ` -#flashcards Q1::A1 - - -#flashcards Q2:::A2 - - -#flashcards Q3::A3 - - -#flashcards This single ==question== turns into ==3 separate== ==cards== - -`; - - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, reviewMode, - DEFAULT_SETTINGS, + settings, text, ); await c.setSequencerDeckTreeFromOriginalText(); @@ -280,10 +259,10 @@ async function checkEmptyPostponementList( burySiblingCards: boolean, flashcardReviewMode: FlashcardReviewMode, ): Promise { - let settings: SRSettings = { ...DEFAULT_SETTINGS }; + const settings: SRSettings = { ...DEFAULT_SETTINGS }; settings.burySiblingCards = burySiblingCards; - let c: TestContext = await setupSample1(flashcardReviewMode, settings); + const c: TestContext = await setupSample1(flashcardReviewMode, settings); expect(c.questionPostponementList.list.length).toEqual(0); expect(c.reviewSequencer.currentCard.front).toEqual("Q2"); @@ -308,7 +287,7 @@ beforeEach(() => { describe("setDeckTree", () => { test("Empty deck", () => { - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, @@ -322,22 +301,22 @@ describe("setDeckTree", () => { // After setDeckTree, the first card in the deck is the current card test("Single level deck with some new cards", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3`; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, text, ); - let deck: Deck = await c.setSequencerDeckTreeFromOriginalText(); + const deck: Deck = await c.setSequencerDeckTreeFromOriginalText(); const flashcardDeck: Deck = deck.getDeckByTopicTag("#flashcards"); expect(flashcardDeck.newFlashcards.length).toEqual(3); expect(c.reviewSequencer.currentDeck.newFlashcards.length).toEqual(3); - let expected = { + const expected = { front: "Q1", back: "A1", }; @@ -347,7 +326,7 @@ Q3::A3`; describe("skipCurrentCard", () => { test("Simple test", async () => { - let c: TestContext = await setupSample1(FlashcardReviewMode.Review, DEFAULT_SETTINGS); + const c: TestContext = await setupSample1(FlashcardReviewMode.Review, DEFAULT_SETTINGS); expect(c.reviewSequencer.currentCard.front).toEqual("Q2"); // No more due cards after current card, so we expect the first new card for topic #flashcards @@ -356,7 +335,7 @@ describe("skipCurrentCard", () => { }); test("Skip repeatedly until no more", async () => { - let c: TestContext = await setupSample1(FlashcardReviewMode.Review, DEFAULT_SETTINGS); + const c: TestContext = await setupSample1(FlashcardReviewMode.Review, DEFAULT_SETTINGS); expect(c.reviewSequencer.currentCard.front).toEqual("Q2"); // No more due cards after current card, so we expect the first new card for topic #flashcards @@ -371,7 +350,7 @@ describe("skipCurrentCard", () => { }); test("Skipping a card skips all sibling cards", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 @@ -385,7 +364,7 @@ describe("skipCurrentCard", () => { `; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, @@ -436,24 +415,24 @@ describe("processReview", () => { describe("FlashcardReviewMode.Review", () => { describe("ReviewResponse.Reset", () => { test("Simple test - 3 cards all due in same deck - reset card moves to end of deck", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3 `; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, text, ); - let deck: Deck = await c.setSequencerDeckTreeFromOriginalText(); + await c.setSequencerDeckTreeFromOriginalText(); // State before calling processReview let card = c.reviewSequencer.currentCard; expect(card.front).toEqual("Q1"); expect(card.scheduleInfo).toMatchObject({ - ease: 270, + latestEase: 270, interval: 4, }); @@ -463,7 +442,7 @@ describe("processReview", () => { card = c.reviewSequencer.currentCard; expect(card.front).toEqual("Q2"); expect(card.scheduleInfo).toMatchObject({ - ease: 270, + latestEase: 270, interval: 5, }); @@ -471,7 +450,7 @@ describe("processReview", () => { card = c.reviewSequencer.currentCard; expect(card.front).toEqual("Q3"); expect(card.scheduleInfo).toMatchObject({ - ease: 270, + latestEase: 270, interval: 6, }); @@ -480,7 +459,7 @@ describe("processReview", () => { card = c.reviewSequencer.currentCard; expect(card.front).toEqual("Q1"); expect(card.scheduleInfo).toMatchObject({ - ease: DEFAULT_SETTINGS.baseEase, + latestEase: DEFAULT_SETTINGS.baseEase, interval: 1, }); }); @@ -502,17 +481,17 @@ describe("processReview", () => { describe("Checking postponement list (after card reviewed, burySiblingCards=false)", () => { test("reviewed question not added to postponement list; sibling cards are sequenced (not deleted)", async () => { - let settings: SRSettings = { ...DEFAULT_SETTINGS }; + const settings: SRSettings = { ...DEFAULT_SETTINGS }; settings.burySiblingCards = false; - let text: string = `#flashcards + const text: string = `#flashcards #flashcards This single ==question== turns into ==3 separate== ==cards== Q1::A1 `; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, settings, @@ -542,17 +521,17 @@ Q1::A1 describe("Checking postponement list (after card reviewed, burySiblingCards=true)", () => { test("Question with multiple cards; reviewed question added to postponement list; sibling cards are buried", async () => { - let settings: SRSettings = { ...DEFAULT_SETTINGS }; + const settings: SRSettings = { ...DEFAULT_SETTINGS }; settings.burySiblingCards = true; - let text: string = ` + const text: string = ` #flashcards ${clozeQuestion1} #flashcards Q1::A1 `; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, settings, @@ -576,7 +555,7 @@ Q1::A1 }); test("Question with multiple cards; card reviewed as hard, after restarting the review process, that whole question skipped and next question is shown", async () => { - let settings: SRSettings = { ...DEFAULT_SETTINGS }; + const settings: SRSettings = { ...DEFAULT_SETTINGS }; settings.burySiblingCards = true; let text: string = ` @@ -635,7 +614,7 @@ Q1::A1 await c.reviewSequencer.processReview(ReviewResponse.Easy); text = c.file.content; expectedCard1Review = "2023-09-09,2,230"; - let expectedCard2Review: string = "2023-09-12,4,270"; + const expectedCard2Review: string = "2023-09-12,4,270"; expect(text).toContain( ``, ); @@ -643,11 +622,11 @@ Q1::A1 }); test("Question with single cards; card reviewed as hard, the question is NOT added to the postponement list", async () => { - let settings: SRSettings = { ...DEFAULT_SETTINGS }; + const settings: SRSettings = { ...DEFAULT_SETTINGS }; settings.burySiblingCards = true; // Question with a single card - let text: string = `#flashcards Q1::A1`; + const text: string = "#flashcards Q1::A1"; // Create the test context setupStaticDateProvider_OriginDatePlusDays(0); @@ -669,26 +648,26 @@ Q1::A1 }); test("Answer includes MathJax within $$", async () => { - let fileText: string = `#flashcards -What is Newman's equation for gravitational force + const fileText: string = `#flashcards +What is Newton's equation for gravitational force ? $$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, fileText, ); await c.setSequencerDeckTreeFromOriginalText(); - expect(c.reviewSequencer.currentCard.front).toContain("What is Newman's equation"); + expect(c.reviewSequencer.currentCard.front).toContain("What is Newton's equation"); // Reviewing the card doesn't change the question, only adds the schedule info await c.reviewSequencer.processReview(ReviewResponse.Easy); - let expectedFileText: string = `${fileText} + const expectedFileText: string = `${fileText} `; - let actual: string = await c.file.read(); + const actual: string = await c.file.read(); expect(actual).toEqual(expectedFileText); }); }); @@ -696,17 +675,17 @@ $$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`; describe("Checking leading/trailing spaces", () => { test("Leading spaces are retained post review", async () => { // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/800 - let settings: SRSettings = { ...DEFAULT_SETTINGS }; + const settings: SRSettings = { ...DEFAULT_SETTINGS }; settings.burySiblingCards = true; - let indent: string = " "; + const indent: string = " "; // Note that "- bar?::baz" is intentionally indented - let text: string = `#flashcards + const text: string = `#flashcards - foo ${indent}- bar?::baz `; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, settings, @@ -727,7 +706,7 @@ ${indent}- bar?::baz describe("ReviewResponse.Easy", () => { test("Next card after reviewed card becomes current; reviewed easy card doesn't resurface", async () => { // [Q1, Q2, Q3] review Q1, then current becomes Q2 - let c: TestContext = await checkReviewResponse_CramMode(ReviewResponse.Easy); + const c: TestContext = await checkReviewResponse_CramMode(ReviewResponse.Easy); expect(c.reviewSequencer.currentCard.front).toEqual("Q2"); skipThenCheckCardFront(c.reviewSequencer, "Q3"); skipThenCheckCardFront(c.reviewSequencer, "Q4"); @@ -740,7 +719,7 @@ ${indent}- bar?::baz describe("ReviewResponse.Hard", () => { test("Next card after reviewed card becomes current; reviewed hard card seen again", async () => { // [Q1, Q2, Q3] review Q1, then current becomes Q2 - let c: TestContext = await checkReviewResponse_CramMode(ReviewResponse.Hard); + const c: TestContext = await checkReviewResponse_CramMode(ReviewResponse.Hard); expect(c.reviewSequencer.currentCard.front).toEqual("Q2"); skipThenCheckCardFront(c.reviewSequencer, "Q3"); skipThenCheckCardFront(c.reviewSequencer, "Q4"); @@ -754,12 +733,12 @@ ${indent}- bar?::baz }); describe("updateCurrentQuestionText", () => { - let space: string = " "; + const space: string = " "; describe("Checking update to file", () => { describe("Single line card type; Settings - schedule on following line", () => { test("Question has schedule on following line before/after update", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 @@ -767,10 +746,11 @@ describe("updateCurrentQuestionText", () => { #flashcards Q3::A3`; - let updatedQ: string = "A much more in depth question::A much more detailed answer"; - let originalStr: string = `#flashcards Q2::A2 + const updatedQ: string = + "A much more in depth question::A much more detailed answer"; + const originalStr: string = `#flashcards Q2::A2 `; - let updatedStr: string = `#flashcards A much more in depth question::A much more detailed answer + const updatedStr: string = `#flashcards A much more in depth question::A much more detailed answer `; await checkUpdateCurrentQuestionText( text, @@ -782,16 +762,17 @@ describe("updateCurrentQuestionText", () => { }); test("Question has schedule on same line (but pushed to following line due to settings)", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3`; - let updatedQ: string = "A much more in depth question::A much more detailed answer"; - let originalStr: string = `#flashcards Q2::A2 `; - let expectedUpdatedStr: string = `#flashcards A much more in depth question::A much more detailed answer + const updatedQ: string = + "A much more in depth question::A much more detailed answer"; + const originalStr: string = "#flashcards Q2::A2 "; + const expectedUpdatedStr: string = `#flashcards A much more in depth question::A much more detailed answer `; await checkUpdateCurrentQuestionText( text, @@ -804,20 +785,22 @@ describe("updateCurrentQuestionText", () => { }); describe("Single line card type; Settings - schedule on same line", () => { - let settings: SRSettings = { ...DEFAULT_SETTINGS }; + const settings: SRSettings = { ...DEFAULT_SETTINGS }; settings.cardCommentOnSameLine = true; test("Question has schedule on same line before/after", async () => { - let text1: string = ` + const text1: string = ` #flashcards Q1::A1 #flashcards Q2::A2 #flashcards Q3::A3`; - let updatedQ: string = "A much more in depth question::A much more detailed answer"; - let originalStr: string = `#flashcards Q2::A2 `; - let updatedStr: string = `#flashcards A much more in depth question::A much more detailed answer `; + const updatedQ: string = + "A much more in depth question::A much more detailed answer"; + const originalStr: string = "#flashcards Q2::A2 "; + const updatedStr: string = + "#flashcards A much more in depth question::A much more detailed answer "; await checkUpdateCurrentQuestionText( text1, updatedQ, @@ -828,7 +811,7 @@ describe("updateCurrentQuestionText", () => { }); test("Question has schedule on following line (but placed on same line due to settings)", async () => { - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards Q2::A2 @@ -836,10 +819,12 @@ describe("updateCurrentQuestionText", () => { #flashcards Q3::A3`; - let updatedQ: string = "A much more in depth question::A much more detailed answer"; - let originalStr: string = `#flashcards Q2::A2 + const updatedQ: string = + "A much more in depth question::A much more detailed answer"; + const originalStr: string = `#flashcards Q2::A2 `; - let updatedStr: string = `#flashcards A much more in depth question::A much more detailed answer `; + const updatedStr: string = + "#flashcards A much more in depth question::A much more detailed answer "; await checkUpdateCurrentQuestionText( text, updatedQ, @@ -852,25 +837,25 @@ describe("updateCurrentQuestionText", () => { describe("Multiline card type; Settings - schedule on following line", () => { test("Question starts immediately after tag; Existing schedule present", async () => { - let originalStr: string = `Q2 + const originalStr: string = `Q2 ? A2 `; - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards ${originalStr} #flashcards Q3::A3`; - let updatedQ: string = `Multiline question + const updatedQ: string = `Multiline question Question starting immediately after tag ? A2 (answer now includes more detail) extra answer line 2`; - let expectedUpdatedStr: string = `Multiline question + const expectedUpdatedStr: string = `Multiline question Question starting immediately after tag ? A2 (answer now includes more detail) @@ -887,25 +872,25 @@ extra answer line 2 }); test("Question starts on same line as tag (after two spaces); Existing schedule present", async () => { - let originalStr: string = `Q2 + const originalStr: string = `Q2 ? A2 `; - let text: string = ` + const text: string = ` #flashcards Q1::A1 #flashcards${space}${space}${originalStr} #flashcards Q3::A3`; - let updatedQ: string = `Multiline question + const updatedQ: string = `Multiline question Question starting immediately after tag ? A2 (answer now includes more detail) extra answer line 2`; - let expectedUpdatedStr: string = `Multiline question + const expectedUpdatedStr: string = `Multiline question Question starting immediately after tag ? A2 (answer now includes more detail) @@ -922,26 +907,26 @@ extra answer line 2 }); test("Question starts line after tag; Existing schedule present", async () => { - let originalStr: string = `#flashcards + const originalStr: string = `#flashcards Q2 ? A2 `; - let text: string = ` + const text: string = ` #flashcards Q1::A1 ${originalStr} #flashcards Q3::A3`; - let updatedQ: string = `Multiline question + const updatedQ: string = `Multiline question Question starting line after tag ? A2 (answer now includes more detail) extra answer line 2`; - let expectedUpdatedStr: string = `#flashcards + const expectedUpdatedStr: string = `#flashcards Multiline question Question starting line after tag ? @@ -959,56 +944,25 @@ extra answer line 2 }); test("Question starts line after tag (no white space after tag); New card", async () => { - let originalQuestionStr: string = `#flashcards + const originalQuestionStr: string = `#flashcards Q2 ? A2`; - let fileText: string = ` + const fileText: string = ` ${originalQuestionStr} #flashcards Q1::A1 #flashcards Q3::A3`; - let updatedQuestionText: string = `Multiline question + const updatedQuestionText: string = `Multiline question Question starting immediately after tag ? A2 (answer now includes more detail) extra answer line 2`; - let expectedUpdatedStr: string = `#flashcards -${updatedQuestionText}`; - - await checkUpdateCurrentQuestionText( - fileText, - updatedQuestionText, - originalQuestionStr, - expectedUpdatedStr, - DEFAULT_SETTINGS, - ); - }); - - test("Question starts line after tag (single space after tag before newline); New card", async () => { - let originalQuestionStr: string = `#flashcards${space} -Q2 -? -A2`; - - let fileText: string = ` -${originalQuestionStr} - -#flashcards Q1::A1 - -#flashcards Q3::A3`; - - let updatedQuestionText: string = `Multiline question -Question starting immediately after tag -? -A2 (answer now includes more detail) -extra answer line 2`; - - let expectedUpdatedStr: string = `#flashcards + const expectedUpdatedStr: string = `#flashcards ${updatedQuestionText}`; await checkUpdateCurrentQuestionText( @@ -1026,36 +980,36 @@ ${updatedQuestionText}`; describe("getDeckStats", () => { describe("Single level deck with some new and due cards", () => { test("Initial stats", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 Q4::A4 `; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, text, ); - let deck: Deck = await c.setSequencerDeckTreeFromOriginalText(); + await c.setSequencerDeckTreeFromOriginalText(); expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(1, 3, 4)); }); test("Reduction in due count after skipping card", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 Q4::A4 `; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, text, ); - let deck: Deck = await c.setSequencerDeckTreeFromOriginalText(); + await c.setSequencerDeckTreeFromOriginalText(); expect(c.reviewSequencer.currentCard.front).toEqual("Q4"); // This is the first card as we are using order_DueFirst_Sequential expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(1, 3, 4)); @@ -1065,19 +1019,19 @@ Q4::A4 }); test("Change in stats after reviewing each card", async () => { - let text: string = `#flashcards + const text: string = `#flashcards Q1::A1 Q2::A2 Q3::A3 Q4::A4 `; - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, DEFAULT_SETTINGS, text, ); - let deck: Deck = await c.setSequencerDeckTreeFromOriginalText(); + await c.setSequencerDeckTreeFromOriginalText(); await checkStats(c, "#flashcards", [ [new DeckStats(1, 3, 4), "Q4", ReviewResponse.Easy], // This is the first card as we are using order_DueFirst_Sequential @@ -1104,17 +1058,18 @@ async function checkStats( describe("Sequences", () => { test("Update question text, followed by review response", async () => { - let text1: string = ` + const text1: string = ` #flashcards Q2::A2 #flashcards Q3::A3`; // Do the update step - let updatedQ: string = "A much more in depth question::A much more detailed answer"; - let originalStr: string = `#flashcards Q2::A2`; - let updatedStr: string = `#flashcards A much more in depth question::A much more detailed answer`; + const updatedQ: string = "A much more in depth question::A much more detailed answer"; + const originalStr: string = "#flashcards Q2::A2"; + const updatedStr: string = + "#flashcards A much more in depth question::A much more detailed answer"; - let c: TestContext = await checkUpdateCurrentQuestionText( + const c: TestContext = await checkUpdateCurrentQuestionText( text1, updatedQ, originalStr, @@ -1126,7 +1081,7 @@ describe("Sequences", () => { await c.reviewSequencer.processReview(ReviewResponse.Hard); // Schedule for the reviewed card has been updated - let expectedText: string = ` + const expectedText: string = ` ${updatedStr} @@ -1152,7 +1107,7 @@ async function checkUpdateCurrentQuestionText( updatedStr: string, settings: SRSettings, ): Promise { - let c: TestContext = TestContext.Create( + const c: TestContext = TestContext.Create( order_DueFirst_Sequential, FlashcardReviewMode.Review, settings, @@ -1165,7 +1120,7 @@ async function checkUpdateCurrentQuestionText( // originalText should remain the same except for the specific substring change from originalStr => updatedStr if (!c.originalText.includes(originalStr)) throw `Text not found: ${originalStr}`; - let expectedFileText: string = c.originalText.replace(originalStr, updatedStr); + const expectedFileText: string = c.originalText.replace(originalStr, updatedStr); expect(await c.file.read()).toEqual(expectedFileText); return c; } diff --git a/tests/unit/helpers/UnitTestHelper.test.ts b/tests/unit/helpers/UnitTestHelper.test.ts deleted file mode 100644 index 519c501d..00000000 --- a/tests/unit/helpers/UnitTestHelper.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { TagCache } from "obsidian"; -import { unitTest_GetAllTagsFromTextEx } from "./UnitTestHelper"; - -describe("unitTest_GetAllTagsFromTextEx", () => { - describe("Without frontmatter", () => { - test("Tags on multiple lines", () => { - // The next line is numbered as line 0, therefore #review is line 2 - const text: string = ` - -#review - ----- -#flashcards/science/chemistry - -# Questions - -Chemistry Question from file underelephant 4A::goodby - -Chemistry Question from file underdog 4B::goodby - -Chemistry Question from file underdog 4C::goodby - -This single {{question}} turns into {{3 separate}} {{cards}} - - -#flashcards/science/misc - - `; - const actual: TagCache[] = unitTest_GetAllTagsFromTextEx(text); - const expected: TagCache[] = [ - createTagCacheObj("#review", 2), - createTagCacheObj("#flashcards/science/chemistry", 5), - createTagCacheObj("#flashcards/science/misc", 18), - ]; - expect(actual).toEqual(expected); - }); - - test("Multiple tags on same line", () => { - // The next line is numbered as line 0, therefore #review is line 2 - const text: string = ` - -#flashcards/science/chemistry #flashcards/science/misc - - - - `; - const actual: TagCache[] = unitTest_GetAllTagsFromTextEx(text); - const expected: TagCache[] = [ - createTagCacheObj("#flashcards/science/chemistry", 2), - createTagCacheObj("#flashcards/science/misc", 2), - ]; - expect(actual).toEqual(expected); - }); - }); -}); - -function createTagCacheObj(tag: string, line: number): any { - return { - tag: tag, - position: { - start: { line: line, col: null, offset: null }, - end: { line: line, col: null, offset: null }, - }, - }; -} diff --git a/tests/unit/helpers/UnitTestHelper.ts b/tests/unit/helpers/UnitTestHelper.ts deleted file mode 100644 index 52f2bf0c..00000000 --- a/tests/unit/helpers/UnitTestHelper.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { TagCache } from "obsidian"; -import { frontmatterTagPseudoLineNum } from "src/SRFile"; -import { extractFrontmatter, splitTextIntoLineArray } from "src/util/utils"; - -export function unitTest_CreateTagCache(tag: string, lineNum: number): TagCache { - return { - tag, - position: { - start: { line: lineNum, col: null, offset: null }, - end: { line: lineNum, col: null, offset: null }, - }, - }; -} - -export function unitTest_GetAllTagsFromTextEx(text: string): TagCache[] { - const [frontmatter, content] = extractFrontmatter(text); - const result = [] as TagCache[]; - let lines: string[]; - - if (frontmatter) { - const dataPrefix: string = " - "; - lines = splitTextIntoLineArray(frontmatter); - let foundTagHeading: boolean = false; - for (let i = 0; i < lines.length; i++) { - const line: string = lines[i]; - if (foundTagHeading) { - if (line.startsWith(dataPrefix)) { - const tagStr: string = line.substring(dataPrefix.length); - result.push(unitTest_CreateTagCache("#" + tagStr, frontmatterTagPseudoLineNum)); - } else { - break; - } - } else { - if (line.startsWith("tags:")) { - foundTagHeading = true; - } - } - } - } - lines = splitTextIntoLineArray(text); - for (let i = 0; i < lines.length; i++) { - const tagRegex = /#[^\s#]+/gi; - const matchList: RegExpMatchArray = lines[i].match(tagRegex); - if (matchList) { - for (const match of matchList) { - const tag: TagCache = { - tag: match, - position: { - start: { line: i, col: null, offset: null }, - end: { line: i, col: null, offset: null }, - }, - }; - result.push(tag); - } - } - } - return result; -} diff --git a/tests/unit/helpers/unit-test-core.ts b/tests/unit/helpers/unit-test-core.ts new file mode 100644 index 00000000..77f017d6 --- /dev/null +++ b/tests/unit/helpers/unit-test-core.ts @@ -0,0 +1,71 @@ +import * as fs from "fs"; +import * as path from "path"; + +import { OsrCore } from "src/core"; +import { QuestionPostponementList } from "src/question-postponement-list"; +import { SRSettings } from "src/settings"; + +import { UnitTestSRFile } from "./unit-test-file"; +import { UnitTestLinkInfoFinder } from "./unit-test-link-info-finder"; + +export class UnitTestOsrCore extends OsrCore { + private buryList: string[]; + // Key: Path + // Value: File content + private fileMap: Map; + private infoFinder: UnitTestLinkInfoFinder; + + constructor(settings: SRSettings) { + super(); + this.buryList = [] as string[]; + this.infoFinder = new UnitTestLinkInfoFinder(); + const questionPostponementList = new QuestionPostponementList( + null, + settings, + this.buryList, + ); + this.init(questionPostponementList, this.infoFinder, settings, () => {}); + } + + // Needed for unit testing: Setup fileMap and the link "info finder" + initializeFileMap(dir: string, files: string[]): void { + this.fileMap = new Map(); + + for (const filename of files) { + const fullPath: string = path.join(dir, filename); + const f: UnitTestSRFile = UnitTestSRFile.CreateFromFsFile(fullPath); + this.fileMap.set(fullPath, f); + } + + // Analyse the links between the notes before calling processFile() finaliseLoad() + this.infoFinder.init(this.fileMap); + } + + async loadTestVault(vaultSubfolder: string): Promise { + this.loadInit(); + + const dir: string = path.join(__dirname, "..", "..", "vaults", vaultSubfolder); + const files: string[] = fs.readdirSync(dir).filter((f) => f != ".obsidian"); + + // Pass 1 + this.initializeFileMap(dir, files); + + // Pass 2: Process all files + for (const filename of files) { + const fullPath: string = path.join(dir, filename); + const f: UnitTestSRFile = this.fileMap.get(fullPath); + await this.processFile(f); + } + + this.finaliseLoad(); + } + + getFileByNoteName(noteName: string): UnitTestSRFile { + const filename: string = this.infoFinder.getFilenameForLink(noteName); + return this.fileMap.get(filename); + } + + getFileMap(): Map { + return this.fileMap; + } +} diff --git a/tests/unit/helpers/UnitTestSRFile.ts b/tests/unit/helpers/unit-test-file.ts similarity index 55% rename from tests/unit/helpers/UnitTestSRFile.ts rename to tests/unit/helpers/unit-test-file.ts index 4c498496..1a192f01 100644 --- a/tests/unit/helpers/UnitTestSRFile.ts +++ b/tests/unit/helpers/unit-test-file.ts @@ -1,7 +1,10 @@ -import { TagCache } from "obsidian"; -import { ISRFile } from "src/SRFile"; -import { unitTest_GetAllTagsFromTextEx } from "./UnitTestHelper"; -import { TextDirection } from "src/util/TextDirection"; +import * as fs from "fs"; +import { TagCache, TFile } from "obsidian"; + +import { ISRFile } from "src/sr-file"; +import { TextDirection } from "src/utils/strings"; + +import { unitTest_BasicFrontmatterParser, unitTest_GetAllTagsFromTextEx } from "./unit-test-helper"; export class UnitTestSRFile implements ISRFile { content: string; @@ -20,6 +23,14 @@ export class UnitTestSRFile implements ISRFile { return ""; } + get tfile(): TFile { + throw "Not supported"; + } + + async getFrontmatter(): Promise> { + return unitTest_BasicFrontmatterParser(await this.read()); + } + getAllTagsFromCache(): string[] { return unitTest_GetAllTagsFromTextEx(this.content).map((item) => item.tag); } @@ -28,8 +39,7 @@ export class UnitTestSRFile implements ISRFile { return unitTest_GetAllTagsFromTextEx(this.content); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getQuestionContext(cardLine: number): string[] { + getQuestionContext(_: number): string[] { return []; } @@ -44,4 +54,9 @@ export class UnitTestSRFile implements ISRFile { async write(content: string): Promise { this.content = content; } + + static CreateFromFsFile(path: string): UnitTestSRFile { + const content: string = fs.readFileSync(path, "utf8"); + return new UnitTestSRFile(content, path); + } } diff --git a/tests/unit/helpers/unit-test-helper.test.ts b/tests/unit/helpers/unit-test-helper.test.ts new file mode 100644 index 00000000..08997ed5 --- /dev/null +++ b/tests/unit/helpers/unit-test-helper.test.ts @@ -0,0 +1,153 @@ +import { TagCache } from "obsidian"; + +import { DEFAULT_SETTINGS } from "src/settings"; + +import { UnitTestOsrCore } from "./unit-test-core"; +import { + unitTest_CreateTagCacheObj, + unitTest_GetAllTagsFromTextEx, + unitTest_ParseForOutgoingLinks, +} from "./unit-test-helper"; +import { UnitTestLinkInfoFinder } from "./unit-test-link-info-finder"; +import { unitTestSetup_StandardDataStoreAlgorithm } from "./unit-test-setup"; + +let linkInfoFinder: UnitTestLinkInfoFinder; + +beforeAll(() => { + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); +}); + +describe("unitTest_GetAllTagsFromTextEx", () => { + describe("Without frontmatter", () => { + test("Tags on multiple lines", () => { + // The next line is numbered as line 0, therefore #review is line 2 + const text: string = ` + +#review + +---- +#flashcards/science/chemistry + +# Questions + +Chemistry Question from file underelephant 4A::goodby + +Chemistry Question from file underdog 4B::goodby + +Chemistry Question from file underdog 4C::goodby + +This single {{question}} turns into {{3 separate}} {{cards}} + + +#flashcards/science/misc + + `; + const actual: TagCache[] = unitTest_GetAllTagsFromTextEx(text); + const expected: TagCache[] = [ + unitTest_CreateTagCacheObj("#review", 2), + unitTest_CreateTagCacheObj("#flashcards/science/chemistry", 5), + unitTest_CreateTagCacheObj("#flashcards/science/misc", 18), + ]; + expect(actual).toEqual(expected); + }); + + test("Multiple tags on same line", () => { + // The next line is numbered as line 0, therefore #review is line 2 + const text: string = ` + +#flashcards/science/chemistry #flashcards/science/misc + + + + `; + const actual: TagCache[] = unitTest_GetAllTagsFromTextEx(text); + const expected: TagCache[] = [ + unitTest_CreateTagCacheObj("#flashcards/science/chemistry", 2), + unitTest_CreateTagCacheObj("#flashcards/science/misc", 2), + ]; + expect(actual).toEqual(expected); + }); + }); +}); + +describe("unitTest_ParseForOutgoingLinks", () => { + test("No outgoing links", () => { + const text: string = ` +The triboelectric effect describes electric charge transfer between two objects when they contact or slide against each other. + +It can occur with different materials, such as: +- the sole of a shoe on a carpet +- balloon rubbing against sweater + +(also known as triboelectricity, triboelectric charging, triboelectrification, or tribocharging) +`; + const links: string[] = unitTest_ParseForOutgoingLinks(text); + expect(links.length).toEqual(0); + }); + + test("Multiple outgoing links on different lines", () => { + const text: string = ` +The triboelectric effect describes electric charge [[transfer between]] two objects when they contact or slide against each other. + +It can occur with different materials, such as: +- the sole of a shoe on a carpet +- balloon rubbing against sweater + +(also known as triboelectricity, triboelectric charging, [[triboelectrification]], or tribocharging) +`; + const links: string[] = unitTest_ParseForOutgoingLinks(text); + const expected: string[] = ["transfer between", "triboelectrification"]; + expect(links).toEqual(expected); + }); + + test("Multiple outgoing links on the one line", () => { + const text: string = ` +The triboelectric effect describes electric charge [[triboelectrification]], or [[tribocharging]]) +`; + const links: string[] = unitTest_ParseForOutgoingLinks(text); + const expected: string[] = ["triboelectrification", "tribocharging"]; + expect(links).toEqual(expected); + }); +}); + +function check_getResolvedLinks(linkName: string, expected: Map): void { + const e: Record = {}; + expected.forEach((n, linkName) => { + const filename: string = linkInfoFinder.getFilenameForLink(linkName); + e[filename] = n; + }); + expect(linkInfoFinder.getResolvedTargetLinksForNoteLink(linkName)).toEqual(e); +} + +describe("UnitTestLinkInfoFinder", () => { + test("No outgoing links", async () => { + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS); + await osrCore.loadTestVault("notes3"); + linkInfoFinder = new UnitTestLinkInfoFinder(); + linkInfoFinder.init(osrCore.getFileMap()); + + // One link from A to each of B, C, D + check_getResolvedLinks( + "A", + new Map([ + ["B", 1], + ["C", 1], + ["D", 1], + ]), + ); + + // No links from B + check_getResolvedLinks("B", new Map([])); + + // One link from C to D + check_getResolvedLinks("C", new Map([["D", 1]])); + + check_getResolvedLinks( + "D", + new Map([ + ["A", 1], + ["B", 2], + ]), + ); + }); +}); diff --git a/tests/unit/helpers/unit-test-helper.ts b/tests/unit/helpers/unit-test-helper.ts new file mode 100644 index 00000000..de418eb4 --- /dev/null +++ b/tests/unit/helpers/unit-test-helper.ts @@ -0,0 +1,146 @@ +import { TagCache } from "obsidian"; + +import { frontmatterTagPseudoLineNum } from "src/sr-file"; +import { splitNoteIntoFrontmatterAndContent, splitTextIntoLineArray } from "src/utils/strings"; + +export function unitTest_CreateTagCacheObj(tag: string, line: number): TagCache { + return { + tag: tag, + position: { + start: { line: line, col: null, offset: null }, + end: { line: line, col: null, offset: null }, + }, + }; +} + +export function unitTest_GetAllTagsFromTextEx(text: string): TagCache[] { + const [frontmatter, _] = splitNoteIntoFrontmatterAndContent(text); + const result = [] as TagCache[]; + let lines: string[]; + + if (frontmatter) { + const dataPrefix: string = " - "; + lines = splitTextIntoLineArray(frontmatter); + let foundTagHeading: boolean = false; + for (let i = 0; i < lines.length; i++) { + const line: string = lines[i]; + if (foundTagHeading) { + if (line.startsWith(dataPrefix)) { + const tagStr: string = line.substring(dataPrefix.length); + result.push( + unitTest_CreateTagCacheObj("#" + tagStr, frontmatterTagPseudoLineNum), + ); + } else { + break; + } + } else { + if (line.startsWith("tags:")) { + foundTagHeading = true; + } + } + } + } + + lines = splitTextIntoLineArray(text); + for (let i = 0; i < lines.length; i++) { + const tagRegex = /#[^\s#]+/gi; + const matchList: RegExpMatchArray = lines[i].match(tagRegex); + if (matchList) { + for (const match of matchList) { + const tag: TagCache = { + tag: match, + position: { + start: { line: i, col: null, offset: null }, + end: { line: i, col: null, offset: null }, + }, + }; + result.push(tag); + } + } + } + return result; +} + +export function unitTest_GetAllTagsFromText(text: string): string[] { + const tagRegex = /#[^\s#]+/gi; + const result: RegExpMatchArray = text.match(tagRegex); + if (!result) return []; + return result; +} + +export function unitTest_BasicFrontmatterParser(text: string): Map { + const result = new Map(); + const map: Map = unitTest_BasicFrontmatterParserEx(text); + map.forEach((value, key) => { + result.set(key, value.pop()); + }); + return result; +} + +export function unitTest_BasicFrontmatterParserEx(text: string): Map { + const [frontmatter, _] = splitNoteIntoFrontmatterAndContent(text); + const result = new Map(); + + if (!frontmatter) return result; + + const keyRegex = /^([A-Za-z0-9_-]+):(.*)$/; + const dataRegex = /^(\s+)-\s+(.+)$/; + const lines: string[] = splitTextIntoLineArray(frontmatter); + let keyName: string = null; + let valueList: string[] = [] as string[]; + + for (let i = 0; i < lines.length; i++) { + const line: string = lines[i]; + + // Is there a key, and optional value? + const keyMatch: RegExpMatchArray = line.match(keyRegex); + if (keyMatch) { + if (keyName) { + result.set(keyName, valueList); + } + keyName = keyMatch[1]; + valueList = [] as string[]; + const value = keyMatch[2].trim(); + if (value) { + valueList.push(value); + } + } else { + // Just a value, related to the last key + const dataMatch: RegExpMatchArray = line.match(dataRegex); + if (keyName && dataMatch) { + const value = dataMatch[1].trim(); + if (value) { + valueList.push(value); + } + } + } + } + if (keyName) { + result.set(keyName, valueList); + } + return result; +} + +export function unitTest_ParseForOutgoingLinks(text: string): string[] { + const linkRegex = /\[\[([\w\s]+)\]\]+/gi; + const matches = text.matchAll(linkRegex); + const result: string[] = [] as string[]; + for (const m of matches) { + result.push(m[1]); + } + return result; +} + +export function unitTest_CheckNoteFrontmatter( + text: string, + expectedDueDate: string, + expectedInterval: number, + expectedEase: number, +): void { + const frontmatter: Map = unitTest_BasicFrontmatterParser(text); + + expect(frontmatter).toBeTruthy(); + expect(frontmatter.get("sr-due")).toEqual(expectedDueDate); + expect(frontmatter.get("sr-interval")).toEqual(expectedInterval + ""); + expect(frontmatter.get("sr-ease")).toEqual(expectedEase + ""); +} diff --git a/tests/unit/helpers/unit-test-link-info-finder.ts b/tests/unit/helpers/unit-test-link-info-finder.ts new file mode 100644 index 00000000..6dd3d7d4 --- /dev/null +++ b/tests/unit/helpers/unit-test-link-info-finder.ts @@ -0,0 +1,67 @@ +import path from "path"; + +import { IOsrVaultNoteLinkInfoFinder } from "src/algorithms/osr/obsidian-vault-notelink-info-finder"; + +import { UnitTestSRFile } from "./unit-test-file"; +import { unitTest_ParseForOutgoingLinks } from "./unit-test-helper"; + +export class UnitTestLinkInfoFinder implements IOsrVaultNoteLinkInfoFinder { + private linkPathMap: Map; + // Key: sourceFilename + // Value: Map + // This is the number of links from sourceFilename to targetFilename + // For simplicity, we just store the filename without the directory or filename extension + private outgoingLinks: Map>; + + init(fileMap: Map) { + // We first need to generate a map between the link names (e.g. the "A" in "[[A]]"), and it's file path) + this.linkPathMap = new Map(); + fileMap.forEach((_, filePath) => { + this.linkPathMap.set(path.parse(filePath).name, filePath); + }); + + // + this.outgoingLinks = new Map>(); + fileMap.forEach((file, sourceFilename) => { + // Find all the (outgoing) links present in the file + const outgoingLinks2: string[] = unitTest_ParseForOutgoingLinks(file.content); + + for (const targetLink of outgoingLinks2) { + const targetFilename: string = this.linkPathMap.get(targetLink); + this.incrementOutgoingLinksCount(sourceFilename, targetFilename); + } + }); + } + + private incrementOutgoingLinksCount(sourceFilename: string, targetFilename: string): void { + if (!this.outgoingLinks.has(sourceFilename)) { + this.outgoingLinks.set(sourceFilename, new Map()); + } + const rec = this.outgoingLinks.get(sourceFilename); + if (!rec.has(targetFilename)) { + rec.set(targetFilename, 0); + } + + rec.set(targetFilename, rec.get(targetFilename) + 1); + } + + getFilenameForLink(linkName: string): string { + return this.linkPathMap.get(linkName); + } + + getResolvedTargetLinksForNoteLink(linkName: string): Record { + const filename = this.linkPathMap.get(linkName); + return this.getResolvedTargetLinksForNotePath(filename); + } + + getResolvedTargetLinksForNotePath(sourcePath: string): Record { + const result: Record = {}; + if (this.outgoingLinks.has(sourcePath)) { + const rec = this.outgoingLinks.get(sourcePath); + rec.forEach((n, filename) => { + result[filename] = n; + }); + } + return result; + } +} diff --git a/tests/unit/helpers/unit-test-setup.ts b/tests/unit/helpers/unit-test-setup.ts new file mode 100644 index 00000000..015db7a5 --- /dev/null +++ b/tests/unit/helpers/unit-test-setup.ts @@ -0,0 +1,13 @@ +import { SrsAlgorithm } from "src/algorithms/base/srs-algorithm"; +import { SrsAlgorithm_Osr } from "src/algorithms/osr/srs-algorithm-osr"; +import { DataStoreAlgorithm } from "src/data-store-algorithm/data-store-algorithm"; +import { DataStoreInNote_AlgorithmOsr } from "src/data-store-algorithm/data-store-in-note-algorithm-osr"; +import { DataStore } from "src/data-stores/base/data-store"; +import { StoreInNote } from "src/data-stores/store-in-note/note"; +import { SRSettings } from "src/settings"; + +export function unitTestSetup_StandardDataStoreAlgorithm(settings: SRSettings) { + DataStore.instance = new StoreInNote(settings); + SrsAlgorithm.instance = new SrsAlgorithm_Osr(settings); + DataStoreAlgorithm.instance = new DataStoreInNote_AlgorithmOsr(settings); +} diff --git a/tests/unit/lang/helpers.test.ts b/tests/unit/lang/helpers.test.ts index 3673eeb1..e4d17357 100644 --- a/tests/unit/lang/helpers.test.ts +++ b/tests/unit/lang/helpers.test.ts @@ -1,6 +1,5 @@ test("Check that localization entries are consistent across all files", () => { jest.isolateModules(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const { localeMap } = require("src/lang/helpers"); const expected_keys: string[] = Object.keys(localeMap["en"]); for (const [language_code, locale] of Object.entries(localeMap) as [string, string[]][]) { @@ -15,7 +14,7 @@ test("Check that localization entries are consistent across all files", () => { const extra_keys = locale_keys.filter((x) => !expected_keys.includes(x)); expect( extra_keys.length, - `The ${language_code} locale includes the following deprecated translations: ${extra_keys}.`, + `The ${language_code} locale includes the following translations that are no longer in use: ${extra_keys}.`, ).toBe(0); } }); @@ -23,13 +22,10 @@ test("Check that localization entries are consistent across all files", () => { test("Test translation unknown locale", () => { jest.isolateModules(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const { moment } = require("obsidian"); const mockLocale = moment.locale as jest.MockedFunction<() => string>; mockLocale.mockImplementation(() => "ki"); // Kikuyu - // eslint-disable-next-line @typescript-eslint/no-var-requires const { t } = require("src/lang/helpers"); - // eslint-disable-next-line @typescript-eslint/no-empty-function const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); expect(t("DECKS")).toEqual("Decks"); expect(consoleSpy).toHaveBeenCalledWith("SRS error: Locale ki not found."); @@ -38,11 +34,9 @@ test("Test translation unknown locale", () => { test("Test translation without interpolation in English", () => { jest.isolateModules(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const { moment } = require("obsidian"); const mockLocale = moment.locale as jest.MockedFunction<() => string>; mockLocale.mockImplementation(() => "en"); - // eslint-disable-next-line @typescript-eslint/no-var-requires const { t } = require("src/lang/helpers"); expect(t("DECKS")).toEqual("Decks"); }); @@ -50,11 +44,9 @@ test("Test translation without interpolation in English", () => { test("Test translation without interpolation in čeština", () => { jest.isolateModules(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const { moment } = require("obsidian"); const mockLocale = moment.locale as jest.MockedFunction<() => string>; mockLocale.mockImplementation(() => "cs"); - // eslint-disable-next-line @typescript-eslint/no-var-requires const { t } = require("src/lang/helpers"); expect(t("DECKS")).toEqual("Balíčky"); }); @@ -62,11 +54,9 @@ test("Test translation without interpolation in čeština", () => { test("Test translation with interpolation in English", () => { jest.isolateModules(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const { moment } = require("obsidian"); const mockLocale = moment.locale as jest.MockedFunction<() => string>; mockLocale.mockImplementation(() => "en"); - // eslint-disable-next-line @typescript-eslint/no-var-requires const { t } = require("src/lang/helpers"); expect(t("STATUS_BAR", { dueNotesCount: 1, dueFlashcardsCount: 2 })).toEqual( "Review: 1 note(s), 2 card(s) due", @@ -76,11 +66,9 @@ test("Test translation with interpolation in English", () => { test("Test translation with interpolation in German", () => { jest.isolateModules(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const { moment } = require("obsidian"); const mockLocale = moment.locale as jest.MockedFunction<() => string>; mockLocale.mockImplementation(() => "de"); - // eslint-disable-next-line @typescript-eslint/no-var-requires const { t } = require("src/lang/helpers"); expect(t("STATUS_BAR", { dueNotesCount: 1, dueFlashcardsCount: 2 })).toEqual( "Wiederholung: 1 Notiz(en), 2 Karte(n) anstehend", diff --git a/tests/unit/note-card-schedule-parser.test.ts b/tests/unit/note-card-schedule-parser.test.ts new file mode 100644 index 00000000..90e1cbe1 --- /dev/null +++ b/tests/unit/note-card-schedule-parser.test.ts @@ -0,0 +1,54 @@ +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/rep-item-schedule-info-osr"; +import { TICKS_PER_DAY } from "src/constants"; +import { DataStore } from "src/data-stores/base/data-store"; +import { DEFAULT_SETTINGS } from "src/settings"; +import { setupStaticDateProvider_20230906 } from "src/utils/dates"; + +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; + +beforeAll(() => { + setupStaticDateProvider_20230906(); + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); +}); + +test("No schedule info for question", () => { + expect(DataStore.getInstance().questionCreateSchedule("A::B", null)).toEqual([]); +}); + +test("Single schedule info for question (on separate line)", () => { + const actual: RepItemScheduleInfo[] = DataStore.getInstance().questionCreateSchedule( + `What symbol represents an electric field:: $\\large \\vec E$ +`, + null, + ); + + expect(actual).toEqual([ + RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-02", 4, 270, -4 * TICKS_PER_DAY), + ]); +}); + +test("Single schedule info for question (on same line)", () => { + const actual: RepItemScheduleInfo[] = DataStore.getInstance().questionCreateSchedule( + "What symbol represents an electric field:: $\\large \\vec E$", + null, + ); + + expect(actual).toEqual([ + RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-02", 4, 270, -4 * TICKS_PER_DAY), + ]); +}); + +test("Multiple schedule info for question (on separate line)", () => { + const actual: RepItemScheduleInfo[] = DataStore.getInstance().questionCreateSchedule( + `This is a really very ==interesting== and ==fascinating== and ==great== test + `, + null, + ); + + expect(actual).toEqual([ + RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230, -3 * TICKS_PER_DAY), + RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-05", 3, 250, -1 * TICKS_PER_DAY), + RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-06", 4, 270, 0), + ]); +}); diff --git a/tests/unit/NoteEaseList.test.ts b/tests/unit/note-ease-list.test.ts similarity index 65% rename from tests/unit/NoteEaseList.test.ts rename to tests/unit/note-ease-list.test.ts index 4f5c4b81..91a49811 100644 --- a/tests/unit/NoteEaseList.test.ts +++ b/tests/unit/note-ease-list.test.ts @@ -1,13 +1,13 @@ -import { NoteEaseList } from "src/NoteEaseList"; +import { NoteEaseList } from "src/note-ease-list"; import { DEFAULT_SETTINGS } from "src/settings"; test("baseEase", async () => { - let list: NoteEaseList = new NoteEaseList(DEFAULT_SETTINGS); + const list: NoteEaseList = new NoteEaseList(DEFAULT_SETTINGS); expect(list.baseEase).toEqual(250); }); test("hasEaseForPath", async () => { - let list: NoteEaseList = new NoteEaseList(DEFAULT_SETTINGS); + const list: NoteEaseList = new NoteEaseList(DEFAULT_SETTINGS); expect(list.hasEaseForPath("Unknown path")).toEqual(false); list.setEaseForPath("Known path", 100); @@ -15,7 +15,7 @@ test("hasEaseForPath", async () => { }); test("getEaseByPath", async () => { - let list: NoteEaseList = new NoteEaseList(DEFAULT_SETTINGS); + const list: NoteEaseList = new NoteEaseList(DEFAULT_SETTINGS); list.setEaseForPath("Known path", 100); expect(list.getEaseByPath("Known path")).toEqual(100); diff --git a/tests/unit/note-file-loader.test.ts b/tests/unit/note-file-loader.test.ts new file mode 100644 index 00000000..c757dc01 --- /dev/null +++ b/tests/unit/note-file-loader.test.ts @@ -0,0 +1,42 @@ +import { Note } from "src/note"; +import { NoteFileLoader } from "src/note-file-loader"; +import { DEFAULT_SETTINGS } from "src/settings"; +import { TopicPath } from "src/topic-path"; +import { TextDirection } from "src/utils/strings"; + +import { UnitTestSRFile } from "./helpers/unit-test-file"; +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; + +const noteFileLoader: NoteFileLoader = new NoteFileLoader(DEFAULT_SETTINGS); + +beforeAll(() => { + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); +}); + +describe("load", () => { + test("Multiple questions, none with too many schedule details", async () => { + const noteText: string = `#flashcards/test +Q1::A1 +#flashcards Q2::A2 + +Q3:::A3 + +`; + const file: UnitTestSRFile = new UnitTestSRFile(noteText); + const note: Note = await noteFileLoader.load(file, TextDirection.Ltr, TopicPath.emptyPath); + expect(note.hasChanged).toEqual(false); + }); + + test("Multiple questions, some with too many schedule details", async () => { + const noteText: string = `#flashcards/test +Q1::A1 +#flashcards Q2::A2 + +Q3:::A3 + +`; + const file: UnitTestSRFile = new UnitTestSRFile(noteText); + const note: Note = await noteFileLoader.load(file, TextDirection.Ltr, TopicPath.emptyPath); + expect(note.hasChanged).toEqual(true); + }); +}); diff --git a/tests/unit/note-parser.test.ts b/tests/unit/note-parser.test.ts new file mode 100644 index 00000000..cb072ca1 --- /dev/null +++ b/tests/unit/note-parser.test.ts @@ -0,0 +1,31 @@ +import { Note } from "src/note"; +import { NoteParser } from "src/note-parser"; +import { DEFAULT_SETTINGS } from "src/settings"; +import { TopicPath } from "src/topic-path"; +import { setupStaticDateProvider_20230906 } from "src/utils/dates"; +import { TextDirection } from "src/utils/strings"; + +import { UnitTestSRFile } from "./helpers/unit-test-file"; +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; + +const parser: NoteParser = new NoteParser(DEFAULT_SETTINGS); + +beforeAll(() => { + setupStaticDateProvider_20230906(); + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); +}); + +describe("Multiple questions in the text", () => { + test("SingleLineBasic: No schedule info", async () => { + const noteText: string = `#flashcards/test +Q1::A1 +Q2::A2 +Q3::A3 +`; + const file: UnitTestSRFile = new UnitTestSRFile(noteText); + const folderTopicPath = TopicPath.emptyPath; + const note: Note = await parser.parse(file, TextDirection.Ltr, folderTopicPath); + const questionList = note.questionList; + expect(questionList.length).toEqual(3); + }); +}); diff --git a/tests/unit/NoteQuestionParser.test.ts b/tests/unit/note-question-parser.test.ts similarity index 64% rename from tests/unit/NoteQuestionParser.test.ts rename to tests/unit/note-question-parser.test.ts index ceee063b..ed6115d0 100644 --- a/tests/unit/NoteQuestionParser.test.ts +++ b/tests/unit/note-question-parser.test.ts @@ -1,32 +1,37 @@ -import { NoteQuestionParser } from "src/NoteQuestionParser"; -import { CardScheduleInfo } from "src/CardSchedule"; +import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info"; +import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/rep-item-schedule-info-osr"; +import { Card } from "src/card"; import { TICKS_PER_DAY } from "src/constants"; -import { CardType, Question } from "src/Question"; +import { NoteQuestionParser } from "src/note-question-parser"; +import { CardType, Question } from "src/question"; import { DEFAULT_SETTINGS, SRSettings } from "src/settings"; -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 { Card } from "src/Card"; - -let parserWithDefaultSettings: NoteQuestionParser = createTest_NoteQuestionParser(DEFAULT_SETTINGS); -let settings_ConvertFoldersToDecks: SRSettings = { ...DEFAULT_SETTINGS }; +import { frontmatterTagPseudoLineNum, ISRFile } from "src/sr-file"; +import { TopicPath, TopicPathList } from "src/topic-path"; +import { setupStaticDateProvider_20230906 } from "src/utils/dates"; +import { TextDirection } from "src/utils/strings"; + +import { UnitTestSRFile } from "./helpers/unit-test-file"; +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; +import { createTest_NoteQuestionParser } from "./sample-items"; + +const parserWithDefaultSettings: NoteQuestionParser = + createTest_NoteQuestionParser(DEFAULT_SETTINGS); +const settings_ConvertFoldersToDecks: SRSettings = { ...DEFAULT_SETTINGS }; settings_ConvertFoldersToDecks.convertFoldersToDecks = true; -let parser_ConvertFoldersToDecks: NoteQuestionParser = createTest_NoteQuestionParser( +const parser_ConvertFoldersToDecks: NoteQuestionParser = createTest_NoteQuestionParser( settings_ConvertFoldersToDecks, ); beforeAll(() => { setupStaticDateProvider_20230906(); + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); }); describe("No flashcard questions", () => { test("No questions in the text", async () => { - let noteText: string = "An interesting note, but no questions"; - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteText: string = "An interesting note, but no questions"; + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const noteFile: ISRFile = new UnitTestSRFile(noteText); expect( await parserWithDefaultSettings.createQuestionList( @@ -39,9 +44,9 @@ describe("No flashcard questions", () => { }); test("A question in the text, but no flashcard tag", async () => { - let noteText: string = "A::B"; - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteText: string = "A::B"; + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const noteFile: ISRFile = new UnitTestSRFile(noteText); expect( await parserWithDefaultSettings.createQuestionList( @@ -56,22 +61,22 @@ describe("No flashcard questions", () => { describe("Single question in the text (without block identifier)", () => { test("SingleLineBasic: No schedule info", async () => { - let noteText: string = `#flashcards + const noteText: string = `#flashcards A::B `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let card1 = { + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const card1 = { cardIdx: 0, - scheduleInfo: null as CardScheduleInfo, + scheduleInfo: null as RepItemScheduleInfo, }; - let expected = [ + const expected = [ { questionType: CardType.SingleLineBasic, topicPathList: TopicPathList.fromPsv("#flashcards", 0), questionText: { - original: `A::B`, + original: "A::B", actualQuestion: "A::B", }, @@ -91,24 +96,21 @@ A::B }); test("SingleLineBasic: With schedule info", async () => { - let noteText: string = `#flashcards/test + const noteText: string = `#flashcards/test A::B `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let delayDays = 3 - 6; - let card1 = { + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const delayDays = 3 - 6; + const scheduleInfo = RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230); + scheduleInfo.delayedBeforeReviewTicks = delayDays * TICKS_PER_DAY; + const card1 = { cardIdx: 0, - scheduleInfo: CardScheduleInfo.fromDueDateStr( - "2023-09-03", - 1, - 230, - delayDays * TICKS_PER_DAY, - ), + scheduleInfo, }; - let expected = [ + const expected = [ { questionType: CardType.SingleLineBasic, topicPathList: TopicPathList.fromPsv("#flashcards/test", 0), @@ -135,18 +137,18 @@ A::B }); test("SingleLineBasic: Multiple topics", async () => { - let noteText: string = `#flashcards/science #flashcards/poetry + const noteText: string = `#flashcards/science #flashcards/poetry A::B `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let expected = [ + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const expected = [ { questionType: CardType.SingleLineBasic, topicPathList: TopicPathList.fromPsv("#flashcards/science|#flashcards/poetry", 0), questionText: { - original: `A::B`, + original: "A::B", actualQuestion: "A::B", }, }, @@ -163,7 +165,7 @@ A::B // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/908 test("SingleLineBasic: Multiple tags in note (including non-flashcard ones)", async () => { - let noteText: string = `--- + const noteText: string = `--- created: 2024-03-11 10:41 tags: - flashcards @@ -175,10 +177,10 @@ tags: ? In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*! `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let expected = [ + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const expected = [ { questionType: CardType.MultiLineBasic, // Explicitly checking that #data-structure and #2024/03-11 are not included @@ -198,17 +200,17 @@ In computer-science, a *heap* is a tree-based data-structure, that satisfies the describe("Single question in the text (with block identifier)", () => { test("SingleLineBasic: No schedule info", async () => { - let noteText: string = `#flashcards + const noteText: string = `#flashcards A::B ^d7cee0 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let card1 = { + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const card1 = { cardIdx: 0, - scheduleInfo: null as CardScheduleInfo, + scheduleInfo: null as RepItemScheduleInfo_Osr, }; - let expected = [ + const expected = [ { topicPathList: { list: [TopicPath.getTopicPathFromTag("#flashcards")], @@ -219,7 +221,7 @@ A::B ^d7cee0 firstLineNum: 1, }, questionText: { - original: `A::B ^d7cee0`, + original: "A::B ^d7cee0", actualQuestion: "A::B", obsidianBlockId: "^d7cee0", }, @@ -240,24 +242,22 @@ A::B ^d7cee0 }); test("SingleLineBasic: With schedule info (next line)", async () => { - let noteText: string = `#flashcards/test + const noteText: string = `#flashcards/test A::B ^d7cee0 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); + + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const delayDays = 3 - 6; + const scheduleInfo = RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230); + scheduleInfo.delayedBeforeReviewTicks = delayDays * TICKS_PER_DAY; - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let delayDays = 3 - 6; - let card1 = { + const card1 = { cardIdx: 0, - scheduleInfo: CardScheduleInfo.fromDueDateStr( - "2023-09-03", - 1, - 230, - delayDays * TICKS_PER_DAY, - ), + scheduleInfo, }; - let expected = [ + const expected = [ { topicPathList: { list: [TopicPath.getTopicPathFromTag("#flashcards/test")], @@ -290,23 +290,20 @@ A::B ^d7cee0 }); test("SingleLineBasic: With schedule info (same line)", async () => { - let noteText: string = `#flashcards/test + const noteText: string = `#flashcards/test A::B ^d7cee0 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let delayDays = 3 - 6; - let card1 = { + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const delayDays = 3 - 6; + const scheduleInfo = RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230); + scheduleInfo.delayedBeforeReviewTicks = delayDays * TICKS_PER_DAY; + const card1 = { cardIdx: 0, - scheduleInfo: CardScheduleInfo.fromDueDateStr( - "2023-09-03", - 1, - 230, - delayDays * TICKS_PER_DAY, - ), + scheduleInfo, }; - let expected = [ + const expected = [ { topicPathList: { list: [TopicPath.getTopicPathFromTag("#flashcards/test")], @@ -317,7 +314,7 @@ A::B ^d7cee0 firstLineNum: 1, }, questionText: { - original: `A::B ^d7cee0`, + original: "A::B ^d7cee0", actualQuestion: "A::B", textHash: "1c6b0b01215dc4", obsidianBlockId: "^d7cee0", @@ -338,23 +335,20 @@ A::B ^d7cee0 }); test("SingleLineBasic: With topic tag and schedule info (same line)", async () => { - let noteText: string = ` + const noteText: string = ` #flashcards/test A::B ^d7cee0 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let delayDays = 3 - 6; - let card1 = { + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const delayDays = 3 - 6; + const scheduleInfo = RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230); + scheduleInfo.delayedBeforeReviewTicks = delayDays * TICKS_PER_DAY; + const card1 = { cardIdx: 0, - scheduleInfo: CardScheduleInfo.fromDueDateStr( - "2023-09-03", - 1, - 230, - delayDays * TICKS_PER_DAY, - ), + scheduleInfo, }; - let expected = [ + const expected = [ { topicPathList: { list: [TopicPath.getTopicPathFromTag("#flashcards/test")], @@ -365,7 +359,7 @@ A::B ^d7cee0 firstLineNum: 1, }, questionText: { - original: `#flashcards/test A::B ^d7cee0`, + original: "#flashcards/test A::B ^d7cee0", actualQuestion: "A::B", obsidianBlockId: "^d7cee0", }, @@ -387,13 +381,13 @@ A::B ^d7cee0 describe("Multiple questions in the text", () => { test("SingleLineBasic: No schedule info", async () => { - let noteText: string = `#flashcards/test + const noteText: string = `#flashcards/test Q1::A1 Q2::A2 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let questionList: Question[] = await parser_ConvertFoldersToDecks.createQuestionList( + const noteFile: ISRFile = new UnitTestSRFile(noteText); + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const questionList: Question[] = await parser_ConvertFoldersToDecks.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -403,15 +397,15 @@ Q2::A2 }); test("SingleLineBasic: Note topic applies to all questions when not overriden", async () => { - let noteText: string = ` + const noteText: string = ` Q1::A1 Q2::A2 Q3::A3 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = new TopicPath(["flashcards", "science"]); - let questionList: Question[] = await parser_ConvertFoldersToDecks.createQuestionList( + const folderTopicPath: TopicPath = new TopicPath(["flashcards", "science"]); + const questionList: Question[] = await parser_ConvertFoldersToDecks.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -424,7 +418,7 @@ Q3::A3 }); test("SingleLineBasic: Tags within frontmatter applies to all questions when not overriden", async () => { - let noteText: string = `--- + const noteText: string = `--- sr-due: 2024-01-17 sr-interval: 16 sr-ease: 278 @@ -435,10 +429,10 @@ Q1::A1 Q2::A2 Q3::A3 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let questionList: Question[] = await parserWithDefaultSettings.createQuestionList( + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const questionList: Question[] = await parserWithDefaultSettings.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -452,8 +446,8 @@ Q3::A3 test("MultiLine: Space before multi line separator", async () => { // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/853 - let noteText: string = ` -#flashcards/test/b853 + const noteText: string = ` +#flashcards/test/b853 Question::Answer @@ -464,11 +458,10 @@ Multiline answer Multiline question2 ?? Multiline answer2 - -`; - let noteFile: ISRFile = new UnitTestSRFile(noteText); - let questionList: Question[] = await parserWithDefaultSettings.createQuestionList( +`; + const noteFile: ISRFile = new UnitTestSRFile(noteText); + const questionList: Question[] = await parserWithDefaultSettings.createQuestionList( noteFile, TextDirection.Ltr, TopicPath.emptyPath, @@ -502,20 +495,20 @@ Multiline answer2 describe("Handling tags within note", () => { describe("Settings mode: Convert folder path to tag", () => { - let settings: SRSettings = { ...DEFAULT_SETTINGS }; + const settings: SRSettings = { ...DEFAULT_SETTINGS }; settings.convertFoldersToDecks = true; - let parser2: NoteQuestionParser = createTest_NoteQuestionParser(settings); + const parser2: NoteQuestionParser = createTest_NoteQuestionParser(settings); test("Folder path applies to all questions within note", async () => { - let noteText: string = ` + const noteText: string = ` Q1::A1 Q2::A2 Q3::A3 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]); - let questionList: Question[] = await parser2.createQuestionList( + const noteFile: ISRFile = new UnitTestSRFile(noteText); + const folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]); + const questionList: Question[] = await parser2.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -527,13 +520,13 @@ describe("Handling tags within note", () => { }); test("Topic tag within note is ignored (outside all questions)", async () => { - let noteText: string = `#flashcards/test + const noteText: string = `#flashcards/test Q1::A1 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]); - let questionList: Question[] = await parser2.createQuestionList( + const noteFile: ISRFile = new UnitTestSRFile(noteText); + const folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]); + const questionList: Question[] = await parser2.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -547,13 +540,13 @@ Q1::A1 // It could be argued that topic tags within a question should override the folder based topic test("Topic tag within note is ignored (within specific question)", async () => { // The tag "#flashcards/test" specifies a different topic than the folderTopicPath below - let noteText: string = ` + const noteText: string = ` #flashcards/test Q1::A1 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]); - let questionList: Question[] = await parser2.createQuestionList( + const noteFile: ISRFile = new UnitTestSRFile(noteText); + const folderTopicPath: TopicPath = new TopicPath(["folder", "subfolder"]); + const questionList: Question[] = await parser2.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -568,16 +561,16 @@ Q1::A1 expect(parserWithDefaultSettings.settings.convertFoldersToDecks).toEqual(false); test("Topic tag before first question applies to all questions", async () => { - let noteText: string = `#flashcards/test + const noteText: string = `#flashcards/test Q1::A1 Q2::A2 Q3::A3 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let expectedPath: string = "#flashcards/test"; - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let questionList: Question[] = await parserWithDefaultSettings.createQuestionList( + const expectedPath: string = "#flashcards/test"; + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const questionList: Question[] = await parserWithDefaultSettings.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -591,27 +584,26 @@ Q1::A1 // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/915#issuecomment-2017508391 test("Topic tag on first line after frontmatter", async () => { - let noteText: string = `--- + const noteText: string = `--- created: 2023-10-26T07:34 --- -#flashcards/English +#flashcards/English ## taunting & teasing & irony & sarcasm Stop trying ==to milk the crowd== for sympathy. // доить толпу `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let expectedPath: string = "#flashcards/English"; - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let expected = [ + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const expected = [ { questionType: CardType.Cloze, topicPathList: TopicPathList.fromPsv("#flashcards/English", 3), // #flashcards/English is on the 4th line, line number 3 cards: [ new Card({ front: "Stop trying [...] for sympathy. // доить толпу", - back: `Stop trying to milk the crowd for sympathy. // доить толпу`, + back: "Stop trying to milk the crowd for sympathy. // доить толпу", }), ], }, @@ -627,15 +619,15 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу }); test("Topic tag within question overrides the note topic, for that topic only", async () => { - let noteText: string = `#flashcards/test + const noteText: string = `#flashcards/test Q1::A1 #flashcards/examination Q2::A2 Q3::This has the "flashcards/test" topic, not "flashcards/examination" `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let questionList: Question[] = await parserWithDefaultSettings.createQuestionList( + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const questionList: Question[] = await parserWithDefaultSettings.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -648,17 +640,16 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу }); test("First topic tag within note (outside questions) is used as the note's topic tag, even if it appears after the first question", async () => { - let noteText: string = ` + const noteText: string = ` Q1::A1 This has the "flashcards/test" topic, even though the first topic tag is after this line in the file #flashcards/test Q2::A2 Q3::A3 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let expectedPath: TopicPath = new TopicPath(["flashcards", "test"]); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let questionList: Question[] = await parserWithDefaultSettings.createQuestionList( + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const questionList: Question[] = await parserWithDefaultSettings.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -670,17 +661,17 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу }); test("The last topic tag within note prior to the question is used as the note's topic tag", async () => { - let noteText: string = ` + const noteText: string = ` Q1::A1 #flashcards/test Q2::A2 #flashcards/examination Q3::This has the "flashcards/examination" topic, not "flashcards/test" `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let questionList: Question[] = await parserWithDefaultSettings.createQuestionList( + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const questionList: Question[] = await parserWithDefaultSettings.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -697,14 +688,13 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу expect(parserWithDefaultSettings.settings.convertFoldersToDecks).toEqual(false); test("Leading white space before topic tag", async () => { - let noteText: string = ` + const noteText: string = ` #flashcards/science Q5::A5 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let expectedPath: TopicPath = new TopicPath(["flashcards", "science"]); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let questionList: Question[] = await parserWithDefaultSettings.createQuestionList( + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const questionList: Question[] = await parserWithDefaultSettings.createQuestionList( noteFile, TextDirection.Ltr, folderTopicPath, @@ -718,31 +708,31 @@ Stop trying ==to milk the crowd== for sympathy. // доить толпу // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/915#issuecomment-2016580471 test("Topic tag at end of line question line (no other tags present)", async () => { - let noteText: string = `--- -Title: "The Taliban at war: 2001-2018" -Authors: "Antonio Giustozzi" + const noteText: string = `--- +Title: "Aegon's Conquest: 2BC-1AC" +Authors: "GRRM" Year: 2019 -URL: -DOI: -Unique Citekey: Talibanwar20012018 -Zotero Link: zotero://select/items/@Talibanwar20012018 +URL: +DOI: +Unique Citekey: AegonsConquest2BC1AC +Zotero Link: zotero://select/items/@AegonsConquest2BC1AC --- -> [!PDF|255, 208, 0] [[The Taliban at War_ 2001 - 2018.pdf#page=10&annotation=1440R|The Taliban at War_ 2001 - 2018, page 10]] -> > The Taliban Emirate, established in 1996, was in 2001 overthrown relatively easily by a coalition of US forces and various Afghan anti-Taliban groups. Few at the end of 2001 expected to hear again from the Taliban, except in the annals of history. Even as signs emerged in 2003 of a Taliban comeback, in the shape of an insurgency against the post-2001 Afghan government and its international sponsors, many did not take it seriously. It was hard to imagine that the Taliban would be able to mount a resilient challenge to a large-scale commitment of forces by the US and its allies. +> [!PDF|255, 208, 0] [[Aegon's Conquest_ 2BC - 1AC.pdf#page=10&annotation=1440R|Aegon's Conquest_ 2BC - 1AC, page 10]] +> > Aegon's Conquest also known as the War of Conquest or simply the Conquest, was the first of the Wars of Conquest initiated by Aegon Targaryen to conquer the continent of Westeros. Supported by his sister-wives, Rhaenys and Visenya, and their dragons, Meraxes and Vhagar, as well as his own, Balerion, Aegon successfully unified six of the Seven Kingdoms of Westeros under the invading forces of House Targaryen within two years. Only Dorne was able to successfully resist Aegon's conquest. Despite its name, the conflict was not entirely resolved on the field of battle, as some regions and houses of Westeros actively supported the Targaryen war effort, while others voluntarily surrendered to their might. -What year was the Taliban Emirate founded?::1996 #flashcards +What year did Aegon's Conquest start?::2BC #flashcards `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let expected = [ + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const expected = [ { questionType: CardType.SingleLineBasic, topicPathList: TopicPathList.fromPsv("#flashcards", 12), cards: [ new Card({ - front: "What year was the Taliban Emirate founded?", - back: "1996 #flashcards", + front: "What year did Aegon's Conquest start?", + back: "2BC #flashcards", }), ], }, @@ -763,7 +753,7 @@ describe("Questions immediately after closing line of frontmatter", () => { // The frontmatter should be discarded // (only the specified question text should be used) test("Multi-line with question", async () => { - let noteText: string = `--- + const noteText: string = `--- created: 2024-03-11 10:41 tags: - flashcards @@ -773,17 +763,17 @@ tags: ? In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*! `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let expected = [ + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const expected = [ { questionType: CardType.MultiLineBasic, // Explicitly checking that #data-structure is not included topicPathList: TopicPathList.fromPsv("#flashcards", frontmatterTagPseudoLineNum), cards: [ new Card({ - front: `**What is a Heap?**`, + front: "**What is a Heap?**", back: "In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*!", }), ], @@ -799,28 +789,34 @@ In computer-science, a *heap* is a tree-based data-structure, that satisfies the ).toMatchObject(expected); }); - test("Multi-line without question (i.e. question is blank)", async () => { - let noteText: string = `--- + test("Multi-line without question is ignored", async () => { + const noteText: string = `--- created: 2024-03-11 10:41 tags: - flashcards - data-structure --- ? -In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*! +A1 + +Q2 +? +A2 `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let expected = [ + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const expected = [ { questionType: CardType.MultiLineBasic, // Explicitly checking that #data-structure is not included topicPathList: TopicPathList.fromPsv("#flashcards", frontmatterTagPseudoLineNum), + + // No card A1; only card Q2/A2 cards: [ new Card({ - front: "", - back: "In computer-science, a *heap* is a tree-based data-structure, that satisfies the *heap property*. A heap is a complete *binary-tree*!", + front: "Q2", + back: "A2", }), ], }, @@ -836,7 +832,7 @@ In computer-science, a *heap* is a tree-based data-structure, that satisfies the }); test("single-line question", async () => { - let noteText: string = `--- + const noteText: string = `--- created: 2024-03-11 10:41 tags: - flashcards @@ -845,10 +841,10 @@ tags: In computer-science, a *heap* is::a tree-based data-structure A::B `; - let noteFile: ISRFile = new UnitTestSRFile(noteText); + const noteFile: ISRFile = new UnitTestSRFile(noteText); - let folderTopicPath: TopicPath = TopicPath.emptyPath; - let expected = [ + const folderTopicPath: TopicPath = TopicPath.emptyPath; + const expected = [ { questionType: CardType.SingleLineBasic, topicPathList: TopicPathList.fromPsv("#flashcards", frontmatterTagPseudoLineNum), @@ -880,51 +876,3 @@ A::B ).toMatchObject(expected); }); }); - -function checkQuestion1(question: Question) { - expect(question.cards.length).toEqual(1); - let card1 = { - cardIdx: 0, - isDue: false, - front: "Q1", - back: "A1", - scheduleInfo: null as CardScheduleInfo, - }; - let expected = { - questionType: CardType.SingleLineBasic, - topicPath: TopicPath.emptyPath, - questionTextOriginal: `Q1::A1`, - questionTextCleaned: "Q1::A1", - lineNo: 1, - hasEditLaterTag: false, - context: "", - hasChanged: false, - }; - expect(question).toMatchObject(expected); - expect(question.cards[0]).toMatchObject(card1); - return question; -} - -function checkQuestion2(question: Question) { - expect(question.cards.length).toEqual(1); - let card1 = { - cardIdx: 0, - isDue: false, - front: "Q2", - back: "A2", - scheduleInfo: null as CardScheduleInfo, - }; - let expected = { - questionType: CardType.SingleLineBasic, - topicPath: TopicPath.emptyPath, - questionTextOriginal: `Q2::A2`, - questionTextCleaned: "Q2::A2", - lineNo: 2, - hasEditLaterTag: false, - context: "", - hasChanged: false, - }; - expect(question).toMatchObject(expected); - expect(question.cards[0]).toMatchObject(card1); - return question; -} diff --git a/tests/unit/note-review-queue.test.ts b/tests/unit/note-review-queue.test.ts new file mode 100644 index 00000000..c6345dfb --- /dev/null +++ b/tests/unit/note-review-queue.test.ts @@ -0,0 +1,52 @@ +import { DueDateHistogram } from "src/due-date-histogram"; +import { DEFAULT_SETTINGS } from "src/settings"; +import { setupStaticDateProvider, setupStaticDateProvider_20230906 } from "src/utils/dates"; + +import { UnitTestOsrCore } from "./helpers/unit-test-core"; +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; + +beforeAll(() => { + setupStaticDateProvider_20230906(); + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); +}); + +function checkHistogramValue(histogram: DueDateHistogram, nDays: number, expectedValue: number) { + expect(histogram.hasEntryForDays(nDays)).toEqual(true); + expect(histogram.get(nDays)).toEqual(expectedValue); +} + +function checkHistogramDueCardCount(histogram: DueDateHistogram, expectedValue: number) { + checkHistogramValue(histogram, DueDateHistogram.dueNowNDays, expectedValue); +} + +describe("determineScheduleInfo", () => { + test("No notes due", async () => { + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS); + + // A.md due 2023-09-10 (in 4 days time) + await osrCore.loadTestVault("notes4"); + const histogram: DueDateHistogram = osrCore.dueDateNoteHistogram; + expect(histogram.hasEntryForDays(DueDateHistogram.dueNowNDays)).toEqual(false); + }); + + test("Note A.md due today", async () => { + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS); + + // A.md due 2023-09-10, so it should be due + setupStaticDateProvider("2023-09-10"); + + await osrCore.loadTestVault("notes4"); + const histogram: DueDateHistogram = osrCore.dueDateNoteHistogram; + checkHistogramDueCardCount(histogram, 1); + }); +}); + +describe("dueNotesCount", () => { + test("No notes due", async () => { + const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS); + + // A.md due 2023-09-10 (in 4 days time) + await osrCore.loadTestVault("notes4"); + expect(osrCore.noteReviewQueue.dueNotesCount).toEqual(1); + }); +}); diff --git a/tests/unit/note.test.ts b/tests/unit/note.test.ts new file mode 100644 index 00000000..ccb4d927 --- /dev/null +++ b/tests/unit/note.test.ts @@ -0,0 +1,82 @@ +import { Deck } from "src/deck"; +import { Note } from "src/note"; +import { NoteFileLoader } from "src/note-file-loader"; +import { NoteParser } from "src/note-parser"; +import { DEFAULT_SETTINGS } from "src/settings"; +import { TopicPath } from "src/topic-path"; +import { TextDirection } from "src/utils/strings"; + +import { UnitTestSRFile } from "./helpers/unit-test-file"; +import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/unit-test-setup"; + +const parser: NoteParser = new NoteParser(DEFAULT_SETTINGS); +const noteFileLoader: NoteFileLoader = new NoteFileLoader(DEFAULT_SETTINGS); + +beforeAll(() => { + unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS); +}); + +describe("appendCardsToDeck", () => { + test("Multiple questions, single card per question", async () => { + const noteText: string = `#flashcards/test +Q1::A1 +Q2::A2 +Q3::A3 +`; + const file: UnitTestSRFile = new UnitTestSRFile(noteText); + const folderTopicPath = TopicPath.emptyPath; + const note: Note = await parser.parse(file, TextDirection.Ltr, folderTopicPath); + const deck: Deck = Deck.emptyDeck; + note.appendCardsToDeck(deck); + const subdeck: Deck = deck.getDeck(new TopicPath(["flashcards", "test"])); + expect(subdeck.newFlashcards[0].front).toEqual("Q1"); + expect(subdeck.newFlashcards[1].front).toEqual("Q2"); + expect(subdeck.newFlashcards[2].front).toEqual("Q3"); + expect(subdeck.dueFlashcards.length).toEqual(0); + }); + + test("Multiple questions, multiple cards per question", async () => { + const noteText: string = `#flashcards/test +Q1:::A1 +Q2:::A2 +Q3:::A3 +`; + const file: UnitTestSRFile = new UnitTestSRFile(noteText); + const folderTopicPath = TopicPath.emptyPath; + const note: Note = await parser.parse(file, TextDirection.Ltr, folderTopicPath); + const deck: Deck = Deck.emptyDeck; + note.appendCardsToDeck(deck); + const subdeck: Deck = deck.getDeck(new TopicPath(["flashcards", "test"])); + expect(subdeck.newFlashcards.length).toEqual(6); + const frontList = subdeck.newFlashcards.map((card) => card.front); + + expect(frontList).toEqual(["Q1", "A1", "Q2", "A2", "Q3", "A3"]); + expect(subdeck.dueFlashcards.length).toEqual(0); + }); +}); + +describe("writeNoteFile", () => { + test("Multiple questions, some with too many schedule details", async () => { + const originalText: string = `#flashcards/test +Q1::A1 +#flashcards Q2::A2 + +Q3:::A3 + +`; + const file: UnitTestSRFile = new UnitTestSRFile(originalText); + const note: Note = await noteFileLoader.load(file, TextDirection.Ltr, TopicPath.emptyPath); + + await note.writeNoteFile(DEFAULT_SETTINGS); + const updatedText: string = file.content; + + const expectedText: string = `#flashcards/test +Q1::A1 +#flashcards Q2::A2 + +Q3:::A3 + +`; + expect(updatedText).toEqual(expectedText); + }); +}); diff --git a/tests/unit/parser.test.ts b/tests/unit/parser.test.ts index 9d2c7b6f..f6f30b8f 100644 --- a/tests/unit/parser.test.ts +++ b/tests/unit/parser.test.ts @@ -1,41 +1,26 @@ -import { parseEx, ParsedQuestionInfo } from "src/parser"; -import { CardType } from "src/Question"; - -const defaultArgs: [string, string, string, string, boolean, boolean, boolean] = [ - "::", - ":::", - "?", - "??", - true, - true, - true, -]; +import { ParsedQuestionInfo, parseEx, setDebugParser } from "src/parser"; +import { ParserOptions } from "src/parser"; +import { CardType } from "src/question"; + +const parserOptions: ParserOptions = { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: true, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: true, +}; /** * This function is a small wrapper around parseEx used for testing only. + * It generates a parser each time, overwriting the default one. * Created when the actual parser changed from returning [CardType, string, number, number] to ParsedQuestionInfo. * It's purpose is to minimise changes to all the test cases here during the parser()->parserEx() change. */ -function parse( - text: string, - singlelineCardSeparator: string, - singlelineReversedCardSeparator: string, - multilineCardSeparator: string, - multilineReversedCardSeparator: string, - convertHighlightsToClozes: boolean, - convertBoldTextToClozes: boolean, - convertCurlyBracketsToClozes: boolean, -): [CardType, string, number, number][] { - const list: ParsedQuestionInfo[] = parseEx( - text, - singlelineCardSeparator, - singlelineReversedCardSeparator, - multilineCardSeparator, - multilineReversedCardSeparator, - convertHighlightsToClozes, - convertBoldTextToClozes, - convertCurlyBracketsToClozes, - ); +function parse(text: string, options: ParserOptions): [CardType, string, number, number][] { + const list: ParsedQuestionInfo[] = parseEx(text, options); const result: [CardType, string, number, number][] = []; for (const item of list) { result.push([item.cardType, item.text, item.firstLineNum, item.lastLineNum]); @@ -44,163 +29,466 @@ function parse( } test("Test parsing of single line basic cards", () => { - expect(parse("Question::Answer", ...defaultArgs)).toEqual([ + // standard symbols + expect(parse("Question::Answer", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question::Answer", 0, 0], ]); - expect(parse("Question::Answer\n", ...defaultArgs)).toEqual([ + expect(parse("Question::Answer\n", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question::Answer\n", 0, 1], ]); - expect(parse("Question::Answer ", ...defaultArgs)).toEqual([ + expect(parse("Question::Answer ", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question::Answer ", 0, 0], ]); - expect(parse("Some text before\nQuestion ::Answer", ...defaultArgs)).toEqual([ + expect(parse("Some text before\nQuestion ::Answer", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question ::Answer", 1, 1], ]); - expect(parse("#Title\n\nQ1::A1\nQ2:: A2", ...defaultArgs)).toEqual([ + expect(parse("#Title\n\nQ1::A1\nQ2:: A2", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Q1::A1", 2, 2], [CardType.SingleLineBasic, "Q2:: A2", 3, 3], ]); - expect(parse("#flashcards/science Question ::Answer", ...defaultArgs)).toEqual([ + expect(parse("#flashcards/science Question ::Answer", parserOptions)).toEqual([ [CardType.SingleLineBasic, "#flashcards/science Question ::Answer", 0, 0], ]); + + // custom symbols + expect( + parse("Question&&Answer", { + singleLineCardSeparator: "&&", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.SingleLineBasic, "Question&&Answer", 0, 0]]); + expect( + parse("Question=Answer", { + singleLineCardSeparator: "=", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.SingleLineBasic, "Question=Answer", 0, 0]]); + + // empty string or whitespace character provided + expect( + parse("Question::Answer", { + singleLineCardSeparator: "", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([]); }); test("Test parsing of single line reversed cards", () => { - expect(parse("Question:::Answer", ...defaultArgs)).toEqual([ + // standard symbols + expect(parse("Question:::Answer", parserOptions)).toEqual([ [CardType.SingleLineReversed, "Question:::Answer", 0, 0], ]); - expect(parse("Some text before\nQuestion :::Answer", ...defaultArgs)).toEqual([ + expect(parse("Some text before\nQuestion :::Answer", parserOptions)).toEqual([ [CardType.SingleLineReversed, "Question :::Answer", 1, 1], ]); - expect(parse("#Title\n\nQ1:::A1\nQ2::: A2", ...defaultArgs)).toEqual([ + expect(parse("#Title\n\nQ1:::A1\nQ2::: A2", parserOptions)).toEqual([ [CardType.SingleLineReversed, "Q1:::A1", 2, 2], [CardType.SingleLineReversed, "Q2::: A2", 3, 3], ]); + + // custom symbols + expect( + parse("Question&&&Answer", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: "&&&", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.SingleLineReversed, "Question&&&Answer", 0, 0]]); + + // empty string or whitespace character provided + expect( + parse("Question:::Answer", { + singleLineCardSeparator: ">", + singleLineReversedCardSeparator: " ", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([]); }); test("Test parsing of multi line basic cards", () => { - expect(parse("Question\n?\nAnswer", ...defaultArgs)).toEqual([ + // standard symbols + expect(parse("Question\n?\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer", 0, 2], ]); - expect(parse("Question\n? \nAnswer", ...defaultArgs)).toEqual([ + expect(parse("Question\n? \nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer", 0, 2], ]); - expect(parse("Question\n?\nAnswer ", ...defaultArgs)).toEqual([ + expect(parse("Question\n?\nAnswer ", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer ", 0, 2], ]); - expect(parse("Question\n?\nAnswer\n", ...defaultArgs)).toEqual([ + expect(parse("Question\n?\nAnswer\n", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer\n", 0, 3], ]); - expect(parse("Question line 1\nQuestion line 2\n?\nAnswer", ...defaultArgs)).toEqual([ + expect(parse("Question line 1\nQuestion line 2\n?\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question line 1\nQuestion line 2\n?\nAnswer", 0, 3], ]); - expect(parse("Question\n?\nAnswer line 1\nAnswer line 2", ...defaultArgs)).toEqual([ + expect(parse("Question\n?\nAnswer line 1\nAnswer line 2", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer line 1\nAnswer line 2", 0, 3], ]); - expect(parse("#Title\n\nLine0\nQ1\n?\nA1\nAnswerExtra\n\nQ2\n?\nA2", ...defaultArgs)).toEqual([ - [ - CardType.MultiLineBasic, - "Line0\nQ1\n?\nA1\nAnswerExtra", - /* Line0 */ 2, - /* AnswerExtra */ 6, - ], + expect(parse("#Title\n\nLine0\nQ1\n?\nA1\nAnswerExtra\n\nQ2\n?\nA2", parserOptions)).toEqual([ + [CardType.MultiLineBasic, "Line0\nQ1\n?\nA1\nAnswerExtra", 2, 6], [CardType.MultiLineBasic, "Q2\n?\nA2", 8, 10], ]); - expect(parse("#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", ...defaultArgs)).toEqual([ + expect(parse("#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", 0, 3], ]); + expect( + parse("Question\n?\nAnswer line 1\nAnswer line 2\n\n---", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.MultiLineBasic, "Question\n?\nAnswer line 1\nAnswer line 2", 0, 4]]); + expect( + parse( + "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion 2\n?\nAnswer line 1\nAnswer line 2\n---\n", + { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: false, + }, + ), + ).toEqual([ + [CardType.MultiLineBasic, "Question 1\n?\nAnswer line 1\nAnswer line 2", 0, 4], + [CardType.MultiLineBasic, "Question 2\n?\nAnswer line 1\nAnswer line 2", 6, 9], + ]); + expect( + parse( + "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion with empty line after question mark\n?\n\nAnswer line 1\nAnswer line 2\n---\n", + { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: false, + }, + ), + ).toEqual([ + [CardType.MultiLineBasic, "Question 1\n?\nAnswer line 1\nAnswer line 2", 0, 4], + [ + CardType.MultiLineBasic, + "Question with empty line after question mark\n?\n\nAnswer line 1\nAnswer line 2", + 6, + 10, + ], + ]); + + // custom symbols + expect( + parse("Question\n@@\nAnswer\n\nsfdg", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "@@", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.MultiLineBasic, "Question\n@@\nAnswer", 0, 2]]); + + // empty string or whitespace character provided + expect( + parse("Question\n?\nAnswer", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([]); }); test("Test parsing of multi line reversed cards", () => { - expect(parse("Question\n??\nAnswer", ...defaultArgs)).toEqual([ + // standard symbols + expect(parse("Question\n??\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineReversed, "Question\n??\nAnswer", 0, 2], ]); - expect(parse("Question line 1\nQuestion line 2\n??\nAnswer", ...defaultArgs)).toEqual([ + expect(parse("Question line 1\nQuestion line 2\n??\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineReversed, "Question line 1\nQuestion line 2\n??\nAnswer", 0, 3], ]); - expect(parse("Question\n??\nAnswer line 1\nAnswer line 2", ...defaultArgs)).toEqual([ + expect(parse("Question\n??\nAnswer line 1\nAnswer line 2", parserOptions)).toEqual([ [CardType.MultiLineReversed, "Question\n??\nAnswer line 1\nAnswer line 2", 0, 3], ]); - expect(parse("#Title\n\nLine0\nQ1\n??\nA1\nAnswerExtra\n\nQ2\n??\nA2", ...defaultArgs)).toEqual( - [ - [ - CardType.MultiLineReversed, - "Line0\nQ1\n??\nA1\nAnswerExtra", - /* Line0 */ 2, - /* AnswerExtra */ 6, - ], - [CardType.MultiLineReversed, "Q2\n??\nA2", 8, 10], - ], - ); + expect(parse("#Title\n\nLine0\nQ1\n??\nA1\nAnswerExtra\n\nQ2\n??\nA2", parserOptions)).toEqual([ + [CardType.MultiLineReversed, "Line0\nQ1\n??\nA1\nAnswerExtra", 2, 6], + [CardType.MultiLineReversed, "Q2\n??\nA2", 8, 10], + ]); + expect( + parse("Question\n??\nAnswer line 1\nAnswer line 2\n\n---", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: true, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: true, + }), + ).toEqual([[CardType.MultiLineReversed, "Question\n??\nAnswer line 1\nAnswer line 2", 0, 4]]); + expect( + parse( + "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion 2\n??\nAnswer line 1\nAnswer line 2\n---\n", + { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: true, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: true, + }, + ), + ).toEqual([ + [CardType.MultiLineBasic, "Question 1\n?\nAnswer line 1\nAnswer line 2", 0, 4], + [CardType.MultiLineReversed, "Question 2\n??\nAnswer line 1\nAnswer line 2", 6, 9], + ]); + + // custom symbols + expect( + parse("Question\n@@@\nAnswer\n---", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "@@", + multilineReversedCardSeparator: "@@@", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([[CardType.MultiLineReversed, "Question\n@@@\nAnswer", 0, 2]]); + + // empty string or whitespace character provided + expect( + parse("Question\n??\nAnswer", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "\t", + multilineCardEndMarker: "---", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([]); }); test("Test parsing of cloze cards", () => { // ==highlights== - expect(parse("cloze ==deletion== test", ...defaultArgs)).toEqual([ + expect(parse("cloze ==deletion== test", parserOptions)).toEqual([ [CardType.Cloze, "cloze ==deletion== test", 0, 0], ]); - expect(parse("cloze ==deletion== test\n", ...defaultArgs)).toEqual([ + expect(parse("cloze ==deletion== test\n", parserOptions)).toEqual([ [CardType.Cloze, "cloze ==deletion== test\n", 0, 1], ]); - expect(parse("cloze ==deletion== test ", ...defaultArgs)).toEqual([ + expect(parse("cloze ==deletion== test ", parserOptions)).toEqual([ [CardType.Cloze, "cloze ==deletion== test ", 0, 0], ]); - expect(parse("==this== is a ==deletion==\n", ...defaultArgs)).toEqual([ + expect(parse("==this== is a ==deletion==\n", parserOptions)).toEqual([ [CardType.Cloze, "==this== is a ==deletion==", 0, 0], ]); expect( parse( "some text before\n\na deletion on\nsuch ==wow==\n\n" + "many text\nsuch surprise ==wow== more ==text==\nsome text after\n\nHmm", - ...defaultArgs, + parserOptions, ), ).toEqual([ [CardType.Cloze, "a deletion on\nsuch ==wow==", 2, 3], [CardType.Cloze, "many text\nsuch surprise ==wow== more ==text==\nsome text after", 5, 7], ]); - expect(parse("srdf ==", ...defaultArgs)).toEqual([]); - expect(parse("lorem ipsum ==p\ndolor won==", ...defaultArgs)).toEqual([]); - expect(parse("lorem ipsum ==dolor won=", ...defaultArgs)).toEqual([]); + expect(parse("srdf ==", parserOptions)).toEqual([]); + expect(parse("lorem ipsum ==p\ndolor won==", parserOptions)).toEqual([]); + expect(parse("lorem ipsum ==dolor won=", parserOptions)).toEqual([]); + // ==highlights== turned off - expect(parse("cloze ==deletion== test", "::", ":::", "?", "??", false, true, false)).toEqual( - [], - ); + expect( + parse("cloze ==deletion== test", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: false, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: true, + }), + ).toEqual([]); // **bolded** - expect(parse("cloze **deletion** test", ...defaultArgs)).toEqual([ + expect(parse("cloze **deletion** test", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test", 0, 0], ]); - expect(parse("cloze **deletion** test\n", ...defaultArgs)).toEqual([ + expect(parse("cloze **deletion** test\n", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test\n", 0, 1], ]); - expect(parse("cloze **deletion** test ", ...defaultArgs)).toEqual([ + expect(parse("cloze **deletion** test ", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test ", 0, 0], ]); - expect(parse("**this** is a **deletion**\n", ...defaultArgs)).toEqual([ + expect(parse("**this** is a **deletion**\n", parserOptions)).toEqual([ [CardType.Cloze, "**this** is a **deletion**", 0, 0], ]); expect( parse( "some text before\n\na deletion on\nsuch **wow**\n\n" + "many text\nsuch surprise **wow** more **text**\nsome text after\n\nHmm", - ...defaultArgs, + parserOptions, ), ).toEqual([ [CardType.Cloze, "a deletion on\nsuch **wow**", 2, 3], [CardType.Cloze, "many text\nsuch surprise **wow** more **text**\nsome text after", 5, 7], ]); - expect(parse("srdf **", ...defaultArgs)).toEqual([]); - expect(parse("lorem ipsum **p\ndolor won**", ...defaultArgs)).toEqual([]); - expect(parse("lorem ipsum **dolor won*", ...defaultArgs)).toEqual([]); + expect(parse("srdf **", parserOptions)).toEqual([]); + expect(parse("lorem ipsum **p\ndolor won**", parserOptions)).toEqual([]); + expect(parse("lorem ipsum **dolor won*", parserOptions)).toEqual([]); + // **bolded** turned off - expect(parse("cloze **deletion** test", "::", ":::", "?", "??", true, false, false)).toEqual( - [], - ); + expect( + parse("cloze **deletion** test", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: true, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: true, + }), + ).toEqual([]); - // both - expect(parse("cloze **deletion** test ==another deletion==!", ...defaultArgs)).toEqual([ + // {{curly}} + expect(parse("cloze {{deletion}} test", parserOptions)).toEqual([ + [CardType.Cloze, "cloze {{deletion}} test", 0, 0], + ]); + expect(parse("cloze {{deletion}} test\n", parserOptions)).toEqual([ + [CardType.Cloze, "cloze {{deletion}} test\n", 0, 1], + ]); + expect(parse("cloze {{deletion}} test ", parserOptions)).toEqual([ + [CardType.Cloze, "cloze {{deletion}} test ", 0, 0], + ]); + expect(parse("{{this}} is a {{deletion}}\n", parserOptions)).toEqual([ + [CardType.Cloze, "{{this}} is a {{deletion}}", 0, 0], + ]); + expect( + parse( + "some text before\n\na deletion on\nsuch {{wow}}\n\n" + + "many text\nsuch surprise {{wow}} more {{text}}\nsome text after\n\nHmm", + parserOptions, + ), + ).toEqual([ + [CardType.Cloze, "a deletion on\nsuch {{wow}}", 2, 3], + [CardType.Cloze, "many text\nsuch surprise {{wow}} more {{text}}\nsome text after", 5, 7], + ]); + expect(parse("srdf {{", parserOptions)).toEqual([]); + expect(parse("srdf }}", parserOptions)).toEqual([]); + expect(parse("lorem ipsum {{p\ndolor won}}", parserOptions)).toEqual([]); + expect(parse("lorem ipsum {{dolor won}", parserOptions)).toEqual([]); + + // {{curly}} turned off + expect( + parse("cloze {{deletion}} test", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: true, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([]); + + // combo + expect(parse("cloze **deletion** test ==another deletion==!", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test ==another deletion==!", 0, 0], ]); + expect( + parse( + "Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nTest 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nHere is some more text.", + { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + convertHighlightsToClozes: true, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }, + ), + ).toEqual([ + [ + CardType.Cloze, + "Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.", + 0, + 7, + ], + [ + CardType.Cloze, + "Test 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.", + 10, + 17, + ], + ]); + + // all disabled + expect( + parse("cloze {{deletion}} test and **deletion** ==another deletion==!", { + singleLineCardSeparator: "::", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: false, + convertBoldTextToClozes: false, + convertCurlyBracketsToClozes: false, + }), + ).toEqual([]); }); test("Test parsing of a mix of card types", () => { @@ -210,7 +498,7 @@ test("Test parsing of a mix of card types", () => { "Duis magna arcu, eleifend rhoncus ==euismod non,==\nlaoreet vitae enim.\n\n" + "Fusce placerat::velit in pharetra gravida\n\n" + "Donec dapibus ullamcorper aliquam.\n??\nDonec dapibus ullamcorper aliquam.\n", - ...defaultArgs, + parserOptions, ), ).toEqual([ [ @@ -226,18 +514,33 @@ test("Test parsing of a mix of card types", () => { CardType.MultiLineReversed, "Donec dapibus ullamcorper aliquam.\n??\nDonec dapibus ullamcorper aliquam.\n", 8, - 11 /* */, + 11, ], ]); }); -test("Test codeblocks", () => { - // no blank lines +test("Test parsing cards with codeblocks", () => { + // `inline` + expect( + parse( + "my inline question containing `some inline code` in it::and this is answer possibly containing `inline` code.", + parserOptions, + ), + ).toEqual([ + [ + CardType.SingleLineBasic, + "my inline question containing `some inline code` in it::and this is answer possibly containing `inline` code.", + 0, + 0, + ], + ]); + + // ```block```, no blank lines expect( parse( "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\nprint('Howdy?')\nlambda x: x[0]\n```", - ...defaultArgs, + parserOptions, ), ).toEqual([ [ @@ -245,16 +548,16 @@ test("Test codeblocks", () => { "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\nprint('Howdy?')\nlambda x: x[0]\n```", 0, - 6 /* ``` */, + 6, ], ]); - // with blank lines + // ```block```, with blank lines expect( parse( "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\n\n\nprint('Howdy?')\n\nlambda x: x[0]\n```", - ...defaultArgs, + parserOptions, ), ).toEqual([ [ @@ -262,11 +565,11 @@ test("Test codeblocks", () => { "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\n\n\nprint('Howdy?')\n\nlambda x: x[0]\n```", 0, - 9 /* ``` */, + 9, ], ]); - // general Markdown syntax + // nested markdown expect( parse( "Nested Markdown?\n?\n" + @@ -279,7 +582,7 @@ test("Test codeblocks", () => { "print('hello world')\n" + "~~~\n" + "````", - ...defaultArgs, + parserOptions, ), ).toEqual([ [ @@ -295,21 +598,108 @@ test("Test codeblocks", () => { "~~~\n" + "````", 0, - 12 /* ``` */, + 12, ], ]); }); test("Test not parsing cards in HTML comments", () => { + expect(parse("", parserOptions)).toEqual([]); + expect(parse("", parserOptions)).toEqual([]); expect( - parse("\n-->", ...defaultArgs), + parse("\n-->", parserOptions), ).toEqual([]); expect( parse( "\n\n-->", - ...defaultArgs, + parserOptions, ), ).toEqual([]); - expect(parse("", ...defaultArgs)).toEqual([]); - expect(parse("", ...defaultArgs)).toEqual([]); + expect(parse("", parserOptions)).toEqual([]); + expect(parse("", parserOptions)).toEqual([]); + expect(parse("", parserOptions)).toEqual([]); +}); + +test("Test not parsing 'cards' in codeblocks", () => { + // block + expect(parse("```\nCodeblockq::CodeblockA\n```", parserOptions)).toEqual([]); + expect(parse("```\nCodeblockq:::CodeblockA\n```", parserOptions)).toEqual([]); + expect( + parse("# Title\n\n```markdown\nsome ==highlighted text==!\n```\n\nmore!", parserOptions), + ).toEqual([]); + expect( + parse("# Title\n```markdown\nsome **bolded text**!\n```\n\nmore!", parserOptions), + ).toEqual([]); + expect(parse("# Title\n\n```\nfoo = {{'a': 2}}\n```\n\nmore!", parserOptions)).toEqual([]); + + // inline + expect(parse("`Inlineq::InlineA`", parserOptions)).toEqual([]); + expect( + parse("# Title\n`if (a & b) {}`\nmore!", { + singleLineCardSeparator: "&", + singleLineReversedCardSeparator: ":::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "", + convertHighlightsToClozes: true, + convertBoldTextToClozes: true, + convertCurlyBracketsToClozes: true, + }), + ).toEqual([]); + expect(parse("# Title\n`if a == b && c == d {}`\nmore!", parserOptions)).toEqual([]); + expect(parse("# Title\n\n`z = a ** b + 5 ** 7`\n\nmore!", parserOptions)).toEqual([]); + expect(parse("# Title\n`foo = {{'a': 2}}`\n\nmore!", parserOptions)).toEqual([]); + + // combo + expect( + parse( + "Question::Answer\n\n```\nCodeblockq::CodeblockA\n```\n\n`Inlineq::InlineA`\n", + parserOptions, + ), + ).toEqual([[CardType.SingleLineBasic, "Question::Answer", 0, 0]]); +}); + +test("Unexpected Error case", () => { + // replace console error log with an empty mock function + const errorSpy = jest.spyOn(global.console, "error").mockImplementation(() => {}); + + expect(parseEx("", null)).toStrictEqual([]); + + expect(errorSpy).toHaveBeenCalled(); + expect(errorSpy.mock.calls[0][0]).toMatch(/^Unexpected error:.*/); + + // clear the mock + errorSpy.mockClear(); + + expect(parseEx("", parserOptions)).toStrictEqual([]); + expect(errorSpy).toHaveBeenCalledTimes(0); + + // restore original console error log + errorSpy.mockRestore(); +}); + +describe("Parser debug messages", () => { + test("Messages disabled", () => { + // replace console error log with an empty mock function + const logSpy = jest.spyOn(global.console, "log").mockImplementation(() => {}); + setDebugParser(false); + + parseEx("", parserOptions); + expect(logSpy).toHaveBeenCalledTimes(0); + + // restore original console error log + logSpy.mockRestore(); + }); + + test("Messages enabled", () => { + // replace console error log with an empty mock function + const logSpy = jest.spyOn(global.console, "log").mockImplementation(() => {}); + setDebugParser(true); + + parseEx("", parserOptions); + expect(logSpy).toHaveBeenCalled(); + + // restore original console error log + logSpy.mockRestore(); + }); }); diff --git a/tests/unit/QuestionType.test.ts b/tests/unit/question-type.test.ts similarity index 94% rename from tests/unit/QuestionType.test.ts rename to tests/unit/question-type.test.ts index 108c153b..bcbcd765 100644 --- a/tests/unit/QuestionType.test.ts +++ b/tests/unit/question-type.test.ts @@ -1,5 +1,5 @@ -import { CardType } from "src/Question"; -import { CardFrontBack, CardFrontBackUtil, QuestionType_ClozeUtil } from "src/QuestionType"; +import { CardType } from "src/question"; +import { CardFrontBack, CardFrontBackUtil, QuestionType_ClozeUtil } from "src/question-type"; import { DEFAULT_SETTINGS, SRSettings } from "src/settings"; test("CardType.SingleLineBasic", () => { @@ -37,7 +37,7 @@ test("CardType.MultiLineReversed", () => { }); test("CardType.Cloze", () => { - let frontHtml = QuestionType_ClozeUtil.renderClozeFront(); + const frontHtml = QuestionType_ClozeUtil.renderClozeFront(); expect( CardFrontBackUtil.expand( @@ -52,7 +52,7 @@ test("CardType.Cloze", () => { ), ]); - let settings2: SRSettings = DEFAULT_SETTINGS; + const settings2: SRSettings = DEFAULT_SETTINGS; settings2.convertBoldTextToClozes = true; settings2.convertHighlightsToClozes = true; settings2.convertCurlyBracketsToClozes = true; diff --git a/tests/unit/Question.test.ts b/tests/unit/question.test.ts similarity index 72% rename from tests/unit/Question.test.ts rename to tests/unit/question.test.ts index e6c65c9b..0412cbe1 100644 --- a/tests/unit/Question.test.ts +++ b/tests/unit/question.test.ts @@ -1,19 +1,18 @@ -import { TopicPath } from "src/TopicPath"; +import { Question, QuestionText } from "src/question"; import { DEFAULT_SETTINGS, SRSettings } from "src/settings"; -import { Question, QuestionText } from "src/Question"; -import { TextDirection } from "src/util/TextDirection"; +import { TextDirection } from "src/utils/strings"; -let settings_cardCommentOnSameLine: SRSettings = { ...DEFAULT_SETTINGS }; +const settings_cardCommentOnSameLine: SRSettings = { ...DEFAULT_SETTINGS }; settings_cardCommentOnSameLine.cardCommentOnSameLine = true; describe("Question", () => { describe("getHtmlCommentSeparator", () => { test("Ends with a code block", async () => { - let text: string = + const text: string = "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\nprint('Howdy?')\nlambda x: x[0]\n```"; - let question: Question = new Question({ + const question: Question = new Question({ questionText: new QuestionText(text, null, text, TextDirection.Ltr, null), }); @@ -22,9 +21,9 @@ describe("Question", () => { }); test("Doesn't end with a code block", async () => { - let text: string = "Q1::A1"; + const text: string = "Q1::A1"; - let question: Question = new Question({ + const question: Question = new Question({ questionText: new QuestionText(text, null, text, TextDirection.Ltr, null), }); diff --git a/tests/unit/SampleItems.ts b/tests/unit/sample-items.ts similarity index 60% rename from tests/unit/SampleItems.ts rename to tests/unit/sample-items.ts index b928032c..eace206d 100644 --- a/tests/unit/SampleItems.ts +++ b/tests/unit/sample-items.ts @@ -1,29 +1,27 @@ -import { Card } from "src/Card"; -import { Deck } from "src/Deck"; -import { Note } from "src/Note"; -import { NoteParser } from "src/NoteParser"; -import { NoteQuestionParser } from "src/NoteQuestionParser"; -import { CardType, Question } from "src/Question"; -import { CardFrontBack, CardFrontBackUtil } from "src/QuestionType"; +import { Deck } from "src/deck"; +import { CardOrder, DeckOrder, DeckTreeIterator } from "src/deck-tree-iterator"; +import { Note } from "src/note"; +import { NoteParser } from "src/note-parser"; +import { NoteQuestionParser } from "src/note-question-parser"; 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"; +import { TopicPath } from "src/topic-path"; +import { TextDirection } from "src/utils/strings"; + +import { UnitTestSRFile } from "./helpers/unit-test-file"; export function createTest_NoteQuestionParser(settings: SRSettings): NoteQuestionParser { - let questionParser: NoteQuestionParser = new NoteQuestionParser(settings); + const questionParser: NoteQuestionParser = new NoteQuestionParser(settings); return questionParser; } export function createTest_NoteParser(): NoteParser { - let result = new NoteParser(DEFAULT_SETTINGS); + const result = new NoteParser(DEFAULT_SETTINGS); return result; } export const test_RefDate_20230906: Date = new Date(2023, 8, 6); export class SampleItemDecks { static async createSingleLevelTree_NewCards(): Promise { - let text: string = ` + const text: string = ` Q1::A1 Q2::A2 Q3::A3`; @@ -31,7 +29,7 @@ Q3::A3`; } static createScienceTree(): Deck { - let deck: Deck = new Deck("Root", null); + const deck: Deck = new Deck("Root", null); deck.getOrCreateDeck(new TopicPath(["Science", "Physics", "Electromagnetism"])); deck.getOrCreateDeck(new TopicPath(["Science", "Physics", "Light"])); deck.getOrCreateDeck(new TopicPath(["Science", "Physics", "Fluids"])); @@ -44,7 +42,7 @@ Q3::A3`; text: string, folderTopicPath: TopicPath = TopicPath.emptyPath, ): Promise { - let file: UnitTestSRFile = new UnitTestSRFile(text); + const file: UnitTestSRFile = new UnitTestSRFile(text); return await this.createDeckFromFile(file, folderTopicPath); } @@ -54,8 +52,8 @@ Q3::A3`; cardOrder: CardOrder, deckOrder: DeckOrder, ): Promise<[Deck, DeckTreeIterator]> { - let deck: Deck = await SampleItemDecks.createDeckFromText(text, folderTopicPath); - let iterator: DeckTreeIterator = new DeckTreeIterator( + const deck: Deck = await SampleItemDecks.createDeckFromText(text, folderTopicPath); + const iterator: DeckTreeIterator = new DeckTreeIterator( { cardOrder, deckOrder, @@ -69,9 +67,9 @@ Q3::A3`; file: UnitTestSRFile, folderTopicPath: TopicPath = TopicPath.emptyPath, ): Promise { - let deck: Deck = new Deck("Root", null); - let noteParser: NoteParser = createTest_NoteParser(); - let note: Note = await noteParser.parse(file, TextDirection.Ltr, folderTopicPath); + const deck: Deck = new Deck("Root", null); + const noteParser: NoteParser = createTest_NoteParser(); + const note: Note = await noteParser.parse(file, TextDirection.Ltr, folderTopicPath); note.appendCardsToDeck(deck); return deck; } diff --git a/tests/unit/scheduling.test.ts b/tests/unit/scheduling.test.ts index 36152e3e..b73e90fb 100644 --- a/tests/unit/scheduling.test.ts +++ b/tests/unit/scheduling.test.ts @@ -1,23 +1,48 @@ -import { schedule, ReviewResponse, textInterval } from "src/scheduling"; +import { ReviewResponse } from "src/algorithms/base/repetition-item"; +import { osrSchedule, textInterval } from "src/algorithms/osr/note-scheduling"; +import { DueDateHistogram } from "src/due-date-histogram"; import { DEFAULT_SETTINGS } from "src/settings"; +const emptyHistogram = new DueDateHistogram(); + test("Test reviewing with default settings", () => { expect( - schedule(ReviewResponse.Easy, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {}), + osrSchedule( + ReviewResponse.Easy, + 1, + DEFAULT_SETTINGS.baseEase, + 0, + DEFAULT_SETTINGS, + emptyHistogram, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase + 20, interval: 4, }); expect( - schedule(ReviewResponse.Good, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {}), + osrSchedule( + ReviewResponse.Good, + 1, + DEFAULT_SETTINGS.baseEase, + 0, + DEFAULT_SETTINGS, + emptyHistogram, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 3, }); expect( - schedule(ReviewResponse.Hard, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {}), + osrSchedule( + ReviewResponse.Hard, + 1, + DEFAULT_SETTINGS.baseEase, + 0, + DEFAULT_SETTINGS, + emptyHistogram, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase - 20, interval: 1, @@ -27,21 +52,42 @@ test("Test reviewing with default settings", () => { test("Test reviewing with default settings & delay", () => { const delay = 2 * 24 * 3600 * 1000; // two day delay expect( - schedule(ReviewResponse.Easy, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}), + osrSchedule( + ReviewResponse.Easy, + 10, + DEFAULT_SETTINGS.baseEase, + delay, + DEFAULT_SETTINGS, + emptyHistogram, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase + 20, interval: 42, }); expect( - schedule(ReviewResponse.Good, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}), + osrSchedule( + ReviewResponse.Good, + 10, + DEFAULT_SETTINGS.baseEase, + delay, + DEFAULT_SETTINGS, + emptyHistogram, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 28, }); expect( - schedule(ReviewResponse.Hard, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}), + osrSchedule( + ReviewResponse.Hard, + 10, + DEFAULT_SETTINGS.baseEase, + delay, + DEFAULT_SETTINGS, + emptyHistogram, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase - 20, interval: 5, @@ -49,59 +95,78 @@ test("Test reviewing with default settings & delay", () => { }); test("Test load balancing, small interval (load balancing disabled)", () => { - const dueDates = { + const originalInterval: number = 1; + const newInterval: number = 3; + const dueDates = new DueDateHistogram({ 0: 1, - 1: 1, + 1: 1, // key = originalInterval 2: 1, 3: 4, - }; + }); expect( - schedule(ReviewResponse.Good, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates), + osrSchedule( + ReviewResponse.Good, + 1, + DEFAULT_SETTINGS.baseEase, + 0, + DEFAULT_SETTINGS, + dueDates, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, - interval: 3, - }); - expect(dueDates).toEqual({ - 0: 1, - 1: 1, - 2: 1, - 3: 5, + interval: newInterval, }); + dueDates.decrement(originalInterval); + dueDates.increment(newInterval); + expect(dueDates).toEqual( + new DueDateHistogram({ + 0: 1, + 1: 0, // One less than before + 2: 1, + 3: 5, // One more than before + }), + ); }); test("Test load balancing", () => { // interval < 7 - let dueDates: Record = { + let dueDates = new DueDateHistogram({ 5: 2, - }; + }); expect( - schedule(ReviewResponse.Good, 2, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates), + osrSchedule( + ReviewResponse.Good, + 2, + DEFAULT_SETTINGS.baseEase, + 0, + DEFAULT_SETTINGS, + dueDates, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 4, }); - expect(dueDates).toEqual({ - 4: 1, - 5: 2, - }); // 7 <= interval < 30 - dueDates = { + dueDates = new DueDateHistogram({ 25: 2, - }; + }); expect( - schedule(ReviewResponse.Good, 10, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates), + osrSchedule( + ReviewResponse.Good, + 10, + DEFAULT_SETTINGS.baseEase, + 0, + DEFAULT_SETTINGS, + dueDates, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 24, }); - expect(dueDates).toEqual({ - 24: 1, - 25: 2, - }); // interval >= 30 - dueDates = { + dueDates = new DueDateHistogram({ 2: 5, 59: 8, 60: 9, @@ -112,25 +177,20 @@ test("Test load balancing", () => { 65: 8, 66: 2, 67: 10, - }; + }); expect( - schedule(ReviewResponse.Good, 25, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates), + osrSchedule( + ReviewResponse.Good, + 25, + DEFAULT_SETTINGS.baseEase, + 0, + DEFAULT_SETTINGS, + dueDates, + ), ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 66, }); - expect(dueDates).toEqual({ - 2: 5, - 59: 8, - 60: 9, - 61: 3, - 62: 5, - 63: 4, - 64: 4, - 65: 8, - 66: 3, - 67: 10, - }); }); test("Test textInterval - desktop", () => { diff --git a/tests/unit/stats.test.ts b/tests/unit/stats.test.ts new file mode 100644 index 00000000..05251bab --- /dev/null +++ b/tests/unit/stats.test.ts @@ -0,0 +1,48 @@ +import { Stats } from "src/stats"; + +describe("Stats", () => { + let stats: Stats; + + beforeEach(() => { + stats = new Stats(); + }); + + test("incrementNew should increment newCount", () => { + stats.incrementNew(); + expect(stats.newCount).toBe(1); + }); + + test("update should update stats correctly", () => { + stats.update(10, 20, 3); // DelayedDays: 10, Interval: 20, Ease: 3 + expect(stats.delayedDays.dict).toEqual({ 10: 1 }); + expect(stats.intervals.dict).toEqual({ 20: 1 }); + expect(stats.eases.dict).toEqual({ 3: 1 }); + expect(stats.youngCount).toBe(1); + expect(stats.matureCount).toBe(0); + }); + + test("getMaxInterval should return the maximum interval", () => { + stats.intervals.dict = { 10: 2, 20: 1, 30: 5 }; + expect(stats.getMaxInterval()).toBe(30); + }); + + test("getAverageInterval should return the average interval", () => { + stats.intervals.dict = { 10: 2, 20: 1, 30: 5 }; + stats.youngCount = 3; + stats.matureCount = 2; + expect(stats.getAverageInterval()).toBe((10 * 2 + 20 * 1 + 30 * 5) / 5); + }); + + test("getAverageEases should return the average ease", () => { + stats.eases.dict = { 1: 2, 2: 1, 3: 5 }; + stats.youngCount = 3; + stats.matureCount = 2; + expect(stats.getAverageEases()).toBe((1 * 2 + 2 * 1 + 3 * 5) / 5); + }); + + test("totalCount should return the sum of youngCount and matureCount", () => { + stats.youngCount = 3; + stats.matureCount = 2; + expect(stats.totalCount).toBe(5); + }); +}); diff --git a/tests/unit/TopicPath.test.ts b/tests/unit/topic-path.test.ts similarity index 75% rename from tests/unit/TopicPath.test.ts rename to tests/unit/topic-path.test.ts index af17047c..b18a5659 100644 --- a/tests/unit/TopicPath.test.ts +++ b/tests/unit/topic-path.test.ts @@ -1,24 +1,21 @@ -import { ISRFile } from "src/SRFile"; -import { TopicPath, TopicPathList } from "src/TopicPath"; -import { DEFAULT_SETTINGS, SRSettings } from "src/settings"; -import { UnitTestSRFile } from "./helpers/UnitTestSRFile"; +import { TopicPath } from "src/topic-path"; describe("Constructor exception handling", () => { test("Constructor rejects null path", () => { const t = () => { - let path: TopicPath = new TopicPath(null); + new TopicPath(null); }; expect(t).toThrow(); }); test("Constructor allows zero length array", () => { - let path: TopicPath = new TopicPath([]); + const path: TopicPath = new TopicPath([]); expect(path.hasPath).toEqual(false); }); test("Constructor rejects path that includes '/'", () => { const t = () => { - let path: TopicPath = new TopicPath(["Hello/Goodbye"]); + new TopicPath(["Hello/Goodbye"]); }; expect(t).toThrow(); }); @@ -26,24 +23,24 @@ describe("Constructor exception handling", () => { describe("shift", () => { test("shift() on multi-part path", () => { - let path: TopicPath = new TopicPath(["Level1", "Level2", "Level3"]); - let result: string = path.shift(); + const path: TopicPath = new TopicPath(["Level1", "Level2", "Level3"]); + const result: string = path.shift(); expect(result).toEqual("Level1"); expect(path).toEqual(new TopicPath(["Level2", "Level3"])); }); test("shift() on single-part path", () => { - let path: TopicPath = new TopicPath(["Level1"]); - let result: string = path.shift(); + const path: TopicPath = new TopicPath(["Level1"]); + const result: string = path.shift(); expect(result).toEqual("Level1"); expect(path.hasPath).toEqual(false); }); test("shift() on empty path", () => { - let path: TopicPath = new TopicPath(["Level1"]); - let result: string = path.shift(); + const path: TopicPath = new TopicPath(["Level1"]); + path.shift(); const t = () => { path.shift(); @@ -54,15 +51,15 @@ describe("shift", () => { describe("getTopicPathFromCardText", () => { test("Card text doesn't include tag", () => { - let cardText: string = "Card text doesn't include tag"; - let path: TopicPath = TopicPath.getTopicPathFromCardText(cardText); + const cardText: string = "Card text doesn't include tag"; + const path: TopicPath = TopicPath.getTopicPathFromCardText(cardText); expect(path).toEqual(null); }); test("Card text includes single level tag", () => { - let cardText: string = "#flashcards Card text does include tag"; - let path: TopicPath = TopicPath.getTopicPathFromCardText(cardText); + const cardText: string = "#flashcards Card text does include tag"; + const path: TopicPath = TopicPath.getTopicPathFromCardText(cardText); expect(path).toEqual(new TopicPath(["flashcards"])); }); @@ -80,9 +77,9 @@ describe("getTopicPathFromCardText", () => { }); test("Card text includes 2 multi level tags", () => { - let cardText: string = + const cardText: string = "#flashcards/science/chemistry Card text includes multiple tag #flashcards/test/chemistry"; - let path: TopicPath = TopicPath.getTopicPathFromCardText(cardText); + const path: TopicPath = TopicPath.getTopicPathFromCardText(cardText); expect(path).toEqual(new TopicPath(["flashcards", "science", "chemistry"])); }); @@ -118,25 +115,25 @@ describe("getTopicPathFromTag", () => { }); test("Single level tag", () => { - let result: TopicPath = TopicPath.getTopicPathFromTag("#flashcard"); + const result: TopicPath = TopicPath.getTopicPathFromTag("#flashcard"); expect(result.path).toEqual(["flashcard"]); }); test("Multi level tag", () => { - let result: TopicPath = TopicPath.getTopicPathFromTag("#flashcard/science/physics"); + const result: TopicPath = TopicPath.getTopicPathFromTag("#flashcard/science/physics"); expect(result.path).toEqual(["flashcard", "science", "physics"]); }); test("Tag with trailing slash", () => { - let result: TopicPath = TopicPath.getTopicPathFromTag("#flashcard/science/physics/"); + const result: TopicPath = TopicPath.getTopicPathFromTag("#flashcard/science/physics/"); expect(result.path).toEqual(["flashcard", "science", "physics"]); }); test("Tag with multiple adjacent slashes", () => { - let result: TopicPath = TopicPath.getTopicPathFromTag("#flashcard///science//physics"); + const result: TopicPath = TopicPath.getTopicPathFromTag("#flashcard///science//physics"); expect(result.path).toEqual(["flashcard", "science", "physics"]); }); @@ -144,20 +141,20 @@ describe("getTopicPathFromTag", () => { describe("isSameOrAncestorOf", () => { test("a, b are both empty", () => { - let a: TopicPath = TopicPath.emptyPath; - let b: TopicPath = TopicPath.emptyPath; + const a: TopicPath = TopicPath.emptyPath; + const b: TopicPath = TopicPath.emptyPath; expect(a.isSameOrAncestorOf(b)).toEqual(true); }); test("a is empty, b has path", () => { - let a: TopicPath = TopicPath.emptyPath; - let b: TopicPath = new TopicPath(["flashcard"]); + const a: TopicPath = TopicPath.emptyPath; + const b: TopicPath = new TopicPath(["flashcard"]); expect(a.isSameOrAncestorOf(b)).toEqual(false); }); test("a has path, b is empty", () => { - let a: TopicPath = new TopicPath(["flashcard"]); - let b: TopicPath = TopicPath.emptyPath; + const a: TopicPath = new TopicPath(["flashcard"]); + const b: TopicPath = TopicPath.emptyPath; expect(a.isSameOrAncestorOf(b)).toEqual(false); }); @@ -196,8 +193,8 @@ describe("isSameOrAncestorOf", () => { describe("clone", () => { test("clone of empty", () => { - let a: TopicPath = TopicPath.emptyPath; - let b: TopicPath = a.clone(); + const a: TopicPath = TopicPath.emptyPath; + const b: TopicPath = a.clone(); expect(b.isEmptyPath).toEqual(true); }); @@ -214,14 +211,14 @@ describe("clone", () => { describe("formatTag", () => { test("Simple test", () => { - let topicPath: TopicPath = new TopicPath(["flashcards", "science"]); + const topicPath: TopicPath = new TopicPath(["flashcards", "science"]); expect(topicPath.formatAsTag()).toEqual("#flashcards/science"); }); test("Empty path", () => { const t = () => { - let str: string = TopicPath.emptyPath.formatAsTag(); + TopicPath.emptyPath.formatAsTag(); }; expect(t).toThrow(); }); diff --git a/tests/unit/util/MultiLineTextFinder.test.ts b/tests/unit/util/MultiLineTextFinder.test.ts deleted file mode 100644 index bec40d9f..00000000 --- a/tests/unit/util/MultiLineTextFinder.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { MultiLineTextFinder } from "src/util/MultiLineTextFinder"; -import { splitTextIntoLineArray } from "src/util/utils"; - -let space: string = " "; -let text10: string = `Some Stuff 0 More Stuff -Some Stuff 1 More Stuff -Some Stuff 2 More Stuff -Some Stuff 3 More Stuff -Some Stuff 4 More Stuff -Some Stuff 5 More Stuff -Some Stuff 6 More Stuff -Some Stuff 7 More Stuff -Some Stuff 8 More Stuff -Some Stuff 9 More Stuff`; -let text20: string = `Some Stuff 0 More Stuff -Some Stuff 1 More Stuff -Some Stuff 2 More Stuff -Some Stuff 3 More Stuff -Some Stuff 4 More Stuff -Some Stuff 5 More Stuff -Some Stuff 6 More Stuff -Some Stuff 7 More Stuff -Some Stuff 8 More Stuff -Some Stuff 9 More Stuff -Some Stuff 10 More Stuff -Some Stuff 11 More Stuff -Some Stuff 12 More Stuff -Some Stuff 13 More Stuff -Some Stuff 14 More Stuff -Some Stuff 15 More Stuff - Some Stuff 16 More Stuff -Some Stuff 17 More Stuff${space}${space} -Some Stuff 18 More Stuff -Some Stuff 19 More Stuff -Some Stuff 20 More Stuff -`; - -describe("find", () => { - describe("Single line search string - Match found", () => { - test("Search string present as complete line within text (identical)", () => { - let searchStr: string = "Some Stuff 14 More Stuff"; - - checkFindResult(text20, searchStr, 14); - }); - - test("Search string present as complete line within text (search has pre/post additional spaces)", () => { - let searchStr: string = " Some Stuff 14 More Stuff"; - checkFindResult(text20, searchStr, 14); - - searchStr = "Some Stuff 14 More Stuff "; - checkFindResult(text20, searchStr, 14); - - searchStr = " Some Stuff 14 More Stuff "; - checkFindResult(text20, searchStr, 14); - }); - - test("Search string present as complete line within text (source text has pre/post additional spaces)", () => { - let searchStr: string = "Some Stuff 16 More Stuff"; - checkFindResult(text20, searchStr, 16); - - searchStr = "Some Stuff 17 More Stuff"; - checkFindResult(text20, searchStr, 17); - }); - }); - - describe("Multi line search string - Match found", () => { - test("Search string present from line 1", () => { - let searchStr: string = `Some Stuff 1 More Stuff - Some Stuff 2 More Stuff - Some Stuff 3 More Stuff`; - - checkFindResult(text20, searchStr, 1); - }); - - test("Search string present mid file", () => { - let searchStr: string = `Some Stuff 9 More Stuff - Some Stuff 10 More Stuff - Some Stuff 11 More Stuff`; - checkFindResult(text20, searchStr, 9); - }); - - test("Search string present at end of file", () => { - let searchStr: string = `Some Stuff 19 More Stuff - Some Stuff 20 More Stuff`; - checkFindResult(text20, searchStr, 19); - }); - }); - - describe("Single line search string - No match found", () => { - test("Search string is a match but only to part of the line", () => { - let searchStr: string = "Stuff 14 More Stuff"; - - checkFindResult(text20, searchStr, null); - }); - }); - - describe("Multi line search string - No match found", () => { - test("Search string doesn't match any source line", () => { - let searchStr: string = `Nothing here that matches - Or hear `; - checkFindResult(text20, searchStr, null); - }); - - test("Some, but not all of the search string lines matches the source", () => { - let searchStr: string = `Some Stuff 9 More Stuff - Some Stuff 10 More Stuff - Some Stuff 11 More Stuff - this line doesn't match`; - checkFindResult(text20, searchStr, null); - }); - }); -}); - -describe("findAndReplace", () => { - test("Multi line search string present as exact match", () => { - let searchStr: string = `Some Stuff 4 More Stuff -Some Stuff 5 More Stuff -Some Stuff 6 More Stuff`; - - let replacementStr: string = `Replacement line`; - - let expectedResult: string = `Some Stuff 0 More Stuff -Some Stuff 1 More Stuff -Some Stuff 2 More Stuff -Some Stuff 3 More Stuff -Replacement line -Some Stuff 7 More Stuff -Some Stuff 8 More Stuff -Some Stuff 9 More Stuff`; - checkFindAndReplaceResult(text10, searchStr, replacementStr, expectedResult); - }); - - test("Multi line search string has pre/post spaces", () => { - let searchStr: string = `Some Stuff 4 More Stuff -${space}Some Stuff 5 More Stuff -Some Stuff 6 More Stuff${space}${space}`; - - let replacementStr: string = `Replacement line 1 -Replacement line 2`; - - let expectedResult: string = `Some Stuff 0 More Stuff -Some Stuff 1 More Stuff -Some Stuff 2 More Stuff -Some Stuff 3 More Stuff -Replacement line 1 -Replacement line 2 -Some Stuff 7 More Stuff -Some Stuff 8 More Stuff -Some Stuff 9 More Stuff`; - checkFindAndReplaceResult(text10, searchStr, replacementStr, expectedResult); - }); - - test("No match found", () => { - let searchStr: string = `Some Stuff 4 More Stuff -Some Stuff 5 More Stuff -Some Stuff 7 More Stuff`; - - let replacementStr: string = `Replacement line 1 -Replacement line 2`; - - let expectedResult: string = null; - checkFindAndReplaceResult(text10, searchStr, replacementStr, expectedResult); - }); -}); - -function checkFindAndReplaceResult( - text: string, - searchStr: string, - replacementStr: string, - expectedResult: string, -) { - let result: string = MultiLineTextFinder.findAndReplace(text, searchStr, replacementStr); - expect(result).toEqual(expectedResult); -} - -function checkFindResult(text: string, searchStr: string, expectedResult: number) { - let textArray = splitTextIntoLineArray(text); - let searchArray = splitTextIntoLineArray(searchStr); - let result: number = MultiLineTextFinder.find(textArray, searchArray); - expect(result).toEqual(expectedResult); -} diff --git a/tests/unit/util/utils.test.ts b/tests/unit/util/utils.test.ts deleted file mode 100644 index e5391edc..00000000 --- a/tests/unit/util/utils.test.ts +++ /dev/null @@ -1,549 +0,0 @@ -import { YAML_FRONT_MATTER_REGEX } from "src/constants"; -import { - convertToStringOrEmpty, - extractFrontmatter, - findLineIndexOfSearchStringIgnoringWs, - isEqualOrSubPath, - literalStringReplace, -} from "src/util/utils"; - -describe("literalStringReplace", () => { - test("Replacement string doesn't have any dollar signs", async () => { - const actual: string = literalStringReplace( - "Original string without dollar signs", - "dollar", - "pound", - ); - expect(actual).toEqual("Original string without pound signs"); - }); - - test("Replacement string has double dollar signs", async () => { - const actual: string = literalStringReplace( - "Original string without dollar signs", - "dollar", - "$", - ); - expect(actual).toEqual("Original string without $ signs"); - }); - - test("Original and search strings has double dollar signs at the end", async () => { - const originalStr: string = `Some stuff at the start - -Something -? -$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`; - - const searchStr: string = `Something -? -$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`; - - const replacementStr: string = `Something -? -$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$ -`; - - const expectedStr: string = `Some stuff at the start - -Something -? -$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$ -`; - - const actual: string = literalStringReplace(originalStr, searchStr, replacementStr); - expect(actual).toEqual(expectedStr); - }); - - test("Original and search strings has double dollar signs at the end", async () => { - const originalStr: string = `Some stuff at the start $$`; - - const searchStr: string = `start $$`; - - const replacementStr: string = `start $$ and end`; - - const expectedStr: string = `Some stuff at the start $$ and end`; - - const actual: string = literalStringReplace(originalStr, searchStr, replacementStr); - expect(actual).toEqual(expectedStr); - }); - - test("Search string not found", async () => { - const originalStr: string = "A very boring string"; - - const searchStr: string = "missing"; - - const replacementStr: string = "replacement"; - - const expectedStr: string = originalStr; - - const actual: string = literalStringReplace(originalStr, searchStr, replacementStr); - expect(actual).toEqual(expectedStr); - }); -}); - -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}---`; -} - -describe("YAML_FRONT_MATTER_REGEX", () => { - test("New line is line feed", async () => { - const sep: string = String.fromCharCode(10); - const text: string = createTestStr1(sep); - expect(YAML_FRONT_MATTER_REGEX.test(text)).toEqual(true); - }); - - test("New line is carriage return line feed", async () => { - const sep: string = String.fromCharCode(13, 10); - const text: string = createTestStr1(sep); - expect(YAML_FRONT_MATTER_REGEX.test(text)).toEqual(true); - }); -}); - -describe("extractFrontmatter", () => { - test("No frontmatter", () => { - let text: string = `Hello -Goodbye`; - let frontmatter: string; - let content: string; - [frontmatter, content] = extractFrontmatter(text); - expect(frontmatter).toEqual(""); - expect(content).toEqual(text); - - text = `--- -Goodbye`; - [frontmatter, content] = extractFrontmatter(text); - expect(frontmatter).toEqual(""); - expect(content).toEqual(text); - }); - - test("With frontmatter (and nothing else)", () => { - let frontmatter: string = `--- -sr-due: 2024-01-17 -sr-interval: 16 -sr-ease: 278 -tags: - - flashcards/aws - - flashcards/datascience ----`; - const text: string = frontmatter; - let content: string; - [frontmatter, content] = extractFrontmatter(text); - expect(frontmatter).toEqual(text); - const frontmatterBlankedOut: string = ` - - - - - - -`; - expect(content).toEqual(frontmatterBlankedOut); - }); - - test("With frontmatter (and content)", () => { - let frontmatter: string = `--- -sr-due: 2024-01-17 -sr-interval: 16 -sr-ease: 278 -tags: - - flashcards/aws - - flashcards/datascience ----`; - const content: string = `#flashcards/science/chemistry - -# Questions - -Chemistry Question from file underelephant 4A::goodby - -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 [f, c] = extractFrontmatter(text); - expect(f).toEqual(frontmatter); - const frontmatterBlankedOut: string = ` - - - - - - -`; - const expectedContent: string = `${frontmatterBlankedOut} -${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] = extractFrontmatter(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] = extractFrontmatter(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] = extractFrontmatter(text); - expect(f).toEqual(frontmatter); - expect(c).toEqual(expectedContent); - }); -}); - -describe("findLineIndexOfSearchStringIgnoringWs", () => { - const space: string = " "; - test("Search string not present", () => { - const lines: string[] = [ - "A very boring multi-line question.", - "(With this extra info, not so boring after all)", - "?", - "A very boring multi-line answer.", - "(With this extra info, not so boring after all)", - ]; - expect(findLineIndexOfSearchStringIgnoringWs(lines, "??")).toEqual(-1); - }); - - test("Search string present, but only on a line with other text", () => { - const lines: string[] = [ - "What do you think of this multi-line question?", - "??", - "A very boring multi-line answer.", - "(With this extra info, not so boring after all)", - ]; - expect(findLineIndexOfSearchStringIgnoringWs(lines, "?")).toEqual(-1); - }); - - test("Search string found at start of text (exactly)", () => { - const lines: string[] = [ - "?", - "A very boring multi-line answer.", - "(With this extra info, not so boring after all)", - ]; - expect(findLineIndexOfSearchStringIgnoringWs(lines, "?")).toEqual(0); - }); - - test("Search line found at start of text (text has whitespace)", () => { - const lines: string[] = [ - `${space}?${space}`, - "A very boring multi-line answer.", - "(With this extra info, not so boring after all)", - ]; - expect(findLineIndexOfSearchStringIgnoringWs(lines, "?")).toEqual(0); - }); - - test("Search line found in middle line of text (exactly)", () => { - const lines: string[] = [ - "What do you think of this multi-line question?", - "(With this extra info, not so boring after all)", - "??", - "A very boring multi-line answer.", - "(With this extra info, not so boring after all)", - ]; - expect(findLineIndexOfSearchStringIgnoringWs(lines, "??")).toEqual(2); - }); - - test("Search line found in middle line of text (text has whitespace)", () => { - const lines: string[] = [ - "What do you think of this multi-line question?", - "(With this extra info, not so boring after all)", - `${space}??`, - "A very boring multi-line answer.", - "(With this extra info, not so boring after all)", - ]; - - 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, - ); - }); - }); -}); diff --git a/tests/unit/utils/dates.test.ts b/tests/unit/utils/dates.test.ts new file mode 100644 index 00000000..2f2b6645 --- /dev/null +++ b/tests/unit/utils/dates.test.ts @@ -0,0 +1,13 @@ +import { formatDate } from "src/utils/dates"; + +describe("Format date", () => { + test("Different input overloads", () => { + expect(formatDate(new Date(2023, 0, 1))).toBe("2023-01-01"); + expect(formatDate(2023, 1, 1)).toBe("2023-01-01"); + expect(formatDate(1672531200000)).toBe("2023-01-01"); + }); + + test("handles a leap year date", () => { + expect(formatDate(2020, 2, 29)).toBe("2020-02-29"); + }); +}); diff --git a/tests/unit/utils/fs.test.ts b/tests/unit/utils/fs.test.ts new file mode 100644 index 00000000..f33fe64c --- /dev/null +++ b/tests/unit/utils/fs.test.ts @@ -0,0 +1,36 @@ +import { pathMatchesPattern } from "src/utils/fs"; + +describe("pathMatchesPattern", () => { + test("Paths that match", () => { + expect(pathMatchesPattern("Computing/AWS/DynamoDB/Streams.md", "Computing/AWS")).toBe(true); + expect(pathMatchesPattern("Computing/AWS/DynamoDB/Streams.md", "Computing/AWS/")).toBe( + true, + ); + expect( + pathMatchesPattern("Computing/GCP/DynamoDB/Streams.md", "Computing/*/DynamoDB/*"), + ).toBe(true); + expect(pathMatchesPattern("Computing/AWS/DynamoDB/Streams.md", "Computing/**")).toBe(true); + expect( + pathMatchesPattern("Computing/AWS/DynamoDB/Streams.md", "Computing/AWS/DynamoDB/*"), + ).toBe(true); + + expect(pathMatchesPattern("Computing/AWS/foo.excalidraw.md", "**/*.excalidraw.md")).toBe( + true, + ); + expect( + pathMatchesPattern( + "Computing/Drawing 2024-09-22 15.12.39.excalidraw.md", + "*/*.excalidraw.md", + ), + ).toBe(true); + }); + + test("Paths that don't match", () => { + expect(pathMatchesPattern("Math/Singular Matrix.md", "Computing/AWS")).toBe(false); + expect(pathMatchesPattern("AWS/DynamoDB/Streams.md", "Computing/*/DynamoDB/")).toBe(false); + + expect(pathMatchesPattern("Computing/AWS/DynamoDB/Streams.md", "**/*.excalidraw.md")).toBe( + false, + ); + }); +}); diff --git a/tests/unit/util/RandomNumberProvider.test.ts b/tests/unit/utils/numbers.test.ts similarity index 62% rename from tests/unit/util/RandomNumberProvider.test.ts rename to tests/unit/utils/numbers.test.ts index de9478be..386243ad 100644 --- a/tests/unit/util/RandomNumberProvider.test.ts +++ b/tests/unit/utils/numbers.test.ts @@ -1,9 +1,10 @@ import { IStaticRandom, - WeightedRandomNumber, setupNextRandomNumber, setupStaticRandomNumberProvider, -} from "src/util/RandomNumberProvider"; + ValueCountDict, + WeightedRandomNumber, +} from "src/utils/numbers"; let provider: WeightedRandomNumber; @@ -12,6 +13,46 @@ beforeAll(() => { provider = WeightedRandomNumber.create(); }); +describe("ValueCountDict", () => { + let valueCountDict: ValueCountDict; + + beforeEach(() => { + valueCountDict = new ValueCountDict(); + }); + + test("incrementCount should increment count of a value", () => { + valueCountDict.incrementCount(5); + valueCountDict.incrementCount(5); + valueCountDict.incrementCount(10); + expect(valueCountDict.dict).toEqual({ 5: 2, 10: 1 }); + }); + + test("getMaxValue should return the maximum value in the dictionary", () => { + valueCountDict.dict = { 5: 2, 10: 1, 3: 5 }; + expect(valueCountDict.getMaxValue()).toBe(10); + }); + + test("getTotalOfValueMultiplyCount should return the sum of value * count", () => { + valueCountDict.dict = { 5: 2, 10: 1, 3: 5 }; + expect(valueCountDict.getTotalOfValueMultiplyCount()).toBe(5 * 2 + 10 * 1 + 3 * 5); + }); + + test("clearCountIfMissing should set count to 0 if missing", () => { + valueCountDict.clearCountIfMissing(5); + expect(valueCountDict.dict[5]).toBe(0); + }); + + test("hasValue should return true if value exists", () => { + valueCountDict.dict = { 5: 2, 10: 1, 3: 5 }; + expect(valueCountDict.hasValue(10)).toBe(true); + }); + + test("hasValue should return false if value does not exist", () => { + valueCountDict.dict = { 5: 2, 10: 1, 3: 5 }; + expect(valueCountDict.hasValue(7)).toBe(false); + }); +}); + describe("WeightedRandomNumber", () => { test("Single weight", () => { const weights: Record = { diff --git a/tests/unit/utils/strings.test.ts b/tests/unit/utils/strings.test.ts new file mode 100644 index 00000000..4d8bb3b5 --- /dev/null +++ b/tests/unit/utils/strings.test.ts @@ -0,0 +1,741 @@ +import { + convertToStringOrEmpty, + cyrb53, + escapeRegexString, + findLineIndexOfSearchStringIgnoringWs, + literalStringReplace, + MultiLineTextFinder, + splitNoteIntoFrontmatterAndContent, + splitTextIntoLineArray, + stringTrimStart, +} from "src/utils/strings"; + +describe("escapeRegexString", () => { + test("should escape special regex characters", () => { + const input = ".*+?^${}()|[]\\"; + const expected = "\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\"; + expect(escapeRegexString(input)).toBe(expected); + }); + + test("should handle a string without special characters", () => { + const input = "abc123"; + const expected = "abc123"; + expect(escapeRegexString(input)).toBe(expected); + }); + + test("should handle an empty string", () => { + const input = ""; + const expected = ""; + expect(escapeRegexString(input)).toBe(expected); + }); + + test("should handle a mixed string with special and normal characters", () => { + const input = "Hello.*+?^${}()|[]\\World"; + const expected = "Hello\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\World"; + expect(escapeRegexString(input)).toBe(expected); + }); +}); + +describe("literalStringReplace", () => { + test("Replacement string doesn't have any dollar signs", async () => { + const actual: string = literalStringReplace( + "Original string without dollar signs", + "dollar", + "pound", + ); + expect(actual).toEqual("Original string without pound signs"); + }); + + test("Replacement string has double dollar signs", async () => { + const actual: string = literalStringReplace( + "Original string without dollar signs", + "dollar", + "$", + ); + expect(actual).toEqual("Original string without $ signs"); + }); + + test("Original and search strings has double dollar signs at the end", async () => { + const originalStr: string = `Some stuff at the start + +Something +? +$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`; + + const searchStr: string = `Something +? +$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$`; + + const replacementStr: string = `Something +? +$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$ +`; + + const expectedStr: string = `Some stuff at the start + +Something +? +$$\\huge F_g=\\frac {G m_1 m_2}{d^2}$$ +`; + + const actual: string = literalStringReplace(originalStr, searchStr, replacementStr); + expect(actual).toEqual(expectedStr); + }); + + test("Original and search strings has double dollar signs at the end", async () => { + const originalStr: string = "Some stuff at the start $$"; + + const searchStr: string = "start $$"; + + const replacementStr: string = "start $$ and end"; + + const expectedStr: string = "Some stuff at the start $$ and end"; + + const actual: string = literalStringReplace(originalStr, searchStr, replacementStr); + expect(actual).toEqual(expectedStr); + }); + + test("Search string not found", async () => { + const originalStr: string = "A very boring string"; + + const searchStr: string = "missing"; + + const replacementStr: string = "replacement"; + + const expectedStr: string = originalStr; + + const actual: string = literalStringReplace(originalStr, searchStr, replacementStr); + expect(actual).toEqual(expectedStr); + }); +}); + +describe("cyrb53", () => { + test("should generate hash for a simple string without seed", () => { + const input = "hello"; + const expectedHash = "106f3a63cd7226"; + expect(cyrb53(input)).toBe(expectedHash); + }); + + test("should generate hash for a simple string with seed", () => { + const input = "hello"; + const seed = 123; + const expectedHash = "6677dacb8051"; + expect(cyrb53(input, seed)).toBe(expectedHash); + }); + + test("should generate hash for an empty string without seed", () => { + const input = ""; + const expectedHash = "bdcb81aee8d83"; + expect(cyrb53(input)).toBe(expectedHash); + }); + + test("should generate hash for an empty string with seed", () => { + const input = ""; + const seed = 987; + const expectedHash = "aba1aab9aab71"; + expect(cyrb53(input, seed)).toBe(expectedHash); + }); + + test("should generate hash for a string with special characters without seed", () => { + const input = "!@#$%^&*()"; + const expectedHash = "d86f2f9eb5a3a"; + expect(cyrb53(input)).toBe(expectedHash); + }); + + test("should generate hash for a string with special characters with seed", () => { + const input = "!@#$%^&*()"; + const seed = 555; + const expectedHash = "1484280e499f6c"; + expect(cyrb53(input, seed)).toBe(expectedHash); + }); +}); + +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"); + }); +}); + +describe("Split Text to array of lines", () => { + const textCR = "Line 1\rLine 2\rLine 3\rLine 4\rLine 5"; + const textLF = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"; + const textCRLF = "Line 1\r\nLine 2\r\nLine 3\r\nLine 4\r\nLine 5"; + const lines = ["Line 1", "Line 2", "Line 3", "Line 4", "Line 5"]; + + test("Line Feed", () => { + expect(splitTextIntoLineArray(textLF)).toStrictEqual(lines); + }); + + test("Carriage Return", () => { + expect(splitTextIntoLineArray(textCR)).toStrictEqual(lines); + }); + + test("Carriage Return + Line Feed", () => { + expect(splitTextIntoLineArray(textCRLF)).toStrictEqual(lines); + }); +}); + +describe("stringTrimStart", () => { + test("Empty string", () => { + expect(stringTrimStart("")).toEqual(["", ""]); + expect(stringTrimStart(undefined)).toEqual(["", ""]); + }); + + test("No white spaces", () => { + expect(stringTrimStart("any text here")).toEqual(["", "any text here"]); + }); + + test("Only white spaces", () => { + expect(stringTrimStart(" ")).toEqual([" ", ""]); + expect(stringTrimStart(" ")).toEqual([" ", ""]); + }); + + test("Leading white spaces", () => { + expect(stringTrimStart(" any text here")).toEqual([" ", "any text here"]); + expect(stringTrimStart(" any text here")).toEqual([" ", "any text here"]); + }); + + test("Trailing white spaces", () => { + expect(stringTrimStart("any text here ")).toEqual(["", "any text here "]); + expect(stringTrimStart("any text here ")).toEqual(["", "any text here "]); + }); + + test("Leading tabs", () => { + expect(stringTrimStart("\tany text here")).toEqual(["\t", "any text here"]); + expect(stringTrimStart("\t\tany text here")).toEqual(["\t\t", "any text here"]); + }); + + test("Trailing tabs", () => { + expect(stringTrimStart("any text here\t")).toEqual(["", "any text here\t"]); + expect(stringTrimStart("any text here\t\t")).toEqual(["", "any text here\t\t"]); + }); + + test("Mixed leading whitespace (spaces and tabs)", () => { + expect(stringTrimStart(" \tany text here")).toEqual([" \t", "any text here"]); + expect(stringTrimStart("\t any text here")).toEqual(["\t ", "any text here"]); + expect(stringTrimStart(" \t any text here")).toEqual([" \t ", "any text here"]); + }); + + test("Mixed trailing whitespace (spaces and tabs)", () => { + expect(stringTrimStart("any text here \t")).toEqual(["", "any text here \t"]); + expect(stringTrimStart("any text here\t ")).toEqual(["", "any text here\t "]); + expect(stringTrimStart("any text here \t ")).toEqual(["", "any text here \t "]); + }); + + test("Newlines and leading spaces", () => { + expect(stringTrimStart("\nany text here")).toEqual(["\n", "any text here"]); + expect(stringTrimStart("\n any text here")).toEqual(["\n ", "any text here"]); + expect(stringTrimStart(" \nany text here")).toEqual([" \n", "any text here"]); + expect(stringTrimStart(" \n any text here")).toEqual([" \n ", "any text here"]); + }); +}); + +describe("splitNoteIntoFrontmatterAndContent", () => { + test("No frontmatter", () => { + let text: string = `Hello +Goodbye`; + let frontmatter: string; + let content: string; + [frontmatter, content] = splitNoteIntoFrontmatterAndContent(text); + expect(frontmatter).toEqual(""); + expect(content).toEqual(text); + + text = `--- +Goodbye`; + [frontmatter, content] = splitNoteIntoFrontmatterAndContent(text); + expect(frontmatter).toEqual(""); + expect(content).toEqual(text); + }); + + test("With frontmatter (and nothing else)", () => { + const expected: string = `--- +sr-due: 2024-01-17 +sr-interval: 16 +sr-ease: 278 +tags: + - flashcards/aws + - flashcards/datascience +---`; + const [frontmatter, content] = splitNoteIntoFrontmatterAndContent(expected); + expect(frontmatter).toEqual(expected); + const frontmatterBlankedOut: string = ` + + + + + + +`; + expect(content).toEqual(frontmatterBlankedOut); + }); + + test("With frontmatter (and content)", () => { + const frontmatter: string = `--- +sr-due: 2024-01-17 +sr-interval: 16 +sr-ease: 278 +tags: + - flashcards/aws + - flashcards/datascience +---`; + const content: string = `#flashcards/science/chemistry + +# Questions + +Chemistry Question from file underelephant 4A::goodby + +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 [f, c] = splitNoteIntoFrontmatterAndContent(text); + expect(f).toEqual(frontmatter); + const frontmatterBlankedOut: string = ` + + + + + + +`; + const expectedContent: string = `${frontmatterBlankedOut} +${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", () => { + const space: string = " "; + test("Search string not present", () => { + const lines: string[] = [ + "A very boring multi-line question.", + "(With this extra info, not so boring after all)", + "?", + "A very boring multi-line answer.", + "(With this extra info, not so boring after all)", + ]; + expect(findLineIndexOfSearchStringIgnoringWs(lines, "??")).toEqual(-1); + }); + + test("Search string present, but only on a line with other text", () => { + const lines: string[] = [ + "What do you think of this multi-line question?", + "??", + "A very boring multi-line answer.", + "(With this extra info, not so boring after all)", + ]; + expect(findLineIndexOfSearchStringIgnoringWs(lines, "?")).toEqual(-1); + }); + + test("Search string found at start of text (exactly)", () => { + const lines: string[] = [ + "?", + "A very boring multi-line answer.", + "(With this extra info, not so boring after all)", + ]; + expect(findLineIndexOfSearchStringIgnoringWs(lines, "?")).toEqual(0); + }); + + test("Search line found at start of text (text has whitespace)", () => { + const lines: string[] = [ + `${space}?${space}`, + "A very boring multi-line answer.", + "(With this extra info, not so boring after all)", + ]; + expect(findLineIndexOfSearchStringIgnoringWs(lines, "?")).toEqual(0); + }); + + test("Search line found in middle line of text (exactly)", () => { + const lines: string[] = [ + "What do you think of this multi-line question?", + "(With this extra info, not so boring after all)", + "??", + "A very boring multi-line answer.", + "(With this extra info, not so boring after all)", + ]; + expect(findLineIndexOfSearchStringIgnoringWs(lines, "??")).toEqual(2); + }); + + test("Search line found in middle line of text (text has whitespace)", () => { + const lines: string[] = [ + "What do you think of this multi-line question?", + "(With this extra info, not so boring after all)", + `${space}??`, + "A very boring multi-line answer.", + "(With this extra info, not so boring after all)", + ]; + + expect(findLineIndexOfSearchStringIgnoringWs(lines, "??")).toEqual(2); + }); +}); + +const space: string = " "; +const text10: string = `Some Stuff 0 More Stuff +Some Stuff 1 More Stuff +Some Stuff 2 More Stuff +Some Stuff 3 More Stuff +Some Stuff 4 More Stuff +Some Stuff 5 More Stuff +Some Stuff 6 More Stuff +Some Stuff 7 More Stuff +Some Stuff 8 More Stuff +Some Stuff 9 More Stuff`; +const text20: string = `Some Stuff 0 More Stuff +Some Stuff 1 More Stuff +Some Stuff 2 More Stuff +Some Stuff 3 More Stuff +Some Stuff 4 More Stuff +Some Stuff 5 More Stuff +Some Stuff 6 More Stuff +Some Stuff 7 More Stuff +Some Stuff 8 More Stuff +Some Stuff 9 More Stuff +Some Stuff 10 More Stuff +Some Stuff 11 More Stuff +Some Stuff 12 More Stuff +Some Stuff 13 More Stuff +Some Stuff 14 More Stuff +Some Stuff 15 More Stuff + Some Stuff 16 More Stuff +Some Stuff 17 More Stuff${space}${space} +Some Stuff 18 More Stuff +Some Stuff 19 More Stuff +Some Stuff 20 More Stuff +`; + +describe("find", () => { + describe("Single line search string - Match found", () => { + test("Search string present as complete line within text (identical)", () => { + const searchStr: string = "Some Stuff 14 More Stuff"; + + checkFindResult(text20, searchStr, 14); + }); + + test("Search string present as complete line within text (search has pre/post additional spaces)", () => { + let searchStr: string = " Some Stuff 14 More Stuff"; + checkFindResult(text20, searchStr, 14); + + searchStr = "Some Stuff 14 More Stuff "; + checkFindResult(text20, searchStr, 14); + + searchStr = " Some Stuff 14 More Stuff "; + checkFindResult(text20, searchStr, 14); + }); + + test("Search string present as complete line within text (source text has pre/post additional spaces)", () => { + let searchStr: string = "Some Stuff 16 More Stuff"; + checkFindResult(text20, searchStr, 16); + + searchStr = "Some Stuff 17 More Stuff"; + checkFindResult(text20, searchStr, 17); + }); + }); + + describe("Multi line search string - Match found", () => { + test("Search string present from line 1", () => { + const searchStr: string = `Some Stuff 1 More Stuff + Some Stuff 2 More Stuff + Some Stuff 3 More Stuff`; + + checkFindResult(text20, searchStr, 1); + }); + + test("Search string present mid file", () => { + const searchStr: string = `Some Stuff 9 More Stuff + Some Stuff 10 More Stuff + Some Stuff 11 More Stuff`; + checkFindResult(text20, searchStr, 9); + }); + + test("Search string present at end of file", () => { + const searchStr: string = `Some Stuff 19 More Stuff + Some Stuff 20 More Stuff`; + checkFindResult(text20, searchStr, 19); + }); + }); + + describe("Single line search string - No match found", () => { + test("Search string is a match but only to part of the line", () => { + const searchStr: string = "Stuff 14 More Stuff"; + + checkFindResult(text20, searchStr, null); + }); + }); + + describe("Multi line search string - No match found", () => { + test("Search string doesn't match any source line", () => { + const searchStr: string = `Nothing here that matches + Or hear `; + checkFindResult(text20, searchStr, null); + }); + + test("Some, but not all of the search string lines matches the source", () => { + const searchStr: string = `Some Stuff 9 More Stuff + Some Stuff 10 More Stuff + Some Stuff 11 More Stuff - this line doesn't match`; + checkFindResult(text20, searchStr, null); + }); + }); +}); + +describe("findAndReplace", () => { + test("Multi line search string present as exact match", () => { + const searchStr: string = `Some Stuff 4 More Stuff +Some Stuff 5 More Stuff +Some Stuff 6 More Stuff`; + + const replacementStr: string = "Replacement line"; + + const expectedResult: string = `Some Stuff 0 More Stuff +Some Stuff 1 More Stuff +Some Stuff 2 More Stuff +Some Stuff 3 More Stuff +Replacement line +Some Stuff 7 More Stuff +Some Stuff 8 More Stuff +Some Stuff 9 More Stuff`; + checkFindAndReplaceResult(text10, searchStr, replacementStr, expectedResult); + }); + + test("Multi line search string has pre/post spaces", () => { + const searchStr: string = `Some Stuff 4 More Stuff +${space}Some Stuff 5 More Stuff +Some Stuff 6 More Stuff${space}${space}`; + + const replacementStr: string = `Replacement line 1 +Replacement line 2`; + + const expectedResult: string = `Some Stuff 0 More Stuff +Some Stuff 1 More Stuff +Some Stuff 2 More Stuff +Some Stuff 3 More Stuff +Replacement line 1 +Replacement line 2 +Some Stuff 7 More Stuff +Some Stuff 8 More Stuff +Some Stuff 9 More Stuff`; + checkFindAndReplaceResult(text10, searchStr, replacementStr, expectedResult); + }); + + test("No match found", () => { + const searchStr: string = `Some Stuff 4 More Stuff +Some Stuff 5 More Stuff +Some Stuff 7 More Stuff`; + + const replacementStr: string = `Replacement line 1 +Replacement line 2`; + + const expectedResult: string = null; + checkFindAndReplaceResult(text10, searchStr, replacementStr, expectedResult); + }); +}); + +function checkFindAndReplaceResult( + text: string, + searchStr: string, + replacementStr: string, + expectedResult: string, +) { + const result: string = MultiLineTextFinder.findAndReplace(text, searchStr, replacementStr); + expect(result).toEqual(expectedResult); +} + +function checkFindResult(text: string, searchStr: string, expectedResult: number) { + const textArray = splitTextIntoLineArray(text); + const searchArray = splitTextIntoLineArray(searchStr); + const result: number = MultiLineTextFinder.find(textArray, searchArray); + expect(result).toEqual(expectedResult); +} diff --git a/tests/unit/utils/types.test.ts b/tests/unit/utils/types.test.ts new file mode 100644 index 00000000..3a9e34df --- /dev/null +++ b/tests/unit/utils/types.test.ts @@ -0,0 +1,73 @@ +import { getKeysPreserveType, getTypedObjectEntries } from "src/utils/types"; + +describe("getTypedObjectEntries", () => { + test("should handle basic object", () => { + expect(getTypedObjectEntries({ name: "Alice", age: 30, isStudent: false })).toEqual([ + ["name", "Alice"], + ["age", 30], + ["isStudent", false], + ]); + }); + + test("should handle empty object", () => { + expect(getTypedObjectEntries({})).toEqual([]); + }); + + test("should handle object with different value types", () => { + expect( + getTypedObjectEntries({ + a: 1, + b: "string", + c: true, + d: null, + e: undefined, + }), + ).toEqual([ + ["a", 1], + ["b", "string"], + ["c", true], + ["d", null], + ["e", undefined], + ]); + }); + + test("should handle object with nested objects", () => { + expect(getTypedObjectEntries({ obj: { nestedKey: "nestedValue" } })).toEqual([ + ["obj", { nestedKey: "nestedValue" }], + ]); + }); + + test("should handle object with array values", () => { + expect(getTypedObjectEntries({ arr: [1, 2, 3] })).toEqual([["arr", [1, 2, 3]]]); + }); + + test("should handle object with function values", () => { + const output = getTypedObjectEntries({ func: () => "result" }); + expect(output.length).toBe(1); + expect(output[0][0]).toBe("func"); + expect(typeof output[0][1]).toBe("function"); + expect(output[0][1]()).toBe("result"); + }); +}); + +describe("getKeysPreserveType", () => { + test("should return keys of a basic object", () => { + expect(getKeysPreserveType({ name: "Alice", age: 30, isStudent: false })).toEqual([ + "name", + "age", + "isStudent", + ]); + }); + + test("should return an empty array for an empty object", () => { + expect(getKeysPreserveType({})).toEqual([]); + }); + + test("should return keys of an object with different value types", () => { + expect(getKeysPreserveType({ a: 1, b: "string", c: true })).toEqual(["a", "b", "c"]); + }); + + test("should return keys of an object with a function value", () => { + expect(getKeysPreserveType({ func: () => "result" })).toEqual(["func"]); + }); +}); diff --git a/tests/e2e/settings.test.js b/tests/vaults/filesButNoQuestions/Newton thought that light was composed of particles.md similarity index 100% rename from tests/e2e/settings.test.js rename to tests/vaults/filesButNoQuestions/Newton thought that light was composed of particles.md diff --git a/tests/vaults/filesButNoQuestions/The nature of light.md b/tests/vaults/filesButNoQuestions/The nature of light.md new file mode 100644 index 00000000..72f3a338 --- /dev/null +++ b/tests/vaults/filesButNoQuestions/The nature of light.md @@ -0,0 +1,2 @@ +[[Scientist question things that I wouldn't]], For example [[The nature of light]]. +I don't think the question would ever occur to me… There is "something" at a distance from myself and I have information about its "state" in real time. How does that information get to me? diff --git a/tests/vaults/notes1/Computation Graph.md b/tests/vaults/notes1/Computation Graph.md new file mode 100644 index 00000000..7b1eeb44 --- /dev/null +++ b/tests/vaults/notes1/Computation Graph.md @@ -0,0 +1,10 @@ +#review + +https://www.coursera.org/learn/advanced-learning-algorithms/lecture/rhcTZ/computation-graph-optional +https://www.coursera.org/learn/advanced-learning-algorithms/lecture/qqczh/larger-neural-network-example-optional + +Computation graph for very simple [[Forward Propagation]] + +![[Pasted image 20230419183000.png]] + +Details about [[Backpropagation]] and algorithm efficiency included in the above Coursera links, but not fleshed out here. diff --git a/tests/vaults/notes2/Triboelectric Effect.md b/tests/vaults/notes2/Triboelectric Effect.md new file mode 100644 index 00000000..8475afcb --- /dev/null +++ b/tests/vaults/notes2/Triboelectric Effect.md @@ -0,0 +1,31 @@ +--- +sr-due: 2025-02-21 +sr-interval: 421 +sr-ease: 270 +--- + +#review + +The triboelectric effect describes electric charge transfer between two objects when they contact or slide against each other. + +It can occur with different materials, such as: + +- the sole of a shoe on a carpet +- balloon rubbing against sweater + +(also known as triboelectricity, triboelectric charging, triboelectrification, or tribocharging) + +# See Also + +[[Triboelectric Effect Examples]] +[[Triboelectric Series]] + +--- + +#flashcards/science/physics + +# Questions + +What is the phenomenon called when electric charge is transferred between two objects when they contact or slide against each other::Triboelectric effect + + diff --git a/tests/vaults/notes3/A.md b/tests/vaults/notes3/A.md new file mode 100644 index 00000000..ffd606c5 --- /dev/null +++ b/tests/vaults/notes3/A.md @@ -0,0 +1,3 @@ +#review + +Really worth reading [[B]], [[C]] and [[D]] diff --git a/tests/vaults/notes3/B.md b/tests/vaults/notes3/B.md new file mode 100644 index 00000000..53cac1c0 --- /dev/null +++ b/tests/vaults/notes3/B.md @@ -0,0 +1,10 @@ +#review + +Very interesting but doesn't reference any other notes + +# Frontmatter Determination + +- Initially no frontmatter +- OSR 1.10.0 +- this note reviewed as easy +- Plugin determined interval 4, ease 270 diff --git a/tests/vaults/notes3/C.md b/tests/vaults/notes3/C.md new file mode 100644 index 00000000..ef0410b4 --- /dev/null +++ b/tests/vaults/notes3/C.md @@ -0,0 +1,3 @@ +#review + +Definitely check out [[D]] diff --git a/tests/vaults/notes3/D.md b/tests/vaults/notes3/D.md new file mode 100644 index 00000000..9e472bae --- /dev/null +++ b/tests/vaults/notes3/D.md @@ -0,0 +1,5 @@ +#review + +I recently read very positive reviews of [[A]] and [[B]]. + +Even people on the bus was saying great things about [[B]] diff --git a/tests/vaults/notes4/A.md b/tests/vaults/notes4/A.md new file mode 100644 index 00000000..d9f6c3fe --- /dev/null +++ b/tests/vaults/notes4/A.md @@ -0,0 +1,16 @@ +--- +sr-due: 2023-09-10 +sr-interval: 4 +sr-ease: 270 +--- + +#review + +Really worth reading [[B]], [[C]] and [[D]] + +# Frontmatter Determination + +- Initially no frontmatter +- OSR 1.10.0 +- this note reviewed as easy +- Plugin determined interval 4, ease 270 diff --git a/tests/vaults/notes4/B.md b/tests/vaults/notes4/B.md new file mode 100644 index 00000000..b249b0ce --- /dev/null +++ b/tests/vaults/notes4/B.md @@ -0,0 +1,10 @@ +#review + +Very interesting but doesn't reference any other notes + +# Frontmatter Determination + +- Initially no frontmatter +- OSR 1.10.0 +- this note reviewed as easy +- Plugin determined interval 4, ease 272 (recognizing this has a link from A with an ease of 270) diff --git a/tests/vaults/notes4/C.md b/tests/vaults/notes4/C.md new file mode 100644 index 00000000..ef0410b4 --- /dev/null +++ b/tests/vaults/notes4/C.md @@ -0,0 +1,3 @@ +#review + +Definitely check out [[D]] diff --git a/tests/vaults/notes4/D.md b/tests/vaults/notes4/D.md new file mode 100644 index 00000000..3bced965 --- /dev/null +++ b/tests/vaults/notes4/D.md @@ -0,0 +1,5 @@ +#review + +I recently read very positive reviews of [[A]] and [[B]]. + +Even people on the bus were saying great things about [[B]] diff --git a/tests/vaults/notes4/notes4_readme.md b/tests/vaults/notes4/notes4_readme.md new file mode 100644 index 00000000..eeafd432 --- /dev/null +++ b/tests/vaults/notes4/notes4_readme.md @@ -0,0 +1,18 @@ +# "A.md" contains: + +- frontmatter (note review of EASY) +- A link to "B.md", C.md + +# "B.md" contains: + +- No frontmatter + +# "C.md" contains: + +- No link to "B.md" + +# "D.md" contains: + +- No frontmatter +- A link to "A.md" +- 2 links to "B.md" diff --git a/tests/vaults/notes5/A.md b/tests/vaults/notes5/A.md new file mode 100644 index 00000000..d9f6c3fe --- /dev/null +++ b/tests/vaults/notes5/A.md @@ -0,0 +1,16 @@ +--- +sr-due: 2023-09-10 +sr-interval: 4 +sr-ease: 270 +--- + +#review + +Really worth reading [[B]], [[C]] and [[D]] + +# Frontmatter Determination + +- Initially no frontmatter +- OSR 1.10.0 +- this note reviewed as easy +- Plugin determined interval 4, ease 270 diff --git a/tests/vaults/notes5/B.md b/tests/vaults/notes5/B.md new file mode 100644 index 00000000..b249b0ce --- /dev/null +++ b/tests/vaults/notes5/B.md @@ -0,0 +1,10 @@ +#review + +Very interesting but doesn't reference any other notes + +# Frontmatter Determination + +- Initially no frontmatter +- OSR 1.10.0 +- this note reviewed as easy +- Plugin determined interval 4, ease 272 (recognizing this has a link from A with an ease of 270) diff --git a/tests/vaults/notes5/C.md b/tests/vaults/notes5/C.md new file mode 100644 index 00000000..ef0410b4 --- /dev/null +++ b/tests/vaults/notes5/C.md @@ -0,0 +1,3 @@ +#review + +Definitely check out [[D]] diff --git a/tests/vaults/notes5/D.md b/tests/vaults/notes5/D.md new file mode 100644 index 00000000..b4d019b4 --- /dev/null +++ b/tests/vaults/notes5/D.md @@ -0,0 +1,10 @@ +#review + +I recently read very positive reviews of [[A]] and [[B]]. + +Even people on the bus were saying great things about [[B]] + +#flashcards + +This is question 1::This is answer 1 +This is question 2::This is answer 2 diff --git a/tests/vaults/notes5/notes5_readme.md b/tests/vaults/notes5/notes5_readme.md new file mode 100644 index 00000000..9dd18344 --- /dev/null +++ b/tests/vaults/notes5/notes5_readme.md @@ -0,0 +1,19 @@ +# "A.md" contains: + +- frontmatter (note review of EASY) +- A link to "B.md", C.md +- 3 questions already reviewed + +# "B.md" contains: + +- No frontmatter + +# "C.md" contains: + +- No link to "B.md" + +# "D.md" contains: + +- No frontmatter +- A link to "A.md" +- 2 links to "B.md" diff --git a/tests/vaults/notes6/A.md b/tests/vaults/notes6/A.md new file mode 100644 index 00000000..7f6959f2 --- /dev/null +++ b/tests/vaults/notes6/A.md @@ -0,0 +1,9 @@ +#flashcards + +There is schedule info for 3 cards, but only 2 cards in the question + +Note: The scheduling comment is on the same line as the question itself. +If it's on the subsequent line lint will complain and require a blank line before the comment +Which isn't recognized by the note parser + +A {{question}} with multiple parts {{Navevo part}} diff --git a/tests/vaults/readme.md b/tests/vaults/readme.md new file mode 100644 index 00000000..af01f25f --- /dev/null +++ b/tests/vaults/readme.md @@ -0,0 +1,20 @@ +These vaults are used by the unit test cases. + +# Test Vaults + +## filesButNoQuestions + +## notes1 + +## notes2 + +## notes3 + +- Some note files, with links between them +- No questions in any of the notes +- No notes already reviewed + +## notes4 + +- Same as notes3, except +- A.md note already reviewed as easy diff --git a/tsconfig.json b/tsconfig.json index 73deb39b..1c5dbafb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "ES6", + "target": "ES2018", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", diff --git a/wdio.conf.js b/wdio.conf.js deleted file mode 100644 index 64b84d90..00000000 --- a/wdio.conf.js +++ /dev/null @@ -1,27 +0,0 @@ -exports.config = { - specs: ["./tests/e2e/**/*.test.js"], - exclude: [], - capabilities: [ - { - maxInstances: 1, - browserName: "chrome", - "goog:chromeOptions": { - binary: process.env.OBSIDIAN_BINARY_PATH, - args: process.env.CI ? ["headless"] : [], - }, - acceptInsecureCerts: true, - }, - ], - logLevel: "info", - bail: 0, - baseUrl: "http://localhost", - waitforTimeout: 10000, - connectionRetryTimeout: 120000, - connectionRetryCount: 3, - framework: "mocha", - reporters: ["spec"], - mochaOpts: { - ui: "bdd", - timeout: 60000, - }, -};