@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.
Files changed (468) hide show
  1. package/CHANGELOG.md +1 -3
  2. package/CHANGELOG.zh.md +1 -2
  3. package/README.md +88 -56
  4. package/README.zh.md +82 -62
  5. package/dist/cli/bootstrap.js +4 -115
  6. package/dist/cli/commands/check/index.js +11 -10
  7. package/dist/cli/commands/config/action.js +5 -9
  8. package/dist/cli/commands/content/lint/action.js +38 -44
  9. package/dist/cli/commands/content/search/action.js +44 -34
  10. package/dist/cli/commands/content/search/i18n.js +6 -0
  11. package/dist/cli/commands/content/search/index.js +19 -1
  12. package/dist/cli/commands/content/validate/action.js +28 -27
  13. package/dist/cli/commands/index.js +1 -3
  14. package/dist/cli/commands/pack/index.js +2 -1
  15. package/dist/cli/commands/publish/build/action.js +39 -11
  16. package/dist/cli/commands/repo/new/action.js +23 -4
  17. package/dist/cli/commands/repo/new/i18n.js +2 -2
  18. package/dist/cli/commands/repo/sync/action.js +24 -4
  19. package/dist/cli/commands/repo/templates.js +2 -1
  20. package/dist/cli/utils/material.js +49 -0
  21. package/dist/core/encrypt.js +24 -1
  22. package/dist/core/index.js +1 -0
  23. package/dist/core/parser/frontmatter.js +39 -34
  24. package/dist/core/parser/parseMaterial.js +174 -24
  25. package/dist/core/romanization/index.js +152 -0
  26. package/dist/core/schemas/both.js +52 -0
  27. package/dist/core/schemas/index.js +3 -0
  28. package/dist/core/schemas/repo.js +55 -0
  29. package/dist/core/schemas/user.js +19 -0
  30. package/dist/core/typing/engine/TypingInput.js +243 -0
  31. package/dist/core/typing/engine/chars.js +30 -0
  32. package/dist/core/typing/engine/feedback.js +15 -0
  33. package/dist/core/typing/engine/index.js +5 -0
  34. package/dist/core/typing/engine/settings.js +8 -0
  35. package/dist/core/typing/engine/stats.js +45 -0
  36. package/dist/core/typing/index.js +9 -1
  37. package/dist/view/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  38. package/dist/view/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  39. package/dist/view/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  40. package/dist/view/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  41. package/dist/view/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  42. package/dist/view/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  43. package/dist/view/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  44. package/dist/view/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  45. package/dist/view/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  46. package/dist/view/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  47. package/dist/view/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  48. package/dist/view/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  49. package/dist/view/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  50. package/dist/view/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  51. package/dist/view/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  52. package/dist/view/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  53. package/dist/view/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  54. package/dist/view/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  55. package/dist/view/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  56. package/dist/view/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  57. package/dist/view/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  58. package/dist/view/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  59. package/dist/view/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  60. package/dist/view/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  61. package/dist/view/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  62. package/dist/view/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  63. package/dist/view/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  64. package/dist/view/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  65. package/dist/view/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  66. package/dist/view/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  67. package/dist/view/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  68. package/dist/view/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  69. package/dist/view/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  70. package/dist/view/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  71. package/dist/view/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  72. package/dist/view/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  73. package/dist/view/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  74. package/dist/view/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  75. package/dist/view/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  76. package/dist/view/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  77. package/dist/view/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  78. package/dist/view/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  79. package/dist/view/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  80. package/dist/view/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  81. package/dist/view/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  82. package/dist/view/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  83. package/dist/view/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  84. package/dist/view/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  85. package/dist/view/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  86. package/dist/view/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  87. package/dist/view/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  88. package/dist/view/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  89. package/dist/view/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  90. package/dist/view/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  91. package/dist/view/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  92. package/dist/view/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  93. package/dist/view/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  94. package/dist/view/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  95. package/dist/view/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  96. package/dist/view/assets/abap-BdImnpbu.js +1 -0
  97. package/dist/view/assets/actionscript-3-B3316cI-.js +1 -0
  98. package/dist/view/assets/ada-bCR0ucgS.js +1 -0
  99. package/dist/view/assets/ahk-CsyLZFj1.js +1 -0
  100. package/dist/view/assets/ahk2-8Zs4aa1G.js +1 -0
  101. package/dist/view/assets/andromeeda-C4gqWexZ.js +1 -0
  102. package/dist/view/assets/angular-html-DA-rfuFy.js +1 -0
  103. package/dist/view/assets/angular-ts-BrjP3tb8.js +1 -0
  104. package/dist/view/assets/apache-Pmp26Uib.js +1 -0
  105. package/dist/view/assets/apex-DhZFqWV2.js +1 -0
  106. package/dist/view/assets/apl-CORt7UWP.js +1 -0
  107. package/dist/view/assets/applescript-Co6uUVPk.js +1 -0
  108. package/dist/view/assets/ara-BRHolxvo.js +1 -0
  109. package/dist/view/assets/asciidoc-CSVQ5wI8.js +1 -0
  110. package/dist/view/assets/asm-D_Q5rh1f.js +1 -0
  111. package/dist/view/assets/astro-HNnZUWAn.js +1 -0
  112. package/dist/view/assets/aurora-x-D-2ljcwZ.js +1 -0
  113. package/dist/view/assets/awk-DMzUqQB5.js +1 -0
  114. package/dist/view/assets/ayu-dark-DYE7WIF3.js +1 -0
  115. package/dist/view/assets/ayu-light-BA47KaF1.js +1 -0
  116. package/dist/view/assets/ayu-mirage-32ctXXKs.js +1 -0
  117. package/dist/view/assets/ballerina-BFfxhgS-.js +1 -0
  118. package/dist/view/assets/bat-CickPsom.js +1 -0
  119. package/dist/view/assets/beancount-k_qm7-4y.js +1 -0
  120. package/dist/view/assets/berry-uYugtg8r.js +1 -0
  121. package/dist/view/assets/bibtex-CHM0blh-.js +1 -0
  122. package/dist/view/assets/bicep-Bmn6On1c.js +1 -0
  123. package/dist/view/assets/bird2-Bx8U0n9b.js +1 -0
  124. package/dist/view/assets/blade-2xfisSek.js +1 -0
  125. package/dist/view/assets/bsl-DlhNcFeZ.js +1 -0
  126. package/dist/view/assets/c-BIGW1oBm.js +1 -0
  127. package/dist/view/assets/c3-Dp5svz6Z.js +1 -0
  128. package/dist/view/assets/cadence-Bv_4Rxtq.js +1 -0
  129. package/dist/view/assets/cairo-KRGpt6FW.js +1 -0
  130. package/dist/view/assets/catppuccin-frappe-CZL1YF0i.js +1 -0
  131. package/dist/view/assets/catppuccin-latte-DH-8KZSZ.js +1 -0
  132. package/dist/view/assets/catppuccin-macchiato-B7yYVSCf.js +1 -0
  133. package/dist/view/assets/catppuccin-mocha-Ct7hS0mc.js +1 -0
  134. package/dist/view/assets/chapel-DTp_pixX.js +1 -0
  135. package/dist/view/assets/clarity-Dn5IMItf.js +1 -0
  136. package/dist/view/assets/clojure-P80f7IUj.js +1 -0
  137. package/dist/view/assets/cmake-D1j8_8rp.js +1 -0
  138. package/dist/view/assets/cobol-nBiQ_Alo.js +1 -0
  139. package/dist/view/assets/codeowners-Bp6g37R7.js +1 -0
  140. package/dist/view/assets/codeql-DsOJ9woJ.js +1 -0
  141. package/dist/view/assets/coffee-Ch7k5sss.js +1 -0
  142. package/dist/view/assets/common-lisp-Cg-RD9OK.js +1 -0
  143. package/dist/view/assets/coq-C7JzOVbR.js +1 -0
  144. package/dist/view/assets/cpp-BMRokrvK.js +1 -0
  145. package/dist/view/assets/crystal-DGywbUpC.js +1 -0
  146. package/dist/view/assets/csharp-DSvCPggb.js +1 -0
  147. package/dist/view/assets/css-CLj8gQPS.js +1 -0
  148. package/dist/view/assets/csv-fuZLfV_i.js +1 -0
  149. package/dist/view/assets/cue-D82EKSYY.js +1 -0
  150. package/dist/view/assets/cypher-COkxafJQ.js +1 -0
  151. package/dist/view/assets/d-85-TOEBH.js +1 -0
  152. package/dist/view/assets/dark-plus-C3mMm8J8.js +1 -0
  153. package/dist/view/assets/dart-bE4Kk8sk.js +1 -0
  154. package/dist/view/assets/dax-CEL-wOlO.js +1 -0
  155. package/dist/view/assets/desktop-BmXAJ9_W.js +1 -0
  156. package/dist/view/assets/diff-D97Zzqfu.js +1 -0
  157. package/dist/view/assets/docker-BcOcwvcX.js +1 -0
  158. package/dist/view/assets/dotenv-Da5cRb03.js +1 -0
  159. package/dist/view/assets/dracula-BzJJZx-M.js +1 -0
  160. package/dist/view/assets/dracula-soft-BXkSAIEj.js +1 -0
  161. package/dist/view/assets/dream-maker-BtqSS_iP.js +1 -0
  162. package/dist/view/assets/edge-FbVlp4U3.js +1 -0
  163. package/dist/view/assets/elixir-CkH2-t6x.js +1 -0
  164. package/dist/view/assets/elm-DbKCFpqz.js +1 -0
  165. package/dist/view/assets/emacs-lisp-C_m_b--Z.js +1 -0
  166. package/dist/view/assets/erb-DXfck5VN.js +1 -0
  167. package/dist/view/assets/erlang-DsQrWhSR.js +1 -0
  168. package/dist/view/assets/everforest-dark-BgDCqdQA.js +1 -0
  169. package/dist/view/assets/everforest-light-C8M2exoo.js +1 -0
  170. package/dist/view/assets/fennel-BYunw83y.js +1 -0
  171. package/dist/view/assets/fish-BvzEVeQv.js +1 -0
  172. package/dist/view/assets/fluent-C4IJs8-o.js +1 -0
  173. package/dist/view/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  174. package/dist/view/assets/fortran-free-form-BxgE0vQu.js +1 -0
  175. package/dist/view/assets/fsharp-CXgrBDvD.js +1 -0
  176. package/dist/view/assets/gdresource-TyuKm33G.js +1 -0
  177. package/dist/view/assets/gdscript-DqcFQ5yU.js +1 -0
  178. package/dist/view/assets/gdshader-DkwncUOv.js +1 -0
  179. package/dist/view/assets/genie-D0YGMca9.js +1 -0
  180. package/dist/view/assets/gherkin-DyxjwDmM.js +1 -0
  181. package/dist/view/assets/git-commit-F4YmCXRG.js +1 -0
  182. package/dist/view/assets/git-rebase-r7XF79zn.js +1 -0
  183. package/dist/view/assets/github-dark-DHJKELXO.js +1 -0
  184. package/dist/view/assets/github-dark-default-Cuk6v7N8.js +1 -0
  185. package/dist/view/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  186. package/dist/view/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  187. package/dist/view/assets/github-light-DAi9KRSo.js +1 -0
  188. package/dist/view/assets/github-light-default-D7oLnXFd.js +1 -0
  189. package/dist/view/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  190. package/dist/view/assets/gleam-BspZqrRM.js +1 -0
  191. package/dist/view/assets/glimmer-js-ByusRIyA.js +1 -0
  192. package/dist/view/assets/glimmer-ts-BfAWNZQY.js +1 -0
  193. package/dist/view/assets/glsl-DplSGwfg.js +1 -0
  194. package/dist/view/assets/gn-n2N0HUVH.js +1 -0
  195. package/dist/view/assets/gnuplot-DdkO51Og.js +1 -0
  196. package/dist/view/assets/go-C27-OAKa.js +1 -0
  197. package/dist/view/assets/graphql-ChdNCCLP.js +1 -0
  198. package/dist/view/assets/groovy-gcz8RCvz.js +1 -0
  199. package/dist/view/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  200. package/dist/view/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  201. package/dist/view/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  202. package/dist/view/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  203. package/dist/view/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  204. package/dist/view/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  205. package/dist/view/assets/hack-BWmVpMyf.js +1 -0
  206. package/dist/view/assets/haml-D5jkg6IW.js +1 -0
  207. package/dist/view/assets/handlebars-BpdQsYii.js +1 -0
  208. package/dist/view/assets/haskell-Df6bDoY_.js +1 -0
  209. package/dist/view/assets/haxe-CfZj7gIn.js +1 -0
  210. package/dist/view/assets/hcl-BWvSN4gD.js +1 -0
  211. package/dist/view/assets/hjson-D5-asLiD.js +1 -0
  212. package/dist/view/assets/hlsl-D3lLCCz7.js +1 -0
  213. package/dist/view/assets/horizon-BUw7H-hv.js +1 -0
  214. package/dist/view/assets/horizon-bright-CUuTKBJd.js +1 -0
  215. package/dist/view/assets/houston-DnULxvSX.js +1 -0
  216. package/dist/view/assets/html-derivative-DlHx6ybY.js +1 -0
  217. package/dist/view/assets/html-pp8916En.js +1 -0
  218. package/dist/view/assets/http-jrhK8wxY.js +1 -0
  219. package/dist/view/assets/hurl-irOxFIW8.js +1 -0
  220. package/dist/view/assets/hxml-2-FPmUDs.js +1 -0
  221. package/dist/view/assets/hy-DFXneXwc.js +1 -0
  222. package/dist/view/assets/imba-DGztddWO.js +1 -0
  223. package/dist/view/assets/index-BLryk3W4.css +1 -0
  224. package/dist/view/assets/index-ByMJp0a5.js +3 -0
  225. package/dist/view/assets/index-CRh4USfC.js +189 -0
  226. package/dist/view/assets/index-JNx7Ohoj.js +153 -0
  227. package/dist/view/assets/ini-BEwlwnbL.js +1 -0
  228. package/dist/view/assets/java-CylS5w8V.js +1 -0
  229. package/dist/view/assets/javascript-wDzz0qaB.js +1 -0
  230. package/dist/view/assets/jinja-f2NsQr07.js +1 -0
  231. package/dist/view/assets/jison-wvAkD_A8.js +1 -0
  232. package/dist/view/assets/json-Cp-IABpG.js +1 -0
  233. package/dist/view/assets/json5-C9tS-k6U.js +1 -0
  234. package/dist/view/assets/jsonc-Des-eS-w.js +1 -0
  235. package/dist/view/assets/jsonl-DcaNXYhu.js +1 -0
  236. package/dist/view/assets/jsonnet-DFQXde-d.js +1 -0
  237. package/dist/view/assets/jssm-C2t-YnRu.js +1 -0
  238. package/dist/view/assets/jsx-g9-lgVsj.js +1 -0
  239. package/dist/view/assets/julia-5Bft2YPA.js +1 -0
  240. package/dist/view/assets/just-Cwhn7H3k.js +1 -0
  241. package/dist/view/assets/kanagawa-dragon-BuwD2xS4.js +1 -0
  242. package/dist/view/assets/kanagawa-lotus-C3DzagqV.js +1 -0
  243. package/dist/view/assets/kanagawa-wave-DFGoQZhC.js +1 -0
  244. package/dist/view/assets/katex-BYZTj3zW.css +1 -0
  245. package/dist/view/assets/katex-C5jXJg4s.js +257 -0
  246. package/dist/view/assets/kdl-DV7GczEv.js +1 -0
  247. package/dist/view/assets/kotlin-BdnUsdx6.js +1 -0
  248. package/dist/view/assets/kusto-wEQ09or8.js +1 -0
  249. package/dist/view/assets/laserwave-DUszq2jm.js +1 -0
  250. package/dist/view/assets/latex-D5pSuvFb.js +1 -0
  251. package/dist/view/assets/lean-BZvkOJ9d.js +1 -0
  252. package/dist/view/assets/less-B1dDrJ26.js +1 -0
  253. package/dist/view/assets/light-plus-B7mTdjB0.js +1 -0
  254. package/dist/view/assets/liquid-C0sCDyMI.js +1 -0
  255. package/dist/view/assets/llvm-BZoOZj88.js +1 -0
  256. package/dist/view/assets/log-2UxHyX5q.js +1 -0
  257. package/dist/view/assets/logo-BtOb2qkB.js +1 -0
  258. package/dist/view/assets/lua-BaeVxFsk.js +1 -0
  259. package/dist/view/assets/luau-BnpPk5vE.js +1 -0
  260. package/dist/view/assets/make-CHLpvVh8.js +1 -0
  261. package/dist/view/assets/markdown-Cvjx9yec.js +1 -0
  262. package/dist/view/assets/marko-DjSrsDqO.js +1 -0
  263. package/dist/view/assets/material-theme-D5KoaKCx.js +1 -0
  264. package/dist/view/assets/material-theme-darker-BfHTSMKl.js +1 -0
  265. package/dist/view/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  266. package/dist/view/assets/material-theme-ocean-CyktbL80.js +1 -0
  267. package/dist/view/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  268. package/dist/view/assets/matlab-D7o27uSR.js +1 -0
  269. package/dist/view/assets/mdc-D1_yUvq7.js +1 -0
  270. package/dist/view/assets/mdx-Cmh6b_Ma.js +1 -0
  271. package/dist/view/assets/mermaid-CQcHuHx7.js +1 -0
  272. package/dist/view/assets/min-dark-CafNBF8u.js +1 -0
  273. package/dist/view/assets/min-light-CTRr51gU.js +1 -0
  274. package/dist/view/assets/mipsasm-CKIfxQSi.js +1 -0
  275. package/dist/view/assets/mojo-DJz3ZmWd.js +1 -0
  276. package/dist/view/assets/monokai-BRVnQi9A.js +1 -0
  277. package/dist/view/assets/moonbit-CHtswR0a.js +1 -0
  278. package/dist/view/assets/move-el3G9tDJ.js +1 -0
  279. package/dist/view/assets/narrat-DRg8JJMk.js +1 -0
  280. package/dist/view/assets/nextflow-C-mBbutL.js +1 -0
  281. package/dist/view/assets/nextflow-groovy-vE_lwT2v.js +1 -0
  282. package/dist/view/assets/nginx-BpAMiNFr.js +1 -0
  283. package/dist/view/assets/night-owl-C39BiMTA.js +1 -0
  284. package/dist/view/assets/night-owl-light-CMTm3GFP.js +1 -0
  285. package/dist/view/assets/nim-BIad80T-.js +1 -0
  286. package/dist/view/assets/nix-CwoSXNpI.js +1 -0
  287. package/dist/view/assets/nord-Ddv68eIx.js +1 -0
  288. package/dist/view/assets/nsis-BlV79W_Q.js +1 -0
  289. package/dist/view/assets/nushell-D3jzshHO.js +1 -0
  290. package/dist/view/assets/objective-c-DXmwc3jG.js +1 -0
  291. package/dist/view/assets/objective-cpp-CLxacb5B.js +1 -0
  292. package/dist/view/assets/ocaml-C0hk2d4L.js +1 -0
  293. package/dist/view/assets/odin-BBf5iR-q.js +1 -0
  294. package/dist/view/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  295. package/dist/view/assets/one-light-C3Wv6jpd.js +1 -0
  296. package/dist/view/assets/openscad-C4EeE6gA.js +1 -0
  297. package/dist/view/assets/org-DM6o9KBp.js +1 -0
  298. package/dist/view/assets/pascal-D93ZcfNL.js +1 -0
  299. package/dist/view/assets/perl-B9cMNwum.js +1 -0
  300. package/dist/view/assets/php-Csjmro_R.js +1 -0
  301. package/dist/view/assets/pkl-u5AG7uiY.js +1 -0
  302. package/dist/view/assets/plastic-3e1v2bzS.js +1 -0
  303. package/dist/view/assets/plsql-ChMvpjG-.js +1 -0
  304. package/dist/view/assets/po-BTJTHyun.js +1 -0
  305. package/dist/view/assets/poimandres-CS3Unz2-.js +1 -0
  306. package/dist/view/assets/polar-C0HS_06l.js +1 -0
  307. package/dist/view/assets/postcss-CXtECtnM.js +1 -0
  308. package/dist/view/assets/powerquery-CEu0bR-o.js +1 -0
  309. package/dist/view/assets/powershell-BmBUJMz7.js +1 -0
  310. package/dist/view/assets/prisma-Vru482bI.js +1 -0
  311. package/dist/view/assets/prolog-CbFg5uaA.js +1 -0
  312. package/dist/view/assets/proto-C7zT0LnQ.js +1 -0
  313. package/dist/view/assets/pug-DKIMFp6K.js +1 -0
  314. package/dist/view/assets/puppet-BMWR74SV.js +1 -0
  315. package/dist/view/assets/purescript-CklMAg4u.js +1 -0
  316. package/dist/view/assets/python-B6aJPvgy.js +1 -0
  317. package/dist/view/assets/qml-3beO22l8.js +1 -0
  318. package/dist/view/assets/qmldir-C8lEn-DE.js +1 -0
  319. package/dist/view/assets/qss-IeuSbFQv.js +1 -0
  320. package/dist/view/assets/r-Cf5RLm7j.js +1 -0
  321. package/dist/view/assets/racket-BqYA7rlc.js +1 -0
  322. package/dist/view/assets/raku-DXvB9xmW.js +1 -0
  323. package/dist/view/assets/razor-BjBPvh-w.js +1 -0
  324. package/dist/view/assets/rbs-CpoqiR4B.js +1 -0
  325. package/dist/view/assets/red-hvxz__6c.js +1 -0
  326. package/dist/view/assets/reg-C-SQnVFl.js +1 -0
  327. package/dist/view/assets/regexp-CDVJQ6XC.js +1 -0
  328. package/dist/view/assets/rel-C3B-1QV4.js +1 -0
  329. package/dist/view/assets/riscv-BM1_JUlF.js +1 -0
  330. package/dist/view/assets/ron-D8l8udqQ.js +1 -0
  331. package/dist/view/assets/rose-pine-5qJOZa0Y.js +1 -0
  332. package/dist/view/assets/rose-pine-dawn-zx0QlTCp.js +1 -0
  333. package/dist/view/assets/rose-pine-moon-C9pEdX9L.js +1 -0
  334. package/dist/view/assets/rosmsg-BJDFO7_C.js +1 -0
  335. package/dist/view/assets/rst-bs7f0vWN.js +1 -0
  336. package/dist/view/assets/ruby-C0TQ7zu5.js +1 -0
  337. package/dist/view/assets/rust-B1yitclQ.js +1 -0
  338. package/dist/view/assets/sas-DEy46yEz.js +1 -0
  339. package/dist/view/assets/sass-Cj5Yp3dK.js +1 -0
  340. package/dist/view/assets/scala-CqE71os6.js +1 -0
  341. package/dist/view/assets/scheme-C98Dy4si.js +1 -0
  342. package/dist/view/assets/scss-D5BDwBP9.js +1 -0
  343. package/dist/view/assets/sdbl-DVxCFoDh.js +1 -0
  344. package/dist/view/assets/shaderlab-Dg9Lc6iA.js +1 -0
  345. package/dist/view/assets/shellscript-Yzrsuije.js +1 -0
  346. package/dist/view/assets/shellsession-BADoaaVG.js +1 -0
  347. package/dist/view/assets/slack-dark-BthQWCQV.js +1 -0
  348. package/dist/view/assets/slack-ochin-DqwNpetd.js +1 -0
  349. package/dist/view/assets/smalltalk-BOQMe2GC.js +1 -0
  350. package/dist/view/assets/smithy-cds9vsN8.js +1 -0
  351. package/dist/view/assets/snazzy-light-Bw305WKR.js +1 -0
  352. package/dist/view/assets/solarized-dark-CpvCGNkr.js +1 -0
  353. package/dist/view/assets/solarized-light-Dlz6yCKv.js +1 -0
  354. package/dist/view/assets/solidity-DijEV5ha.js +1 -0
  355. package/dist/view/assets/soy-8wufbnw4.js +1 -0
  356. package/dist/view/assets/sparql-rVzFXLq3.js +1 -0
  357. package/dist/view/assets/splunk-BtCnVYZw.js +1 -0
  358. package/dist/view/assets/sql-CRqJ_cUM.js +1 -0
  359. package/dist/view/assets/ssh-config-_ykCGR6B.js +1 -0
  360. package/dist/view/assets/stata-DI20mbqo.js +1 -0
  361. package/dist/view/assets/stylus-BEDo0Tqx.js +1 -0
  362. package/dist/view/assets/surrealql-Cjom0U5J.js +1 -0
  363. package/dist/view/assets/svelte-Cy7k_4gC.js +1 -0
  364. package/dist/view/assets/swift-C2oV4EkX.js +1 -0
  365. package/dist/view/assets/synthwave-84-CbfX1IO0.js +1 -0
  366. package/dist/view/assets/system-verilog-0hqHdDBg.js +1 -0
  367. package/dist/view/assets/systemd-4A_iFExJ.js +1 -0
  368. package/dist/view/assets/talonscript-CkByrt1z.js +1 -0
  369. package/dist/view/assets/tasl-QIJgUcNo.js +1 -0
  370. package/dist/view/assets/tcl-dwOrl1Do.js +1 -0
  371. package/dist/view/assets/templ-DhtptRzy.js +1 -0
  372. package/dist/view/assets/terraform-BETggiCN.js +1 -0
  373. package/dist/view/assets/tex-D96PA37w.js +1 -0
  374. package/dist/view/assets/tokyo-night-hegEt444.js +1 -0
  375. package/dist/view/assets/toml-vGWfd6FD.js +1 -0
  376. package/dist/view/assets/ts-tags-D351s5mN.js +1 -0
  377. package/dist/view/assets/tsv-B_m7g4N7.js +1 -0
  378. package/dist/view/assets/tsx-COt5Ahok.js +1 -0
  379. package/dist/view/assets/turtle-BsS91CYL.js +1 -0
  380. package/dist/view/assets/twig-27uCiNez.js +1 -0
  381. package/dist/view/assets/typescript-BPQ3VLAy.js +1 -0
  382. package/dist/view/assets/typespec-CAFt9gP4.js +1 -0
  383. package/dist/view/assets/typst-BUadGCkm.js +1 -0
  384. package/dist/view/assets/v-BGw2Nkan.js +1 -0
  385. package/dist/view/assets/vala-CsfeWuGM.js +1 -0
  386. package/dist/view/assets/vb-Cu-pLBUe.js +1 -0
  387. package/dist/view/assets/verilog-nZwndyjY.js +1 -0
  388. package/dist/view/assets/vesper-DRje8inN.js +1 -0
  389. package/dist/view/assets/vhdl-CeAyd5Ju.js +1 -0
  390. package/dist/view/assets/viml-CJc9bBzg.js +1 -0
  391. package/dist/view/assets/vitesse-black-Bkuqu6BP.js +1 -0
  392. package/dist/view/assets/vitesse-dark-D0r3Knsf.js +1 -0
  393. package/dist/view/assets/vitesse-light-CVO1_9PV.js +1 -0
  394. package/dist/view/assets/vue-BqiEGhQt.js +1 -0
  395. package/dist/view/assets/vue-html-AaS7Mt5G.js +1 -0
  396. package/dist/view/assets/vue-vine-BoDAl6tE.js +1 -0
  397. package/dist/view/assets/vyper-CDx5xZoG.js +1 -0
  398. package/dist/view/assets/wasm-CG6Dc4jp.js +1 -0
  399. package/dist/view/assets/wasm-MzD3tlZU.js +1 -0
  400. package/dist/view/assets/wenyan-BV7otONQ.js +1 -0
  401. package/dist/view/assets/wgsl-Dx-B1_4e.js +1 -0
  402. package/dist/view/assets/wikitext-BhOHFoWU.js +1 -0
  403. package/dist/view/assets/wit-5i3qLPDT.js +1 -0
  404. package/dist/view/assets/wolfram-lXgVvXCa.js +1 -0
  405. package/dist/view/assets/xml-sdJ4AIDG.js +1 -0
  406. package/dist/view/assets/xsl-CtQFsRM5.js +1 -0
  407. package/dist/view/assets/yaml-Buea-lGh.js +1 -0
  408. package/dist/view/assets/zenscript-DVFEvuxE.js +1 -0
  409. package/dist/view/assets/zig-VOosw3JB.js +1 -0
  410. package/dist/view/index.html +10 -2
  411. package/docs/README.md +21 -15
  412. package/docs/README.zh.md +15 -9
  413. package/docs/en/authoring.md +85 -80
  414. package/docs/en/cli-reference.md +110 -72
  415. package/docs/en/deployment.md +183 -0
  416. package/docs/en/getting-started.md +112 -0
  417. package/docs/en/material-types.md +241 -0
  418. package/docs/en/settings.md +133 -0
  419. package/docs/en/shortcuts.md +89 -0
  420. package/docs/en/typing-rules.md +208 -0
  421. package/docs/zh/authoring.md +74 -69
  422. package/docs/zh/cli-reference.md +92 -58
  423. package/docs/zh/deployment.md +179 -0
  424. package/docs/zh/getting-started.md +104 -0
  425. package/docs/zh/material-types.md +769 -0
  426. package/docs/zh/settings.md +120 -0
  427. package/docs/zh/shortcuts.md +83 -0
  428. package/docs/zh/typing-rules.md +191 -0
  429. package/package.json +13 -11
  430. package/templates/en/QTYPE.md +155 -495
  431. package/templates/en/README.md +11 -11
  432. package/templates/en/TODO.md +1 -1
  433. package/templates/en/material/article.md +20 -0
  434. package/templates/en/material/assets/article/sunrise.svg +12 -0
  435. package/templates/en/material/assets/code/tree.svg +19 -0
  436. package/templates/en/material/assets/qa/login-flow.svg +17 -0
  437. package/templates/en/material/assets/sentence/pangram.svg +6 -0
  438. package/templates/en/material/assets/snippet/moon.svg +8 -0
  439. package/templates/en/material/assets/word/apple.svg +5 -0
  440. package/templates/en/material/code.md +55 -0
  441. package/templates/en/material/qa.md +33 -0
  442. package/templates/en/material/sentence.md +18 -0
  443. package/templates/en/material/snippet.md +17 -0
  444. package/templates/en/material/word.md +20 -0
  445. package/templates/zh/QTYPE.md +191 -473
  446. package/templates/zh/README.md +6 -6
  447. package/templates/zh/index.md +1 -1
  448. package/templates/zh/material/article.md +18 -0
  449. package/templates/zh/material/assets/article/sunrise.svg +12 -0
  450. package/templates/zh/material/assets/code/tree.svg +19 -0
  451. package/templates/zh/material/assets/qa/login-flow.svg +17 -0
  452. package/templates/zh/material/assets/sentence/pangram.svg +6 -0
  453. package/templates/zh/material/assets/snippet/moon.svg +8 -0
  454. package/templates/zh/material/assets/word/apple.svg +5 -0
  455. package/templates/zh/material/code.md +55 -0
  456. package/templates/zh/material/qa.md +33 -0
  457. package/templates/zh/material/sentence.md +19 -0
  458. package/templates/zh/material/snippet.md +20 -0
  459. package/templates/zh/material/word.md +20 -0
  460. package/dist/cli/commands/check/action.js +0 -56
  461. package/dist/cli/commands/check/i18n.js +0 -26
  462. package/dist/cli/commands/plugin/i18n.js +0 -28
  463. package/dist/cli/commands/plugin/index.js +0 -41
  464. package/dist/core/typing/engine.js +0 -112
  465. package/dist/view/assets/index-DBABH3dG.js +0 -111
  466. package/dist/view/assets/index-RTLZ-zOC.css +0 -1
  467. package/templates/en/material/example.md +0 -47
  468. 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
