├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ ├── build.yml
│ ├── deploy-to-gh-pages.yml
│ ├── lint.yml
│ ├── test.yml
│ └── zip-ankiaddon.yml
├── .gitignore
├── LICENSE
├── README-ankiweb.html
├── README.adoc
├── SECURITY.md
├── anki
├── __init__.py
├── designer
│ ├── model_settings.ui
│ └── settings.ui
├── gui
│ ├── forms
│ │ └── .gitignore
│ ├── model_settings.py
│ └── settings.py
├── icons
│ └── occlude.png
├── manifest.json
├── src
│ ├── __init__.py
│ ├── addcards.py
│ ├── addon_manager.py
│ ├── editor
│ │ ├── __init__.py
│ │ └── text_wrap.py
│ ├── hook.py
│ ├── models.py
│ ├── simulate_typing.py
│ ├── update.py
│ ├── utils.py
│ ├── version.py
│ └── webview
│ │ └── __init__.py
└── web
│ ├── .gitignore
│ ├── default.js
│ ├── editable.js
│ ├── editor.js
│ ├── license.txt
│ └── user.js
├── babel.config.js
├── bin
├── compile-anki.sh
├── compile-parser.sh
├── compile-setups.js
├── compile-style.js
├── create-favicon.sh
├── install-dependencies.sh
├── link.sh
├── serve.sh
└── zip.sh
├── build
└── .gitignore
├── docs
├── 404.md
├── CNAME
├── Gemfile
├── Gemfile.lock
├── README.adoc
├── _config.yml
├── _data
│ ├── buttons.yml
│ ├── setups
│ │ ├── assign_categories.yml
│ │ ├── blanking_cloze.yml
│ │ ├── click_to_reveal_cloze.yml
│ │ ├── debug.yml
│ │ ├── default_cloze.yml
│ │ ├── default_multiple_choice.yml
│ │ ├── default_shuffle.yml
│ │ ├── defining_lists.yml
│ │ ├── delims.yml
│ │ ├── fancy_multiple_choice.yml
│ │ ├── fancy_shuffle.yml
│ │ ├── flashcard.yml
│ │ ├── generate.yml
│ │ ├── input_multiple_choice.yml
│ │ ├── keydown_to_reveal_cloze.yml
│ │ ├── mandarin_support.yml
│ │ ├── obscuring_cloze.yml
│ │ ├── occlusions.yml
│ │ ├── occlusions_highlight.yml
│ │ ├── order.yml
│ │ ├── pick_eval.yml
│ │ ├── pick_eval_padding.yml
│ │ ├── shuffle_quest.yml
│ │ └── templated_shuffle.yml
│ └── snippets
│ │ ├── cloze
│ │ ├── activate_cloze.yml
│ │ ├── activate_cloze_conflict.yml
│ │ ├── activate_cloze_with_occur.yml
│ │ ├── click_to_reveal.yml
│ │ ├── click_to_reveal_single.yml
│ │ ├── first_example.yml
│ │ ├── hide_context.yml
│ │ ├── hiding_cloze.yml
│ │ ├── hiding_cloze_symbols.yml
│ │ ├── hints.yml
│ │ ├── numbered_cloze.yml
│ │ └── zero_cloze.yml
│ │ ├── flashcard
│ │ ├── around_ctxt.yml
│ │ ├── around_range.yml
│ │ └── top_bottom_range.yml
│ │ ├── generation
│ │ ├── picking.yml
│ │ ├── picking_index.yml
│ │ ├── picking_padding.yml
│ │ ├── picking_template.yml
│ │ ├── picking_unique.yml
│ │ └── simple.yml
│ │ ├── home
│ │ └── intro_example.yml
│ │ ├── multiple_choice
│ │ ├── animals.yml
│ │ ├── formal_questions.yml
│ │ ├── latin_proverbs.yml
│ │ ├── simple_nonzero.yml
│ │ └── simple_zero.yml
│ │ ├── occlusions
│ │ ├── bones.yml
│ │ ├── bones_with_other.yml
│ │ ├── bones_with_other2.yml
│ │ ├── cell.yml
│ │ ├── cell_flashcard.yml
│ │ ├── create.yml
│ │ └── formulas.yml
│ │ ├── shuffling
│ │ ├── assign_shuffle.yml
│ │ ├── assign_shuffle_ol.yml
│ │ ├── deadlock.yml
│ │ ├── first_example.yml
│ │ ├── individual_items.yml
│ │ ├── inline_vs_list.yml
│ │ ├── japanese.yml
│ │ ├── lines.yml
│ │ ├── list_items.yml
│ │ ├── mixed_styles.yml
│ │ ├── nesting.yml
│ │ ├── non_contiguous.yml
│ │ ├── on_extra_line.yml
│ │ ├── onesided_nesting.yml
│ │ └── preserve_item_count.yml
│ │ └── stylizing
│ │ └── mandarin.yml
├── _includes
│ ├── codeDisplay.md
│ ├── codeSection.md
│ ├── head_custom.html
│ ├── header-doc.md
│ ├── info.svg
│ ├── js
│ │ ├── codeDisplay.js
│ │ ├── docButtons.js
│ │ ├── prismSetup.js
│ │ ├── testerFunc.js
│ │ └── testerPreset.js
│ ├── tester.html
│ └── toc-doc.md
├── _layouts
│ ├── doc.html
│ ├── redirect.html
│ └── tester.html
├── _sass
│ ├── filterManagerButton.scss
│ └── toggleMemoizationButton.scss
├── assets
│ ├── css
│ │ ├── codeDisplay.scss
│ │ ├── main.scss
│ │ └── tester.scss
│ ├── icons
│ │ ├── check-mark.svg
│ │ ├── down.svg
│ │ ├── up.svg
│ │ └── x-mark.svg
│ ├── images
│ │ ├── after.png
│ │ ├── anki-fields.png
│ │ ├── before.png
│ │ ├── cell.png
│ │ ├── cranial-bones.png
│ │ ├── editcurrent.png
│ │ ├── formulas.png
│ │ ├── reviewer.png
│ │ └── wikipedia.png
│ ├── js
│ │ └── .gitignore
│ └── setups
│ │ └── .gitkeep
├── clozes
│ ├── blanking-obscuring.md
│ ├── creating.md
│ ├── incremental-reveal.md
│ └── index.md
├── flashcard
│ ├── activating-in-a-range.md
│ ├── activating-selectively.md
│ ├── index.md
│ └── switching.md
├── generation
│ ├── evaluation.md
│ ├── index.md
│ ├── padded-picking.md
│ ├── picking.md
│ └── random.md
├── home
│ ├── how-to-use.md
│ ├── index.md
│ ├── installation.md
│ └── the-name.md
├── multiple-choice
│ ├── categories.md
│ ├── creating.md
│ ├── index.md
│ └── radio-button-checkbox.md
├── occlusions
│ ├── create_occlusions.md
│ ├── index.md
│ ├── render_occlusions.md
│ ├── with_other_types.md
│ ├── with_other_types2.md
│ └── without_context.md
├── shuffling
│ ├── index.md
│ ├── ordering.md
│ ├── shuffle_quest.md
│ ├── shuffling.md
│ └── templated.md
├── stylizing
│ ├── index.md
│ └── mandarin.md
└── tester
│ └── index.md
├── images
├── logo.png
├── logo.pxm
├── textlogo.png
└── weblogo.png
├── index.ts
├── jest.config.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── serve.json
├── src
├── browser
│ ├── index.ts
│ ├── menu.ts
│ ├── menuConstruction.ts
│ ├── moveResize.ts
│ ├── occlusionEditor.ts
│ ├── rect.ts
│ ├── scaleZoom.ts
│ ├── svgClasses.ts
│ └── utils.ts
├── filterManager.ts
├── filterManager
│ ├── deferred.ts
│ ├── filters.ts
│ ├── index.ts
│ ├── priorityQueue.ts
│ ├── registrar.ts
│ └── storage.ts
├── flashcard
│ ├── cloze.ts
│ ├── deciders.ts
│ ├── flashcardTemplate.ts
│ ├── inactiveAdapter.ts
│ ├── index.ts
│ ├── multipleChoice.ts
│ ├── shuffleQuestion.ts
│ └── spec.ts
├── generator.ts
├── index.ts
├── patterns.ts
├── recipes
│ ├── debug.ts
│ ├── delim.ts
│ ├── generating.ts
│ ├── index.ts
│ ├── meta.ts
│ ├── ordering.ts
│ ├── preferenceStore
│ │ ├── boolStore.ts
│ │ ├── index.ts
│ │ ├── numberStore.ts
│ │ └── storeTemplate.ts
│ ├── sharedStore
│ │ ├── index.ts
│ │ ├── listStore.ts
│ │ ├── pickers.ts
│ │ ├── setList.ts
│ │ └── storeTemplate.ts
│ ├── shuffling.ts
│ └── simple.ts
├── sequencers.ts
├── sortInStrategies.ts
├── styleList.ts
├── stylizer.ts
├── template
│ ├── anki
│ │ ├── delay.ts
│ │ ├── index.ts
│ │ ├── initialize.ts
│ │ ├── persistence.ts
│ │ ├── qaNodes.ts
│ │ └── utils.ts
│ ├── browser
│ │ ├── childNodes.ts
│ │ ├── index.ts
│ │ └── intersplice.ts
│ ├── delimiters.ts
│ ├── index.ts
│ ├── nodes.ts
│ ├── optics
│ │ ├── circumfix.ts
│ │ ├── consumers.ts
│ │ ├── index.ts
│ │ ├── mapped.ts
│ │ ├── profunctors.ts
│ │ ├── separated.ts
│ │ ├── stripped.ts
│ │ ├── templated.ts
│ │ └── utils.ts
│ ├── parser
│ │ ├── .gitignore
│ │ ├── grammar.ne
│ │ ├── index.ts
│ │ ├── tagBuilder.ts
│ │ └── tokenizer.ts
│ ├── tagSelector
│ │ ├── .gitignore
│ │ ├── grammar.ne
│ │ ├── index.ts
│ │ └── tokenizer.ts
│ ├── template.ts
│ ├── types.ts
│ └── utils.ts
├── types.ts
├── utils.ts
├── version.ts
└── wrappers
│ ├── collection.ts
│ ├── index.ts
│ ├── product.ts
│ ├── sum.ts
│ └── wrappers.ts
├── style
├── README.adoc
├── _cloze.scss
├── _multiple-choice.scss
├── _rect.scss
├── _shuffle-question.scss
├── _shuffle.scss
├── _utils.scss
├── base.scss
└── editor.scss
├── test
├── README.md
├── browser
│ ├── .parentlock
│ ├── dist
│ │ └── README.md
│ ├── index.html
│ └── specs
│ │ ├── childnode.spec.js
│ │ └── intersplice.spec.js
└── src
│ └── template
│ └── tagSelector.spec.ts
├── tsconfig.json
└── website
├── .gitignore
├── README.md
├── babel.config.js
├── docs
├── clozes
│ └── creating.mdx
├── doc1.mdx
├── doc2.mdx
├── flashcard.mdx
├── installation.mdx
├── mdx.mdx
├── showcase.mdx
├── snippets
│ └── example.yml
└── tryit.mdx
├── docusaurus.config.js
├── package.json
├── sidebars.js
├── src
├── codeMirrorCloset.js
├── components
│ ├── CodeEditor
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── ContextControls
│ │ └── index.tsx
│ ├── Example
│ │ ├── index.tsx
│ │ └── styles.module.css
│ ├── ExampleCompiled
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── ExampleSyntax
│ │ ├── index.tsx
│ │ └── styles.css
│ ├── SetupDrawer
│ │ └── index.tsx
│ ├── SetupTooltip
│ │ └── index.tsx
│ └── TabButtonPanel
│ │ ├── index.tsx
│ │ └── styles.css
├── contexts
│ ├── frontBack.ts
│ └── index.ts
├── css
│ └── custom.css
├── examples
│ ├── clozesNothingHappens
│ │ ├── index.ts
│ │ └── text.html
│ ├── firstExample
│ │ ├── index.ts
│ │ └── text.html
│ └── index.ts
├── icons
│ └── anki.svg
├── pages
│ ├── helloReact.tsx
│ └── styles.module.css
├── prismSetup.js
└── setups
│ ├── clozes
│ ├── index.ts
│ └── setup.js
│ └── index.ts
├── static
└── .nojekyll
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | indent_style = space
4 | indent_size = 4
5 | end_of_line = lf
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 |
9 | [*.{scss,css,yml}]
10 | indent_size = 2
11 |
12 | [*.{html,js,jsx,ts,tsx,py}]
13 | indent_size = 4
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | grammar.ts
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": [
5 | "@typescript-eslint"
6 | ],
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Fill this out once it is possible
4 | patreon: hgiesel
5 | ko_fi: hgiesel
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: bug
6 | assignees: hgiesel
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | - Anki client: [e.g. AnkiDesktop, AnkiDroid, AnkiMobile on iOS, AnkiWeb]
13 | - if AnkiDesktop, please name your OS [e.g. Windows, macOS, Linux]
14 | - if AnkiDesktop, please name the version. You can find it in the "About" section [e.g. 2.1.26]
15 |
16 | **Debug information**
17 | To help you debug, I need some information. I have two suggestions for you to help me. It would be best, if you did both, as that way I can help you the quickest.
18 |
19 | #### Method 1 (preferred method)
20 |
21 | 1. You put the cards which do not work (however many you like), and put them into a new Anki Deck.
22 | 1. You export it by finding it on the main screen, clicking on the small right cog (wheel) next to it, and choose "Export"
23 | 1. As Export format choose "**Anki Deck Package**", **uncheck** "Include scheduling information", **check** "Include media".
24 | 1. You will get a file with the ending ".apkg". Upload this file here.
25 |
26 | #### Method 2
27 |
28 | 1. You download the [AnkiWebView Inspector](https://ankiweb.net/shared/info/31746032)
29 | 1. Open a card which doesn't work in the reviewer, right-click on the card, and select "Inspect". This will open a tab to the right.
30 | 1. In this tab, select "Console", however usually it is automatically selected.
31 | 1. Look for an error message, or other kind of output.
32 | 1. If there is none, try typing in `closetPromise` and click enter.
33 | 1. Take a screenshot of the error (or absence of it), and upload it here.
34 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Build Closet
5 |
6 | on:
7 | push:
8 | branches: [release, master, dev]
9 | pull_request:
10 | branches: [master]
11 |
12 | jobs:
13 | build:
14 | strategy:
15 | matrix:
16 | node-version: [13.x, 15.x]
17 | os: [macos-latest, ubuntu-latest]
18 |
19 | runs-on: ${{ matrix.os }}
20 |
21 | steps:
22 | - name: Checkout 🛎️
23 | uses: actions/checkout@v2
24 |
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 |
30 | - name: Install dependencies
31 | run: npm run-script install-dependencies
32 |
33 | - name: Install and Build 🔧
34 | run: |
35 | npm install
36 | npm run-script build
37 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-to-gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - release
7 |
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout 🛎️
14 | uses: actions/checkout@v2
15 | with:
16 | persist-credentials: false
17 |
18 | - name: Install Dependencies 🔧
19 | run: |
20 | npm run-script install-dependencies
21 |
22 | - name: Install and Build 🔧
23 | run: |
24 | npm install
25 | npm run-script build:docs
26 |
27 | - name: Deploy to GitHub Pages 🚀
28 | uses: peaceiris/actions-gh-pages@v3.6.1
29 | with:
30 | github_token: ${{ secrets.GITHUB_TOKEN }}
31 | publish_branch: gh-pages
32 | publish_dir: ./docs
33 | enable_jekyll: true
34 |
35 | user_name: Henrik Giesel
36 | user_email: hengiesel@gmail.com
37 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Lint Closet
5 |
6 | on:
7 | push:
8 | branches: [release, master, dev]
9 | pull_request:
10 | branches: [master]
11 |
12 | jobs:
13 | build:
14 | strategy:
15 | matrix:
16 | node-version: [13.x, 15.x]
17 | os: [macos-latest, ubuntu-latest]
18 |
19 | runs-on: ${{ matrix.os }}
20 |
21 | steps:
22 | - name: Checkout 🛎️
23 | uses: actions/checkout@v2
24 |
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 |
30 | - name: Install dependencies
31 | run: npm run-script install-dependencies
32 |
33 | - name: Install and Lint 🔧
34 | run: |
35 | npm install
36 | npm run-script lint
37 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Test Closet
5 |
6 | on:
7 | push:
8 | branches: [release, master, dev]
9 | pull_request:
10 | branches: [master]
11 |
12 | jobs:
13 | build:
14 | strategy:
15 | matrix:
16 | node-version: [13.x, 15.x]
17 | os: [macos-latest, ubuntu-latest]
18 |
19 | runs-on: ${{ matrix.os }}
20 |
21 | steps:
22 | - name: Checkout 🛎️
23 | uses: actions/checkout@v2
24 |
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 |
30 | - name: Install dependencies
31 | run: |
32 | npm run-script install-dependencies
33 |
34 | - name: Install and Build 🔧
35 | run: |
36 | npm install
37 | npm run-script compile:parser
38 | npm run-script test
39 |
--------------------------------------------------------------------------------
/.github/workflows/zip-ankiaddon.yml:
--------------------------------------------------------------------------------
1 | name: Zip Anki add-ons
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout 🛎️
14 | uses: actions/checkout@v2
15 | with:
16 | persist-credentials: false
17 |
18 | - name: Install and Build 🔧
19 | run: |
20 | npm install
21 | npm run-script zip
22 |
23 | - name: Upload to GitHub
24 | uses: actions/upload-artifact@v2
25 | with:
26 | name: Addons
27 | path: build/ClosetForAnki.ankiaddon
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # build for npm
4 | index.js
5 |
6 | # compiled src outside of docs
7 | node_modules/*
8 |
9 | # for Jekyll
10 | _site/
11 | .sass-cache/
12 | .jekyll-cache/
13 | .jekyll-metadata
14 |
15 | # generated by bundle
16 | .bundle/
17 | vendor/
18 |
19 | # compiled closet for test
20 | dist/**/*
21 |
22 | # test suite compile
23 | test/browser/dist/**/*
24 | !test/browser/dist/README.md
25 |
26 | # docs
27 | ## compiled images
28 | docs/favicon.ico
29 | docs/assets/images/*logo.png
30 | ## compiled closet
31 | docs/assets/js/*.js
32 | docs/assets/js/*.js.map
33 | docs/assets/css/closet.css
34 |
35 | # anki
36 | anki/meta.json
37 | __pycache__
38 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | image:https://github.com/hgiesel/closet/workflows/Build%20Closet/badge.svg[Build Closet]
2 | image:https://github.com/hgiesel/closet/workflows/Test%20Closet/badge.svg[Test Closet]
3 | image:https://github.com/hgiesel/closet/workflows/Lint%20Closet/badge.svg[Lint Closet]
4 | image:https://github.com/hgiesel/closet/workflows/Zip%20Anki%20add-ons/badge.svg[Zip Anki add-ons]
5 | image:https://github.com/hgiesel/closet/workflows/Deploy%20to%20GitHub%20Pages/badge.svg[Deploy to GitHub Pages]
6 |
7 | Closet is a powerful templating engine for use in flashcards, especially link:https://apps.ankiweb.net/[Anki] flashcards.
8 |
9 | image::https://github.com/hgiesel/closet/blob/master/images/textlogo.png[ClosetLogo,width=100%,align="center",link="https://closetengine.com"]
10 |
11 | == How to Build for Anki
12 |
13 | `npm run zip`
14 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | all | :x: |
8 | | none | :white_check_mark: |
9 |
10 | ## Reporting a Vulnerability
11 |
--------------------------------------------------------------------------------
/anki/__init__.py:
--------------------------------------------------------------------------------
1 | from .src import init
2 |
3 | init()
4 |
--------------------------------------------------------------------------------
/anki/designer/model_settings.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Settings
4 |
5 |
6 |
7 | 0
8 | 0
9 | 247
10 | 110
11 |
12 |
13 |
14 |
15 | 0
16 | 0
17 |
18 |
19 |
20 | Closet Per Model Settings
21 |
22 |
23 | -
24 |
25 |
26 | Cancel
27 |
28 |
29 |
30 | -
31 |
32 |
33 | Enable Closet (for Asset Manager)
34 |
35 |
36 |
37 | -
38 |
39 |
40 | Qt::Horizontal
41 |
42 |
43 |
44 | -
45 |
46 |
47 | Save
48 |
49 |
50 |
51 | -
52 |
53 |
54 | Current inserted Closet version
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/anki/gui/forms/.gitignore:
--------------------------------------------------------------------------------
1 | *.py
2 |
--------------------------------------------------------------------------------
/anki/gui/model_settings.py:
--------------------------------------------------------------------------------
1 | from aqt import QDialog, QLayout, QKeySequence, qtmajor
2 | from ..src.version import version
3 |
4 | if qtmajor < 6:
5 | from .forms.qt5.model_settings_ui import Ui_Settings
6 | else:
7 | from .forms.qt6.model_settings_ui import Ui_Settings
8 |
9 |
10 | class ModelSettings(QDialog):
11 | def __init__(self, mw, callback):
12 | super().__init__(parent=mw)
13 |
14 | self.mw = mw
15 |
16 | self.ui = Ui_Settings()
17 | self.ui.setupUi(self)
18 |
19 | self.callback = callback
20 | self.ui.saveButton.clicked.connect(self.accept)
21 | self.ui.cancelButton.clicked.connect(self.reject)
22 |
23 | self.layout().setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
24 |
25 | def setupUi(
26 | self,
27 | closet_enabled: bool,
28 | closet_version: str,
29 | ):
30 | self.ui.closetEnabled.setChecked(closet_enabled)
31 |
32 | uptodate = closet_version == version
33 | uptodate_text = (
34 | "(up-to-date)" if uptodate else f"(the add-on is on version {version})"
35 | )
36 |
37 | self.ui.versionLabel.setText(
38 | f"Inserted Closet: {closet_version} {uptodate_text}"
39 | )
40 |
41 | def accept(self):
42 | self.callback(
43 | self.ui.closetEnabled.isChecked(),
44 | )
45 |
46 | super().accept()
47 |
--------------------------------------------------------------------------------
/anki/gui/settings.py:
--------------------------------------------------------------------------------
1 | from aqt import QDialog, QLayout, QKeySequence, qtmajor
2 | from ..src.version import version
3 |
4 | if qtmajor < 6:
5 | from .forms.qt5.settings_ui import Ui_Settings
6 | else:
7 | from .forms.qt6.settings_ui import Ui_Settings
8 |
9 |
10 | behaviors = ["autopaste", "copy"]
11 |
12 |
13 | class Settings(QDialog):
14 | def __init__(self, parent, callback):
15 | super().__init__(parent=parent)
16 |
17 | self.ui = Ui_Settings()
18 | self.ui.setupUi(self)
19 |
20 | self.cb = callback
21 | self.layout().setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
22 |
23 | def setupUi(
24 | self, occlude_shortcut: str, occlude_accept_behavior: str, max_height: int
25 | ) -> None:
26 | self.ui.occludeShortcut.setKeySequence(QKeySequence(occlude_shortcut))
27 | self.ui.occlusionAcceptBehavior.setCurrentIndex(
28 | behaviors.index(occlude_accept_behavior)
29 | )
30 | self.ui.maxHeight.setValue(max_height)
31 |
32 | self.ui.versionInfo.setText(f"Closet {version}")
33 |
34 | def accept(self):
35 | occlude_shortcut = self.ui.occludeShortcut.keySequence().toString()
36 | occlude_accept_behavior = behaviors[
37 | self.ui.occlusionAcceptBehavior.currentIndex()
38 | ]
39 | max_height = self.ui.maxHeight.value()
40 |
41 | self.cb(occlude_shortcut, occlude_accept_behavior, max_height)
42 | super().accept()
43 |
--------------------------------------------------------------------------------
/anki/icons/occlude.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/anki/icons/occlude.png
--------------------------------------------------------------------------------
/anki/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Closet For Anki",
3 | "package": "closet_for_anki"
4 | }
5 |
--------------------------------------------------------------------------------
/anki/src/__init__.py:
--------------------------------------------------------------------------------
1 | from aqt.gui_hooks import profile_did_open
2 |
3 | from .hook import setup_script, install_script
4 | from .update import update_closet
5 |
6 | from .webview import init_webview
7 | from .editor import init_editor
8 | from .addcards import init_addcards
9 |
10 | from .models import init_models_dialog
11 | from .addon_manager import init_addon_manager
12 |
13 |
14 | def init():
15 | setup_script()
16 | profile_did_open.append(install_script)
17 | profile_did_open.append(update_closet)
18 |
19 | init_webview()
20 | init_editor()
21 | init_addcards()
22 |
23 | init_models_dialog()
24 | init_addon_manager()
25 |
--------------------------------------------------------------------------------
/anki/src/addcards.py:
--------------------------------------------------------------------------------
1 | from aqt import mw, dialogs
2 | from aqt.gui_hooks import add_cards_will_add_note
3 | from aqt.qt import QKeySequence
4 | from aqt.utils import shortcut
5 |
6 | from .utils import occlude_shortcut
7 |
8 |
9 | def check_if_occlusion_editor_open(problem, _note):
10 | addcards = dialogs._dialogs["AddCards"][1]
11 | shortcut_as_text = shortcut(QKeySequence(occlude_shortcut.value).toString())
12 |
13 | occlusion_problem = (
14 | (
15 | "Closet Occlusion Editor is still open. "
16 | f'Please accept by right-clicking and selecting "Accept" or reject the occlusions by using {shortcut_as_text}.'
17 | )
18 | if addcards.editor.occlusion_editor_active
19 | else None
20 | )
21 |
22 | return problem or occlusion_problem
23 |
24 |
25 | def init_addcards():
26 | add_cards_will_add_note.append(check_if_occlusion_editor_open)
27 |
--------------------------------------------------------------------------------
/anki/src/addon_manager.py:
--------------------------------------------------------------------------------
1 | from typing import Union, Optional
2 |
3 | from aqt import mw
4 | from aqt.addons import AddonsDialog
5 | from aqt.gui_hooks import addons_dialog_will_show
6 |
7 | from ..gui.settings import Settings
8 |
9 | from .utils import occlude_shortcut, occlusion_behavior, max_height, AcceptBehaviors
10 |
11 |
12 | def set_settings(
13 | shortcut: str,
14 | behavior: AcceptBehaviors,
15 | maxheight: int,
16 | ):
17 | occlude_shortcut.value = shortcut
18 | occlusion_behavior.value = behavior
19 | max_height.value = maxheight
20 |
21 |
22 | addons_current: Optional[AddonsDialog] = None
23 |
24 |
25 | def save_addons_window(addons):
26 | global addons_current
27 | addons_current = addons
28 |
29 |
30 | def show_settings():
31 | dialog = Settings(addons_current, set_settings)
32 |
33 | dialog.setupUi(
34 | occlude_shortcut.value,
35 | occlusion_behavior.value,
36 | max_height.value,
37 | )
38 | return dialog.open()
39 |
40 |
41 | def init_addon_manager():
42 | addons_dialog_will_show.append(save_addons_window)
43 | mw.addonManager.setConfigAction(__name__, show_settings)
44 |
--------------------------------------------------------------------------------
/anki/src/editor/text_wrap.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from ..simulate_typing import activate_matching_fields
4 |
5 |
6 | def get_base_top(editor, prefix: str, suffix: str) -> int:
7 | matches = []
8 | query = re.escape(prefix) + r"(\d+)" + re.escape(suffix)
9 |
10 | for name, item in editor.note.items():
11 | matches.extend(re.findall(query, item))
12 |
13 | values = [0]
14 | values.extend([int(x) for x in matches])
15 |
16 | return max(values)
17 |
18 |
19 | def get_top_index(editor, prefix: str, suffix: str) -> int:
20 | base_top = get_base_top(editor, prefix, suffix)
21 |
22 | return base_top + 1 if base_top == 0 else base_top
23 |
24 |
25 | def get_incremented_index(editor, prefix: str, suffix: str) -> int:
26 | base_top = get_base_top(editor, prefix, suffix)
27 |
28 | return base_top + 1
29 |
30 |
31 | def activate_matching_field(indexer):
32 | def get_value(editor, prefix: str, suffix: str) -> int:
33 | current_index = indexer(editor, prefix, suffix)
34 | was_filled = activate_matching_fields(editor, [current_index])[0]
35 |
36 | return current_index if was_filled else 0
37 |
38 | return get_value
39 |
40 |
41 | def no_index(_editor, _prefix: str, _suffix: str) -> str:
42 | return ""
43 |
44 |
45 | def top_index(type_):
46 | if type_ == "free":
47 | return get_top_index
48 | elif type_ == "flashcard":
49 | return activate_matching_field(get_top_index)
50 | else: # type_ == "none":
51 | return no_index
52 |
53 |
54 | def incremented_index(type_):
55 | if type_ == "free":
56 | return get_incremented_index
57 | elif type_ == "flashcard":
58 | return activate_matching_field(get_incremented_index)
59 | else: # type_ == "none":
60 | return no_index
61 |
--------------------------------------------------------------------------------
/anki/src/models.py:
--------------------------------------------------------------------------------
1 | from aqt.gui_hooks import models_did_init_buttons
2 | from aqt import mw
3 |
4 | from anki.lang import _
5 |
6 | from .utils import closet_enabled, closet_version_per_model
7 | from ..gui.model_settings import ModelSettings
8 |
9 |
10 | def set_settings(
11 | enabled: bool,
12 | ):
13 | closet_enabled.value = enabled
14 |
15 |
16 | def on_closet(models):
17 | current_row: int = models.form.modelsList.currentRow()
18 | model_id: int = models.models[current_row].id
19 |
20 | closet_enabled.model_id = model_id
21 | closet_version_per_model.model_id = model_id
22 |
23 | dialog = ModelSettings(mw, set_settings)
24 |
25 | dialog.setupUi(closet_enabled.value, closet_version_per_model.value)
26 | return dialog.exec()
27 |
28 |
29 | def init_closet_button(buttons, models):
30 | buttons.append(
31 | (
32 | _("Closet..."),
33 | lambda: on_closet(models),
34 | )
35 | )
36 |
37 | return buttons
38 |
39 |
40 | def init_models_dialog():
41 | models_did_init_buttons.append(init_closet_button)
42 |
--------------------------------------------------------------------------------
/anki/src/simulate_typing.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import re
4 |
5 |
6 | def is_text_empty(editor, text) -> bool:
7 | return editor.mungeHTML(text) == ""
8 |
9 |
10 | def escape_js_text(text: str) -> str:
11 | return text.replace("\\", "\\\\").replace('"', '\\"').replace("'", "\\'")
12 |
13 |
14 | def make_insertion_js(field_index: int, text: str) -> str:
15 | escaped = escape_js_text(text)
16 |
17 | cmd = (
18 | f"pycmd(`key:{field_index}:${{getNoteId()}}:{escaped}`); "
19 | f"EditorCloset.setFieldHTML({field_index}, `{escaped}`); "
20 | )
21 | return cmd
22 |
23 |
24 | def insert_into_zero_indexed(editor, text: str) -> None:
25 | for index, (name, item) in enumerate(editor.note.items()):
26 | match = re.search(r"\d+$", name)
27 |
28 | if not match or int(match[0]) != 0:
29 | continue
30 |
31 | editor.web.eval(f"EditorCloset.insertIntoZeroIndexed(`{text}`, {index}); ")
32 | break
33 |
34 |
35 | def activate_matching_fields(editor, indices: List[int]) -> List[bool]:
36 | founds = [False for index in indices]
37 |
38 | for index, (name, item) in enumerate(editor.note.items()):
39 | match = re.search(r"\d+$", name)
40 |
41 | if not match:
42 | continue
43 |
44 | matched = int(match[0])
45 |
46 | if matched not in indices:
47 | continue
48 |
49 | founds[indices.index(matched)] = True
50 |
51 | # TODO anki behavior for empty fields is kinda weird right now:
52 | if not is_text_empty(editor, item):
53 | continue
54 |
55 | editor.web.eval(make_insertion_js(index, "active"))
56 |
57 | return founds
58 |
--------------------------------------------------------------------------------
/anki/src/version.py:
--------------------------------------------------------------------------------
1 | # NOTE should be same as under ../../src/version.ts
2 | versionInfo = [
3 | 0, # MAJOR
4 | 6, # MINOR
5 | 1, # PATCH
6 | ]
7 |
8 | prereleaseInfo = []
9 |
10 | version = ".".join([str(comp) for comp in versionInfo]) + (
11 | f'-{".".join([str(comp) for comp in prereleaseInfo])}'
12 | if len(prereleaseInfo) > 0
13 | else ""
14 | )
15 |
--------------------------------------------------------------------------------
/anki/web/.gitignore:
--------------------------------------------------------------------------------
1 | # automatically generated files
2 | closet.js
3 | closet.css
4 | editor.css
5 |
--------------------------------------------------------------------------------
/anki/web/default.js:
--------------------------------------------------------------------------------
1 | filterManager.install(
2 | closet.recipes.shuffle({ tagname: "mix" }),
3 | closet.recipes.order({ tagname: "ord" }),
4 |
5 | closet.flashcard.recipes.cloze({
6 | tagname: "c",
7 | defaultBehavior: closet.flashcard.behaviors.Show,
8 | }),
9 | closet.flashcard.recipes.multipleChoice({
10 | tagname: "mc",
11 | defaultBehavior: closet.flashcard.behaviors.Show,
12 | }),
13 | closet.flashcard.recipes.sort({
14 | tagname: "sort",
15 | defaultBehavior: closet.flashcard.behaviors.Show,
16 | }),
17 |
18 | closet.browser.recipes.rect({
19 | tagname: "rect",
20 | defaultBehavior: closet.flashcard.behaviors.Show,
21 | }),
22 | );
23 |
--------------------------------------------------------------------------------
/anki/web/editable.js:
--------------------------------------------------------------------------------
1 | const elements = closet.template.anki.getQaChildNodes();
2 | const memory = chooseMemory("closet__1");
3 | const filterManager = closet.FilterManager.make(preset, memory.map);
4 |
5 | const output = [[elements, memory, filterManager]];
6 |
7 | /* here goes the setup - change it to fit your own needs */
8 |
9 | $$setupCode;
10 |
--------------------------------------------------------------------------------
/anki/web/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) Microsoft Corporation.
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted.
5 |
6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
8 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
12 | PERFORMANCE OF THIS SOFTWARE.
13 |
--------------------------------------------------------------------------------
/anki/web/user.js:
--------------------------------------------------------------------------------
1 | function closetUserLogic(closet, preset, chooseMemory) {
2 | $$editableCode;
3 | return output;
4 | }
5 |
6 | var getAnkiPrefix = () =>
7 | globalThis.ankiPlatform === "desktop"
8 | ? ""
9 | : globalThis.AnkiDroidJS
10 | ? "https://appassets.androidplatform.net"
11 | : ".";
12 |
13 | var closetPromise = import(`${getAnkiPrefix()}/__closet-$$version.js`);
14 | closetPromise
15 | .then(
16 | ({ closet }) =>
17 | closet.template.anki.initialize(
18 | closet,
19 | closetUserLogic,
20 | "$$cardType",
21 | "$$tagsFull",
22 | "$$side",
23 | ),
24 | (error) => console.log("An error occured while loading Closet:", error),
25 | )
26 | .catch((error) =>
27 | console.log("An error occured while executing Closet:", error),
28 | );
29 |
30 | if (globalThis.onUpdateHook) {
31 | onUpdateHook.push(() => closetPromise);
32 | }
33 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = api => {
2 | api.cache(true)
3 |
4 | const presets = [
5 | [
6 | "@babel/preset-env", {
7 | "targets": {
8 | "chrome": "57",
9 | "browsers": "last 2 versions, ie 10-11"
10 | }
11 | }
12 | ],
13 | ]
14 |
15 | const plugins = [
16 | "@babel/plugin-proposal-class-properties"
17 | ]
18 |
19 | return {
20 | presets,
21 | plugins,
22 | "sourceMaps": true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bin/compile-anki.sh:
--------------------------------------------------------------------------------
1 | DIR="$(cd "$(dirname "$0")/../anki" && pwd -P)"
2 |
3 | mkdir -p "$DIR/gui/forms/qt5" "$DIR/gui/forms/qt6"
4 |
5 | for filename in "$DIR/designer/"*'.ui'; do
6 | python -m PyQt5.uic.pyuic "$filename" > "$DIR/gui/forms/qt5/$(basename ${filename%.*})_ui.py"
7 | python -m PyQt6.uic.pyuic "$filename" > "$DIR/gui/forms/qt6/$(basename ${filename%.*})_ui.py"
8 | done
9 |
10 | echo 'Was successfully compiled!'
11 |
--------------------------------------------------------------------------------
/bin/compile-parser.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | DIR="$(cd "$(dirname "$0")/../src" && pwd -P)"
3 |
4 | if ! type nearleyc &> /dev/null; then
5 | echo 'No nearleyc executable found in $PATH'
6 | exit
7 | fi
8 |
9 | ################ TEMPLATE
10 |
11 | template="$DIR/template/parser/grammar"
12 | template_source="${template}.ne"
13 | template_target="${template}.ts"
14 |
15 | nearleyc "$template_source" > "$template_target"
16 |
17 | ################ TEMPLATE ADAPTIONS
18 |
19 | remove_import="import { templateTokenizer } from './tokenizer'"
20 |
21 | sed -i.bak -e "s#$remove_import##" "$template_target"
22 |
23 | open_grammar='const grammar: Grammar = {'
24 | new_open_grammar='const makeGrammar = (tokenizer: NearleyLexer): Grammar => {'
25 |
26 | sed -i.bak -e "s/$open_grammar/$new_open_grammar\n$open_grammar/" "$template_target"
27 |
28 | close_grammar='ParserStart: "start",'
29 | new_close_grammar='return grammar'
30 |
31 | sed -i.bak -e "s/$close_grammar/$close_grammar\n};\n$new_close_grammar/" "$template_target"
32 |
33 | old_export='export default grammar;'
34 | new_export='export default makeGrammar;'
35 |
36 | sed -i.bak -e "s/$old_export/$new_export/" "$template_target"
37 |
38 | old_lexer='interface NearleyLexer {'
39 | export_lexer='export interface NearleyLexer {'
40 |
41 | sed -i.bak -e "s/$old_lexer/$export_lexer/" "$template_target"
42 |
43 | ################ TAG SELECTOR
44 |
45 | tagSelector="$DIR/template/tagSelector/grammar"
46 | tagSelector_source="${tagSelector}.ne"
47 | tagSelector_target="${tagSelector}.ts"
48 |
49 | nearleyc "$tagSelector_source" > "$tagSelector_target"
50 |
51 | ################ TAG SELECTOR ADAPTIONS
52 |
53 | type_invalid_lexer='Lexer: tagSelectorTokenizer,'
54 | type_forced_lexer='Lexer: tagSelectorTokenizer as unknown as NearleyLexer,'
55 |
56 | sed -i.bak -e "s/$type_invalid_lexer/$type_forced_lexer/" "$tagSelector_target"
57 |
58 | ################ CLEANUP
59 |
60 | rm -f "$template_target.bak"
61 | rm -f "$tagSelector_target.bak"
62 |
--------------------------------------------------------------------------------
/bin/compile-style.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const sass = require("sass");
4 |
5 | const __basedir = `${__dirname}/..`;
6 | const args = process.argv.slice(2);
7 |
8 | const renderFile = (input, output) => {
9 | sass.render(
10 | {
11 | file: input,
12 | },
13 | (error, result) => {
14 | if (error) {
15 | console.error(error);
16 | return;
17 | }
18 |
19 | fs.writeFile(output, result.css, (error) => {
20 | if (error) {
21 | return console.log(error);
22 | }
23 |
24 | console.log(`SCSS was successfully compiled to ${output}`);
25 | });
26 | },
27 | );
28 | };
29 |
30 | const renderFromArg = (arg) => {
31 | switch (arg) {
32 | case "anki":
33 | renderFile(
34 | path.join(__basedir, "style", "editor.scss"),
35 | `${__basedir}/anki/web/editor.css`,
36 | );
37 |
38 | renderFile(
39 | path.join(__basedir, "style", "base.scss"),
40 | `${__basedir}/anki/web/closet.css`,
41 | );
42 | break;
43 |
44 | case "dist":
45 | renderFile(
46 | path.join(__basedir, "style", "base.scss"),
47 | `${__basedir}/dist/closet.css`,
48 | );
49 | break;
50 |
51 | case "docs":
52 | renderFile(
53 | path.join(__basedir, "style", "base.scss"),
54 | `${__basedir}/docs/assets/css/closet.css`,
55 | );
56 | break;
57 | }
58 | };
59 |
60 | renderFromArg(args[0]);
61 |
--------------------------------------------------------------------------------
/bin/create-favicon.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | declare DIR="$(cd "$(dirname "$0")/.." && pwd -P)"
3 |
4 | convert "$DIR/images/logo.png" -define icon:auto-resize=64,48,32,16 "$DIR/docs/favicon.ico"
5 |
6 | # copy logo to image folder"
7 | cp -f "$DIR/images/weblogo.png" "$DIR/docs/assets/images"
8 | cp -f "$DIR/images/logo.png" "$DIR/docs/assets/images"
9 |
--------------------------------------------------------------------------------
/bin/install-dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | declare DIR="$(cd "$(dirname "$0")/.." && pwd -P)"
3 |
4 | if [[ "$(uname)" == 'Darwin' ]]; then
5 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
6 | brew install imagemagick pyqt5
7 |
8 | elif [[ "$(uname)" == 'Linux' ]]; then
9 | # Ubuntu
10 | sudo apt-get update
11 | sudo apt-get install --fix-missing qtcreator pyqt5-dev-tools
12 |
13 | else
14 | echo 'Unknown plattform'
15 | exit 5
16 | fi
17 |
--------------------------------------------------------------------------------
/bin/link.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | DIR="$(cd "$(dirname "$0")/../anki" && pwd -P)"
3 | addon_name="ClosetForAnkiDev"
4 | customdir=''
5 |
6 | if [[ -d "$customdir" ]]; then
7 | target="$customdir/$addon_name"
8 |
9 | elif [[ -d "$HOME/.local/share/AnkiDev/addons21" ]]; then
10 | target="$HOME/.local/share/AnkiDev/addons21/$addon_name"
11 |
12 | elif [[ $(uname) = 'Darwin' ]]; then
13 | target="$HOME/Library/Application Support/Anki2/addons21/$addon_name"
14 |
15 | elif [[ $(uname) = 'Linux' ]]; then
16 | target="$HOME/.local/share/Anki2/addons21/$addon_name"
17 |
18 | else
19 | echo 'Unknown platform'
20 | exit -1
21 | fi
22 |
23 | if [[ "$1" =~ ^-?d$ ]]; then
24 | if [[ ! -h "$target" ]]; then
25 | echo 'Directory was not linked'
26 | else
27 | rm "$target"
28 | fi
29 |
30 | elif [[ "$1" =~ ^-?c$ ]]; then
31 | if [[ ! -h "$target" ]]; then
32 | ln -s "$DIR" "$target"
33 | else
34 | echo 'Directory was already linked.'
35 | fi
36 |
37 | else
38 | echo 'Unknown command'
39 | exit -2
40 | fi
41 |
--------------------------------------------------------------------------------
/bin/serve.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | declare DIR="$(cd "$(dirname "$0")/../docs" && pwd -P)"
3 |
4 | # does not work:
5 | # bundle exec --gemfile="$sourcedir/Gemfile" jekyll serve --source "$sourcedir" --destination "$sourcedir/_site"
6 |
7 | cd "$DIR"
8 | bundle exec jekyll serve
9 |
--------------------------------------------------------------------------------
/bin/zip.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | declare DIR="$(cd "$(dirname "$0")/.." && pwd -P)"
3 | declare addon_id='ClosetForAnki'
4 |
5 | cd "$DIR/anki"
6 |
7 | declare debug_target="./src/utils.py"
8 |
9 | # turn off DEBUG mode
10 | sed -i.bak -e "s#DEBUG = True#DEBUG = False#" "$debug_target"
11 |
12 | zip -r "$DIR/build/$addon_id.ankiaddon" \
13 | *".py" \
14 | "manifest.json" \
15 | "gui/"*".py" \
16 | "gui/forms/qt5/"*".py" \
17 | "gui/forms/qt6/"*".py" \
18 | "icons/"*".png" \
19 | "src/"*".py" \
20 | "src/editor/"*".py" \
21 | "src/webview/"*".py" \
22 | "web/"*
23 |
24 | # turn on DEBUG mode
25 | sed -i.bak -e "s#DEBUG = False#DEBUG = True#" "$debug_target"
26 | rm "$debug_target.bak"
27 |
--------------------------------------------------------------------------------
/build/.gitignore:
--------------------------------------------------------------------------------
1 | *.ankiaddon
2 |
--------------------------------------------------------------------------------
/docs/404.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: 404
4 | nav_exclude: true
5 | permalink: /404.html
6 | ---
7 |
8 |
21 |
22 |
23 |
404
24 |
25 |
Page not found :(
26 |
The requested page could not be found.
27 |
28 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | closetengine.com
2 |
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | # Hello! This is where you manage which Jekyll version is used to run.
3 | # When you want to use a different version, change it below, save the
4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
5 | #
6 | # bundle exec jekyll serve
7 | #
8 | # This will help ensure the proper Jekyll version is running.
9 | # Happy Jekylling!
10 | # gem "jekyll", "~> 4.0.0"
11 | # This is the default theme for new Jekyll sites. You may change this to anything you like.
12 | gem "minima", "~> 2.5"
13 | gem "just-the-docs"
14 |
15 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
16 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
17 | gem "github-pages", group: :jekyll_plugins
18 | # If you have any plugins, put them here!
19 | group :jekyll_plugins do
20 | gem "jekyll-feed", "~> 0.12"
21 | end
22 |
23 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
24 | # and associated library.
25 | install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
26 | gem "tzinfo", "~> 1.2"
27 | gem "tzinfo-data"
28 | end
29 |
30 | # Performance-booster for watching directories on Windows
31 | gem "wdm", "~> 0.1.1", :install_if => Gem.win_platform?
32 |
--------------------------------------------------------------------------------
/docs/README.adoc:
--------------------------------------------------------------------------------
1 | = Closet homepage
2 |
3 | Live link:https://closetengine.com[here].
4 |
5 | Note to myself: Don't keep `.gitignore` files in here.
6 | It does not behave well with the deploy script.
7 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 | #
11 | # Site settings
12 | # These are used to personalize your new site. If you look in the HTML files,
13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
14 | # You can create any custom variable you would like, and they will be accessible
15 | # in the templates via {{ site.myvariable }}.
16 |
17 | title: The Closet Engine
18 | email: hengiesel@example.com
19 |
20 | description: >-
21 | Template Engine for Text and Image Manipulation
22 |
23 | url: https://closetengine.com
24 | baseurl: /
25 | github_username: hgiesel
26 |
27 | # Docs: https://pmarsceill.github.io/just-the-docs/
28 | remote_theme: pmarsceill/just-the-docs
29 |
30 | aux_links:
31 | Closet on GitHub:
32 | - //github.com/hgiesel/closet/
33 |
34 | logo: "/assets/images/weblogo.png"
35 |
36 | search_enabled: true
37 | heading_anchors: true
38 | color_scheme: dark
39 |
40 | strict_front_matter: true
41 |
42 | # Exclude from processing.
43 | # The following items will not be processed, by default.
44 | # Any item listed under the `exclude:` key here will be automatically added to
45 | # the internal "default list".
46 | #
47 | # Excluded items can be processed by explicitly listing the directories or
48 | # their entries' file path in the `include:` list.
49 | #
50 | # exclude:
51 | # - .sass-cache/
52 | # - .jekyll-cache/
53 | # - gemfiles/
54 | # - Gemfile
55 | # - Gemfile.lock
56 | # - node_modules/
57 | # - vendor/bundle/
58 | # - vendor/cache/
59 | # - vendor/gems/
60 | # - vendor/ruby/
61 |
62 | compress_html:
63 | blanklines: true
64 |
--------------------------------------------------------------------------------
/docs/_data/buttons.yml:
--------------------------------------------------------------------------------
1 | frontBack: 'Frontside, q, {"side": "front"}; Backside, a, {"side": "back"}'
2 |
3 | twoCards: 'Frontside 1, q1, {side: "front",cardNumber: 1}; Backside 1, a1, {side: "back",cardNumber: 1}; Frontside 2, q2, {side: "front",cardNumber: 2}; Backside 2, a2, {side: "back",cardNumber: 2}'
4 | threeCards: 'Front 1, q1, {side: "front",cardNumber: 1}; Back 1, a1, {side: "back",cardNumber: 1}; Front 2, q2, {side: "front",cardNumber: 2}; Back 2, a2, {side: "back",cardNumber: 2}; Front 3, q3, {side: "front",cardNumber: 3}; Back 3, a3, {side: "back",cardNumber: 3}'
5 |
6 | fourCards: 'F1, q1, {side: "front",cardNumber: 1}; B1, a1, {side: "back",cardNumber: 1}; F2, q2, {side: "front",cardNumber: 2}; B2, a2, {side: "back",cardNumber: 2}; F3, q3, {side: "front",cardNumber: 3}; B3, a3, {side: "back",cardNumber: 3}; F4, q4, {side: "front",cardNumber: 4}; B4, a4, {side: "back",cardNumber: 4}'
7 | fiveCards: 'F1, q1, {side: "front",cardNumber: 1}; B1, a1, {side: "back",cardNumber: 1}; F2, q2, {side: "front",cardNumber: 2}; B2, a2, {side: "back",cardNumber: 2}; F3, q3, {side: "front",cardNumber: 3}; B3, a3, {side: "back",cardNumber: 3}; F4, q4, {side: "front",cardNumber: 4}; B4, a4, {side: "back",cardNumber: 4}; F5, q5, {side: "front",cardNumber: 5}; B5, a5, {side: "back",cardNumber: 5}'
8 | sixCards: 'F1, q1, {side: "front",cardNumber: 1}; B1, a1, {side: "back",cardNumber: 1}; F2, q2, {side: "front",cardNumber: 2}; B2, a2, {side: "back",cardNumber: 2}; F3, q3, {side: "front",cardNumber: 3}; B3, a3, {side: "back",cardNumber: 3}; F4, q4, {side: "front",cardNumber: 4}; B4, a4, {side: "back",cardNumber: 4}; F5, q5, {side: "front",cardNumber: 5}; B5, a5, {side: "back",cardNumber: 5}; F6, q6, {side: "front",cardNumber: 6}; B6, a6, {side: "back",cardNumber: 6}'
9 |
--------------------------------------------------------------------------------
/docs/_data/setups/assign_categories.yml:
--------------------------------------------------------------------------------
1 | name: Assign categories
2 | code: |
3 | const baseStylizer = closet.Stylizer.make({
4 | separator: '・',
5 | processor: (v) => `《${v}》`,
6 | })
7 |
8 | const colorMapper = (v, _i, cat) => {
9 | if (v === '') {
10 | return []
11 | }
12 |
13 | const theColor = cat === 0
14 | ? 'yellow'
15 | : cat === 1
16 | ? 'darkseagreen'
17 | : 'indianred'
18 |
19 | return `${v} `
22 | }
23 |
24 | const theBackStylizer = baseStylizer.toStylizer({
25 | mapper: colorMapper,
26 | })
27 |
28 | const theFrontStylizer = baseStylizer.toStylizer({
29 | mapper: (v) => v === '' ? [] : `${v} `,
30 | })
31 |
32 | const theContexter = (tag) => {
33 | const flattedValues = tag.values.flatMap((vs, i) => vs.map(v => [v, i]))
34 | return flattedValues.map((v, i) => colorMapper(v[0], i, v[1]))
35 | }
36 |
37 | const assignCategorySettings = {
38 | backStylizer: theBackStylizer,
39 | frontStylizer: theFrontStylizer,
40 |
41 | inactiveStylizer: baseStylizer,
42 | contexter: theContexter,
43 | }
44 |
45 | filterManager.install(
46 | closet.flashcard.recipes.multipleChoice({
47 | tagname: 'mc',
48 | ...assignCategorySettings,
49 | }),
50 | )
51 |
--------------------------------------------------------------------------------
/docs/_data/setups/blanking_cloze.yml:
--------------------------------------------------------------------------------
1 | name: Blanking Clozes
2 | code: |
3 | const blank = function(tag) {
4 | return [tag.values[0].replace(closet.unicodeAlphanumericPattern, '_')]
5 | }
6 |
7 | const blankOptions = {
8 | frontEllipser: blank,
9 | frontStylizer: closet.Stylizer.make({
10 | process: v => `${v} `,
11 | separator: '',
12 | }),
13 | }
14 |
15 | filterManager.install(
16 | closet.flashcard.recipes.cloze({
17 | tagname: 'c',
18 | ...blankOptions,
19 | }),
20 | )
21 |
--------------------------------------------------------------------------------
/docs/_data/setups/click_to_reveal_cloze.yml:
--------------------------------------------------------------------------------
1 | name: Click to reveal cloze
2 | code: |
3 | closet.browser.appendStyleTag(`
4 | .cl--obscure-hint {
5 | filter: blur(0.25em);
6 | }
7 |
8 | .cl--obscure-fix::after {
9 | content: 'XXXXXXXXXX';
10 | filter: blur(0.25em);
11 | }
12 |
13 | .cl--obscure-fix > span {
14 | display: none;
15 | }`)
16 |
17 | const removeObscure = function(event) {
18 | if (event.currentTarget.classList.contains('cl--obscure-clickable')) {
19 | event.currentTarget.classList.remove('cl--obscure')
20 | event.currentTarget.classList.remove('cl--obscure-hint')
21 | event.currentTarget.classList.remove('cl--obscure-fix')
22 | }
23 | }
24 |
25 | const wrappedCloze= closet.wrappers.aftermath(closet.flashcard.recipes.cloze, () => {
26 | document.querySelectorAll('.cl--obscure')
27 | .forEach(tag => {
28 | tag.addEventListener('click', removeObscure, {
29 | once: true,
30 | })
31 | })
32 | })
33 |
34 | const obscureAndClick = (t) => {
35 | return [`${t.values[0]} `]
36 | }
37 |
38 | const obscureAndClickFix = (t) => {
39 | return [`${t.values[0]} `]
40 | }
41 |
42 | const frontStylizer = closet.Stylizer.make({
43 | processor: v => `${v} `,
44 | })
45 |
46 | filterManager.install(
47 | wrappedCloze({
48 | tagname: 'c',
49 | frontEllipser: obscureAndClick,
50 | frontStylizer: frontStylizer,
51 | }),
52 |
53 | wrappedCloze({
54 | tagname: 'cx',
55 | frontEllipser: obscureAndClickFix,
56 | frontStylizer: frontStylizer,
57 | }),
58 | )
59 |
--------------------------------------------------------------------------------
/docs/_data/setups/debug.yml:
--------------------------------------------------------------------------------
1 | name: Debug features
2 | code: |
3 | filterManager.install(
4 | closet.recipes.debug(),
5 | closet.recipes.define({ tagname: 'def' }),
6 | closet.flashcard.recipes.specification.hide({ tagname: 'spec' }),
7 | )
8 |
9 | ## Example of new syntax
10 | # name: Debug features
11 | # documentation: |
12 | # Test
13 | # versions:
14 | # - name: v1
15 | # note: This is the note
16 | # support:
17 | # from: v0.0.0
18 | # code: |
19 | # filterManager.install(
20 | # closet.recipes.debug(),
21 | # closet.recipes.define({ tagname: 'def' }),
22 | # closet.recipes.specification.hide({ tagname: 'spec' }),
23 | # )
24 |
--------------------------------------------------------------------------------
/docs/_data/setups/default_cloze.yml:
--------------------------------------------------------------------------------
1 | name: "Default cloze"
2 | code: |
3 | filterManager.install(closet.flashcard.recipes.cloze({
4 | tagname: 'c',
5 | }))
6 |
--------------------------------------------------------------------------------
/docs/_data/setups/default_multiple_choice.yml:
--------------------------------------------------------------------------------
1 | name: Default Multiple Choice
2 | code: |
3 | filterManager.install(
4 | closet.flashcard.recipes.multipleChoice({ tagname: 'mc' }),
5 | )
6 |
--------------------------------------------------------------------------------
/docs/_data/setups/default_shuffle.yml:
--------------------------------------------------------------------------------
1 | name: "Default Shuffle"
2 | code: |
3 | filterManager.install(closet.recipes.shuffle({ tagname: 'mix' }))
4 |
--------------------------------------------------------------------------------
/docs/_data/setups/defining_lists.yml:
--------------------------------------------------------------------------------
1 | name: Definining lists
2 | code: |
3 | filterManager.install(
4 | closet.recipes.setList({ tagname: 'setl' }),
5 | )
6 |
--------------------------------------------------------------------------------
/docs/_data/setups/delims.yml:
--------------------------------------------------------------------------------
1 | name: Setting delimiters
2 | code: |
3 | filterManager.install(
4 | closet.recipes.delimiter(),
5 | )
6 |
--------------------------------------------------------------------------------
/docs/_data/setups/fancy_shuffle.yml:
--------------------------------------------------------------------------------
1 | name: Fancy shuffle
2 | code: |
3 | const colorWheel = function*() {
4 | while (true) {
5 | yield 'pink'
6 | yield 'lime'
7 | yield 'yellow'
8 | }
9 | }
10 |
11 | const cw = colorWheel()
12 | cw.next()
13 |
14 | const colorfulWithMiddleDot = closet.Stylizer.make({
15 | separator: '・',
16 | mapper: v => (
17 | `${v} `
18 | ),
19 | processor: v => `〈${v}〉`,
20 | })
21 |
22 | const withMiddleDot = closet.Stylizer.make({
23 | separator: '・',
24 | })
25 |
26 | const withSlash = closet.Stylizer.make({
27 | separator: ' / ',
28 | })
29 |
30 | const withVs = closet.Stylizer.make({
31 | separator: ' vs ',
32 | mapper: (v, i) => `${i}: ${v}`
33 | })
34 |
35 | filterManager.install(
36 | closet.recipes.shuffle({
37 | tagname: 'cmix',
38 | stylizer: colorfulWithMiddleDot,
39 | }),
40 |
41 | closet.recipes.shuffle({
42 | tagname: 'amix',
43 | stylizer: withMiddleDot,
44 | }),
45 |
46 | closet.recipes.shuffle({
47 | tagname: 'vmix',
48 | stylizer: withVs,
49 | }),
50 |
51 | closet.recipes.shuffle({
52 | tagname: 'mix',
53 | stylizer: withSlash
54 | }),
55 | )
56 |
--------------------------------------------------------------------------------
/docs/_data/setups/flashcard.yml:
--------------------------------------------------------------------------------
1 | name: "Flashcard features"
2 | code: |
3 | filterManager.install(
4 | closet.recipes.activate({ tagname: 'on', storeId: 'flashcardActive' }),
5 | closet.recipes.deactivate({ tagname: 'off', storeId: 'flashcardActive' }),
6 |
7 | closet.wrappers.product(closet.recipes.setNumber, closet.recipes.setNumber)({
8 | tagname: 'around',
9 | optionsFirst: { storeId: 'flashcardActiveTop' },
10 | optionsSecond: { storeId: 'flashcardActiveBottom' },
11 | }),
12 |
13 | closet.recipes.setNumber({ tagname: 'up', storeId: 'flashcardActiveTop' }),
14 | closet.recipes.setNumber({ tagname: 'down', storeId: 'flashcardActiveBottom' }),
15 |
16 | closet.recipes.activate({ tagname: 'show', storeId: 'flashcardShow' }),
17 | closet.recipes.activate({ tagname: 'hide', storeId: 'flashcardHide' }),
18 |
19 | closet.recipes.setNumber({ tagname: 'top', storeId: 'flashcardShowTop' }),
20 | closet.recipes.setNumber({ tagname: 'bottom', storeId: 'flashcardShowBottom' }),
21 |
22 | closet.wrappers.product(closet.recipes.setNumber, closet.recipes.setNumber)({
23 | tagname: 'ctxt',
24 | optionsFirst: { storeId: 'flashcardShowTop' },
25 | optionsSecond: { storeId: 'flashcardShowBottom' },
26 | }),
27 | )
28 |
--------------------------------------------------------------------------------
/docs/_data/setups/generate.yml:
--------------------------------------------------------------------------------
1 | name: Generators
2 | code: |
3 | filterManager.install(
4 | closet.recipes.generateInteger({ tagname: 'gen' }),
5 | closet.recipes.generateReal({ tagname: 'genr' }),
6 | )
7 |
--------------------------------------------------------------------------------
/docs/_data/setups/obscuring_cloze.yml:
--------------------------------------------------------------------------------
1 | name: Obscuring clozes
2 | code: |
3 | const firstValue = (tag) => [tag.values[0]]
4 | const obscureYellow = closet.Stylizer.make({
5 | processor: v => `${v} `,
6 | })
7 |
8 | const obscureOptions = {
9 | frontEllipser: firstValue,
10 | frontStylizer: obscureYellow,
11 | }
12 |
13 | filterManager.install(
14 | closet.flashcard.recipes.cloze({
15 | tagname: 'c',
16 | ...obscureOptions,
17 | }),
18 | )
19 |
--------------------------------------------------------------------------------
/docs/_data/setups/occlusions.yml:
--------------------------------------------------------------------------------
1 | name: Make occlusions
2 | code: |
3 | globalThis.target = closet.browser.recipes.occlusionEditor()(filterManager.registrar)
4 |
5 | filterManager.install(
6 | closet.browser.recipes.rect({ tagname: 'rect' }),
7 | )
8 |
--------------------------------------------------------------------------------
/docs/_data/setups/occlusions_highlight.yml:
--------------------------------------------------------------------------------
1 | name: Occlusions with highlighting border
2 | code: |
3 | const backProperties = {
4 | fill: 'transparent',
5 | strokeWidth: '2px',
6 | stroke: 'red',
7 | }
8 |
9 | filterManager.install(
10 | closet.browser.recipes.rect({ tagname: 'rect', backProperties }),
11 | )
12 |
--------------------------------------------------------------------------------
/docs/_data/setups/order.yml:
--------------------------------------------------------------------------------
1 | name: Ordering
2 | code: |
3 | filterManager.install(closet.recipes.order({ tagname: 'ord' }))
4 |
--------------------------------------------------------------------------------
/docs/_data/setups/pick_eval.yml:
--------------------------------------------------------------------------------
1 | name: Picking and Evaluation
2 | code: |
3 | filterManager.install(
4 | closet.recipes.pick({ tagname: 'pick' }),
5 | closet.recipes.pickIndex({ tagname: 'pi' }),
6 | closet.recipes.pickCardNumber({ tagname: 'sel' }),
7 | )
8 |
--------------------------------------------------------------------------------
/docs/_data/setups/pick_eval_padding.yml:
--------------------------------------------------------------------------------
1 | name: Picking and Evaluation with padding
2 | code: |
3 | const postprocess = (value, valueList) => (tag) => {
4 | return value.padEnd(valueList.reduce((accu, value) => Math.max(accu, value.length), 0), ' ')
5 | }
6 |
7 | filterManager.install(
8 | closet.recipes.pick({ tagname: 'ppick', postprocess }),
9 | closet.recipes.pickIndex({ tagname: 'ppi', postprocess }),
10 | closet.recipes.pickCardNumber({ tagname: 'psel', postprocess }),
11 | )
12 |
--------------------------------------------------------------------------------
/docs/_data/setups/shuffle_quest.yml:
--------------------------------------------------------------------------------
1 | name: "Shuffle Quest"
2 | code: |
3 | filterManager.install(
4 | closet.recipes.mingle.show({ tagname: 'shuf' }),
5 | closet.recipes.sort.show({ tagname: 'sort' }),
6 | closet.recipes.jumble.show({ tagname: 'jumb' }),
7 | )
8 |
--------------------------------------------------------------------------------
/docs/_data/setups/templated_shuffle.yml:
--------------------------------------------------------------------------------
1 | name: Shuffling lines / list items
2 | code: |
3 | const sequencer = closet.sequencers.acrossNumberedCustom("mix")
4 |
5 | const brOptions = {
6 | tagname: 'mixbr',
7 | optics: [
8 | closet.template.optics.separated({ sep: " " }),
9 | ],
10 | evaluate: (stylizer, values) => () => stylizer.toStylizer({ separator: " " }).stylize(values),
11 | sequencer,
12 | }
13 |
14 | const liOptions = {
15 | tagname: 'mixli',
16 | optics: [
17 | closet.template.optics.templated({ before: "", after: " " }),
18 | ],
19 | evaluate: (stylizer, values) => (tag) => tag.traverse(_ => values),
20 | sequencer,
21 | }
22 |
23 | filterManager.install(
24 | closet.recipes.shuffle(brOptions),
25 | closet.recipes.shuffle(liOptions),
26 | )
27 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/activate_cloze.yml:
--------------------------------------------------------------------------------
1 | name: Activate clozes
2 | code: |
3 | The four major dopaminergic pathways:
4 | * [[c1::Mesolimbic pathway::starts with M[[on::c4;c5;c6]]]]
5 | * [[c2::Mesocortical pathway]]
6 | * [[c3::Nigostriatal pathway::starts with N]]
7 | * [[c4::Tuberoinfundibular pathway]]
8 | * [[c5::Hypothalamospinal projection]]
9 | * [[c6::Incertohypothalamic pathway]]
10 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/activate_cloze_conflict.yml:
--------------------------------------------------------------------------------
1 | name: "Activate clozes with conflict"
2 | code: |
3 | The four major dopaminergic pathways:
4 | * [[c1::Mesolimbic pathway::starts with M[[on::c1:0;c2:0;c3:0]]]]
5 | * [[c1::Mesocortical pathway[[off::c1:0;c2:0]]]]
6 | * [[c2::Nigostriatal pathway::starts with N]]
7 | * [[c2::Tuberoinfundibular pathway]]
8 | * [[c3::Hypothalamospinal projection]]
9 | * [[c3::Incertohypothalamic pathway]]
10 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/activate_cloze_with_occur.yml:
--------------------------------------------------------------------------------
1 | name: "Activate clozes with occur"
2 | code: |
3 | The four major dopaminergic pathways:
4 | * [[c1::Mesolimbic pathway::starts with M[[on::c1:0;c2:0;c3:0]]]]
5 | * [[c1::Mesocortical pathway]]
6 | * [[c2::Nigostriatal pathway::starts with N]]
7 | * [[c2::Tuberoinfundibular pathway]]
8 | * [[c3::Hypothalamospinal projection]]
9 | * [[c3::Incertohypothalamic pathway]]
10 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/click_to_reveal.yml:
--------------------------------------------------------------------------------
1 | name: Click to reveal cloze
2 | code: |
3 | The four major dopaminergic pathways:
4 | * [[c1::Mesolimbic pathway]]
5 | * [[c1::Mesocortical pathway]]
6 | * [[c2::Nigostriatal pathway]]
7 | * [[cx2::Tuberoinfundibular pathway]]
8 | * [[cx3::Hypothalamospinal projection]]
9 | * [[cx3::Incertohypothalamic pathway]]
10 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/click_to_reveal_single.yml:
--------------------------------------------------------------------------------
1 | name: Click to reveal cloze single
2 | code: |
3 | The four major dopaminergic pathways:
4 | * Mesol[[c0::imbic pathway]]
5 | * Mesoc[[c0::ortical pathway]]
6 | * Ni[[c0::gostriatal pathway]]
7 | * Tub[[c0::eroinfundibular pathway]]
8 | * Hy[[c0::pothalamospinal projection]]
9 | * In[[c0::certohypothalamic pathway]]
10 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/first_example.yml:
--------------------------------------------------------------------------------
1 | name: "First example of clozes"
2 | code: |
3 | The capital of Botswana is [[c::Gaborone]].
4 | The capital of Argentina is [[c::Buenos Aires]].
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/hide_context.yml:
--------------------------------------------------------------------------------
1 | name: "Hide context without card types"
2 | code: |
3 | The DNA forms a structure called a [[c0::double helix]].
4 |
5 | [[cr::In molecular biology, the term "double helix" refers to the structure formed by double-stranded molecules of nucleic acids such as DNA.
6 | The double helical structure of a nucleic acid complex arises as a consequence of its secondary structure, and is a fundamental component in determining its tertiary structure.]]
7 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/hiding_cloze.yml:
--------------------------------------------------------------------------------
1 | name: "Hiding and Revealing clozes"
2 | code: |
3 | Catecholamines: [[c1::dopamine, noradrenaline, adrenaline]]
4 | Indolamines: [[ch2::serotonin]]
5 | Imidazoles: [[cr3::histamin]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/hiding_cloze_symbols.yml:
--------------------------------------------------------------------------------
1 | name: Hiding cloze in different languages
2 | code: |
3 | イミダゾール:[[c1::ドーパミン・アドレナリン・ノルアドレナリン]]
4 | 咪唑:[[ch2::多巴胺/肾上腺素/去甲肾上腺素]]
5 | JavaScript expression: [[cr3::const value = f(3 + 5);]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/hints.yml:
--------------------------------------------------------------------------------
1 | name: Using hints
2 | code: |
3 | Catecholamines: [[c1::dopamine, noradrenaline, adrenaline::three of them]]
4 | Indolamines: [[ch2::serotonin::starts with s]]
5 | Imidazoles: [[cr3::histamin::h...]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/numbered_cloze.yml:
--------------------------------------------------------------------------------
1 | name: "Numbered clozes"
2 | code: |
3 | The capital of Botswana is [[c1::Gaborone]].
4 | The capital of Argentina is [[c2::Buenos Aires]].
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/cloze/zero_cloze.yml:
--------------------------------------------------------------------------------
1 | name: "Zero clozes"
2 | code: |
3 | The capital of Botswana is [[c0::Gaborone]].
4 | # contrast this with the behavior of unnumbered clozes:
5 | The capital of Argentina is [[c::Buenos Aires]].
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/flashcard/around_ctxt.yml:
--------------------------------------------------------------------------------
1 | name: Show context with clozes
2 | code: |
3 | [[cr1::Shall I compare thee to a summer’s day?[[ctxt::cr*=2]][[up::cr*=1]]]]
4 | [[cr2::Thou art more lovely and more temperate.]]
5 | [[cr3::Rough winds do shake the darling buds of May,]]
6 | [[cr4::And summer’s lease hath all too short a date.]]
7 | [[cr5::Sometime too hot the eye of heaven shines,]]
8 | [[cr6::And often is his gold complexion dimmed;]]
9 | [[cr7::And every fair from fair sometime declines,]]
10 | [[cr8::By chance, or nature’s changing course, untrimmed;]]
11 | [[cr9::But thy eternal summer shall not fade,]]
12 | [[cr10::Nor lose possession of that fair thou ow’st,]]
13 | [[cr11::Nor shall death brag thou wand’rest in his shade,]]
14 | [[cr12::When in eternal lines to Time thou grow’st.]]
15 | [[cr13::So long as men can breathe, or eyes can see,]]
16 | [[cr14::So long lives this, and this gives life to thee.]]
17 |
18 | -- William Shakespeare, "Shall I Compare Thee To A Summer's Day?"
19 |
--------------------------------------------------------------------------------
/docs/_data/snippets/flashcard/around_range.yml:
--------------------------------------------------------------------------------
1 | name: Around Range
2 | code: |
3 | [[c1::Mesolimbic pathway::starts with M[[around::c*=1]]]]
4 | [[c2::Mesocortical pathway]]
5 | [[c3::Nigostriatal pathway::starts with N]]
6 | [[c4::Tuberoinfundibular pathway]]
7 | [[c5::Hypothalamospinal projection]]
8 | [[c6::Incertohypothalamic pathway]]
9 |
--------------------------------------------------------------------------------
/docs/_data/snippets/flashcard/top_bottom_range.yml:
--------------------------------------------------------------------------------
1 | name: Top and Bottom Range
2 | code: |
3 | [[c1::Mesolimbic pathway::starts with M[[up::c1=1,c2=1,c3=1]][[down::c4=1,c5=1,c6=1]]]]
4 | [[c2::Mesocortical pathway]]
5 | [[c3::Nigostriatal pathway::starts with N]]
6 | [[c4::Tuberoinfundibular pathway]]
7 | [[c5::Hypothalamospinal projection]]
8 | [[c6::Incertohypothalamic pathway]]
9 |
--------------------------------------------------------------------------------
/docs/_data/snippets/generation/picking.yml:
--------------------------------------------------------------------------------
1 | name: Simple picking
2 | code: |
3 | My favorite fruit is [[pick::fruits[[setl::fruits::apple||banana||grapefruit]]]]
4 | Your favorite fruit is [[pick::fruits]]
5 | Her favorite fruit is [[pick::fruits]]
6 | His favorite fruit is [[pick::fruits]]
7 |
--------------------------------------------------------------------------------
/docs/_data/snippets/generation/picking_index.yml:
--------------------------------------------------------------------------------
1 | name: Picking with indices
2 | code: |
3 | Peter's favorite fruit is [[pi::fruits[[setl::fruits::apple||banana||grapefruit]]]].
4 | Susan's favorite fruit is [[pi2::fruits]].
5 | Daniel's favorite fruit is [[pi1::fruits]].
6 | Alan's favorite fruit is [[pi1::fruits]], as well.
7 |
--------------------------------------------------------------------------------
/docs/_data/snippets/generation/picking_padding.yml:
--------------------------------------------------------------------------------
1 | name: Picking with additional padding
2 | code: |
3 | Numbers in a matrix ([[ppi::numbers[[setl::numbers::13553||-42||3.4||-2213498]]]], [[ppi1::numbers]])
4 | ([[ppi2::numbers]], [[ppi3::numbers]])
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/generation/picking_template.yml:
--------------------------------------------------------------------------------
1 | name: Picking as a template
2 | code: |
3 | [[pi::v]] + [[pi1::v]] = [[pi2::v]]
8 |
--------------------------------------------------------------------------------
/docs/_data/snippets/generation/picking_unique.yml:
--------------------------------------------------------------------------------
1 | name: Picking uniquely
2 | code: |
3 | Peter's favorite fruit is [[pick1::fruits[[setl::fruits::apple||banana||grapefruit]]]]
4 | Paul's favorite fruit is [[pick1::fruits]]
5 | Patty's favorite fruit is [[pick1::fruits]]
6 |
7 | Susan's favorite fruit is [[pick2::fruits]]
8 | Saul's favorite fruit is [[pick2::fruits]]
9 | Sam's favorite fruit is [[pick2::fruits]]
10 | Sandra's favorite fruit is [[pick2::fruits]]
11 |
--------------------------------------------------------------------------------
/docs/_data/snippets/generation/simple.yml:
--------------------------------------------------------------------------------
1 | name: Number generation
2 | code: |
3 | Number between 0 and 10: [[gen::0,10]]
4 | Real number between 0 and 10: [[genr::0,10]]
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/home/intro_example.yml:
--------------------------------------------------------------------------------
1 | name: How to use the web site example
2 | code: |
3 | Closet is a [[c0::template engine]].
4 |
--------------------------------------------------------------------------------
/docs/_data/snippets/multiple_choice/animals.yml:
--------------------------------------------------------------------------------
1 | name: Animals
2 | code: |
3 | Legend: bees (yellow with black); crocodiles (cyan); cats (red).
4 | [[mc1::Anthophila::::Panthera]]
5 | [[mch2::Colletidae::Mecistops::Catopuma]]
6 | [[mcr3::Megachiidae::Osteolaemus::Prionailurus]]
7 |
--------------------------------------------------------------------------------
/docs/_data/snippets/multiple_choice/formal_questions.yml:
--------------------------------------------------------------------------------
1 | name: Formal questions
2 | code: |
3 | Which of the following are valid HTML input types?
4 | [[mc1::checkbox||color::radiobutton]]
5 |
6 | Which of these is a true statement about the parietal bone?
7 | [[mc2::each bone is roughly quadrilateral in form]]
8 | [[mc2::::has two surfaces, four borders, and six angles]]
9 | [[mc2::::it is named from the Latin paries (-ietis), rock]]
10 |
11 | Which one of these is NOT a part of the brain?
12 | [[mc3::Dendritic spine::Ventral horn||Basal ganglia||Prefrontal cortex]]
13 |
--------------------------------------------------------------------------------
/docs/_data/snippets/multiple_choice/latin_proverbs.yml:
--------------------------------------------------------------------------------
1 | name: Latin proverbs
2 | code: |
3 | Vestis virum reddit: [[mc1::The clothes make the man::Learn from your mistake||What goes around comes arround]]
4 | Hostium munera, non munera: [[mch2::Know who your friends are::Nothing comes from nothing||All that is fair must fade]]
5 | In dubio abstine: [[mch2::If you're unsure what to do, do nothing::Dare to be wise||Empty pots make the most noise]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/multiple_choice/simple_nonzero.yml:
--------------------------------------------------------------------------------
1 | name: Simple Multiple Choice example with indices
2 | code: |
3 | The capital of Italy: [[mc1::Rome::Berlin||Paris]]
4 | The capital of Germany: [[mc2::Berlin::Rome||Paris]]
5 | The capital of France: [[mc3::Paris::Rome||Berlin]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/multiple_choice/simple_zero.yml:
--------------------------------------------------------------------------------
1 | name: Simple Multiple Choice example
2 | code: |
3 | The capital of Italy: [[mc0::Rome::Berlin||Paris]]
4 |
--------------------------------------------------------------------------------
/docs/_data/snippets/occlusions/bones.yml:
--------------------------------------------------------------------------------
1 | name: Bones occlusion example
2 | code: |
3 |
4 |
10 |
--------------------------------------------------------------------------------
/docs/_data/snippets/occlusions/bones_with_other.yml:
--------------------------------------------------------------------------------
1 | name: Bones occlusion with shuffling
2 | code: |
3 |
4 |
12 | [[mix1::[[c1::Frontal bone]]: vertically oriented [[c1::squamous]] part, and the horizontally oriented [[c1::orbital]] part.]]
13 | [[mix1::[[c2::Sphenoid bone]]: in front of the [[c2::basilar]] part of the [[c2::occipital]] bone]]
14 | [[mix1::[[c3::Ethmoid bone]]: separates the [[c3::nasal cavity]] from the brain.]]
15 | [[mix1::[[c4::Parietal bone]]: each bone is roughly [[c4::quadrilateral]] in form]]
16 | [[mix1::[[c5::Temporal bone]]: situated at the sides and base of the skull, and lateral to the [[c5::temporal lobes]] of the [[c5::cerebral cortex]].]]
17 | [[mix1::[[c6::Occipital bone]]: is [[c6::trapezoidal]] in shape.]]
18 |
--------------------------------------------------------------------------------
/docs/_data/snippets/occlusions/bones_with_other2.yml:
--------------------------------------------------------------------------------
1 | name: Bones occlusion with picking
2 | code: |
3 |
4 |
19 | [[sel]]
20 |
--------------------------------------------------------------------------------
/docs/_data/snippets/occlusions/cell.yml:
--------------------------------------------------------------------------------
1 | name: Cell occlusion example
2 | code: |
3 |
4 |
12 |
--------------------------------------------------------------------------------
/docs/_data/snippets/occlusions/cell_flashcard.yml:
--------------------------------------------------------------------------------
1 | name: Cell occlusion example with flashcard features
2 | code: |
3 |
4 |
11 |
--------------------------------------------------------------------------------
/docs/_data/snippets/occlusions/create.yml:
--------------------------------------------------------------------------------
1 | name: Create occlusions
2 | code: |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/occlusions/formulas.yml:
--------------------------------------------------------------------------------
1 | name: Formulas for without context
2 | code: |
3 | What is the [[pi::q]]?
8 | [[c0:: ]]
9 |
14 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/assign_shuffle.yml:
--------------------------------------------------------------------------------
1 | name: Assign shuffles
2 | code: |
3 | Shades of [[sort1::red]]: [[sort2::pink::maroon::burgundy::fuchsia]]
4 | Shades of [[sort1::green]]: [[sort2::teal::olive::mint::lime]]
5 | Shades of [[sort1::blue]]: [[sort2::sapphire::savoy::ultramarine]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/assign_shuffle_ol.yml:
--------------------------------------------------------------------------------
1 | name: Assign shuffles in ordered list
2 | code: |
3 | Step [[jumb4::1]]: [[shuf4::Combine the onion, celery, carrot, and [[mc3::olive::canola||sunflower]] oil[[ord::shuf4,jumb4]]]].
4 | Step [[jumb4::2]]: [[shuf4::Add the meat and cook, stirring frequently]].
5 | Step [[jumb4::3]]: [[shuf4::Add the [[c1::tomato]] paste]].
6 | Step [[jumb4::4]]: [[shuf4::Cook on low for [[c2::8::more than 4]] hours]].
7 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/deadlock.yml:
--------------------------------------------------------------------------------
1 | name: "Deadlock shuffle"
2 | code: |
3 | [[mix2::I [[mix3::will [[mix1::a||b||c]]]]]]]]
4 | [[mix3::never [[mix2::resolve [[mix1::x||y||z]]]]]]
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/first_example.yml:
--------------------------------------------------------------------------------
1 | name: "First example of shuffling"
2 | code: |
3 | Capitals of Europe: [[mix::Paris||Rome||Berlin||London]]
4 | Capitals of Asia: [[mix::New Delhi||Beijing||Seoul]]
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/individual_items.yml:
--------------------------------------------------------------------------------
1 | name: "Order individual items"
2 | code: |
3 | Capital of [[mix1::India[[ord::mix1,mix2]]]]: [[mix2::New Delhi]]
4 | Capital of [[mix1::China]]: [[mix2::Beijing]]
5 | Population of [[mix3::South Korea[[ord::mix3,mix4]]]]: [[mix4::51,709,098]]
6 | Population of [[mix3::Kazakhstan]]: [[mix4::18,448,600]]
7 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/inline_vs_list.yml:
--------------------------------------------------------------------------------
1 | name: "Order inline items with lists"
2 | code: |
3 | [[mix1::Russell's sign||Granulosa cell tumor[[ord::mix1,mix2]]]]
4 | - [[mix2::indirect sign of bulimia nervosa or anorexia nervosa]]
5 | - [[mix2::ovarian tumor associated with estrogen secretion]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/japanese.yml:
--------------------------------------------------------------------------------
1 | name: "Shuffling Japanese"
2 | code: |
3 | すべての僕の[[cmix::想い||心||悲しみ||幸せ]]は…
4 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/lines.yml:
--------------------------------------------------------------------------------
1 | name: Shuffle lines
2 | code: |
3 | [[#mixbr]]
4 | Line 1
5 | Line 2
6 | Line 3
7 | [[/mixbr]]
8 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/list_items.yml:
--------------------------------------------------------------------------------
1 | name: Shuffle list items
2 | code: |
3 | [[#mixli]]
4 | This is a list with three items:
5 | List item 1 List item 2 List item 3
6 | [[/]]
7 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/mixed_styles.yml:
--------------------------------------------------------------------------------
1 | name: "Shuffling with mixed styles"
2 | code: |
3 | Shuffling with centered dot: [[amix1::Susan||Joanne||Lisa]]
4 | Shuffling with slashes: [[mix1::Peter||John||Mark]]
5 | Shuffling with centered dot again: [[amix1::Robin||Paula]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/nesting.yml:
--------------------------------------------------------------------------------
1 | name: "Nested shuffling"
2 | code: |
3 | [[mix1::The continent of Asia has the following countries: [[mix::Singapore||China||Kazakhstan||Pakistan||India]]]]
4 | [[mix1::The continent of Africa has the following countries: [[mix::Ghana||Senegal||Ethiopia||Egypt]]]]
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/non_contiguous.yml:
--------------------------------------------------------------------------------
1 | name: "Non-contiguous shuffling"
2 | code: |
3 | [[mix1::A soldier’s a man]]
4 | [[mix1::A life’s but a span]]
5 | [[mix1::Why then let a soldier drink.]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/on_extra_line.yml:
--------------------------------------------------------------------------------
1 | name: Order on extra line
2 | code: |
3 | Countries of Asia: [[mix1::India||China||South Korea||Kazakhstan]]
4 | Capitals of Asia: [[mix2::New Delhi||Beijing||Seoul||Nur-Sultan]]
5 | [[ord::mix1,mix2]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/onesided_nesting.yml:
--------------------------------------------------------------------------------
1 | name: "Shuffling with one-sided nesting"
2 | code: |
3 | [[mix1::[[mix2::[[mix3::I am nested very deeply]]]]]]
4 | [[mix1::I am nested with only one level]]
5 |
--------------------------------------------------------------------------------
/docs/_data/snippets/shuffling/preserve_item_count.yml:
--------------------------------------------------------------------------------
1 | name: "Shuffling preserves item count"
2 | code: |
3 | Reddish colors: [[mix1::magenta||pink]]
4 | Blueish colors: [[mix1::indigo||cyan||teal]]
5 | Greenish colors: [[mix1::lime]]
6 |
--------------------------------------------------------------------------------
/docs/_data/snippets/stylizing/mandarin.yml:
--------------------------------------------------------------------------------
1 | name: Mandarin with tones
2 | code: |
3 | 动物; 警察; 文化; 鲸鱼; 姐姐
4 |
5 | [[tf::dongwu ]] [[tf::jing cha ]] [[tf::wen hua ]] [[tf::jing yu ]] [[tf::jing yu ]] [[tf::jie jie ]]
6 |
7 | [[ts::dong4wu4]] [[ts::jing3cha2]] [[ts::wen2hua4]] [[ts::jing1yu2]] [[ts::jie3jie5]]
8 |
9 | [[bpmf::dong4wu4]] [[bpmf::jing3cha2]] [[bpmf::wen2hua4]] [[bpmf::jing1yu2]] [[bpmf::jie3jie5]]
10 |
--------------------------------------------------------------------------------
/docs/_includes/codeDisplay.md:
--------------------------------------------------------------------------------
1 | {% assign theButtons = include.buttons | split: "; " %}
2 |
3 | {% assign contentId = include.content.name | slugify %}
4 |
5 | {% assign theSetups = include.setups | split: ',' %}
6 |
7 | {% assign setupIds = '' | split: '' %}
8 | {% for setup in theSetups %}
9 | {% assign slugifiedName = setup | slugify %}
10 | {% assign setupIds = setupIds | push: slugifiedName %}
11 | {% endfor %}
12 |
13 | {% assign setupId = theSetups | join: '-and-' %}
14 | {% assign setupString = setupIds | join: ',' %}
15 |
16 | {% assign theId = contentId | append: "-with-" | append: setupId %}
17 |
18 | {% assign fmCode = '' | split: '' %}
19 |
20 | {% capture newLine %}
21 | {% endcapture %}
22 |
23 | {% for setup in site.data.setups %}
24 | {% if theSetups contains setup[0] %}
25 | {% assign codeSnippetName = "/** " | append: setup[1].name | append: " */" %}
26 | {% assign code = setup[1].code %}
27 | {% assign fmCode = fmCode | push: codeSnippetName | push: code %}
28 | {% endif %}
29 | {% endfor %}
30 |
31 |
32 |
33 |
setup
34 |
35 |
36 |
37 |
38 | {% for button in theButtons %}
39 | {% assign theButton = button | split: ", " %}
40 | {{ theButton[0] }}
41 | {% endfor %}
42 | Try it yourself
43 |
44 |
45 |
46 |
{{ fmCode | join: newLine | escape_once }}
47 |
48 |
49 |
52 |
53 | {% include codeSection.md %}
54 |
55 |
--------------------------------------------------------------------------------
/docs/_includes/codeSection.md:
--------------------------------------------------------------------------------
1 | ```closet
2 | {{ include.content.code | strip }}
3 | ```
4 |
--------------------------------------------------------------------------------
/docs/_includes/head_custom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/_includes/header-doc.md:
--------------------------------------------------------------------------------
1 | # {{ page.title }}
2 | {: .no_toc }
3 |
4 | {% assign setups = site.data.setups %}
5 | {% assign buttons = site.data.buttons %}
6 |
7 | {% assign home = site.data.snippets.home %}
8 | {% assign mc = site.data.snippets.multiple_choice %}
9 | {% assign generation = site.data.snippets.generation %}
10 | {% assign cloze = site.data.snippets.cloze %}
11 | {% assign flashcard = site.data.snippets.flashcard %}
12 | {% assign shuffling = site.data.snippets.shuffling %}
13 | {% assign occlusions = site.data.snippets.occlusions %}
14 |
--------------------------------------------------------------------------------
/docs/_includes/info.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/_includes/js/codeDisplay.js:
--------------------------------------------------------------------------------
1 | {% capture newLine %}
2 | {% endcapture %}
3 |
4 | {% assign contentCode = include.content.code | replace: "'", "\\'" | strip | newline_to_br | strip_newlines %}
5 | {% assign fmCode = include.fmCode | join: newLine | strip %}
6 |
7 | {% assign fmName = include.theId | replace: "-", "" | replace: "_", "" %}
8 |
9 | const {{ fmName }}filterManager = closet.FilterManager.make()
10 |
11 | const {{ fmName }}func = (filterManager) => {
12 | {{ fmCode }}
13 | return filterManager
14 | }
15 |
16 | {{ fmName }}func({{ fmName }}filterManager)
17 |
18 | {% for button in include.theButtons %}
19 | {% assign theButton = button | split: ", " %}
20 |
21 | readyRenderButton(
22 | '#{{ include.theId }}',
23 | '{{ theButton[1] }}',
24 | '{{ contentCode }}',
25 | {{ theButton[2] }} /* the preset */,
26 | {{ fmName }}filterManager /* filterManager */,
27 | ){% if forloop.first == true %}.dispatchEvent(new Event('click')){% endif %}
28 | {% endfor %}
29 |
30 | readyFmButton(
31 | '#{{ include.theId }}',
32 | `{{ fmCode | replace: "`", "\\`" | replace: "$", "\\$" }}`,
33 | )
34 |
35 | readyTryButton(
36 | '#{{ include.theId }}',
37 | '{{ contentCode }}',
38 | '{{ include.setupString }}',
39 | )
40 |
--------------------------------------------------------------------------------
/docs/_includes/js/prismSetup.js:
--------------------------------------------------------------------------------
1 | Prism.languages.closet = {
2 | tagstart: {
3 | pattern: /\[\[[a-zA-Z]+\d*/u,
4 | inside: {
5 | tagstart: /\[\[/u,
6 | tagname: /[a-zA-Z]+\d*/u,
7 | },
8 | },
9 | tagend: /\]\]/,
10 | altsep: /\|\|/,
11 | argsep: /::/,
12 | }
13 |
--------------------------------------------------------------------------------
/docs/_includes/toc-doc.md:
--------------------------------------------------------------------------------
1 | # {{ page.title }}
2 | {: .no_toc }
3 |
4 | ## Table of contents
5 | {: .no_toc .text-delta }
6 |
7 | 1. TOC
8 | {:toc}
9 |
10 | {% assign setups = site.data.setups %}
11 | {% assign buttons = site.data.buttons %}
12 |
13 | {% assign home = site.data.snippets.home %}
14 | {% assign mc = site.data.snippets.multiple_choice %}
15 | {% assign generation = site.data.snippets.generation %}
16 | {% assign cloze = site.data.snippets.cloze %}
17 | {% assign flashcard = site.data.snippets.flashcard %}
18 | {% assign shuffling = site.data.snippets.shuffling %}
19 | {% assign occlusions = site.data.snippets.occlusions %}
20 |
--------------------------------------------------------------------------------
/docs/_layouts/doc.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 | {{ content }}
19 |
--------------------------------------------------------------------------------
/docs/_layouts/redirect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Redirecting...
13 | Click here if you are not redirected.
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/_layouts/tester.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | ---
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ content }}
20 |
21 |
25 |
--------------------------------------------------------------------------------
/docs/_sass/toggleMemoizationButton.scss:
--------------------------------------------------------------------------------
1 | input[type="checkbox"] {
2 | display: none;
3 | }
4 |
5 | .memoize-label {
6 | position: relative;
7 | padding-right: 2rem;
8 |
9 | &::after {
10 | display: inline;
11 |
12 | transform: scale(.55);
13 | transform-origin: center;
14 |
15 | position: absolute;
16 | content: url("/assets/icons/x-mark.svg");
17 |
18 | #memoize-checkbox:checked ~ & {
19 | content: url("/assets/icons/check-mark.svg");
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/docs/assets/css/main.scss:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 | .clearfix:before, .clearfix:after {
5 | content: " ";
6 | display: table;
7 | }
8 |
9 | .clearfix:after {
10 | clear: both;
11 | }
12 |
13 | .d-inline-block {
14 | position: relative;
15 | }
16 |
17 | a[href="//github.com/hgiesel/closet/"] {
18 | padding-right: 3rem;
19 |
20 | // overwrite just-the-docs so GitHub link can contain image
21 | overflow: visible;
22 |
23 | &:after {
24 | content: url(https://github.githubassets.com/images/modules/logos_page/Octocat.png);
25 | object-fit: contain;
26 | transform: scale(.06) translate(0, -20rem);
27 | width: 0rem;
28 | height: 0rem;
29 |
30 | }
31 | }
32 |
33 | code {
34 | // overwrite just-the-docs in favor of prismjs
35 | border-style: none;
36 | }
37 |
--------------------------------------------------------------------------------
/docs/assets/css/tester.scss:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 | #tester-container {
5 | display: grid;
6 | grid-row-gap: 0.5rem;
7 |
8 | .btn-group {
9 | display: flex;
10 | justify-content: flex-start;
11 | align-content: center;
12 | flex-wrap: wrap;
13 |
14 | // paints button content always over #template-applied
15 | z-index: 3;
16 |
17 | & > * {
18 | margin-right: 0.5rem;
19 | margin-top: 0.5rem;
20 | }
21 | }
22 | }
23 |
24 | textarea {
25 | resize: none;
26 | }
27 |
28 | .bordered {
29 | padding: 20px 5px;
30 | border: 2px solid grey;
31 | border-radius: 4px;
32 |
33 | position: relative;
34 | }
35 |
36 | .desc {
37 | position: absolute;
38 | right: 3px;
39 | bottom: 3px;
40 | opacity: 0.5;
41 | }
42 |
43 | .CodeMirror {
44 | height: auto;
45 | border: 5px solid #eee;
46 |
47 | &.failed {
48 | border: 5px solid #dc322f;
49 | }
50 | }
51 |
52 | .CodeMirror-empty {
53 | color: lightgray;
54 | }
55 |
56 |
57 | @import "toggleMemoizationButton";
58 | @import "filterManagerButton";
59 |
--------------------------------------------------------------------------------
/docs/assets/icons/check-mark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/assets/icons/down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/assets/icons/up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/assets/icons/x-mark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/assets/images/after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/after.png
--------------------------------------------------------------------------------
/docs/assets/images/anki-fields.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/anki-fields.png
--------------------------------------------------------------------------------
/docs/assets/images/before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/before.png
--------------------------------------------------------------------------------
/docs/assets/images/cell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/cell.png
--------------------------------------------------------------------------------
/docs/assets/images/cranial-bones.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/cranial-bones.png
--------------------------------------------------------------------------------
/docs/assets/images/editcurrent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/editcurrent.png
--------------------------------------------------------------------------------
/docs/assets/images/formulas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/formulas.png
--------------------------------------------------------------------------------
/docs/assets/images/reviewer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/reviewer.png
--------------------------------------------------------------------------------
/docs/assets/images/wikipedia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/images/wikipedia.png
--------------------------------------------------------------------------------
/docs/assets/js/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/js/.gitignore
--------------------------------------------------------------------------------
/docs/assets/setups/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/docs/assets/setups/.gitkeep
--------------------------------------------------------------------------------
/docs/clozes/incremental-reveal.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Incremental Reveal
4 | nav_order: 3
5 | permalink: clozes/incremental-reveal
6 | parent: Clozes
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Reveal clozes on click
13 |
14 | Using the obscuring feature, you can also create more interactive cards, which allow user input.
15 | In this example, you have to click on each cloze, to reveal its content.
16 | This is also dubbed _incremental reveal_, or _incremental clozes_.
17 |
18 | {% include codeDisplay.md content=cloze.click_to_reveal setups="click_to_reveal_cloze" buttons=buttons.threeCards %}
19 |
20 | As you can see, the `c` tag provides the effect of a typical incremental cloze.
21 | The `cx` tag is useful for cases, where simply obscuring the text would still yield too much context.
22 |
23 | As an alternative to separating out a single note into multiple cards, some people also like to make one large incremental cloze card.
24 | This can be useful for cramming for a exam.
25 |
26 | {% include codeDisplay.md content=cloze.click_to_reveal_single setups="click_to_reveal_cloze" buttons=buttons.frontBack %}
27 |
28 | ---
29 | ## Reveal clozes on key press
30 |
31 | Rather than using clicks, we can also utilize key presses.
32 | The following clozes will not react to clicks, but only to key presses.
33 | In particular, pressing `Q` will reveal one cloze, pressing `W` will reveal all clozes at once.
34 |
35 | {% include codeDisplay.md content=cloze.click_to_reveal_single setups="keydown_to_reveal_cloze" buttons=buttons.frontBack %}
36 |
--------------------------------------------------------------------------------
/docs/clozes/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Clozes
4 | nav_order: 5
5 | permalink: clozes
6 | has_children: true
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## What are clozes?
13 |
14 | According to [Wikipedia](https://en.wikipedia.org/wiki/Cloze_test):
15 |
16 | > A cloze test (also cloze deletion test) is an exercise, test, or assessment consisting of a portion of language with certain items, words, or signs removed (cloze text), where the participant is asked to replace the missing language item.
17 |
18 | ---
19 | ## Test and answer context
20 |
21 | The rendering of the cloze needs to _context-sensitive_.
22 | Depending on the context cloze is rendered, it needs to be rendered differently:
23 |
24 | 1. as a _test_, it can be rendered as an [ellipsis](https://en.wikipedia.org/wiki/Ellipsis), or a [hint](creating#hints).
25 | The hint might be text provided by the user, or simply the answer, but processed in a specific way, e.g. [obscured, or blanked out](blanking-obscuring).
26 | 1. as the _answer_, or _reveal_, it needs to show what was hidden.
27 |
28 | For flashcards, _test_ will be synonmymous with the _front_, _answer_ with the _back_.
29 |
30 | ---
31 | ## Active and inactive clozes
32 |
33 | In a flashcard, you usually want to test each piece of knowledge individually.
34 | This is also called the [minimum information principle](https://www.supermemo.com/de/archives1990-2015/articles/20rules#minimum%20information%20principle).
35 | In Anki, this is facilitated in the form of cards.
36 |
37 | ```closet
38 | The capital of Portugal is [[c1::Lisbon]]. Its area is [[c2::92km²]].
39 | ```
40 |
41 | Using the above template, we could create two different cards, both testing one piece of knowledge on the note.
42 | This introduces the notion of _active_ clozes, compared to _inactive_ ones.
43 |
44 | In Anki, on a card `Cloze 1`, the active cloze would be `c1`, whereas `c2` is inactive, and vice versa.
45 | Within Closet we can be more flexibile, but generally we will adhere to this convention.
46 |
47 | ---
48 | ## Summary
49 |
50 | Altogether, we can think of a cloze as text which can be rendered in 4 different ways:
51 | * _test_ and _active_
52 | * _test_ and _inactive_
53 | * _answer_ and _active_
54 | * _answer_ and _inactive_
55 |
--------------------------------------------------------------------------------
/docs/flashcard/activating-in-a-range.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Activating in a range
4 | nav_order: 2
5 | permalink: flashcard/activating-in-a-range
6 | parent: Flashcard Types
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Activating tags in a range
13 |
14 | This is also known as _overlapping clozes_.
15 |
16 | {% include codeDisplay.md content=flashcard.around_range setups="default_cloze,flashcard" buttons=buttons.sixCards %}
17 |
18 | It is especially popular with remembering poems.
19 |
20 | {% include codeDisplay.md content=flashcard.top_bottom_range setups="default_cloze,flashcard" buttons=buttons.sixCards %}
21 |
--------------------------------------------------------------------------------
/docs/flashcard/activating-selectively.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Activating selectively
4 | nav_order: 1
5 | permalink: flashcard/activating-selectively
6 | parent: Flashcard Types
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Selectively activating tags
13 |
14 | You can largely change which cards are considered active, using _activation_ and _deactivation_ tags.
15 | This is especially useful when you use card sections (TODO).
16 |
17 | {% include codeDisplay.md content=cloze.activate_cloze setups="default_cloze,flashcard" buttons=buttons.threeCards %}
18 |
19 | ### Using occurrence numbers
20 |
21 | Using the notation `fulltag:occurence`, you can go into more detail when specifying clozes.
22 | All tags are enumerated while they are generated, starting at `0`.
23 | This way you can specifically choose which cloze to activate, even if they share the same name.
24 |
25 | {% include codeDisplay.md content=cloze.activate_cloze_with_occur setups="default_cloze,flashcard" buttons=buttons.threeCards %}
26 |
27 | ---
28 | ## Evaluation order
29 |
30 | However keep in mind that tags are evaluated in a certain order.
31 | You need to use the activation tag, before you
32 |
33 | {% include codeDisplay.md content=cloze.activate_cloze_conflict setups="default_cloze,flashcard" buttons=buttons.threeCards %}
34 |
--------------------------------------------------------------------------------
/docs/flashcard/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Flashcard Types
4 | nav_order: 8
5 | permalink: flashcard
6 | has_children: true
7 | ---
8 |
9 | # Flashcard types
10 |
11 | Flashcard types include [clozes](../clozes), [multiple choice questions](../multiple-choice), and [image occlusions](../occlusions).
12 |
--------------------------------------------------------------------------------
/docs/flashcard/switching.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Switching between subtypes
4 | nav_order: 3
5 | permalink: flashcard/switching
6 | parent: Flashcard Types
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Switching to showing tags
13 |
14 | This can be used to show context around clozes
15 |
16 | {% include codeDisplay.md content=flashcard.around_ctxt setups="default_cloze,flashcard" buttons=buttons.sixCards %}
17 |
--------------------------------------------------------------------------------
/docs/generation/evaluation.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Evaluating values
4 | nav_order: 4
5 | permalink: /generation/evaluation
6 | parent: Generation
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Evaluation of value lists
13 |
14 | ...still in work...
15 |
--------------------------------------------------------------------------------
/docs/generation/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Generation
4 | nav_order: 4
5 | permalink: /generation
6 | has_children: true
7 | ---
8 |
9 | # Value Generation
10 |
11 | This includes the generation of integers and real numbers, as well as values from value lists.
12 |
--------------------------------------------------------------------------------
/docs/generation/padded-picking.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Padded picking
4 | nav_order: 3
5 | permalink: /generation/padded-picking
6 | parent: Generation
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Picking with padding
13 |
14 | {% include codeDisplay.md content=generation.picking_padding setups="defining_lists,pick_eval_padding" buttons=buttons.frontBack %}
15 |
--------------------------------------------------------------------------------
/docs/generation/picking.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Picking values
4 | nav_order: 2
5 | permalink: /generation/picking
6 | parent: Generation
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Picking from value lists
13 |
14 | Picking can be used to generate random values based on a set list of values.
15 |
16 | {% include codeDisplay.md content=generation.picking setups="defining_lists,pick_eval" buttons=buttons.frontBack %}
17 |
18 | ---
19 | ## Picking unique values
20 |
21 | You can control the picking of values and restrict it to unique values, if you provide an index.
22 | Within the same picking index, the same value will never be picked twice.
23 |
24 | In fact it will refuse to pick any further value, once it exhausted the values.
25 | So make sure, you don't pick more values than there are within the list.
26 |
27 | Notice that this makes it behave similiar to [shuffling values](/shuffling).
28 |
29 | {% include codeDisplay.md content=generation.picking_unique setups="defining_lists,pick_eval" buttons=buttons.frontBack %}
30 |
31 | ---
32 | ## Picking using indices
33 |
34 | As an alternative to random picking, you can also pick with an index.
35 | The index is provided as the tag index.
36 | When picking this way, the same value can be caught multiple times without any issue.
37 |
38 | {% include codeDisplay.md content=generation.picking_index setups="defining_lists,pick_eval" buttons=buttons.frontBack %}
39 |
40 | This is especially useful, if you want to specify a template, and only provide the different values.
41 |
42 | {% include codeDisplay.md content=generation.picking_template setups="debug,defining_lists,pick_eval" buttons=buttons.threeCards %}
43 |
--------------------------------------------------------------------------------
/docs/generation/random.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Random numbers
4 | nav_order: 1
5 | permalink: /generation/random
6 | parent: Generation
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Generation of random numbers
13 |
14 | {% include codeDisplay.md content=generation.simple setups="generate" buttons=buttons.frontBack %}
15 |
--------------------------------------------------------------------------------
/docs/home/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Home
4 | nav_order: 1
5 | permalink: /
6 | has_children: true
7 | has_toc: false
8 | ---
9 |
10 | # Closet
11 |
12 | **Closet** is a unique mix between a [templating engine](https://en.wikipedia.org/wiki/Template_processor) and a [markup language](https://en.wikipedia.org/wiki/Markup_language) written in [TypeScript](https://www.typescriptlang.org/) for the generation of flashcards, especially for the use in the [Anki](https://apps.ankiweb.net/) flashcard software.
13 |
14 | It allows to use many "effects", that you'd like to have on flashcards, like [shuffling text items](/shuffling), [clozes](/clozes), [multiple choice](/multiple-choice), or [image occlusions](/occlusions).
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/home/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Installation
4 | nav_order: 2
5 | permalink: installation
6 | parent: Home
7 | ---
8 |
9 | {% include header-doc.md %}
10 |
--------------------------------------------------------------------------------
/docs/home/the-name.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Why the name?
4 | nav_order: 1
5 | permalink: why-the-name
6 | parent: Home
7 | ---
8 |
9 | {% include header-doc.md %}
10 |
11 | The first time I wrote code in this vein, I called the script __Multiple Choice__.
12 |
13 | Slowly adding functionality, I decided that Multiple Choice didn't quite describe what it did, so I called it __List Randomizer__.
14 |
15 | at some point, I decided that it should rather be called __Set Randomizer__, because if you randomize elements, the order of the elements is inherently meaningless, unlike a list.
16 |
17 | Finally, when I rewrote the whole project from scratch, I chose the name __Closet__.
18 | The functionality was **clo**se to **Set**lang, but not quite the same (much better, rather!).
19 |
--------------------------------------------------------------------------------
/docs/multiple-choice/categories.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Assigning categories
4 | nav_order: 3
5 | permalink: multiple-choice/categories
6 | parent: Multiple Choice
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Example of assignning categories
13 |
14 | Here we have several names of animal species, which are rendered in different ways.
15 |
16 | {% include codeDisplay.md content=mc.animals setups="assign_categories" buttons=buttons.threeCards %}
17 |
18 | ---
19 | ## How it works
20 |
21 | In its essence, the multiple choice filter actually assigns categories.
22 | These categories are stylized using a `Stylizer`.
23 |
24 | ```closet
25 | [[tagname::value1||value2::value3::value4||value5]]
26 | ```
27 |
28 | If `` was a tag implementing the `multipleChoice` filter, `value1` and `value2` would belong to category 1, `value3` to category 2, and `value4` and `value5` belongs to category 3.
29 | In multiple choice questions, the first category happens to be _correct answer_, and the second category _wrong answer_.
30 |
--------------------------------------------------------------------------------
/docs/multiple-choice/creating.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Creating multiple choice
4 | nav_order: 1
5 | permalink: multiple-choice/creating
6 | parent: Multiple Choice
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Creating multiple choice
13 |
14 | {% include codeDisplay.md content=mc.simple_zero setups="default_multiple_choice" buttons=buttons.frontBack %}
15 |
16 | With indexes.
17 |
18 | {% include codeDisplay.md content=mc.simple_nonzero setups="default_multiple_choice" buttons=buttons.threeCards %}
19 |
20 | ---
21 | ## Graphical effects
22 |
23 | {% include codeDisplay.md content=mc.simple_zero setups="fancy_multiple_choice" buttons=buttons.frontBack %}
24 |
--------------------------------------------------------------------------------
/docs/multiple-choice/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Multiple Choice
4 | nav_order: 6
5 | permalink: /multiple-choice
6 | has_children: true
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Multiple choice questions
13 |
14 | In these sections we will see how to create and stylize questions where we provide *correct answers* and *wrong answers*.
15 | Further on we will generalize these to *answer categories*.
16 |
17 | ---
18 | ## Similiarities to clozes
19 |
20 | Just like [clozes](../clozes), multiple choices are one kind of flashcard types, and supports the same notions:
21 |
22 | 1. Both have the notion of _test_, _answer_, _inactive_, and _active_
23 | 1. Both understand the messages understood by any [flashcard types](../flashcard)
24 |
--------------------------------------------------------------------------------
/docs/multiple-choice/radio-button-checkbox.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Using radio buttons and checkboxes
4 | nav_order: 2
5 | permalink: multiple-choice/input
6 | parent: Multiple Choice
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Using radio buttons and checkboxes
13 |
14 | {% include codeDisplay.md content=mc.formal_questions setups="input_multiple_choice" buttons=buttons.threeCards %}
15 |
16 |
--------------------------------------------------------------------------------
/docs/occlusions/create_occlusions.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Create occlusions
4 | nav_order: 2
5 | permalink: /occlusions/create-occlusions
6 | parent: Image Occlusions
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Creating occlusions
13 |
14 | Ideally, you'd have a GUI to create graphical elements, like image occlusions.
15 | Such an interface is generated by using a special `makeOcclusions` tag.
16 |
17 | This will turn each image in the template into their own occlusion interface.
18 |
19 | {% include codeDisplay.md content=occlusions.create setups="occlusions" buttons=buttons.frontBack %}
20 |
--------------------------------------------------------------------------------
/docs/occlusions/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Image Occlusions
4 | nav_order: 7
5 | permalink: occlusions
6 | has_children: true
7 | ---
8 |
9 | #
10 |
--------------------------------------------------------------------------------
/docs/occlusions/render_occlusions.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Render occlusions
4 | nav_order: 2
5 | permalink: /occlusions/render-occlusions
6 | parent: Image Occlusions
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Render occlusions
13 |
14 | Occlusions can be defined by using the position of the origin (top left), and the width and height of the rectangle.
15 |
16 | {% include codeDisplay.md content=occlusions.bones setups="occlusions" buttons=buttons.sixCards %}
17 |
18 | Similar to clozes, there are three subtypes of occlusions:
19 | - showing occlusions
20 | - hiding occlusions
21 | - revealing occlusions
22 |
23 | {% include codeDisplay.md content=occlusions.cell setups="occlusions" buttons=buttons.threeCards %}
24 |
25 | To be more exact, occlusions also react to the flashcard interface.
26 | This means, they can be used with the flashcard specific commands.
27 |
28 | {% include codeDisplay.md content=occlusions.cell_flashcard setups="occlusions,flashcard" buttons=buttons.sixCards %}
29 |
--------------------------------------------------------------------------------
/docs/occlusions/with_other_types.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Using occlusions with clozes and shuffling
4 | nav_order: 4
5 | permalink: /occlusions/with-other-types
6 | parent: Image Occlusions
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Using occlusions with other effects
13 |
14 | Occlusions can be combined with other flashcard features.
15 |
16 | {% include codeDisplay.md content=occlusions.bones_with_other setups="occlusions,default_shuffle,click_to_reveal_cloze" buttons=buttons.sixCards %}
17 |
--------------------------------------------------------------------------------
/docs/occlusions/with_other_types2.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Using occlusions with clozes and picking
4 | nav_order: 5
5 | permalink: /occlusions/with-other-types2
6 | parent: Image Occlusions
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Using occlusions with other effects
13 |
14 | Occlusions can be combined with other flashcard features.
15 |
16 | {% include codeDisplay.md content=occlusions.bones_with_other2 setups="occlusions,default_shuffle,click_to_reveal_cloze,defining_lists,pick_eval" buttons=buttons.sixCards %}
17 |
--------------------------------------------------------------------------------
/docs/occlusions/without_context.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Occlusions without context
4 | nav_order: 3
5 | permalink: /occlusions/without-context
6 | parent: Image Occlusions
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Occlusions without context
13 |
14 | This is a different concept for displaying occlusions dubbed "occlusions without context".
15 | It will completely obscure the image for the front, and highlight the intended section for the back.
16 | It can be useful for things like formula tables.
17 |
18 | {% include codeDisplay.md content=occlusions.formulas setups="debug,defining_lists,pick_eval,default_cloze,occlusions_highlight" buttons=buttons.threeCards %}
19 |
--------------------------------------------------------------------------------
/docs/shuffling/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Shuffling
4 | nav_order: 3
5 | permalink: /shuffling
6 | has_children: true
7 | ---
8 |
9 | # Shuffling
10 |
11 | **Closet** offers a wide array of default functionality, that you can use out-of-the-box.
12 |
--------------------------------------------------------------------------------
/docs/shuffling/ordering.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Preserving item orders
4 | nav_order: 2
5 | permalink: /shuffling/ordering
6 | parent: Shuffling
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Ordering
13 |
14 | Sometimes you want to shuffle items, however you still want to preserve some relation in the text.
15 | In these cases, the `ord` tag can come in handy.
16 | Let's look at a first example:
17 |
18 | {% include codeDisplay.md content=shuffling.on_extra_line setups="default_shuffle,order" buttons=buttons.frontBack %}
19 |
20 | In this example, we shuffle the countries, and the capitals.
21 | However we don't want to get countries and capitals to get out of order with each other respectively.
22 | The `ord` tag alters the `mix` tag in a way, that it will still shuffle at random, but preserve this connection.
23 |
24 | {% include codeDisplay.md content=shuffling.individual_items setups="default_shuffle,order" buttons=buttons.frontBack %}
25 |
26 | When shuffling sentences, the `ord` tag is especially handy, because you can focus on the important parts.
27 |
28 | ---
29 | ## Ordering inline and non-contiguous shuffles
30 |
31 | {% include codeDisplay.md content=shuffling.inline_vs_list setups="fancy_shuffle,order" buttons=buttons.frontBack %}
32 |
33 | The `ord` tag can also relate values from a `mix` single with individual `mix` tags
34 |
--------------------------------------------------------------------------------
/docs/shuffling/shuffle_quest.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Shuffling as flashcard types
4 | nav_order: 4
5 | permalink: /shuffling/shuffling-quest
6 | parent: Shuffling
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Shuffling as a flashcard type
13 |
14 | Shuffling can also be applied as a [flashcard type](../flashcard), which you can use to test yourself.
15 |
16 | {% include codeDisplay.md content=shuffling.assign_shuffle setups="shuffle_quest" buttons=buttons.twoCards %}
17 |
18 | Notice that `sort1` is only activated for card 1, `sort2` is only activated for card 2.
19 | This makes them similar to how [clozes](../clozes) behave, for example.
20 |
21 | ---
22 | ## Overview of `shuffle`, `sort`, and `jumble`
23 |
24 | Altogether there are three different kinds of these "shuffling as flashcard" types:
25 | - `shuffle`: cards will be shuffled on both sides
26 | - `sort`: cards will be shuffled on the front, but not on the back
27 | - `jumble`: cards will be shuffled on the back, but not the front
28 |
29 | ---
30 | ## Combining with other flashcard types
31 |
32 | You can also combine it with other flashcard types:
33 |
34 | {% include codeDisplay.md content=shuffling.assign_shuffle_ol setups="shuffle_quest,default_cloze,default_multiple_choice,order" buttons=buttons.fourCards %}
35 |
--------------------------------------------------------------------------------
/docs/shuffling/templated.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Shuffling lines / list items
4 | nav_order: 3
5 | permalink: /shuffling/templated
6 | parent: Shuffling
7 | ---
8 |
9 | {% include toc-doc.md %}
10 |
11 | ---
12 | ## Shuffle lines
13 |
14 | Closet is powerful enough to work on HTML elements themselves
15 |
16 | {% include codeDisplay.md content=shuffling.lines setups="templated_shuffle" buttons=buttons.frontBack %}
17 |
18 | ---
19 | ## Shuffle List Items
20 |
21 | {% include codeDisplay.md content=shuffling.list_items setups="templated_shuffle" buttons=buttons.frontBack %}
22 |
--------------------------------------------------------------------------------
/docs/stylizing/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Stylizing
4 | nav_order: 2
5 | permalink: /stylizing
6 | has_children: true
7 | ---
8 |
9 | # Stylizing
10 |
11 | **Closet** offers a wide array of default functionality, that you can use out-of-the-box.
12 |
--------------------------------------------------------------------------------
/docs/stylizing/mandarin.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | title: Mandarin tone stylizing
4 | nav_order: 2
5 | permalink: stylizing/mandarin
6 | parent: Stylizing
7 | ---
8 |
9 | {% assign b = "Render, render, {}, false" %}
10 | {% assign bOneTwo ='Frontside, q, {"side": "front"}, true; Backside, a, {"side": "back"}, true' %}
11 |
12 | {% assign stylizing = site.data.snippets.stylizing %}
13 | {% assign setups = site.data.setups %}
14 |
15 | {% include toc-doc.md %}
16 |
17 | ---
18 | ## Ordering
19 |
20 | {% include codeDisplay.md content=stylizing.mandarin setups="mandarin_support" buttons=bOneTwo %}
21 |
--------------------------------------------------------------------------------
/docs/tester/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: tester
3 | title: Try out Closet!
4 | nav_order: 10
5 | permalink: /tester
6 | ---
7 |
8 | # Test Closet Inside Your Browser
9 |
10 | {% include tester.html %}
11 |
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/images/logo.png
--------------------------------------------------------------------------------
/images/logo.pxm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/images/logo.pxm
--------------------------------------------------------------------------------
/images/textlogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/images/textlogo.png
--------------------------------------------------------------------------------
/images/weblogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/images/weblogo.png
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export * as closet from './src'
2 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { defaults } = require('jest-config')
2 |
3 | module.exports = {
4 | roots: [
5 | '',
6 | ],
7 | transform: {
8 | '^.+\\.tsx?$': 'ts-jest',
9 | },
10 | testRegex: 'test/.*\\.spec\\.tsx?$',
11 | moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
12 | verbose: true,
13 |
14 | modulePaths: [
15 | '/node_modules',
16 | ],
17 | }
18 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { nodeResolve } from '@rollup/plugin-node-resolve'
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import typescript from '@rollup/plugin-typescript'
4 | import babel from '@rollup/plugin-babel'
5 | import { terser } from "rollup-plugin-terser"
6 |
7 | const babelOptions = {
8 | exclude: 'node_modules/**',
9 | babelHelpers: 'bundled',
10 | }
11 |
12 | const terserOptions = {
13 | output: {
14 | comments: false,
15 | },
16 |
17 | compress: {
18 | drop_console: false,
19 | pure_funcs: [],
20 | },
21 | }
22 |
23 | const tsconfigDist = {
24 | declaration: true,
25 | declarationDir: "dist/",
26 | }
27 |
28 | /**
29 | * available configs:
30 | * --configDocs
31 | * --configAnki
32 | * --configDist
33 | * --configDistWeb
34 | * --configDev
35 | */
36 | export default args => {
37 | const destination = args.configAnki
38 | ? { file: 'anki/web/closet.js' }
39 | : args.configDocs
40 | ? { file: 'docs/assets/js/closet.js' }
41 | : args.configDist
42 | ? { dir: 'dist/' }
43 | /* args.configDistWeb */
44 | : { file: 'dist/closet.min.js' }
45 |
46 |
47 | const format = args.configAnki
48 | ? 'esm'
49 | : args.configDocs || args.configDistWeb
50 | ? 'iife'
51 | /* args.configDist */
52 | : 'cjs'
53 |
54 | const plugins = [
55 | nodeResolve(),
56 | commonjs(),
57 | typescript(args.configDist
58 | ? tsconfigDist
59 | : {}
60 | ),
61 | babel(babelOptions),
62 | ]
63 |
64 | if (!args.configDev) {
65 | plugins.push(terser(terserOptions))
66 | }
67 |
68 | return {
69 | input: 'index.ts',
70 | output: {
71 | name: 'closet',
72 | ...destination,
73 | format,
74 | sourcemap: args.configDistWeb,
75 | },
76 | plugins,
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/serve.json:
--------------------------------------------------------------------------------
1 | {
2 | "public": "test/browser",
3 | "cleanUrls": true,
4 | "directoryListing": true,
5 | "trailingSlash": false
6 | }
7 |
--------------------------------------------------------------------------------
/src/browser/index.ts:
--------------------------------------------------------------------------------
1 | import { occlusionMakerRecipe as occlusionEditor } from "./occlusionEditor";
2 | import { rectRecipes as rect } from "./rect";
3 |
4 | export const recipes = {
5 | occlusionEditor,
6 | rect,
7 | };
8 |
--------------------------------------------------------------------------------
/src/browser/menuConstruction.ts:
--------------------------------------------------------------------------------
1 | export interface MenuItem {
2 | label: string;
3 | itemId: string;
4 | html?: boolean;
5 | clickEvent?: (event: MouseEvent) => void;
6 | sub?: MenuItem[];
7 | }
8 |
9 | const processMenuItem = (item: MenuItem) => {
10 | const liElement = document.createElement("li");
11 | const aElement = document.createElement("a");
12 |
13 | if (item.html) {
14 | aElement.innerHTML = item.label;
15 | } else {
16 | aElement.innerText = item.label;
17 | }
18 |
19 | aElement.id = item.itemId;
20 | aElement.classList.add("context-menu-item");
21 |
22 | if (item.clickEvent) {
23 | aElement.addEventListener("click", item.clickEvent);
24 | }
25 |
26 | liElement.appendChild(aElement);
27 |
28 | if (item.sub && item.sub.length > 0) {
29 | liElement.appendChild(processMenuItems(item.sub));
30 | }
31 |
32 | return liElement;
33 | };
34 |
35 | const processMenuItems = (items: MenuItem[]) => {
36 | const ulElement = document.createElement("ul");
37 | const liElements = items.map(processMenuItem);
38 |
39 | for (const li of liElements) {
40 | ulElement.appendChild(li);
41 | }
42 |
43 | return ulElement;
44 | };
45 |
46 | export const constructMenu = (
47 | menuId: string,
48 | items: MenuItem[],
49 | ): HTMLElement => {
50 | const navElement = document.createElement("nav");
51 | navElement.classList.add("context-menu");
52 | navElement.id = menuId;
53 | navElement.appendChild(processMenuItems(items));
54 |
55 | return navElement;
56 | };
57 |
--------------------------------------------------------------------------------
/src/browser/scaleZoom.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * functions for reversing css properties zoom and transform
3 | */
4 |
5 | export type Reverser = ([x, y]: [number, number]) => [number, number];
6 |
7 | const reverseZoom = (style: CSSStyleDeclaration): Reverser => ([origX, origY]: [
8 | number,
9 | number,
10 | ]): [number, number] => {
11 | const zoomString = style.getPropertyValue("zoom");
12 | const zoomValue = Number(zoomString);
13 |
14 | if (Number.isNaN(zoomValue) || zoomValue <= 0) {
15 | return [origX, origY];
16 | }
17 |
18 | return [origX / zoomValue, origY / zoomValue];
19 | };
20 |
21 | export const reverseEffects = (style: CSSStyleDeclaration): Reverser => ([
22 | x,
23 | y,
24 | ]: [number, number]) => {
25 | const zoom = reverseZoom(style);
26 |
27 | return zoom([x, y]);
28 | };
29 |
--------------------------------------------------------------------------------
/src/filterManager/registrar.ts:
--------------------------------------------------------------------------------
1 | import { Filterable, FilterApi, Readiable, WeakFilter } from "./filters";
2 | import { Storage } from "./storage";
3 |
4 | export class RegistrarApi<
5 | F extends Filterable,
6 | T extends Readiable,
7 | D extends Record
8 | > {
9 | private filters: FilterApi;
10 | private options: Storage>;
11 |
12 | constructor(filters: FilterApi, options: Storage>) {
13 | this.filters = filters;
14 | this.options = options;
15 | }
16 |
17 | register(
18 | name: string,
19 | filter: WeakFilter,
20 | options: Partial = {},
21 | ): void {
22 | this.filters.register(name, filter);
23 | this.options.set(name, options);
24 | }
25 |
26 | getOptions(name: string): Partial {
27 | return this.options.get(name, {});
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/flashcard/index.ts:
--------------------------------------------------------------------------------
1 | import type { Recipe } from "../types";
2 | import type { FlashcardPreset } from "./flashcardTemplate";
3 |
4 | export type { FlashcardPreset } from "./flashcardTemplate";
5 | export interface FlashcardRecipes {
6 | show: Recipe;
7 | hide: Recipe;
8 | reveal: Recipe;
9 | }
10 |
11 | export * as deciders from "./deciders";
12 | export { FlashcardBehavior as behaviors } from "./flashcardTemplate";
13 |
14 | import { clozeRecipes as cloze } from "./cloze";
15 | import { multipleChoiceRecipes as multipleChoice } from "./multipleChoice";
16 | import { specRecipes as specification } from "./spec";
17 |
18 | import {
19 | mingleRecipes as mingle,
20 | sortRecipes as sort,
21 | jumbleRecipes as jumble,
22 | } from "./shuffleQuestion";
23 |
24 | export const recipes = {
25 | cloze,
26 | multipleChoice,
27 | specification,
28 | mingle,
29 | sort,
30 | jumble,
31 | };
32 |
--------------------------------------------------------------------------------
/src/flashcard/spec.ts:
--------------------------------------------------------------------------------
1 | import type { TagNode, Recipe, InactiveBehavior } from "../types";
2 | import type { FlashcardTemplate, FlashcardPreset } from "./flashcardTemplate";
3 |
4 | import {
5 | makeFlashcardTemplate,
6 | generateFlashcardRecipes,
7 | } from "./flashcardTemplate";
8 |
9 | const insert = (tag: TagNode) => ({ result: tag.values, parse: true });
10 | const noinsert = () => "";
11 |
12 | const specPublicApi = (
13 | frontInactive: InactiveBehavior,
14 | backInactive: InactiveBehavior,
15 | ): Recipe => (
16 | options: {
17 | tagname?: string;
18 | flashcardTemplate?: FlashcardTemplate;
19 | } = {},
20 | ) => {
21 | const {
22 | tagname = "spec",
23 | flashcardTemplate = makeFlashcardTemplate(),
24 | } = options;
25 |
26 | const specOptions = { capture: true };
27 | const specRecipe = flashcardTemplate(frontInactive, backInactive);
28 |
29 | return specRecipe(tagname, insert, insert, insert, noinsert, specOptions);
30 | };
31 |
32 | export const specRecipes = generateFlashcardRecipes(specPublicApi);
33 |
--------------------------------------------------------------------------------
/src/generator.ts:
--------------------------------------------------------------------------------
1 | export type NumberGenAlgorithm = () => number;
2 | // type NumberGen = (min: number, max: number, extra: number, banDomain: string[], filter: boolean) => Generator
3 |
4 | const maxTries = 1_000;
5 |
6 | export const numberGenerator = function* (
7 | gen: NumberGenAlgorithm,
8 | filter: boolean,
9 | banDomain: number[] = [],
10 | prematureStop: (banDomain: number[]) => boolean = () => false,
11 | ): Generator {
12 | const filteredValues: number[] = [];
13 | let tries = 0;
14 |
15 | while (!prematureStop(banDomain) && tries < maxTries) {
16 | const randomValue = gen();
17 |
18 | if (
19 | !banDomain.includes(randomValue) &&
20 | !filteredValues.includes(randomValue)
21 | ) {
22 | if (filter) {
23 | filteredValues.push(randomValue);
24 | }
25 |
26 | yield randomValue;
27 | }
28 |
29 | tries++;
30 | }
31 |
32 | return filteredValues;
33 | };
34 |
35 | export const intAlgorithm = (
36 | min: number,
37 | max: number,
38 | ): NumberGenAlgorithm => () => min + Math.floor(Math.random() * (max - min));
39 | export const intOutput = (value: number, extra: number) =>
40 | String(value * extra);
41 | export const intStop = (min: number, max: number) => (banDomain: number[]) =>
42 | banDomain.length >= max - min;
43 |
44 | export const realAlgorithm = (
45 | min: number,
46 | max: number,
47 | ): NumberGenAlgorithm => () => min + Math.random() * (max - min);
48 | export const realOutput = (value: number, extra: number) =>
49 | value.toFixed(extra);
50 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./version";
2 | export * from "./filterManager";
3 | export * as template from "./template";
4 |
5 | export * as recipes from "./recipes";
6 | export * as flashcard from "./flashcard";
7 | export * as browser from "./browser";
8 |
9 | export { Stylizer } from "./stylizer";
10 | export * as wrappers from "./wrappers";
11 | export * as sequencers from "./sequencers";
12 | export * as sortInStrategies from "./sortInStrategies";
13 | export * as patterns from "./patterns";
14 |
--------------------------------------------------------------------------------
/src/recipes/debug.ts:
--------------------------------------------------------------------------------
1 | import type { Registrar, TagNode, Internals } from "../types";
2 |
3 | export const debugRecipe = () => >(
4 | registrar: Registrar,
5 | ) => {
6 | const pathFilter = (_t: TagNode, { path }: Internals) => path.join(":");
7 |
8 | registrar.register("tagpath", pathFilter);
9 | registrar.register("never", () => {
10 | /* nothing */
11 | });
12 | registrar.register("empty", () => "");
13 | registrar.register("key", ({ key }: TagNode) => key);
14 |
15 | registrar.register(
16 | "stopIteration",
17 | ({ values }: TagNode, { filters }: Internals) => {
18 | const endAtIteration = Number(values);
19 | const savedBase = filters.getOrDefault("base");
20 |
21 | filters.register(
22 | "base",
23 | (tag: TagNode, internals: Internals) => {
24 | return internals.iteration >= endAtIteration
25 | ? tag.text ?? ""
26 | : savedBase(tag, internals);
27 | },
28 | );
29 |
30 | return { ready: true };
31 | },
32 | );
33 |
34 | registrar.register("memorytest", (_tag, { memory }: Internals) => {
35 | const memoryTestKey = "base:memorytest";
36 | return String(memory.fold(memoryTestKey, (v: number) => ++v, 0));
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/src/recipes/delim.ts:
--------------------------------------------------------------------------------
1 | import type { Registrar, TagNode, Internals, WeakFilterResult } from "../types";
2 |
3 | import { separated } from "../template/optics";
4 |
5 | const delimOptions = {
6 | inlineOptics: [separated({ sep: "::", max: 2 })],
7 | capture: true,
8 | };
9 |
10 | export const delimRecipe = (
11 | options: {
12 | tagname?: string;
13 | } = {},
14 | ) => >(registrar: Registrar) => {
15 | const { tagname = "delim" } = options;
16 |
17 | const delimFilter = (
18 | tag: TagNode,
19 | { template, isCapture }: Internals,
20 | ): WeakFilterResult => {
21 | if (isCapture) {
22 | const [open, close] = tag.inlineValues;
23 |
24 | template.parser.update({ open, close });
25 |
26 | return {
27 | result: tag.blockText,
28 | parse: true,
29 | };
30 | }
31 |
32 | // Reset delimiters
33 | template.parser.update();
34 |
35 | return {
36 | result: tag.innerNodes,
37 | };
38 | };
39 |
40 | registrar.register(tagname, delimFilter, delimOptions);
41 | };
42 |
--------------------------------------------------------------------------------
/src/recipes/generating.ts:
--------------------------------------------------------------------------------
1 | import type { Registrar, TagNode, Internals } from "../types";
2 | import type { NumberGenAlgorithm } from "../generator";
3 |
4 | import {
5 | numberGenerator,
6 | intAlgorithm,
7 | intOutput,
8 | realAlgorithm,
9 | realOutput,
10 | } from "../generator";
11 |
12 | import { separated } from "../template/optics";
13 |
14 | const generateOptics = [separated({ sep: "," })];
15 |
16 | const generateTemplate = (
17 | algorithm: (min: number, max: number) => NumberGenAlgorithm,
18 | outputAlgorithm: (value: number, extra: number) => string,
19 | defaultExtra: number,
20 | ) => ({
21 | tagname = "gen",
22 | uniqueConstraintId = "uniq",
23 | optics = generateOptics,
24 | } = {}) => >(registrar: Registrar) => {
25 | const uniqConstraintPrefix = `gen:${uniqueConstraintId}`;
26 |
27 | const generateFilter = (
28 | { values, fullOccur, num }: TagNode,
29 | { memory }: Internals,
30 | ) => {
31 | const [min = 1, max = 100, extra = defaultExtra] =
32 | values.length === 1
33 | ? [1, Number(values[0]), defaultExtra]
34 | : values.map(Number);
35 |
36 | const uniqueConstraintId = Number.isInteger(num)
37 | ? `${uniqConstraintPrefix}:${num}`
38 | : uniqConstraintPrefix;
39 |
40 | const generateId = `gen:${tagname}:${fullOccur}`;
41 |
42 | const result = memory.lazy(generateId, (): string => {
43 | const gen = numberGenerator(
44 | algorithm(min, max),
45 | true,
46 | Number.isInteger(num) ? memory.get(uniqueConstraintId, []) : [],
47 | );
48 |
49 | const generated = gen.next();
50 |
51 | return generated.done
52 | ? ""
53 | : outputAlgorithm(generated.value as number, extra);
54 | });
55 |
56 | return { ready: true, result: result };
57 | };
58 |
59 | registrar.register(tagname, generateFilter, { optics });
60 | };
61 |
62 | export const generateInteger = generateTemplate(intAlgorithm, intOutput, 1);
63 | export const generateReal = generateTemplate(realAlgorithm, realOutput, 2);
64 |
--------------------------------------------------------------------------------
/src/recipes/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./preferenceStore";
2 | export * from "./sharedStore";
3 |
4 | export { applyRecipe as apply, processRecipe as process, styleRecipe as style } from "./simple";
5 | export { shufflingRecipe as shuffle } from "./shuffling";
6 | export { orderingRecipe as order } from "./ordering";
7 | export { generateInteger, generateReal } from "./generating";
8 | export { debugRecipe as debug } from "./debug";
9 | export { defRecipe as define } from "./meta";
10 | export { delimRecipe as delimiter } from "./delim";
11 |
--------------------------------------------------------------------------------
/src/recipes/meta.ts:
--------------------------------------------------------------------------------
1 | import type { Registrar, TagNode, WeakFilterResult } from "../types";
2 |
3 | import { separated } from "../template/optics";
4 |
5 | const paramPattern = /%(.)/gu;
6 | const defOptions = {
7 | optics: [separated({ sep: "::", max: 2 })],
8 | capture: true,
9 | };
10 |
11 | const matcher = (tag: TagNode) => (match: string, p1: string) => {
12 | let num = null;
13 |
14 | switch (p1) {
15 | case "%":
16 | return p1;
17 |
18 | case "n":
19 | return typeof tag.num === "number" ? String(tag.num) : "";
20 |
21 | case "k":
22 | return tag.key;
23 | case "f":
24 | return tag.fullKey;
25 |
26 | default:
27 | num = Number(p1);
28 |
29 | return Number.isNaN(num) ? match : tag.values[num] ?? "";
30 | }
31 | };
32 |
33 | export const defRecipe = (
34 | options: {
35 | tagname?: string;
36 | } = {},
37 | ) => >(registrar: Registrar) => {
38 | const { tagname = "def" } = options;
39 |
40 | const innerOptions = { optics: [separated({ sep: "::" })] };
41 |
42 | const defFilter = (tag: TagNode): WeakFilterResult => {
43 | const [definedTag, template] = tag.values;
44 |
45 | const innerFilter = (tag: TagNode) => {
46 | const result = template.replace(paramPattern, matcher(tag));
47 |
48 | return {
49 | result: result,
50 | parse: true,
51 | };
52 | };
53 |
54 | registrar.register(definedTag, innerFilter, innerOptions);
55 |
56 | return {
57 | ready: true,
58 | };
59 | };
60 |
61 | registrar.register(tagname, defFilter, defOptions);
62 | };
63 |
--------------------------------------------------------------------------------
/src/recipes/preferenceStore/boolStore.ts:
--------------------------------------------------------------------------------
1 | import type { Registrar } from "../../types";
2 |
3 | import {
4 | PreferenceStore,
5 | storeTemplate,
6 | defaultSeparated,
7 | innerSeparated,
8 | } from "./storeTemplate";
9 |
10 | import { mapped } from "../../template/optics";
11 |
12 | class BoolStore extends PreferenceStore {
13 | on(selector: string): void {
14 | this.set(selector, true);
15 | }
16 |
17 | off(selector: string): void {
18 | this.set(selector, false);
19 | }
20 | }
21 |
22 | const boolStoreFilterTemplate = storeTemplate(BoolStore);
23 |
24 | export const activateRecipe = ({
25 | tagname = "on",
26 | storeId = "active",
27 | optic = defaultSeparated,
28 | } = {}) => (registrar: Registrar>) =>
29 | registrar.register(
30 | tagname,
31 | boolStoreFilterTemplate(storeId, false, (selector) => (activateMap) => {
32 | activateMap.on(selector);
33 | }),
34 | {
35 | separators: [optic, mapped(), innerSeparated],
36 | },
37 | );
38 |
39 | export const deactivateRecipe = >({
40 | tagname = "off",
41 | storeId = "active",
42 | optic = defaultSeparated,
43 | } = {}) => (registrar: Registrar) =>
44 | registrar.register(
45 | tagname,
46 | boolStoreFilterTemplate(storeId, false, (selector) => (activateMap) =>
47 | activateMap.off(selector),
48 | ),
49 | {
50 | separators: [optic, mapped(), innerSeparated],
51 | },
52 | );
53 |
--------------------------------------------------------------------------------
/src/recipes/preferenceStore/index.ts:
--------------------------------------------------------------------------------
1 | export type StoreGetter = {
2 | get: (key: string, num: number | null | undefined, occur: number) => T;
3 | };
4 |
5 | export const constantGet = (v: T): StoreGetter => ({ get: () => v });
6 |
7 | export { setNumberRecipe as setNumber } from "./numberStore";
8 | export {
9 | activateRecipe as activate,
10 | deactivateRecipe as deactivate,
11 | } from "./boolStore";
12 |
--------------------------------------------------------------------------------
/src/recipes/preferenceStore/numberStore.ts:
--------------------------------------------------------------------------------
1 | import type { Registrar } from "../../types";
2 |
3 | import {
4 | PreferenceStore,
5 | storeTemplate,
6 | defaultSeparated,
7 | innerSeparated,
8 | } from "./storeTemplate";
9 |
10 | import { mapped } from "../../template/optics";
11 |
12 | class NumberStore extends PreferenceStore {
13 | setNumber(selector: string, value: number): void {
14 | this.set(selector, Number.isNaN(value) ? 0 : value);
15 | }
16 | }
17 |
18 | const numStoreFilterTemplate = storeTemplate(NumberStore);
19 |
20 | export const setNumberRecipe = >({
21 | tagname = "set",
22 | storeId = "numerical",
23 | separator = defaultSeparated,
24 | assignmentSeparator = innerSeparated,
25 | } = {}) => (registrar: Registrar) =>
26 | registrar.register(
27 | tagname,
28 | numStoreFilterTemplate(storeId, 0, (selector, val) => (numberMap) =>
29 | numberMap.setNumber(selector, Number(val)),
30 | ),
31 | {
32 | optics: [separator, mapped(), assignmentSeparator],
33 | },
34 | );
35 |
--------------------------------------------------------------------------------
/src/recipes/preferenceStore/storeTemplate.ts:
--------------------------------------------------------------------------------
1 | import type { TagNode, Internals } from "../../types";
2 |
3 | import { TagSelector } from "../../template/";
4 |
5 | import { separated } from "../../template/optics";
6 |
7 | export const defaultSeparated = separated({ sep: ";" });
8 | export const innerSeparated = separated({ sep: "=", trim: true, max: 2 });
9 |
10 | export class PreferenceStore {
11 | /**
12 | * Values can be stored in a preference store
13 | * Tags can inquire against value stores if they provide
14 | * 1. the storeId, and
15 | * 2. their identification, typically (key, num, fullOccur)
16 | *
17 | * Values cannot be updated, only overwritten
18 | * You would save settings regarding a specific tag in here,
19 | * not make a shared value, for that see `SharedStore` (TODO)
20 | */
21 |
22 | selectors: [TagSelector, T][];
23 | defaultValue: T;
24 |
25 | constructor(defaultValue: T) {
26 | this.defaultValue = defaultValue;
27 | this.selectors = [];
28 | }
29 |
30 | protected set(selector: string, value: T): void {
31 | this.selectors.unshift([TagSelector.make(selector), value]);
32 | }
33 |
34 | get(key: string, num: number, fullOccur: number): T {
35 | const found = this.selectors.find(([selector]) =>
36 | selector.check(key, num, fullOccur),
37 | );
38 |
39 | return found ? found[1] : this.defaultValue;
40 | }
41 | }
42 |
43 | export const storeTemplate = , U>(
44 | Store: new (u: U) => Store,
45 | ) => (
46 | storeId: string,
47 | defaultValue: U,
48 | operation: (...vals: string[]) => (a: Store) => void,
49 | ) => >(
50 | tag: TagNode,
51 | { cache }: Internals,
52 | ) => {
53 | const commands = tag.values;
54 |
55 | commands.forEach((cmd: string) => {
56 | cache.over(storeId, operation(...cmd), new Store(defaultValue));
57 | });
58 |
59 | return { ready: true };
60 | };
61 |
--------------------------------------------------------------------------------
/src/recipes/sharedStore/index.ts:
--------------------------------------------------------------------------------
1 | export { setListRecipe as setList } from "./setList";
2 |
3 | export {
4 | pickRandomRecipe as pick,
5 | pickIndexRecipe as pickIndex,
6 | pickCardNumberRecipe as pickCardNumber,
7 | } from "./pickers";
8 |
--------------------------------------------------------------------------------
/src/recipes/sharedStore/listStore.ts:
--------------------------------------------------------------------------------
1 | import { SharedStore, storeTemplate } from "./storeTemplate";
2 |
3 | export class ListStore extends SharedStore {
4 | setList(storeKey: string, list: string[]): void {
5 | this.set(storeKey, list);
6 | }
7 |
8 | overwriteList(storeKey: string, newList: string[], fromIndex = 0): void {
9 | const list = this.getList(storeKey);
10 |
11 | for (let i = 0; i < newList.length; i++) {
12 | list[fromIndex + i] = newList[i];
13 | }
14 |
15 | this.setList(storeKey, list);
16 | }
17 |
18 | getList(storeKey: string): string[] {
19 | return this.get(storeKey, []);
20 | }
21 | }
22 |
23 | export const listStoreTemplate = storeTemplate(ListStore);
24 |
--------------------------------------------------------------------------------
/src/recipes/sharedStore/storeTemplate.ts:
--------------------------------------------------------------------------------
1 | import type { TagNode, Internals } from "../../types";
2 |
3 | import { Storage } from "../../filterManager/storage";
4 |
5 | export class SharedStore extends Storage {
6 | /**
7 | * Values in here are accessed with:
8 | * 1. the storeId, and
9 | * 2. a secondary storeKey
10 | *
11 | * Values can be updated
12 | * You would save values here that are shared among tags in here,
13 | * for configuring behavior of tags, see `PreferenceStore`
14 | */
15 | }
16 |
17 | export const storeTemplate = , U>(
18 | Store: new (map: Map) => Store,
19 | ) => (
20 | storeId: string,
21 | operation: >(
22 | tag: TagNode,
23 | internals: Internals,
24 | ) => (store: Store) => string | void,
25 | ) => >(
26 | tag: TagNode,
27 | internals: Internals,
28 | ) => {
29 | const result = (internals.cache.over(
30 | storeId,
31 | operation(tag, internals),
32 | new Store(new Map()),
33 | ) ?? "") as string;
34 |
35 | return { ready: true, result: result };
36 | };
37 |
--------------------------------------------------------------------------------
/src/recipes/shuffling.ts:
--------------------------------------------------------------------------------
1 | import type { TagNode, Registrar, Internals, Eval, Un } from "../types";
2 |
3 | import { Stylizer } from "../stylizer";
4 | import { acrossNumberedTag } from "../sequencers";
5 | import { topUp } from "../sortInStrategies";
6 |
7 | import { separated } from "../template/optics";
8 |
9 | const defaultStylizer = Stylizer.make({
10 | processor: (s: string) => `${s} `,
11 | mapper: (s: string) => `${s} `,
12 | separator: ' ',
13 | });
14 |
15 | const defaultEval = (
16 | stylizer: Stylizer,
17 | mixedValues: string[],
18 | ): Eval => (): string => stylizer.stylize(mixedValues);
19 |
20 | const defaultOptics = [separated({ sep: "||" })];
21 |
22 | export const shufflingRecipe = ({
23 | tagname = "mix",
24 | stylizer = defaultStylizer,
25 | evaluate = defaultEval,
26 | sortInStrategy = topUp,
27 | sequencer = acrossNumberedTag,
28 | optics = defaultOptics,
29 | } = {}) => (registrar: Registrar) => {
30 | const shuffleFilter = (tag: TagNode, internals: Internals) => {
31 | const getValues: Eval = ({ values }: TagNode): string[] =>
32 | values ?? [];
33 |
34 | const shuffler: Eval = sequencer(
35 | getValues as any,
36 | sortInStrategy,
37 | ) as Eval;
38 |
39 | const maybeValues = shuffler(tag, internals);
40 | if (maybeValues) {
41 | return evaluate(stylizer, maybeValues)(tag, internals as any);
42 | }
43 | };
44 |
45 | registrar.register(tagname, shuffleFilter, { optics });
46 | };
47 |
--------------------------------------------------------------------------------
/src/recipes/simple.ts:
--------------------------------------------------------------------------------
1 | import type { TagNode, WeakFilter, Recipe, Registrar, Un } from "../types";
2 |
3 | import { Stylizer } from "../stylizer";
4 | import { id } from "../utils";
5 | import { separated } from "../template/optics";
6 |
7 |
8 | // Used for internals, for externals applyRecipe should be preferred
9 | export const simpleRecipe = (
10 | weakFilter: WeakFilter,
11 | ): Recipe => ({
12 | tagname = "s",
13 | }: {
14 | tagname?: string,
15 | } = {}) => (registrar: Registrar) => {
16 | registrar.register(tagname, weakFilter);
17 | };
18 |
19 | const applyToValues = (f: (v: V) => V): WeakFilter => (tag: TagNode): V => f(tag.values)
20 |
21 | export const applyRecipe = ({
22 | tagname = "s",
23 | apply = applyToValues(id),
24 | optics = [],
25 | } = {}) => (registrar: Registrar) => {
26 | registrar.register(tagname, apply, { optics });
27 | };
28 |
29 | export const processRecipe = ({
30 | tagname = "s",
31 | processor = id,
32 | optics = [],
33 | } = {}) => (registrar: Registrar) => {
34 | registrar.register(tagname, applyToValues(processor), { optics });
35 | };
36 |
37 | export const styleRecipe = ({
38 | tagname = "s",
39 | stylizer = Stylizer.make(),
40 | optics = [separated("::")],
41 | } = {}) => (registrar: Registrar) => {
42 | registrar.register(tagname, applyToValues(stylizer.stylize.bind(stylizer)), { optics });
43 | };
44 |
--------------------------------------------------------------------------------
/src/sortInStrategies.ts:
--------------------------------------------------------------------------------
1 | import { shuffle } from "./utils";
2 |
3 | export type SortInStrategy = (indices: number[], toLength: number) => number[];
4 |
5 | export const topUp: SortInStrategy = (
6 | indices: number[],
7 | toLength: number,
8 | ): number[] => {
9 | /**
10 | * topUpSortingIndices([0,1], 4) => [0,1,2,3] or [0,1,3,2]
11 | */
12 |
13 | if (indices.length >= toLength) {
14 | // indices already have sufficient length
15 | return indices;
16 | }
17 |
18 | const newIndices = shuffle(
19 | Array.from(
20 | new Array(toLength - indices.length),
21 | (_x: undefined, i: number) => i + indices.length,
22 | ),
23 | );
24 |
25 | const result = [...indices, ...newIndices];
26 |
27 | return result;
28 | };
29 |
30 | export const mixIn: SortInStrategy = (
31 | indices: number[],
32 | toLength: number,
33 | ): number[] => {
34 | /**
35 | * topUpSortingIndices([0,1], 4) => [2,3,0,1], [2,0,3,1], [0,1,2,3], [0,1,3,2], etc.
36 | */
37 |
38 | if (indices.length >= toLength) {
39 | // indices already have sufficient length
40 | return indices;
41 | }
42 |
43 | const newIndices = Array.from(
44 | new Array(toLength - indices.length),
45 | (_x: undefined, i: number) => i + indices.length,
46 | );
47 |
48 | const result = [...indices];
49 | newIndices.forEach((newIndex: number) =>
50 | result.splice(
51 | Math.floor(
52 | Math.random() * (result.length + 1),
53 | ) /* possible insertion positions */,
54 | 0,
55 | newIndex,
56 | ),
57 | );
58 |
59 | return result;
60 | };
61 |
--------------------------------------------------------------------------------
/src/styleList.ts:
--------------------------------------------------------------------------------
1 | import type { Stylizer, Eval, TagNode, Internals, WeakFilter } from "./types";
2 |
3 | const transpose = (array: (T | T[])[]): T[][] =>
4 | Array.isArray(array[0])
5 | ? array[0].map((_, index) =>
6 | array.map((value: any /* T[] */) => value[index]),
7 | )
8 | : [array];
9 |
10 | export type StyleList = (string | [string, ...any[]])[];
11 |
12 | export const listStylize = >(
13 | stylizer: Stylizer,
14 | toList: Eval,
15 | ): WeakFilter => (tag: TagNode, internals: Internals) =>
16 | internals.ready
17 | ? stylizer.stylize(
18 | ...(transpose(toList(tag, internals)) as [any, ...any[]]),
19 | )
20 | : { ready: false };
21 |
22 | export const listStylizeMaybe = >(
23 | stylizer: Stylizer,
24 | toListMaybe: Eval,
25 | ): WeakFilter => (tag: TagNode, internals: Internals) => {
26 | if (!internals.ready) {
27 | return { ready: false };
28 | }
29 |
30 | const maybeValues = toListMaybe(tag, internals);
31 |
32 | if (!maybeValues) {
33 | return { ready: false };
34 | }
35 |
36 | return stylizer.stylize(...(transpose(maybeValues) as [any, ...any[]]));
37 | };
38 |
--------------------------------------------------------------------------------
/src/stylizer.ts:
--------------------------------------------------------------------------------
1 | import { id } from "./utils";
2 |
3 | type StringFunction = (v: string, ...a: any[]) => string;
4 | type StringPlusFunction = (v: string, i: number, ...a: any[]) => string;
5 |
6 | export class Stylizer {
7 | protected readonly separator: string;
8 | protected readonly mapper: StringPlusFunction;
9 | protected readonly processor: StringFunction;
10 |
11 | protected constructor(
12 | separator: string,
13 | mapper: StringPlusFunction,
14 | processor: StringFunction,
15 | ) {
16 | this.separator = separator;
17 | this.mapper = mapper;
18 | this.processor = processor;
19 | }
20 |
21 | static make({
22 | separator = "",
23 | mapper = id as StringPlusFunction,
24 | processor = id as StringFunction,
25 | } = {}) {
26 | return new Stylizer(separator, mapper, processor);
27 | }
28 |
29 | toStylizer({
30 | separator = this.separator,
31 | mapper = this.mapper,
32 | processor = this.processor,
33 | } = {}): Stylizer {
34 | return new Stylizer(separator, mapper, processor);
35 | }
36 |
37 | stylize(input: string[], ...args: unknown[][]): string {
38 | return this.processor(
39 | input
40 | .flatMap((v: string, i: number, l: string[]) =>
41 | this.mapper(v, i, ...args.map((arg) => arg[i]), l),
42 | )
43 | .join(this.separator),
44 | );
45 | }
46 |
47 | /* improved version of stylize */
48 | stylizeFull(
49 | input: string[],
50 | mapArgs: unknown[][] = [],
51 | processArgs: unknown[] = [],
52 | ): string {
53 | return this.processor(
54 | input
55 | .flatMap((v: string, i: number, l: string[]) =>
56 | this.mapper(v, i, ...mapArgs.map((arg) => arg[i]), l),
57 | )
58 | .join(this.separator),
59 | ...processArgs,
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/template/anki/index.ts:
--------------------------------------------------------------------------------
1 | export { init, initialize } from "./initialize";
2 | export { ankiLog } from "./utils";
3 | export { getQaChildNodes } from "./qaNodes";
4 |
--------------------------------------------------------------------------------
/src/template/anki/qaNodes.ts:
--------------------------------------------------------------------------------
1 | import type { ChildNodeSpan, BrowserTemplateNode } from "../browser";
2 | import { interspliceChildNodes } from "../browser";
3 |
4 | const isElement = (tag: Node): tag is Element =>
5 | tag.nodeType === Node.ELEMENT_NODE;
6 |
7 | const isStyleElement = (tag: Element): tag is HTMLStyleElement =>
8 | tag.tagName === "STYLE";
9 |
10 | const isScriptElement = (tag: Element): tag is HTMLScriptElement =>
11 | tag.tagName === "SCRIPT";
12 |
13 | const isJavaScriptElement = (tag: Element): tag is HTMLScriptElement =>
14 | isScriptElement(tag) &&
15 | (tag.type.length === 0 || tag.type.endsWith("javascript"));
16 |
17 | // Anki Asset Manager support
18 | const isAnkiAssetManagerElement = (tag: Element): tag is HTMLDivElement =>
19 | tag.id === "anki-am";
20 |
21 | const isTemplateableElement = (tag: Node): boolean =>
22 | isElement(tag) &&
23 | (isStyleElement(tag) ||
24 | isJavaScriptElement(tag) ||
25 | isAnkiAssetManagerElement(tag));
26 |
27 | export const getQaChildNodes = (): ChildNodeSpan[] | null => {
28 | if (!globalThis.document) {
29 | return null;
30 | }
31 |
32 | const qa = globalThis.document.getElementById("qa");
33 |
34 | if (!qa) {
35 | return null;
36 | }
37 |
38 | return interspliceChildNodes(qa, {
39 | type: "predicate",
40 | value: (tag: BrowserTemplateNode): boolean =>
41 | !isTemplateableElement(tag as Node),
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/src/template/anki/utils.ts:
--------------------------------------------------------------------------------
1 | interface DebugMode {
2 | closetDebug: boolean;
3 | }
4 |
5 | export const ankiLog = (...objs: unknown[]): boolean => {
6 | if (!(globalThis as typeof globalThis & DebugMode).closetDebug) {
7 | return false;
8 | }
9 |
10 | const logDiv = Object.assign(document.createElement("div"), {
11 | className: "closet-log",
12 | innerText: objs.map(String).join("\n"),
13 | });
14 |
15 | const qa = document.getElementById("qa");
16 |
17 | if (!qa) {
18 | return false;
19 | }
20 |
21 | qa.appendChild(logDiv);
22 | return true;
23 | };
24 |
--------------------------------------------------------------------------------
/src/template/browser/intersplice.ts:
--------------------------------------------------------------------------------
1 | import type { ChildNodePredicate, BrowserTemplateNode } from "./childNodes";
2 |
3 | import { ChildNodeSpan } from "./childNodes";
4 |
5 | const makePositions = (
6 | template: ChildNodePredicate,
7 | currentIndex = 0,
8 | ): [ChildNodePredicate, ChildNodePredicate] => {
9 | const fromSkip: ChildNodePredicate = {
10 | type: "predicate",
11 | value: template.value,
12 | startAtIndex: currentIndex,
13 | exclusive: false,
14 | };
15 | const toSkip: ChildNodePredicate = {
16 | type: "predicate",
17 | value: (v: BrowserTemplateNode): boolean => !template.value(v),
18 | startAtIndex: currentIndex,
19 | exclusive: true,
20 | };
21 |
22 | return [fromSkip, toSkip];
23 | };
24 |
25 | export const interspliceChildNodes = (
26 | parent: Element,
27 | skip: ChildNodePredicate,
28 | ): ChildNodeSpan[] => {
29 | const result: ChildNodeSpan[] = [];
30 | let currentSpan = new ChildNodeSpan(parent, ...makePositions(skip));
31 |
32 | while (currentSpan.valid) {
33 | result.push(currentSpan);
34 |
35 | currentSpan = new ChildNodeSpan(
36 | parent,
37 | ...makePositions(skip, currentSpan.to + 1),
38 | );
39 | }
40 |
41 | return result;
42 | };
43 |
--------------------------------------------------------------------------------
/src/template/delimiters.ts:
--------------------------------------------------------------------------------
1 | export interface Delimiters {
2 | open: string;
3 | sep: string;
4 | close: string;
5 | }
6 |
7 | export const defaultDelimiters = {
8 | open: "[[",
9 | sep: "::",
10 | close: "]]",
11 | };
12 |
--------------------------------------------------------------------------------
/src/template/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | Template,
3 | TagRenderer,
4 | TemplateInfo,
5 | IterationInfo,
6 | ResultInfo,
7 | } from "./template";
8 | export { BrowserTemplate } from "./browser";
9 | export { Parser } from "./parser";
10 | export { TagSelector } from "./tagSelector";
11 |
12 | export * as browser from "./browser";
13 | export * as anki from "./anki";
14 | export * as optics from "./optics";
15 |
--------------------------------------------------------------------------------
/src/template/optics/circumfix.ts:
--------------------------------------------------------------------------------
1 | export interface Circumfix {
2 | before: string;
3 | after: string;
4 | }
5 |
6 | export type WeakCircumfix = Partial | string;
7 |
8 | export const weakCircumfixToCircumfix = (ws: WeakCircumfix): Circumfix =>
9 | typeof ws === "string"
10 | ? {
11 | before: ws,
12 | after: ws,
13 | }
14 | : {
15 | before: ws.before ?? "",
16 | after: ws.after ?? "",
17 | };
18 |
--------------------------------------------------------------------------------
/src/template/optics/consumers.ts:
--------------------------------------------------------------------------------
1 | import type { ProfunctorDict } from "./profunctors";
2 |
3 | export const run = (zooms: any, dict: ProfunctorDict, f: (a: A) => B) => {
4 | return zooms.reverse().reduce((accu: any, z: any) => z(dict, accu), f);
5 | };
6 |
--------------------------------------------------------------------------------
/src/template/optics/index.ts:
--------------------------------------------------------------------------------
1 | export type { Optic } from "./utils";
2 | export type { WeakCircumfix } from "./circumfix";
3 | export type { WeakSeparator } from "./separated";
4 |
5 | export { run } from "./consumers";
6 | export { dictFunction, dictForget } from "./profunctors";
7 |
8 | export { separated } from "./separated";
9 | export { templated, templatedRegex } from "./templated";
10 | export { stripped, strippedRegex } from "./stripped";
11 | export { mapped } from "./mapped";
12 |
--------------------------------------------------------------------------------
/src/template/optics/mapped.ts:
--------------------------------------------------------------------------------
1 | import type { ProfunctorDict } from "./profunctors";
2 | import type { Optic } from "./utils";
3 |
4 | const fmap = (f: (a: A) => B) => (xs: A[]): B[] => {
5 | return xs.map(f);
6 | };
7 |
8 | export const mapped = (): Optic => {
9 | return (_dict: ProfunctorDict, f0: (a: A) => B) => {
10 | return fmap(f0);
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/src/template/optics/stripped.ts:
--------------------------------------------------------------------------------
1 | import type { ProfunctorDict } from "./profunctors";
2 | import type { Optic } from "./utils";
3 | import { WeakCircumfix, weakCircumfixToCircumfix } from "./circumfix";
4 | import { escapeRegExp, regExpString } from "./utils";
5 |
6 | export const strippedRegex = (wc: WeakCircumfix): Optic => {
7 | const { before, after } = weakCircumfixToCircumfix(wc);
8 |
9 | const getter = (text: string): [string, null] => {
10 | const regex = new RegExp(
11 | `^${regExpString(before)}(.*?)${regExpString(after)}$`,
12 | "su",
13 | );
14 | let match = text;
15 |
16 | text.replace(regex, (_match: string, inner: string): string => {
17 | match = inner;
18 | return "";
19 | });
20 |
21 | return [match, null];
22 | };
23 |
24 | const setter = ([repl]: [string, null]): string => {
25 | return repl;
26 | };
27 |
28 | return (
29 | dict: ProfunctorDict,
30 | f0: (s: string) => string,
31 | ): ((s: string) => string) => {
32 | const f1 = dict.first(f0);
33 | const f2 = dict.dimap(getter, setter, f1 as any);
34 | return f2 as any;
35 | };
36 | };
37 |
38 | export const stripped = ({
39 | before,
40 | after,
41 | }: {
42 | before: string;
43 | after: string;
44 | }): Optic =>
45 | strippedRegex({
46 | before: `^${escapeRegExp(before)}`,
47 | after: `${escapeRegExp(after)}$`,
48 | });
49 |
--------------------------------------------------------------------------------
/src/template/optics/utils.ts:
--------------------------------------------------------------------------------
1 | import { ProfunctorDict } from "./profunctors";
2 |
3 | // to hard to type in TS
4 | export type Optic = (
5 | dict: ProfunctorDict,
6 | f: (a: any) => any,
7 | ) => (x: any) => any;
8 |
9 | export const escapeRegExp = (s: string): string => {
10 | return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11 | };
12 |
13 | export const regExpString = (re: string | RegExp): string => {
14 | return re instanceof RegExp ? re.source : re;
15 | };
16 |
--------------------------------------------------------------------------------
/src/template/parser/.gitignore:
--------------------------------------------------------------------------------
1 | grammar.ts
2 |
--------------------------------------------------------------------------------
/src/template/parser/grammar.ne:
--------------------------------------------------------------------------------
1 | @{%
2 | import { TextNode, EscapedNode } from '../nodes'
3 |
4 | import { tagBuilder } from './tagBuilder'
5 | %}
6 |
7 | @preprocessor typescript
8 | @lexer tokenizer
9 |
10 | #################################
11 |
12 | start -> content {% id %}
13 |
14 | content -> node:* {% id %}
15 | node -> text {% id %}
16 | | inlinetag {% id %}
17 | | blocktag {% id %}
18 |
19 | text -> %text {% ([match]) => new TextNode(match.value) %}
20 | | %escapeseq {% ([match]) => new EscapedNode(match.value) %}
21 |
22 | inlinetag -> %inlineopen inline {%
23 | ([/* open */, [name, inlineNodes, hasInline]]) => tagBuilder.build(
24 | name.value,
25 | inlineNodes,
26 | hasInline,
27 | )
28 | %}
29 |
30 | blocktag -> %blockopen inline content blockclose {%
31 | ([/* open */, [name, inlineNodes, hasInline], blockNodes, closename], _location, reject) =>
32 | !closename || name.value === closename.value
33 | ? tagBuilder.build(
34 | name.value,
35 | inlineNodes,
36 | hasInline,
37 | blockNodes,
38 | true,
39 | )
40 | : reject
41 | %}
42 |
43 | inline -> %keyname (%sep content):? %close {% ([name, match]) => match
44 | ? [name, match[1], true]
45 | : [name, [], false]
46 | %}
47 |
48 | blockclose -> %blockclose %keyname:? %close {% ([,name]) => name %}
49 |
--------------------------------------------------------------------------------
/src/template/tagSelector/.gitignore:
--------------------------------------------------------------------------------
1 | grammar.ts
2 |
--------------------------------------------------------------------------------
/src/template/tagSelector/tokenizer.ts:
--------------------------------------------------------------------------------
1 | import type { Lexer } from "moo";
2 |
3 | import { compile } from "moo";
4 |
5 | export const tagSelectorTokenizer: Lexer = compile({
6 | text: {
7 | match: /[a-zA-Z_]+/u,
8 | },
9 |
10 | numDigits: {
11 | match: /\d+(?=:|$)/u,
12 | },
13 |
14 | digits: {
15 | match: /\d+/u,
16 | },
17 |
18 | slash: {
19 | match: "/",
20 | },
21 |
22 | escapeseq: {
23 | match: /%\w/u,
24 | },
25 |
26 | // has to come before star
27 | multiplierSeq: {
28 | match: /\*n\+/u,
29 | },
30 |
31 | allStar: {
32 | match: /^\*$/u,
33 | },
34 |
35 | numStar: {
36 | match: /\*(?=:|$)/u,
37 | },
38 |
39 | star: {
40 | match: "*",
41 | },
42 |
43 | groupOpen: {
44 | match: "{",
45 | },
46 |
47 | groupAlternative: {
48 | match: ",",
49 | },
50 |
51 | rangeSpecifier: {
52 | match: /-/u,
53 | },
54 |
55 | numGroupClose: {
56 | match: /\}(?=:|$)/u,
57 | },
58 |
59 | groupClose: {
60 | match: "}",
61 | },
62 |
63 | occurSep: {
64 | match: ":",
65 | },
66 | });
67 |
--------------------------------------------------------------------------------
/src/template/types.ts:
--------------------------------------------------------------------------------
1 | import type { ASTNode, TagNode } from "./nodes";
2 | import type { Optic } from "./optics";
3 |
4 | export type TagPath = number[];
5 |
6 | export enum Status {
7 | Ready,
8 | NotReady,
9 | ContinueTags,
10 | // when ready == true; result needs parsing, iterate down the new structure
11 | ContainsTags,
12 | // when ready == false; result needs parsing, but continue in normal order
13 | }
14 |
15 | export interface ProcessorOutput {
16 | result: string | ASTNode[] | null;
17 | status: Status;
18 | }
19 |
20 | export interface DataOptions {
21 | optics?: Optic[];
22 | blockOptics: Optic[];
23 | inlineOptics: Optic[];
24 | capture: boolean;
25 | [k: string]: unknown;
26 | }
27 |
28 | export interface RoundInfo {
29 | path: TagPath;
30 | depth: number;
31 | ready: boolean;
32 | isCapture: boolean;
33 | }
34 |
35 | export type TagAccessor = (name: string) => TagProcessor;
36 |
37 | export interface TagProcessor {
38 | execute: (data: TagNode, round: RoundInfo) => ProcessorOutput;
39 | getOptions: () => DataOptions;
40 | }
41 |
--------------------------------------------------------------------------------
/src/template/utils.ts:
--------------------------------------------------------------------------------
1 | export function* intersperse2d(
2 | lists: T[][],
3 | delim: T,
4 | ): Generator {
5 | let first = true;
6 |
7 | for (const list of lists) {
8 | if (first) {
9 | first = false;
10 | } else {
11 | yield delim;
12 | }
13 |
14 | for (const item of list) {
15 | yield item;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const id = (v: T): T => v;
2 | export const id2 = (_v: U, w: T): T => w;
3 | export const constant = (x: T) => (_y: U) => x;
4 |
5 | export const zeroWidthSpace = "";
6 |
7 | export const shuffle = (array: T[]): T[] => {
8 | const result = array.slice(0);
9 | let currentIndex = array.length;
10 |
11 | // While there remain elements to shuffle...
12 | while (currentIndex !== 0) {
13 | // Pick a remaining element...
14 | const randomIndex = Math.floor(Math.random() * currentIndex);
15 | currentIndex -= 1;
16 |
17 | // And swap it with the current element.
18 | const temporaryValue: T = result[currentIndex];
19 | result[currentIndex] = result[randomIndex];
20 | result[randomIndex] = temporaryValue;
21 | }
22 |
23 | return result;
24 | };
25 |
26 | export const sortWithIndices = (items: T[], indices: number[]): T[] => {
27 | const result: T[] = [];
28 |
29 | for (const idx of indices) {
30 | const maybeItem: T = items[idx];
31 |
32 | if (maybeItem) {
33 | result.push(maybeItem);
34 | }
35 | }
36 |
37 | if (indices.length < items.length) {
38 | const remainingItemIndices: number[] = Array.from(
39 | new Array(items.length - indices.length),
40 | (_x, i) => i + indices.length,
41 | );
42 |
43 | for (const idx of remainingItemIndices) {
44 | result.push(items[idx]);
45 | }
46 | }
47 |
48 | return result;
49 | };
50 |
--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------
1 | // NOTE should be same as under ../anki/src/utils.py
2 | export const versionInfo = [0 /* MAJOR */, 6 /* MINOR */, 1 /* PATCH */];
3 |
4 | export const prereleaseInfo = [];
5 |
6 | export const version =
7 | versionInfo.join(".") +
8 | (prereleaseInfo.length > 0 ? `-${prereleaseInfo.join(".")}` : "");
9 |
--------------------------------------------------------------------------------
/src/wrappers/collection.ts:
--------------------------------------------------------------------------------
1 | import type { Registrar, Recipe, WrapOptions } from "../types";
2 |
3 | import { defaultTagnameGetter, defaultTagnameSetter } from "./wrappers";
4 |
5 | type RecipePart> = [
6 | Recipe,
7 | Partial,
8 | ];
9 |
10 | export const collection = >(
11 | recipeParts: RecipePart[],
12 | ): Recipe => (options?: Record) => (
13 | registrar: Registrar,
14 | ) => {
15 | const theOptions = options ?? {};
16 |
17 | for (const [recipe, wrapOptions = {}] of recipeParts) {
18 | const {
19 | setTagnames = defaultTagnameSetter,
20 | getTagnames = defaultTagnameGetter,
21 | } = wrapOptions;
22 |
23 | recipe(setTagnames(theOptions, getTagnames(theOptions)))(registrar);
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/wrappers/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | wrap,
3 | wrapWithDeferred as deferred,
4 | wrapWithAftermath as aftermath,
5 | } from "./wrappers";
6 | export { sum, sumFour } from "./sum";
7 | export { product } from "./product";
8 | export { collection } from "./collection";
9 |
--------------------------------------------------------------------------------
/src/wrappers/product.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | TagNode,
3 | RecipeOptions,
4 | Registrar,
5 | WeakFilter,
6 | WeakFilterResult,
7 | Internals,
8 | Recipe,
9 | WrapOptions,
10 | } from "../types";
11 |
12 | import { defaultTagnameGetter, defaultTagnameSetter } from "./wrappers";
13 |
14 | export const product = >(
15 | recipeFirst: Recipe,
16 | recipeSecond: Recipe,
17 | multiply: (
18 | fst: WeakFilterResult,
19 | snd: WeakFilterResult,
20 | ) => WeakFilter = () => () => ({ ready: true }),
21 | { wrapId, setTagnames }: WrapOptions = {
22 | wrapId: "product",
23 | getTagnames: defaultTagnameGetter,
24 | setTagnames: defaultTagnameSetter,
25 | },
26 | ): Recipe => ({
27 | tagname = "prod",
28 |
29 | optionsFirst = {},
30 | optionsSecond = {},
31 | }: {
32 | tagname?: string;
33 |
34 | optionsFirst?: RecipeOptions;
35 | optionsSecond?: RecipeOptions;
36 | } = {}) => (registrar: Registrar) => {
37 | const tagnameTrue = `${tagname}:${wrapId}:fst`;
38 | const tagnameFalse = `${tagname}:${wrapId}:snd`;
39 |
40 | recipeFirst(setTagnames(optionsFirst, [tagnameTrue]))(registrar);
41 | recipeSecond(setTagnames(optionsSecond, [tagnameFalse]))(registrar);
42 |
43 | const productFilter = (tag: TagNode, internals: Internals) => {
44 | return multiply(
45 | internals.filters.getOrDefault(tagnameTrue)(tag, internals),
46 | internals.filters.getOrDefault(tagnameFalse)(tag, internals),
47 | )(tag, internals);
48 | };
49 |
50 | registrar.register(
51 | tagname,
52 | productFilter,
53 | registrar.getOptions(tagnameTrue /* have to be same for True/False */),
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/style/README.adoc:
--------------------------------------------------------------------------------
1 | = Closet CSS classees
2 |
3 | .General
4 | ----
5 | .is-front
6 | .is-back
7 | .is-active
8 | .is-inactive
9 | ----
10 |
11 | .Clozes
12 | ----
13 | .closet-cloze
14 | .closet-cloze__answer
15 | .closet-cloze__hint
16 | .closet-cloze__ellipsis
17 | ----
18 |
19 | .Multiple Choice
20 | ----
21 | .closet-multiple-choice
22 | .closet-multiple-choice__item
23 | .closet-multiple-choice__separator
24 | .closet-multiple-choice__option
25 | .closet-multiple-choice__correct
26 | .closet-mulitple-choice__wrong
27 | .closet-mulitple-choice__ellipsis
28 | ----
29 |
--------------------------------------------------------------------------------
/style/_cloze.scss:
--------------------------------------------------------------------------------
1 | @use "utils";
2 |
3 | @mixin cloze {
4 | $denim-blue: #1560bd;
5 |
6 | .closet-cloze {
7 | @include utils.wrap("[", "]");
8 |
9 | &.is-active {
10 | color: $denim-blue;
11 | }
12 |
13 | &.is-back,
14 | &.is-inactive {
15 | @include utils.wrap-reset;
16 | }
17 | }
18 |
19 | .closet-cloze__hint:empty {
20 | @include utils.placeholder("...");
21 | }
22 |
23 | .closet-cloze__ellipsis {
24 | @include utils.placeholder("...");
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/style/_multiple-choice.scss:
--------------------------------------------------------------------------------
1 | @use "utils";
2 |
3 | @mixin multiple-choice {
4 | .closet-multiple-choice {
5 | @include utils.wrap("( ", " )");
6 |
7 | &.is-inactive {
8 | @include utils.wrap-reset;
9 | }
10 | }
11 |
12 | .closet-multiple-choice__separator {
13 | @include utils.placeholder(", ");
14 | }
15 |
16 | .closet-multiple-choice__option {
17 | color: orange;
18 | }
19 |
20 | .closet-multiple-choice__correct {
21 | color: lime;
22 | }
23 |
24 | .closet-multiple-choice__wrong {
25 | color: red;
26 | }
27 |
28 | .closet-multiple-choice__ellipsis {
29 | @include utils.placeholder("...");
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/style/_rect.scss:
--------------------------------------------------------------------------------
1 | @use "utils";
2 |
3 | @mixin rect {
4 | .closet-rect__rect {
5 | fill: moccasin;
6 | stroke: olive;
7 |
8 | .is-active & {
9 | fill: salmon;
10 | stroke: yellow;
11 | }
12 |
13 | .is-back.is-active & {
14 | fill: transparent;
15 | stroke: transparent;
16 | }
17 | }
18 |
19 | .closet-rect__ellipsis {
20 | fill: transparent;
21 | stroke: transparent;
22 | }
23 |
24 | .closet-rect__label {
25 | stroke: black;
26 | stroke-width: 0.5;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/style/_shuffle-question.scss:
--------------------------------------------------------------------------------
1 | @use "utils";
2 |
3 | @mixin shuffle-question($name) {
4 | .closet-#{$name} {
5 | @include utils.wrap("«", "»");
6 |
7 | &.is-active .closet-#{$name}__item {
8 | color: mediumpurple;
9 | }
10 |
11 | &.is-inactive {
12 | @include utils.wrap-reset;
13 | }
14 | }
15 |
16 | .closet-#{$name}__separator {
17 | @include utils.placeholder(", ");
18 | }
19 |
20 | .closet-#{$name}__ellipsis {
21 | @include utils.placeholder("...");
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/style/_shuffle.scss:
--------------------------------------------------------------------------------
1 | @use "utils";
2 |
3 | @mixin shuffle {
4 | .closet-shuffle__separator {
5 | @include utils.placeholder(", ");
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/style/_utils.scss:
--------------------------------------------------------------------------------
1 | @mixin wrap($before, $after) {
2 | &::before {
3 | content: $before;
4 | }
5 |
6 | &::after {
7 | content: $after;
8 | }
9 | }
10 |
11 | @mixin wrap-reset {
12 | &::before {
13 | content: normal;
14 | }
15 |
16 | &::after {
17 | content: normal;
18 | }
19 | }
20 |
21 | @mixin placeholder($s) {
22 | &::before {
23 | content: $s;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/style/base.scss:
--------------------------------------------------------------------------------
1 | @use "shuffle";
2 | @use "cloze";
3 | @use "multiple-choice";
4 | @use "shuffle-question";
5 | @use "rect";
6 |
7 | .android .card:not(.mathjax-rendered) {
8 | visibility: hidden;
9 | }
10 |
11 | .closet-delay {
12 | visibility: hidden;
13 | }
14 |
15 | .closet-log {
16 | color: black;
17 | background-color: rgb(255, 147, 40);
18 |
19 | margin-top: 1rem;
20 | padding: 1rem;
21 | border-radius: 0.5rem;
22 | }
23 |
24 | @include shuffle.shuffle;
25 |
26 | @include cloze.cloze;
27 | @include multiple-choice.multiple-choice;
28 | @include shuffle-question.shuffle-question("mingle");
29 | @include shuffle-question.shuffle-question("jumble");
30 | @include shuffle-question.shuffle-question("sort");
31 |
32 | @include rect.rect;
33 |
--------------------------------------------------------------------------------
/style/editor.scss:
--------------------------------------------------------------------------------
1 | @use "rect";
2 |
3 | img {
4 | max-width: 100% !important;
5 | max-height: var(--closet-max-height);
6 | }
7 |
8 | .closet-select-mode {
9 | /* slightly below top */
10 | vertical-align: 20%;
11 | /* night-mode fix */
12 | background-color: var(--frame-bg);
13 | border-color: var(--border);
14 | color: var(--text-fg);
15 | }
16 |
17 | @include rect.rect;
18 |
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | # Tests
2 |
3 | ## Browser tests
4 |
5 | Only work if you turn off CORS for the file protocol
6 | To do this in firefox, go to about:config, and set the following to false:
7 |
8 | - `security.fileuri.strict_origin_policy`
9 |
--------------------------------------------------------------------------------
/test/browser/.parentlock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/test/browser/.parentlock
--------------------------------------------------------------------------------
/test/browser/dist/README.md:
--------------------------------------------------------------------------------
1 | # Test dist
2 |
3 | Output of build-test goes here
4 |
--------------------------------------------------------------------------------
/test/browser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Mocha
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | You need to run `npm run serve-test` and open the served link!
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
get me
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/test/browser/specs/intersplice.spec.js:
--------------------------------------------------------------------------------
1 | import { interspliceChildNodes } from "../dist/src/browserUtils.js";
2 |
3 | const assert = chai.assert;
4 |
5 | describe("interspliceChildNodes", () => {
6 | before(() => {
7 | const parent = document.querySelector("#intersplicetest");
8 | assert.strictEqual(
9 | parent.childNodes.length,
10 | 15,
11 | "parent has wrong childNodes",
12 | );
13 | });
14 |
15 | it("finds individual nodes", () => {
16 | const parent = document.querySelector("#intersplicetest");
17 | const test = interspliceChildNodes(parent, {
18 | type: "predicate",
19 | value: (v) => v.className === "foo",
20 | });
21 |
22 | assert.lengthOf(test, 3, "should have 3 child nodes");
23 | });
24 |
25 | it("finds subspan from nodes", () => {
26 | const parent = document.querySelector("#intersplicetest");
27 | const test = interspliceChildNodes(parent, {
28 | type: "predicate",
29 | value: (v) =>
30 | v.className === "bar" || v.nodeType === Node.TEXT_NODE,
31 | });
32 |
33 | assert.lengthOf(test, 3, "should have 3 child nodes");
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/src/template/tagSelector.spec.ts:
--------------------------------------------------------------------------------
1 | import { TagSelector } from "../../../src/template/tagSelector";
2 |
3 | describe("parseTagSelector", () => {
4 | describe("catch-all patterns", () => {
5 | test("* matches everything", () => {
6 | const pred = TagSelector.make("*");
7 |
8 | expect(pred.check("c", 1, 5)).toBeTruthy();
9 | expect(pred.check("hello", null, 0)).toBeTruthy();
10 | });
11 |
12 | test("** matches everything", () => {
13 | const pred = TagSelector.make("**");
14 |
15 | expect(pred.check("c", 1, 5)).toBeTruthy();
16 | expect(pred.check("hello", null, 0)).toBeTruthy();
17 | });
18 |
19 | test("**:* matches everything", () => {
20 | const pred = TagSelector.make("**:*");
21 |
22 | expect(pred.check("c", 1, 5)).toBeTruthy();
23 | expect(pred.check("hello", null, 0)).toBeTruthy();
24 | });
25 |
26 | test("*{} matches any text, but no nums", () => {
27 | const pred = TagSelector.make("*{}");
28 |
29 | expect(pred.check("c", null, 5)).toBeTruthy();
30 | expect(pred.check("hello", null, 0)).toBeTruthy();
31 | expect(pred.check("cr", 1, 5)).toBeFalsy();
32 | expect(pred.check("hella", 2, 0)).toBeFalsy();
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "strict": true,
5 | "downlevelIteration": true,
6 | "moduleResolution": "node",
7 | "typeRoots" : [
8 | "./node_modules/@types",
9 | "./typings"
10 | ],
11 | "lib": [
12 | "es2019",
13 | "es2019.array",
14 | "dom"
15 | ]
16 | },
17 | "exclude": [
18 | "node_modules",
19 | "website",
20 | "dist",
21 | "build",
22 | "test",
23 | ],
24 | }
25 |
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator.
4 |
5 | ## Installation
6 |
7 | ```console
8 | yarn install
9 | ```
10 |
11 | ## Local Development
12 |
13 | ```console
14 | yarn start
15 | ```
16 |
17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ## Build
20 |
21 | ```console
22 | yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ## Deployment
28 |
29 | ```console
30 | GIT_USER= USE_SSH=true yarn deploy
31 | ```
32 |
33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
34 |
--------------------------------------------------------------------------------
/website/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
3 | plugins: [
4 | [
5 | "prismjs",
6 | {
7 | languages: ["javascript", "css", "html"],
8 | plugins: ["line-numbers", "show-language"],
9 | css: true,
10 | },
11 | ],
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/website/docs/clozes/creating.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Creating Clozes
3 | slug: /clozes/creating
4 | ---
5 |
6 | import Example from "@site/src/components/Example";
7 |
8 | ## What are clozes?
9 |
10 | According to Wikipedia:
11 |
12 | > A cloze test (also cloze deletion test) is an exercise, test, or assessment consisting of a portion of language with certain items, words, or signs removed (cloze text), where the participant is asked to replace the missing language item.
13 |
14 | ## Unnumbered Clozes
15 |
16 | Clozes are surrounded by c tags.
17 | By default, an unnumbered cloze will always be considered inactive.
18 |
19 |
20 |
21 | As you can see, absolutely nothing happens.
22 | This is the specified behavior: the tag is inactive, and the default behavior for inactive clozes of this kind is to show their content.
23 |
24 | You can make this more interesting by either using numbered clozes, or by using hiding or revealing clozes.
25 |
26 | ## Numbered Clozes
27 |
28 | Numbered clozes will be the active their respective context.
29 |
30 | :::anki
31 | This context is provided for us in the form of card types.
32 | Closet will use this information and render the card accordingly.
33 | :::
34 |
35 | ## Zero Clozes
36 |
37 | ## Showing, Hiding, and Revealing Clozes
38 |
39 | ## Hints
40 |
--------------------------------------------------------------------------------
/website/docs/doc1.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Style Guide
3 | slug: /
4 | ---
5 |
6 | import Example from "@site/src/components/Example";
7 |
8 | Code display example:
9 |
10 |
11 |
12 | and once more
13 |
14 |
15 |
--------------------------------------------------------------------------------
/website/docs/doc2.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: doc2
3 | title: Document Number 2
4 | ---
5 |
6 | This is a link to [another document.](doc1.mdx) This is a link to an [external page.](http://www.example.com/)
7 |
--------------------------------------------------------------------------------
/website/docs/flashcard.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Flashcard Filters
3 | ---
4 |
5 | _Flashcard filters_ describe a family of filters that are especially useful for flashcards.
6 | They include:
7 |
8 | - clozes
9 | - multiple choice questions (MCQs)
10 | - shuffling as flashcard types
11 | - specifications
12 | - occlusions
13 |
14 | They all have a few concepts in common:
15 |
16 | ## Question and answer context
17 |
18 | The rendering of the effect is _context-sensitive_.
19 | Depending on the context the effect is rendered differently:
20 |
21 | 1. as a _question_, _test_, or _front_, it needs to show the user the task to be solved, the question to be answered.
22 |
23 | :::info Examples
24 | For _clozes_, this could be an ellipsis, or a hint; for _MCQs_, this would typically be the answer options.
25 | :::
26 |
27 | 2. as the _answer_, _reveal_, or _back_, it needs to provide a solution to the prior task or question.
28 |
29 | :::info Examples
30 | For _clozes_, this would be the revealed answer; for _MCQs_ it would need to provide some highlight to the correct answer.
31 | :::
32 |
33 | For flashcards, _test_ will be synonmymous with the _front_, _answer_ with the _back_.
34 |
35 | ## Active and inactive context
36 |
37 | In a flashcard, you usually want to test each piece of knowledge individually.
38 | This is also called the [minimum information principle](https://www.supermemo.com/de/archives1990-2015/articles/20rules#minimum%20information%20principle).
39 | In Anki, this is facilitated in the form of cards.
40 |
41 | ```closet
42 | The capital of Portugal is [[c1::Lisbon]]. Its area is [[c2::92km²]].
43 | ```
44 |
45 | Using the above template, we could create two different cards, both testing one piece of knowledge on the note.
46 | This introduces the notion of _active_ flashcard effect, compared to _inactive_ ones.
47 |
48 | ---
49 |
50 | In Anki, on a card **Card 1**, the active flashcard effects end in a _1_, like `c1`, or `mc1` whereas `c2`/`mc2` are inactive, and vice versa.
51 | Within Closet we can be more flexibile, but generally we will adhere to this convention.
52 |
--------------------------------------------------------------------------------
/website/docs/installation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Installing Closet
3 | ---
4 |
--------------------------------------------------------------------------------
/website/docs/mdx.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: mdx
3 | title: Powered by MDX
4 | ---
5 |
6 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/).
7 |
8 | export const Highlight = ({ children, color }) => (
9 |
17 | {children}
18 |
19 | );
20 |
21 | Docusaurus green and
22 | Facebook blue
23 | are my favorite colors.
24 |
25 | I can write **Markdown** alongside my _JSX_!
26 |
--------------------------------------------------------------------------------
/website/docs/showcase.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Showcase
3 | ---
4 |
--------------------------------------------------------------------------------
/website/docs/snippets/example.yml:
--------------------------------------------------------------------------------
1 | name: Example
2 | text: |
3 | This is the template
4 |
--------------------------------------------------------------------------------
/website/docs/tryit.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Try It Online
3 | ---
4 |
5 | import CodeEditor from "@site/src/components/CodeEditor";
6 |
7 |
8 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "website",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "serve": "docusaurus serve",
12 | "clear": "docusaurus clear"
13 | },
14 | "dependencies": {
15 | "@docusaurus/core": "^2.0.0",
16 | "@docusaurus/preset-classic": "^3.0.0",
17 | "@material-ui/core": "^4.11.3",
18 | "@mdx-js/react": "^1.6.21",
19 | "closetjs": "^0.4.8-1",
20 | "clsx": "^1.1.1",
21 | "codemirror": "^5.59.2",
22 | "loaders": "file:plugins/loaders",
23 | "prismjs": "^1.23.0",
24 | "ramda": "^0.27.1",
25 | "react": "^16.8.4",
26 | "react-codemirror2": "^7.2.1",
27 | "react-dom": "^16.8.4",
28 | "react-icons": "^4.2.0",
29 | "react-use": "^17.1.1"
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.5%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | },
43 | "devDependencies": {
44 | "@docusaurus/module-type-aliases": "^2.0.0-alpha.70",
45 | "@tsconfig/docusaurus": "^1.0.2",
46 | "@types/prismjs": "^1.16.3",
47 | "@types/ramda": "types/npm-ramda#dist",
48 | "@types/react": "^17.0.2",
49 | "@types/react-helmet": "^6.1.0",
50 | "@types/react-router-dom": "^5.1.7",
51 | "babel-plugin-prismjs": "^2.0.1",
52 | "css-loader": "^5.0.2",
53 | "html-loader": "^2.1.1",
54 | "raw-loader": "^4.0.2",
55 | "sass-loader": "^11.0.1",
56 | "style-loader": "^2.0.0",
57 | "ts-loader": "^8.0.17",
58 | "typescript": "^4.1.5",
59 | "yaml-loader": "^0.6.0"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/website/sidebars.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | someSidebar: {
3 | Introduction: ["doc1", "doc2", "flashcard"],
4 | Clozes: ["clozes/creating"],
5 | Features: ["mdx"],
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/website/src/codeMirrorCloset.js:
--------------------------------------------------------------------------------
1 | CodeMirror.defineSimpleMode("closet", {
2 | // The start state contains the rules that are intially used
3 | start: [
4 | // tagstart
5 | { regex: TAG_START, token: "atom", push: "key" },
6 | // text
7 | { regex: /[\s\S]+?(?=\[\[|$)/u },
8 | ],
9 |
10 | key: [
11 | // keyname
12 | { regex: /[a-zA-Z]+\d*/u, token: "string" },
13 | // sep
14 | { regex: ARG_SEP, token: "atom", next: "intag" },
15 | // tagend
16 | { regex: TAG_END, token: "atom", pop: true },
17 | ],
18 |
19 | intag: [
20 | // tagstart
21 | { regex: TAG_START, token: "atom", push: "key" },
22 | // tagend
23 | { regex: TAG_END, token: "atom", pop: 1 },
24 | // argsep
25 | { regex: ARG_SEP, token: "atom" },
26 | // altsep
27 | { regex: ALT_SEP, token: "atom" },
28 | // valuestext
29 | { regex: /[\s\S]+?(?=\[\[|\]\]|\|\||::)/u },
30 | ],
31 | });
32 |
--------------------------------------------------------------------------------
/website/src/components/CodeEditor/styles.css:
--------------------------------------------------------------------------------
1 | .CodeMirror {
2 | height: auto;
3 | border: 5px solid #eee;
4 | }
5 |
6 | html[data-theme="dark"] .MuiSwitch-track {
7 | background-color: white;
8 | }
9 |
10 | .MuiFormControlLabel-root {
11 | margin-left: 0 !important;
12 | margin-right: 0 !important;
13 | }
14 |
15 | .MuiFormGroup-row {
16 | margin: 0.2rem 0;
17 | column-gap: 0.6rem;
18 | }
19 |
20 | .MuiTextField-root {
21 | width: 98px;
22 | }
23 |
24 | .MuiButton-containedPrimary {
25 | background-color: #10ac7d;
26 | }
27 |
--------------------------------------------------------------------------------
/website/src/components/ContextControls/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 |
3 | import TextField from "@material-ui/core/TextField";
4 | import FormControlLabel from "@material-ui/core/FormControlLabel";
5 | import Switch from "@material-ui/core/Switch";
6 |
7 | interface ContextControlsProps {
8 | onContextChange?: (cardNumber: number, isBack: boolean) => void;
9 | defaultCardNumber?: number;
10 | defaultIsBack?: boolean;
11 | }
12 |
13 | const ContextControls = ({
14 | onContextChange = () => {},
15 | defaultCardNumber = 1,
16 | defaultIsBack = false,
17 | }: ContextControlsProps) => {
18 | const [cardNumber, setCardNumber] = useState(defaultCardNumber);
19 | const [isBack, setSide] = useState(defaultIsBack);
20 |
21 | useEffect(() => {
22 | onContextChange(cardNumber, isBack);
23 | }, [isBack, cardNumber]);
24 |
25 | return (
26 | <>
27 | setCardNumber(Number((event.target as any).value))}
40 | />
41 |
42 | setSide(checked)}
46 | defaultChecked={defaultIsBack}
47 | color="primary"
48 | />
49 | }
50 | label={isBack ? "Backside" : "Frontside"}
51 | />
52 | >
53 | );
54 | };
55 |
56 | export default ContextControls;
57 |
--------------------------------------------------------------------------------
/website/src/components/Example/index.tsx:
--------------------------------------------------------------------------------
1 | import type { ExampleInfo } from "../../examples";
2 |
3 | import React from "react";
4 | import { useAsync } from "react-use";
5 | import { TiPlus, TiEquals } from "react-icons/ti";
6 |
7 | import ExampleSyntax from "../ExampleSyntax";
8 | import ExampleCompiled from "../ExampleCompiled";
9 |
10 | import styles from "./styles.module.css";
11 |
12 | const fetchExampleText = async (name: string): Promise => {
13 | const module = await import(
14 | `!raw-loader!@site/src/examples/${name}/text.html`
15 | );
16 | return module["default"];
17 | };
18 |
19 | const fetchExample = async (name: string): Promise => {
20 | return import(`@site/src/examples/${name}`);
21 | };
22 |
23 | type CodeDisplayProps = { name: string };
24 |
25 | const CodeDisplay = ({ name }: CodeDisplayProps) => {
26 | const exampleText = useAsync(async () => fetchExampleText(name), [name]);
27 | const example = useAsync(async () => fetchExample(name), [name]);
28 |
29 | return (
30 |
31 | {exampleText.loading ? (
32 |
33 | ) : (
34 |
35 | )}
36 |
37 | {example.loading || exampleText.loading ? (
38 |
39 | ) : (
40 | <>
41 |
42 |
setup.setup)}
45 | context={example.value.context}
46 | className={styles["pre-bottom"]}
47 | />
48 | >
49 | )}
50 |
51 | );
52 | };
53 |
54 | export default CodeDisplay;
55 |
--------------------------------------------------------------------------------
/website/src/components/Example/styles.module.css:
--------------------------------------------------------------------------------
1 | .example {
2 | position: relative;
3 | }
4 |
5 | .icon-plus {
6 | position: absolute;
7 | left: 0;
8 | right: 0;
9 | margin: -0.5rem auto 0;
10 | transform: scale(2.5);
11 |
12 | pointer-events: none;
13 | }
14 |
15 | .icon-equals {
16 | position: absolute;
17 | left: 0;
18 | right: 0;
19 | margin: 2.6rem auto 0;
20 | transform: scale(2.5);
21 |
22 | pointer-events: none;
23 | }
24 |
25 | :root {
26 | --anki-nightfield: #3a3a3a;
27 | --anki-nightborder: #777777;
28 | }
29 |
30 | html[data-theme="dark"] pre {
31 | background-color: var(--anki-nightfield);
32 | }
33 |
34 | html[data-theme="light"] pre {
35 | background-color: #e4e4e4;
36 | }
37 |
38 | pre * {
39 | margin: 0;
40 | }
41 |
42 | .pre-top {
43 | margin-bottom: 0;
44 | border-bottom-right-radius: 0;
45 | border-bottom-left-radius: 0;
46 | }
47 |
48 | .pre-top code {
49 | white-space: normal;
50 | }
51 |
52 | .pre-bottom {
53 | white-space: normal;
54 | margin-top: 0;
55 | border-top-right-radius: 0;
56 | border-top-left-radius: 0;
57 | }
58 |
--------------------------------------------------------------------------------
/website/src/components/ExampleCompiled/index.tsx:
--------------------------------------------------------------------------------
1 | import type { ContextInfo, Context } from "../../contexts";
2 | import type { Setup } from "../../setups";
3 |
4 | import React, { useRef } from "react";
5 |
6 | import { indexBy, prop } from "ramda";
7 |
8 | import TabButtonPanel from "../TabButtonPanel";
9 |
10 | import "./styles.css";
11 |
12 | import { closet } from "closetjs";
13 | import "@site/node_modules/closetjs/dist/closet.css";
14 |
15 | const prepareRenderer = (
16 | text: string,
17 | setups: Setup[],
18 | contextData: Context[],
19 | ) => {
20 | let filterManager = closet.FilterManager.make();
21 |
22 | for (const setup of setups) {
23 | setup(closet as any, filterManager);
24 | }
25 |
26 | const contexts = indexBy(prop("value"), contextData);
27 |
28 | return (value: string, indexChanged: boolean, target: HTMLElement): void => {
29 | const newContext = contexts[value].data;
30 | filterManager.switchPreset(newContext);
31 |
32 | closet.template.Template.make(text).render(filterManager, ([result]) => {
33 | target.innerHTML = result;
34 | });
35 | };
36 | };
37 |
38 | interface ExampleCompiledProps {
39 | text: string;
40 | setups: Setup[];
41 | context: ContextInfo;
42 | className: string;
43 | }
44 |
45 | const ExampleCompiled = ({
46 | text,
47 | setups,
48 | context,
49 | className = "",
50 | }: ExampleCompiledProps) => {
51 | const renderContainer = useRef();
52 | const renderer = prepareRenderer(text, setups, context.values);
53 |
54 | return (
55 |
56 |
60 | renderer(value, indexChanged, renderContainer.current)
61 | }
62 | />
63 |
64 |
65 | );
66 | };
67 |
68 | export default ExampleCompiled;
69 |
--------------------------------------------------------------------------------
/website/src/components/ExampleCompiled/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --anki-nightfield: #3a3a3a;
3 | --anki-nightborder: #777777;
4 | }
5 |
6 | .code-compiled {
7 | margin-top: 0 !important;
8 | }
9 |
--------------------------------------------------------------------------------
/website/src/components/ExampleSyntax/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from "react";
2 | import Prism, { highlight, highlightElement } from "prismjs";
3 |
4 | import "./styles.css";
5 |
6 | Prism.languages.closet = {
7 | tagopen: {
8 | pattern: /\[\[[a-zA-Z]+\d*/u,
9 | inside: {
10 | tagstart: /\[\[/u,
11 | tagname: /[a-zA-Z]+\d*/u,
12 | },
13 | },
14 | tagend: /\]\]/,
15 | altsep: /\|\|/,
16 | argsep: /::/,
17 | };
18 |
19 | type ExampleSyntaxProps = { text: string; className: string };
20 |
21 | const ExampleSyntax = ({ text, className = "" }: ExampleSyntaxProps) => {
22 | const codeContainer = useRef();
23 |
24 | const [highlightedText, setHighlightedText] = useState("");
25 |
26 | useEffect(() => {
27 | const highlighted = highlight(
28 | text.replace(/
39 |
44 |
45 | );
46 | };
47 |
48 | export default ExampleSyntax;
49 |
--------------------------------------------------------------------------------
/website/src/components/ExampleSyntax/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --light-purple: #f781f9;
3 | --baby-blue: #67cdcc;
4 | --docusaurus-green: #27c2a0;
5 | --cherry-tomato: #e94b3cff;
6 | --orange: #f2aa4cff;
7 | --blazing-yellow: #fee715ff;
8 | --mint: #adefd1ff;
9 | --blue-bossom: #2da8d8ff;
10 | --danger-red: #d9514eff;
11 | }
12 |
13 | html[data-theme="light"] :is(.token.tagstart, .token.tagend, .token.argsep) {
14 | color: var(--blue-bossom);
15 | }
16 |
17 | html[data-theme="dark"] :is(.token.tagstart, .token.tagend, .token.argsep) {
18 | color: var(--baby-blue);
19 | }
20 |
21 | html[data-theme="light"] .token.tagname {
22 | color: var(--danger-red);
23 | }
24 |
25 | html[data-theme="dark"] .token.tagname {
26 | color: var(--light-purple);
27 | }
28 |
--------------------------------------------------------------------------------
/website/src/components/SetupTooltip/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Button from "@material-ui/core/Button";
3 | import Tooltip from "@material-ui/core/Tooltip";
4 |
5 | export default function InteractiveTooltips() {
6 | return (
7 |
8 | Interactive
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/website/src/components/TabButtonPanel/styles.css:
--------------------------------------------------------------------------------
1 | .tabs {
2 | justify-content: stretch;
3 | flex-direction: row !important;
4 | }
5 |
6 | .tabs::-webkit-scrollbar {
7 | height: 0.6rem;
8 | background-color: var(--ifm-scrollbar-track-background-color);
9 | }
10 |
11 | .tabs::-webkit-scrollbar-thumb {
12 | background: var(--ifm-scrollbar-thumb-background-color);
13 | }
14 |
15 | .tabs__item {
16 | white-space: nowrap;
17 | user-select: none;
18 | border-radius: 0;
19 |
20 | padding: 0.5rem var(--ifm-tabs-padding-horizontal);
21 | margin: auto 0 !important;
22 | }
23 |
24 | .tabs__item--active {
25 | border-bottom-color: transparent;
26 | background-color: var(--ifm-hover-overlay);
27 | }
28 |
29 | html[data-theme="light"] .tabs__item--activated {
30 | animation: blinker 0.1s ease-out;
31 | }
32 |
33 | html[data-theme="dark"] .tabs__item--activated {
34 | animation: blinker-dark 0.1s ease-out;
35 | }
36 |
37 | @keyframes blinker {
38 | 50% {
39 | background-color: var(--ifm-color-gray-000);
40 | }
41 | }
42 |
43 | @keyframes blinker-dark {
44 | 50% {
45 | background-color: var(--ifm-color-gray-800);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/website/src/contexts/frontBack.ts:
--------------------------------------------------------------------------------
1 | export const defaultValue = "f";
2 | export const values = [
3 | {
4 | label: "Front",
5 | value: "f",
6 | data: {
7 | side: "front",
8 | },
9 | },
10 | {
11 | label: "Back",
12 | value: "b",
13 | data: {
14 | side: "back",
15 | },
16 | },
17 | ];
18 |
--------------------------------------------------------------------------------
/website/src/contexts/index.ts:
--------------------------------------------------------------------------------
1 | export type ContextData = Record;
2 |
3 | export interface Context {
4 | label: string;
5 | value: string;
6 | data: Record;
7 | }
8 |
9 | export interface ContextInfo {
10 | defaultValue: string;
11 | values: Context[];
12 | }
13 |
--------------------------------------------------------------------------------
/website/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 | /**
3 | * Any CSS included here will be global. The classic template
4 | * bundles Infima by default. Infima is a CSS framework designed to
5 | * work well for content-centric websites.
6 | */
7 |
8 | /* You can override the default Infima variables here. */
9 | :root {
10 | --ifm-color-primary: #25c2a0;
11 | --ifm-color-primary-dark: rgb(33, 175, 144);
12 | --ifm-color-primary-darker: rgb(31, 165, 136);
13 | --ifm-color-primary-darkest: rgb(26, 136, 112);
14 | --ifm-color-primary-light: rgb(70, 203, 174);
15 | --ifm-color-primary-lighter: rgb(102, 212, 189);
16 | --ifm-color-primary-lightest: rgb(146, 224, 208);
17 | --ifm-code-font-size: 95%;
18 | }
19 |
20 | .docusaurus-highlight-code-line {
21 | background-color: rgb(72, 77, 91);
22 | display: block;
23 | margin: 0 calc(-1 * var(--ifm-pre-padding));
24 | padding: 0 var(--ifm-pre-padding);
25 | }
26 |
27 | html[data-theme="light"] .alert--anki {
28 | color: black;
29 | border-color: white;
30 | background-color: darkgrey;
31 | }
32 |
33 | html[data-theme="dark"] .alert--anki {
34 | color: white;
35 | border-color: black;
36 | background-color: #707070;
37 | }
38 |
--------------------------------------------------------------------------------
/website/src/examples/clozesNothingHappens/index.ts:
--------------------------------------------------------------------------------
1 | import * as clozes from "../../setups/clozes";
2 | import * as frontBack from "../../contexts/frontBack";
3 |
4 | export const name = "clozesNothingHappens";
5 | export const setups = [clozes];
6 | export const context = frontBack;
7 |
--------------------------------------------------------------------------------
/website/src/examples/clozesNothingHappens/text.html:
--------------------------------------------------------------------------------
1 | The capital of Botswana is [[c::Gaborone]].
2 | The capital of Argentina is [[c::Buenos Aires]].
3 |
--------------------------------------------------------------------------------
/website/src/examples/firstExample/index.ts:
--------------------------------------------------------------------------------
1 | import * as clozes from "../../setups/clozes";
2 | import * as frontBack from "../../contexts/frontBack";
3 |
4 | export const name = "firstExample";
5 | export const setups = [clozes];
6 | export const context = frontBack;
7 |
--------------------------------------------------------------------------------
/website/src/examples/firstExample/text.html:
--------------------------------------------------------------------------------
1 | Hello world
2 |
3 | This is a [[c0::cloze]]
4 |
5 |
9 |
--------------------------------------------------------------------------------
/website/src/examples/index.ts:
--------------------------------------------------------------------------------
1 | import type { ContextInfo } from "../contexts";
2 | import type { SetupInfo } from "../setups";
3 |
4 | export type ExampleInfo = {
5 | name: string;
6 | description?: string;
7 | context: ContextInfo;
8 | setups: SetupInfo[];
9 | };
10 |
--------------------------------------------------------------------------------
/website/src/pages/helloReact.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Layout from "@theme/Layout";
3 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
4 |
5 | function Home() {
6 | const context = useDocusaurusContext();
7 | const { siteConfig = {} } = context;
8 | return (
9 |
13 | This is a test bla foo
14 |
15 | );
16 | }
17 |
18 | export default Home;
19 |
--------------------------------------------------------------------------------
/website/src/pages/styles.module.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 |
3 | /**
4 | * CSS files with the .module.css suffix will be treated as CSS modules
5 | * and scoped locally.
6 | */
7 |
8 | .heroBanner {
9 | padding: 4rem 0;
10 | text-align: center;
11 | position: relative;
12 | overflow: hidden;
13 | }
14 |
15 | @media screen and (max-width: 966px) {
16 | .heroBanner {
17 | padding: 2rem;
18 | }
19 | }
20 |
21 | .buttons {
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 | }
26 |
27 | .features {
28 | display: flex;
29 | align-items: center;
30 | padding: 2rem 0;
31 | width: 100%;
32 | }
33 |
34 | .featureImage {
35 | height: 200px;
36 | width: 200px;
37 | }
38 |
--------------------------------------------------------------------------------
/website/src/prismSetup.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/website/src/prismSetup.js
--------------------------------------------------------------------------------
/website/src/setups/clozes/index.ts:
--------------------------------------------------------------------------------
1 | export const name = "clozes";
2 | export const title = "Clozes default";
3 |
4 | export { default as setup } from "./setup";
5 |
--------------------------------------------------------------------------------
/website/src/setups/clozes/setup.js:
--------------------------------------------------------------------------------
1 | function clozes(closet, filterManager, _preset, _memory) {
2 | filterManager.install(
3 | closet.flashcard.recipes.cloze({
4 | tagname: "c",
5 | }),
6 | );
7 | }
8 |
9 | export default clozes;
10 |
--------------------------------------------------------------------------------
/website/src/setups/index.ts:
--------------------------------------------------------------------------------
1 | export type Setup = (closet: NodeModule, filterManager: any) => any;
2 |
3 | export interface SetupInfo {
4 | name: string;
5 | title: string;
6 | setup: Setup;
7 | }
8 |
9 | export * as clozes from "./clozes";
10 |
--------------------------------------------------------------------------------
/website/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hgiesel/closet/045dc44e389a0658f9688ad984ee0bbbd09c41ea/website/static/.nojekyll
--------------------------------------------------------------------------------
/website/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/docusaurus/tsconfig.json",
3 | "include": ["src/"],
4 | "compilerOptions": {
5 | "noEmit": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------