@qnroa/qtype 0.0.2 → 0.0.4

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 +44 -3
  2. package/CHANGELOG.zh.md +36 -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 +88 -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-1XXyxiQO.js +189 -0
  224. package/dist/view/assets/index-BLryk3W4.css +1 -0
  225. package/dist/view/assets/index-DIBSxFYl.js +3 -0
  226. package/dist/view/assets/index-DgHvnMV9.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 +11 -3
  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
@@ -62,11 +62,30 @@ export async function repoNewAction(opts) {
62
62
  fs.writeFileSync(path.join(repoDir, fileName), readTemplate(templatesDir, fileName, vars));
63
63
  console.log(` + ${fileName}`);
64
64
  }
65
- // 4. Seed material/example.md and empty .gitkeep so the directory exists
66
- // but stays trivially prunable if the user wipes the seed.
65
+ // 4. Seed one starter per material type + .gitkeep so the directory
66
+ // exists but stays trivially prunable if the user wipes the seed.
67
+ // Each starter is a valid `qtype check`-clean deck the user can
68
+ // copy, rename, and edit. Their image assets ship alongside so the
69
+ // demos render out of the box.
67
70
  fs.mkdirSync(path.join(repoDir, 'material'), { recursive: true });
68
- fs.writeFileSync(path.join(repoDir, 'material', 'example.md'), readTemplate(templatesDir, path.join('material', 'example.md'), vars));
69
- console.log(` + material/example.md`);
71
+ const starters = [
72
+ ['word', 'apple.svg'],
73
+ ['sentence', 'pangram.svg'],
74
+ ['article', 'sunrise.svg'],
75
+ ['snippet', 'moon.svg'],
76
+ ['qa', 'login-flow.svg'],
77
+ ['code', 'tree.svg'],
78
+ ];
79
+ for (const [type, asset] of starters) {
80
+ const mdRel = path.join('material', `${type}.md`);
81
+ fs.writeFileSync(path.join(repoDir, mdRel), readTemplate(templatesDir, mdRel, vars));
82
+ console.log(` + ${mdRel}`);
83
+ const assetRel = path.join('material', 'assets', type, asset);
84
+ const assetDir = path.dirname(path.join(repoDir, assetRel));
85
+ fs.mkdirSync(assetDir, { recursive: true });
86
+ fs.writeFileSync(path.join(repoDir, assetRel), readTemplate(templatesDir, assetRel, vars));
87
+ console.log(` + ${assetRel}`);
88
+ }
70
89
  fs.writeFileSync(path.join(repoDir, 'material', '.gitkeep'), '');
71
90
  console.log(` + material/.gitkeep`);
72
91
  // 5. Write .qtype/config.json with the repo language.
@@ -16,7 +16,7 @@ export const messages = {
16
16
  'repo.new.msg.done': '仓库已建好:{path}',
17
17
  'repo.new.msg.nextSteps': '下一步:',
18
18
  'repo.new.msg.next.cd': 'cd {name}',
19
- 'repo.new.msg.next.edit': 'vim material/example.md # 或添加新的 material/*.md',
19
+ 'repo.new.msg.next.edit': 'ls material/ # 6 种类型的入门模板,改哪个都行',
20
20
  'repo.new.msg.next.commit': 'git add -A && git commit # 首次提交',
21
21
  'repo.new.msg.next.gitHint': 'git init # 如需版本控制,也可以 qtype repo new --git',
22
22
  'repo.new.tpl.defaultDescription': '资料库',
@@ -38,7 +38,7 @@ export const messages = {
38
38
  'repo.new.msg.done': 'Repo created: {path}',
39
39
  'repo.new.msg.nextSteps': 'Next steps:',
40
40
  'repo.new.msg.next.cd': 'cd {name}',
41
- 'repo.new.msg.next.edit': 'vim material/example.md # or add new material/*.md',
41
+ 'repo.new.msg.next.edit': 'ls material/ # 6 starter templates, edit any of them',
42
42
  'repo.new.msg.next.commit': 'git add -A && git commit # initial commit',
43
43
  'repo.new.msg.next.gitHint': 'git init # or re-run with qtype repo new --git',
44
44
  'repo.new.tpl.defaultDescription': 'material library',
@@ -16,12 +16,32 @@ function buildSyncPlan() {
16
16
  { kind: 'file', target: 'TODO.md', source: 'TODO.md', managed: false },
17
17
  { kind: 'file', target: 'index.md', source: 'index.md', managed: false },
18
18
  { kind: 'file', target: 'log.md', source: 'log.md', managed: false },
19
- {
19
+ // Starter cards — one per material type. Users copy the one that
20
+ // matches what they're writing, rename it, edit content, and are
21
+ // off to the races. Every file is a valid `qtype check`-clean deck
22
+ // out of the box (the smoke test asserts this).
23
+ ...['word', 'sentence', 'article', 'snippet', 'qa', 'code'].map((type) => ({
20
24
  kind: 'file',
21
- target: 'material/example.md',
22
- source: path.join('material', 'example.md'),
25
+ target: `material/${type}.md`,
26
+ source: path.join('material', `${type}.md`),
23
27
  managed: false,
24
- },
28
+ })),
29
+ // Starter card image assets — SVGs referenced by the starters.
30
+ // `currentColor`-stroked so they inherit the site's text color and
31
+ // adapt to dark/light theme without extra work.
32
+ ...[
33
+ ['word', 'apple.svg'],
34
+ ['sentence', 'pangram.svg'],
35
+ ['article', 'sunrise.svg'],
36
+ ['snippet', 'moon.svg'],
37
+ ['qa', 'login-flow.svg'],
38
+ ['code', 'tree.svg'],
39
+ ].map(([type, name]) => ({
40
+ kind: 'file',
41
+ target: `material/assets/${type}/${name}`,
42
+ source: path.join('material', 'assets', type, name),
43
+ managed: false,
44
+ })),
25
45
  // ── Skeleton (create-only, never touched) ───────
26
46
  { kind: 'dir', target: '.qtype', managed: false },
27
47
  { kind: 'dir', target: 'material', managed: false },
@@ -9,7 +9,8 @@ import { fileURLToPath } from 'node:url';
9
9
  * ├── en/
10
10
  * │ ├── QTYPE.md README.md TODO.md index.md log.md
11
11
  * │ └── material/
12
- * │ └── example.md
12
+ * │ ├── word.md article.md qa.md
13
+ * │ └── sentence.md snippet.md code.md
13
14
  * └── zh/
14
15
  * └── (same files)
15
16
  *
@@ -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
+ }