├── .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 | 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::jingcha]] [[tf::wenhua]] [[tf::jingyu]] [[tf::jingyu]] [[tf::jiejie]] 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 | 34 | 35 |
    36 | 37 |
    38 | {% for button in theButtons %} 39 | {% assign theButton = button | split: ", " %} 40 | 41 | {% endfor %} 42 | 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 | Closet logo 18 | Closet logo 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 | 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 |
      6 |
    • foo
    • 7 |
    • bar
    • 8 |
    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 | --------------------------------------------------------------------------------