+ }
@@ -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
- return globalThis.crypto.subtle.importKey('raw', toBufferSource(raw), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
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>
@@ -6,3 +6,4 @@ export * from './typing/index.js';
6
6
  // without a duplicate export conflict.
7
7
  export { charToKeystrokes, textToCharPlans } from './keystroke/index.js';
8
8
  export { normalizeText, validateTypingText } from './keystroke/textNormalize.js';
9
+ export * from './encrypt.js';
@@ -1,46 +1,43 @@
1
1
  const FM_RE = /^---\n([\s\S]*?)\n---\n?/;
2
- export function parseFrontmatter(block) {
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 = block.match(FM_RE);
19
+ const match = source.match(FM_RE);
5
20
  if (!match)
6
- return { meta, rest: block };
21
+ return { meta, rest: source };
7
22
  const body = match[1];
8
- const rest = block.slice(match[0].length);
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
- const [, key, rawValue] = kv;
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
- return { meta, rest };
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 { parseFrontmatter } from './frontmatter.js';
1
+ import { parseMaterialFrontmatter } from './frontmatter.js';
2
2
  import { textToCharPlans } from '../keystroke/index.js';
3
3
  import { validateTypingText } from '../keystroke/textNormalize.js';
4
- const CARD_SEPARATOR = /\n\n---\n\n/;
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 blocks = normalized
8
- .split(CARD_SEPARATOR)
9
- .map((s) => s.trim())
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 < blocks.length; i++) {
13
- const card = parseCard(blocks[i], filePath, i);
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
- function parseCard(block, filePath, index) {
20
- const { meta, rest } = parseFrontmatter(block);
21
- const qMatch = rest.match(/^##\s*Q:?\s*([\s\S]*?)(?=\n##\s*A:?|$)/m);
22
- const aMatch = rest.match(/\n##\s*A:?\s*([\s\S]*)$/m);
23
- if (!qMatch || !aMatch)
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 question = qMatch[1].trim();
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
- if (!question || !rawAnswer)
146
+ const targetText = extractTypingText(rawAnswer);
147
+ if (!targetText)
28
148
  return null;
29
- // Normalize the answer for typing: smart quotes/dashes rewritten, emoji &
30
- // zero-width chars stripped. Compute the keystroke plan and list any chars
31
- // that still don't have one so the UI can warn.
32
- const validated = validateTypingText(rawAnswer);
33
- const charPlans = textToCharPlans(validated.text);
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 `![](url)` 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,3 @@
1
+ export { userOnlySchema } from './user.js';
2
+ export { repoOnlySchema } from './repo.js';
3
+ export { bothSchema } from './both.js';
@@ -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
+ };