From f3f690df3fef950025a42bcb759a5c41b8acb5b9 Mon Sep 17 00:00:00 2001 From: Stephen Mwangi Date: Sun, 13 Oct 2024 12:09:43 +0300 Subject: [PATCH] refactor: parser & cloze patterns --- .github/workflows/release.yml | 3 +- .github/workflows/test.yml | 5 +- .npmrc | 1 - package.json | 2 +- pnpm-lock.yaml | 204 ++++++++----- src/gui/settings.tsx | 114 ++++++- src/lang/locale/ar.ts | 2 + src/lang/locale/cz.ts | 2 + src/lang/locale/de.ts | 2 + src/lang/locale/en.ts | 2 + src/lang/locale/es.ts | 2 + src/lang/locale/fr.ts | 2 + src/lang/locale/it.ts | 2 + src/lang/locale/ja.ts | 2 + src/lang/locale/ko.ts | 2 + src/lang/locale/pl.ts | 2 + src/lang/locale/pt-br.ts | 2 + src/lang/locale/ru.ts | 2 + src/lang/locale/tr.ts | 2 + src/lang/locale/zh-cn.ts | 2 + src/lang/locale/zh-tw.ts | 2 + src/main.ts | 25 +- src/note-question-parser.ts | 22 +- src/parser.ts | 509 ++++++++++--------------------- src/question-type.ts | 63 +--- src/settings.ts | 23 ++ tests/unit/core.test.ts | 8 +- tests/unit/parser.test.ts | 339 ++++++++++---------- tests/unit/question-type.test.ts | 32 +- 29 files changed, 647 insertions(+), 733 deletions(-) delete mode 100644 .npmrc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6e0e03e..09edb6ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,14 +7,13 @@ on: env: PLUGIN_NAME: obsidian-spaced-repetition - COREPACK_ENABLE_STRICT: 0 jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6feb6574..dcec74ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,6 @@ on: pull_request: branches: [master] -env: - COREPACK_ENABLE_STRICT: 0 - jobs: lint_and_test_code: runs-on: ubuntu-latest @@ -17,7 +14,7 @@ jobs: with: node-version: "20" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Dependencies run: npm install -g pnpm && pnpm install diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 86efb405..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -use-node-version=20.17.0 diff --git a/package.json b/package.json index baf201f6..4d28b1ce 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,9 @@ }, "dependencies": { "chart.js": "^4.4.4", + "clozecraft": "^0.4.0", "minimatch": "^10.0.1", "pagerank.js": "^1.0.2", - "peggy": "^4.0.3", "short-uuid": "^5.2.0" }, "packageManager": "pnpm@^9.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 853107a4..3f4b89fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,15 +11,15 @@ importers: chart.js: specifier: ^4.4.4 version: 4.4.4 + clozecraft: + specifier: ^0.4.0 + version: 0.4.0 minimatch: specifier: ^10.0.1 version: 10.0.1 pagerank.js: specifier: ^1.0.2 version: 1.0.2 - peggy: - specifier: ^4.0.3 - version: 4.0.3 short-uuid: specifier: ^5.2.0 version: 5.2.0 @@ -65,7 +65,7 @@ importers: version: 55.0.0(eslint@9.11.1) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.7.1) + version: 29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 @@ -83,7 +83,7 @@ importers: version: 3.3.3 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.7.1))(typescript@5.5.4) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)))(typescript@5.5.4) tslib: specifier: ^2.7.0 version: 2.7.0 @@ -278,6 +278,10 @@ packages: '@codemirror/view@6.33.0': resolution: {integrity: sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} @@ -556,6 +560,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@kurkle/color@0.3.2': resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} @@ -571,10 +578,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@peggyjs/from-mem@1.3.0': - resolution: {integrity: sha512-kzGoIRJjkg3KuGI4bopz9UvF3KguzfxalHRDEIdqEZUe45xezsQ6cx30e0RKuxPUexojQRBfu89Okn7f4/QXsw==} - engines: {node: '>=18'} - '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -588,6 +591,18 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -769,6 +784,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -897,6 +915,9 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clozecraft@0.4.0: + resolution: {integrity: sha512-VeXEZV6/Ar0sLbru53lpn9bNf6dpBtQtywUJ0K76WOCrI9a61jSgEo/aspwUuKdpR7kAtAhAlsYDDEaZj87wqQ==} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -921,10 +942,6 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -943,6 +960,9 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1000,6 +1020,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -1609,10 +1633,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -1783,11 +1803,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - peggy@4.0.3: - resolution: {integrity: sha512-v7/Pt6kGYsfXsCrfb52q7/yg5jaAwiVaUMAPLPvy4DJJU6Wwr72t6nDIqIDkGfzd1B4zeVuTnQT0RGeOhe/uSA==} - engines: {node: '>=18'} - hasBin: true - picocolors@1.1.0: resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} @@ -1908,11 +1923,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} - engines: {node: '>=10'} - hasBin: true - semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -1940,10 +1950,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - source-map-generator@0.8.0: - resolution: {integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==} - engines: {node: '>= 10'} - source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -2079,6 +2085,20 @@ packages: esbuild: optional: true + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tslib@2.7.0: resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} @@ -2144,6 +2164,9 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -2235,9 +2258,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -2246,6 +2266,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2466,6 +2490,11 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + optional: true + '@esbuild/aix-ppc64@0.23.1': optional: true @@ -2600,7 +2629,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -2614,7 +2643,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.7.1) + jest-config: 29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -2770,6 +2799,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + optional: true + '@kurkle/color@0.3.2': {} '@nodelib/fs.scandir@2.1.5': @@ -2784,10 +2819,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@peggyjs/from-mem@1.3.0': - dependencies: - semver: 7.6.0 - '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -2800,6 +2831,18 @@ snapshots: '@tootallnate/once@2.0.0': {} + '@tsconfig/node10@1.0.11': + optional: true + + '@tsconfig/node12@1.0.11': + optional: true + + '@tsconfig/node14@1.0.3': + optional: true + + '@tsconfig/node16@1.0.4': + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.25.6 @@ -3019,6 +3062,9 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@4.1.3: + optional: true + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -3172,6 +3218,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clozecraft@0.4.0: {} + co@4.6.0: {} collect-v8-coverage@1.0.2: {} @@ -3192,8 +3240,6 @@ snapshots: dependencies: delayed-stream: 1.0.0 - commander@12.1.0: {} - commander@7.2.0: {} concat-map@0.0.1: {} @@ -3204,13 +3250,13 @@ snapshots: dependencies: browserslist: 4.24.0 - create-jest@29.7.0(@types/node@22.7.1): + create-jest@29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.7.1) + jest-config: 29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -3219,6 +3265,9 @@ snapshots: - supports-color - ts-node + create-require@1.1.1: + optional: true + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -3257,6 +3306,9 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.2: + optional: true + domexception@4.0.0: dependencies: webidl-conversions: 7.0.0 @@ -3723,16 +3775,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.7.1): + jest-cli@29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.7.1) + create-jest: 29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.7.1) + jest-config: 29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -3742,7 +3794,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.7.1): + jest-config@29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)): dependencies: '@babel/core': 7.25.2 '@jest/test-sequencer': 29.7.0 @@ -3768,6 +3820,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.7.1 + ts-node: 10.9.2(@types/node@22.7.1)(typescript@5.5.4) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -4004,12 +4057,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.7.1): + jest@29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.7.1) + jest-cli: 29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -4107,10 +4160,6 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - make-dir@4.0.0: dependencies: semver: 7.6.3 @@ -4260,12 +4309,6 @@ snapshots: path-parse@1.0.7: {} - peggy@4.0.3: - dependencies: - '@peggyjs/from-mem': 1.3.0 - commander: 12.1.0 - source-map-generator: 0.8.0 - picocolors@1.1.0: {} picomatch@2.3.1: {} @@ -4360,10 +4403,6 @@ snapshots: semver@6.3.1: {} - semver@7.6.0: - dependencies: - lru-cache: 6.0.0 - semver@7.6.3: {} shebang-command@2.0.0: @@ -4383,8 +4422,6 @@ snapshots: slash@3.0.0: {} - source-map-generator@0.8.0: {} - source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -4488,12 +4525,12 @@ snapshots: dependencies: typescript: 5.5.4 - ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.7.1))(typescript@5.5.4): + ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)))(typescript@5.5.4): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.7.1) + jest: 29.7.0(@types/node@22.7.1)(ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -4508,6 +4545,25 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.25.2) esbuild: 0.23.1 + ts-node@10.9.2(@types/node@22.7.1)(typescript@5.5.4): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.7.1 + acorn: 8.12.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + tslib@2.7.0: {} type-check@0.4.0: @@ -4559,6 +4615,9 @@ snapshots: uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: + optional: true + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -4633,8 +4692,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yargs-parser@21.1.1: {} yargs@17.7.2: @@ -4647,4 +4704,7 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yn@3.1.1: + optional: true + yocto-queue@0.1.0: {} diff --git a/src/gui/settings.tsx b/src/gui/settings.tsx index f78d1f03..386ca955 100644 --- a/src/gui/settings.tsx +++ b/src/gui/settings.tsx @@ -158,7 +158,6 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.flashcardCardOrder = value; await this.plugin.savePluginData(); - // Need to redisplay as changing this setting affects the "deck order" setting this.display(); }), ); @@ -202,9 +201,20 @@ export class SRSettingTab extends PluginSettingTab { toggle .setValue(this.plugin.data.settings.convertHighlightsToClozes) .onChange(async (value) => { + const hightlightPattern = "==[123;;]answer[;;hint]=="; + const clozePatternSet = new Set(this.plugin.data.settings.clozePatterns); + + if (value) { + clozePatternSet.add(hightlightPattern); + } else { + clozePatternSet.delete(hightlightPattern); + } + + this.plugin.data.settings.clozePatterns = [...clozePatternSet]; this.plugin.data.settings.convertHighlightsToClozes = value; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); + + this.display(); }), ); @@ -212,9 +222,20 @@ export class SRSettingTab extends PluginSettingTab { toggle .setValue(this.plugin.data.settings.convertBoldTextToClozes) .onChange(async (value) => { + const boldPattern = "**[123;;]answer[;;hint]**"; + const clozePatternSet = new Set(this.plugin.data.settings.clozePatterns); + + if (value) { + clozePatternSet.add(boldPattern); + } else { + clozePatternSet.delete(boldPattern); + } + + this.plugin.data.settings.clozePatterns = [...clozePatternSet]; this.plugin.data.settings.convertBoldTextToClozes = value; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); + + this.display(); }), ); @@ -224,9 +245,66 @@ export class SRSettingTab extends PluginSettingTab { toggle .setValue(this.plugin.data.settings.convertCurlyBracketsToClozes) .onChange(async (value) => { + const curlyBracketsPattern = "{{[123;;]answer[;;hint]}}"; + const clozePatternSet = new Set(this.plugin.data.settings.clozePatterns); + + if (value) { + clozePatternSet.add(curlyBracketsPattern); + } else { + clozePatternSet.delete(curlyBracketsPattern); + } + + this.plugin.data.settings.clozePatterns = [...clozePatternSet]; this.plugin.data.settings.convertCurlyBracketsToClozes = value; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); + + this.display(); + }), + ); + + new Setting(containerEl) + .setName(t("CLOZE_PATTERNS")) + .setDesc(t("CLOZE_PATTERNS_DESC")) + .addTextArea((text) => + text + .setPlaceholder( + "Example:\n==[123;;]answer[;;hint]==\n**[123;;]answer[;;hint]**\n{{[123;;]answer[;;hint]}}", + ) + .setValue(this.plugin.data.settings.clozePatterns.join("\n")) + .onChange((value) => { + applySettingsUpdate(async () => { + const hightlightPattern = "==[123;;]answer[;;hint]=="; + const boldPattern = "**[123;;]answer[;;hint]**"; + const curlyBracketsPattern = "{{[123;;]answer[;;hint]}}"; + + const clozePatternSet = new Set( + value + .split(/\n+/) + .map((v) => v.trim()) + .filter((v) => v), + ); + + if (clozePatternSet.has(hightlightPattern)) { + this.plugin.data.settings.convertHighlightsToClozes = true; + } else { + this.plugin.data.settings.convertHighlightsToClozes = false; + } + + if (clozePatternSet.has(boldPattern)) { + this.plugin.data.settings.convertBoldTextToClozes = true; + } else { + this.plugin.data.settings.convertBoldTextToClozes = false; + } + + if (clozePatternSet.has(curlyBracketsPattern)) { + this.plugin.data.settings.convertCurlyBracketsToClozes = true; + } else { + this.plugin.data.settings.convertCurlyBracketsToClozes = false; + } + + this.plugin.data.settings.clozePatterns = [...clozePatternSet]; + await this.plugin.savePluginData(); + }); }), ); @@ -239,7 +317,6 @@ export class SRSettingTab extends PluginSettingTab { .onChange((value) => { applySettingsUpdate(async () => { this.plugin.data.settings.singleLineCardSeparator = value; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); }); }), @@ -251,8 +328,8 @@ export class SRSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.data.settings.singleLineCardSeparator = DEFAULT_SETTINGS.singleLineCardSeparator; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); + this.display(); }); }); @@ -266,7 +343,6 @@ export class SRSettingTab extends PluginSettingTab { .onChange((value) => { applySettingsUpdate(async () => { this.plugin.data.settings.singleLineReversedCardSeparator = value; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); }); }), @@ -278,8 +354,8 @@ export class SRSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.data.settings.singleLineReversedCardSeparator = DEFAULT_SETTINGS.singleLineReversedCardSeparator; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); + this.display(); }); }); @@ -293,7 +369,6 @@ export class SRSettingTab extends PluginSettingTab { .onChange((value) => { applySettingsUpdate(async () => { this.plugin.data.settings.multilineCardSeparator = value; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); }); }), @@ -305,8 +380,8 @@ export class SRSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.data.settings.multilineCardSeparator = DEFAULT_SETTINGS.multilineCardSeparator; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); + this.display(); }); }); @@ -320,7 +395,6 @@ export class SRSettingTab extends PluginSettingTab { .onChange((value) => { applySettingsUpdate(async () => { this.plugin.data.settings.multilineReversedCardSeparator = value; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); }); }), @@ -332,8 +406,8 @@ export class SRSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.data.settings.multilineReversedCardSeparator = DEFAULT_SETTINGS.multilineReversedCardSeparator; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); + this.display(); }); }); @@ -347,7 +421,6 @@ export class SRSettingTab extends PluginSettingTab { .onChange((value) => { applySettingsUpdate(async () => { this.plugin.data.settings.multilineCardEndMarker = value; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); }); }), @@ -359,8 +432,8 @@ export class SRSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.data.settings.multilineCardEndMarker = DEFAULT_SETTINGS.multilineCardEndMarker; - this.plugin.debouncedGenerateParser(); await this.plugin.savePluginData(); + this.display(); }); }); @@ -446,6 +519,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.maxNDaysNotesReviewQueue = DEFAULT_SETTINGS.maxNDaysNotesReviewQueue; await this.plugin.savePluginData(); + this.display(); }); }); @@ -465,6 +539,7 @@ export class SRSettingTab extends PluginSettingTab { .map((v) => v.trim()) .filter((v) => v); await this.plugin.savePluginData(); + this.display(); }); }), @@ -556,6 +631,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.flashcardHeightPercentage = DEFAULT_SETTINGS.flashcardHeightPercentage; await this.plugin.savePluginData(); + this.display(); }); }); @@ -581,6 +657,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.flashcardWidthPercentage = DEFAULT_SETTINGS.flashcardWidthPercentage; await this.plugin.savePluginData(); + this.display(); }); }); @@ -605,6 +682,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.flashcardEasyText = DEFAULT_SETTINGS.flashcardEasyText; await this.plugin.savePluginData(); + this.display(); }); }); @@ -628,6 +706,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.flashcardGoodText = DEFAULT_SETTINGS.flashcardGoodText; await this.plugin.savePluginData(); + this.display(); }); }); @@ -651,6 +730,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.flashcardHardText = DEFAULT_SETTINGS.flashcardHardText; await this.plugin.savePluginData(); + this.display(); }); }); @@ -676,6 +756,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.reviewButtonDelay = DEFAULT_SETTINGS.reviewButtonDelay; await this.plugin.savePluginData(); + this.display(); }); }); @@ -731,6 +812,7 @@ export class SRSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.data.settings.baseEase = DEFAULT_SETTINGS.baseEase; await this.plugin.savePluginData(); + this.display(); }); }); @@ -756,6 +838,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.lapsesIntervalChange = DEFAULT_SETTINGS.lapsesIntervalChange; await this.plugin.savePluginData(); + this.display(); }); }); @@ -793,6 +876,7 @@ export class SRSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.data.settings.easyBonus = DEFAULT_SETTINGS.easyBonus; await this.plugin.savePluginData(); + this.display(); }); }); @@ -831,6 +915,7 @@ export class SRSettingTab extends PluginSettingTab { this.plugin.data.settings.maximumInterval = DEFAULT_SETTINGS.maximumInterval; await this.plugin.savePluginData(); + this.display(); }); }); @@ -855,6 +940,7 @@ export class SRSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.data.settings.maxLinkFactor = DEFAULT_SETTINGS.maxLinkFactor; await this.plugin.savePluginData(); + this.display(); }); }); diff --git a/src/lang/locale/ar.ts b/src/lang/locale/ar.ts index 47e88f74..efe8035a 100644 --- a/src/lang/locale/ar.ts +++ b/src/lang/locale/ar.ts @@ -115,6 +115,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "Convert ==hightlights== to clozes?", CONVERT_BOLD_TEXT_TO_CLOZES: "Convert **bolded text** to clozes?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "Convert {{curly brackets}} to clozes?", + CLOZE_PATTERNS: "Cloze Patterns", + CLOZE_PATTERNS_DESC: "Enter cloze patterns separated by newlines", INLINE_CARDS_SEPARATOR: "فاصل من أجل البطاقات المضمنة", FIX_SEPARATORS_MANUALLY_WARNING: "ضع في حسابك أنه بعد تغيير هذا ، يجب عليك تعديل أي بطاقات لديك بالفعل يدويًا", diff --git a/src/lang/locale/cz.ts b/src/lang/locale/cz.ts index 41be4a66..bcff34ae 100644 --- a/src/lang/locale/cz.ts +++ b/src/lang/locale/cz.ts @@ -118,6 +118,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "Převést ==zvýraznění== na clozes?", CONVERT_BOLD_TEXT_TO_CLOZES: "Převést **tučný text** na clozes?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "Převést {{složené závorky}} na clozes?", + CLOZE_PATTERNS: "Cloze vzory", + CLOZE_PATTERNS_DESC: "Zadejte cloze vzory oddělené odřádkováním", INLINE_CARDS_SEPARATOR: "Oddělovač pro inline kartičky", FIX_SEPARATORS_MANUALLY_WARNING: "Pozor. Jakmile toto změníte, budete muset ručně upravit všechny existující kartičky.", diff --git a/src/lang/locale/de.ts b/src/lang/locale/de.ts index 45882f93..a633219d 100644 --- a/src/lang/locale/de.ts +++ b/src/lang/locale/de.ts @@ -131,6 +131,8 @@ export default { CONVERT_BOLD_TEXT_TO_CLOZES: "**Fettgedruckten** Text in Lückentextkarten umwandeln?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "{{Geschweifte Klammern}} Text in Lückentextkarten umwandeln?", + CLOZE_PATTERNS: "Lückentextmuster", + CLOZE_PATTERNS_DESC: "Geben Sie Lückentextmuster durch Zeilenumbrüche getrennt ein", INLINE_CARDS_SEPARATOR: "Trennzeichen für einzeilige Lernkarten", FIX_SEPARATORS_MANUALLY_WARNING: "Wenn diese Einstellung geändert wird, dann müssen die entsprechenden Lernkarten manuell angepasst werden.", diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 7083d81d..d1a1076d 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -118,6 +118,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "Convert ==hightlights== to clozes?", CONVERT_BOLD_TEXT_TO_CLOZES: "Convert **bolded text** to clozes?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "Convert {{curly brackets}} to clozes?", + CLOZE_PATTERNS: "Cloze Patterns", + CLOZE_PATTERNS_DESC: "Enter cloze patterns separated by newlines", INLINE_CARDS_SEPARATOR: "Separator for inline flashcards", FIX_SEPARATORS_MANUALLY_WARNING: "Note that after changing this you have to manually edit any flashcards you already have.", diff --git a/src/lang/locale/es.ts b/src/lang/locale/es.ts index 849ac63c..89a36129 100644 --- a/src/lang/locale/es.ts +++ b/src/lang/locale/es.ts @@ -118,6 +118,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "¿Convertir ==resaltados== a deletreo de huecos?", CONVERT_BOLD_TEXT_TO_CLOZES: "¿Convertir **texto en negrita** a deletreo de huecos?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "¿Convertir {{llaves rizadas}} a deletreo de huecos?", + CLOZE_PATTERNS: "Patrones de deletreo de huecos", + CLOZE_PATTERNS_DESC: "Escriba los patrones de deletreo de huecos separados por saltos de línea", INLINE_CARDS_SEPARATOR: "Separador de tarjetas de memorización en línea", FIX_SEPARATORS_MANUALLY_WARNING: "Note que después de cambiar este ajuste, tendrá que cambiar manualmente todas las notas que tenga.", diff --git a/src/lang/locale/fr.ts b/src/lang/locale/fr.ts index 3619bc2d..886afba8 100644 --- a/src/lang/locale/fr.ts +++ b/src/lang/locale/fr.ts @@ -119,6 +119,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "Convertir ==soulignages== en trous ?", CONVERT_BOLD_TEXT_TO_CLOZES: "Convertir **gras** en trous ?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "Convertir {{crochets}} en trous ?", + CLOZE_PATTERNS: "Cloze Patterns", + CLOZE_PATTERNS_DESC: "Enter cloze patterns separated by newlines", INLINE_CARDS_SEPARATOR: "Séparateur pour flashcards en une ligne", FIX_SEPARATORS_MANUALLY_WARNING: "Après avoir changé ce réglage, vous devrez manuellement mettre à jour toutes vos flashcards.", diff --git a/src/lang/locale/it.ts b/src/lang/locale/it.ts index 05cb68ff..bae87c1a 100644 --- a/src/lang/locale/it.ts +++ b/src/lang/locale/it.ts @@ -123,6 +123,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "Convertire ==testo evidenziato== in spazi da riempire?", CONVERT_BOLD_TEXT_TO_CLOZES: "Convertire **testo in grassetto** in spazi da riempire", CONVERT_CURLY_BRACKETS_TO_CLOZES: "Convertire {{parentesi graffe}} in spazi da riempire?", + CLOZE_PATTERNS: "Modelli di spazi da riempire", + CLOZE_PATTERNS_DESC: "Inserisci i modelli di spazi da riempire separati da a capo", INLINE_CARDS_SEPARATOR: "Separatore per schede sulla stessa riga", FIX_SEPARATORS_MANUALLY_WARNING: "Si avvisa che dopo aver cambiato questo dovrai manualmente modificare le schede che hai già.", diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index ffe479f7..2ad77de6 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -121,6 +121,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "==ハイライト==を穴埋めとして使用しますか?", CONVERT_BOLD_TEXT_TO_CLOZES: "**ボールド体**を穴埋めとして使用しますか?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "{{中括弧}}を穴埋めとして使用しますか?", + CLOZE_PATTERNS: "穴埋めパターン", + CLOZE_PATTERNS_DESC: "改行で区切って穴埋めパターンを入力してください。", INLINE_CARDS_SEPARATOR: "インラインフラッシュカードに使用するセパレーター", FIX_SEPARATORS_MANUALLY_WARNING: "このオプションを変更する場合には、作成済みのフラッシュカードを手動で編集し直す必要があることに注意してください。", diff --git a/src/lang/locale/ko.ts b/src/lang/locale/ko.ts index 79242d47..b97ad5c8 100644 --- a/src/lang/locale/ko.ts +++ b/src/lang/locale/ko.ts @@ -119,6 +119,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "==hightlights== 를 빈 칸 채우기로 전환하시겠습니까?", CONVERT_BOLD_TEXT_TO_CLOZES: "**bolded text** 를 빈 칸 채우기로 전환하시겠습니까?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "{{curly brackets}} 를 빈 칸 채우기로 전환하시겠습니까?", + CLOZE_PATTERNS: "빈 칸 채우기 패턴", + CLOZE_PATTERNS_DESC: "빈 칸 채우기 패턴을 입력해주세요. 줄바꿈으로 구분합니다.", INLINE_CARDS_SEPARATOR: "인라인 플래시카드 구분자", FIX_SEPARATORS_MANUALLY_WARNING: "주의: 이 옵션을 수정한 후에는 이미 작성된 플래시카드를 수동으로 수정해야 함을 주의하십시오.", diff --git a/src/lang/locale/pl.ts b/src/lang/locale/pl.ts index 0d2f6b70..41462117 100644 --- a/src/lang/locale/pl.ts +++ b/src/lang/locale/pl.ts @@ -121,6 +121,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "Konwertować ==podświetlenia== na karty zamaskowane?", CONVERT_BOLD_TEXT_TO_CLOZES: "Konwertować pogrubiony tekst na karty zamaskowane?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "Konwertować {{klamry}} na karty zamaskowane?", + CLOZE_PATTERNS: "Wzory kart zamaskowanych", + CLOZE_PATTERNS_DESC: "Wprowadź wzory kart zamaskowanych oddzielone nowymi liniami", INLINE_CARDS_SEPARATOR: "Separator dla kart zamaskowanych w linii", FIX_SEPARATORS_MANUALLY_WARNING: "Pamiętaj, że po zmianie tego musisz ręcznie edytować wszystkie karty zamaskowane, które już masz.", diff --git a/src/lang/locale/pt-br.ts b/src/lang/locale/pt-br.ts index 4155bd96..ff3921e7 100644 --- a/src/lang/locale/pt-br.ts +++ b/src/lang/locale/pt-br.ts @@ -121,6 +121,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "Converter ==marca-texto== em omissões?", CONVERT_BOLD_TEXT_TO_CLOZES: "Converter **texto em negrito** em omissões?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "Converter {{chaves}} em omissões?", + CLOZE_PATTERNS: "Padrões de Omissão", + CLOZE_PATTERNS_DESC: "Entre os padrões de omissão separados por quebras de linha", INLINE_CARDS_SEPARATOR: "Separador para flashcards inline", FIX_SEPARATORS_MANUALLY_WARNING: "Note que depois de mudar isso você vai ter que manualmente mudar quaisquer flashcards que você tenha.", diff --git a/src/lang/locale/ru.ts b/src/lang/locale/ru.ts index e87e8943..3254d2c0 100644 --- a/src/lang/locale/ru.ts +++ b/src/lang/locale/ru.ts @@ -129,6 +129,8 @@ export default { CONVERT_BOLD_TEXT_TO_CLOZES: "Конвертировать **жирный текст** в пропуски (пример: [...])?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "Конвертировать {{фигурные скобки}} в пропуски (пример: [...])?", + CLOZE_PATTERNS: "Шаблоны пропусков", + CLOZE_PATTERNS_DESC: "Введите шаблоны пропусков, разделенные переводами строк", INLINE_CARDS_SEPARATOR: "Разделитель для внутристрочных карточек", FIX_SEPARATORS_MANUALLY_WARNING: "Внимание! После изменения этого вам придётся вручную редактировать уже существующие карточки", diff --git a/src/lang/locale/tr.ts b/src/lang/locale/tr.ts index f3bd4387..0c14776f 100644 --- a/src/lang/locale/tr.ts +++ b/src/lang/locale/tr.ts @@ -118,6 +118,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "==Vurgulanan== metni gizli kartlara dönüştür?", CONVERT_BOLD_TEXT_TO_CLOZES: "**Kalın metni** gizli kartlara dönüştür?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "{{Kıvırcık parantezleri}} gizli kartlara dönüştür?", + CLOZE_PATTERNS: "Cloze Patterns", + CLOZE_PATTERNS_DESC: "Enter cloze patterns separated by newlines", INLINE_CARDS_SEPARATOR: "Satır içi flash kartlar için ayırıcı", FIX_SEPARATORS_MANUALLY_WARNING: "Bunu değiştirdikten sonra mevcut flash kartlarınızı manuel olarak düzenlemeniz gerektiğini unutmayın.", diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index bd7f696a..4f231109 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -111,6 +111,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "将 ==高亮== 转换为完形填空?", CONVERT_BOLD_TEXT_TO_CLOZES: "将 **粗体** 转换为完形填空?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "将 {{大括号}} 转换为完形填空?", + CLOZE_PATTERNS: "完形填空模式", + CLOZE_PATTERNS_DESC: "输入以换行符分隔的完形填空模式", INLINE_CARDS_SEPARATOR: "单行卡片的分隔符", FIX_SEPARATORS_MANUALLY_WARNING: "注意:更改此选项后你将需要自行更改已存在卡片的分隔符。", INLINE_REVERSED_CARDS_SEPARATOR: "单行翻转卡片的分隔符", diff --git a/src/lang/locale/zh-tw.ts b/src/lang/locale/zh-tw.ts index e7335965..5c5b690a 100644 --- a/src/lang/locale/zh-tw.ts +++ b/src/lang/locale/zh-tw.ts @@ -111,6 +111,8 @@ export default { CONVERT_HIGHLIGHTS_TO_CLOZES: "將 ==高亮== 轉換為填空克漏字?", CONVERT_BOLD_TEXT_TO_CLOZES: "將 **粗體** 轉換為填空克漏字?", CONVERT_CURLY_BRACKETS_TO_CLOZES: "將 {{大括號}} 轉換為填空克漏字?", + CLOZE_PATTERNS: "填空克漏字模式", + CLOZE_PATTERNS_DESC: "輸入以換行符分隔的填空克漏字模式", INLINE_CARDS_SEPARATOR: "單行卡片的分隔字元", FIX_SEPARATORS_MANUALLY_WARNING: "注意:更改此選項後你將需要自行更改已存在卡片的分隔字元。", INLINE_REVERSED_CARDS_SEPARATOR: "單行反轉卡片的分隔字元", diff --git a/src/main.ts b/src/main.ts index bbb758f4..d6a6d42b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,7 @@ import { t } from "src/lang/helpers"; import { NextNoteReviewHandler } from "src/next-note-review-handler"; import { Note } from "src/note"; import { NoteFileLoader } from "src/note-file-loader"; -import { generateParser, setDebugParser } from "src/parser"; +import { setDebugParser } from "src/parser"; import { DEFAULT_DATA, PluginData } from "src/plugin-data"; import { QuestionPostponementList } from "src/question-postponement-list"; import { DEFAULT_SETTINGS, SettingsUtil, SRSettings, upgradeSettings } from "src/settings"; @@ -45,8 +45,6 @@ export default class SRPlugin extends Plugin { private osrSidebar: OsrSidebar; private nextNoteReviewHandler: NextNoteReviewHandler; - private debouncedGenerateParserTimeout: number | null = null; - private ribbonIcon: HTMLElement | null = null; private statusBar: HTMLElement | null = null; private fileMenuHandler: ( @@ -435,27 +433,6 @@ export default class SRPlugin extends Plugin { await this.saveData(this.data); } - async debouncedGenerateParser(timeoutMs = 250) { - if (this.debouncedGenerateParserTimeout) { - clearTimeout(this.debouncedGenerateParserTimeout); - } - - this.debouncedGenerateParserTimeout = window.setTimeout(async () => { - const parserOptions = { - singleLineCardSeparator: this.data.settings.singleLineCardSeparator, - singleLineReversedCardSeparator: this.data.settings.singleLineReversedCardSeparator, - multilineCardSeparator: this.data.settings.multilineCardSeparator, - multilineReversedCardSeparator: this.data.settings.multilineReversedCardSeparator, - multilineCardEndMarker: this.data.settings.multilineCardEndMarker, - convertHighlightsToClozes: this.data.settings.convertHighlightsToClozes, - convertBoldTextToClozes: this.data.settings.convertBoldTextToClozes, - convertCurlyBracketsToClozes: this.data.settings.convertCurlyBracketsToClozes, - }; - generateParser(parserOptions); - this.debouncedGenerateParserTimeout = null; - }, timeoutMs); - } - showRibbonIcon(status: boolean) { // if it does not exit, we create it if (!this.ribbonIcon) { diff --git a/src/note-question-parser.ts b/src/note-question-parser.ts index fc917691..491aca80 100644 --- a/src/note-question-parser.ts +++ b/src/note-question-parser.ts @@ -4,7 +4,7 @@ import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info" import { Card } from "src/card"; import { DataStore } from "src/data-stores/base/data-store"; import { frontmatterTagPseudoLineNum, ISRFile } from "src/file"; -import { ParsedQuestionInfo, parseEx, ParserOptions } from "src/parser"; +import { parse, ParsedQuestionInfo, ParserOptions } from "src/parser"; import { Question, QuestionText } from "src/question"; import { CardFrontBack, CardFrontBackUtil } from "src/question-type"; import { SettingsUtil, SRSettings } from "src/settings"; @@ -143,20 +143,18 @@ export class NoteQuestionParser { } private parseQuestions(): ParsedQuestionInfo[] { - // We pass contentText which has the frontmatter blanked out; see extractFrontmatter for reasoning + const settings = this.settings; const parserOptions: ParserOptions = { - singleLineCardSeparator: this.settings.singleLineCardSeparator, - singleLineReversedCardSeparator: this.settings.singleLineReversedCardSeparator, - multilineCardSeparator: this.settings.multilineCardSeparator, - multilineReversedCardSeparator: this.settings.multilineReversedCardSeparator, - multilineCardEndMarker: this.settings.multilineCardEndMarker, - convertHighlightsToClozes: this.settings.convertHighlightsToClozes, - convertBoldTextToClozes: this.settings.convertBoldTextToClozes, - convertCurlyBracketsToClozes: this.settings.convertCurlyBracketsToClozes, + singleLineCardSeparator: settings.singleLineCardSeparator, + singleLineReversedCardSeparator: settings.singleLineReversedCardSeparator, + multilineCardSeparator: settings.multilineCardSeparator, + multilineReversedCardSeparator: settings.multilineReversedCardSeparator, + multilineCardEndMarker: settings.multilineCardEndMarker, + clozePatterns: settings.clozePatterns, }; - const result: ParsedQuestionInfo[] = parseEx(this.contentText, parserOptions); - return result; + // We pass contentText which has the frontmatter blanked out; see extractFrontmatter for reasoning + return parse(this.contentText, parserOptions); } private createQuestionObject( diff --git a/src/parser.ts b/src/parser.ts index fa46e5fd..a99f9991 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,9 +1,7 @@ -import { generate, Parser } from "peggy"; +import { ClozeCrafter } from "clozecraft"; import { CardType } from "src/question"; -let parser: Parser | null = null; -let oldOptions: ParserOptions; export let debugParser = false; export interface ParserOptions { @@ -12,323 +10,7 @@ export interface ParserOptions { multilineCardSeparator: string; multilineReversedCardSeparator: string; multilineCardEndMarker: string; - convertHighlightsToClozes: boolean; - convertBoldTextToClozes: boolean; - convertCurlyBracketsToClozes: boolean; -} - -function areParserOptionsEqual(options1: ParserOptions, options2: ParserOptions): boolean { - return ( - options1.singleLineCardSeparator === options2.singleLineCardSeparator && - options1.singleLineReversedCardSeparator === options2.singleLineReversedCardSeparator && - options1.multilineCardSeparator === options2.multilineCardSeparator && - options1.multilineReversedCardSeparator === options2.multilineReversedCardSeparator && - options1.multilineCardEndMarker === options2.multilineCardEndMarker && - options1.convertHighlightsToClozes === options2.convertHighlightsToClozes && - options1.convertBoldTextToClozes === options2.convertBoldTextToClozes && - options1.convertCurlyBracketsToClozes === options2.convertCurlyBracketsToClozes - ); -} - -export function generateParser(options: ParserOptions): Parser { - let grammar: string | null = null; - - // Debug the grammar before generating the parser `generate(grammar)` from the grammar. - if (debugParser) { - if (grammar === null) { - grammar = generateGrammar(options); - } - console.log( - "The parsers grammar is provided below. You can test it with https://peggyjs.org/online.html.", - ); - console.log({ - info: "Copy the grammar by right-clicking on the property grammar and copying it as a string. Then, paste it in https://peggyjs.org/online.html.", - grammar: grammar, - }); - } - - // If the parser did not already exist or if the parser options changed since last the last - // parser was generated, we generate a new parser. Otherwise, we skip the block to save - // some execution time. - if (parser === null || !areParserOptionsEqual(options, oldOptions)) { - /* GENERATE A NEW PARSER */ - - oldOptions = Object.assign({}, options); - - grammar = generateGrammar(options); - - if (debugParser) { - const t0 = Date.now(); - parser = generate(grammar); - const t1 = Date.now(); - console.log("New parser generated in " + (t1 - t0) + " milliseconds."); - } else { - parser = generate(grammar); - } - } else { - if (debugParser) { - console.log("Parser already exists. No need to generate a new parser."); - } - } - - return parser; -} - -function generateGrammar(options: ParserOptions): string { - // Contains the grammar for cloze cards - let clozesGrammar = ""; - - // An array contianing the types of cards enabled by the user - const cardRulesList: string[] = ["html_comment", "tilde_code", "backprime_code"]; - - // Include reversed inline flashcards rule only if the user provided a non-empty marker for reversed inline flashcards - if (options.singleLineCardSeparator.trim() !== "") cardRulesList.push("inline_rev_card"); - - // Include inline flashcards rule only if the user provided a non-empty marker for inline flashcards - if (options.singleLineCardSeparator.trim() !== "") cardRulesList.push("inline_card"); - - // Include reversed multiline flashcards rule only if the user provided a non-empty marker for reversed multiline flashcards - if (options.multilineReversedCardSeparator.trim() !== "") - cardRulesList.push("multiline_rev_card"); - - // Include multiline flashcards rule only if the user provided a non-empty marker for multiline flashcards - if (options.multilineCardSeparator.trim() !== "") cardRulesList.push("multiline_card"); - - const clozeRulesList: string[] = []; - if (options.convertHighlightsToClozes) clozeRulesList.push("cloze_equal"); - if (options.convertBoldTextToClozes) clozeRulesList.push("cloze_star"); - if (options.convertCurlyBracketsToClozes) clozeRulesList.push("cloze_bracket"); - - // Include cloze cards only if the user enabled at least one type of cloze cards - if (clozeRulesList.length > 0) { - cardRulesList.push("cloze_card"); - const clozeRules = clozeRulesList.join(" / "); - clozesGrammar = ` -cloze_card -= $(multiline_before_cloze? cloze_line (multiline_after_cloze)? (newline annotation)?) { - return createParsedQuestionInfo(CardType.Cloze,text().trimEnd(),location().start.line-1,location().end.line-1); -} - -cloze_line -= ((!cloze_text (inline_code / non_newline))* cloze_text) text_line_nonterminated? - -multiline_before_cloze -= (!cloze_line nonempty_text_line)+ - -multiline_after_cloze -= e:(!(newline separator_line) text_line1)+ - -cloze_text -= ${clozeRules} - -cloze_equal -= cloze_mark_equal (!cloze_mark_equal non_newline)+ cloze_mark_equal - -cloze_mark_equal -= "==" - -cloze_star -= cloze_mark_star (!cloze_mark_star non_newline)+ cloze_mark_star - -cloze_mark_star -= "**" - -cloze_bracket -= cloze_mark_bracket_open (!cloze_mark_bracket_close non_newline)+ cloze_mark_bracket_close - -cloze_mark_bracket_open -= "{{" - -cloze_mark_bracket_close -= "}}" -`; - } - - // Important: we need to include `loose_line` rule to detect any other loose line. - // Otherwise, we get a syntax error because the parser is likely not able to reach the end - // of the file, as it may encounter loose lines, which it would not know how to handle. - cardRulesList.push("loose_line"); - - const cardRules = cardRulesList.join(" / "); - - return `{ - // The fallback case is important if we want to test the rules with https://peggyjs.org/online.html - const CardTypeFallBack = { - SingleLineBasic: 0, - SingleLineReversed: 1, - MultiLineBasic: 2, - MultiLineReversed: 3, - Cloze: 4, - }; - - // The fallback case is important if we want to test the rules with https://peggyjs.org/online.html - const createParsedQuestionInfoFallBack = (cardType, text, firstLineNum, lastLineNum) => { - return {cardType, text, firstLineNum, lastLineNum}; - }; - - const CardType = options.CardType ? options.CardType : CardTypeFallBack; - const createParsedQuestionInfo = options.createParsedQuestionInfo ? options.createParsedQuestionInfo : createParsedQuestionInfoFallBack; - - function filterBlocks(b) { - return b.filter( (d) => d !== null ) - } -} - -main -= blocks:block* { return filterBlocks(blocks); } - -/* The input text to the parser contains arbitrary text, not just card definitions. -Hence we fallback to matching on loose_line. The result from loose_line is filtered out by filterBlocks() */ -block -= ${cardRules} - -html_comment -= $("" (html_comment / .))* "-->" newline?) { - return null; -} - -/* Obsidian tag definition: https://help.obsidian.md/Editing+and+formatting/Tags#Tag+format */ -tag -= $("#" + name:([a-zA-Z/\\-_] { return 1; } / [0-9]{ return 0;})+ &{ - // check if it is a valid Obsidian tag - (Tags must contain at least one non-numerical character) - return name.includes(1); -}) - -inline_card -= e:inline newline? { return e; } - -inline -= $(left:(!inline_mark (inline_code / non_newline))+ inline_mark right:text_till_newline (newline annotation)?) { - return createParsedQuestionInfo(CardType.SingleLineBasic,text(),location().start.line-1,location().end.line-1); -} - -inline_rev_card -= e:inline_rev newline? { return e; } - -inline_rev -= left:(!inline_rev_mark (inline_code / non_newline))+ inline_rev_mark right:text_till_newline (newline annotation)? { - return createParsedQuestionInfo(CardType.SingleLineReversed,text(),location().start.line-1,location().end.line-1); -} - -multiline_card -= c:multiline separator_line { - return c; -} - -multiline -= arg1:multiline_before multiline_mark arg2:multiline_after { - return createParsedQuestionInfo(CardType.MultiLineBasic,(arg1+"${options.multilineCardSeparator}\\n"+arg2.trimEnd()),location().start.line-1,location().end.line-2); -} - -multiline_before -= $(!multiline_mark nonempty_text_line)+ - -multiline_after -= $(!separator_line (tilde_code / backprime_code / text_line))+ - -inline_code -= $("\`" (!"\`" .)* "\`") - -tilde_code -= $( - " "* left:$tilde_marker text_line - (!(middle:$tilde_marker &{ return left.length===middle.length;}) (tilde_code / text_line))* - (right:$tilde_marker &{ return left.length===right.length; }) - newline -) { return null; } - -tilde_marker -= "~~~" "~"* - -backprime_code -= $( - " "* left:$backprime_marker text_line - (!(middle:$backprime_marker &{ return left.length===middle.length;}) (backprime_code / text_line))* - (right:$backprime_marker &{ return left.length===right.length; }) - newline -) { return null; } - -backprime_marker -= "\`\`\`" "\`"* - -multiline_rev_card -= @multiline_rev separator_line - -multiline_rev -= arg1:multiline_rev_before multiline_rev_mark arg2:multiline_rev_after { - return createParsedQuestionInfo(CardType.MultiLineReversed,(arg1+"${options.multilineReversedCardSeparator}\\n"+arg2.trimEnd()),location().start.line-1,location().end.line-2); -} - -multiline_rev_before -= $(!multiline_rev_mark nonempty_text_line)+ - -multiline_rev_after -= $(!separator_line text_line)+ - -${clozesGrammar} - -inline_mark -= "${options.singleLineCardSeparator}" - -inline_rev_mark -= "${options.singleLineReversedCardSeparator}" - -multiline_mark -= optional_whitespaces "${options.multilineCardSeparator}" optional_whitespaces newline - -multiline_rev_mark -= optional_whitespaces "${options.multilineReversedCardSeparator}" optional_whitespaces newline - -end_card_mark -= "${options.multilineCardEndMarker}" - -separator_line -= end_card_mark optional_whitespaces newline - -text_line_nonterminated -= $nonempty_text_till_newline - -nonempty_text_line -= nonempty_text_till_newline newline - -text_line -= @$text_till_newline newline - -// very likely, it is possible to homogeneize/modify the rules to use only either 'text_line1' or 'text_line' -text_line1 -= newline @$text_till_newline - -loose_line -= $((text_till_newline newline) / nonempty_text_till_newline) { - return null; - } - -annotation -= $("" .)+ "-->") - -nonempty_text_till_newline -= $(inline_code / non_newline)+ - -text_till_newline -= $non_newline* - -non_newline -= [^\\n] - -newline -= $[\\n] - -empty_line -= $(whitespace_char* [\\n]) - -nonemptyspace -= [^ \\f\\t\\v\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff] - -optional_whitespaces -= whitespace_char* - -whitespace_char = ([ \\f\\t\\v\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]) -`; + clozePatterns: string[]; } export function setDebugParser(value: boolean) { @@ -345,7 +27,7 @@ export class ParsedQuestionInfo { constructor(cardType: CardType, text: string, firstLineNum: number, lastLineNum: number) { this.cardType = cardType; - this.text = text; // text.replace(/\s*$/gm, ""); // reproduce the same old behavior as when adding new lines with trimEnd. It is not clear why we need it in real life. However, it is needed to pass the tests. + this.text = text; this.firstLineNum = firstLineNum; this.lastLineNum = lastLineNum; } @@ -355,51 +37,172 @@ export class ParsedQuestionInfo { } } +function markerInsideCodeBlock(text: string, marker: string, markerIndex: number): boolean { + let goingBack = markerIndex - 1, + goingForward = markerIndex + marker.length; + let backTicksBefore = 0, + backTicksAfter = 0; + + while (goingBack >= 0) { + if (text[goingBack] === "`") backTicksBefore++; + goingBack--; + } + + while (goingForward < text.length) { + if (text[goingForward] === "`") backTicksAfter++; + goingForward++; + } + + // If there's an odd number of backticks before and after, + // the marker is inside an inline code block + return backTicksBefore % 2 === 1 && backTicksAfter % 2 === 1; +} + +function hasInlineMarker(text: string, marker: string): boolean { + // No marker provided + if (marker.length == 0) return false; + + // Make sure it's an "exact" match + // For instance: + // hasInlineMarker("a:::b", "::") -> false + // hasInlineMarker("a::b", "::") -> true + const markerIdx = text.indexOf(marker); + if (markerIdx === -1) return false; + + const prevChar = markerIdx > 0 ? text[markerIdx - 1] : null; + const nextChar = + markerIdx + marker.length < text.length ? text[markerIdx + marker.length] : null; + const markerMatchesExactly = prevChar !== marker[0] && nextChar !== marker[0]; + if (!markerMatchesExactly) return false; + + // Check if it's inside an inline code block + return !markerInsideCodeBlock(text, marker, markerIdx); +} + /** * Returns flashcards found in `text` * * It is best that the text does not contain frontmatter, see extractFrontmatter for reasoning * - * EXCEPTIONS: The underlying peggy parser can throw an exception if the input it receives does - * not conform to the grammar it was built with. However, the grammar used in generating this - * parser, see generateParser(), intentionally matches all input text and therefore - * this function should not throw an exception. - * * @param text - The text to extract flashcards from - * @param options - Plugin's settings - * @returns An array of [CardType, card text, line number] tuples + * @param ParserOptions - Parser options + * @returns An array of parsed question information */ -export function parseEx(text: string, options: ParserOptions): ParsedQuestionInfo[] { +export function parse(text: string, options: ParserOptions): ParsedQuestionInfo[] { if (debugParser) { console.log("Text to parse:\n<<<" + text + ">>>"); } - let cards: ParsedQuestionInfo[] = []; - try { - if (!options) throw new Error("No parser options provided."); + const cards: ParsedQuestionInfo[] = []; + let cardText = ""; + let cardType: CardType | null = null; + let firstLineNo = 0, + lastLineNo = 0; + + const clozecrafter = new ClozeCrafter(options.clozePatterns); + const lines: string[] = text.replaceAll("\r\n", "\n").split("\n"); + for (let i = 0; i < lines.length; i++) { + const currentLine = lines[i], + currentTrimmed = lines[i].trim(); + + // Skip everything in HTML comments + if (currentLine.startsWith("")) i++; + i++; + continue; + } + + // Have we reached a card end marker? + const isEmptyLine = currentTrimmed.length == 0; + const hasMultilineCardEndMarker = + options.multilineCardEndMarker && currentTrimmed == options.multilineCardEndMarker; + if ( + // We've possibly reached the end of a card + (isEmptyLine && !options.multilineCardEndMarker) || + // Empty line & we're not picking up any card + (isEmptyLine && cardText.length == 0) || + // We've reached the end of a multi line card & + // we're using custom end markers + hasMultilineCardEndMarker + ) { + if (cardType) { + // Create a new card + lastLineNo = i - 1; + cards.push( + new ParsedQuestionInfo(cardType, cardText.trimEnd(), firstLineNo, lastLineNo), + ); + cardType = null; + } + + cardText = ""; + firstLineNo = i + 1; + continue; + } + + // Update card text + if (cardText.length > 0) { + cardText += "\n"; + } + cardText += currentLine.trimEnd(); - const parser: Parser = generateParser(options); + // Pick up inline cards + const hasSingleLineCardSeparator: boolean = hasInlineMarker( + currentLine, + options.singleLineCardSeparator, + ); + if ( + hasSingleLineCardSeparator || + // Has single line reversed card separator + hasInlineMarker(currentLine, options.singleLineReversedCardSeparator) + ) { + cardType = hasSingleLineCardSeparator + ? CardType.SingleLineBasic + : CardType.SingleLineReversed; + cardText = currentLine; + firstLineNo = i; + + // Pick up scheduling information if present + if (i + 1 < lines.length && lines[i + 1].startsWith(" -The final schedule info "!2033-03-03,3,333" has been deleted + A {{question}} with multiple parts {{Navevo part}} + + The final schedule info "!2033-03-03,3,333" has been deleted */ const file = osrCore.getFileByNoteName("A"); expect(file.content).toContain(""); diff --git a/tests/unit/parser.test.ts b/tests/unit/parser.test.ts index f6f30b8f..9eb7ff81 100644 --- a/tests/unit/parser.test.ts +++ b/tests/unit/parser.test.ts @@ -1,4 +1,4 @@ -import { ParsedQuestionInfo, parseEx, setDebugParser } from "src/parser"; +import { parse, ParsedQuestionInfo, setDebugParser } from "src/parser"; import { ParserOptions } from "src/parser"; import { CardType } from "src/question"; @@ -8,19 +8,21 @@ const parserOptions: ParserOptions = { multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "", - convertHighlightsToClozes: true, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: true, + clozePatterns: [ + "==[123;;]answer[;;hint]==", + "**[123;;]answer[;;hint]**", + "{{[123;;]answer[;;hint]}}", + ], }; /** - * This function is a small wrapper around parseEx used for testing only. + * This function is a small wrapper around parse used for testing only. * It generates a parser each time, overwriting the default one. * Created when the actual parser changed from returning [CardType, string, number, number] to ParsedQuestionInfo. * It's purpose is to minimise changes to all the test cases here during the parser()->parserEx() change. */ -function parse(text: string, options: ParserOptions): [CardType, string, number, number][] { - const list: ParsedQuestionInfo[] = parseEx(text, options); +function parseT(text: string, options: ParserOptions): [CardType, string, number, number][] { + const list: ParsedQuestionInfo[] = parse(text, options); const result: [CardType, string, number, number][] = []; for (const item of list) { result.push([item.cardType, item.text, item.firstLineNum, item.lastLineNum]); @@ -30,150 +32,148 @@ function parse(text: string, options: ParserOptions): [CardType, string, number, test("Test parsing of single line basic cards", () => { // standard symbols - expect(parse("Question::Answer", parserOptions)).toEqual([ + expect(parseT("Question::Answer", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question::Answer", 0, 0], ]); - expect(parse("Question::Answer\n", parserOptions)).toEqual([ + expect(parseT("Question::Answer\n", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question::Answer\n", 0, 1], ]); - expect(parse("Question::Answer ", parserOptions)).toEqual([ + expect(parseT("Question::Answer ", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question::Answer ", 0, 0], ]); - expect(parse("Some text before\nQuestion ::Answer", parserOptions)).toEqual([ + expect(parseT("Some text before\nQuestion ::Answer", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Question ::Answer", 1, 1], ]); - expect(parse("#Title\n\nQ1::A1\nQ2:: A2", parserOptions)).toEqual([ + expect(parseT("#Title\n\nQ1::A1\nQ2:: A2", parserOptions)).toEqual([ [CardType.SingleLineBasic, "Q1::A1", 2, 2], [CardType.SingleLineBasic, "Q2:: A2", 3, 3], ]); - expect(parse("#flashcards/science Question ::Answer", parserOptions)).toEqual([ + expect(parseT("#flashcards/science Question ::Answer", parserOptions)).toEqual([ [CardType.SingleLineBasic, "#flashcards/science Question ::Answer", 0, 0], ]); // custom symbols expect( - parse("Question&&Answer", { + parseT("Question&&Answer", { singleLineCardSeparator: "&&", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([[CardType.SingleLineBasic, "Question&&Answer", 0, 0]]); expect( - parse("Question=Answer", { + parseT("Question=Answer", { singleLineCardSeparator: "=", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([[CardType.SingleLineBasic, "Question=Answer", 0, 0]]); // empty string or whitespace character provided expect( - parse("Question::Answer", { + parseT("Question::Answer", { singleLineCardSeparator: "", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([]); }); test("Test parsing of single line reversed cards", () => { // standard symbols - expect(parse("Question:::Answer", parserOptions)).toEqual([ + expect(parseT("Question:::Answer", parserOptions)).toEqual([ [CardType.SingleLineReversed, "Question:::Answer", 0, 0], ]); - expect(parse("Some text before\nQuestion :::Answer", parserOptions)).toEqual([ + expect(parseT("Some text before\nQuestion :::Answer", parserOptions)).toEqual([ [CardType.SingleLineReversed, "Question :::Answer", 1, 1], ]); - expect(parse("#Title\n\nQ1:::A1\nQ2::: A2", parserOptions)).toEqual([ + expect(parseT("#Title\n\nQ1:::A1\nQ2::: A2", parserOptions)).toEqual([ [CardType.SingleLineReversed, "Q1:::A1", 2, 2], [CardType.SingleLineReversed, "Q2::: A2", 3, 3], ]); // custom symbols expect( - parse("Question&&&Answer", { + parseT("Question&&&Answer", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: "&&&", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([[CardType.SingleLineReversed, "Question&&&Answer", 0, 0]]); + expect( + parseT("Question::Answer", { + singleLineCardSeparator: ":::", + singleLineReversedCardSeparator: "::", + multilineCardSeparator: "?", + multilineReversedCardSeparator: "??", + multilineCardEndMarker: "---", + clozePatterns: [], + }), + ).toEqual([[CardType.SingleLineReversed, "Question::Answer", 0, 0]]); // empty string or whitespace character provided expect( - parse("Question:::Answer", { + parseT("Question:::Answer", { singleLineCardSeparator: ">", singleLineReversedCardSeparator: " ", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([]); }); test("Test parsing of multi line basic cards", () => { // standard symbols - expect(parse("Question\n?\nAnswer", parserOptions)).toEqual([ + expect(parseT("Question\n?\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer", 0, 2], ]); - expect(parse("Question\n? \nAnswer", parserOptions)).toEqual([ + expect(parseT("Question\n? \nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer", 0, 2], ]); - expect(parse("Question\n?\nAnswer ", parserOptions)).toEqual([ + expect(parseT("Question\n?\nAnswer ", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer ", 0, 2], ]); - expect(parse("Question\n?\nAnswer\n", parserOptions)).toEqual([ + expect(parseT("Question\n?\nAnswer\n", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer\n", 0, 3], ]); - expect(parse("Question line 1\nQuestion line 2\n?\nAnswer", parserOptions)).toEqual([ + expect(parseT("Question line 1\nQuestion line 2\n?\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question line 1\nQuestion line 2\n?\nAnswer", 0, 3], ]); - expect(parse("Question\n?\nAnswer line 1\nAnswer line 2", parserOptions)).toEqual([ + expect(parseT("Question\n?\nAnswer line 1\nAnswer line 2", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer line 1\nAnswer line 2", 0, 3], ]); - expect(parse("#Title\n\nLine0\nQ1\n?\nA1\nAnswerExtra\n\nQ2\n?\nA2", parserOptions)).toEqual([ + expect(parseT("#Title\n\nLine0\nQ1\n?\nA1\nAnswerExtra\n\nQ2\n?\nA2", parserOptions)).toEqual([ [CardType.MultiLineBasic, "Line0\nQ1\n?\nA1\nAnswerExtra", 2, 6], [CardType.MultiLineBasic, "Q2\n?\nA2", 8, 10], ]); - expect(parse("#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", parserOptions)).toEqual([ + expect(parseT("#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineBasic, "#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", 0, 3], ]); expect( - parse("Question\n?\nAnswer line 1\nAnswer line 2\n\n---", { + parseT("Question\n?\nAnswer line 1\nAnswer line 2\n\n---", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: false, + clozePatterns: ["**[123;;]answer[;;hint]**"], }), ).toEqual([[CardType.MultiLineBasic, "Question\n?\nAnswer line 1\nAnswer line 2", 0, 4]]); expect( - parse( + parseT( "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion 2\n?\nAnswer line 1\nAnswer line 2\n---\n", { singleLineCardSeparator: "::", @@ -181,9 +181,7 @@ test("Test parsing of multi line basic cards", () => { multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: false, + clozePatterns: ["**[123;;]answer[;;hint]**"], }, ), ).toEqual([ @@ -191,7 +189,7 @@ test("Test parsing of multi line basic cards", () => { [CardType.MultiLineBasic, "Question 2\n?\nAnswer line 1\nAnswer line 2", 6, 9], ]); expect( - parse( + parseT( "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion with empty line after question mark\n?\n\nAnswer line 1\nAnswer line 2\n---\n", { singleLineCardSeparator: "::", @@ -199,9 +197,7 @@ test("Test parsing of multi line basic cards", () => { multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: false, + clozePatterns: ["**[123;;]answer[;;hint]**"], }, ), ).toEqual([ @@ -216,62 +212,62 @@ test("Test parsing of multi line basic cards", () => { // custom symbols expect( - parse("Question\n@@\nAnswer\n\nsfdg", { + parseT("Question\n@@\nAnswer\n\nsfdg", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "@@", multilineReversedCardSeparator: "??", multilineCardEndMarker: "", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([[CardType.MultiLineBasic, "Question\n@@\nAnswer", 0, 2]]); // empty string or whitespace character provided expect( - parse("Question\n?\nAnswer", { + parseT("Question\n?\nAnswer", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([]); }); test("Test parsing of multi line reversed cards", () => { // standard symbols - expect(parse("Question\n??\nAnswer", parserOptions)).toEqual([ + expect(parseT("Question\n??\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineReversed, "Question\n??\nAnswer", 0, 2], ]); - expect(parse("Question line 1\nQuestion line 2\n??\nAnswer", parserOptions)).toEqual([ + expect(parseT("Question line 1\nQuestion line 2\n??\nAnswer", parserOptions)).toEqual([ [CardType.MultiLineReversed, "Question line 1\nQuestion line 2\n??\nAnswer", 0, 3], ]); - expect(parse("Question\n??\nAnswer line 1\nAnswer line 2", parserOptions)).toEqual([ + expect(parseT("Question\n??\nAnswer line 1\nAnswer line 2", parserOptions)).toEqual([ [CardType.MultiLineReversed, "Question\n??\nAnswer line 1\nAnswer line 2", 0, 3], ]); - expect(parse("#Title\n\nLine0\nQ1\n??\nA1\nAnswerExtra\n\nQ2\n??\nA2", parserOptions)).toEqual([ - [CardType.MultiLineReversed, "Line0\nQ1\n??\nA1\nAnswerExtra", 2, 6], - [CardType.MultiLineReversed, "Q2\n??\nA2", 8, 10], - ]); + expect(parseT("#Title\n\nLine0\nQ1\n??\nA1\nAnswerExtra\n\nQ2\n??\nA2", parserOptions)).toEqual( + [ + [CardType.MultiLineReversed, "Line0\nQ1\n??\nA1\nAnswerExtra", 2, 6], + [CardType.MultiLineReversed, "Q2\n??\nA2", 8, 10], + ], + ); expect( - parse("Question\n??\nAnswer line 1\nAnswer line 2\n\n---", { + parseT("Question\n??\nAnswer line 1\nAnswer line 2\n\n---", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: true, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: true, + clozePatterns: [ + "==[123;;]answer[;;hint]==", + "**[123;;]answer[;;hint]**", + "{{[123;;]answer[;;hint]}}", + ], }), ).toEqual([[CardType.MultiLineReversed, "Question\n??\nAnswer line 1\nAnswer line 2", 0, 4]]); expect( - parse( + parseT( "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion 2\n??\nAnswer line 1\nAnswer line 2\n---\n", { singleLineCardSeparator: "::", @@ -279,9 +275,11 @@ test("Test parsing of multi line reversed cards", () => { multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: true, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: true, + clozePatterns: [ + "==[123;;]answer[;;hint]==", + "**[123;;]answer[;;hint]**", + "{{[123;;]answer[;;hint]}}", + ], }, ), ).toEqual([ @@ -291,49 +289,45 @@ test("Test parsing of multi line reversed cards", () => { // custom symbols expect( - parse("Question\n@@@\nAnswer\n---", { + parseT("Question\n@@@\nAnswer\n---", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "@@", multilineReversedCardSeparator: "@@@", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([[CardType.MultiLineReversed, "Question\n@@@\nAnswer", 0, 2]]); // empty string or whitespace character provided expect( - parse("Question\n??\nAnswer", { + parseT("Question\n??\nAnswer", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "\t", multilineCardEndMarker: "---", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([]); }); test("Test parsing of cloze cards", () => { // ==highlights== - expect(parse("cloze ==deletion== test", parserOptions)).toEqual([ + expect(parseT("cloze ==deletion== test", parserOptions)).toEqual([ [CardType.Cloze, "cloze ==deletion== test", 0, 0], ]); - expect(parse("cloze ==deletion== test\n", parserOptions)).toEqual([ + expect(parseT("cloze ==deletion== test\n", parserOptions)).toEqual([ [CardType.Cloze, "cloze ==deletion== test\n", 0, 1], ]); - expect(parse("cloze ==deletion== test ", parserOptions)).toEqual([ + expect(parseT("cloze ==deletion== test ", parserOptions)).toEqual([ [CardType.Cloze, "cloze ==deletion== test ", 0, 0], ]); - expect(parse("==this== is a ==deletion==\n", parserOptions)).toEqual([ + expect(parseT("==this== is a ==deletion==\n", parserOptions)).toEqual([ [CardType.Cloze, "==this== is a ==deletion==", 0, 0], ]); expect( - parse( + parseT( "some text before\n\na deletion on\nsuch ==wow==\n\n" + "many text\nsuch surprise ==wow== more ==text==\nsome text after\n\nHmm", parserOptions, @@ -342,39 +336,37 @@ test("Test parsing of cloze cards", () => { [CardType.Cloze, "a deletion on\nsuch ==wow==", 2, 3], [CardType.Cloze, "many text\nsuch surprise ==wow== more ==text==\nsome text after", 5, 7], ]); - expect(parse("srdf ==", parserOptions)).toEqual([]); - expect(parse("lorem ipsum ==p\ndolor won==", parserOptions)).toEqual([]); - expect(parse("lorem ipsum ==dolor won=", parserOptions)).toEqual([]); + expect(parseT("srdf ==", parserOptions)).toEqual([]); + expect(parseT("lorem ipsum ==p\ndolor won==", parserOptions)).toEqual([]); + expect(parseT("lorem ipsum ==dolor won=", parserOptions)).toEqual([]); // ==highlights== turned off expect( - parse("cloze ==deletion== test", { + parseT("cloze ==deletion== test", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "", - convertHighlightsToClozes: false, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: true, + clozePatterns: ["**[123;;]answer[;;hint]**", "{{[123;;]answer[;;hint]}}"], }), ).toEqual([]); // **bolded** - expect(parse("cloze **deletion** test", parserOptions)).toEqual([ + expect(parseT("cloze **deletion** test", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test", 0, 0], ]); - expect(parse("cloze **deletion** test\n", parserOptions)).toEqual([ + expect(parseT("cloze **deletion** test\n", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test\n", 0, 1], ]); - expect(parse("cloze **deletion** test ", parserOptions)).toEqual([ + expect(parseT("cloze **deletion** test ", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test ", 0, 0], ]); - expect(parse("**this** is a **deletion**\n", parserOptions)).toEqual([ + expect(parseT("**this** is a **deletion**\n", parserOptions)).toEqual([ [CardType.Cloze, "**this** is a **deletion**", 0, 0], ]); expect( - parse( + parseT( "some text before\n\na deletion on\nsuch **wow**\n\n" + "many text\nsuch surprise **wow** more **text**\nsome text after\n\nHmm", parserOptions, @@ -383,39 +375,37 @@ test("Test parsing of cloze cards", () => { [CardType.Cloze, "a deletion on\nsuch **wow**", 2, 3], [CardType.Cloze, "many text\nsuch surprise **wow** more **text**\nsome text after", 5, 7], ]); - expect(parse("srdf **", parserOptions)).toEqual([]); - expect(parse("lorem ipsum **p\ndolor won**", parserOptions)).toEqual([]); - expect(parse("lorem ipsum **dolor won*", parserOptions)).toEqual([]); + expect(parseT("srdf **", parserOptions)).toEqual([]); + expect(parseT("lorem ipsum **p\ndolor won**", parserOptions)).toEqual([]); + expect(parseT("lorem ipsum **dolor won*", parserOptions)).toEqual([]); // **bolded** turned off expect( - parse("cloze **deletion** test", { + parseT("cloze **deletion** test", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "", - convertHighlightsToClozes: true, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: true, + clozePatterns: ["==[123;;]answer[;;hint]==", "{{[123;;]answer[;;hint]}}"], }), ).toEqual([]); // {{curly}} - expect(parse("cloze {{deletion}} test", parserOptions)).toEqual([ + expect(parseT("cloze {{deletion}} test", parserOptions)).toEqual([ [CardType.Cloze, "cloze {{deletion}} test", 0, 0], ]); - expect(parse("cloze {{deletion}} test\n", parserOptions)).toEqual([ + expect(parseT("cloze {{deletion}} test\n", parserOptions)).toEqual([ [CardType.Cloze, "cloze {{deletion}} test\n", 0, 1], ]); - expect(parse("cloze {{deletion}} test ", parserOptions)).toEqual([ + expect(parseT("cloze {{deletion}} test ", parserOptions)).toEqual([ [CardType.Cloze, "cloze {{deletion}} test ", 0, 0], ]); - expect(parse("{{this}} is a {{deletion}}\n", parserOptions)).toEqual([ + expect(parseT("{{this}} is a {{deletion}}\n", parserOptions)).toEqual([ [CardType.Cloze, "{{this}} is a {{deletion}}", 0, 0], ]); expect( - parse( + parseT( "some text before\n\na deletion on\nsuch {{wow}}\n\n" + "many text\nsuch surprise {{wow}} more {{text}}\nsome text after\n\nHmm", parserOptions, @@ -424,53 +414,49 @@ test("Test parsing of cloze cards", () => { [CardType.Cloze, "a deletion on\nsuch {{wow}}", 2, 3], [CardType.Cloze, "many text\nsuch surprise {{wow}} more {{text}}\nsome text after", 5, 7], ]); - expect(parse("srdf {{", parserOptions)).toEqual([]); - expect(parse("srdf }}", parserOptions)).toEqual([]); - expect(parse("lorem ipsum {{p\ndolor won}}", parserOptions)).toEqual([]); - expect(parse("lorem ipsum {{dolor won}", parserOptions)).toEqual([]); + expect(parseT("srdf {{", parserOptions)).toEqual([]); + expect(parseT("srdf }}", parserOptions)).toEqual([]); + expect(parseT("lorem ipsum {{p\ndolor won}}", parserOptions)).toEqual([]); + expect(parseT("lorem ipsum {{dolor won}", parserOptions)).toEqual([]); // {{curly}} turned off expect( - parse("cloze {{deletion}} test", { + parseT("cloze {{deletion}} test", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "", - convertHighlightsToClozes: true, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: false, + clozePatterns: ["==[123;;]answer[;;hint]==", "**[123;;]answer[;;hint]**"], }), ).toEqual([]); // combo - expect(parse("cloze **deletion** test ==another deletion==!", parserOptions)).toEqual([ + expect(parseT("cloze **deletion** test ==another deletion==!", parserOptions)).toEqual([ [CardType.Cloze, "cloze **deletion** test ==another deletion==!", 0, 0], ]); expect( - parse( - "Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nTest 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nHere is some more text.", + parseT( + "Test 1\nTest 2\nThis is a cloze with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nTest 3\nTest 4\nThis is a cloze with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nHere is some more text.", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "---", - convertHighlightsToClozes: true, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: ["==[123;;]answer[;;hint]=="], }, ), ).toEqual([ [ CardType.Cloze, - "Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.", + "Test 1\nTest 2\nThis is a cloze with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.", 0, 7, ], [ CardType.Cloze, - "Test 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.", + "Test 3\nTest 4\nThis is a cloze with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.", 10, 17, ], @@ -478,22 +464,20 @@ test("Test parsing of cloze cards", () => { // all disabled expect( - parse("cloze {{deletion}} test and **deletion** ==another deletion==!", { + parseT("cloze {{deletion}} test and **deletion** ==another deletion==!", { singleLineCardSeparator: "::", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "", - convertHighlightsToClozes: false, - convertBoldTextToClozes: false, - convertCurlyBracketsToClozes: false, + clozePatterns: [], }), ).toEqual([]); }); test("Test parsing of a mix of card types", () => { expect( - parse( + parseT( "# Lorem Ipsum\n\nLorem ipsum dolor ==sit amet==, consectetur ==adipiscing== elit.\n" + "Duis magna arcu, eleifend rhoncus ==euismod non,==\nlaoreet vitae enim.\n\n" + "Fusce placerat::velit in pharetra gravida\n\n" + @@ -522,7 +506,7 @@ test("Test parsing of a mix of card types", () => { test("Test parsing cards with codeblocks", () => { // `inline` expect( - parse( + parseT( "my inline question containing `some inline code` in it::and this is answer possibly containing `inline` code.", parserOptions, ), @@ -534,10 +518,13 @@ test("Test parsing cards with codeblocks", () => { 0, ], ]); + expect(parseT("this has some ==`inline`== code", parserOptions)).toEqual([ + [CardType.Cloze, "this has some ==`inline`== code", 0, 0], + ]); // ```block```, no blank lines expect( - parse( + parseT( "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\nprint('Howdy?')\nlambda x: x[0]\n```", parserOptions, @@ -554,7 +541,7 @@ test("Test parsing cards with codeblocks", () => { // ```block```, with blank lines expect( - parse( + parseT( "How do you ... Python?\n?\n" + "```\nprint('Hello World!')\n\n\nprint('Howdy?')\n\nlambda x: x[0]\n```", parserOptions, @@ -571,7 +558,7 @@ test("Test parsing cards with codeblocks", () => { // nested markdown expect( - parse( + parseT( "Nested Markdown?\n?\n" + "````ad-note\n\n" + "```git\n" + @@ -604,87 +591,73 @@ test("Test parsing cards with codeblocks", () => { }); test("Test not parsing cards in HTML comments", () => { - expect(parse("", parserOptions)).toEqual([]); - expect(parse("", parserOptions)).toEqual([]); + expect(parseT("", parserOptions)).toEqual([]); + expect(parseT("", parserOptions)).toEqual([]); expect( - parse("\n-->", parserOptions), + parseT("\n-->", parserOptions), ).toEqual([]); expect( - parse( + parseT( "\n\n-->", parserOptions, ), ).toEqual([]); - expect(parse("", parserOptions)).toEqual([]); - expect(parse("", parserOptions)).toEqual([]); - expect(parse("", parserOptions)).toEqual([]); + expect(parseT("", parserOptions)).toEqual([]); + expect(parseT("", parserOptions)).toEqual([]); + expect(parseT("", parserOptions)).toEqual([]); + expect(parseT("something something\n", parserOptions)).toEqual([]); + + // cards found outside comment + expect( + parseT("something something\n\n\n\na::b", parserOptions), + ).toEqual([[CardType.SingleLineBasic, "a::b", 4, 4]]); }); test("Test not parsing 'cards' in codeblocks", () => { // block - expect(parse("```\nCodeblockq::CodeblockA\n```", parserOptions)).toEqual([]); - expect(parse("```\nCodeblockq:::CodeblockA\n```", parserOptions)).toEqual([]); + expect(parseT("```\nCodeblockq::CodeblockA\n```", parserOptions)).toEqual([]); + expect(parseT("```\nCodeblockq:::CodeblockA\n```", parserOptions)).toEqual([]); expect( - parse("# Title\n\n```markdown\nsome ==highlighted text==!\n```\n\nmore!", parserOptions), + parseT("# Title\n\n```markdown\nsome ==highlighted text==!\n```\n\nmore!", parserOptions), ).toEqual([]); expect( - parse("# Title\n```markdown\nsome **bolded text**!\n```\n\nmore!", parserOptions), + parseT("# Title\n```markdown\nsome **bolded text**!\n```\n\nmore!", parserOptions), ).toEqual([]); - expect(parse("# Title\n\n```\nfoo = {{'a': 2}}\n```\n\nmore!", parserOptions)).toEqual([]); + expect(parseT("# Title\n\n```\nfoo = {{'a': 2}}\n```\n\nmore!", parserOptions)).toEqual([]); // inline - expect(parse("`Inlineq::InlineA`", parserOptions)).toEqual([]); + expect(parseT("`Inlineq::InlineA`", parserOptions)).toEqual([]); expect( - parse("# Title\n`if (a & b) {}`\nmore!", { + parseT("# Title\n`if (a & b) {}`\nmore!", { singleLineCardSeparator: "&", singleLineReversedCardSeparator: ":::", multilineCardSeparator: "?", multilineReversedCardSeparator: "??", multilineCardEndMarker: "", - convertHighlightsToClozes: true, - convertBoldTextToClozes: true, - convertCurlyBracketsToClozes: true, + clozePatterns: [ + "==[123;;]answer[;;hint]==", + "**[123;;]answer[;;hint]**", + "{{[123;;]answer[;;hint]}}", + ], }), ).toEqual([]); - expect(parse("# Title\n`if a == b && c == d {}`\nmore!", parserOptions)).toEqual([]); - expect(parse("# Title\n\n`z = a ** b + 5 ** 7`\n\nmore!", parserOptions)).toEqual([]); - expect(parse("# Title\n`foo = {{'a': 2}}`\n\nmore!", parserOptions)).toEqual([]); // combo expect( - parse( + parseT( "Question::Answer\n\n```\nCodeblockq::CodeblockA\n```\n\n`Inlineq::InlineA`\n", parserOptions, ), ).toEqual([[CardType.SingleLineBasic, "Question::Answer", 0, 0]]); }); -test("Unexpected Error case", () => { - // replace console error log with an empty mock function - const errorSpy = jest.spyOn(global.console, "error").mockImplementation(() => {}); - - expect(parseEx("", null)).toStrictEqual([]); - - expect(errorSpy).toHaveBeenCalled(); - expect(errorSpy.mock.calls[0][0]).toMatch(/^Unexpected error:.*/); - - // clear the mock - errorSpy.mockClear(); - - expect(parseEx("", parserOptions)).toStrictEqual([]); - expect(errorSpy).toHaveBeenCalledTimes(0); - - // restore original console error log - errorSpy.mockRestore(); -}); - describe("Parser debug messages", () => { test("Messages disabled", () => { // replace console error log with an empty mock function const logSpy = jest.spyOn(global.console, "log").mockImplementation(() => {}); setDebugParser(false); - parseEx("", parserOptions); + parse("", parserOptions); expect(logSpy).toHaveBeenCalledTimes(0); // restore original console error log @@ -696,7 +669,7 @@ describe("Parser debug messages", () => { const logSpy = jest.spyOn(global.console, "log").mockImplementation(() => {}); setDebugParser(true); - parseEx("", parserOptions); + parse("", parserOptions); expect(logSpy).toHaveBeenCalled(); // restore original console error log diff --git a/tests/unit/question-type.test.ts b/tests/unit/question-type.test.ts index 3f5e88ed..c8e78786 100644 --- a/tests/unit/question-type.test.ts +++ b/tests/unit/question-type.test.ts @@ -1,5 +1,5 @@ import { CardType } from "src/question"; -import { CardFrontBack, CardFrontBackUtil, QuestionTypeClozeUtil } from "src/question-type"; +import { CardFrontBack, CardFrontBackUtil, QuestionTypeClozeFormatter } from "src/question-type"; import { DEFAULT_SETTINGS, SRSettings } from "src/settings"; test("CardType.SingleLineBasic", () => { @@ -37,7 +37,7 @@ test("CardType.MultiLineReversed", () => { }); test("CardType.Cloze", () => { - const frontHtml = QuestionTypeClozeUtil.renderClozeFront(); + const clozeFormatter = new QuestionTypeClozeFormatter(); expect( CardFrontBackUtil.expand( @@ -47,22 +47,24 @@ test("CardType.Cloze", () => { ), ).toEqual([ new CardFrontBack( - "This is a very " + frontHtml + " test", - "This is a very " + QuestionTypeClozeUtil.renderClozeBack("interesting") + " test", + "This is a very " + clozeFormatter.asking() + " test", + "This is a very " + clozeFormatter.showingAnswer("interesting") + " test", ), ]); const settings2: SRSettings = DEFAULT_SETTINGS; - settings2.convertBoldTextToClozes = true; - settings2.convertHighlightsToClozes = true; - settings2.convertCurlyBracketsToClozes = true; + settings2.clozePatterns = [ + "==[123;;]answer[;;hint]==", + "**[123;;]answer[;;hint]**", + "{{[123;;]answer[;;hint]}}", + ]; expect( CardFrontBackUtil.expand(CardType.Cloze, "This is a very **interesting** test", settings2), ).toEqual([ new CardFrontBack( - "This is a very " + frontHtml + " test", - "This is a very " + QuestionTypeClozeUtil.renderClozeBack("interesting") + " test", + "This is a very " + clozeFormatter.asking() + " test", + "This is a very " + clozeFormatter.showingAnswer("interesting") + " test", ), ]); @@ -70,8 +72,8 @@ test("CardType.Cloze", () => { CardFrontBackUtil.expand(CardType.Cloze, "This is a very {{interesting}} test", settings2), ).toEqual([ new CardFrontBack( - "This is a very " + frontHtml + " test", - "This is a very " + QuestionTypeClozeUtil.renderClozeBack("interesting") + " test", + "This is a very " + clozeFormatter.asking() + " test", + "This is a very " + clozeFormatter.showingAnswer("interesting") + " test", ), ]); @@ -82,10 +84,6 @@ test("CardType.Cloze", () => { settings2, ), ).toEqual([ - new CardFrontBack( - "This is a really very [...] and fascinating and great test", - "This is a really very interesting and fascinating and great test", - ), new CardFrontBack( "This is a really very interesting and [...] and great test", "This is a really very interesting and fascinating and great test", @@ -94,5 +92,9 @@ test("CardType.Cloze", () => { "This is a really very interesting and fascinating and [...] test", "This is a really very interesting and fascinating and great test", ), + new CardFrontBack( + "This is a really very [...] and fascinating and great test", + "This is a really very interesting and fascinating and great test", + ), ]); });