@qnroa/qtype 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -3
- package/CHANGELOG.zh.md +1 -2
- package/README.md +88 -56
- package/README.zh.md +82 -62
- package/dist/cli/bootstrap.js +4 -115
- package/dist/cli/commands/check/index.js +11 -10
- package/dist/cli/commands/config/action.js +5 -9
- package/dist/cli/commands/content/lint/action.js +38 -44
- package/dist/cli/commands/content/search/action.js +44 -34
- package/dist/cli/commands/content/search/i18n.js +6 -0
- package/dist/cli/commands/content/search/index.js +19 -1
- package/dist/cli/commands/content/validate/action.js +28 -27
- package/dist/cli/commands/index.js +1 -3
- package/dist/cli/commands/pack/index.js +2 -1
- package/dist/cli/commands/publish/build/action.js +39 -11
- package/dist/cli/commands/repo/new/action.js +23 -4
- package/dist/cli/commands/repo/new/i18n.js +2 -2
- package/dist/cli/commands/repo/sync/action.js +24 -4
- package/dist/cli/commands/repo/templates.js +2 -1
- package/dist/cli/utils/material.js +49 -0
- package/dist/core/encrypt.js +24 -1
- package/dist/core/index.js +1 -0
- package/dist/core/parser/frontmatter.js +39 -34
- package/dist/core/parser/parseMaterial.js +174 -24
- package/dist/core/romanization/index.js +152 -0
- package/dist/core/schemas/both.js +52 -0
- package/dist/core/schemas/index.js +3 -0
- package/dist/core/schemas/repo.js +55 -0
- package/dist/core/schemas/user.js +19 -0
- package/dist/core/typing/engine/TypingInput.js +243 -0
- package/dist/core/typing/engine/chars.js +30 -0
- package/dist/core/typing/engine/feedback.js +15 -0
- package/dist/core/typing/engine/index.js +5 -0
- package/dist/core/typing/engine/settings.js +8 -0
- package/dist/core/typing/engine/stats.js +45 -0
- package/dist/core/typing/index.js +9 -1
- package/dist/view/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/view/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/view/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/view/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/view/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/view/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/view/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/view/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/view/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/view/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/view/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/view/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/view/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/view/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/view/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/view/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/view/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/view/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/view/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/view/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/view/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/view/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/view/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/view/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/view/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/view/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/view/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/view/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/view/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/view/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/view/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/view/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/view/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/view/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/view/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/view/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/view/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/view/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/view/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/view/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/view/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/view/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/view/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/view/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/view/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/view/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/view/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/view/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/view/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/view/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/view/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/view/assets/abap-BdImnpbu.js +1 -0
- package/dist/view/assets/actionscript-3-B3316cI-.js +1 -0
- package/dist/view/assets/ada-bCR0ucgS.js +1 -0
- package/dist/view/assets/ahk-CsyLZFj1.js +1 -0
- package/dist/view/assets/ahk2-8Zs4aa1G.js +1 -0
- package/dist/view/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/view/assets/angular-html-DA-rfuFy.js +1 -0
- package/dist/view/assets/angular-ts-BrjP3tb8.js +1 -0
- package/dist/view/assets/apache-Pmp26Uib.js +1 -0
- package/dist/view/assets/apex-DhZFqWV2.js +1 -0
- package/dist/view/assets/apl-CORt7UWP.js +1 -0
- package/dist/view/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/view/assets/ara-BRHolxvo.js +1 -0
- package/dist/view/assets/asciidoc-CSVQ5wI8.js +1 -0
- package/dist/view/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/view/assets/astro-HNnZUWAn.js +1 -0
- package/dist/view/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/view/assets/awk-DMzUqQB5.js +1 -0
- package/dist/view/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/view/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/view/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/view/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/view/assets/bat-CickPsom.js +1 -0
- package/dist/view/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/view/assets/berry-uYugtg8r.js +1 -0
- package/dist/view/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/view/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/view/assets/bird2-Bx8U0n9b.js +1 -0
- package/dist/view/assets/blade-2xfisSek.js +1 -0
- package/dist/view/assets/bsl-DlhNcFeZ.js +1 -0
- package/dist/view/assets/c-BIGW1oBm.js +1 -0
- package/dist/view/assets/c3-Dp5svz6Z.js +1 -0
- package/dist/view/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/view/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/view/assets/catppuccin-frappe-CZL1YF0i.js +1 -0
- package/dist/view/assets/catppuccin-latte-DH-8KZSZ.js +1 -0
- package/dist/view/assets/catppuccin-macchiato-B7yYVSCf.js +1 -0
- package/dist/view/assets/catppuccin-mocha-Ct7hS0mc.js +1 -0
- package/dist/view/assets/chapel-DTp_pixX.js +1 -0
- package/dist/view/assets/clarity-Dn5IMItf.js +1 -0
- package/dist/view/assets/clojure-P80f7IUj.js +1 -0
- package/dist/view/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/view/assets/cobol-nBiQ_Alo.js +1 -0
- package/dist/view/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/view/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/view/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/view/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/view/assets/coq-C7JzOVbR.js +1 -0
- package/dist/view/assets/cpp-BMRokrvK.js +1 -0
- package/dist/view/assets/crystal-DGywbUpC.js +1 -0
- package/dist/view/assets/csharp-DSvCPggb.js +1 -0
- package/dist/view/assets/css-CLj8gQPS.js +1 -0
- package/dist/view/assets/csv-fuZLfV_i.js +1 -0
- package/dist/view/assets/cue-D82EKSYY.js +1 -0
- package/dist/view/assets/cypher-COkxafJQ.js +1 -0
- package/dist/view/assets/d-85-TOEBH.js +1 -0
- package/dist/view/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/view/assets/dart-bE4Kk8sk.js +1 -0
- package/dist/view/assets/dax-CEL-wOlO.js +1 -0
- package/dist/view/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/view/assets/diff-D97Zzqfu.js +1 -0
- package/dist/view/assets/docker-BcOcwvcX.js +1 -0
- package/dist/view/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/view/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/view/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/view/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/view/assets/edge-FbVlp4U3.js +1 -0
- package/dist/view/assets/elixir-CkH2-t6x.js +1 -0
- package/dist/view/assets/elm-DbKCFpqz.js +1 -0
- package/dist/view/assets/emacs-lisp-C_m_b--Z.js +1 -0
- package/dist/view/assets/erb-DXfck5VN.js +1 -0
- package/dist/view/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/view/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/view/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/view/assets/fennel-BYunw83y.js +1 -0
- package/dist/view/assets/fish-BvzEVeQv.js +1 -0
- package/dist/view/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/view/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/view/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/view/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/view/assets/gdresource-TyuKm33G.js +1 -0
- package/dist/view/assets/gdscript-DqcFQ5yU.js +1 -0
- package/dist/view/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/view/assets/genie-D0YGMca9.js +1 -0
- package/dist/view/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/view/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/view/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/view/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/view/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/view/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/view/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/view/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/view/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/view/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/view/assets/gleam-BspZqrRM.js +1 -0
- package/dist/view/assets/glimmer-js-ByusRIyA.js +1 -0
- package/dist/view/assets/glimmer-ts-BfAWNZQY.js +1 -0
- package/dist/view/assets/glsl-DplSGwfg.js +1 -0
- package/dist/view/assets/gn-n2N0HUVH.js +1 -0
- package/dist/view/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/view/assets/go-C27-OAKa.js +1 -0
- package/dist/view/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/view/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/view/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/view/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/view/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/view/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/view/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/view/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/view/assets/hack-BWmVpMyf.js +1 -0
- package/dist/view/assets/haml-D5jkg6IW.js +1 -0
- package/dist/view/assets/handlebars-BpdQsYii.js +1 -0
- package/dist/view/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/view/assets/haxe-CfZj7gIn.js +1 -0
- package/dist/view/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/view/assets/hjson-D5-asLiD.js +1 -0
- package/dist/view/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/view/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/view/assets/horizon-bright-CUuTKBJd.js +1 -0
- package/dist/view/assets/houston-DnULxvSX.js +1 -0
- package/dist/view/assets/html-derivative-DlHx6ybY.js +1 -0
- package/dist/view/assets/html-pp8916En.js +1 -0
- package/dist/view/assets/http-jrhK8wxY.js +1 -0
- package/dist/view/assets/hurl-irOxFIW8.js +1 -0
- package/dist/view/assets/hxml-2-FPmUDs.js +1 -0
- package/dist/view/assets/hy-DFXneXwc.js +1 -0
- package/dist/view/assets/imba-DGztddWO.js +1 -0
- package/dist/view/assets/index-BLryk3W4.css +1 -0
- package/dist/view/assets/index-ByMJp0a5.js +3 -0
- package/dist/view/assets/index-CRh4USfC.js +189 -0
- package/dist/view/assets/index-JNx7Ohoj.js +153 -0
- package/dist/view/assets/ini-BEwlwnbL.js +1 -0
- package/dist/view/assets/java-CylS5w8V.js +1 -0
- package/dist/view/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/view/assets/jinja-f2NsQr07.js +1 -0
- package/dist/view/assets/jison-wvAkD_A8.js +1 -0
- package/dist/view/assets/json-Cp-IABpG.js +1 -0
- package/dist/view/assets/json5-C9tS-k6U.js +1 -0
- package/dist/view/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/view/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/view/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/view/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/view/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/view/assets/julia-5Bft2YPA.js +1 -0
- package/dist/view/assets/just-Cwhn7H3k.js +1 -0
- package/dist/view/assets/kanagawa-dragon-BuwD2xS4.js +1 -0
- package/dist/view/assets/kanagawa-lotus-C3DzagqV.js +1 -0
- package/dist/view/assets/kanagawa-wave-DFGoQZhC.js +1 -0
- package/dist/view/assets/katex-BYZTj3zW.css +1 -0
- package/dist/view/assets/katex-C5jXJg4s.js +257 -0
- package/dist/view/assets/kdl-DV7GczEv.js +1 -0
- package/dist/view/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/view/assets/kusto-wEQ09or8.js +1 -0
- package/dist/view/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/view/assets/latex-D5pSuvFb.js +1 -0
- package/dist/view/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/view/assets/less-B1dDrJ26.js +1 -0
- package/dist/view/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/view/assets/liquid-C0sCDyMI.js +1 -0
- package/dist/view/assets/llvm-BZoOZj88.js +1 -0
- package/dist/view/assets/log-2UxHyX5q.js +1 -0
- package/dist/view/assets/logo-BtOb2qkB.js +1 -0
- package/dist/view/assets/lua-BaeVxFsk.js +1 -0
- package/dist/view/assets/luau-BnpPk5vE.js +1 -0
- package/dist/view/assets/make-CHLpvVh8.js +1 -0
- package/dist/view/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/view/assets/marko-DjSrsDqO.js +1 -0
- package/dist/view/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/view/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/view/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/view/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/view/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/view/assets/matlab-D7o27uSR.js +1 -0
- package/dist/view/assets/mdc-D1_yUvq7.js +1 -0
- package/dist/view/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/view/assets/mermaid-CQcHuHx7.js +1 -0
- package/dist/view/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/view/assets/min-light-CTRr51gU.js +1 -0
- package/dist/view/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/view/assets/mojo-DJz3ZmWd.js +1 -0
- package/dist/view/assets/monokai-BRVnQi9A.js +1 -0
- package/dist/view/assets/moonbit-CHtswR0a.js +1 -0
- package/dist/view/assets/move-el3G9tDJ.js +1 -0
- package/dist/view/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/view/assets/nextflow-C-mBbutL.js +1 -0
- package/dist/view/assets/nextflow-groovy-vE_lwT2v.js +1 -0
- package/dist/view/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/view/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/view/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/view/assets/nim-BIad80T-.js +1 -0
- package/dist/view/assets/nix-CwoSXNpI.js +1 -0
- package/dist/view/assets/nord-Ddv68eIx.js +1 -0
- package/dist/view/assets/nsis-BlV79W_Q.js +1 -0
- package/dist/view/assets/nushell-D3jzshHO.js +1 -0
- package/dist/view/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/view/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/view/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/view/assets/odin-BBf5iR-q.js +1 -0
- package/dist/view/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/view/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/view/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/view/assets/org-DM6o9KBp.js +1 -0
- package/dist/view/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/view/assets/perl-B9cMNwum.js +1 -0
- package/dist/view/assets/php-Csjmro_R.js +1 -0
- package/dist/view/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/view/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/view/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/view/assets/po-BTJTHyun.js +1 -0
- package/dist/view/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/view/assets/polar-C0HS_06l.js +1 -0
- package/dist/view/assets/postcss-CXtECtnM.js +1 -0
- package/dist/view/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/view/assets/powershell-BmBUJMz7.js +1 -0
- package/dist/view/assets/prisma-Vru482bI.js +1 -0
- package/dist/view/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/view/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/view/assets/pug-DKIMFp6K.js +1 -0
- package/dist/view/assets/puppet-BMWR74SV.js +1 -0
- package/dist/view/assets/purescript-CklMAg4u.js +1 -0
- package/dist/view/assets/python-B6aJPvgy.js +1 -0
- package/dist/view/assets/qml-3beO22l8.js +1 -0
- package/dist/view/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/view/assets/qss-IeuSbFQv.js +1 -0
- package/dist/view/assets/r-Cf5RLm7j.js +1 -0
- package/dist/view/assets/racket-BqYA7rlc.js +1 -0
- package/dist/view/assets/raku-DXvB9xmW.js +1 -0
- package/dist/view/assets/razor-BjBPvh-w.js +1 -0
- package/dist/view/assets/rbs-CpoqiR4B.js +1 -0
- package/dist/view/assets/red-hvxz__6c.js +1 -0
- package/dist/view/assets/reg-C-SQnVFl.js +1 -0
- package/dist/view/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/view/assets/rel-C3B-1QV4.js +1 -0
- package/dist/view/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/view/assets/ron-D8l8udqQ.js +1 -0
- package/dist/view/assets/rose-pine-5qJOZa0Y.js +1 -0
- package/dist/view/assets/rose-pine-dawn-zx0QlTCp.js +1 -0
- package/dist/view/assets/rose-pine-moon-C9pEdX9L.js +1 -0
- package/dist/view/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/view/assets/rst-bs7f0vWN.js +1 -0
- package/dist/view/assets/ruby-C0TQ7zu5.js +1 -0
- package/dist/view/assets/rust-B1yitclQ.js +1 -0
- package/dist/view/assets/sas-DEy46yEz.js +1 -0
- package/dist/view/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/view/assets/scala-CqE71os6.js +1 -0
- package/dist/view/assets/scheme-C98Dy4si.js +1 -0
- package/dist/view/assets/scss-D5BDwBP9.js +1 -0
- package/dist/view/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/view/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/view/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/view/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/view/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/view/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/view/assets/smalltalk-BOQMe2GC.js +1 -0
- package/dist/view/assets/smithy-cds9vsN8.js +1 -0
- package/dist/view/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/view/assets/solarized-dark-CpvCGNkr.js +1 -0
- package/dist/view/assets/solarized-light-Dlz6yCKv.js +1 -0
- package/dist/view/assets/solidity-DijEV5ha.js +1 -0
- package/dist/view/assets/soy-8wufbnw4.js +1 -0
- package/dist/view/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/view/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/view/assets/sql-CRqJ_cUM.js +1 -0
- package/dist/view/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/view/assets/stata-DI20mbqo.js +1 -0
- package/dist/view/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/view/assets/surrealql-Cjom0U5J.js +1 -0
- package/dist/view/assets/svelte-Cy7k_4gC.js +1 -0
- package/dist/view/assets/swift-C2oV4EkX.js +1 -0
- package/dist/view/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/view/assets/system-verilog-0hqHdDBg.js +1 -0
- package/dist/view/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/view/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/view/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/view/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/view/assets/templ-DhtptRzy.js +1 -0
- package/dist/view/assets/terraform-BETggiCN.js +1 -0
- package/dist/view/assets/tex-D96PA37w.js +1 -0
- package/dist/view/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/view/assets/toml-vGWfd6FD.js +1 -0
- package/dist/view/assets/ts-tags-D351s5mN.js +1 -0
- package/dist/view/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/view/assets/tsx-COt5Ahok.js +1 -0
- package/dist/view/assets/turtle-BsS91CYL.js +1 -0
- package/dist/view/assets/twig-27uCiNez.js +1 -0
- package/dist/view/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/view/assets/typespec-CAFt9gP4.js +1 -0
- package/dist/view/assets/typst-BUadGCkm.js +1 -0
- package/dist/view/assets/v-BGw2Nkan.js +1 -0
- package/dist/view/assets/vala-CsfeWuGM.js +1 -0
- package/dist/view/assets/vb-Cu-pLBUe.js +1 -0
- package/dist/view/assets/verilog-nZwndyjY.js +1 -0
- package/dist/view/assets/vesper-DRje8inN.js +1 -0
- package/dist/view/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/view/assets/viml-CJc9bBzg.js +1 -0
- package/dist/view/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/view/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/view/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/view/assets/vue-BqiEGhQt.js +1 -0
- package/dist/view/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/view/assets/vue-vine-BoDAl6tE.js +1 -0
- package/dist/view/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/view/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/view/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/view/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/view/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/view/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/view/assets/wit-5i3qLPDT.js +1 -0
- package/dist/view/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/view/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/view/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/view/assets/yaml-Buea-lGh.js +1 -0
- package/dist/view/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/view/assets/zig-VOosw3JB.js +1 -0
- package/dist/view/index.html +10 -2
- package/docs/README.md +21 -15
- package/docs/README.zh.md +15 -9
- package/docs/en/authoring.md +85 -80
- package/docs/en/cli-reference.md +110 -72
- package/docs/en/deployment.md +183 -0
- package/docs/en/getting-started.md +112 -0
- package/docs/en/material-types.md +241 -0
- package/docs/en/settings.md +133 -0
- package/docs/en/shortcuts.md +89 -0
- package/docs/en/typing-rules.md +208 -0
- package/docs/zh/authoring.md +74 -69
- package/docs/zh/cli-reference.md +92 -58
- package/docs/zh/deployment.md +179 -0
- package/docs/zh/getting-started.md +104 -0
- package/docs/zh/material-types.md +769 -0
- package/docs/zh/settings.md +120 -0
- package/docs/zh/shortcuts.md +83 -0
- package/docs/zh/typing-rules.md +191 -0
- package/package.json +13 -11
- package/templates/en/QTYPE.md +155 -495
- package/templates/en/README.md +11 -11
- package/templates/en/TODO.md +1 -1
- package/templates/en/material/article.md +20 -0
- package/templates/en/material/assets/article/sunrise.svg +12 -0
- package/templates/en/material/assets/code/tree.svg +19 -0
- package/templates/en/material/assets/qa/login-flow.svg +17 -0
- package/templates/en/material/assets/sentence/pangram.svg +6 -0
- package/templates/en/material/assets/snippet/moon.svg +8 -0
- package/templates/en/material/assets/word/apple.svg +5 -0
- package/templates/en/material/code.md +55 -0
- package/templates/en/material/qa.md +33 -0
- package/templates/en/material/sentence.md +18 -0
- package/templates/en/material/snippet.md +17 -0
- package/templates/en/material/word.md +20 -0
- package/templates/zh/QTYPE.md +191 -473
- package/templates/zh/README.md +6 -6
- package/templates/zh/index.md +1 -1
- package/templates/zh/material/article.md +18 -0
- package/templates/zh/material/assets/article/sunrise.svg +12 -0
- package/templates/zh/material/assets/code/tree.svg +19 -0
- package/templates/zh/material/assets/qa/login-flow.svg +17 -0
- package/templates/zh/material/assets/sentence/pangram.svg +6 -0
- package/templates/zh/material/assets/snippet/moon.svg +8 -0
- package/templates/zh/material/assets/word/apple.svg +5 -0
- package/templates/zh/material/code.md +55 -0
- package/templates/zh/material/qa.md +33 -0
- package/templates/zh/material/sentence.md +19 -0
- package/templates/zh/material/snippet.md +20 -0
- package/templates/zh/material/word.md +20 -0
- package/dist/cli/commands/check/action.js +0 -56
- package/dist/cli/commands/check/i18n.js +0 -26
- package/dist/cli/commands/plugin/i18n.js +0 -28
- package/dist/cli/commands/plugin/index.js +0 -41
- package/dist/core/typing/engine.js +0 -112
- package/dist/view/assets/index-DBABH3dG.js +0 -111
- package/dist/view/assets/index-RTLZ-zOC.css +0 -1
- package/templates/en/material/example.md +0 -47
- package/templates/zh/material/example.md +0 -47
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for walking a qtype repo's material/ directory.
|
|
3
|
+
*
|
|
4
|
+
* Every CLI command that reads material files (publish build, content
|
|
5
|
+
* lint / validate / search, check) goes through here so:
|
|
6
|
+
* - The "recursively descend into subfolders" behavior is a single
|
|
7
|
+
* implementation (used to be duplicated with different depths).
|
|
8
|
+
* - Only `.md` files are yielded (any accidental `.DS_Store`, images,
|
|
9
|
+
* etc. are skipped).
|
|
10
|
+
* - Paths are always returned with forward slashes so downstream
|
|
11
|
+
* JSON manifests and URL building agree even on Windows.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
/** Directory name under a qtype repo root that holds the deck files. */
|
|
16
|
+
export const MATERIAL_DIR = 'material';
|
|
17
|
+
/**
|
|
18
|
+
* Yield every `.md` file under `<repoRoot>/material/`, recursively.
|
|
19
|
+
* Returns an empty array when the directory doesn't exist — callers
|
|
20
|
+
* that treat a missing dir as fatal should check that separately.
|
|
21
|
+
*/
|
|
22
|
+
export function walkMaterial(repoRoot) {
|
|
23
|
+
const root = path.join(repoRoot, MATERIAL_DIR);
|
|
24
|
+
if (!fs.existsSync(root))
|
|
25
|
+
return [];
|
|
26
|
+
const out = [];
|
|
27
|
+
const walk = (subdir) => {
|
|
28
|
+
const dir = path.join(root, subdir);
|
|
29
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
30
|
+
const rel = subdir ? `${subdir}/${entry.name}` : entry.name;
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
walk(rel);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!entry.name.endsWith('.md'))
|
|
36
|
+
continue;
|
|
37
|
+
out.push({ rel, abs: path.join(root, rel) });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
walk('');
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read a material file. Small wrapper for consistency across commands
|
|
45
|
+
* that all want utf-8 text.
|
|
46
|
+
*/
|
|
47
|
+
export function readMaterial(file) {
|
|
48
|
+
return fs.readFileSync(file.abs, 'utf8');
|
|
49
|
+
}
|
package/dist/core/encrypt.js
CHANGED
|
@@ -24,6 +24,23 @@
|
|
|
24
24
|
* path stays clean (plaintext → argon2id → raw bytes → AES key).
|
|
25
25
|
*/
|
|
26
26
|
import { argon2id } from '@noble/hashes/argon2.js';
|
|
27
|
+
// argon2id parameters — tuned 2026-08 against @noble/hashes on a mid-range
|
|
28
|
+
// laptop. Target: ≥500ms per derivation so the offline brute-force cost
|
|
29
|
+
// stays material without making the interactive login feel sluggish.
|
|
30
|
+
//
|
|
31
|
+
// Benchmark (M-series, `argon2id(pw, 16-byte salt, { dkLen: 32 })`, median
|
|
32
|
+
// of 3 warm runs):
|
|
33
|
+
// 32 MiB / t=2 → ~223ms ✗ below target
|
|
34
|
+
// 32 MiB / t=3 → ~327ms ✗ below target
|
|
35
|
+
// 64 MiB / t=2 → ~445ms ✗ just under target
|
|
36
|
+
// 64 MiB / t=3 → ~669ms ✓ current — comfortable margin above 500ms
|
|
37
|
+
// 96 MiB / t=3 → ~949ms ✗ ~1s login lag on modest hardware
|
|
38
|
+
// 128 MiB / t=3 → ~1272ms ✗ same
|
|
39
|
+
//
|
|
40
|
+
// Threat model recap: static-site content decrypts client-side, so an
|
|
41
|
+
// attacker with the bundle can brute force offline unbounded. Higher
|
|
42
|
+
// costs only linearly slow that attack. Sub-second is the plateau
|
|
43
|
+
// beyond which UX pain outweighs security gain for this class of app.
|
|
27
44
|
export const ARGON2_MEMORY = 64 * 1024;
|
|
28
45
|
export const ARGON2_TIME = 3;
|
|
29
46
|
export const ARGON2_PARALLEL = 1;
|
|
@@ -78,7 +95,13 @@ export async function deriveAesKey(password, saltB64, params = DEFAULT_ARGON2_PA
|
|
|
78
95
|
p: params.p,
|
|
79
96
|
dkLen: AES_KEY_LEN,
|
|
80
97
|
});
|
|
81
|
-
|
|
98
|
+
// `extractable: true` so the view layer can `exportKey('raw', …)` and
|
|
99
|
+
// stash the bytes in sessionStorage — that's what lets a page reload
|
|
100
|
+
// skip the argon2 re-derivation. On a static-site threat model this
|
|
101
|
+
// grants nothing new: whoever controls the tab already sees every
|
|
102
|
+
// AES.decrypt() plaintext, so making the CryptoKey itself extractable
|
|
103
|
+
// adds no attacker capability.
|
|
104
|
+
return globalThis.crypto.subtle.importKey('raw', toBufferSource(raw), { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']);
|
|
82
105
|
}
|
|
83
106
|
// Bridge helper: WebCrypto's DOM BufferSource type demands
|
|
84
107
|
// Uint8Array<ArrayBuffer>, while noble/hashes returns Uint8Array<ArrayBufferLike>
|
package/dist/core/index.js
CHANGED
|
@@ -1,46 +1,43 @@
|
|
|
1
1
|
const FM_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Material-level frontmatter (top of the file, before the first `#`).
|
|
4
|
+
*
|
|
5
|
+
* ---
|
|
6
|
+
* title: <标题>
|
|
7
|
+
* type: word | sentence | article | qa | code
|
|
8
|
+
* tags: [<t1>, <t2>]
|
|
9
|
+
* created: YYYY-MM-DD
|
|
10
|
+
* updated: YYYY-MM-DD
|
|
11
|
+
* ---
|
|
12
|
+
*
|
|
13
|
+
* Card-level frontmatter no longer exists — everything a card carries
|
|
14
|
+
* is expressed by its markdown structure (see parseMaterial for the
|
|
15
|
+
* per-type rules).
|
|
16
|
+
*/
|
|
17
|
+
export function parseMaterialFrontmatter(source) {
|
|
3
18
|
const meta = { tags: [] };
|
|
4
|
-
const match =
|
|
19
|
+
const match = source.match(FM_RE);
|
|
5
20
|
if (!match)
|
|
6
|
-
return { meta, rest:
|
|
21
|
+
return { meta, rest: source };
|
|
7
22
|
const body = match[1];
|
|
8
|
-
const
|
|
23
|
+
const fields = {};
|
|
9
24
|
for (const line of body.split('\n')) {
|
|
10
25
|
const kv = line.match(/^(\w+)\s*:\s*(.+)$/);
|
|
11
26
|
if (!kv)
|
|
12
27
|
continue;
|
|
13
|
-
|
|
14
|
-
const value = rawValue.trim();
|
|
15
|
-
if (key === 'tags') {
|
|
16
|
-
meta.tags = parseTagList(value);
|
|
17
|
-
}
|
|
18
|
-
else if (key === 'difficulty') {
|
|
19
|
-
const n = Number(value);
|
|
20
|
-
if (!Number.isNaN(n))
|
|
21
|
-
meta.difficulty = n;
|
|
22
|
-
}
|
|
23
|
-
else if (key === 'hint') {
|
|
24
|
-
const v = value;
|
|
25
|
-
if (v === 'none' || v === 'ghost')
|
|
26
|
-
meta.hint = v;
|
|
27
|
-
}
|
|
28
|
-
else if (key === 'type') {
|
|
29
|
-
if (value === 'word' || value === 'sentence' || value === 'normal') {
|
|
30
|
-
meta.type = value;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
else if (key === 'phonetic') {
|
|
34
|
-
meta.phonetic = value;
|
|
35
|
-
}
|
|
36
|
-
else if (key === 'gloss') {
|
|
37
|
-
meta.gloss = value;
|
|
38
|
-
}
|
|
39
|
-
else if (key === 'translation') {
|
|
40
|
-
meta.translation = value;
|
|
41
|
-
}
|
|
28
|
+
fields[kv[1]] = kv[2].trim();
|
|
42
29
|
}
|
|
43
|
-
|
|
30
|
+
if (fields.title)
|
|
31
|
+
meta.title = fields.title;
|
|
32
|
+
if (fields.type)
|
|
33
|
+
meta.type = fields.type;
|
|
34
|
+
if (fields.tags)
|
|
35
|
+
meta.tags = parseTagList(fields.tags);
|
|
36
|
+
if (fields.created)
|
|
37
|
+
meta.created = fields.created;
|
|
38
|
+
if (fields.updated)
|
|
39
|
+
meta.updated = fields.updated;
|
|
40
|
+
return { meta, rest: source.slice(match[0].length) };
|
|
44
41
|
}
|
|
45
42
|
function parseTagList(value) {
|
|
46
43
|
const trimmed = value.trim();
|
|
@@ -56,3 +53,11 @@ function parseTagList(value) {
|
|
|
56
53
|
.map((s) => s.trim())
|
|
57
54
|
.filter(Boolean);
|
|
58
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Kept for backwards compatibility of the barrel export. Card-level
|
|
58
|
+
* frontmatter is no longer used; this always returns empty meta and
|
|
59
|
+
* the original block unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export function parseFrontmatter(block) {
|
|
62
|
+
return { meta: { tags: [] }, rest: block };
|
|
63
|
+
}
|
|
@@ -1,42 +1,192 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseMaterialFrontmatter } from './frontmatter.js';
|
|
2
2
|
import { textToCharPlans } from '../keystroke/index.js';
|
|
3
3
|
import { validateTypingText } from '../keystroke/textNormalize.js';
|
|
4
|
-
|
|
4
|
+
import { containsCJK, romanize } from '../romanization/index.js';
|
|
5
5
|
export function parseMaterial(source, filePath = 'inline') {
|
|
6
6
|
const normalized = source.replace(/\r\n/g, '\n').trim() + '\n';
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.filter(Boolean);
|
|
7
|
+
const { meta: materialMeta, rest } = parseMaterialFrontmatter(normalized);
|
|
8
|
+
const materialType = materialMeta.type ?? 'article';
|
|
9
|
+
const rawCards = splitByH1(rest);
|
|
11
10
|
const cards = [];
|
|
12
|
-
for (let i = 0; i <
|
|
13
|
-
const card = parseCard(
|
|
11
|
+
for (let i = 0; i < rawCards.length; i++) {
|
|
12
|
+
const card = parseCard(rawCards[i], materialType, filePath, i);
|
|
14
13
|
if (card)
|
|
15
|
-
cards.push(card);
|
|
14
|
+
cards.push(attachRomanization(card));
|
|
16
15
|
}
|
|
17
|
-
return { filePath, cards };
|
|
16
|
+
return { filePath, meta: materialMeta, cards };
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Split the material body (after frontmatter) into per-card chunks,
|
|
20
|
+
* one per H1. Each chunk starts with its `# heading` line and includes
|
|
21
|
+
* everything up to the next H1 (or end-of-file).
|
|
22
|
+
*/
|
|
23
|
+
function splitByH1(body) {
|
|
24
|
+
const lines = body.split('\n');
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let current = null;
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (/^#\s+/.test(line)) {
|
|
29
|
+
if (current)
|
|
30
|
+
chunks.push(current.join('\n').trim());
|
|
31
|
+
current = [line];
|
|
32
|
+
}
|
|
33
|
+
else if (current) {
|
|
34
|
+
current.push(line);
|
|
35
|
+
}
|
|
36
|
+
// Lines before the first H1 are ignored (material-level prose).
|
|
37
|
+
}
|
|
38
|
+
if (current)
|
|
39
|
+
chunks.push(current.join('\n').trim());
|
|
40
|
+
return chunks.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* If the card's `answer` contains any Han character, attach a
|
|
44
|
+
* romanization (pinyin) so the typing engine has a keyboard-typable
|
|
45
|
+
* target. Non-CJK cards get nothing (fields stay undefined).
|
|
46
|
+
*/
|
|
47
|
+
function attachRomanization(card) {
|
|
48
|
+
if (!containsCJK(card.answer))
|
|
49
|
+
return card;
|
|
50
|
+
const rom = romanize(card.answer);
|
|
51
|
+
if (!rom.text)
|
|
52
|
+
return card;
|
|
53
|
+
return {
|
|
54
|
+
...card,
|
|
55
|
+
romanization: rom.text,
|
|
56
|
+
glyphMap: rom.glyphMap,
|
|
57
|
+
romanizationAccepted: rom.accepted,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function parseCard(chunk, materialType, filePath, index) {
|
|
61
|
+
const lines = chunk.split('\n');
|
|
62
|
+
const h1Line = lines[0];
|
|
63
|
+
const titleMatch = h1Line.match(/^#\s+(.*)$/);
|
|
64
|
+
if (!titleMatch)
|
|
65
|
+
return null;
|
|
66
|
+
const title = titleMatch[1].trim();
|
|
67
|
+
if (!title)
|
|
24
68
|
return null;
|
|
25
|
-
const
|
|
69
|
+
const body = lines.slice(1).join('\n').trim();
|
|
70
|
+
switch (materialType) {
|
|
71
|
+
case 'word':
|
|
72
|
+
case 'sentence':
|
|
73
|
+
return buildAnnotatedCard(title, body, filePath, index);
|
|
74
|
+
case 'qa':
|
|
75
|
+
case 'code':
|
|
76
|
+
return buildQACard(title, body, filePath, index);
|
|
77
|
+
case 'article':
|
|
78
|
+
case 'snippet':
|
|
79
|
+
default:
|
|
80
|
+
return buildArticleCard(title, body, filePath, index);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* word / sentence: H1 is the typing target. H2/H3/H4 are optional
|
|
85
|
+
* single-line annotation slots.
|
|
86
|
+
*/
|
|
87
|
+
function buildAnnotatedCard(title, body, filePath, index) {
|
|
88
|
+
const validated = validateTypingText(title);
|
|
89
|
+
const answer = validated.text;
|
|
90
|
+
if (!answer)
|
|
91
|
+
return null;
|
|
92
|
+
const h2 = extractHeadingContent(body, 2);
|
|
93
|
+
const h3 = extractHeadingContent(body, 3);
|
|
94
|
+
const h4 = extractHeadingContent(body, 4);
|
|
95
|
+
const charPlans = textToCharPlans(answer);
|
|
96
|
+
return {
|
|
97
|
+
id: `${filePath}#${index}`,
|
|
98
|
+
title: answer,
|
|
99
|
+
answer,
|
|
100
|
+
phonetic: h2 || undefined,
|
|
101
|
+
gloss: h3 || undefined,
|
|
102
|
+
image: h4 || undefined,
|
|
103
|
+
charPlans,
|
|
104
|
+
unknownChars: validated.unknowns,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* article: H1 is the section heading; the entire body under H1 is
|
|
109
|
+
* the typing target. Images and other non-text markdown are stripped
|
|
110
|
+
* from the typing text but preserved in `question` so the renderer
|
|
111
|
+
* can show them alongside the arena.
|
|
112
|
+
*/
|
|
113
|
+
function buildArticleCard(title, body, filePath, index) {
|
|
114
|
+
const rawTarget = stripImages(body).trim();
|
|
115
|
+
if (!rawTarget)
|
|
116
|
+
return null;
|
|
117
|
+
const validated = validateTypingText(rawTarget);
|
|
118
|
+
const answer = validated.text;
|
|
119
|
+
if (!answer)
|
|
120
|
+
return null;
|
|
121
|
+
const charPlans = textToCharPlans(answer);
|
|
122
|
+
return {
|
|
123
|
+
id: `${filePath}#${index}`,
|
|
124
|
+
title,
|
|
125
|
+
answer,
|
|
126
|
+
// `question` carries the raw body so images render in the card
|
|
127
|
+
// shell alongside the arena. The arena itself gets `answer`, which
|
|
128
|
+
// is the images-stripped normalized text.
|
|
129
|
+
question: body,
|
|
130
|
+
charPlans,
|
|
131
|
+
unknownChars: validated.unknowns,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* qa / code: H1 is the card heading; `## Q` marks the prompt; `## A`
|
|
136
|
+
* marks the answer (typing target). Both sub-sections are captured as
|
|
137
|
+
* raw markdown; the answer is stripped of image markdown for typing.
|
|
138
|
+
*/
|
|
139
|
+
function buildQACard(title, body, filePath, index) {
|
|
140
|
+
const qaMatch = body.match(/##\s*Q\b\s*\n([\s\S]*?)(?=\n##\s*A\b|$)/);
|
|
141
|
+
const aMatch = body.match(/##\s*A\b\s*\n([\s\S]*)$/);
|
|
142
|
+
if (!aMatch)
|
|
143
|
+
return null;
|
|
144
|
+
const question = qaMatch ? qaMatch[1].trim() : '';
|
|
26
145
|
const rawAnswer = aMatch[1].trim();
|
|
27
|
-
|
|
146
|
+
const targetText = extractTypingText(rawAnswer);
|
|
147
|
+
if (!targetText)
|
|
28
148
|
return null;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const charPlans = textToCharPlans(
|
|
149
|
+
const validated = validateTypingText(targetText);
|
|
150
|
+
const answer = validated.text;
|
|
151
|
+
if (!answer)
|
|
152
|
+
return null;
|
|
153
|
+
const charPlans = textToCharPlans(answer);
|
|
34
154
|
return {
|
|
35
155
|
id: `${filePath}#${index}`,
|
|
156
|
+
title,
|
|
157
|
+
answer,
|
|
36
158
|
question,
|
|
37
|
-
answer: validated.text,
|
|
38
|
-
meta,
|
|
39
159
|
charPlans,
|
|
40
160
|
unknownChars: validated.unknowns,
|
|
41
161
|
};
|
|
42
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the inline text of the first H2/H3/H4 in a block, `null` if
|
|
165
|
+
* absent. Only same-line content is returned — H4 for word cards is
|
|
166
|
+
* expected to be a single `` on the heading line.
|
|
167
|
+
*/
|
|
168
|
+
function extractHeadingContent(body, level) {
|
|
169
|
+
const hashes = '#'.repeat(level);
|
|
170
|
+
const re = new RegExp(`^${hashes}\\s+(.*)$`, 'm');
|
|
171
|
+
const m = body.match(re);
|
|
172
|
+
return m ? m[1].trim() : null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Strip markdown images so the typing target contains only characters
|
|
176
|
+
* the user can actually type. Fenced code fences themselves are
|
|
177
|
+
* removed (backticks aren't typed), but their contents are kept.
|
|
178
|
+
*/
|
|
179
|
+
function extractTypingText(md) {
|
|
180
|
+
return stripFencedCodeMarkers(stripImages(md)).trim();
|
|
181
|
+
}
|
|
182
|
+
function stripImages(md) {
|
|
183
|
+
return md.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Remove the ``` fences around fenced code blocks so the code body
|
|
187
|
+
* itself becomes the typing target. The optional language tag on the
|
|
188
|
+
* opening fence is also dropped.
|
|
189
|
+
*/
|
|
190
|
+
function stripFencedCodeMarkers(md) {
|
|
191
|
+
return md.replace(/```[^\n]*\n?/g, '').replace(/```\s*$/g, '');
|
|
192
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* romanization — turn CJK text into a keyboard-typable roman-alphabet
|
|
3
|
+
* form so users can practice against the actual key sequence (pinyin),
|
|
4
|
+
* not the composed glyph they can only produce via an IME.
|
|
5
|
+
*
|
|
6
|
+
* Currently supports Chinese via `pinyin-pro`. The field name and API
|
|
7
|
+
* are neutral so we can plug in romaji / hangul romanization later
|
|
8
|
+
* behind the same abstraction.
|
|
9
|
+
*
|
|
10
|
+
* Output:
|
|
11
|
+
* text — the roman string (typing target)
|
|
12
|
+
* glyphMap[i] — index into the original text that romanChar `i`
|
|
13
|
+
* belongs to. Non-CJK chars are 1:1 self-mapped;
|
|
14
|
+
* each CJK char's pinyin syllable + a trailing space
|
|
15
|
+
* all map back to that single CJK char.
|
|
16
|
+
*
|
|
17
|
+
* Punctuation:
|
|
18
|
+
* Full-width CJK punctuation (`,` `。` `;` etc.) is folded to its
|
|
19
|
+
* ASCII equivalent (`,` `.` `;`) so the user can actually type it
|
|
20
|
+
* without switching to a CJK IME.
|
|
21
|
+
*/
|
|
22
|
+
import { pinyin } from 'pinyin-pro';
|
|
23
|
+
/** True if the string contains any CJK Han character. */
|
|
24
|
+
const HAN_RE = /\p{Script=Han}/u;
|
|
25
|
+
export function containsCJK(s) {
|
|
26
|
+
return HAN_RE.test(s);
|
|
27
|
+
}
|
|
28
|
+
/** Full-width CJK punctuation → ASCII fold table. */
|
|
29
|
+
const PUNCT_FOLD = {
|
|
30
|
+
',': ',',
|
|
31
|
+
'。': '.',
|
|
32
|
+
';': ';',
|
|
33
|
+
':': ':',
|
|
34
|
+
'?': '?',
|
|
35
|
+
'!': '!',
|
|
36
|
+
'、': ',',
|
|
37
|
+
'(': '(',
|
|
38
|
+
')': ')',
|
|
39
|
+
'「': '"',
|
|
40
|
+
'」': '"',
|
|
41
|
+
'『': '"',
|
|
42
|
+
'』': '"',
|
|
43
|
+
'“': '"',
|
|
44
|
+
'”': '"',
|
|
45
|
+
'‘': "'",
|
|
46
|
+
'’': "'",
|
|
47
|
+
'《': '<',
|
|
48
|
+
'》': '>',
|
|
49
|
+
'·': '.',
|
|
50
|
+
'—': '-',
|
|
51
|
+
'…': '.',
|
|
52
|
+
'~': '~',
|
|
53
|
+
};
|
|
54
|
+
function foldPunct(ch) {
|
|
55
|
+
return PUNCT_FOLD[ch] ?? ch;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build the romanization + glyph map for a piece of mixed CJK/ASCII
|
|
59
|
+
* text. Multi-reading Hanzi get their alternate pinyins folded into
|
|
60
|
+
* `accepted[i]` so any valid reading types-through cleanly.
|
|
61
|
+
*/
|
|
62
|
+
export function romanize(text) {
|
|
63
|
+
const chars = Array.from(text);
|
|
64
|
+
const out = [];
|
|
65
|
+
const map = [];
|
|
66
|
+
const accepted = [];
|
|
67
|
+
for (let i = 0; i < chars.length; i++) {
|
|
68
|
+
const ch = chars[i];
|
|
69
|
+
if (HAN_RE.test(ch)) {
|
|
70
|
+
// Default reading — drives the visible text and the syllable
|
|
71
|
+
// length. Multi-reading Hanzi additionally contribute their
|
|
72
|
+
// letters into `accepted` at aligned positions.
|
|
73
|
+
const defaultSyl = pinyin(ch, {
|
|
74
|
+
toneType: 'none',
|
|
75
|
+
type: 'string',
|
|
76
|
+
v: true,
|
|
77
|
+
});
|
|
78
|
+
let readings;
|
|
79
|
+
if (!defaultSyl || defaultSyl === ch) {
|
|
80
|
+
// pinyin-pro has no entry for this glyph. Emit a single '?'
|
|
81
|
+
// placeholder so downstream indexing stays aligned; consumers
|
|
82
|
+
// can accept any letter here (rare chars are effectively
|
|
83
|
+
// free-pass).
|
|
84
|
+
readings = ['?'];
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const multi = pinyin(ch, {
|
|
88
|
+
toneType: 'none',
|
|
89
|
+
type: 'string',
|
|
90
|
+
v: true,
|
|
91
|
+
multiple: true,
|
|
92
|
+
});
|
|
93
|
+
// Some pinyin-pro versions return the multi-reading string
|
|
94
|
+
// as space-separated syllables. Split and dedupe. Always keep
|
|
95
|
+
// defaultSyl first so `text` uses it.
|
|
96
|
+
const parts = typeof multi === 'string'
|
|
97
|
+
? multi.split(' ').map((s) => s.trim()).filter(Boolean)
|
|
98
|
+
: [defaultSyl];
|
|
99
|
+
readings = Array.from(new Set([defaultSyl, ...parts]));
|
|
100
|
+
}
|
|
101
|
+
const primary = readings[0];
|
|
102
|
+
for (let k = 0; k < primary.length; k++) {
|
|
103
|
+
const c = primary[k];
|
|
104
|
+
out.push(c);
|
|
105
|
+
map.push(i);
|
|
106
|
+
// Union of the k-th letter of every reading whose length > k.
|
|
107
|
+
const set = new Set();
|
|
108
|
+
if (primary === '?') {
|
|
109
|
+
// Rare-char slot: accept ANY letter (represented by empty
|
|
110
|
+
// set treated as wildcard by consumers).
|
|
111
|
+
set.add('*');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
for (const r of readings) {
|
|
115
|
+
if (k < r.length)
|
|
116
|
+
set.add(r[k]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
accepted.push(set);
|
|
120
|
+
}
|
|
121
|
+
// Separator space between syllables.
|
|
122
|
+
out.push(' ');
|
|
123
|
+
map.push(i);
|
|
124
|
+
accepted.push(new Set([' ']));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const folded = foldPunct(ch);
|
|
128
|
+
out.push(folded);
|
|
129
|
+
map.push(i);
|
|
130
|
+
accepted.push(new Set([folded]));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Trim trailing separator.
|
|
134
|
+
while (out.length > 0 &&
|
|
135
|
+
out[out.length - 1] === ' ' &&
|
|
136
|
+
map[map.length - 1] === chars.length - 1) {
|
|
137
|
+
out.pop();
|
|
138
|
+
map.pop();
|
|
139
|
+
accepted.pop();
|
|
140
|
+
}
|
|
141
|
+
return { text: out.join(''), glyphMap: map, accepted };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if `ch` is acceptable at position `i` in a romanization.
|
|
145
|
+
* Handles the wildcard '*' used for rare-char placeholders.
|
|
146
|
+
*/
|
|
147
|
+
export function isAccepted(accepted, i, ch) {
|
|
148
|
+
const set = accepted[i];
|
|
149
|
+
if (!set)
|
|
150
|
+
return false;
|
|
151
|
+
return set.has('*') || set.has(ch);
|
|
152
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "both" schema: user can set a global default and any single repo
|
|
3
|
+
* can override it. Registered on BOTH kv stores. Read precedence at
|
|
4
|
+
* runtime: repo > user > schema default.
|
|
5
|
+
*/
|
|
6
|
+
import { enumValidator, boolValidator, positiveIntValidator, } from '../../store/index.js';
|
|
7
|
+
export const bothSchema = {
|
|
8
|
+
// Repo language. Drives:
|
|
9
|
+
// • which template locale (examples/zh vs examples/en) new repos are
|
|
10
|
+
// scaffolded from
|
|
11
|
+
// • the web UI language — the reader is locked to repo.lang so the UI
|
|
12
|
+
// matches the material's own language
|
|
13
|
+
//
|
|
14
|
+
// Values are BCP 47 codes (`zh-CN` / `en-US`); the short forms `zh` /
|
|
15
|
+
// `en` are accepted for backwards compatibility with older `.qtype/
|
|
16
|
+
// config.json` files and normalised at read time.
|
|
17
|
+
'repo.lang': {
|
|
18
|
+
validator: enumValidator(['zh-CN', 'en-US', 'zh', 'en']),
|
|
19
|
+
default: 'en-US',
|
|
20
|
+
},
|
|
21
|
+
'typing.theme': {
|
|
22
|
+
validator: enumValidator(['mocha', 'latte']),
|
|
23
|
+
default: 'mocha',
|
|
24
|
+
},
|
|
25
|
+
'typing.targetWpm': {
|
|
26
|
+
validator: positiveIntValidator(),
|
|
27
|
+
},
|
|
28
|
+
'typing.tabBehavior': {
|
|
29
|
+
validator: enumValidator(['reset', 'input', 'disabled']),
|
|
30
|
+
default: 'reset',
|
|
31
|
+
},
|
|
32
|
+
'typing.hint': {
|
|
33
|
+
validator: enumValidator(['none', 'ghost']),
|
|
34
|
+
default: 'none',
|
|
35
|
+
},
|
|
36
|
+
'typing.showKeyboard': {
|
|
37
|
+
validator: boolValidator(),
|
|
38
|
+
default: true,
|
|
39
|
+
},
|
|
40
|
+
'typing.showHands': {
|
|
41
|
+
validator: boolValidator(),
|
|
42
|
+
default: true,
|
|
43
|
+
},
|
|
44
|
+
// Mermaid pre-render mode. `prerender` (default) uses puppeteer at build
|
|
45
|
+
// time to bake SVGs into dist/material/mermaid.json; `client` ships raw
|
|
46
|
+
// ```mermaid fences and lets the browser render them; `off` treats mermaid
|
|
47
|
+
// as plain code blocks.
|
|
48
|
+
'publish.mermaid': {
|
|
49
|
+
validator: enumValidator(['prerender', 'client', 'off']),
|
|
50
|
+
default: 'prerender',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repo-only config schema: keys tied to a specific repo, never
|
|
3
|
+
* useful as a user-wide default. Registered on the repo KV only.
|
|
4
|
+
*/
|
|
5
|
+
import { enumValidator, nonEmptyStringValidator, boolValidator, } from '../../store/index.js';
|
|
6
|
+
export const repoOnlySchema = {
|
|
7
|
+
'repo.name': {
|
|
8
|
+
validator: nonEmptyStringValidator(),
|
|
9
|
+
},
|
|
10
|
+
// ── Password protection for `publish build` ─────────────
|
|
11
|
+
// On/off toggle. When true, `publish build` reads the other three fields
|
|
12
|
+
// and encrypts every material/*.md before writing to dist.
|
|
13
|
+
'publish.password.enabled': {
|
|
14
|
+
validator: boolValidator(),
|
|
15
|
+
default: false,
|
|
16
|
+
},
|
|
17
|
+
// Argon2id PHC-style hash of the plaintext password, used to verify the
|
|
18
|
+
// password entered at build time. NOT the AES key — that's derived
|
|
19
|
+
// separately via `publish.password.aesSalt`. Empty string when protection
|
|
20
|
+
// is off; shape enforced by `publish password set` writing a well-formed
|
|
21
|
+
// PHC string.
|
|
22
|
+
'publish.password.hash': {
|
|
23
|
+
validator: (v) => typeof v === 'string'
|
|
24
|
+
? null
|
|
25
|
+
: { code: 'err.schema.nonEmptyString', fallback: 'must be a string' },
|
|
26
|
+
default: '',
|
|
27
|
+
},
|
|
28
|
+
// Base64 salt used with argon2id (password → AES-256 key). Rotated only
|
|
29
|
+
// when the user calls `publish password set` — stable across rebuilds so
|
|
30
|
+
// the same password unlocks previously published sites.
|
|
31
|
+
'publish.password.aesSalt': {
|
|
32
|
+
validator: (v) => typeof v === 'string'
|
|
33
|
+
? null
|
|
34
|
+
: { code: 'err.schema.nonEmptyString', fallback: 'must be a string' },
|
|
35
|
+
default: '',
|
|
36
|
+
},
|
|
37
|
+
// What to encrypt. `all` covers material/ + any future encrypted assets;
|
|
38
|
+
// `material-only` limits encryption to material/*.md (same behavior in
|
|
39
|
+
// v0 since qtype has no other content streams).
|
|
40
|
+
'publish.password.scope': {
|
|
41
|
+
validator: enumValidator(['all', 'material-only', 'selected']),
|
|
42
|
+
default: 'all',
|
|
43
|
+
},
|
|
44
|
+
// When scope='selected', the list of file names (relative to material/)
|
|
45
|
+
// to encrypt. Ignored otherwise.
|
|
46
|
+
'publish.password.selected': {
|
|
47
|
+
validator: (v) => Array.isArray(v) && v.every((x) => typeof x === 'string')
|
|
48
|
+
? null
|
|
49
|
+
: {
|
|
50
|
+
code: 'err.schema.nonEmptyString',
|
|
51
|
+
fallback: 'must be an array of strings',
|
|
52
|
+
},
|
|
53
|
+
default: [],
|
|
54
|
+
},
|
|
55
|
+
};
|