yanki 2.0.0 → 2.0.2

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 (655) hide show
  1. package/dist/bin/abap-lNEwo1Fh.js +1 -0
  2. package/dist/bin/actionscript-3-WtXz7Nc9.js +1 -0
  3. package/dist/bin/ada-CxieIYPC.js +1 -0
  4. package/dist/bin/andromeeda-CEVX-4Db.js +1 -0
  5. package/dist/bin/angular-html-CAOiQ-Sh.js +1 -0
  6. package/dist/bin/angular-html-qzs4hSq7.js +1 -0
  7. package/dist/bin/angular-ts-CugMBDjk.js +1 -0
  8. package/dist/bin/apache-zgaAodRu.js +1 -0
  9. package/dist/bin/apex-BNA6bSzI.js +1 -0
  10. package/dist/bin/apl-BOFh_hoE.js +1 -0
  11. package/dist/bin/applescript-D9ApUiQg.js +1 -0
  12. package/dist/bin/ara-Xn981wPy.js +1 -0
  13. package/dist/bin/asciidoc-C90J3qhX.js +1 -0
  14. package/dist/bin/asm-Cvg1-8xg.js +1 -0
  15. package/dist/bin/astro-Cm_dHcy3.js +1 -0
  16. package/dist/bin/aurora-x-BLDZZUdf.js +1 -0
  17. package/dist/bin/awk-BjDw91sP.js +1 -0
  18. package/dist/bin/ayu-dark-CV3szcXN.js +1 -0
  19. package/dist/bin/ayu-light-VFwhcb7b.js +1 -0
  20. package/dist/bin/ayu-mirage-jd_AuG1F.js +1 -0
  21. package/dist/bin/ballerina-vBN0WIlg.js +1 -0
  22. package/dist/bin/bat-C7p6Ubpb.js +1 -0
  23. package/dist/bin/beancount-zb-3xb6T.js +1 -0
  24. package/dist/bin/berry-B_fAwMGD.js +1 -0
  25. package/dist/bin/bibtex-AJFrnpSy.js +1 -0
  26. package/dist/bin/bicep-ZTMcEuYT.js +1 -0
  27. package/dist/bin/bird2-_hZ3FStw.js +1 -0
  28. package/dist/bin/blade-U1S3oe2k.js +1 -0
  29. package/dist/bin/bsl-0ORkkYmh.js +1 -0
  30. package/dist/bin/c-CdETmdiZ.js +1 -0
  31. package/dist/bin/c-DIXYfOSP.js +1 -0
  32. package/dist/bin/c3-CF_7Rl_o.js +1 -0
  33. package/dist/bin/cadence-BoKQX0Lv.js +1 -0
  34. package/dist/bin/cairo-kiUMtAsX.js +1 -0
  35. package/dist/bin/catppuccin-frappe-CHSjY7K5.js +1 -0
  36. package/dist/bin/catppuccin-latte-BIei23jc.js +1 -0
  37. package/dist/bin/catppuccin-macchiato-stEzqrNw.js +1 -0
  38. package/dist/bin/catppuccin-mocha-Csibodaz.js +1 -0
  39. package/dist/bin/clarity-CF2X0I3d.js +1 -0
  40. package/dist/bin/cli.js +309 -22
  41. package/dist/bin/clojure-D4EmLgj3.js +1 -0
  42. package/dist/bin/cmake-BADnWbaq.js +1 -0
  43. package/dist/bin/cmake-D8YNkuR-.js +1 -0
  44. package/dist/bin/cobol-0fKuGiZv.js +1 -0
  45. package/dist/bin/codeowners-CKcVmpS-.js +1 -0
  46. package/dist/bin/codeql-aBgygx2C.js +1 -0
  47. package/dist/bin/coffee-7hoZPzSj.js +1 -0
  48. package/dist/bin/common-lisp-C0AzPNil.js +1 -0
  49. package/dist/bin/coq-FNCdvNHe.js +1 -0
  50. package/dist/bin/cpp-5C3nn41J.js +1 -0
  51. package/dist/bin/cpp-gdwUInvu.js +1 -0
  52. package/dist/bin/crystal-Cc66vuxC.js +1 -0
  53. package/dist/bin/csharp-2Bjq_Rxi.js +1 -0
  54. package/dist/bin/csharp-CIll8D6u.js +1 -0
  55. package/dist/bin/css-C-sfFA03.js +1 -0
  56. package/dist/bin/css-CWs_acR3.js +1 -0
  57. package/dist/bin/csv-BU_TMxAS.js +1 -0
  58. package/dist/bin/csv-DxS-HYUf.js +1 -0
  59. package/dist/bin/cue-DV_1hEYT.js +1 -0
  60. package/dist/bin/cypher-DrvyYDIr.js +1 -0
  61. package/dist/bin/d-Dly9ufdX.js +1 -0
  62. package/dist/bin/dark-plus-B42qAV4C.js +1 -0
  63. package/dist/bin/dart-C2ROqx0Z.js +1 -0
  64. package/dist/bin/dax-vDoOL-fx.js +1 -0
  65. package/dist/bin/desktop-DoZVZZ4h.js +1 -0
  66. package/dist/bin/diff-By4RszRJ.js +1 -0
  67. package/dist/bin/diff-DP2jXnqp.js +1 -0
  68. package/dist/bin/docker-Bp8c7Dqi.js +1 -0
  69. package/dist/bin/dotenv-CEJeNbhS.js +1 -0
  70. package/dist/bin/dracula-DmN2zRHi.js +1 -0
  71. package/dist/bin/dracula-soft-CMTkd8vI.js +1 -0
  72. package/dist/bin/dream-maker-D3zsoW7J.js +1 -0
  73. package/dist/bin/edge-DetFnTa9.js +1 -0
  74. package/dist/bin/elixir-CrM98V7h.js +1 -0
  75. package/dist/bin/elm-CD-7RX5d.js +1 -0
  76. package/dist/bin/emacs-lisp-Cj_Ixk9i.js +1 -0
  77. package/dist/bin/erb-KO-P0SR0.js +1 -0
  78. package/dist/bin/erlang-BFKCgnPw.js +1 -0
  79. package/dist/bin/everforest-dark-C1ZUi0Ki.js +1 -0
  80. package/dist/bin/everforest-light-BiIEIFyN.js +1 -0
  81. package/dist/bin/fennel-DAD8VWfy.js +1 -0
  82. package/dist/bin/fish-G6fwZxok.js +1 -0
  83. package/dist/bin/fluent-BOBTNP2g.js +1 -0
  84. package/dist/bin/fortran-fixed-form-t-oisds5.js +1 -0
  85. package/dist/bin/fortran-free-form-By4p5LLA.js +1 -0
  86. package/dist/bin/fortran-free-form-C3X_sfp9.js +1 -0
  87. package/dist/bin/fsharp-Dq5iPDb2.js +1 -0
  88. package/dist/bin/gdresource-BNzPbQHR.js +1 -0
  89. package/dist/bin/gdscript-DF4DHgCD.js +1 -0
  90. package/dist/bin/gdscript-bYJURCOy.js +1 -0
  91. package/dist/bin/gdshader-BPirqxsQ.js +1 -0
  92. package/dist/bin/gdshader-KOuf2xVk.js +1 -0
  93. package/dist/bin/genie-DHT45pJu.js +1 -0
  94. package/dist/bin/gherkin-BOftJ3Qu.js +1 -0
  95. package/dist/bin/git-commit-Bkc1RnxN.js +1 -0
  96. package/dist/bin/git-rebase-BfAeU5XI.js +1 -0
  97. package/dist/bin/github-dark-Dhlw38v_.js +1 -0
  98. package/dist/bin/github-dark-default-ChurEZVr.js +1 -0
  99. package/dist/bin/github-dark-dimmed-Bg8moaOR.js +1 -0
  100. package/dist/bin/github-dark-high-contrast-BuMAw1v5.js +1 -0
  101. package/dist/bin/github-light-DUFhx0nQ.js +1 -0
  102. package/dist/bin/github-light-default-DhNuYLfL.js +1 -0
  103. package/dist/bin/github-light-high-contrast-VDpphmUr.js +1 -0
  104. package/dist/bin/gleam-BQORp6Ww.js +1 -0
  105. package/dist/bin/glimmer-js-BZd4ZfVU.js +1 -0
  106. package/dist/bin/glimmer-ts-UFNn11p7.js +1 -0
  107. package/dist/bin/glsl-DFAnV4bR.js +1 -0
  108. package/dist/bin/glsl-r9XJn22U.js +1 -0
  109. package/dist/bin/gn-BUvkHyO2.js +1 -0
  110. package/dist/bin/gnuplot-BiNqzIdJ.js +1 -0
  111. package/dist/bin/go-GcrXPybz.js +1 -0
  112. package/dist/bin/go-b5i9ZOwA.js +1 -0
  113. package/dist/bin/graphql-AasBDd3n.js +1 -0
  114. package/dist/bin/graphql-J1xs8wG6.js +1 -0
  115. package/dist/bin/groovy-DZM0Oins.js +1 -0
  116. package/dist/bin/gruvbox-dark-hard-B0-6V_Sa.js +1 -0
  117. package/dist/bin/gruvbox-dark-medium-B2kDYkf7.js +1 -0
  118. package/dist/bin/gruvbox-dark-soft-D80qdOIx.js +1 -0
  119. package/dist/bin/gruvbox-light-hard-np83Fhw4.js +1 -0
  120. package/dist/bin/gruvbox-light-medium--BtU_nk6.js +1 -0
  121. package/dist/bin/gruvbox-light-soft-BQQJZmHA.js +1 -0
  122. package/dist/bin/hack-DTChbFem.js +1 -0
  123. package/dist/bin/haml-Dz37fDMr.js +1 -0
  124. package/dist/bin/haml-lACpmly2.js +1 -0
  125. package/dist/bin/handlebars-B7ppocUa.js +1 -0
  126. package/dist/bin/haskell-ChFomDou.js +1 -0
  127. package/dist/bin/haxe-B4EsxHyL.js +1 -0
  128. package/dist/bin/haxe-Bgb4tX8C.js +1 -0
  129. package/dist/bin/hcl-B_oQREVc.js +1 -0
  130. package/dist/bin/hjson-B6rsoOCm.js +1 -0
  131. package/dist/bin/hlsl-CxhYg9sd.js +1 -0
  132. package/dist/bin/hlsl-Dzk9GcSJ.js +1 -0
  133. package/dist/bin/horizon-DC2iZgoY.js +1 -0
  134. package/dist/bin/horizon-bright-yXhu6egb.js +1 -0
  135. package/dist/bin/houston-CAkU4Xzv.js +1 -0
  136. package/dist/bin/html-Crjq5rc3.js +1 -0
  137. package/dist/bin/html-JSn89dCS.js +1 -0
  138. package/dist/bin/html-derivative-CsguVNJC.js +1 -0
  139. package/dist/bin/html-derivative-DhivRM2V.js +1 -0
  140. package/dist/bin/http-CfCri10f.js +1 -0
  141. package/dist/bin/hurl-C4sIrBP8.js +1 -0
  142. package/dist/bin/hxml-BY0JJAFc.js +1 -0
  143. package/dist/bin/hy-BzSjeCAF.js +1 -0
  144. package/dist/bin/imba-BCVM_gFu.js +1 -0
  145. package/dist/bin/ini-CK7-QP3O.js +1 -0
  146. package/dist/bin/java-BGzTmoXK.js +1 -0
  147. package/dist/bin/java-Dgsz0B-P.js +1 -0
  148. package/dist/bin/javascript-BNwJ9fBE.js +1 -0
  149. package/dist/bin/javascript-DTXNtrg0.js +1 -0
  150. package/dist/bin/jinja-D4pZPcl3.js +1 -0
  151. package/dist/bin/jison-CU_JndNM.js +1 -0
  152. package/dist/bin/json-BadQRMks.js +1 -0
  153. package/dist/bin/json-Dttibclf.js +1 -0
  154. package/dist/bin/json5-D-uNcxgL.js +1 -0
  155. package/dist/bin/jsonc-Mhn2hM5D.js +1 -0
  156. package/dist/bin/jsonl-DEwECwXy.js +1 -0
  157. package/dist/bin/jsonnet-aP9lctbz.js +1 -0
  158. package/dist/bin/jssm-Bzk9BSR5.js +1 -0
  159. package/dist/bin/jsx-CoE327E5.js +1 -0
  160. package/dist/bin/jsx-YopGa7XH.js +1 -0
  161. package/dist/bin/julia-DjiOTm9M.js +1 -0
  162. package/dist/bin/just-oDTZGZbN.js +1 -0
  163. package/dist/bin/kanagawa-dragon-DRuqSBxu.js +1 -0
  164. package/dist/bin/kanagawa-lotus-DFk8eUao.js +1 -0
  165. package/dist/bin/kanagawa-wave-wqQYYPW0.js +1 -0
  166. package/dist/bin/kdl-BBdrnnFN.js +1 -0
  167. package/dist/bin/kotlin-BOAeJ3Zn.js +1 -0
  168. package/dist/bin/kusto-D9rJ_wbq.js +1 -0
  169. package/dist/bin/laserwave-po4bDaOp.js +1 -0
  170. package/dist/bin/latex-B8d5LcJ8.js +1 -0
  171. package/dist/bin/lean--6SzPv5H.js +1 -0
  172. package/dist/bin/less-BrjQyxTC.js +1 -0
  173. package/dist/bin/less-DRwZNTsh.js +1 -0
  174. package/dist/bin/light-plus-B1SMNL2F.js +1 -0
  175. package/dist/bin/liquid-DuGPT5mZ.js +1 -0
  176. package/dist/bin/llvm-7RtiJHjj.js +1 -0
  177. package/dist/bin/log-KNtIq3vN.js +1 -0
  178. package/dist/bin/logo-BhWYf_9m.js +1 -0
  179. package/dist/bin/lua-BTFYnP6K.js +1 -0
  180. package/dist/bin/lua-mUj-7rV8.js +1 -0
  181. package/dist/bin/luau-CmcVV_DK.js +1 -0
  182. package/dist/bin/make-Df5gTOt9.js +1 -0
  183. package/dist/bin/markdown-BtUDO16i.js +1 -0
  184. package/dist/bin/markdown-dl1m7YAL.js +1 -0
  185. package/dist/bin/marko-CEM61Zib.js +1 -0
  186. package/dist/bin/material-theme-BhPjDEOe.js +1 -0
  187. package/dist/bin/material-theme-darker-LB0WiHs5.js +1 -0
  188. package/dist/bin/material-theme-lighter-b9Tsrc9R.js +1 -0
  189. package/dist/bin/material-theme-ocean-B1e0t2_i.js +1 -0
  190. package/dist/bin/material-theme-palenight-1CdY1ykl.js +1 -0
  191. package/dist/bin/matlab-Qa9-GHKp.js +1 -0
  192. package/dist/bin/mdc-Dya9Bdmr.js +1 -0
  193. package/dist/bin/mdx-DrGeQtwF.js +1 -0
  194. package/dist/bin/mermaid-CcPTwP9f.js +1 -0
  195. package/dist/bin/min-dark-Dujx5q9i.js +1 -0
  196. package/dist/bin/min-light-wZS1kyfV.js +1 -0
  197. package/dist/bin/mipsasm-BfvsshAY.js +1 -0
  198. package/dist/bin/mojo-C2BYzC14.js +1 -0
  199. package/dist/bin/monokai-w9z2AnJS.js +1 -0
  200. package/dist/bin/moonbit-DjpzDFB5.js +1 -0
  201. package/dist/bin/move-Bxr9ef96.js +1 -0
  202. package/dist/bin/narrat-DcRshums.js +1 -0
  203. package/dist/bin/nextflow-groovy-COoKx28o.js +1 -0
  204. package/dist/bin/nextflow-groovy-CpiNfI-c.js +1 -0
  205. package/dist/bin/nextflow-hLaJXrFj.js +1 -0
  206. package/dist/bin/nginx-CR6uKwZy.js +1 -0
  207. package/dist/bin/night-owl-CplppJJB.js +1 -0
  208. package/dist/bin/night-owl-light-BpOQXyuL.js +1 -0
  209. package/dist/bin/nim-DceKa9Nn.js +1 -0
  210. package/dist/bin/nix-CtTYb1HK.js +1 -0
  211. package/dist/bin/nord-hoPJ5Dy3.js +1 -0
  212. package/dist/bin/nushell-Cn3fXdgH.js +1 -0
  213. package/dist/bin/objective-c-BkduaOlQ.js +1 -0
  214. package/dist/bin/objective-cpp-Cn2j-b6G.js +1 -0
  215. package/dist/bin/ocaml-DqvuWcoq.js +1 -0
  216. package/dist/bin/odin-D7ha6-Ni.js +1 -0
  217. package/dist/bin/one-dark-pro-eMAAhFAk.js +1 -0
  218. package/dist/bin/one-light-CSbFTW4a.js +1 -0
  219. package/dist/bin/open-8AenGr4K.js +2 -0
  220. package/dist/bin/openscad-Dy0BqjE1.js +1 -0
  221. package/dist/bin/pascal-HLRSlJ2v.js +1 -0
  222. package/dist/bin/perl-B-LgtJc7.js +1 -0
  223. package/dist/bin/perl-B6GAvvHF.js +1 -0
  224. package/dist/bin/php-BAXKJjQn.js +1 -0
  225. package/dist/bin/php-D9zsHNF7.js +1 -0
  226. package/dist/bin/pkl-D0LsOQD5.js +1 -0
  227. package/dist/bin/plastic-DH0ROn14.js +1 -0
  228. package/dist/bin/plsql-Ckotkn8G.js +1 -0
  229. package/dist/bin/po-Qix2N2fA.js +1 -0
  230. package/dist/bin/poimandres-HErFvNSF.js +1 -0
  231. package/dist/bin/polar-DHAM8Rqb.js +1 -0
  232. package/dist/bin/postcss-D_yqFB4d.js +1 -0
  233. package/dist/bin/postcss-hgbxLNuF.js +1 -0
  234. package/dist/bin/powerquery-sXjCytsm.js +1 -0
  235. package/dist/bin/powershell-Br9y9LlC.js +1 -0
  236. package/dist/bin/prisma-COBUUaqE.js +1 -0
  237. package/dist/bin/prolog-BbJTvtA-.js +1 -0
  238. package/dist/bin/proto-DiR3FLO1.js +1 -0
  239. package/dist/bin/pug-F80Qp1DF.js +1 -0
  240. package/dist/bin/puppet-CNdK406T.js +1 -0
  241. package/dist/bin/purescript-Cgw06S43.js +1 -0
  242. package/dist/bin/python-BLuJ3b5I.js +1 -0
  243. package/dist/bin/python-DRu0B6X_.js +1 -0
  244. package/dist/bin/qml-Brw1WH4u.js +1 -0
  245. package/dist/bin/qmldir-tOWyMniz.js +1 -0
  246. package/dist/bin/qss-CQ_tB_vx.js +1 -0
  247. package/dist/bin/r-1QWtk6BQ.js +1 -0
  248. package/dist/bin/r-CeXuGenk.js +1 -0
  249. package/dist/bin/racket-xM_JlWdJ.js +1 -0
  250. package/dist/bin/raku-CsjbV0q-.js +1 -0
  251. package/dist/bin/razor-DKKN_syA.js +1 -0
  252. package/dist/bin/red-2s42RJtx.js +1 -0
  253. package/dist/bin/reg-CDO-JwKW.js +1 -0
  254. package/dist/bin/regexp-Aby82bH7.js +1 -0
  255. package/dist/bin/regexp-WPJh6EdZ.js +1 -0
  256. package/dist/bin/rel-Lnk3efq_.js +1 -0
  257. package/dist/bin/riscv-B6rT4cSG.js +1 -0
  258. package/dist/bin/ron-BiBOkEWn.js +1 -0
  259. package/dist/bin/rose-pine-CxwcSun-.js +1 -0
  260. package/dist/bin/rose-pine-dawn-DkCSMZNl.js +1 -0
  261. package/dist/bin/rose-pine-moon-CRcpU7ID.js +1 -0
  262. package/dist/bin/rosmsg-_EpaFX4E.js +1 -0
  263. package/dist/bin/rst-BHI_EFqA.js +1 -0
  264. package/dist/bin/ruby-DME9-fO-.js +1 -0
  265. package/dist/bin/ruby-Go_q6-3o.js +1 -0
  266. package/dist/bin/rust-Dzeg0wAa.js +1 -0
  267. package/dist/bin/sas-Dx90SbnG.js +1 -0
  268. package/dist/bin/sass-CfSmloHa.js +1 -0
  269. package/dist/bin/scala-BzM4CFnq.js +1 -0
  270. package/dist/bin/scheme-BqJLyHNb.js +1 -0
  271. package/dist/bin/scss-ByAjeRJP.js +1 -0
  272. package/dist/bin/scss-D8wnyi0z.js +1 -0
  273. package/dist/bin/sdbl-CSGqAXn4.js +1 -0
  274. package/dist/bin/sdbl-CVVKgb7O.js +1 -0
  275. package/dist/bin/shaderlab-8CiQ7btM.js +1 -0
  276. package/dist/bin/shellscript-CED9H1ao.js +1 -0
  277. package/dist/bin/shellscript-DhB4mnQ-.js +1 -0
  278. package/dist/bin/shellsession-xr72W2pP.js +1 -0
  279. package/dist/bin/slack-dark-BLjlSbC0.js +1 -0
  280. package/dist/bin/slack-ochin-BeSJq7AY.js +1 -0
  281. package/dist/bin/smalltalk-DrKePzpE.js +1 -0
  282. package/dist/bin/snazzy-light-hGizAcbG.js +1 -0
  283. package/dist/bin/solarized-dark-B47oUJnZ.js +1 -0
  284. package/dist/bin/solarized-light-COfgMzjY.js +1 -0
  285. package/dist/bin/solidity-93cGmCAT.js +1 -0
  286. package/dist/bin/soy-Cy2jSWuh.js +1 -0
  287. package/dist/bin/sparql-Bq74Yvwh.js +1 -0
  288. package/dist/bin/splunk-C1Hlgf0c.js +1 -0
  289. package/dist/bin/sql-BfsqdN68.js +1 -0
  290. package/dist/bin/sql-CWYhyc-w.js +1 -0
  291. package/dist/bin/ssh-config-mSCJO_Nw.js +1 -0
  292. package/dist/bin/stata-DawaFFYJ.js +1 -0
  293. package/dist/bin/stylus-CtR9dQIO.js +1 -0
  294. package/dist/bin/stylus-Dxd5rS5f.js +1 -0
  295. package/dist/bin/surrealql-B1iahgCh.js +1 -0
  296. package/dist/bin/svelte-CHomAVJT.js +1 -0
  297. package/dist/bin/swift-CmhdkGbJ.js +1 -0
  298. package/dist/bin/synthwave-84-C8lHUQgz.js +1 -0
  299. package/dist/bin/system-verilog-C5NPuZ4w.js +1 -0
  300. package/dist/bin/systemd-DWf13jtC.js +1 -0
  301. package/dist/bin/talonscript-BHUwvink.js +1 -0
  302. package/dist/bin/tasl-BqepUa6R.js +1 -0
  303. package/dist/bin/tcl-Cq_3s_LD.js +1 -0
  304. package/dist/bin/templ-BRBP2pJf.js +1 -0
  305. package/dist/bin/terraform-CJUb39F2.js +1 -0
  306. package/dist/bin/tex-5doNg0Ig.js +1 -0
  307. package/dist/bin/tex-BLh5zE4g.js +1 -0
  308. package/dist/bin/tokyo-night-DToTZO1M.js +1 -0
  309. package/dist/bin/toml-CcAy-xez.js +1 -0
  310. package/dist/bin/ts-tags-C0IwWXwE.js +1 -0
  311. package/dist/bin/tsv-2LD76W9Y.js +1 -0
  312. package/dist/bin/tsx-BMmbqG-q.js +1 -0
  313. package/dist/bin/tsx-BTd5Sf56.js +1 -0
  314. package/dist/bin/turtle-BFrhIdAM.js +1 -0
  315. package/dist/bin/turtle-D-xcuGf_.js +1 -0
  316. package/dist/bin/twig-D5Nfm9La.js +1 -0
  317. package/dist/bin/typescript-CPs8nV3S.js +1 -0
  318. package/dist/bin/typescript-DcBfMx29.js +1 -0
  319. package/dist/bin/typespec-CJG5em1O.js +1 -0
  320. package/dist/bin/typst-CdguKrlh.js +1 -0
  321. package/dist/bin/v-VnqxfSIy.js +1 -0
  322. package/dist/bin/vala-CPlxddOG.js +1 -0
  323. package/dist/bin/vb-Do7lrS7D.js +1 -0
  324. package/dist/bin/verilog-OGZUphCy.js +1 -0
  325. package/dist/bin/vesper-D6bcMq-9.js +1 -0
  326. package/dist/bin/vhdl-vQqxP3uc.js +1 -0
  327. package/dist/bin/viml-CRzBk174.js +1 -0
  328. package/dist/bin/vitesse-black-DZPzP4WR.js +1 -0
  329. package/dist/bin/vitesse-dark-Dn_i46a6.js +1 -0
  330. package/dist/bin/vitesse-light-BYktrL59.js +1 -0
  331. package/dist/bin/vue-html-D7v7uXO6.js +1 -0
  332. package/dist/bin/vue-vine-lH9fGYq_.js +1 -0
  333. package/dist/bin/vue-zYUVH6uJ.js +1 -0
  334. package/dist/bin/vyper-CxG5w_hf.js +1 -0
  335. package/dist/bin/wasm-C2OXzu0D.js +1 -0
  336. package/dist/bin/wasm-DfcgBtd_.js +1 -0
  337. package/dist/bin/wenyan-CeYhWKkN.js +1 -0
  338. package/dist/bin/wgsl-gGAb2oSq.js +1 -0
  339. package/dist/bin/wikitext-BNkXva03.js +1 -0
  340. package/dist/bin/wit-snpmR1aT.js +1 -0
  341. package/dist/bin/wolfram-BUWAXgpt.js +1 -0
  342. package/dist/bin/xml-CCj7o04Q.js +1 -0
  343. package/dist/bin/xml-CGrMwkeu.js +1 -0
  344. package/dist/bin/xsl-C-lr_lSi.js +1 -0
  345. package/dist/bin/yaml-Brl8JesV.js +1 -0
  346. package/dist/bin/yaml-Dg2Msz_B.js +1 -0
  347. package/dist/bin/zenscript-DAB1FI6O.js +1 -0
  348. package/dist/bin/zig-DZTxXyCC.js +1 -0
  349. package/dist/lib/index.d.ts +1 -1
  350. package/dist/lib/index.js +2911 -30
  351. package/dist/standalone/abap-lNEwo1Fh.js +1 -0
  352. package/dist/standalone/actionscript-3-WtXz7Nc9.js +1 -0
  353. package/dist/standalone/ada-CxieIYPC.js +1 -0
  354. package/dist/standalone/andromeeda-BylpTWnC.js +1 -0
  355. package/dist/standalone/angular-html-BDfh9ICS.js +1 -0
  356. package/dist/standalone/angular-ts-WiBasIpj.js +1 -0
  357. package/dist/standalone/apache-zgaAodRu.js +1 -0
  358. package/dist/standalone/apex-BNA6bSzI.js +1 -0
  359. package/dist/standalone/apl-B3nAIz3U.js +1 -0
  360. package/dist/standalone/applescript-D9ApUiQg.js +1 -0
  361. package/dist/standalone/ara-Xn981wPy.js +1 -0
  362. package/dist/standalone/asciidoc-C90J3qhX.js +1 -0
  363. package/dist/standalone/asm-Cvg1-8xg.js +1 -0
  364. package/dist/standalone/astro-C0A5Ee0R.js +1 -0
  365. package/dist/standalone/aurora-x-z2QLMo08.js +1 -0
  366. package/dist/standalone/awk-L1WqJyjl.js +1 -0
  367. package/dist/standalone/ayu-dark-DFAYKmY8.js +1 -0
  368. package/dist/standalone/ayu-light-ChPCIrww.js +1 -0
  369. package/dist/standalone/ayu-mirage-Be8UDvXu.js +1 -0
  370. package/dist/standalone/ballerina-DOw61Yuq.js +1 -0
  371. package/dist/standalone/bat-BJGaXoOz.js +1 -0
  372. package/dist/standalone/beancount-Cu6FxzuA.js +1 -0
  373. package/dist/standalone/berry-DiPZpURY.js +1 -0
  374. package/dist/standalone/bibtex-Dp74FGRo.js +1 -0
  375. package/dist/standalone/bicep-BwSAodHP.js +1 -0
  376. package/dist/standalone/bird2-B05GFxQw.js +1 -0
  377. package/dist/standalone/blade-Bh52-e5e.js +1 -0
  378. package/dist/standalone/bsl-DrHuh5kR.js +1 -0
  379. package/dist/standalone/c-C-hboKX2.js +1 -0
  380. package/dist/standalone/c3-BS3NJrFe.js +1 -0
  381. package/dist/standalone/cadence-DKUaB06W.js +1 -0
  382. package/dist/standalone/cairo-D055eoAu.js +1 -0
  383. package/dist/standalone/catppuccin-frappe-Dq9QR7eZ.js +1 -0
  384. package/dist/standalone/catppuccin-latte-yUwHFQst.js +1 -0
  385. package/dist/standalone/catppuccin-macchiato-KYmbHQyq.js +1 -0
  386. package/dist/standalone/catppuccin-mocha-DFrUhns1.js +1 -0
  387. package/dist/standalone/clarity-DKB_EYUy.js +1 -0
  388. package/dist/standalone/clojure-D7D2Zxsu.js +1 -0
  389. package/dist/standalone/cmake-Bju7ahwU.js +1 -0
  390. package/dist/standalone/cobol-C9xdim3W.js +1 -0
  391. package/dist/standalone/codeowners-Di1OvpcW.js +1 -0
  392. package/dist/standalone/codeql-KhL_IfCA.js +1 -0
  393. package/dist/standalone/coffee-CWthmHqg.js +1 -0
  394. package/dist/standalone/common-lisp-DL0ShAR3.js +1 -0
  395. package/dist/standalone/coq-YuXzTVD8.js +1 -0
  396. package/dist/standalone/cpp-BP_mYM2q.js +1 -0
  397. package/dist/standalone/crystal-BP8Y_JHu.js +1 -0
  398. package/dist/standalone/csharp-X4m7dqIW.js +1 -0
  399. package/dist/standalone/css-DZpGhzjZ.js +1 -0
  400. package/dist/standalone/csv-D_FJD-_L.js +1 -0
  401. package/dist/standalone/cue-Brrvnugl.js +1 -0
  402. package/dist/standalone/cypher-CieA2-Ow.js +1 -0
  403. package/dist/standalone/d-BeWU-E1F.js +1 -0
  404. package/dist/standalone/dark-plus-t8phC8vs.js +1 -0
  405. package/dist/standalone/dart-CcoouBGm.js +1 -0
  406. package/dist/standalone/dax-DZs9015r.js +1 -0
  407. package/dist/standalone/desktop-Bmf3DIqD.js +1 -0
  408. package/dist/standalone/diff-M4XuV4o4.js +1 -0
  409. package/dist/standalone/docker-Bf7rtiym.js +1 -0
  410. package/dist/standalone/dotenv-BwfGDw9z.js +1 -0
  411. package/dist/standalone/dracula-__iKjS5Q.js +1 -0
  412. package/dist/standalone/dracula-soft-Cn3YGlZ1.js +1 -0
  413. package/dist/standalone/dream-maker-BOxs3X-U.js +1 -0
  414. package/dist/standalone/edge-C7ZV1b67.js +1 -0
  415. package/dist/standalone/elixir-DaqZx2n-.js +1 -0
  416. package/dist/standalone/elm-Dop4RWjQ.js +1 -0
  417. package/dist/standalone/emacs-lisp-C1K9lKmb.js +1 -0
  418. package/dist/standalone/erb-CYhomfpC.js +1 -0
  419. package/dist/standalone/erlang-DX8GMz3W.js +1 -0
  420. package/dist/standalone/everforest-dark-CG-xS58q.js +1 -0
  421. package/dist/standalone/everforest-light-VnlgGH1r.js +1 -0
  422. package/dist/standalone/fennel-Bkp2zi6e.js +1 -0
  423. package/dist/standalone/fish-BSydEoPu.js +1 -0
  424. package/dist/standalone/fluent-CdeSNonp.js +1 -0
  425. package/dist/standalone/fortran-fixed-form-Q6JIc71r.js +1 -0
  426. package/dist/standalone/fortran-free-form-uMmrusbk.js +1 -0
  427. package/dist/standalone/fsharp-BNMUC2iW.js +1 -0
  428. package/dist/standalone/gdresource-BlhCtIAh.js +1 -0
  429. package/dist/standalone/gdscript-CjXE_VF4.js +1 -0
  430. package/dist/standalone/gdshader-D3GnIQMO.js +1 -0
  431. package/dist/standalone/genie-DjRp0HIF.js +1 -0
  432. package/dist/standalone/gherkin-CO-9Xf3O.js +1 -0
  433. package/dist/standalone/git-commit-BmPljjxo.js +1 -0
  434. package/dist/standalone/git-rebase-DVpnhtFe.js +1 -0
  435. package/dist/standalone/github-dark-DW2tvKKC.js +1 -0
  436. package/dist/standalone/github-dark-default-ChsP8enW.js +1 -0
  437. package/dist/standalone/github-dark-dimmed-DbObBUjn.js +1 -0
  438. package/dist/standalone/github-dark-high-contrast-DGQlFkmA.js +1 -0
  439. package/dist/standalone/github-light-JgN_--pJ.js +1 -0
  440. package/dist/standalone/github-light-default-Dyse0wyU.js +1 -0
  441. package/dist/standalone/github-light-high-contrast-CeZ5HGZn.js +1 -0
  442. package/dist/standalone/gleam-BIhdqQ3b.js +1 -0
  443. package/dist/standalone/glimmer-js-BxvF_2QK.js +1 -0
  444. package/dist/standalone/glimmer-ts-AM8hgR0M.js +1 -0
  445. package/dist/standalone/glsl-CeERM8Pu.js +1 -0
  446. package/dist/standalone/gn--jXLY5_N.js +1 -0
  447. package/dist/standalone/gnuplot-Bxet1c3v.js +1 -0
  448. package/dist/standalone/go-s39PxAzr.js +1 -0
  449. package/dist/standalone/graphql-B_NxG4GM.js +1 -0
  450. package/dist/standalone/groovy-CzT_Iae7.js +1 -0
  451. package/dist/standalone/gruvbox-dark-hard-C1uR-pvB.js +1 -0
  452. package/dist/standalone/gruvbox-dark-medium-C_NCtEMg.js +1 -0
  453. package/dist/standalone/gruvbox-dark-soft-DbASFM7u.js +1 -0
  454. package/dist/standalone/gruvbox-light-hard-BisBk_cZ.js +1 -0
  455. package/dist/standalone/gruvbox-light-medium-Cqf5GdYM.js +1 -0
  456. package/dist/standalone/gruvbox-light-soft-R3xWeYua.js +1 -0
  457. package/dist/standalone/hack-DOi7Q-_3.js +1 -0
  458. package/dist/standalone/haml-DqnJGs3l.js +1 -0
  459. package/dist/standalone/handlebars-BD-GTNyX.js +1 -0
  460. package/dist/standalone/haskell-BU_KScmc.js +1 -0
  461. package/dist/standalone/haxe-Dq20ysgQ.js +1 -0
  462. package/dist/standalone/hcl-C-NSNXCz.js +1 -0
  463. package/dist/standalone/hjson-C1slXyS7.js +1 -0
  464. package/dist/standalone/hlsl-nviGPNKF.js +1 -0
  465. package/dist/standalone/horizon-bright-CHsP19AA.js +1 -0
  466. package/dist/standalone/horizon-pIEktodb.js +1 -0
  467. package/dist/standalone/houston-DM-vL-Qy.js +1 -0
  468. package/dist/standalone/html-Dg4T4CWG.js +1 -0
  469. package/dist/standalone/html-derivative-Dy01fcFN.js +1 -0
  470. package/dist/standalone/http-Dcg4Y0LN.js +1 -0
  471. package/dist/standalone/hurl-DLBSghyU.js +1 -0
  472. package/dist/standalone/hxml-CkY94ZZG.js +1 -0
  473. package/dist/standalone/hy-CNbyPkG-.js +1 -0
  474. package/dist/standalone/imba-C3JgZFAu.js +1 -0
  475. package/dist/standalone/index.d.ts +1607 -0
  476. package/dist/standalone/index.js +228 -0
  477. package/dist/standalone/ini-CnSxfbfK.js +1 -0
  478. package/dist/standalone/java-CzlXKE3t.js +1 -0
  479. package/dist/standalone/javascript-FKiEkpOl.js +1 -0
  480. package/dist/standalone/jinja-rn3l-epj.js +1 -0
  481. package/dist/standalone/jison-Bb1qcUQw.js +1 -0
  482. package/dist/standalone/json-E5qb7BD2.js +1 -0
  483. package/dist/standalone/json5-BlfBFW_Y.js +1 -0
  484. package/dist/standalone/jsonc-BRJexVfP.js +1 -0
  485. package/dist/standalone/jsonl-D859SsoH.js +1 -0
  486. package/dist/standalone/jsonnet-sFIVPoTa.js +1 -0
  487. package/dist/standalone/jssm-BKvxMEP9.js +1 -0
  488. package/dist/standalone/jsx-D6UqkuWx.js +1 -0
  489. package/dist/standalone/julia-Bf0YFlEW.js +1 -0
  490. package/dist/standalone/just-CgnMO4TC.js +1 -0
  491. package/dist/standalone/kanagawa-dragon-BVeOdnTJ.js +1 -0
  492. package/dist/standalone/kanagawa-lotus-BPxif4UL.js +1 -0
  493. package/dist/standalone/kanagawa-wave-yTBcVIG8.js +1 -0
  494. package/dist/standalone/kdl-DHueRo8f.js +1 -0
  495. package/dist/standalone/kotlin-Dq1wwHQP.js +1 -0
  496. package/dist/standalone/kusto-SD8_YGH-.js +1 -0
  497. package/dist/standalone/laserwave-x5LKCxGn.js +1 -0
  498. package/dist/standalone/latex-CHYOx8yi.js +1 -0
  499. package/dist/standalone/lean-xWP4NbYS.js +1 -0
  500. package/dist/standalone/less-To8q_Luw.js +1 -0
  501. package/dist/standalone/light-plus-B9N5DQI_.js +1 -0
  502. package/dist/standalone/liquid-BTKRZY_L.js +1 -0
  503. package/dist/standalone/llvm-DSlzjbDz.js +1 -0
  504. package/dist/standalone/log-BUZNzUKc.js +1 -0
  505. package/dist/standalone/logo-Cdvy4UIe.js +1 -0
  506. package/dist/standalone/lua-RcKjZiH7.js +1 -0
  507. package/dist/standalone/luau-ChD5_OIQ.js +1 -0
  508. package/dist/standalone/make-PTqunrmv.js +1 -0
  509. package/dist/standalone/markdown-tUO1dt9m.js +1 -0
  510. package/dist/standalone/marko-CLH1Sh4A.js +1 -0
  511. package/dist/standalone/material-theme-CZJ_pRWf.js +1 -0
  512. package/dist/standalone/material-theme-darker-NnzT0yiw.js +1 -0
  513. package/dist/standalone/material-theme-lighter-B-3DgVxS.js +1 -0
  514. package/dist/standalone/material-theme-ocean-BZZHghfO.js +1 -0
  515. package/dist/standalone/material-theme-palenight-B9O9JO4H.js +1 -0
  516. package/dist/standalone/matlab-Co_sDlVB.js +1 -0
  517. package/dist/standalone/mdc-C1ZFsTbz.js +1 -0
  518. package/dist/standalone/mdx-B4AGJXox.js +1 -0
  519. package/dist/standalone/mermaid-4pySd3sl.js +1 -0
  520. package/dist/standalone/min-dark-DSigFwoh.js +1 -0
  521. package/dist/standalone/min-light-b8nqGai7.js +1 -0
  522. package/dist/standalone/mipsasm-DbZprEQm.js +1 -0
  523. package/dist/standalone/mojo-4kIBaoam.js +1 -0
  524. package/dist/standalone/monokai-BKSWoNnW.js +1 -0
  525. package/dist/standalone/moonbit-ZcSH7VpN.js +1 -0
  526. package/dist/standalone/move-BAMKTWoZ.js +1 -0
  527. package/dist/standalone/narrat-DYLRa4xG.js +1 -0
  528. package/dist/standalone/nextflow-CE20fCoX.js +1 -0
  529. package/dist/standalone/nextflow-groovy-Brjjw2F-.js +1 -0
  530. package/dist/standalone/nginx-CJQ7hxI6.js +1 -0
  531. package/dist/standalone/night-owl-BtS72I6-.js +1 -0
  532. package/dist/standalone/night-owl-light-fbFrWwTF.js +1 -0
  533. package/dist/standalone/nim-Czafvbe4.js +1 -0
  534. package/dist/standalone/nix-DlQjHQ1N.js +1 -0
  535. package/dist/standalone/nord-Kzx5CafP.js +1 -0
  536. package/dist/standalone/nushell-B04C_1w9.js +1 -0
  537. package/dist/standalone/objective-c-DO0dC44X.js +1 -0
  538. package/dist/standalone/objective-cpp-C6jZz_vb.js +1 -0
  539. package/dist/standalone/ocaml-D9wp3EJd.js +1 -0
  540. package/dist/standalone/odin-u5wG6Qd6.js +1 -0
  541. package/dist/standalone/one-dark-pro-x1jnl7Dq.js +1 -0
  542. package/dist/standalone/one-light-CHHJSb-V.js +1 -0
  543. package/dist/standalone/open-CGQvvIUq.js +2 -0
  544. package/dist/standalone/openscad-H5hW1BA6.js +1 -0
  545. package/dist/standalone/pascal-h7T9h7AG.js +1 -0
  546. package/dist/standalone/perl-CChoGwxr.js +1 -0
  547. package/dist/standalone/php-BW6t2_Q8.js +1 -0
  548. package/dist/standalone/pkl-Bs_8OmWM.js +1 -0
  549. package/dist/standalone/plastic-MRrPKBd1.js +1 -0
  550. package/dist/standalone/plsql-BrbZPaQW.js +1 -0
  551. package/dist/standalone/po-Diz0LmjW.js +1 -0
  552. package/dist/standalone/poimandres-D3Al1iQC.js +1 -0
  553. package/dist/standalone/polar-BCwHILUV.js +1 -0
  554. package/dist/standalone/postcss-D6Lmyt9A.js +1 -0
  555. package/dist/standalone/powerquery-CQr4BVoo.js +1 -0
  556. package/dist/standalone/powershell-B6zzL9QO.js +1 -0
  557. package/dist/standalone/prisma-yySgczME.js +1 -0
  558. package/dist/standalone/prolog-CZTkGNiW.js +1 -0
  559. package/dist/standalone/proto-Bu_Nhf7u.js +1 -0
  560. package/dist/standalone/pug-DJ5mt9QE.js +1 -0
  561. package/dist/standalone/puppet-CJffH6Q6.js +1 -0
  562. package/dist/standalone/purescript-CC7chrMM.js +1 -0
  563. package/dist/standalone/python-C0zrVFze.js +1 -0
  564. package/dist/standalone/qml-BxVojlB6.js +1 -0
  565. package/dist/standalone/qmldir-C14XBFWV.js +1 -0
  566. package/dist/standalone/qss-B5Hlft_e.js +1 -0
  567. package/dist/standalone/r-BJ2VNcvN.js +1 -0
  568. package/dist/standalone/racket-BhgskJzI.js +1 -0
  569. package/dist/standalone/raku-DsquHgLn.js +1 -0
  570. package/dist/standalone/razor-Dp2BNdgE.js +1 -0
  571. package/dist/standalone/red-B7RxoP7i.js +1 -0
  572. package/dist/standalone/reg-Bax0jtzQ.js +1 -0
  573. package/dist/standalone/regexp-BTh6yLg7.js +1 -0
  574. package/dist/standalone/rel-BJ9pN2G2.js +1 -0
  575. package/dist/standalone/riscv-C3I2jhjQ.js +1 -0
  576. package/dist/standalone/ron-B4yZ2p1h.js +1 -0
  577. package/dist/standalone/rose-pine-CesGHTut.js +1 -0
  578. package/dist/standalone/rose-pine-dawn-H7rm9o-e.js +1 -0
  579. package/dist/standalone/rose-pine-moon-BLFdnJwM.js +1 -0
  580. package/dist/standalone/rosmsg-xA4_Syxo.js +1 -0
  581. package/dist/standalone/rst-DL5K0SsC.js +1 -0
  582. package/dist/standalone/ruby-DuNhMjQX.js +1 -0
  583. package/dist/standalone/rust-BwguOFUq.js +1 -0
  584. package/dist/standalone/sas-CgrIRNnH.js +1 -0
  585. package/dist/standalone/sass-BZyNoloZ.js +1 -0
  586. package/dist/standalone/scala-CekfPUXd.js +1 -0
  587. package/dist/standalone/scheme-B2vB2nlX.js +1 -0
  588. package/dist/standalone/scss-B5Q0dl0C.js +1 -0
  589. package/dist/standalone/sdbl-B-nwTmgB.js +1 -0
  590. package/dist/standalone/shaderlab-B3x1KxeX.js +1 -0
  591. package/dist/standalone/shellscript-BUzexP2h.js +1 -0
  592. package/dist/standalone/shellsession-iw5paaXf.js +1 -0
  593. package/dist/standalone/slack-dark-DCR6M5dd.js +1 -0
  594. package/dist/standalone/slack-ochin-BxQ3FsyT.js +1 -0
  595. package/dist/standalone/smalltalk-Cy1j24XO.js +1 -0
  596. package/dist/standalone/snazzy-light-CjEw4xHM.js +1 -0
  597. package/dist/standalone/solarized-dark-XlQ9gXgZ.js +1 -0
  598. package/dist/standalone/solarized-light-isNaysOv.js +1 -0
  599. package/dist/standalone/solidity-C1mh1SRB.js +1 -0
  600. package/dist/standalone/soy-BExHEi9F.js +1 -0
  601. package/dist/standalone/sparql-38UvPuKm.js +1 -0
  602. package/dist/standalone/splunk-oM69IznW.js +1 -0
  603. package/dist/standalone/sql-CqT9Lw7j.js +1 -0
  604. package/dist/standalone/ssh-config-B0ZrS7Sv.js +1 -0
  605. package/dist/standalone/stata-XyxwKkW7.js +1 -0
  606. package/dist/standalone/stylus-B9JQrvWs.js +1 -0
  607. package/dist/standalone/surrealql-DeE9iwmm.js +1 -0
  608. package/dist/standalone/svelte-DbSBwS6o.js +1 -0
  609. package/dist/standalone/swift-CaafmPxt.js +1 -0
  610. package/dist/standalone/synthwave-84-BX-qCxDD.js +1 -0
  611. package/dist/standalone/system-verilog-J8T_iWHG.js +1 -0
  612. package/dist/standalone/systemd-DVL5CBpd.js +1 -0
  613. package/dist/standalone/talonscript-JJfmMcGp.js +1 -0
  614. package/dist/standalone/tasl-B1QiBRw2.js +1 -0
  615. package/dist/standalone/tcl-CqllcB5H.js +1 -0
  616. package/dist/standalone/templ-hC3Z5CVs.js +1 -0
  617. package/dist/standalone/terraform-wW0EQRMO.js +1 -0
  618. package/dist/standalone/tex-CpSHM71U.js +1 -0
  619. package/dist/standalone/tokyo-night-D6xJgjfK.js +1 -0
  620. package/dist/standalone/toml-BHrgvSPm.js +1 -0
  621. package/dist/standalone/ts-tags-BiWG72vW.js +1 -0
  622. package/dist/standalone/tsv-D73M_gRj.js +1 -0
  623. package/dist/standalone/tsx-D6awiul2.js +1 -0
  624. package/dist/standalone/turtle-C6QRqIYF.js +1 -0
  625. package/dist/standalone/twig-BGs9c0m6.js +1 -0
  626. package/dist/standalone/typescript-CWgyaUtY.js +1 -0
  627. package/dist/standalone/typespec-oIccER07.js +1 -0
  628. package/dist/standalone/typst-63_R-dcN.js +1 -0
  629. package/dist/standalone/v-DNh9rpzt.js +1 -0
  630. package/dist/standalone/vala-yZrEMgbl.js +1 -0
  631. package/dist/standalone/vb-CWiXLMNc.js +1 -0
  632. package/dist/standalone/verilog-D_YgnmVW.js +1 -0
  633. package/dist/standalone/vesper-BoY7BUOC.js +1 -0
  634. package/dist/standalone/vhdl-CIOMuzRr.js +1 -0
  635. package/dist/standalone/viml-KvIzNj-z.js +1 -0
  636. package/dist/standalone/vitesse-black-BC5aDvua.js +1 -0
  637. package/dist/standalone/vitesse-dark-C_RcLVAX.js +1 -0
  638. package/dist/standalone/vitesse-light-CbYhMxH_.js +1 -0
  639. package/dist/standalone/vue-HXXkW9Ol.js +1 -0
  640. package/dist/standalone/vue-html-DMGAGeUM.js +1 -0
  641. package/dist/standalone/vue-vine-CumWe8dY.js +1 -0
  642. package/dist/standalone/vyper-DQ6Iue0m.js +1 -0
  643. package/dist/standalone/wasm-BVujMf4j.js +1 -0
  644. package/dist/standalone/wasm-CttxofWT.js +1 -0
  645. package/dist/standalone/wenyan-sFr85_Wa.js +1 -0
  646. package/dist/standalone/wgsl-C8sJ4ScM.js +1 -0
  647. package/dist/standalone/wikitext-BHbI4LrJ.js +1 -0
  648. package/dist/standalone/wit-BrulQYCT.js +1 -0
  649. package/dist/standalone/wolfram-CkTHL-YU.js +1 -0
  650. package/dist/standalone/xml-OYKGlsbQ.js +1 -0
  651. package/dist/standalone/xsl-B9zh_nzP.js +1 -0
  652. package/dist/standalone/yaml-C2UdUGt-.js +1 -0
  653. package/dist/standalone/zenscript-CvnF8pfB.js +1 -0
  654. package/dist/standalone/zig-DrTOhqiK.js +1 -0
  655. package/package.json +22 -11
package/dist/lib/index.js CHANGED
@@ -1,5 +1,94 @@
1
- import{deepmerge as e}from"deepmerge-ts";import t from"plur";import n from"pretty-ms";import{YankiConnect as r,defaultYankiConnectOptions as i}from"yanki-connect";import a from"@shikijs/rehype";import{toText as o}from"hast-util-to-text";import s from"rehype-format";import c from"rehype-parse";import l from"rehype-raw";import u from"rehype-stringify";import d from"remark-rehype";import{unified as f}from"unified";import{u as p}from"unist-builder";import{CONTINUE as m,EXIT as h,SKIP as g,visit as _}from"unist-util-visit";import v from"@sindresorhus/fnv1a";import y from"path-browserify-esm";import b from"@sindresorhus/slugify";import{sha256 as x}from"crypto-hash";import S from"@stdlib/assert-is-absolute-path";import C from"slash";import ee from"remark-breaks";import te from"filenamify";import{nanoid as ne}from"nanoid";import re from"remark-denden-ruby";import w from"remark-flexible-markers";import ie from"remark-frontmatter";import ae from"remark-gfm";import oe from"remark-github-beta-blockquote-admonitions";import se from"remark-math";import ce from"remark-parse";import{parse as le,stringify as ue}from"yaml";import{sanitizeUri as de}from"micromark-util-sanitize-uri";function T(e,t){return v(e,{size:t===8?32:64}).toString(16).padStart(t,`0`)}function fe(e){return e.charAt(0).toUpperCase()+e.slice(1)}function E(e,t,n=`...`,r=` `){if(e.length<=t)return e;let i=t-n.length,a=e.split(r);for(;a.length>1&&a.join(r).length>i;)a.pop();return`${a.join(r).slice(0,i)}${n}`}function pe(e){return e.toLowerCase().replaceAll(/[^\da-z]/gi,` `).trim().replaceAll(/ +/g,`-`)}function D(e){if(e!==void 0)return e.trim()===``?void 0:e}function me(e,...t){return he(e,...t)}function he(e,...t){let n=e.reduce((e,n,r)=>`${e}${n}${String(t[r]??``)}`,``).split(/\r?\n/).filter(e=>e.trim()!==``),r=/^(\s+)/.exec(n[0])?.[0]??``,i=RegExp(`^${r}`);return n.map(e=>e.replace(i,``).trimEnd()).join(`
2
- `)}function ge(e,t){let n=e.match(t);if(n?.index===void 0)return[e,void 0];let{index:r}=n;return[e.slice(0,r),e.slice(r)]}const _e=me`
1
+ import { deepmerge } from "deepmerge-ts";
2
+ import plur from "plur";
3
+ import prettyMilliseconds from "pretty-ms";
4
+ import { YankiConnect, defaultYankiConnectOptions } from "yanki-connect";
5
+ import rehypeShiki from "@shikijs/rehype";
6
+ import { toText } from "hast-util-to-text";
7
+ import rehypeFormat from "rehype-format";
8
+ import rehypeParse from "rehype-parse";
9
+ import rehypeRaw from "rehype-raw";
10
+ import rehypeStringify from "rehype-stringify";
11
+ import remarkRehype from "remark-rehype";
12
+ import { unified } from "unified";
13
+ import { u } from "unist-builder";
14
+ import { CONTINUE, EXIT, SKIP, visit } from "unist-util-visit";
15
+ import fnv1a from "@sindresorhus/fnv1a";
16
+ import path from "path-browserify-esm";
17
+ import slugify from "@sindresorhus/slugify";
18
+ import { sha256 } from "crypto-hash";
19
+ import isAbsolutePath from "@stdlib/assert-is-absolute-path";
20
+ import slash from "slash";
21
+ import remarkBreaks from "remark-breaks";
22
+ import { uint8ArrayToBase64 } from "uint8array-extras";
23
+ import filenamify from "filenamify";
24
+ import { nanoid } from "nanoid";
25
+ import remarkRuby from "remark-denden-ruby";
26
+ import remarkFlexibleMarkers from "remark-flexible-markers";
27
+ import remarkFrontmatter from "remark-frontmatter";
28
+ import remarkGfm from "remark-gfm";
29
+ import remarkGithubBetaBlockquoteAdmonitions from "remark-github-beta-blockquote-admonitions";
30
+ import remarkMath from "remark-math";
31
+ import remarkParse from "remark-parse";
32
+ import { parse, stringify } from "yaml";
33
+ import { sanitizeUri } from "micromark-util-sanitize-uri";
34
+
35
+ //#region src/lib/utilities/string.ts
36
+ function getHash(text, length) {
37
+ return fnv1a(text, { size: length === 8 ? 32 : 64 }).toString(16).padStart(length, "0");
38
+ }
39
+ function capitalize(text) {
40
+ return text.charAt(0).toUpperCase() + text.slice(1);
41
+ }
42
+ /**
43
+ * Truncates on word boundary and adds ellipsis. Does not give special treatment
44
+ * to file extensions. If there are no spaces in the text, it will truncate at
45
+ * `maxLength` without respect for word boundaries.
46
+ * @param text Text to truncate
47
+ * @param maxLength Maximum length excluding ellipsis
48
+ * @param truncationString String to append to truncated text. Defaults to '...'
49
+ * @param wordBoundary Character to consider a word boundary. Defaults to a space.
50
+ * @returns Truncated string
51
+ */
52
+ function truncateOnWordBoundary(text, maxLength, truncationString = "...", wordBoundary = " ") {
53
+ if (text.length <= maxLength) return text;
54
+ const maxLengthSafe = maxLength - truncationString.length;
55
+ const words = text.split(wordBoundary);
56
+ while (words.length > 1 && words.join(wordBoundary).length > maxLengthSafe) words.pop();
57
+ return `${words.join(wordBoundary).slice(0, maxLengthSafe)}${truncationString}`;
58
+ }
59
+ function cleanClassName(className) {
60
+ return className.toLowerCase().replaceAll(/[^\da-z]/gi, " ").trim().replaceAll(/ +/g, "-");
61
+ }
62
+ function emptyIsUndefined(text) {
63
+ if (text === void 0) return;
64
+ return text.trim() === "" ? void 0 : text;
65
+ }
66
+ /**
67
+ * Mainly for nice formatting with prettier. But the line wrapping means we have to strip surplus whitespace.
68
+ * @public
69
+ */
70
+ function css(strings, ...values) {
71
+ return trimLeadingIndentation(strings, ...values);
72
+ }
73
+ function trimLeadingIndentation(strings, ...values) {
74
+ const lines = strings.reduce((result, text, i) => `${result}${text}${String(values[i] ?? "")}`, "").split(/\r?\n/).filter((line) => line.trim() !== "");
75
+ const leadingSpace = /^(\s+)/.exec(lines[0])?.[0] ?? "";
76
+ const leadingSpaceRegex = new RegExp(`^${leadingSpace}`);
77
+ return lines.map((line) => line.replace(leadingSpaceRegex, "").trimEnd()).join("\n");
78
+ }
79
+ function splitAtFirstMatch(text, regex) {
80
+ const match = text.match(regex);
81
+ if (match?.index === void 0) return [text, void 0];
82
+ const { index } = match;
83
+ return [text.slice(0, index), text.slice(index)];
84
+ }
85
+
86
+ //#endregion
87
+ //#region src/lib/shared/constants.ts
88
+ /**
89
+ * The default CSS to use for cards. This matches Anki's default. Stored in the Yanki card models and shared across all Yanki-managed notes regardless of namespace.
90
+ */
91
+ const CSS_DEFAULT_STYLE = css`
3
92
  .card {
4
93
  font-family: arial;
5
94
  font-size: 20px;
@@ -7,44 +96,2836 @@ import{deepmerge as e}from"deepmerge-ts";import t from"plur";import n from"prett
7
96
  color: black;
8
97
  background-color: white;
9
98
  }
10
- `,O=`yanki`,k=`(Empty)`,ve=p(`element`,{properties:{},tagName:`p`},[p(`element`,{properties:{},tagName:`em`},[p(`text`,k)])]),A=`Untitled`,ye=[`avif`,`gif`,`ico`,`jpeg`,`jpg`,`png`,`svg`,`tif`,`tiff`,`webp`],be=[`3gp`,`aac`,`avi`,`flac`,`flv`,`m4a`,`mkv`,`mov`,`mp3`,`mp4`,`mpeg`,`mpg`,`oga`,`ogg`,`ogv`,`ogx`,`opus`,`spx`,`swf`,`wav`,`webm`],xe=[`md`,`pdf`],Se=[...be,...ye,...xe],j=typeof window>`u`?typeof process>`u`?`other`:`node`:`browser`;j===`browser`?/windows/i.test(navigator.userAgent)||/mac/i.test(navigator.userAgent)||/linux/i.test(navigator.userAgent)||/ubuntu/i.test(navigator.userAgent):j===`node`&&(process.platform===`win32`||process.platform===`darwin`||process.platform);const M={allFilePaths:[],ankiConnectOptions:i,ankiWeb:!1,basePath:void 0,checkDatabase:!0,cwd:y.process_cwd,dryRun:!1,fetchAdapter:void 0,fileAdapter:void 0,manageFilenames:`off`,maxFilenameLength:60,namespace:`Yanki`,obsidianVault:void 0,resolveUrls:!0,strictLineBreaks:!0,strictMatching:!1,syncMediaAssets:`local`};async function N(){if(j===`node`){let e=await import(`node:fs/promises`);if(e===void 0)throw Error(`Error loading file functions in Node environment`);return{async readFile(t){return e.readFile(t,`utf8`)},async readFileBuffer(t){return e.readFile(t)},async rename(t,n){await e.rename(t,n)},async stat(t){return e.stat(t)},async writeFile(t,n){await e.writeFile(t,n,`utf8`)}}}throw Error(`The "readFile", "readFileBuffer", "rename" , "stat", and "writeFile" function implementations must be provided to the function when running in the browser`)}function P(){return fetch.bind(globalThis)}async function Ce(e,t){try{return await t.stat(e),!0}catch{return!1}}async function we(e,t,n=`metadata`){switch(n){case`content`:return(await x(await t.readFileBuffer(e))).slice(0,16);case`metadata`:{let{mtimeMs:n,size:r}=await t.stat(e),i=`${n??``}${r??``}`;return i===``?we(e,t,`name`):T(i,16)}case`name`:return T(e,16)}}function F(e,t=!1){return Ee(e,t),Te(e)}function Te(e){return e.normalize(`NFC`).trim()}function Ee(e,t=!1){let n=[];e.trim().length===0&&n.push(`Cannot be empty`),e.trim().length>60&&n.push(`Cannot be longer than 60 characters`);let r=[[/:/,`Colon`],[/\u0000/,`Null`],[/\u0001/,`Start of Heading`],[/\u0002/,`Start of Text`],[/\u0003/,`End of Text`],[/\u0004/,`End of Transmission`],[/\u0005/,`Enquiry`],[/\u0006/,`Acknowledge`],[/\u0007/,`Bell`],[/\u0008/,`Backspace`],[/\u0009/,`Horizontal Tab`],[/\u000A/,`Line Feed`],[/\u000B/,`Vertical Tab`],[/\u000C/,`Form Feed`],[/\u000D/,`Carriage Return`],[/\u000E/,`Shift Out`],[/\u000F/,`Shift In`],[/\u0010/,`Data Link Escape`],[/\u0011/,`Device Control 1`],[/\u0012/,`Device Control 2`],[/\u0013/,`Device Control 3`],[/\u0014/,`Device Control 4`],[/\u0015/,`Negative Acknowledge`],[/\u0016/,`Synchronous Idle`],[/\u0017/,`End of Transmission Block`],[/\u0018/,`Cancel`],[/\u0019/,`End of Medium`],[/\u001A/,`Substitute`],[/\u001B/,`Escape`],[/\u001C/,`File Separator`],[/\u001D/,`Group Separator`],[/\u001E/,`Record Separator`],[/\u001F/,`Unit Separator`],[/\u007F/,`Delete`],[/\u0080/,`Padding Character`],[/\u0081/,`High Octet Preset`],[/\u0082/,`Break Permitted Here`],[/\u0083/,`No Break Here`],[/\u0084/,`Index`],[/\u0085/,`Next Line`],[/\u0086/,`Start of Selected Area`],[/\u0087/,`End of Selected Area`],[/\u0088/,`Character Tabulation Set`],[/\u0089/,`Character Tabulation with Justification`],[/\u008A/,`Line Tabulation Set`],[/\u008B/,`Partial Line Forward`],[/\u008C/,`Partial Line Backward`],[/\u008D/,`Reverse Line Feed`],[/\u008E/,`Single Shift Two`],[/\u008F/,`Single Shift Three`],[/\u0090/,`Device Control String`],[/\u0091/,`Private Use One`],[/\u0092/,`Private Use Two`],[/\u0093/,`Set Transmit State`],[/\u0094/,`Cancel Character`],[/\u0095/,`Message Waiting`],[/\u0096/,`Start of Protected Area`],[/\u0097/,`End of Protected Area`],[/\u0098/,`Start of String`],[/\u0099/,`Single Graphic Character Introducer`],[/\u009A/,`Single Character Introducer`],[/\u009B/,`Control Sequence Introducer`],[/\u009C/,`String Terminator`],[/\u009D/,`Operating System Command`],[/\u009E/,`Privacy Message`],[/\u009F/,`Application Program Command`],[/\u00A0/,`Non-breaking Space`],[/\u00AD/,`Soft Hyphen`],[/\u200B/,`Zero-width Space`],[/\u200C/,`Zero-width Non-joiner`],[/\u200D/,`Zero-width Joiner`],[/\u200E/,`Left-to-right Mark`],[/\u200F/,`Right-to-left Mark`],[/\u202A/,`Left-to-right Embedding`],[/\u202B/,`Right-to-left Embedding`],[/\u202C/,`Pop Directional Formatting`],[/\u202D/,`Left-to-right Override`],[/\u202E/,`Right-to-left Override`],[/\uFEFF/,`Byte Order Mark (BOM)`]];t||r.push([/\*/,`Asterisk`]);for(let[t,i]of r){let r=e.match(t);if(r){let e=JSON.stringify(r[0]).slice(1,-1);n.push(`Forbidden character: ${i}: "${e}"`)}}if(n.length>0)throw Error(`Invalid namespace "${e}":\n\t- ${n.join(`
11
- - `)}`)}function De(e){return`yanki-media-${b(Te(e)).replaceAll(/-+/g,`-`)}`}function Oe(e){let t={"application/octet-stream":`mp4`,"application/ogg":`ogx`,"application/pdf":`pdf`,"application/x-shockwave-flash":`swf`,"audio/aac":`aac`,"audio/flac":`flac`,"audio/mp4":`m4a`,"audio/mpeg":`mp3`,"audio/ogg":`ogg`,"audio/opus":`opus`,"audio/wav":`wav`,"audio/webm":`webm`,"audio/x-speex":`spx`,"image/avif":`avif`,"image/gif":`gif`,"image/jpeg":`jpg`,"image/png":`png`,"image/svg+xml":`svg`,"image/tiff":`tif`,"image/vnd.microsoft.icon":`ico`,"image/webp":`webp`,"image/x-icon":`ico`,"text/markdown":`md`,"video/3gpp":`3gp`,"video/flv":`flv`,"video/matroska":`mkv`,"video/mp4":`mp4`,"video/mpeg":`mpg`,"video/msvideo":`avi`,"video/ogg":`ogv`,"video/quicktime":`mov`,"video/webm":`webm`,"video/x-flv":`flv`,"video/x-matroska":`mkv`,"video/x-msvideo":`avi`}[e];if(t!==void 0)return t}function I(e){return S.posix(e)||S.win32(e)}const ke=/^\\\\\?\\.+/;function L(e){if(ke.test(e))return console.warn(`Unsupported extended length path detected: ${e}`),e;let t=C(e),n=y.normalize(t);return t.startsWith(`./`)?`./${n}`:n}function Ae(e,t){let{basePath:n,compoundBase:r=!1,cwd:i}=t;return n!==void 0&&(I(n)||console.warn(`Base path "${n}" is not absolute`),i.startsWith(n)||console.warn(`CWD "${i}" does not start with base path "${n}"`)),I(i)||console.warn(`CWD "${i}" is not absolute`),I(e)?n===void 0||/^[A-Z]:/i.test(e)||!r&&e.startsWith(n)?e:y.join(n,e):y.join(i,e)}function je(e,t){let n=RegExp(`^${t}`,`i`);return e.replace(n,``)}function R(e){let t=y.dirname(e),[n,r]=ge(y.basename(e),/[#?^]/);return[y.join(t,n),r]}function z(e){return R(e)[0]}function Me(e){return R(e).at(1)??``}function Ne(e){return Pe(e)!==``}function Pe(e){return y.extname(z(e))}function Fe(e,t){return Ne(e)?e:Ie(e,t)}function Ie(e,t){let[n,r]=R(e);return`${n}.${t}${r??``}`}function Le(e){try{return decodeURI(e)}catch(t){console.warn(`Error decoding URI text: "${e}"`,t);return}}function B(e){try{let t=new URL(e),n=/^[a-z]:/i,r=/^file:/i;return(r.test(t.protocol)||n.test(t.protocol))&&!r.test(e)?void 0:t}catch{return}}function V(e){return B(e)!==void 0}function Re(e){let t=B(e);return t?.protocol===`file:`?t.pathname:e}function H(e){let t=B(e);if(t===void 0){let t=L(e);return I(t)||t.startsWith(`./`)||t.startsWith(`../`)?`localFilePath`:`localFileName`}return t.protocol===`file:`?`localFileUrl`:t.protocol===`obsidian:`?`obsidianVaultUrl`:t.protocol===`http:`||t.protocol===`https:`?`remoteHttpUrl`:`unsupportedProtocolUrl`}function ze(e,t){if(e===void 0)return;e instanceof Headers||(e=Be(e));let n=(e instanceof Headers?t.map(t=>e.get(t)):t.map(t=>e[t])).filter(e=>e!=null).join(``);if(n!==``)return n}function Be(e){let t={};for(let[n,r]of Object.entries(e))t[n.toLowerCase()]=r;return t}async function Ve(e,t){try{return(await t(e,{method:`HEAD`}))?.status===200}catch{return!1}}async function He(e,t,n=`metadata`){switch(n){case`metadata`:if(t===void 0)return He(e,t,`name`);try{let n=ze((await t(e,{method:`HEAD`}))?.headers,[`content-type`]);if(n===void 0)throw Error(`No content-type header found`);return Oe(n)}catch{return He(e,t,`name`)}case`name`:{let t,n=B(e);if(n===void 0){console.warn(`Could not parse URL: ${e}`);return}let r=n.pathname.split(`.`);return t=r.length>1?r.at(-1):n.search.split(`.`).at(-1),Se.includes(t??``)?t:void 0}}}async function Ue(e,t,n=`metadata`){switch(n){case`content`:return console.warn("`content` hash mode is not yet implemented for URLs"),Ue(e,t,`metadata`);case`metadata`:try{let n=ze((await t(e,{method:`HEAD`}))?.headers,[`etag`,`last-modified`,`content-length`]);if(n===void 0)throw Error(`No headers found`);return T(n,16)}catch{return Ue(e,t,`name`)}case`name`:return T(e,16)}}function We(e){let t=B(e);return t===void 0?void 0:{host:`${t.protocol}//${t.hostname}`,port:Number.parseInt(t.port,10)}}function Ge(e,t){return`${e}:${t}`}async function Ke(e,t){let n=V(e)?await He(e,t):y.extname(e).slice(1);if(!(n===void 0||!Se.includes(n)))return n}async function qe(e,t,n){return V(e)?Ve(e,n):Ce(e,t)}async function Je(e,t,n,r,i){if(!await qe(e,r,i))return;let a=De(t),o=await Ye(e,r,i),s=n===void 0?``:`.${n}`,c;if(c=`${a}-${o}${s}`,c.length>120)throw Error(`Filename too long: ${c}`);return c}async function Ye(e,t,n){return V(e)?Ue(e,n):we(e,t)}const Xe=f().use(function(){return function(e,t){if(t.data.strictLineBreaks===!1){ee()(e);return}return e}}).use(d,{allowDangerousHtml:!0}).use(l).use(function(){return function(e){let t=!1;_(e,(e,n,r)=>{if(r===void 0||n===void 0||e.type!==`element`)return m;if(e.tagName===`pre`&&e.children.length===1&&e.children[0].type===`element`&&e.children[0].tagName===`code`&&Array.isArray(e.children[0].properties.className)&&e.children[0].properties.className.includes(`language-math`)&&(t=!0,r.children.splice(n,1,e.children[0])),e.tagName===`code`&&Array.isArray(e.properties.className)&&e.properties.className.includes(`language-math`)){let n=e.properties.className.includes(`math-display`)||t;t=!1,e.tagName=n?`div`:`span`,e.children=[{type:`text`,value:n?String.raw`\[`:String.raw`\(`},...e.children,{type:`text`,value:n?String.raw`\]`:String.raw`\)`}]}})}}).use(a,{defaultLanguage:`plaintext`,fallbackLanguage:`plaintext`,themes:{dark:`github-dark`,light:`github-light`}}).use(s).use(u),Ze={...M};async function U(t,n){if(t===void 0)return``;let{cssClassNames:r,fetchAdapter:i=P(),fileAdapter:a=await N(),namespace:o,strictLineBreaks:s,syncMediaAssets:c,useEmptyPlaceholder:l}=e(Ze,n??{}),u=await Xe.run(t,{data:{strictLineBreaks:s}}),d=p(`root`,[p(`element`,{properties:{className:r?.map(e=>pe(e))},tagName:`div`},u.children)]),f=[];_(d,`element`,(e,t,n)=>{if(n===void 0||t===void 0||e.tagName!==`img`)return m;if(typeof e.properties.src!=`string`||e.properties?.src?.trim().length===0)return console.warn(`Image has no src`),m;let r,s=H(e.properties.src);switch(s){case`localFilePath`:if(r=Le(e.properties.src),r===void 0)return m;r=z(r);break;case`unsupportedProtocolUrl`:case`localFileName`:console.warn(`Unsupported URL for media asset, treating as link: "${e.properties.src}"`),r=e.properties.src;break;case`obsidianVaultUrl`:case`localFileUrl`:r=e.properties.src;break;case`remoteHttpUrl`:r=e.properties.src;break}f.push(async()=>{let l=await Ke(r,s===`obsidianVaultUrl`?void 0:i),u=l!==void 0&&s!==`unsupportedProtocolUrl`&&s!==`localFileName`&&s!==`obsidianVaultUrl`&&s!==`localFileUrl`,d=(s===`localFilePath`&&c===`local`||s===`remoteHttpUrl`&&c===`remote`||c===`all`)&&u,f=d?await Je(r,o,l,a,i):void 0,m=f??r,h=d&&f!==void 0?`true`:`false`;if(!u||xe.includes(l)){let i=V(m)?m:`${m}${Me(String(e.properties.dataYankiSrcOriginal))}`;n.children.splice(t,1,p(`element`,{properties:{className:[`yanki-media`,`yanki-media-${u?`file`:`unsupported`}`],"data-yanki-alt-text":e.properties.alt,"data-yanki-media-src":r,"data-yanki-media-sync":h,"data-yanki-src":m,"data-yanki-src-original":e.properties.dataYankiSrcOriginal},tagName:`span`},[p(`element`,{properties:{href:i},tagName:`a`},[p(`text`,decodeURI(String(e.properties.dataYankiSrcOriginal)))])]))}else ye.includes(l)?(e.properties.src=m,e.properties.className=[`yanki-media`,`yanki-media-image`],e.properties.dataYankiMediaSrc=r,e.properties.dataYankiMediaSync=h):be.includes(l)&&n.children.splice(t,1,p(`element`,{properties:{className:[`yanki-media`,`yanki-media-audio-video`],"data-yanki-alt-text":e.properties.alt,"data-yanki-media-src":r,"data-yanki-media-sync":h,"data-yanki-src":m,"data-yanki-src-original":e.properties.dataYankiSrcOriginal},tagName:`span`},[p(`text`,`[sound:${m}]`)]))})});for(let e of f)await e();_(d,`element`,(e,t,n)=>{if(n===void 0||t===void 0||e.tagName!==`img`||D(String(e.properties.alt))===void 0)return m;let{alt:r,height:i,width:a}=it(String(e.properties.alt??``));r===void 0?delete e.properties.alt:e.properties.alt=r,i!==void 0&&(e.properties.height=i),a!==void 0&&(e.properties.width=a)});let h=ot(d);if(h&&!l)return``;let g=h?st(d,ve):d;return $e(Xe.stringify(g)).trim()}const Qe=f().use(c,{fragment:!0});function $e(e){return`<!-- This HTML was generated by Yanki, a Markdown to Anki converter. Do not edit directly. -->\n${e}`}function et(e){return o(e)}function tt(e){return et(Qe.parse(e))}function nt(e){return tt(e).split(`
12
- `).map(e=>e.trim()).filter(e=>e.length>0).join(` `)}function W(e){return tt(e).split(`
13
- `).map(e=>e.trim()).find(e=>e.length>0)??``}function rt(e){let t=Qe.parse(e),n=[];return _(t,`element`,e=>{if((e.tagName===`img`||e.tagName===`span`)&&e.properties?.dataYankiMediaSync===`true`){let t=e.properties?.src??e.properties?.dataYankiSrc,r=e.properties?.dataYankiMediaSrc;t!==void 0&&r!==void 0&&typeof t==`string`&&typeof r==`string`&&n.push({filename:t,originalSrc:r})}}),n}function it(e){let t=e.split(`|`),n=D(t.pop()),r=D(t.join(`|`));if(n!==void 0){let{width:e,height:t}=at(n);if(e!==void 0||t!==void 0)return{alt:r,height:t,width:e}}return{alt:e,height:void 0,width:void 0}}function at(e){if(!/^[\dx]+$/.test(e))return{width:void 0,height:void 0};if(!e.includes(`x`)){let t=Number.parseInt(e,10);if(!Number.isNaN(t))return{width:t,height:void 0}}let[t,n]=e.split(`x`).map(e=>Number.parseInt(e,10));return{width:Number.isNaN(t)||t===void 0?void 0:t,height:Number.isNaN(n)||n===void 0?void 0:n}}function ot(e){let t=!1;return _(e,e=>{if(!t){if(e.type===`element`){let n=e,{tagName:r}=n;if([`img`,`video`,`audio`,`iframe`,`object`,`embed`,`canvas`,`svg`,`picture`].includes(r)||o(n).trim())return t=!0,h}if(e.type===`text`&&e.value!==void 0&&e.value.trim()!==``)return t=!0,h}}),!t}function st(e,t){return _(e,`element`,e=>{if(e.tagName===`div`)return e.children.unshift(t),h}),e}const G=[{cardTemplates:[{Back:`{{FrontSide}}
99
+ `;
100
+ /**
101
+ * CSS class to always include in a top-level div wrapper in the card template to allow for custom styling.
102
+ */
103
+ const CSS_DEFAULT_CLASS_NAME = "yanki";
104
+ /**
105
+ * Whether to require changes to notes, models, or decks before invoking an
106
+ * AnkiWeb sync. Seems like a good idea, but this is tricky... because if you
107
+ * change the AnkiWeb flag after doing a sync, and haven't changed any files,
108
+ * you won't end up pushing changes to AnkiWeb, which seems to contradict
109
+ * expectations even though it would be more performant in the typical case.
110
+ *
111
+ * Only applies if the `ankiWeb` flag is true.
112
+ */
113
+ const SYNC_TO_ANKI_WEB_EVEN_IF_UNCHANGED = true;
114
+ /**
115
+ * The maximum length of a namespace. This is used to ensure that the namespace
116
+ * is easy to type in CLI commands and doesn't hog too much semantic space in
117
+ * Media filenames.
118
+ */
119
+ const NOTE_NAMESPACE_MAX_LENGTH = 60;
120
+ /**
121
+ * The default deck to put a card in if the deck deck can not be inferred from
122
+ * the file path, e.g. when the `sync` command is used directly instead of
123
+ * `syncFiles`.
124
+ */
125
+ const NOTE_DEFAULT_DECK_NAME = "Yanki";
126
+ /**
127
+ * Text to show if a note 'Front' field is empty, and content is required for a semantically valid card.
128
+ */
129
+ const NOTE_DEFAULT_EMPTY_TEXT = "(Empty)";
130
+ /**
131
+ * HTML element to use to present `NOTE_DEFAULT_EMPTY_TEXT`.
132
+ * TODO consider hidden span?
133
+ */
134
+ const NOTE_DEFAULT_EMPTY_HAST = u("element", {
135
+ properties: {},
136
+ tagName: "p"
137
+ }, [u("element", {
138
+ properties: {},
139
+ tagName: "em"
140
+ }, [u("text", NOTE_DEFAULT_EMPTY_TEXT)])]);
141
+ const MEDIA_DEFAULT_HASH_MODE_FILE = "metadata";
142
+ const MEDIA_DEFAULT_HASH_MODE_URL = "metadata";
143
+ const MEDIA_INCLUDE_LEGIBLE_FILENAME = false;
144
+ /**
145
+ * How to first attempt to infer the asset type behind a URL.
146
+ *
147
+ * - `metadata`: Fetch the head and hope for a `Content-Type` header.
148
+ * - `name`: Infer the extension from the URL alone, won't work if there's nothing extension-like in the `pathname`.
149
+ */
150
+ const MEDIA_URL_CONTENT_TYPE_MODE = "metadata";
151
+ /**
152
+ * Anki enforces limits on media asset filenames. Older versions allowed up to 255, but it will be 120 moving forward.
153
+ * https://github.com/ankitects/anki/blob/e41c4573d789afe8b020fab5d9d1eede50c3fa3d/rslib/src/sync/media/mod.rs#L20
154
+ */
155
+ const MEDIA_FILENAME_MAX_LENGTH = 120;
156
+ /**
157
+ * Filename to use when a media asset has no name. Will be appended with counter parenthetical as needed.
158
+ */
159
+ const MEDIA_DEFAULT_EMPTY_FILENAME = "Untitled";
160
+ /**
161
+ * Supported image extensions for Anki media assets.
162
+ *
163
+ * Note that while officially "supported", some of these are not universally compatible across Anki platforms.
164
+ *
165
+ * Via https://github.com/ankitects/anki/blob/e41c4573d789afe8b020fab5d9d1eede50c3fa3d/qt/aqt/editor.py#L62
166
+ */
167
+ const MEDIA_SUPPORTED_IMAGE_EXTENSIONS = [
168
+ "avif",
169
+ "gif",
170
+ "ico",
171
+ "jpeg",
172
+ "jpg",
173
+ "png",
174
+ "svg",
175
+ "tif",
176
+ "tiff",
177
+ "webp"
178
+ ];
179
+ /**
180
+ * Supported audio / video extensions for Anki media assets.
181
+ *
182
+ * Note that while officially "supported", some of these are not universally
183
+ * compatible across Anki platforms.
184
+ *
185
+ * Via
186
+ * https://github.com/ankitects/anki/blob/e41c4573d789afe8b020fab5d9d1eede50c3fa3d/qt/aqt/editor.py#L63-L85
187
+ */
188
+ const MEDIA_SUPPORTED_AUDIO_VIDEO_EXTENSIONS = [
189
+ "3gp",
190
+ "aac",
191
+ "avi",
192
+ "flac",
193
+ "flv",
194
+ "m4a",
195
+ "mkv",
196
+ "mov",
197
+ "mp3",
198
+ "mp4",
199
+ "mpeg",
200
+ "mpg",
201
+ "oga",
202
+ "ogg",
203
+ "ogv",
204
+ "ogx",
205
+ "opus",
206
+ "spx",
207
+ "swf",
208
+ "wav",
209
+ "webm"
210
+ ];
211
+ /**
212
+ * Anki seems happy to open PDF files and download markdown files that have been
213
+ * added to the assets folder...
214
+ * https://help.obsidian.md/Files+and+folders/Accepted+file+formats
215
+ */
216
+ const MEDIA_SUPPORTED_FILE_EXTENSIONS = ["md", "pdf"];
217
+ const MEDIA_SUPPORTED_EXTENSIONS = [
218
+ ...MEDIA_SUPPORTED_AUDIO_VIDEO_EXTENSIONS,
219
+ ...MEDIA_SUPPORTED_IMAGE_EXTENSIONS,
220
+ ...MEDIA_SUPPORTED_FILE_EXTENSIONS
221
+ ];
14
222
 
15
- <hr id=answer>
223
+ //#endregion
224
+ //#region src/lib/utilities/platform.ts
225
+ const ENVIRONMENT = typeof window === "undefined" ? typeof process === "undefined" ? "other" : "node" : "browser";
226
+ const PLATFORM = ENVIRONMENT === "browser" ? /windows/i.test(navigator.userAgent) ? "windows" : /mac/i.test(navigator.userAgent) ? "mac" : /linux/i.test(navigator.userAgent) || /ubuntu/i.test(navigator.userAgent) ? "linux" : "other" : ENVIRONMENT === "node" ? process.platform === "win32" ? "windows" : process.platform === "darwin" ? "mac" : process.platform === "linux" ? "linux" : "other" : "other";
16
227
 
17
- {{Back}}`,Front:`{{Front}}`,YankiNamespace:`{{YankiNamespace}}`}],inOrderFields:[`Front`,`Back`,`YankiNamespace`],modelName:`Yanki - Basic`},{cardTemplates:[{Back:`{{cloze:Front}}<br>
18
- {{Back}}`,Front:`{{cloze:Front}}`,YankiNamespace:`{{YankiNamespace}}`}],inOrderFields:[`Front`,`Back`,`YankiNamespace`],isCloze:!0,modelName:`Yanki - Cloze`},{cardTemplates:[{Back:`{{Front}}
228
+ //#endregion
229
+ //#region src/lib/shared/types.ts
230
+ const defaultGlobalOptions = {
231
+ allFilePaths: [],
232
+ ankiConnectOptions: defaultYankiConnectOptions,
233
+ ankiWeb: false,
234
+ basePath: void 0,
235
+ checkDatabase: true,
236
+ cwd: path.process_cwd,
237
+ dryRun: false,
238
+ fetchAdapter: void 0,
239
+ fileAdapter: void 0,
240
+ manageFilenames: "off",
241
+ maxFilenameLength: 60,
242
+ namespace: "Yanki",
243
+ obsidianVault: void 0,
244
+ resolveUrls: true,
245
+ strictLineBreaks: true,
246
+ strictMatching: false,
247
+ syncMediaAssets: "local"
248
+ };
249
+ async function getDefaultFileAdapter() {
250
+ if (ENVIRONMENT === "node") {
251
+ const nodeFs = await import("node:fs/promises");
252
+ if (nodeFs === void 0) throw new Error("Error loading file functions in Node environment");
253
+ return {
254
+ async readFile(filePath) {
255
+ return nodeFs.readFile(filePath, "utf8");
256
+ },
257
+ async readFileBuffer(filePath) {
258
+ return new Uint8Array(await nodeFs.readFile(filePath));
259
+ },
260
+ async rename(oldPath, newPath) {
261
+ await nodeFs.rename(oldPath, newPath);
262
+ },
263
+ async stat(filePath) {
264
+ return nodeFs.stat(filePath);
265
+ },
266
+ async writeFile(filePath, data) {
267
+ await nodeFs.writeFile(filePath, data, "utf8");
268
+ }
269
+ };
270
+ }
271
+ throw new Error("The \"readFile\", \"readFileBuffer\", \"rename\" , \"stat\", and \"writeFile\" function implementations must be provided to the function when running in the browser");
272
+ }
273
+ function getDefaultFetchAdapter() {
274
+ return fetch.bind(globalThis);
275
+ }
276
+
277
+ //#endregion
278
+ //#region src/lib/utilities/file.ts
279
+ async function fileExists(absoluteFilePath, fileAdapter) {
280
+ try {
281
+ await fileAdapter.stat(absoluteFilePath);
282
+ return true;
283
+ } catch {
284
+ return false;
285
+ }
286
+ }
287
+ async function getFileContentHash(absoluteFilePath, fileAdapter, mode = MEDIA_DEFAULT_HASH_MODE_FILE) {
288
+ switch (mode) {
289
+ case "content": return (await sha256(await fileAdapter.readFileBuffer(absoluteFilePath))).slice(0, 16);
290
+ case "metadata": {
291
+ const { mtimeMs, size } = await fileAdapter.stat(absoluteFilePath);
292
+ const stringToHash = `${mtimeMs ?? ""}${size ?? ""}`;
293
+ if (stringToHash === "") return getFileContentHash(absoluteFilePath, fileAdapter, "name");
294
+ return getHash(stringToHash, 16);
295
+ }
296
+ case "name": return getHash(absoluteFilePath, 16);
297
+ }
298
+ }
19
299
 
20
- <hr id=answer>
300
+ //#endregion
301
+ //#region src/lib/utilities/namespace.ts
302
+ /**
303
+ * Convenience
304
+ * @returns sanitized valid namespace
305
+ * @throws {Error} If namespace is invalid
306
+ */
307
+ function validateAndSanitizeNamespace(namespace, allowAsterisk = false) {
308
+ validateNamespace(namespace, allowAsterisk);
309
+ return sanitizeNamespace(namespace);
310
+ }
311
+ /**
312
+ * Used internally before storing and searching
313
+ * @returns sanitized namespace
314
+ */
315
+ function sanitizeNamespace(namespace) {
316
+ return namespace.normalize("NFC").trim();
317
+ }
318
+ /**
319
+ * Used whenever a users is creating data with a namespace they've provided.
320
+ *
321
+ * Note that namespaces are case insensitive!
322
+ *
323
+ * Namespace validation is tricky, because the user has to agree with the system
324
+ * about the letter of the namespace, otherwise there's a risk of data loss. For
325
+ * this reason, validation is strict and throws errors, so that the user can
326
+ * understand and correct their input so that they know the proper form for
327
+ * subsequent uses of the namespace string — especially if they're using the CLI.
328
+ *
329
+ * Silently correcting the namespace would be a bad idea, because the user might
330
+ * not realize that the namespace has been changed, and then they might not be
331
+ * able to find their notes.
332
+ * @throws {Error}
333
+ */
334
+ function validateNamespace(namespace, allowAsterisk = false) {
335
+ const errorMessages = [];
336
+ if (namespace.trim().length === 0) errorMessages.push("Cannot be empty");
337
+ if (namespace.trim().length > NOTE_NAMESPACE_MAX_LENGTH) errorMessages.push(`Cannot be longer than ${NOTE_NAMESPACE_MAX_LENGTH} characters`);
338
+ const forbiddenCharacters = [
339
+ [/:/, "Colon"],
340
+ [/\u0000/, "Null"],
341
+ [/\u0001/, "Start of Heading"],
342
+ [/\u0002/, "Start of Text"],
343
+ [/\u0003/, "End of Text"],
344
+ [/\u0004/, "End of Transmission"],
345
+ [/\u0005/, "Enquiry"],
346
+ [/\u0006/, "Acknowledge"],
347
+ [/\u0007/, "Bell"],
348
+ [/\u0008/, "Backspace"],
349
+ [/\u0009/, "Horizontal Tab"],
350
+ [/\u000A/, "Line Feed"],
351
+ [/\u000B/, "Vertical Tab"],
352
+ [/\u000C/, "Form Feed"],
353
+ [/\u000D/, "Carriage Return"],
354
+ [/\u000E/, "Shift Out"],
355
+ [/\u000F/, "Shift In"],
356
+ [/\u0010/, "Data Link Escape"],
357
+ [/\u0011/, "Device Control 1"],
358
+ [/\u0012/, "Device Control 2"],
359
+ [/\u0013/, "Device Control 3"],
360
+ [/\u0014/, "Device Control 4"],
361
+ [/\u0015/, "Negative Acknowledge"],
362
+ [/\u0016/, "Synchronous Idle"],
363
+ [/\u0017/, "End of Transmission Block"],
364
+ [/\u0018/, "Cancel"],
365
+ [/\u0019/, "End of Medium"],
366
+ [/\u001A/, "Substitute"],
367
+ [/\u001B/, "Escape"],
368
+ [/\u001C/, "File Separator"],
369
+ [/\u001D/, "Group Separator"],
370
+ [/\u001E/, "Record Separator"],
371
+ [/\u001F/, "Unit Separator"],
372
+ [/\u007F/, "Delete"],
373
+ [/\u0080/, "Padding Character"],
374
+ [/\u0081/, "High Octet Preset"],
375
+ [/\u0082/, "Break Permitted Here"],
376
+ [/\u0083/, "No Break Here"],
377
+ [/\u0084/, "Index"],
378
+ [/\u0085/, "Next Line"],
379
+ [/\u0086/, "Start of Selected Area"],
380
+ [/\u0087/, "End of Selected Area"],
381
+ [/\u0088/, "Character Tabulation Set"],
382
+ [/\u0089/, "Character Tabulation with Justification"],
383
+ [/\u008A/, "Line Tabulation Set"],
384
+ [/\u008B/, "Partial Line Forward"],
385
+ [/\u008C/, "Partial Line Backward"],
386
+ [/\u008D/, "Reverse Line Feed"],
387
+ [/\u008E/, "Single Shift Two"],
388
+ [/\u008F/, "Single Shift Three"],
389
+ [/\u0090/, "Device Control String"],
390
+ [/\u0091/, "Private Use One"],
391
+ [/\u0092/, "Private Use Two"],
392
+ [/\u0093/, "Set Transmit State"],
393
+ [/\u0094/, "Cancel Character"],
394
+ [/\u0095/, "Message Waiting"],
395
+ [/\u0096/, "Start of Protected Area"],
396
+ [/\u0097/, "End of Protected Area"],
397
+ [/\u0098/, "Start of String"],
398
+ [/\u0099/, "Single Graphic Character Introducer"],
399
+ [/\u009A/, "Single Character Introducer"],
400
+ [/\u009B/, "Control Sequence Introducer"],
401
+ [/\u009C/, "String Terminator"],
402
+ [/\u009D/, "Operating System Command"],
403
+ [/\u009E/, "Privacy Message"],
404
+ [/\u009F/, "Application Program Command"],
405
+ [/\u00A0/, "Non-breaking Space"],
406
+ [/\u00AD/, "Soft Hyphen"],
407
+ [/\u200B/, "Zero-width Space"],
408
+ [/\u200C/, "Zero-width Non-joiner"],
409
+ [/\u200D/, "Zero-width Joiner"],
410
+ [/\u200E/, "Left-to-right Mark"],
411
+ [/\u200F/, "Right-to-left Mark"],
412
+ [/\u202A/, "Left-to-right Embedding"],
413
+ [/\u202B/, "Right-to-left Embedding"],
414
+ [/\u202C/, "Pop Directional Formatting"],
415
+ [/\u202D/, "Left-to-right Override"],
416
+ [/\u202E/, "Right-to-left Override"],
417
+ [/\uFEFF/, "Byte Order Mark (BOM)"]
418
+ ];
419
+ if (!allowAsterisk) forbiddenCharacters.push([/\*/, "Asterisk"]);
420
+ for (const [regex, description] of forbiddenCharacters) {
421
+ const match = namespace.match(regex);
422
+ if (match) {
423
+ const character = JSON.stringify(match[0]).slice(1, -1);
424
+ errorMessages.push(`Forbidden character: ${description}: "${character}"`);
425
+ }
426
+ }
427
+ if (errorMessages.length > 0) throw new Error(`Invalid namespace "${namespace}":\n\t- ${errorMessages.join("\n - ")}`);
428
+ }
429
+ /**
430
+ * Get sanitized namespace with yanki-media- prefix (for ease of searching)
431
+ */
432
+ function getSlugifiedNamespace(namespace) {
433
+ return `yanki-media-${slugify(sanitizeNamespace(namespace)).replaceAll(/-+/g, "-")}`;
434
+ }
21
435
 
22
- {{type:Back}}`,Front:`{{Front}}
436
+ //#endregion
437
+ //#region src/lib/utilities/mime.ts
438
+ /**
439
+ * Only supports MIMEs for valid Anki media types.
440
+ */
441
+ function getFileExtensionForMimeType(mimeType) {
442
+ const result = {
443
+ "application/octet-stream": "mp4",
444
+ "application/ogg": "ogx",
445
+ "application/pdf": "pdf",
446
+ "application/x-shockwave-flash": "swf",
447
+ "audio/aac": "aac",
448
+ "audio/flac": "flac",
449
+ "audio/mp4": "m4a",
450
+ "audio/mpeg": "mp3",
451
+ "audio/ogg": "ogg",
452
+ "audio/opus": "opus",
453
+ "audio/wav": "wav",
454
+ "audio/webm": "webm",
455
+ "audio/x-speex": "spx",
456
+ "image/avif": "avif",
457
+ "image/gif": "gif",
458
+ "image/jpeg": "jpg",
459
+ "image/png": "png",
460
+ "image/svg+xml": "svg",
461
+ "image/tiff": "tif",
462
+ "image/vnd.microsoft.icon": "ico",
463
+ "image/webp": "webp",
464
+ "image/x-icon": "ico",
465
+ "text/markdown": "md",
466
+ "video/3gpp": "3gp",
467
+ "video/flv": "flv",
468
+ "video/matroska": "mkv",
469
+ "video/mp4": "mp4",
470
+ "video/mpeg": "mpg",
471
+ "video/msvideo": "avi",
472
+ "video/ogg": "ogv",
473
+ "video/quicktime": "mov",
474
+ "video/webm": "webm",
475
+ "video/x-flv": "flv",
476
+ "video/x-matroska": "mkv",
477
+ "video/x-msvideo": "avi"
478
+ }[mimeType];
479
+ if (result === void 0) return;
480
+ return result;
481
+ }
23
482
 
24
- {{type:Back}}`,YankiNamespace:`{{YankiNamespace}}`}],inOrderFields:[`Front`,`Back`,`YankiNamespace`],modelName:`Yanki - Basic (type in the answer)`},{cardTemplates:[{Back:`{{FrontSide}}
483
+ //#endregion
484
+ //#region src/lib/utilities/path.ts
485
+ /**
486
+ * The browserify polyfill doesn't implement win32 absolute path detection...
487
+ * @param filePath Normalized path
488
+ * @returns Whether the path is absolute
489
+ */
490
+ function isAbsolute(filePath) {
491
+ return isAbsolutePath.posix(filePath) || isAbsolutePath.win32(filePath);
492
+ }
493
+ const RE_WINDOWS_EXTENDED_LENGTH_PATH = /^\\\\\?\\.+/;
494
+ /**
495
+ * Converts all paths to cross-platform 'mixed' style with forward slashes.
496
+ * Warns on unsupported Windows extended length paths.
497
+ * @param filePath Path to normalize
498
+ * @returns normalized path
499
+ */
500
+ function normalize(filePath) {
501
+ if (RE_WINDOWS_EXTENDED_LENGTH_PATH.test(filePath)) {
502
+ console.warn(`Unsupported extended length path detected: ${filePath}`);
503
+ return filePath;
504
+ }
505
+ const basicPath = slash(filePath);
506
+ const normalizedPath = path.normalize(basicPath);
507
+ if (basicPath.startsWith("./")) return `./${normalizedPath}`;
508
+ return normalizedPath;
509
+ }
510
+ /**
511
+ * Special handling for `/absolute-path.md` style links in Obsidian
512
+ * and static site generators, where absolute paths are relative to a base path
513
+ * instead of the volume root.
514
+ *
25
515
 
26
- <hr id=answer>
516
+ *
517
+ * Paths starting with Windows drive letters, while technically absolute, are _not_ prepended with the base:
518
+ * - If no base path is provided, paths are resolved relative to the the provided CWD.
519
+ * - If paths are relative, the base paths are ignored and the CWD is used.
520
+ *
521
+ * All path values are normalized and in 'mixed' platform style.
522
+ */
523
+ function resolveWithBasePath(filePath, options) {
524
+ const { basePath, compoundBase = false, cwd } = options;
525
+ if (basePath !== void 0) {
526
+ if (!isAbsolute(basePath)) console.warn(`Base path "${basePath}" is not absolute`);
527
+ if (!cwd.startsWith(basePath)) console.warn(`CWD "${cwd}" does not start with base path "${basePath}"`);
528
+ }
529
+ if (!isAbsolute(cwd)) console.warn(`CWD "${cwd}" is not absolute`);
530
+ if (isAbsolute(filePath)) {
531
+ if (basePath === void 0 || /^[A-Z]:/i.test(filePath) || !compoundBase && filePath.startsWith(basePath)) return filePath;
532
+ return path.join(basePath, filePath);
533
+ }
534
+ return path.join(cwd, filePath);
535
+ }
536
+ function stripBasePath(filePath, basePath) {
537
+ const regex = new RegExp(`^${basePath}`, "i");
538
+ return filePath.replace(regex, "");
539
+ }
540
+ function getBaseAndQueryParts(filePath) {
541
+ const directoryPath = path.dirname(filePath);
542
+ const [base, query] = splitAtFirstMatch(path.basename(filePath), /[#?^]/);
543
+ return [path.join(directoryPath, base), query];
544
+ }
545
+ function getBase(filePath) {
546
+ return getBaseAndQueryParts(filePath)[0];
547
+ }
548
+ function getQuery(filePath) {
549
+ return getBaseAndQueryParts(filePath).at(1) ?? "";
550
+ }
551
+ function hasExtension(filePath) {
552
+ return getExtension(filePath) !== "";
553
+ }
554
+ function getExtension(filePath) {
555
+ return path.extname(getBase(filePath));
556
+ }
557
+ function addExtensionIfMissing(filePath, extension) {
558
+ if (hasExtension(filePath)) return filePath;
559
+ return addExtension(filePath, extension);
560
+ }
561
+ function addExtension(filePath, extension) {
562
+ const [base, query] = getBaseAndQueryParts(filePath);
563
+ return `${base}.${extension}${query ?? ""}`;
564
+ }
27
565
 
28
- {{Back}}{{#Extra}}
566
+ //#endregion
567
+ //#region src/lib/utilities/url.ts
568
+ function safeDecodeURI(text) {
569
+ try {
570
+ return decodeURI(text);
571
+ } catch (error) {
572
+ console.warn(`Error decoding URI text: "${text}"`, error);
573
+ return;
574
+ }
575
+ }
576
+ /**
577
+ * Parse a string into a URL object if parsable, and return undefined otherwise
578
+ * (e.g. if it's a file path) _instead_ of throwing an error like the native URL
579
+ * constructor does.
580
+ */
581
+ function safeParseUrl(text) {
582
+ try {
583
+ const url = new URL(text);
584
+ const driveLetterPattern = /^[a-z]:/i;
585
+ const filePrefixPattern = /^file:/i;
586
+ if ((filePrefixPattern.test(url.protocol) || driveLetterPattern.test(url.protocol)) && !filePrefixPattern.test(text)) return;
587
+ return url;
588
+ } catch {
589
+ return;
590
+ }
591
+ }
592
+ function isUrl(text) {
593
+ return safeParseUrl(text) !== void 0;
594
+ }
595
+ /**
596
+ * Helper to "filter" file URLs into path strings so they're treated
597
+ * correctly in mdastToHtml
598
+ * @todo Need stuff from node's implementation, fileURLToPath?
599
+ */
600
+ function fileUrlToPath(url) {
601
+ const parsedUrl = safeParseUrl(url);
602
+ if (parsedUrl?.protocol === "file:") return parsedUrl.pathname;
603
+ return url;
604
+ }
605
+ function getSrcType(filePathOrUrl) {
606
+ const url = safeParseUrl(filePathOrUrl);
607
+ if (url === void 0) {
608
+ const normalizedPath = normalize(filePathOrUrl);
609
+ if (isAbsolute(normalizedPath) || normalizedPath.startsWith("./") || normalizedPath.startsWith("../")) return "localFilePath";
610
+ return "localFileName";
611
+ }
612
+ if (url.protocol === "file:") return "localFileUrl";
613
+ if (url.protocol === "obsidian:") return "obsidianVaultUrl";
614
+ if (url.protocol === "http:" || url.protocol === "https:") return "remoteHttpUrl";
615
+ return "unsupportedProtocolUrl";
616
+ }
617
+ /**
618
+ * Supports both Header type and Record<string, string> type
619
+ * @param headers Headers object or record from a fetch response
620
+ * @param headerKeys Headers to include in the string
621
+ * @returns a concatenated string of the header contents, suitable for hashing, or undefined if no matching headers are present
622
+ */
623
+ function getHeadersString(headers, headerKeys) {
624
+ if (headers === void 0) return;
625
+ if (!(headers instanceof Headers)) headers = convertKeysToLowercase(headers);
626
+ const headerString = (headers instanceof Headers ? headerKeys.map((key) => headers.get(key)) : headerKeys.map((key) => headers[key])).filter((value) => value !== null && value !== void 0).join("");
627
+ if (headerString === "") return;
628
+ return headerString;
629
+ }
630
+ function convertKeysToLowercase(object) {
631
+ const result = {};
632
+ for (const [key, value] of Object.entries(object)) result[key.toLowerCase()] = value;
633
+ return result;
634
+ }
635
+ async function urlExists(url, fetchAdapter) {
636
+ try {
637
+ return (await fetchAdapter(url, { method: "HEAD" }))?.status === 200;
638
+ } catch {
639
+ return false;
640
+ }
641
+ }
642
+ async function getFileExtensionFromUrl(url, fetchAdapter, mode = MEDIA_URL_CONTENT_TYPE_MODE) {
643
+ switch (mode) {
644
+ case "metadata":
645
+ if (fetchAdapter === void 0) return getFileExtensionFromUrl(url, fetchAdapter, "name");
646
+ try {
647
+ const contentTypeHeaderValue = getHeadersString((await fetchAdapter(url, { method: "HEAD" }))?.headers, ["content-type"]);
648
+ if (contentTypeHeaderValue === void 0) throw new Error("No content-type header found");
649
+ return getFileExtensionForMimeType(contentTypeHeaderValue);
650
+ } catch {
651
+ return getFileExtensionFromUrl(url, fetchAdapter, "name");
652
+ }
653
+ case "name": {
654
+ let extensionInUrl;
655
+ const parsedUrl = safeParseUrl(url);
656
+ if (parsedUrl === void 0) {
657
+ console.warn(`Could not parse URL: ${url}`);
658
+ return;
659
+ }
660
+ const pathnameParts = parsedUrl.pathname.split(".");
661
+ if (pathnameParts.length > 1) extensionInUrl = pathnameParts.at(-1);
662
+ else extensionInUrl = parsedUrl.search.split(".").at(-1);
663
+ if (MEDIA_SUPPORTED_EXTENSIONS.includes(extensionInUrl ?? "")) return extensionInUrl;
664
+ return;
665
+ }
666
+ }
667
+ }
668
+ /**
669
+ * Tradeoffs between content change sensitivity and sync speed / efficiency,
670
+ * especially for remote assets.
671
+ *
672
+ * - `filename`: Use the filename of the media asset, no network required.
673
+ * - `metadata`: Use the metadata of the media asset, either fstat stuff for
674
+ * files, or reading the headers for URLs... requires a network request for
675
+ * remote urls. Falls through to `filename` if not available.
676
+ * - `content`: Actually read the content of the media asset, requires reading
677
+ * the file or fetching the URL. Not yet implemented. Falls through to
678
+ * `metadata` if not available.
679
+ */
680
+ async function getUrlContentHash(url, fetchAdapter, mode = MEDIA_DEFAULT_HASH_MODE_URL) {
681
+ switch (mode) {
682
+ case "content":
683
+ console.warn("`content` hash mode is not yet implemented for URLs");
684
+ return getUrlContentHash(url, fetchAdapter, "metadata");
685
+ case "metadata": try {
686
+ const stringToHash = getHeadersString((await fetchAdapter(url, { method: "HEAD" }))?.headers, [
687
+ "etag",
688
+ "last-modified",
689
+ "content-length"
690
+ ]);
691
+ if (stringToHash === void 0) throw new Error("No headers found");
692
+ return getHash(stringToHash, 16);
693
+ } catch {
694
+ return getUrlContentHash(url, fetchAdapter, "name");
695
+ }
696
+ case "name": return getHash(url, 16);
697
+ }
698
+ }
699
+ function urlToHostAndPort(url) {
700
+ const parsedUrl = safeParseUrl(url);
701
+ return parsedUrl === void 0 ? void 0 : {
702
+ host: `${parsedUrl.protocol}//${parsedUrl.hostname}`,
703
+ port: Number.parseInt(parsedUrl.port, 10)
704
+ };
705
+ }
706
+ function hostAndPortToUrl(host, port) {
707
+ return `${host}:${port}`;
708
+ }
29
709
 
30
- <hr>
710
+ //#endregion
711
+ //#region src/lib/utilities/media.ts
712
+ /**
713
+ * Get the extension of a media file, if it's supported
714
+ * @returns Extension without the `.`, possibly an extra string if no extension is found
715
+ * @todo Check for how it handles query strings
716
+ * @todo Clean up type casting
717
+ */
718
+ async function getAnkiMediaFilenameExtension(pathOrUrl, fetchAdapter) {
719
+ const extensionCandidate = isUrl(pathOrUrl) ? await getFileExtensionFromUrl(pathOrUrl, fetchAdapter) : path.extname(pathOrUrl).slice(1);
720
+ if (extensionCandidate === void 0 || !MEDIA_SUPPORTED_EXTENSIONS.includes(extensionCandidate)) return;
721
+ return extensionCandidate;
722
+ }
723
+ function getLegibleFilename(pathOrUrl, maxLength) {
724
+ let legibleFilename;
725
+ const parsedUrl = safeParseUrl(pathOrUrl);
726
+ if (parsedUrl === void 0) {
727
+ const filePath = pathOrUrl;
728
+ legibleFilename = path.basename(filePath, path.extname(filePath));
729
+ } else legibleFilename = path.basename(parsedUrl.pathname, path.extname(parsedUrl.pathname));
730
+ if (legibleFilename === void 0) throw new Error(`Could not create a legible file name for: ${pathOrUrl}`);
731
+ return truncateOnWordBoundary(slugify(legibleFilename.trim()).replaceAll(/-+/g, "-"), maxLength, "...", "-");
732
+ }
733
+ /**
734
+ * Check if a media asset exists
735
+ */
736
+ async function mediaAssetExists(absolutePathOrUrl, fileAdapter, fetchAdapter) {
737
+ if (isUrl(absolutePathOrUrl)) return urlExists(absolutePathOrUrl, fetchAdapter);
738
+ return fileExists(absolutePathOrUrl, fileAdapter);
739
+ }
740
+ /**
741
+ * Get a safe filename for an Anki media asset
742
+ * Anki truncates long file names... so we crush the complete path down to a hash
743
+ */
744
+ async function getSafeAnkiMediaFilename(absolutePathOrUrl, namespace, fileExtension, fileAdapter, fetchAdapter) {
745
+ if (!await mediaAssetExists(absolutePathOrUrl, fileAdapter, fetchAdapter)) return;
746
+ const safeNamespace = getSlugifiedNamespace(namespace);
747
+ const assetHash = await getContentHash(absolutePathOrUrl, fileAdapter, fetchAdapter);
748
+ const resolvedFileExtension = fileExtension === void 0 ? "" : `.${fileExtension}`;
749
+ let safeFilename;
750
+ if (MEDIA_INCLUDE_LEGIBLE_FILENAME) safeFilename = `${safeNamespace}-${assetHash}-${getLegibleFilename(absolutePathOrUrl, MEDIA_FILENAME_MAX_LENGTH - (safeNamespace.length + 1 + assetHash.length + 1 + resolvedFileExtension.length))}${resolvedFileExtension}`;
751
+ else safeFilename = `${safeNamespace}-${assetHash}${resolvedFileExtension}`;
752
+ if (safeFilename.length > MEDIA_FILENAME_MAX_LENGTH) throw new Error(`Filename too long: ${safeFilename}`);
753
+ return safeFilename;
754
+ }
755
+ async function getContentHash(absolutePathOrUrl, fileAdapter, fetchAdapter) {
756
+ return isUrl(absolutePathOrUrl) ? getUrlContentHash(absolutePathOrUrl, fetchAdapter) : getFileContentHash(absolutePathOrUrl, fileAdapter);
757
+ }
31
758
 
32
- {{Extra}}{{/Extra}}`,Front:`{{Front}}`,YankiNamespace:`{{YankiNamespace}}`},{Back:`{{FrontSide}}
759
+ //#endregion
760
+ //#region src/lib/parse/rehype-mathjax-anki.ts
761
+ /**
762
+ * Non-rendering replacement for the `rehype-mathjax` plugin, which takes output
763
+ * from `remark-math` and wraps it in Anki-specific syntax.
764
+ *
765
+ * See https://docs.ankiweb.net/math.html?#mathjax
766
+ */
767
+ const plugin$3 = function() {
768
+ return function(tree) {
769
+ let fenced = false;
770
+ visit(tree, (node, index, parent) => {
771
+ if (parent === void 0 || index === void 0 || node.type !== "element") return CONTINUE;
772
+ if (node.tagName === "pre" && node.children.length === 1 && node.children[0].type === "element" && node.children[0].tagName === "code" && Array.isArray(node.children[0].properties.className) && node.children[0].properties.className.includes("language-math")) {
773
+ fenced = true;
774
+ parent.children.splice(index, 1, node.children[0]);
775
+ }
776
+ if (node.tagName === "code" && Array.isArray(node.properties.className) && node.properties.className.includes("language-math")) {
777
+ const isBlock = node.properties.className.includes("math-display") || fenced;
778
+ fenced = false;
779
+ node.tagName = isBlock ? "div" : "span";
780
+ node.children = [
781
+ {
782
+ type: "text",
783
+ value: isBlock ? String.raw`\[` : String.raw`\(`
784
+ },
785
+ ...node.children,
786
+ {
787
+ type: "text",
788
+ value: isBlock ? String.raw`\]` : String.raw`\)`
789
+ }
790
+ ];
791
+ }
792
+ });
793
+ };
794
+ };
33
795
 
34
- <hr id=answer>
796
+ //#endregion
797
+ //#region src/lib/parse/remark-conditional-breaks.ts
798
+ const plugin$2 = function() {
799
+ return function(tree, file) {
800
+ if (file.data.strictLineBreaks === false) {
801
+ remarkBreaks()(tree);
802
+ return;
803
+ }
804
+ return tree;
805
+ };
806
+ };
35
807
 
36
- {{Front}}{{#Extra}}
808
+ //#endregion
809
+ //#region src/lib/parse/rehype-utilities.ts
810
+ const processor = unified().use(plugin$2).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(plugin$3).use(rehypeShiki, {
811
+ defaultLanguage: "plaintext",
812
+ fallbackLanguage: "plaintext",
813
+ themes: {
814
+ dark: "github-dark",
815
+ light: "github-light"
816
+ }
817
+ }).use(rehypeFormat).use(rehypeStringify);
818
+ const defaultMdastToHtmlOptions = { ...defaultGlobalOptions };
819
+ async function mdastToHtml(mdast, options) {
820
+ if (mdast === void 0) return "";
821
+ const { cssClassNames, fetchAdapter = getDefaultFetchAdapter(), fileAdapter = await getDefaultFileAdapter(), namespace, strictLineBreaks, syncMediaAssets, useEmptyPlaceholder } = deepmerge(defaultMdastToHtmlOptions, options ?? {});
822
+ const hast = await processor.run(mdast, { data: { strictLineBreaks } });
823
+ const hastWithClass = u("root", [u("element", {
824
+ properties: { className: cssClassNames?.map((name) => cleanClassName(name)) },
825
+ tagName: "div"
826
+ }, hast.children)]);
827
+ const treeMutationPromises = [];
828
+ visit(hastWithClass, "element", (node, index, parent) => {
829
+ if (parent === void 0 || index === void 0 || node.tagName !== "img") return CONTINUE;
830
+ if (typeof node.properties.src !== "string" || node.properties?.src?.trim().length === 0) {
831
+ console.warn("Image has no src");
832
+ return CONTINUE;
833
+ }
834
+ let absolutePathOrUrl;
835
+ const srcType = getSrcType(node.properties.src);
836
+ switch (srcType) {
837
+ case "localFilePath":
838
+ absolutePathOrUrl = safeDecodeURI(node.properties.src);
839
+ if (absolutePathOrUrl === void 0) return CONTINUE;
840
+ absolutePathOrUrl = getBase(absolutePathOrUrl);
841
+ break;
842
+ case "unsupportedProtocolUrl":
843
+ case "localFileName":
844
+ console.warn(`Unsupported URL for media asset, treating as link: "${node.properties.src}"`);
845
+ absolutePathOrUrl = node.properties.src;
846
+ break;
847
+ case "obsidianVaultUrl":
848
+ case "localFileUrl":
849
+ absolutePathOrUrl = node.properties.src;
850
+ break;
851
+ case "remoteHttpUrl":
852
+ absolutePathOrUrl = node.properties.src;
853
+ break;
854
+ }
855
+ treeMutationPromises.push(async () => {
856
+ const extension = await getAnkiMediaFilenameExtension(absolutePathOrUrl, srcType === "obsidianVaultUrl" ? void 0 : fetchAdapter);
857
+ const supportedMedia = extension !== void 0 && srcType !== "unsupportedProtocolUrl" && srcType !== "localFileName" && srcType !== "obsidianVaultUrl" && srcType !== "localFileUrl";
858
+ const yankiSyncMedia = (srcType === "localFilePath" && syncMediaAssets === "local" || srcType === "remoteHttpUrl" && syncMediaAssets === "remote" || syncMediaAssets === "all") && supportedMedia;
859
+ const ankiMediaFilename = yankiSyncMedia ? await getSafeAnkiMediaFilename(absolutePathOrUrl, namespace, extension, fileAdapter, fetchAdapter) : void 0;
860
+ const finalSrc = ankiMediaFilename ?? absolutePathOrUrl;
861
+ const syncEnabled = yankiSyncMedia && ankiMediaFilename !== void 0 ? "true" : "false";
862
+ if (!supportedMedia || MEDIA_SUPPORTED_FILE_EXTENSIONS.includes(extension)) {
863
+ const finalSourceWithQuery = isUrl(finalSrc) ? finalSrc : `${finalSrc}${getQuery(String(node.properties.dataYankiSrcOriginal))}`;
864
+ parent.children.splice(index, 1, u("element", {
865
+ properties: {
866
+ className: ["yanki-media", `yanki-media-${supportedMedia ? "file" : "unsupported"}`],
867
+ "data-yanki-alt-text": node.properties.alt,
868
+ "data-yanki-media-src": absolutePathOrUrl,
869
+ "data-yanki-media-sync": syncEnabled,
870
+ "data-yanki-src": finalSrc,
871
+ "data-yanki-src-original": node.properties.dataYankiSrcOriginal
872
+ },
873
+ tagName: "span"
874
+ }, [u("element", {
875
+ properties: { href: finalSourceWithQuery },
876
+ tagName: "a"
877
+ }, [u("text", decodeURI(String(node.properties.dataYankiSrcOriginal)))])]));
878
+ } else if (MEDIA_SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
879
+ node.properties.src = finalSrc;
880
+ node.properties.className = ["yanki-media", "yanki-media-image"];
881
+ node.properties.dataYankiMediaSrc = absolutePathOrUrl;
882
+ node.properties.dataYankiMediaSync = syncEnabled;
883
+ } else if (MEDIA_SUPPORTED_AUDIO_VIDEO_EXTENSIONS.includes(extension)) parent.children.splice(index, 1, u("element", {
884
+ properties: {
885
+ className: ["yanki-media", "yanki-media-audio-video"],
886
+ "data-yanki-alt-text": node.properties.alt,
887
+ "data-yanki-media-src": absolutePathOrUrl,
888
+ "data-yanki-media-sync": syncEnabled,
889
+ "data-yanki-src": finalSrc,
890
+ "data-yanki-src-original": node.properties.dataYankiSrcOriginal
891
+ },
892
+ tagName: "span"
893
+ }, [u("text", `[sound:${finalSrc}]`)]));
894
+ });
895
+ });
896
+ for (const mutationPromise of treeMutationPromises) await mutationPromise();
897
+ visit(hastWithClass, "element", (node, index, parent) => {
898
+ if (parent === void 0 || index === void 0 || node.tagName !== "img" || emptyIsUndefined(String(node.properties.alt)) === void 0) return CONTINUE;
899
+ const { alt, height, width } = parseDimensionsFromAltText(String(node.properties.alt ?? ""));
900
+ if (alt === void 0) delete node.properties.alt;
901
+ else node.properties.alt = alt;
902
+ if (height !== void 0) node.properties.height = height;
903
+ if (width !== void 0) node.properties.width = width;
904
+ });
905
+ const isEmpty = isVisuallyEmpty(hastWithClass);
906
+ if (isEmpty && !useEmptyPlaceholder) return "";
907
+ const nonEmptyHast = isEmpty ? addFirstChildToFirstDiv(hastWithClass, NOTE_DEFAULT_EMPTY_HAST) : hastWithClass;
908
+ return addBoilerplateComment(processor.stringify(nonEmptyHast)).trim();
909
+ }
910
+ const htmlProcessor = unified().use(rehypeParse, { fragment: true });
911
+ function addBoilerplateComment(html) {
912
+ return `<!-- This HTML was generated by Yanki, a Markdown to Anki converter. Do not edit directly. -->\n${html}`;
913
+ }
914
+ function hastToPlainText(hast) {
915
+ return toText(hast);
916
+ }
917
+ function htmlToPlainText(html) {
918
+ return hastToPlainText(htmlProcessor.parse(html));
919
+ }
920
+ function getAllLinesOfHtmlAsPlainText(html) {
921
+ return htmlToPlainText(html).split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join(" ");
922
+ }
923
+ function getFirstLineOfHtmlAsPlainText(html) {
924
+ return htmlToPlainText(html).split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "";
925
+ }
926
+ function extractMediaFromHtml(html) {
927
+ const hast = htmlProcessor.parse(html);
928
+ const media = [];
929
+ visit(hast, "element", (node) => {
930
+ if ((node.tagName === "img" || node.tagName === "span") && node.properties?.dataYankiMediaSync === "true") {
931
+ const filename = node.properties?.src ?? node.properties?.dataYankiSrc;
932
+ const originalSrc = node.properties?.dataYankiMediaSrc;
933
+ if (filename !== void 0 && originalSrc !== void 0 && typeof filename === "string" && typeof originalSrc === "string") media.push({
934
+ filename,
935
+ originalSrc
936
+ });
937
+ }
938
+ });
939
+ return media;
940
+ }
941
+ function parseDimensionsFromAltText(alt) {
942
+ const altParts = alt.split("|");
943
+ const lastAltPart = emptyIsUndefined(altParts.pop());
944
+ const firstAltPart = emptyIsUndefined(altParts.join("|"));
945
+ if (lastAltPart !== void 0) {
946
+ const { width, height } = parseDimensions(lastAltPart);
947
+ if (width !== void 0 || height !== void 0) return {
948
+ alt: firstAltPart,
949
+ height,
950
+ width
951
+ };
952
+ }
953
+ return {
954
+ alt,
955
+ height: void 0,
956
+ width: void 0
957
+ };
958
+ }
959
+ function parseDimensions(dimensions) {
960
+ if (!/^[\dx]+$/.test(dimensions)) return {
961
+ width: void 0,
962
+ height: void 0
963
+ };
964
+ if (!dimensions.includes("x")) {
965
+ const widthOnly = Number.parseInt(dimensions, 10);
966
+ if (!Number.isNaN(widthOnly)) return {
967
+ width: widthOnly,
968
+ height: void 0
969
+ };
970
+ }
971
+ const [width, height] = dimensions.split("x").map((dim) => Number.parseInt(dim, 10));
972
+ return {
973
+ width: Number.isNaN(width) || width === void 0 ? void 0 : width,
974
+ height: Number.isNaN(height) || height === void 0 ? void 0 : height
975
+ };
976
+ }
977
+ /**
978
+ * Determine if a HAST tree is visually empty.
979
+ * @param tree - The HAST tree to check.
980
+ * @returns - True if the tree is visually empty, otherwise false.
981
+ */
982
+ function isVisuallyEmpty(tree) {
983
+ let hasVisualContent = false;
984
+ visit(tree, (node) => {
985
+ if (hasVisualContent) return;
986
+ if (node.type === "element") {
987
+ const element = node;
988
+ const { tagName } = element;
989
+ if ([
990
+ "img",
991
+ "video",
992
+ "audio",
993
+ "iframe",
994
+ "object",
995
+ "embed",
996
+ "canvas",
997
+ "svg",
998
+ "picture"
999
+ ].includes(tagName)) {
1000
+ hasVisualContent = true;
1001
+ return EXIT;
1002
+ }
1003
+ if (toText(element).trim()) {
1004
+ hasVisualContent = true;
1005
+ return EXIT;
1006
+ }
1007
+ }
1008
+ if (node.type === "text" && node.value !== void 0 && node.value.trim() !== "") {
1009
+ hasVisualContent = true;
1010
+ return EXIT;
1011
+ }
1012
+ });
1013
+ return !hasVisualContent;
1014
+ }
1015
+ /**
1016
+ * Add a first child to the first div element in a HAST tree.
1017
+ * Intended for use with the "div-wrapped" HAST tree generated early in `mdastToHtml`.
1018
+ * @param tree - The HAST tree to modify in place.
1019
+ * @param newChild - The new child node to add.
1020
+ * @returns - The modified-in-place HAST tree.
1021
+ */
1022
+ function addFirstChildToFirstDiv(tree, newChild) {
1023
+ visit(tree, "element", (node) => {
1024
+ if (node.tagName === "div") {
1025
+ node.children.unshift(newChild);
1026
+ return EXIT;
1027
+ }
1028
+ });
1029
+ return tree;
1030
+ }
37
1031
 
38
- <hr>
1032
+ //#endregion
1033
+ //#region src/lib/model/model.ts
1034
+ const yankiModels = [
1035
+ {
1036
+ cardTemplates: [{
1037
+ Back: "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}",
1038
+ Front: "{{Front}}",
1039
+ YankiNamespace: "{{YankiNamespace}}"
1040
+ }],
1041
+ inOrderFields: [
1042
+ "Front",
1043
+ "Back",
1044
+ "YankiNamespace"
1045
+ ],
1046
+ modelName: "Yanki - Basic"
1047
+ },
1048
+ {
1049
+ cardTemplates: [{
1050
+ Back: "{{cloze:Front}}<br>\n{{Back}}",
1051
+ Front: "{{cloze:Front}}",
1052
+ YankiNamespace: "{{YankiNamespace}}"
1053
+ }],
1054
+ inOrderFields: [
1055
+ "Front",
1056
+ "Back",
1057
+ "YankiNamespace"
1058
+ ],
1059
+ isCloze: true,
1060
+ modelName: "Yanki - Cloze"
1061
+ },
1062
+ {
1063
+ cardTemplates: [{
1064
+ Back: "{{Front}}\n\n<hr id=answer>\n\n{{type:Back}}",
1065
+ Front: "{{Front}}\n\n{{type:Back}}",
1066
+ YankiNamespace: "{{YankiNamespace}}"
1067
+ }],
1068
+ inOrderFields: [
1069
+ "Front",
1070
+ "Back",
1071
+ "YankiNamespace"
1072
+ ],
1073
+ modelName: "Yanki - Basic (type in the answer)"
1074
+ },
1075
+ {
1076
+ cardTemplates: [{
1077
+ Back: "{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}{{#Extra}}\n\n<hr>\n\n{{Extra}}{{/Extra}}",
1078
+ Front: "{{Front}}",
1079
+ YankiNamespace: "{{YankiNamespace}}"
1080
+ }, {
1081
+ Back: "{{FrontSide}}\n\n<hr id=answer>\n\n{{Front}}{{#Extra}}\n\n<hr>\n\n{{Extra}}{{/Extra}}",
1082
+ Front: "{{Back}}",
1083
+ YankiNamespace: "{{YankiNamespace}}"
1084
+ }],
1085
+ inOrderFields: [
1086
+ "Front",
1087
+ "Back",
1088
+ "Extra",
1089
+ "YankiNamespace"
1090
+ ],
1091
+ modelName: "Yanki - Basic (and reversed card with extra)"
1092
+ }
1093
+ ];
1094
+ const yankiModelNames = yankiModels.map((model) => model.modelName);
1095
+ const legacyYankiModelNames = ["Yanki - Basic (and reversed card)"];
1096
+
1097
+ //#endregion
1098
+ //#region src/lib/utilities/anki-connect.ts
1099
+ async function deleteNotes(client, notes, dryRun = false) {
1100
+ if (dryRun) return;
1101
+ const noteIds = notes.map((note) => note.noteId).filter((noteId) => noteId !== void 0);
1102
+ await client.note.deleteNotes({ notes: noteIds });
1103
+ }
1104
+ /**
1105
+ * Add a note to Anki.
1106
+ *
1107
+ * Does "just in time" creation of requisite models and decks.
1108
+ *
1109
+ * Duplicates will be created if present in the source. It's up to the user to
1110
+ * manage their Markdown files as they like.
1111
+ * @param client An instance of YankiConnect
1112
+ * @param note The note to add
1113
+ * @param dryRun If true, the note will not be created and an ID of 0 will be returned
1114
+ * @returns The ID of the newly created note in Anki
1115
+ * @throws {Error}
1116
+ */
1117
+ async function addNote(client, note, dryRun, fileAdapter) {
1118
+ if (note.noteId !== void 0) throw new Error("Note already has an ID");
1119
+ if (dryRun) return 0;
1120
+ const newNote = await client.note.addNote({ note: {
1121
+ ...note,
1122
+ options: { allowDuplicate: true }
1123
+ } }).catch(async (error) => {
1124
+ if (error instanceof Error) {
1125
+ if (error.message === `model was not found: ${note.modelName}`) {
1126
+ const model = yankiModels.find((model) => model.modelName === note.modelName);
1127
+ if (model === void 0) throw new Error(`Model not found: ${note.modelName}`);
1128
+ await client.model.createModel(model);
1129
+ return addNote(client, note, dryRun, fileAdapter);
1130
+ }
1131
+ if (error.message === `deck was not found: ${note.deckName}`) {
1132
+ if (note.deckName === "") throw new Error("Deck name is empty");
1133
+ await client.deck.createDeck({ deck: note.deckName });
1134
+ return addNote(client, note, dryRun, fileAdapter);
1135
+ }
1136
+ throw error;
1137
+ } else throw new TypeError("Unknown error");
1138
+ });
1139
+ if (newNote === null) throw new Error("Note creation failed");
1140
+ await uploadMediaForNote(client, note, dryRun, fileAdapter);
1141
+ return newNote;
1142
+ }
1143
+ /**
1144
+ * Updates a note in Anki.
1145
+ * @param client An instance of YankiConnect
1146
+ * @param localNote A note read from a markdown file
1147
+ * @param remoteNote A note loaded from Anki
1148
+ * @returns True if the note was updated, false otherwise.
1149
+ * @throws {Error} If the local note ID or remote note cards are undefined, or if model/deck errors occur.
1150
+ */
1151
+ async function updateNote(client, localNote, remoteNote, dryRun, fileAdapter) {
1152
+ if (localNote.noteId === void 0) throw new Error("Local note ID is undefined");
1153
+ if (remoteNote.cards === void 0) throw new Error("Remote note cards are undefined");
1154
+ let updated = false;
1155
+ if (localNote.deckName !== remoteNote.deckName) {
1156
+ if (localNote.deckName === "") throw new Error("Local deck name is empty");
1157
+ if (!dryRun) await client.deck.changeDeck({
1158
+ cards: remoteNote.cards,
1159
+ deck: localNote.deckName
1160
+ });
1161
+ updated = true;
1162
+ }
1163
+ if (!areTagsEqual(localNote.tags ?? [], remoteNote.tags ?? []) || !areFieldsEqual(localNote.fields, remoteNote.fields) || localNote.modelName !== remoteNote.modelName) {
1164
+ if (!dryRun) {
1165
+ await client.note.updateNoteModel({ note: {
1166
+ ...localNote,
1167
+ id: localNote.noteId,
1168
+ tags: localNote.tags ?? []
1169
+ } }).catch(async (error) => {
1170
+ if (error instanceof Error) {
1171
+ if (error.message === `Model '${localNote.modelName}' not found`) {
1172
+ const model = yankiModels.find((model) => model.modelName === localNote.modelName);
1173
+ if (model === void 0) throw new Error(`Model not found: ${localNote.modelName}`);
1174
+ await client.model.createModel(model);
1175
+ return updateNote(client, localNote, remoteNote, dryRun, fileAdapter);
1176
+ }
1177
+ throw error;
1178
+ } else throw new TypeError("Unknown error");
1179
+ });
1180
+ await uploadMediaForNote(client, localNote, dryRun, fileAdapter);
1181
+ }
1182
+ updated = true;
1183
+ }
1184
+ return updated;
1185
+ }
1186
+ /**
1187
+ * Helper to compare local and remote field contents.
1188
+ * @returns True if the fields are equal, false otherwise.
1189
+ */
1190
+ function areFieldsEqual(localFields, remoteFields) {
1191
+ for (const key of [
1192
+ "Front",
1193
+ "Back",
1194
+ "Extra"
1195
+ ]) if (key in localFields && key in remoteFields) {
1196
+ if (localFields[key].normalize("NFC") !== remoteFields[key].normalize("NFC")) return false;
1197
+ } else if (key in localFields || key in remoteFields) return false;
1198
+ return true;
1199
+ }
1200
+ function areNotesEqual(noteA, noteB, includeId = true) {
1201
+ if (includeId && noteA.noteId !== noteB.noteId) return false;
1202
+ if (noteA.deckName !== noteB.deckName) return false;
1203
+ if (noteA.modelName !== noteB.modelName) return false;
1204
+ if (!areFieldsEqual(noteA.fields, noteB.fields)) return false;
1205
+ if (!areTagsEqual(noteA.tags ?? [], noteB.tags ?? [])) return false;
1206
+ return true;
1207
+ }
1208
+ /**
1209
+ * Helper function to compare two arrays of tags.
1210
+ * Note some nuances around case insensitivity as discussed here:
1211
+ * https://github.com/kitschpatrol/yanki-obsidian/issues/44
1212
+ * Anki will alphabetically sort tags, so we sort as well.
1213
+ * Duplicate tags are ignored in Anki, so we ignore them here:
1214
+ * ['yes', 'yes'] is considered equal to ['yes'].
1215
+ * Tags in different orders are considered equal:
1216
+ * ['yes', 'no'] is considered equal to ['no', 'yes'].
1217
+ * @returns True if the tags are equal, false otherwise.
1218
+ */
1219
+ function areTagsEqual(localTags, remoteTags) {
1220
+ const localTagsSet = new Set(localTags.map((tag) => tag.normalize("NFC").toLowerCase()));
1221
+ const remoteTagsSet = new Set(remoteTags.map((tag) => tag.normalize("NFC").toLowerCase()));
1222
+ return new Set([...localTagsSet, ...remoteTagsSet]).size === remoteTagsSet.size;
1223
+ }
1224
+ /**
1225
+ * Get all notes from Anki that match the model prefix.
1226
+ * @param client An instance of YankiConnect
1227
+ * @param namespace The value of the YankiNamespace field, or search with '*' to get all notes. Defaults to the global default namespace.
1228
+ * @returns An array of YankiNote objects
1229
+ * @throws {Error}
1230
+ */
1231
+ async function getRemoteNotes(client, namespace = defaultGlobalOptions.namespace) {
1232
+ return await getRemoteNotesById(client, await client.note.findNotes({ query: `"YankiNamespace:${namespace}"` }));
1233
+ }
1234
+ /**
1235
+ * Get all data from Anki required to populate the YankiNote type.
1236
+ *
1237
+ * Handles some extra footwork to identify the deck name and validate the model
1238
+ * name. There's no way to get everything we need in one shot from Anki-Connect.
1239
+ *
1240
+ * Undefined elements in the returned array are subsequently used to identify
1241
+ * notes that need to be created.
1242
+ * @param client An instance of YankiConnect
1243
+ * @param noteIds An array of local note IDs to (attempt) to fetch
1244
+ * @returns Array of YankiNote objects, with undefined for notes that could not be found.
1245
+ * @throws {Error} If an unknown model name or multiple decks are found for a note, or if no deck is found.
1246
+ */
1247
+ async function getRemoteNotesById(client, noteIds) {
1248
+ const ankiNotes = await client.note.notesInfo({ notes: noteIds });
1249
+ const yankiNotes = [];
1250
+ if (ankiNotes.every((ankiNote) => ankiNote.noteId === void 0)) return Array.from({ length: ankiNotes.length }).fill(void 0);
1251
+ const allCardIds = ankiNotes.flatMap((note) => note.cards ?? []);
1252
+ const deckToCardMap = await client.deck.getDecks({ cards: allCardIds });
1253
+ const cardIdToDeckMap = /* @__PURE__ */ new Map();
1254
+ for (const [deck, cards] of Object.entries(deckToCardMap)) for (const card of cards) cardIdToDeckMap.set(card, deck);
1255
+ const deckFilteredStatusMap = /* @__PURE__ */ new Map();
1256
+ const unfilteredDeckNoteIdMap = /* @__PURE__ */ new Map();
1257
+ for (const ankiNote of ankiNotes) {
1258
+ if (ankiNote.noteId === void 0) {
1259
+ yankiNotes.push(void 0);
1260
+ continue;
1261
+ }
1262
+ if (![...legacyYankiModelNames, ...yankiModelNames].includes(ankiNote.modelName)) throw new Error(`Unknown model name ${ankiNote.modelName} for note ${ankiNote.noteId}`);
1263
+ const deckNamesForNote = [...new Set(ankiNote.cards.map((card) => cardIdToDeckMap.get(card)))];
1264
+ if (deckNamesForNote.length > 1) {}
1265
+ let deckName = deckNamesForNote.at(0);
1266
+ if (deckName === void 0) throw new Error(`No deck found for cards in note ${ankiNote.noteId}`);
1267
+ if (!deckFilteredStatusMap.has(deckName)) {
1268
+ const deckConfig = await client.deck.getDeckConfig({ deck: deckName });
1269
+ const isFiltered = Boolean(deckConfig.dyn);
1270
+ deckFilteredStatusMap.set(deckName, isFiltered);
1271
+ if (isFiltered) {
1272
+ const allDeckNames = await client.deck.deckNames();
1273
+ for (const localName of allDeckNames) if (!deckFilteredStatusMap.has(localName)) {
1274
+ const deckConfig = await client.deck.getDeckConfig({ deck: localName });
1275
+ deckFilteredStatusMap.set(localName, Boolean(deckConfig.dyn));
1276
+ }
1277
+ const sortedUnfilteredDeckNames = [...deckFilteredStatusMap.entries()].filter(([deck, isFiltered]) => !isFiltered && deck !== "Default").map(([deck]) => deck).sort((a, b) => b.split("::").length - a.split("::").length);
1278
+ sortedUnfilteredDeckNames.push("Default");
1279
+ for (const localDeckName of sortedUnfilteredDeckNames) unfilteredDeckNoteIdMap.set(localDeckName, void 0);
1280
+ }
1281
+ }
1282
+ if (deckFilteredStatusMap.get(deckName)) {
1283
+ const namesToCheck = unfilteredDeckNoteIdMap.keys();
1284
+ let found = false;
1285
+ for (const nameToCheck of namesToCheck) {
1286
+ if (unfilteredDeckNoteIdMap.get(nameToCheck) === void 0) unfilteredDeckNoteIdMap.set(nameToCheck, await client.note.findNotes({ query: `"deck:${nameToCheck}"` }));
1287
+ if (unfilteredDeckNoteIdMap.get(nameToCheck)?.includes(ankiNote.noteId)) {
1288
+ deckName = nameToCheck;
1289
+ found = true;
1290
+ break;
1291
+ }
1292
+ }
1293
+ if (!found) throw new Error(`No matching non-filtered deck found for note ${ankiNote.noteId}`);
1294
+ }
1295
+ yankiNotes.push({
1296
+ cards: ankiNote.cards,
1297
+ deckName,
1298
+ fields: {
1299
+ Back: ankiNote.fields.Back.value ?? "",
1300
+ ...ankiNote.fields.Extra !== void 0 && { Extra: ankiNote.fields.Extra.value ?? "" },
1301
+ Front: ankiNote.fields.Front.value ?? "",
1302
+ YankiNamespace: ankiNote.fields.YankiNamespace.value ?? ""
1303
+ },
1304
+ modelName: ankiNote.modelName,
1305
+ noteId: ankiNote.noteId,
1306
+ tags: ankiNote.tags
1307
+ });
1308
+ }
1309
+ return yankiNotes;
1310
+ }
1311
+ async function deleteOrphanedDecks(client, activeNotes, originalNotes, dryRun) {
1312
+ const activeNoteDeckNames = [...new Set(activeNotes.map((note) => note.deckName))].filter(Boolean);
1313
+ const orphanedDeckNames = [...new Set(originalNotes.map((note) => note.deckName))].filter(Boolean).filter((deckName) => !activeNoteDeckNames.includes(deckName));
1314
+ const orphanedParentDeckNames = [];
1315
+ for (const orphanedDeckName of orphanedDeckNames) {
1316
+ const parts = orphanedDeckName.split("::");
1317
+ if (parts.length === 1) continue;
1318
+ while (parts.length > 1) {
1319
+ parts.pop();
1320
+ const parentDeckName = parts.join("::");
1321
+ if (activeNoteDeckNames.some((deckName) => deckName.startsWith(`${parentDeckName}::`))) break;
1322
+ orphanedParentDeckNames.push(parentDeckName);
1323
+ }
1324
+ }
1325
+ const deckDeletionCandidates = [...new Set([...orphanedDeckNames, ...orphanedParentDeckNames])].sort();
1326
+ const decksToDelete = [];
1327
+ for (const deckName of deckDeletionCandidates) {
1328
+ const deckStatsObject = await client.deck.getDeckStats({ decks: [deckName] });
1329
+ const deckStatsValues = Object.values(deckStatsObject);
1330
+ if (deckStatsValues.length > 1) {
1331
+ console.warn(`Multiple decks found for deck name: ${deckName}`);
1332
+ continue;
1333
+ }
1334
+ const deckStats = deckStatsValues.at(0);
1335
+ if (deckStats === void 0) {
1336
+ console.warn(`Deck not found for deck name: ${deckName}`);
1337
+ continue;
1338
+ }
1339
+ const cardCount = Math.max(deckStats.total_in_deck, deckStats.new_count + deckStats.learn_count + deckStats.review_count);
1340
+ if (dryRun) {
1341
+ const originalCount = originalNotes.filter((note) => note.deckName === deckName).length;
1342
+ const activeCount = activeNotes.filter((note) => note.deckName === deckName).length;
1343
+ if (cardCount === originalCount && activeCount === 0 && !decksToDelete.includes(deckName)) decksToDelete.push(deckName);
1344
+ } else if (cardCount === 0 && !decksToDelete.includes(deckName)) decksToDelete.push(deckName);
1345
+ }
1346
+ if (!dryRun) await client.deck.deleteDecks({
1347
+ cardsToo: true,
1348
+ decks: decksToDelete
1349
+ });
1350
+ return decksToDelete;
1351
+ }
1352
+ /**
1353
+ * Global! Does not respect namespace. You can write namespace checks into your css if you want.
1354
+ */
1355
+ async function updateModelStyle(client, modelName, css, dryRun) {
1356
+ let currentCss;
1357
+ try {
1358
+ const { css } = await client.model.modelStyling({ modelName });
1359
+ currentCss = css;
1360
+ } catch (error) {
1361
+ if (error instanceof Error) {
1362
+ if (error.message === `model was not found: ${modelName}`) {
1363
+ const model = yankiModels.find((model) => model.modelName === modelName);
1364
+ if (model === void 0) throw new Error(`Model not found: ${modelName}`);
1365
+ if (dryRun) return false;
1366
+ await client.model.createModel(model);
1367
+ return updateModelStyle(client, model.modelName, css, dryRun);
1368
+ }
1369
+ throw error;
1370
+ } else throw new TypeError("Unknown error");
1371
+ }
1372
+ if (currentCss !== void 0 && currentCss === css) return false;
1373
+ if (!dryRun) await client.model.updateModelStyling({ model: {
1374
+ css,
1375
+ name: modelName
1376
+ } });
1377
+ return true;
1378
+ }
1379
+ async function getModelStyle(client, modelName = yankiModelNames[0]) {
1380
+ const { css } = await client.model.modelStyling({ modelName });
1381
+ return css;
1382
+ }
1383
+ /**
1384
+ * Upload all media files for a note to Anki.
1385
+ * @returns Original source name of media files uploaded
1386
+ */
1387
+ async function uploadMediaForNote(client, note, dryRun, fileAdapter) {
1388
+ const mediaPaths = extractMediaFromHtml(`${note.fields.Front}\n${note.fields.Back}\n${note.fields.Extra}`);
1389
+ const uploadedMedia = [];
1390
+ for (const { filename, originalSrc } of mediaPaths) if ((await client.media.getMediaFilesNames({ pattern: filename })).length === 0) {
1391
+ if (!dryRun) try {
1392
+ const ankiMediaFilename = await client.media.storeMediaFile(isUrl(originalSrc) ? {
1393
+ deleteExisting: true,
1394
+ filename,
1395
+ url: originalSrc
1396
+ } : {
1397
+ data: uint8ArrayToBase64(await fileAdapter.readFileBuffer(originalSrc)),
1398
+ deleteExisting: true,
1399
+ filename
1400
+ });
1401
+ if (filename !== ankiMediaFilename) console.warn(`Anki media filename mismatch: Expected: "${filename}" -> Received: "${ankiMediaFilename}"`);
1402
+ } catch (error) {
1403
+ console.warn(`Anki could not store media file: "${filename}"\n${String(error)}`);
1404
+ }
1405
+ uploadedMedia.push({
1406
+ filename,
1407
+ originalSrc
1408
+ });
1409
+ }
1410
+ return uploadedMedia;
1411
+ }
1412
+ async function deleteUnusedMedia(client, liveNotes, namespace, dryRun) {
1413
+ if (dryRun) return [];
1414
+ const slugifiedNamespace = getSlugifiedNamespace(namespace);
1415
+ const activeMediaFilenames = [];
1416
+ for (const note of liveNotes) {
1417
+ const mediaPaths = extractMediaFromHtml(`${note.fields.Front}\n${note.fields.Back}\n${note.fields.Extra}`);
1418
+ for (const { filename } of mediaPaths) activeMediaFilenames.push(filename);
1419
+ }
1420
+ const allMediaInNamespace = await client.media.getMediaFilesNames({ pattern: `${slugifiedNamespace}-*` });
1421
+ const deletedMediaFilenames = [];
1422
+ for (const remoteMediaFilename of allMediaInNamespace) if (!activeMediaFilenames.includes(remoteMediaFilename)) {
1423
+ await client.media.deleteMediaFile({ filename: remoteMediaFilename });
1424
+ deletedMediaFilenames.push(remoteMediaFilename);
1425
+ }
1426
+ return deletedMediaFilenames;
1427
+ }
1428
+ /**
1429
+ * Request permission to access Anki through Anki-Connect.
1430
+ * @returns 'ankiUnreachable' if Anki is not open, or 'granted' if everything is copacetic
1431
+ * @throws {Error} If access is denied
1432
+ */
1433
+ async function requestPermission(client) {
1434
+ try {
1435
+ const { permission } = await client.miscellaneous.requestPermission();
1436
+ if (permission === "denied") throw new Error("Permission denied, please add this source to the \"webCorsOriginList\" in the Anki-Connect add-on configuration options.");
1437
+ else return "granted";
1438
+ } catch (error) {
1439
+ if (error instanceof Error && (error.message === "fetch failed" || error.message === "net::ERR_CONNECTION_REFUSED")) return "ankiUnreachable";
1440
+ throw error;
1441
+ }
1442
+ }
1443
+ async function syncToAnkiWeb(client) {
1444
+ try {
1445
+ await client.miscellaneous.sync();
1446
+ } catch {
1447
+ console.warn("Could not sync to AnkiWeb.");
1448
+ }
1449
+ }
1450
+
1451
+ //#endregion
1452
+ //#region src/lib/actions/clean.ts
1453
+ const defaultCleanOptions = { ...defaultGlobalOptions };
1454
+ /**
1455
+ * Deletes all remote notes in Anki associated with the given namespace.
1456
+ *
1457
+ * Use with significant caution. Mostly useful for testing.
1458
+ * @returns The IDs of the notes that were deleted
1459
+ * @throws {Error} If Anki is unreachable or another error occurs during deletion.
1460
+ */
1461
+ async function cleanNotes(options) {
1462
+ const startTime = performance.now();
1463
+ const { ankiConnectOptions, ankiWeb, dryRun, namespace } = deepmerge(defaultCleanOptions, options ?? {});
1464
+ const sanitizedNamespace = validateAndSanitizeNamespace(namespace, true);
1465
+ const client = new YankiConnect(ankiConnectOptions);
1466
+ if (await requestPermission(client) === "ankiUnreachable") throw new Error("Anki is unreachable. Is Anki running?");
1467
+ const remoteNotes = await getRemoteNotes(client, sanitizedNamespace);
1468
+ await deleteNotes(client, remoteNotes, dryRun);
1469
+ const deletedDecks = await deleteOrphanedDecks(client, [], remoteNotes, dryRun);
1470
+ const deletedMedia = await deleteUnusedMedia(client, [], sanitizedNamespace, dryRun);
1471
+ const isChanged = remoteNotes.length > 0 || deletedDecks.length > 0;
1472
+ if (!dryRun && ankiWeb && (isChanged || SYNC_TO_ANKI_WEB_EVEN_IF_UNCHANGED)) await syncToAnkiWeb(client);
1473
+ return {
1474
+ ankiWeb,
1475
+ deletedDecks,
1476
+ deletedMedia,
1477
+ deletedNotes: remoteNotes,
1478
+ dryRun,
1479
+ duration: performance.now() - startTime,
1480
+ namespace: sanitizedNamespace
1481
+ };
1482
+ }
1483
+ function formatCleanResult(result, verbose = false) {
1484
+ const deckCount = result.deletedDecks.length;
1485
+ const noteCount = result.deletedNotes.length;
1486
+ const mediaCount = result.deletedMedia.length;
1487
+ if (deckCount === 0 && noteCount === 0 && mediaCount === 0) return "Nothing to delete";
1488
+ const lines = [`${result.dryRun ? "Will" : "Successfully"} deleted ${noteCount} ${plur("note", noteCount)}, ${deckCount} ${plur("deck", deckCount)}, and ${mediaCount} media ${plur("asset", mediaCount)} from Anki${result.dryRun ? "" : ` in ${prettyMilliseconds(result.duration)}`}.`];
1489
+ if (verbose) {
1490
+ if (noteCount > 0) {
1491
+ lines.push("", result.dryRun ? "Notes to delete:" : "Deleted notes:");
1492
+ for (const note of result.deletedNotes) {
1493
+ const noteFrontText = truncateOnWordBoundary(getFirstLineOfHtmlAsPlainText(note.fields.Front), 50);
1494
+ lines.push(` Note ID ${note.noteId} ${noteFrontText}`);
1495
+ }
1496
+ }
1497
+ if (deckCount > 0) {
1498
+ lines.push("", result.dryRun ? "Decks to delete:" : "Deleted decks:");
1499
+ for (const deck of result.deletedDecks) lines.push(` ${deck}`);
1500
+ }
1501
+ if (mediaCount > 0) {
1502
+ lines.push("", result.dryRun ? "Media assets to delete:" : "Deleted media assets:");
1503
+ for (const asset of result.deletedMedia) lines.push(` ${asset}`);
1504
+ }
1505
+ }
1506
+ return lines.join("\n");
1507
+ }
1508
+
1509
+ //#endregion
1510
+ //#region src/lib/actions/list.ts
1511
+ const defaultListOptions = { ...defaultGlobalOptions };
1512
+ /**
1513
+ * Description List notes currently in Anki...
1514
+ */
1515
+ async function listNotes(options) {
1516
+ const startTime = performance.now();
1517
+ const { ankiConnectOptions, namespace } = deepmerge(defaultListOptions, options ?? {});
1518
+ const sanitizedNamespace = validateAndSanitizeNamespace(namespace, true);
1519
+ const client = new YankiConnect(ankiConnectOptions);
1520
+ if (await requestPermission(client) === "ankiUnreachable") throw new Error("Anki is unreachable. Is Anki running?");
1521
+ const notes = await getRemoteNotes(client, sanitizedNamespace);
1522
+ return {
1523
+ duration: performance.now() - startTime,
1524
+ namespace: sanitizedNamespace,
1525
+ notes
1526
+ };
1527
+ }
1528
+ function formatListResult(result) {
1529
+ if (result.notes.length === 0) return "No notes found.";
1530
+ const lines = [];
1531
+ for (const note of result.notes) {
1532
+ const noteFrontText = truncateOnWordBoundary(getFirstLineOfHtmlAsPlainText(note.fields.Front), 50);
1533
+ lines.push(`Note ID ${note.noteId} ${noteFrontText}`);
1534
+ }
1535
+ return lines.join("\n");
1536
+ }
1537
+
1538
+ //#endregion
1539
+ //#region src/lib/utilities/filenames.ts
1540
+ function getSafeTitleForNote(note, manageFilenames, maxLength) {
1541
+ if (manageFilenames === "off") throw new Error("manageFilenames must not be off");
1542
+ switch (note.modelName) {
1543
+ case "Yanki - Basic":
1544
+ case "Yanki - Basic (and reversed card with extra)":
1545
+ case "Yanki - Basic (type in the answer)": {
1546
+ const cleanFront = emptyIsUndefined(getSafeFilename(note.fields.Front).replace(NOTE_DEFAULT_EMPTY_TEXT, "").replace(MEDIA_DEFAULT_EMPTY_FILENAME, ""));
1547
+ const cleanBack = emptyIsUndefined(getSafeFilename(note.fields.Back).replace(NOTE_DEFAULT_EMPTY_TEXT, "").replace(MEDIA_DEFAULT_EMPTY_FILENAME, ""));
1548
+ switch (manageFilenames) {
1549
+ case "prompt": return getSafeFilename(cleanFront ?? cleanBack ?? "", maxLength);
1550
+ case "response": return getSafeFilename(cleanBack ?? cleanFront ?? "", maxLength);
1551
+ }
1552
+ }
1553
+ case "Yanki - Cloze": {
1554
+ const cleanFront = getAllLinesOfHtmlAsPlainText(note.fields.Front);
1555
+ const textBeforeCloze = emptyIsUndefined(cleanFront.split("{{").at(0) ?? "");
1556
+ const firstClozeText = emptyIsUndefined(/\{\{\w\d*\s?:{0,2}([^:}]+)/.exec(cleanFront)?.at(1));
1557
+ const textAfterCloze = emptyIsUndefined(cleanFront.split("}}").at(1)?.split("{{").at(0) ?? "");
1558
+ switch (manageFilenames) {
1559
+ case "prompt": return getSafeFilename(textBeforeCloze ?? textAfterCloze ?? firstClozeText ?? "", maxLength);
1560
+ case "response": return getSafeFilename(firstClozeText ?? textBeforeCloze ?? textAfterCloze ?? "", maxLength);
1561
+ }
1562
+ }
1563
+ }
1564
+ }
1565
+ /**
1566
+ * Get a safe filename for a media asset
1567
+ * @param text Text to be converted to a safe filename
1568
+ * @param maxLength If undefined, no truncation will take place. If defined, a maximum maximum length of the filename will be enforced.
1569
+ * @returns A safe filename
1570
+ */
1571
+ function getSafeFilename(text, maxLength) {
1572
+ let basicSafeFilename = filenamify(getFirstLineOfHtmlAsPlainText(text).trim(), {
1573
+ maxLength: Number.MAX_SAFE_INTEGER,
1574
+ replacement: " "
1575
+ }).replaceAll(/\s+/g, " ").trim();
1576
+ if (basicSafeFilename.length === 0) basicSafeFilename = MEDIA_DEFAULT_EMPTY_FILENAME;
1577
+ basicSafeFilename = basicSafeFilename.normalize("NFC");
1578
+ if (maxLength === void 0) return basicSafeFilename;
1579
+ const safeMaxLength = Math.min(maxLength, MEDIA_FILENAME_MAX_LENGTH - 12);
1580
+ return truncateOnWordBoundary(basicSafeFilename, safeMaxLength);
1581
+ }
1582
+ function getUniqueFilePath(filePath, existingFilenames) {
1583
+ let newFilePath = appendFilenameIncrement(filePath, 1);
1584
+ let increment = 2;
1585
+ while (existingFilenames.includes(newFilePath.toLowerCase())) {
1586
+ newFilePath = appendFilenameIncrement(filePath, increment);
1587
+ increment++;
1588
+ }
1589
+ return newFilePath;
1590
+ }
1591
+ function auditUniqueFilePath(filePath, existingFilenames) {
1592
+ const testPath = appendFilenameIncrement(filePath, 2);
1593
+ if (existingFilenames.includes(testPath.toLowerCase())) return filePath;
1594
+ return stripFilenameIncrement(filePath);
1595
+ }
1596
+ /**
1597
+ * Strip the trailing increment from a filename
1598
+ * @param filename File name with or without an extension, and possibly with a (1)
1599
+ * @returns filename without the increment
1600
+ */
1601
+ function stripFilenameIncrement(filename) {
1602
+ const validExtension = filename.endsWith(".") || filename.endsWith(")") ? void 0 : path.extname(filename);
1603
+ const strippedBaseNameWithoutExtension = path.basename(filename, validExtension).replace(/\s\(\d+\)$/, "");
1604
+ return path.join(path.dirname(filename), `${strippedBaseNameWithoutExtension}${validExtension ?? ""}`);
1605
+ }
1606
+ function appendFilenameIncrement(filename, value) {
1607
+ const extension = path.extname(filename);
1608
+ const baseNameWithIncrement = `${stripFilenameIncrement(path.basename(filename, extension))} (${value})`;
1609
+ return path.join(path.dirname(filename), `${baseNameWithIncrement}${extension}`);
1610
+ }
1611
+ function getTemporarilyUniqueFilePath(filePath) {
1612
+ return `${filePath}-${nanoid(8)}`;
1613
+ }
1614
+
1615
+ //#endregion
1616
+ //#region src/lib/utilities/resolve-link.ts
1617
+ const defaultResolveLinkOptions = {
1618
+ allFilePaths: [],
1619
+ basePath: void 0,
1620
+ convertFilePathsToProtocol: "none",
1621
+ obsidianVaultName: void 0
1622
+ };
1623
+ /**
1624
+ * Resolve a file path, URL, or wiki-style named links to an absolute path.
1625
+ *
1626
+ * Warning:
1627
+ * Wiki name link resolution is CASE INSENSITIVE, like in Obsidian, though
1628
+ * the case of the matching file will be preserved in the returned path.
1629
+ * @param filePathOrUrl May be one of:
1630
+ * - Wiki named link
1631
+ * - Relative file path
1632
+ * - Bare file path
1633
+ * - Absolute file path
1634
+ * - HTTP protocol URL string
1635
+ * - File protocol URL string
1636
+ * - Obsidian protocol URL string
1637
+ * (All file paths can be Windows or POSIX, with or without URI encoding, with
1638
+ * or without funky Obsidian-style post-extension block and heading anchor
1639
+ * additions.)
1640
+ * @returns Resolved absolute path or URL One of:
1641
+ * - Resolved absolute POSIX-style paths
1642
+ * - Removes any file path query parameters
1643
+ * - Not URI-encoded
1644
+ * - Retains original case
1645
+ * - HTTP protocol URL
1646
+ * - Obsidian protocol vault URL (Optionally, this will include file query parameters)
1647
+ */
1648
+ function resolveLink(filePathOrUrl, options) {
1649
+ const { allFilePaths, basePath, convertFilePathsToProtocol, cwd, obsidianVaultName, type } = deepmerge(defaultResolveLinkOptions, options ?? {});
1650
+ if (convertFilePathsToProtocol === "obsidian" && obsidianVaultName === void 0) console.warn(`convertFilePathsToProtocol is 'obsidian', but no obsidianVaultName provided`);
1651
+ const decodedUrl = safeDecodeURI(filePathOrUrl) ?? filePathOrUrl;
1652
+ switch (getSrcType(decodedUrl)) {
1653
+ case "localFileName": {
1654
+ let resolvedUrl = addExtensionIfMissing(normalize(decodedUrl), "md");
1655
+ resolvedUrl = resolveNameLink(resolvedUrl, cwd, allFilePaths ?? []) ?? resolveWithBasePath(decodedUrl, {
1656
+ basePath,
1657
+ cwd
1658
+ });
1659
+ if (getSrcType(resolvedUrl) === "localFilePath") return resolveLink(resolvedUrl, {
1660
+ allFilePaths,
1661
+ basePath,
1662
+ convertFilePathsToProtocol,
1663
+ cwd,
1664
+ obsidianVaultName,
1665
+ type
1666
+ });
1667
+ console.warn(`Failed to convert local file wiki-style name to path: ${filePathOrUrl} --> ${resolvedUrl}`);
1668
+ return resolvedUrl;
1669
+ }
1670
+ case "localFilePath": {
1671
+ const resolvedUrl = resolveWithBasePath(normalize(decodedUrl), {
1672
+ basePath,
1673
+ cwd
1674
+ });
1675
+ const resolvedUrlWithMatchedExtension = pathExistsInAllFiles(addExtensionIfMissing(resolvedUrl, "md"), allFilePaths ?? []) ?? pathExistsInAllFiles(addExtension(resolvedUrl, "md"), allFilePaths ?? []) ?? void 0;
1676
+ if (resolvedUrlWithMatchedExtension !== void 0) {
1677
+ if (convertFilePathsToProtocol !== "none" && (type === "link" || type === "embed" && [".md", ".pdf"].includes(getExtension(resolvedUrlWithMatchedExtension)))) {
1678
+ if (convertFilePathsToProtocol === "obsidian" && obsidianVaultName !== void 0) return createObsidianVaultLink(resolvedUrlWithMatchedExtension, basePath ?? "", obsidianVaultName);
1679
+ if (convertFilePathsToProtocol === "file") return createFileLink(resolvedUrlWithMatchedExtension);
1680
+ }
1681
+ return getBase(resolvedUrlWithMatchedExtension);
1682
+ }
1683
+ return getBase(resolvedUrl);
1684
+ }
1685
+ case "localFileUrl": {
1686
+ const resolvedUrl = normalize(fileUrlToPath(filePathOrUrl));
1687
+ if (getSrcType(resolvedUrl) === "localFilePath") return resolveLink(resolvedUrl, {
1688
+ allFilePaths,
1689
+ basePath,
1690
+ convertFilePathsToProtocol,
1691
+ cwd,
1692
+ obsidianVaultName,
1693
+ type
1694
+ });
1695
+ console.warn(`Failed to convert file URL to path: ${filePathOrUrl} --> ${resolvedUrl}`);
1696
+ return resolvedUrl;
1697
+ }
1698
+ case "obsidianVaultUrl": return filePathOrUrl;
1699
+ case "remoteHttpUrl": return filePathOrUrl;
1700
+ case "unsupportedProtocolUrl":
1701
+ console.warn(`Unsupported URL protocol: ${filePathOrUrl}`);
1702
+ return filePathOrUrl;
1703
+ }
1704
+ }
1705
+ /**
1706
+ * Convert from a (usually wiki-style) named link to an absolute path to an
1707
+ * actual file to match Obsidian's undocumented link resolution algorithm.
1708
+ *
1709
+ * See Obsidian's `getFirstLinkpathDest()` for a roughly equivalent algorithm.
1710
+ *
1711
+ * Obsidian seems to treat note links slightly differently from image / asset links.
1712
+ * @param name Non-URI-encoded name of the file, may have file extension, if no
1713
+ * match with a non-.md extension is found, a match will be attempted with .md
1714
+ * regardless. (POSIX-style paths.)
1715
+ * @param cwd Absolute path to the current working directory of the file from
1716
+ * which we're resolving the link. (POSIX-style paths)
1717
+ * @param allFilePaths Array of absolute paths to all other files in the paths
1718
+ * to be considered. (POSIX-style paths.)
1719
+ * @returns Absolute path to the best matching file with the name provided, or
1720
+ * undefined if there's no valid match. (POSIX-style paths.)
1721
+ */
1722
+ function resolveNameLink(name, cwd, allFilePaths) {
1723
+ if (allFilePaths.length === 0) return;
1724
+ const [base, query] = getBaseAndQueryParts(name);
1725
+ const baseWithoutMd = base.replace(/\.md$/, "").toLowerCase();
1726
+ const pathsToName = allFilePaths.filter((filePath) => {
1727
+ return filePath.replace(/\.md$/, "").toLowerCase().endsWith(baseWithoutMd);
1728
+ });
1729
+ if (pathsToName.length === 0) return;
1730
+ if (pathsToName.length === 1) return `${pathsToName[0]}${query ?? ""}`;
1731
+ return `${[...pathsToName].sort((a, b) => {
1732
+ if (!base.endsWith(".md") || base.includes(path.sep)) {
1733
+ const aHasCwd = a.startsWith(cwd);
1734
+ if (aHasCwd !== b.startsWith(cwd)) return aHasCwd ? -1 : 1;
1735
+ }
1736
+ const aDepth = a.split(path.sep).length;
1737
+ const bDepth = b.split(path.sep).length;
1738
+ if (aDepth !== bDepth) return aDepth - bDepth;
1739
+ return a.localeCompare(b);
1740
+ })[0]}${query ?? ""}`;
1741
+ }
1742
+ /**
1743
+ * Check for presence of a path in a list in a case- and query- agnostic manner.
1744
+ * Ignores .md extensions to simplify matching files
1745
+ * @param filePath File path with file extension. (POSIX-style path.)
1746
+ * @param allFilePaths Array of absolute file paths to check. (POSIX-style paths.)
1747
+ * @returns The file path if it is present in the list of all file paths, or
1748
+ * undefined if it is not.
1749
+ */
1750
+ function pathExistsInAllFiles(filePath, allFilePaths) {
1751
+ const base = getBase(filePath);
1752
+ if (allFilePaths.some((file) => file.toLowerCase().endsWith(base.toLowerCase()))) return filePath;
1753
+ }
1754
+ function createFileLink(absolutePath) {
1755
+ return `file://${absolutePath}`;
1756
+ }
1757
+ function createObsidianVaultLink(absolutePath, basePath, obsidianVault) {
1758
+ const relativePath = stripBasePath(absolutePath, basePath);
1759
+ return `obsidian://open?vault=${encodeURIComponent(obsidianVault)}&file=${encodeURIComponent(relativePath)}`;
1760
+ }
1761
+
1762
+ //#endregion
1763
+ //#region src/lib/parse/remark-resolve-links.ts
1764
+ const plugin$1 = function(options) {
1765
+ const { allFilePaths = [], basePath, cwd, enabled = true, obsidianVault } = options;
1766
+ return function(tree) {
1767
+ if (!enabled) return;
1768
+ visit(tree, "link", (node) => {
1769
+ node.data ??= {};
1770
+ node.data.hProperties = {
1771
+ ...node.data?.hProperties,
1772
+ "data-yanki-src-original": node.url
1773
+ };
1774
+ const resolvedLink = resolveLink(node.url, {
1775
+ allFilePaths,
1776
+ basePath,
1777
+ convertFilePathsToProtocol: obsidianVault === void 0 ? "none" : "obsidian",
1778
+ cwd,
1779
+ obsidianVaultName: obsidianVault,
1780
+ type: "link"
1781
+ });
1782
+ node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink);
1783
+ });
1784
+ visit(tree, "image", (node) => {
1785
+ node.data ??= {};
1786
+ node.data.hProperties = {
1787
+ ...node.data?.hProperties,
1788
+ "data-yanki-src-original": node.url
1789
+ };
1790
+ const resolvedLink = resolveLink(node.url, {
1791
+ allFilePaths,
1792
+ basePath,
1793
+ convertFilePathsToProtocol: obsidianVault === void 0 ? "none" : "obsidian",
1794
+ cwd,
1795
+ obsidianVaultName: obsidianVault,
1796
+ type: "embed"
1797
+ });
1798
+ node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink);
1799
+ });
1800
+ };
1801
+ };
1802
+
1803
+ //#endregion
1804
+ //#region src/lib/parse/wiki-basic/mdast-util-wiki-basic.ts
1805
+ function wikiBasicFromMarkdown() {
1806
+ let url = "";
1807
+ let label;
1808
+ return {
1809
+ enter: {
1810
+ wikiEmbed: enterWikiEmbed,
1811
+ wikiLabel: enterWikiLabel,
1812
+ wikiLink: enterWikiLink,
1813
+ wikiUrl: enterWikiUrl
1814
+ },
1815
+ exit: {
1816
+ wikiEmbed: exitWikiEmbed,
1817
+ wikiLabel: exitWikiLabel,
1818
+ wikiLink: exitWikiLink,
1819
+ wikiUrl: exitWikiUrl
1820
+ }
1821
+ };
1822
+ function enterWikiLink(token) {
1823
+ url = "";
1824
+ label = void 0;
1825
+ this.enter({
1826
+ children: [],
1827
+ title: void 0,
1828
+ type: "link",
1829
+ url: ""
1830
+ }, token);
1831
+ }
1832
+ function enterWikiEmbed(token) {
1833
+ url = "";
1834
+ label = void 0;
1835
+ this.enter({
1836
+ type: "image",
1837
+ url: ""
1838
+ }, token);
1839
+ }
1840
+ function enterWikiUrl() {
1841
+ this.buffer();
1842
+ }
1843
+ function exitWikiUrl() {
1844
+ url = this.resume();
1845
+ }
1846
+ function enterWikiLabel() {
1847
+ this.buffer();
1848
+ }
1849
+ function exitWikiLabel() {
1850
+ label = this.resume();
1851
+ }
1852
+ function exitWikiLink(token) {
1853
+ const currentNode = this.stack.at(-1);
1854
+ currentNode.url = sanitizeUri(url);
1855
+ currentNode.children = [{
1856
+ type: "text",
1857
+ value: emptyIsUndefined((label ?? "").replaceAll("|", "")) ?? url.split("#").pop() ?? url.split("/").pop() ?? url
1858
+ }];
1859
+ this.exit(token);
1860
+ }
1861
+ function exitWikiEmbed(token) {
1862
+ const currentNode = this.stack.at(-1);
1863
+ currentNode.url = sanitizeUri(url);
1864
+ if (label !== void 0) currentNode.alt = label;
1865
+ this.exit(token);
1866
+ }
1867
+ }
1868
+
1869
+ //#endregion
1870
+ //#region src/lib/parse/wiki-basic/micromark-extension-wiki-basic.ts
1871
+ function wikiBasic() {
1872
+ return { text: {
1873
+ 33: {
1874
+ name: "wikiEmbed",
1875
+ tokenize: tokenizeWikiRef
1876
+ },
1877
+ 91: {
1878
+ name: "wikiLink",
1879
+ tokenize: tokenizeWikiRef
1880
+ }
1881
+ } };
1882
+ function tokenizeWikiRef(effects, ok, nok) {
1883
+ let isEmbed = false;
1884
+ let inLabel = false;
1885
+ let linkLength = 0;
1886
+ let labelLength = 0;
1887
+ return bangOrBracket;
1888
+ function bangOrBracket(code) {
1889
+ if (code === 33) {
1890
+ isEmbed = true;
1891
+ effects.enter("wikiEmbed");
1892
+ effects.enter("wikiMarker");
1893
+ effects.consume(code);
1894
+ return firstOpeningMarker;
1895
+ }
1896
+ effects.enter("wikiLink");
1897
+ effects.enter("wikiMarker");
1898
+ return firstOpeningMarker(code);
1899
+ }
1900
+ function firstOpeningMarker(code) {
1901
+ if (code === 91) {
1902
+ effects.consume(code);
1903
+ return secondOpeningMarker;
1904
+ }
1905
+ return nok(code);
1906
+ }
1907
+ function secondOpeningMarker(code) {
1908
+ if (code === 91) {
1909
+ effects.consume(code);
1910
+ effects.exit("wikiMarker");
1911
+ return startUrl;
1912
+ }
1913
+ return nok(code);
1914
+ }
1915
+ function startUrl(code) {
1916
+ if (code === -5 || code === -4 || code === -3 || code === null) return nok(code);
1917
+ if (code === 124) return nok(code);
1918
+ if (code === 93) return nok(code);
1919
+ effects.enter("wikiUrl");
1920
+ effects.enter("chunkString", { contentType: "string" });
1921
+ effects.consume(code);
1922
+ linkLength++;
1923
+ return insideUrl;
1924
+ }
1925
+ function insideUrl(code) {
1926
+ if (code === -5 || code === -4 || code === -3 || code === null) return nok(code);
1927
+ if (code === 124) {
1928
+ if (linkLength === 1) return nok(code);
1929
+ return transitionToLabel(code);
1930
+ }
1931
+ if (code === 92) return effects.check({
1932
+ partial: true,
1933
+ tokenize: backslashPipeLookahead
1934
+ }, transitionToLabel, normalUrlChar)(code);
1935
+ if (code === 93) return lookaheadClosingMarker(code);
1936
+ return normalUrlChar(code);
1937
+ }
1938
+ function normalUrlChar(code) {
1939
+ effects.consume(code);
1940
+ linkLength++;
1941
+ return insideUrl;
1942
+ }
1943
+ function startLabel(code) {
1944
+ if (code === -5 || code === -4 || code === -3 || code === null) return nok(code);
1945
+ if (code === 93) return lookaheadClosingMarker(code);
1946
+ effects.enter("wikiLabel");
1947
+ effects.enter("chunkString", { contentType: "string" });
1948
+ effects.consume(code);
1949
+ labelLength++;
1950
+ return insideLabel;
1951
+ }
1952
+ function insideLabel(code) {
1953
+ if (code === -5 || code === -4 || code === -3 || code === null) return nok(code);
1954
+ if (code === 93) return lookaheadClosingMarker(code);
1955
+ effects.consume(code);
1956
+ labelLength++;
1957
+ return insideLabel;
1958
+ }
1959
+ /**
1960
+ * When encountering a right square bracket, we must look ahead at the next character
1961
+ * to determine whether it indicates the end of the [[wikilink]] or is
1962
+ * simply part of the label text.
1963
+ */
1964
+ function lookaheadClosingMarker(code) {
1965
+ if (code !== 93) return nok(code);
1966
+ return effects.check({
1967
+ partial: true,
1968
+ tokenize: closingMarkerLookahead
1969
+ }, firstClosingMarker, consumeMarker)(code);
1970
+ }
1971
+ function firstClosingMarker(code) {
1972
+ if (code !== 93) return nok(code);
1973
+ if (inLabel) {
1974
+ if (labelLength > 0) {
1975
+ effects.exit("chunkString");
1976
+ effects.exit("wikiLabel");
1977
+ }
1978
+ } else if (linkLength > 0) {
1979
+ effects.exit("chunkString");
1980
+ effects.exit("wikiUrl");
1981
+ }
1982
+ effects.enter("wikiMarker");
1983
+ effects.consume(code);
1984
+ return secondClosingMarker;
1985
+ }
1986
+ function secondClosingMarker(code) {
1987
+ if (code !== 93) return nok(code);
1988
+ if (linkLength === 0) return nok(code);
1989
+ effects.consume(code);
1990
+ effects.exit("wikiMarker");
1991
+ if (isEmbed) effects.exit("wikiEmbed");
1992
+ else effects.exit("wikiLink");
1993
+ return ok;
1994
+ }
1995
+ function consumeMarker(code) {
1996
+ if (code !== 93) return nok(code);
1997
+ effects.consume(code);
1998
+ if (inLabel) {
1999
+ labelLength++;
2000
+ return insideLabel;
2001
+ }
2002
+ linkLength++;
2003
+ return insideUrl;
2004
+ }
2005
+ /** If the next character is also `]`, run `ok`, else `nok`. */
2006
+ function closingMarkerLookahead(effects, ok, nok) {
2007
+ return start;
2008
+ function start(code) {
2009
+ if (code !== 93) return nok(code);
2010
+ effects.enter("wikiMarkerTemp");
2011
+ effects.consume(code);
2012
+ effects.exit("wikiMarkerTemp");
2013
+ return ok(code);
2014
+ }
2015
+ }
2016
+ /** Check if backslash is followed by pipe. */
2017
+ function backslashPipeLookahead(effects, ok, nok) {
2018
+ return start;
2019
+ function start(code) {
2020
+ if (code !== 92) return nok(code);
2021
+ effects.enter("wikiMarkerTemp");
2022
+ effects.consume(code);
2023
+ return checkNext;
2024
+ }
2025
+ function checkNext(code) {
2026
+ if (code === 124) {
2027
+ effects.consume(code);
2028
+ effects.exit("wikiMarkerTemp");
2029
+ return ok(code);
2030
+ }
2031
+ effects.exit("wikiMarkerTemp");
2032
+ return nok(code);
2033
+ }
2034
+ }
2035
+ function transitionToLabel(code) {
2036
+ effects.exit("chunkString");
2037
+ effects.exit("wikiUrl");
2038
+ effects.enter("wikiMarker");
2039
+ effects.consume(code);
2040
+ if (code === 92) return consumePipeAfterBackslash;
2041
+ effects.exit("wikiMarker");
2042
+ inLabel = true;
2043
+ return effects.check({
2044
+ partial: true,
2045
+ tokenize: closingBracketLookahead
2046
+ }, lookaheadClosingMarker, startLabel)(code);
2047
+ }
2048
+ /** Check if next character is a closing bracket. */
2049
+ function closingBracketLookahead(effects, ok, nok) {
2050
+ return start;
2051
+ function start(code) {
2052
+ if (code === 93) {
2053
+ effects.enter("wikiMarkerTemp");
2054
+ effects.consume(code);
2055
+ effects.exit("wikiMarkerTemp");
2056
+ return ok(code);
2057
+ }
2058
+ return nok(code);
2059
+ }
2060
+ }
2061
+ function consumePipeAfterBackslash(code) {
2062
+ if (code !== 124) return nok(code);
2063
+ effects.consume(code);
2064
+ effects.exit("wikiMarker");
2065
+ inLabel = true;
2066
+ return startLabel;
2067
+ }
2068
+ }
2069
+ }
2070
+
2071
+ //#endregion
2072
+ //#region src/lib/parse/wiki-basic/remark-wiki-basic.ts
2073
+ /**
2074
+ * This Remark plugin ONLY turns wiki links and Obsidian-style wiki link image
2075
+ * and media embeds into regular mdast link and image nodes.
2076
+ *
2077
+ * All wiki-style embeds are treated as images.
2078
+ *
2079
+ * Obsidian also supports wiki links in Markdown-style image and link syntax, so
2080
+ * handling resolution here would miss those cases, so:
2081
+ * - Resolution of wiki link into absolute paths happens later in
2082
+ * remark-resolve-links.ts
2083
+ * - Parsing of Obsidian-style image size from alias / alt text happens later in
2084
+ * rehype-utilities.ts
2085
+ *
2086
+ *
2087
+ * Note that only wiki links support spaces in the src, regular markdown links
2088
+ * MUST be URI-encoded in the Markdown source Here, we URI-encode for
2089
+ * consistency with the regular image syntax in the resulting HAST `<>` escaped
2090
+ * spaces handled correctly
2091
+ *
2092
+ * Images are also used for audio, and video, and other embeds in Obsidian...
2093
+ */
2094
+ const plugin = function() {
2095
+ const data = this.data();
2096
+ data.micromarkExtensions = [...data.micromarkExtensions ?? [], wikiBasic()];
2097
+ data.fromMarkdownExtensions = [...data.fromMarkdownExtensions ?? [], wikiBasicFromMarkdown()];
2098
+ };
2099
+
2100
+ //#endregion
2101
+ //#region src/lib/parse/remark-utilities.ts
2102
+ const defaultAstFromMarkdownOptions = { ...defaultGlobalOptions };
2103
+ async function getAstFromMarkdown(markdown, options) {
2104
+ const { allFilePaths, basePath, cwd, obsidianVault, resolveUrls } = deepmerge(defaultAstFromMarkdownOptions, options ?? {});
2105
+ const processor = unified().use(remarkParse).use(remarkFrontmatter, [{
2106
+ anywhere: false,
2107
+ marker: "-",
2108
+ type: "yaml"
2109
+ }]).use(plugin).use(remarkGfm, { singleTilde: false }).use(plugin$1, {
2110
+ allFilePaths,
2111
+ basePath,
2112
+ cwd,
2113
+ enabled: resolveUrls,
2114
+ obsidianVault
2115
+ }).use(remarkMath).use(remarkGithubBetaBlockquoteAdmonitions).use(remarkFlexibleMarkers).use(remarkRuby);
2116
+ return processor.run(processor.parse(markdown));
2117
+ }
2118
+ function isText(node) {
2119
+ return node.type === "text";
2120
+ }
2121
+ function deleteFirstNodeOfType(tree, nodeType) {
2122
+ visit(tree, nodeType, (_, index, parent) => {
2123
+ if (parent && index !== void 0) {
2124
+ parent.children.splice(index, 1);
2125
+ return EXIT;
2126
+ }
2127
+ });
2128
+ return tree;
2129
+ }
2130
+ /**
2131
+ * Trims all leading spaces from the first text node and all trailing spaces from
2132
+ * the last text node in an array of phrasing content nodes.
2133
+ *
2134
+ * This is useful in cases where surrounding white space in text nodes is not
2135
+ * necessary and should be removed to clean up the content.
2136
+ * @param nodes - An array of phrasing content nodes.
2137
+ * @returns The modified array of nodes with leading and trailing spaces trimmed.
2138
+ */
2139
+ function trimLeadingAndTrailingSpaces(nodes) {
2140
+ const firstNode = nodes.at(0);
2141
+ if (firstNode?.type === "text") {
2142
+ firstNode.value = firstNode.value.trimStart();
2143
+ if (firstNode.value === "") nodes.shift();
2144
+ }
2145
+ const lastNode = nodes.at(-1);
2146
+ if (lastNode?.type === "text") {
2147
+ lastNode.value = lastNode.value.trimEnd();
2148
+ if (lastNode.value === "") nodes.pop();
2149
+ }
2150
+ return nodes;
2151
+ }
2152
+ function replaceDeleteNodesWithClozeMarkup(ast) {
2153
+ let clozeIndex = 1;
2154
+ visit(ast, "delete", (node, index, parent) => {
2155
+ if (parent === void 0 || index === void 0 || !("children" in node) || node.children.length === 0) return CONTINUE;
2156
+ if (node.children.length > 0 && isText(node.children[0])) {
2157
+ const result = /^[(|]?(\d{1,2})(?:[\s).|]|$)(.*)$/.exec(node.children[0].value);
2158
+ if (result !== null && (node.children.length > 1 || (result.at(2) ?? "").length > 0)) {
2159
+ const possibleClozeIndex = Number.parseInt(result.at(1) ?? "", 10);
2160
+ if (!Number.isNaN(possibleClozeIndex)) {
2161
+ clozeIndex = possibleClozeIndex;
2162
+ node.children[0].value = (result.at(2)?.trim().length ?? 0) > 0 ? result.at(2) ?? "" : "";
2163
+ }
2164
+ }
2165
+ }
2166
+ const lastNode = node.children.at(-1);
2167
+ const clozeNodes = node.children.length > 1 && lastNode?.type === "emphasis" ? [
2168
+ u("text", `{{c${clozeIndex}::`),
2169
+ ...trimLeadingAndTrailingSpaces(node.children.slice(0, -1)),
2170
+ u("text", "::"),
2171
+ ...trimLeadingAndTrailingSpaces(node.children.slice(-1)),
2172
+ u("text", "}}")
2173
+ ] : [
2174
+ u("text", `{{c${clozeIndex}::`),
2175
+ ...trimLeadingAndTrailingSpaces(node.children),
2176
+ u("text", "}}")
2177
+ ];
2178
+ parent.children.splice(index, 1, ...clozeNodes);
2179
+ clozeIndex += 1;
2180
+ });
2181
+ return ast;
2182
+ }
2183
+ function splitTreeAtThematicBreak(tree) {
2184
+ let splitIndex;
2185
+ let isDoubleBreak = false;
2186
+ visit(tree, "thematicBreak", (_, index, parent) => {
2187
+ if (index === void 0 || parent === void 0) return CONTINUE;
2188
+ splitIndex = index;
2189
+ if (parent.children[index + 1]?.type === "thematicBreak") isDoubleBreak = true;
2190
+ return EXIT;
2191
+ });
2192
+ if (splitIndex === void 0) return [tree, void 0];
2193
+ return [{
2194
+ children: tree.children.slice(0, splitIndex),
2195
+ type: "root"
2196
+ }, {
2197
+ children: tree.children.slice(splitIndex + (isDoubleBreak ? 2 : 1)),
2198
+ type: "root"
2199
+ }];
2200
+ }
2201
+ function getYankiModelNameFromTree(ast) {
2202
+ let probableType;
2203
+ visit(ast, (node) => {
2204
+ if (node.type === "thematicBreak") {
2205
+ probableType = void 0;
2206
+ return EXIT;
2207
+ }
2208
+ if (node.type === "delete") {
2209
+ probableType = `Yanki - Cloze`;
2210
+ return EXIT;
2211
+ }
2212
+ });
2213
+ if (probableType !== void 0) return probableType;
2214
+ if (!hasThematicBreak(ast) && isLastVisibleNodeEmphasisWithOthers(ast)) return `Yanki - Basic (type in the answer)`;
2215
+ let lastNode;
2216
+ visit(ast, (node, index, parent) => {
2217
+ if (parent === null || index === null) return CONTINUE;
2218
+ if (node.type === "thematicBreak") {
2219
+ if (probableType === void 0) probableType = "Yanki - Basic";
2220
+ else if (probableType === "Yanki - Basic" && lastNode?.type === "thematicBreak") {
2221
+ probableType = "Yanki - Basic (and reversed card with extra)";
2222
+ return EXIT;
2223
+ }
2224
+ }
2225
+ lastNode = node;
2226
+ });
2227
+ return probableType ?? `Yanki - Basic`;
2228
+ }
2229
+ function getFrontmatterFromTree(ast) {
2230
+ let rawYaml;
2231
+ visit(ast, "yaml", (node) => {
2232
+ if (!("value" in node)) return CONTINUE;
2233
+ rawYaml = node.value;
2234
+ return EXIT;
2235
+ });
2236
+ if (!rawYaml) return {};
2237
+ const parsedYaml = parse(rawYaml);
2238
+ if (!parsedYaml) throw new Error("Could not parse frontmatter");
2239
+ return parsedYaml;
2240
+ }
2241
+ function hasThematicBreak(ast) {
2242
+ let hasThematicBreak = false;
2243
+ visit(ast, "thematicBreak", () => {
2244
+ hasThematicBreak = true;
2245
+ return EXIT;
2246
+ });
2247
+ return hasThematicBreak;
2248
+ }
2249
+ function isLastVisibleNodeEmphasisWithOthers(ast) {
2250
+ let lastVisibleNode;
2251
+ let visibleCount = 0;
2252
+ visit(ast, (node) => {
2253
+ if (node.type === "text" && node.value.trim() !== "") {
2254
+ lastVisibleNode = node;
2255
+ visibleCount++;
2256
+ } else if (node.type === "emphasis" && node.children.some((child) => child.type === "text" && child.value.trim() !== "")) {
2257
+ lastVisibleNode = node;
2258
+ visibleCount++;
2259
+ return SKIP;
2260
+ }
2261
+ });
2262
+ return lastVisibleNode?.type === "emphasis" && visibleCount > 1;
2263
+ }
2264
+ function removeLastEmphasis(ast) {
2265
+ let lastEmphasisNode;
2266
+ let lastEmphasisParent;
2267
+ let lastEmphasisIndex;
2268
+ visit(ast, "emphasis", (node, index, parent) => {
2269
+ if (parent === void 0 || index === void 0 || node.type !== "emphasis") return CONTINUE;
2270
+ lastEmphasisNode = node;
2271
+ lastEmphasisParent = parent;
2272
+ lastEmphasisIndex = index;
2273
+ });
2274
+ if (lastEmphasisParent && lastEmphasisNode && typeof lastEmphasisIndex === "number") {
2275
+ lastEmphasisParent.children.splice(lastEmphasisIndex, 1);
2276
+ return lastEmphasisNode;
2277
+ }
2278
+ }
2279
+
2280
+ //#endregion
2281
+ //#region src/lib/parse/parse.ts
2282
+ const defaultGetNoteFromMarkdownOptions = {
2283
+ namespaceValidationAndSanitization: true,
2284
+ ...defaultGlobalOptions
2285
+ };
2286
+ async function getNoteFromMarkdown(markdown, options) {
2287
+ const { allFilePaths, basePath, cwd, fetchAdapter = getDefaultFetchAdapter(), fileAdapter = await getDefaultFileAdapter(), namespace, namespaceValidationAndSanitization, obsidianVault, resolveUrls, strictLineBreaks, syncMediaAssets } = deepmerge(defaultGetNoteFromMarkdownOptions, options ?? {});
2288
+ const sanitizedNamespace = namespaceValidationAndSanitization ? validateAndSanitizeNamespace(namespace) : namespace;
2289
+ let ast = await getAstFromMarkdown(markdown, {
2290
+ allFilePaths,
2291
+ basePath,
2292
+ cwd,
2293
+ obsidianVault,
2294
+ resolveUrls
2295
+ });
2296
+ const modelName = getYankiModelNameFromTree(ast);
2297
+ const frontmatter = getFrontmatterFromTree(ast);
2298
+ ast = deleteFirstNodeOfType(ast, "yaml");
2299
+ let front = "";
2300
+ let back = "";
2301
+ let extra;
2302
+ switch (modelName) {
2303
+ case "Yanki - Basic":
2304
+ case "Yanki - Basic (and reversed card with extra)": {
2305
+ let [firstPart, secondPart] = splitTreeAtThematicBreak(ast);
2306
+ let extraPart;
2307
+ if (secondPart !== void 0 && modelName === "Yanki - Basic (and reversed card with extra)") {
2308
+ extra = "";
2309
+ const [newSecondPart, newExtraPart] = splitTreeAtThematicBreak(secondPart);
2310
+ secondPart = newSecondPart;
2311
+ extraPart = newExtraPart;
2312
+ }
2313
+ front = await mdastToHtml(firstPart, {
2314
+ cssClassNames: [
2315
+ CSS_DEFAULT_CLASS_NAME,
2316
+ `namespace-${sanitizedNamespace}`,
2317
+ "front",
2318
+ `model-${modelName}`
2319
+ ],
2320
+ fetchAdapter,
2321
+ fileAdapter,
2322
+ namespace: sanitizedNamespace,
2323
+ strictLineBreaks,
2324
+ syncMediaAssets,
2325
+ useEmptyPlaceholder: true
2326
+ });
2327
+ back = await mdastToHtml(secondPart, {
2328
+ cssClassNames: [
2329
+ CSS_DEFAULT_CLASS_NAME,
2330
+ `namespace-${sanitizedNamespace}`,
2331
+ "back",
2332
+ `model-${modelName}`
2333
+ ],
2334
+ fetchAdapter,
2335
+ fileAdapter,
2336
+ namespace: sanitizedNamespace,
2337
+ strictLineBreaks,
2338
+ syncMediaAssets,
2339
+ useEmptyPlaceholder: true
2340
+ });
2341
+ if (extraPart !== void 0) extra = await mdastToHtml(extraPart, {
2342
+ cssClassNames: [
2343
+ CSS_DEFAULT_CLASS_NAME,
2344
+ `namespace-${sanitizedNamespace}`,
2345
+ "extra",
2346
+ `model-${modelName}`
2347
+ ],
2348
+ fetchAdapter,
2349
+ fileAdapter,
2350
+ namespace: sanitizedNamespace,
2351
+ strictLineBreaks,
2352
+ syncMediaAssets,
2353
+ useEmptyPlaceholder: false
2354
+ });
2355
+ break;
2356
+ }
2357
+ case "Yanki - Basic (type in the answer)": {
2358
+ const secondPart = removeLastEmphasis(ast);
2359
+ if (secondPart === void 0) throw new Error("Could not find emphasis in Basic (type in the answer) note AST.");
2360
+ const firstPart = ast;
2361
+ const secondPartHast = u("root", u("paragraph", secondPart.children));
2362
+ front = await mdastToHtml(firstPart, {
2363
+ cssClassNames: [
2364
+ CSS_DEFAULT_CLASS_NAME,
2365
+ `namespace-${sanitizedNamespace}`,
2366
+ "front",
2367
+ `model-${modelName}`
2368
+ ],
2369
+ fetchAdapter,
2370
+ fileAdapter,
2371
+ namespace: sanitizedNamespace,
2372
+ strictLineBreaks,
2373
+ syncMediaAssets,
2374
+ useEmptyPlaceholder: true
2375
+ });
2376
+ back = await mdastToHtml(secondPartHast, {
2377
+ cssClassNames: [
2378
+ CSS_DEFAULT_CLASS_NAME,
2379
+ `namespace-${sanitizedNamespace}`,
2380
+ "back",
2381
+ `model-${modelName}`
2382
+ ],
2383
+ fetchAdapter,
2384
+ fileAdapter,
2385
+ namespace: sanitizedNamespace,
2386
+ strictLineBreaks,
2387
+ syncMediaAssets,
2388
+ useEmptyPlaceholder: false
2389
+ });
2390
+ break;
2391
+ }
2392
+ case "Yanki - Cloze": {
2393
+ const [firstPart, secondPart] = splitTreeAtThematicBreak(ast);
2394
+ front = await mdastToHtml(replaceDeleteNodesWithClozeMarkup(firstPart), {
2395
+ cssClassNames: [
2396
+ CSS_DEFAULT_CLASS_NAME,
2397
+ `namespace-${sanitizedNamespace}`,
2398
+ "front",
2399
+ `model-${modelName}`
2400
+ ],
2401
+ fetchAdapter,
2402
+ fileAdapter,
2403
+ namespace: sanitizedNamespace,
2404
+ strictLineBreaks,
2405
+ syncMediaAssets,
2406
+ useEmptyPlaceholder: true
2407
+ });
2408
+ back = await mdastToHtml(secondPart, {
2409
+ cssClassNames: [
2410
+ CSS_DEFAULT_CLASS_NAME,
2411
+ `namespace-${sanitizedNamespace}`,
2412
+ "back",
2413
+ `model-${modelName}`
2414
+ ],
2415
+ fetchAdapter,
2416
+ fileAdapter,
2417
+ namespace: sanitizedNamespace,
2418
+ strictLineBreaks,
2419
+ syncMediaAssets,
2420
+ useEmptyPlaceholder: false
2421
+ });
2422
+ break;
2423
+ }
2424
+ }
2425
+ return {
2426
+ deckName: "",
2427
+ fields: {
2428
+ Back: back,
2429
+ ...extra !== void 0 && { Extra: extra },
2430
+ Front: front,
2431
+ YankiNamespace: sanitizedNamespace
2432
+ },
2433
+ modelName,
2434
+ noteId: frontmatter.noteId ?? void 0,
2435
+ tags: obsidianTagsToAnkiTags(frontmatter.tags)
2436
+ };
2437
+ }
2438
+ function obsidianTagsToAnkiTags(tags) {
2439
+ return (typeof tags === "string" ? [tags] : tags ?? []).map((tag) => tag.replaceAll("/", "::"));
2440
+ }
2441
+
2442
+ //#endregion
2443
+ //#region src/lib/actions/load-local-notes.ts
2444
+ const defaultLoadOptions = { ...defaultGlobalOptions };
2445
+ async function loadLocalNotes(allLocalFilePaths, options) {
2446
+ const { allFilePaths, basePath, fetchAdapter = getDefaultFetchAdapter(), fileAdapter = await getDefaultFileAdapter(), namespace, obsidianVault, strictLineBreaks, syncMediaAssets } = deepmerge(defaultLoadOptions, options ?? {});
2447
+ const sanitizedNamespace = validateAndSanitizeNamespace(namespace);
2448
+ allLocalFilePaths.sort((a, b) => a.localeCompare(b));
2449
+ const deckNamesFromFilePaths = getDeckNamesFromFilePaths(allLocalFilePaths);
2450
+ return await Promise.all(allLocalFilePaths.map(async (filePath, index) => {
2451
+ const markdown = await fileAdapter.readFile(filePath);
2452
+ const note = await getNoteFromMarkdown(markdown, {
2453
+ allFilePaths,
2454
+ basePath,
2455
+ cwd: path.dirname(filePath),
2456
+ fetchAdapter,
2457
+ fileAdapter,
2458
+ namespace: sanitizedNamespace,
2459
+ namespaceValidationAndSanitization: false,
2460
+ obsidianVault,
2461
+ strictLineBreaks,
2462
+ syncMediaAssets
2463
+ });
2464
+ if (note.deckName === "") note.deckName = deckNamesFromFilePaths[index];
2465
+ return {
2466
+ filePath,
2467
+ filePathOriginal: filePath,
2468
+ markdown,
2469
+ note
2470
+ };
2471
+ }));
2472
+ }
2473
+ const defaultDeckNamesFromFilePathsOptions = { mode: "common-root" };
2474
+ /**
2475
+ * Helper function to infer deck names from file paths if `deckName` not defined in the note's frontmatter.
2476
+ *
2477
+ * `deckName` will always override the inferred deck name.
2478
+ *
2479
+ * Depends on the context of _all_ file paths passed to `syncNoteFiles`.
2480
+ *
2481
+ * Example of paths -> deck names with `common-root`:
2482
+ * /base/foo/note.md -> foo
2483
+ * /base/foo/baz/note.md -> foo::baz
2484
+ *
2485
+ * Example of paths -> deck names with `common-root`:
2486
+ * /base/foo/note.md -> foo
2487
+ * /base/foo/note.md -> foo
2488
+ *
2489
+ * Example of paths -> deck names with `common-parent`:
2490
+ * /base/foo/note.md -> base::foo
2491
+ * /base/foo/baz/note.md -> base::foo::baz
2492
+ *
2493
+ * Example of paths -> deck names with `common-parent`:
2494
+ * /base/foo/note.md -> foo
2495
+ * /base/foo/note.md -> foo
2496
+ * @param absoluteFilePaths Absolute paths to all markdown Anki note files. (Ensures proper resolution if path module is polyfilled.)
2497
+ * @returns array of ::-delimited deck paths
2498
+ */
2499
+ function getDeckNamesFromFilePaths(absoluteFilePaths, options) {
2500
+ const { mode } = deepmerge(defaultDeckNamesFromFilePathsOptions, options ?? {});
2501
+ if (absoluteFilePaths.length === 0) return [];
2502
+ const filePathSegments = absoluteFilePaths.map((filePath) => path.dirname(filePath).split(path.sep));
2503
+ const commonPathSegments = filePathSegments.reduce((acc, pathSegments) => {
2504
+ return acc.filter((segment, index) => segment === pathSegments[index]);
2505
+ });
2506
+ const lastSegmentHasFile = filePathSegments.some((pathSegments) => pathSegments.at(-1) === commonPathSegments.at(-1));
2507
+ const offset = mode === "common-parent" ? lastSegmentHasFile ? 1 : 1 : lastSegmentHasFile ? 1 : 0;
2508
+ return filePathSegments.map((pathSegments) => pathSegments.slice(commonPathSegments.length - offset).join("::"));
2509
+ }
2510
+
2511
+ //#endregion
2512
+ //#region src/lib/actions/rename.ts
2513
+ const defaultRenameNotesOptions = { ...defaultGlobalOptions };
2514
+ async function renameNotes(notes, options) {
2515
+ const { dryRun, fileAdapter = await getDefaultFileAdapter(), manageFilenames, maxFilenameLength } = deepmerge(defaultRenameNotesOptions, options ?? {});
2516
+ if (manageFilenames !== "off") {
2517
+ const newFilePaths = [];
2518
+ for (const noteToRename of notes) {
2519
+ const { filePath: filePathOriginal, note } = noteToRename;
2520
+ if (filePathOriginal === void 0) throw new Error("File path is undefined");
2521
+ const newFilename = getSafeTitleForNote(note, manageFilenames, maxFilenameLength);
2522
+ const newUniqueFilePath = getUniqueFilePath(path.join(path.dirname(filePathOriginal), `${newFilename}${path.extname(filePathOriginal)}`), newFilePaths);
2523
+ noteToRename.filePath = newUniqueFilePath;
2524
+ newFilePaths.push(newUniqueFilePath.toLowerCase());
2525
+ }
2526
+ for (const noteToRename of notes) {
2527
+ const { filePath } = noteToRename;
2528
+ if (filePath === void 0) throw new Error("File path is undefined");
2529
+ noteToRename.filePath = auditUniqueFilePath(filePath, newFilePaths);
2530
+ }
2531
+ const intermediateRenamePlan = /* @__PURE__ */ new Map();
2532
+ for (const noteToRename of notes) {
2533
+ const { filePath, filePathOriginal } = noteToRename;
2534
+ if (filePathOriginal === void 0) throw new Error("Original file path is undefined.");
2535
+ if (filePath === void 0) throw new Error("File path is undefined.");
2536
+ if (filePath === filePathOriginal) continue;
2537
+ let safeNewFilePath = filePath;
2538
+ if (notes.some(({ filePath: someFilePath, filePathOriginal: someFilePathOriginal }) => someFilePath !== filePath && someFilePathOriginal?.toLowerCase() === filePath.toLowerCase())) {
2539
+ safeNewFilePath = getTemporarilyUniqueFilePath(filePath);
2540
+ intermediateRenamePlan.set(safeNewFilePath, filePath);
2541
+ }
2542
+ if (!dryRun) await fileAdapter.rename(filePathOriginal, safeNewFilePath);
2543
+ }
2544
+ for (const [temporarilyUniquePath, newPath] of intermediateRenamePlan) if (!dryRun) await fileAdapter.rename(temporarilyUniquePath, newPath);
2545
+ }
2546
+ notes.sort((a, b) => a.filePath.localeCompare(b.filePath));
2547
+ return notes;
2548
+ }
2549
+ const defaultRenameFilesOptions = { ...defaultGlobalOptions };
2550
+ /**
2551
+ * Currently used for testing and by `yanki-obsidian`.
2552
+ */
2553
+ async function renameFiles(allLocalFilePaths, options) {
2554
+ const { allFilePaths: allFilePathsRaw, basePath: basePathRaw, dryRun, fetchAdapter = getDefaultFetchAdapter(), fileAdapter = await getDefaultFileAdapter(), manageFilenames, maxFilenameLength, namespace: namespaceRaw, obsidianVault, strictLineBreaks, syncMediaAssets } = deepmerge(defaultRenameFilesOptions, options ?? {});
2555
+ const allLocalFilePathsNormalized = allLocalFilePaths.map((file) => normalize(file));
2556
+ const basePath = basePathRaw === void 0 ? void 0 : normalize(basePathRaw);
2557
+ return {
2558
+ dryRun,
2559
+ notes: await renameNotes(await loadLocalNotes(allLocalFilePathsNormalized, {
2560
+ allFilePaths: allFilePathsRaw.map((file) => normalize(file)),
2561
+ basePath,
2562
+ fetchAdapter,
2563
+ fileAdapter,
2564
+ namespace: validateAndSanitizeNamespace(namespaceRaw),
2565
+ obsidianVault,
2566
+ strictLineBreaks,
2567
+ syncMediaAssets
2568
+ }), {
2569
+ dryRun,
2570
+ fileAdapter,
2571
+ manageFilenames,
2572
+ maxFilenameLength
2573
+ })
2574
+ };
2575
+ }
2576
+
2577
+ //#endregion
2578
+ //#region src/lib/actions/style.ts
2579
+ const defaultSetStyleOptions = {
2580
+ css: CSS_DEFAULT_STYLE,
2581
+ ...defaultGlobalOptions
2582
+ };
2583
+ const defaultGetStyleOptions = { ...defaultGlobalOptions };
2584
+ async function getStyle(options) {
2585
+ const { ankiConnectOptions } = deepmerge(defaultSetStyleOptions, options ?? {});
2586
+ const client = new YankiConnect(ankiConnectOptions);
2587
+ if (await requestPermission(client) === "ankiUnreachable") throw new Error("Anki is unreachable. Is Anki running?");
2588
+ const cssSet = /* @__PURE__ */ new Set();
2589
+ for (const modelName of yankiModelNames) {
2590
+ const css = await getModelStyle(client, modelName);
2591
+ cssSet.add(css);
2592
+ }
2593
+ if (cssSet.size === 0) throw new Error("No CSS found in any Yanki model.");
2594
+ if (cssSet.size > 1) throw new Error("Expected all Yanki models to have identical CSS.");
2595
+ return [...cssSet][0];
2596
+ }
2597
+ async function setStyle(options) {
2598
+ const startTime = performance.now();
2599
+ const { ankiConnectOptions, ankiWeb, css, dryRun } = deepmerge(defaultSetStyleOptions, options ?? {});
2600
+ const client = new YankiConnect(ankiConnectOptions);
2601
+ if (await requestPermission(client) === "ankiUnreachable") throw new Error("Anki is unreachable. Is Anki running?");
2602
+ const modelsReport = [];
2603
+ for (const modelName of yankiModelNames) {
2604
+ const updated = await updateModelStyle(client, modelName, css, dryRun);
2605
+ modelsReport.push({
2606
+ action: updated ? "updated" : "unchanged",
2607
+ name: modelName
2608
+ });
2609
+ }
2610
+ const isChanged = modelsReport.some((model) => model.action !== "unchanged");
2611
+ if (!dryRun && ankiWeb && (isChanged || SYNC_TO_ANKI_WEB_EVEN_IF_UNCHANGED)) await syncToAnkiWeb(client);
2612
+ return {
2613
+ ankiWeb,
2614
+ dryRun,
2615
+ duration: performance.now() - startTime,
2616
+ models: modelsReport
2617
+ };
2618
+ }
2619
+ function formatSetStyleResult(result, verbose = false) {
2620
+ const lines = [];
2621
+ const unchangedModels = result.models.filter((model) => model.action === "unchanged");
2622
+ const updatedModels = result.models.filter((model) => model.action === "updated");
2623
+ lines.push(`${result.dryRun ? "Will" : "Successfully"} update ${updatedModels.length} ${plur("model", updatedModels.length)} and left ${unchangedModels.length} ${plur("model", unchangedModels.length)} unchanged${result.dryRun ? "" : ` in ${prettyMilliseconds(result.duration)}`}.`);
2624
+ if (verbose) {
2625
+ if (updatedModels.length > 0) {
2626
+ lines.push("", result.dryRun ? "Models to update:" : "Updated models:");
2627
+ for (const model of updatedModels) lines.push(` ${model.name}`);
2628
+ }
2629
+ if (unchangedModels.length > 0) {
2630
+ lines.push("", result.dryRun ? "Models unchanged:" : "Unchanged models:");
2631
+ for (const model of unchangedModels) lines.push(` ${model.name}`);
2632
+ }
2633
+ }
2634
+ return lines.join("\n");
2635
+ }
2636
+
2637
+ //#endregion
2638
+ //#region src/lib/model/frontmatter.ts
2639
+ /**
2640
+ * Update the noteId in the frontmatter of a markdown string.
2641
+ *
2642
+ * Used when a noteId is received from Anki after creating a note.
2643
+ *
2644
+ *
2645
+ * String manipulation is ugly, but it ensures that the markdown format is
2646
+ * preserved verbatim. Running it through the unified AST and then
2647
+ * remarkStringify would possibly change the format.
2648
+ * @param markdown Raw markdown string with frontmatter.
2649
+ * @param noteId The value to set the noteId to. If undefined, the noteId will
2650
+ * be removed from the frontmatter. (Useful for testing.)
2651
+ * @returns Raw markdown string with updated frontmatter.
2652
+ */
2653
+ async function setNoteIdInFrontmatter(markdown, noteId) {
2654
+ const [frontmatterStart, frontmatterEnd] = getFrontmatterRange(markdown);
2655
+ const lines = markdown.split(/\r?\n/);
2656
+ if (frontmatterStart === void 0 || frontmatterEnd === void 0) {
2657
+ if (noteId === void 0) return markdown;
2658
+ return [
2659
+ "---",
2660
+ stringify({ noteId }).trim(),
2661
+ "---\n",
2662
+ ...lines
2663
+ ].join("\n");
2664
+ }
2665
+ const parsedFrontmatter = await parse(lines.slice(frontmatterStart + 1, frontmatterEnd).join("\n")) ?? {};
2666
+ if (noteId === void 0) {
2667
+ delete parsedFrontmatter.noteId;
2668
+ if (Object.keys(parsedFrontmatter).length === 0) {
2669
+ const markdownWithoutFrontmatter = lines.slice(frontmatterEnd + 1);
2670
+ if (markdownWithoutFrontmatter[0].trim() === "") return markdownWithoutFrontmatter.slice(1).join("\n");
2671
+ return markdownWithoutFrontmatter.join("\n");
2672
+ }
2673
+ } else parsedFrontmatter.noteId = noteId;
2674
+ const newFrontmatter = stringify(parsedFrontmatter, { lineWidth: 0 }).trim();
2675
+ return [
2676
+ ...lines.slice(0, frontmatterStart + 1),
2677
+ newFrontmatter,
2678
+ ...lines.slice(frontmatterEnd)
2679
+ ].join("\n");
2680
+ }
2681
+ function getFrontmatterRange(markdown) {
2682
+ const lines = markdown.split(/\r?\n/);
2683
+ if (!lines.join("").trim().startsWith("---")) return [void 0, void 0];
2684
+ const frontmatterStart = lines.findIndex((line) => line.startsWith("---"));
2685
+ const frontmatterEnd = lines.findIndex((line, index) => index > frontmatterStart && line.startsWith("---"));
2686
+ if (frontmatterStart === -1 || frontmatterEnd === -1) return [void 0, void 0];
2687
+ return [frontmatterStart, frontmatterEnd];
2688
+ }
2689
+
2690
+ //#endregion
2691
+ //#region src/lib/actions/sync-notes.ts
2692
+ const defaultSyncNotesOptions = { ...defaultGlobalOptions };
2693
+ /**
2694
+ * Syncs local notes to Anki.
2695
+ * @param allLocalNotes All the YankiNotes to sync
2696
+ * @returns The synced notes (with new IDs where applicable), plus some stats
2697
+ * about the sync
2698
+ * @throws {Error} For various reasons...
2699
+ */
2700
+ async function syncNotes(allLocalNotes, options) {
2701
+ const startTime = performance.now();
2702
+ const allLocalNotesCopy = structuredClone(allLocalNotes);
2703
+ const { ankiConnectOptions, ankiWeb, checkDatabase, dryRun, fileAdapter, namespace, strictMatching } = deepmerge(defaultSyncNotesOptions, options ?? {});
2704
+ const sanitizedNamespace = validateAndSanitizeNamespace(namespace);
2705
+ const synced = [];
2706
+ const client = new YankiConnect(ankiConnectOptions);
2707
+ if (await requestPermission(client) === "ankiUnreachable") return {
2708
+ ankiWeb,
2709
+ deletedDecks: [],
2710
+ deletedMedia: [],
2711
+ dryRun,
2712
+ duration: performance.now() - startTime,
2713
+ fixedDatabase: false,
2714
+ namespace: sanitizedNamespace,
2715
+ synced: allLocalNotesCopy.map((note) => ({
2716
+ action: "ankiUnreachable",
2717
+ note
2718
+ }))
2719
+ };
2720
+ for (const localNote of allLocalNotesCopy) if (localNote.deckName === "") localNote.deckName = NOTE_DEFAULT_DECK_NAME;
2721
+ const allRemoteNotes = await getRemoteNotes(client, "*");
2722
+ const remoteNotes = allRemoteNotes.filter((remoteNote) => remoteNote.fields.YankiNamespace === sanitizedNamespace);
2723
+ for (const localNote of allLocalNotesCopy) {
2724
+ if (localNote.noteId === void 0) continue;
2725
+ const duplicates = findNotesWithDuplicateIds(allLocalNotesCopy, localNote.noteId);
2726
+ if (duplicates.length <= 1) continue;
2727
+ const noteToKeep = selectNoteToKeep(duplicates, remoteNotes.find((remote) => remote.noteId === localNote.noteId));
2728
+ for (const duplicate of duplicates) if (duplicate !== noteToKeep) duplicate.noteId = void 0;
2729
+ }
2730
+ const matchedIds = new Set(allLocalNotesCopy.filter((localNote) => localNote.noteId !== void 0 && remoteNotes.some((remote) => localNote.noteId === remote.noteId)).map((note) => note.noteId));
2731
+ for (const localNote of allLocalNotesCopy) {
2732
+ let remoteNote = allRemoteNotes.find((remote) => remote.noteId === localNote.noteId);
2733
+ if (remoteNote?.fields.YankiNamespace !== sanitizedNamespace) {
2734
+ localNote.noteId = void 0;
2735
+ remoteNote = void 0;
2736
+ }
2737
+ if (remoteNote === void 0) {
2738
+ localNote.noteId = strictMatching ? void 0 : findRemoteContentMatchId(localNote, remoteNotes, matchedIds);
2739
+ if (localNote.noteId === void 0) {
2740
+ localNote.noteId = await addNote(client, {
2741
+ ...localNote,
2742
+ noteId: void 0
2743
+ }, dryRun, fileAdapter ?? void 0);
2744
+ synced.push({
2745
+ action: "created",
2746
+ note: localNote
2747
+ });
2748
+ } else synced.push({
2749
+ action: "matched",
2750
+ note: localNote
2751
+ });
2752
+ } else {
2753
+ if (remoteNote.noteId === void 0) throw new Error("Remote note ID is undefined");
2754
+ const wasUpdated = await updateNote(client, localNote, remoteNote, dryRun, fileAdapter ?? void 0);
2755
+ synced.push({
2756
+ action: wasUpdated ? "updated" : "unchanged",
2757
+ note: localNote
2758
+ });
2759
+ }
2760
+ if (localNote.noteId === void 0) throw new Error("Note ID is undefined");
2761
+ matchedIds.add(localNote.noteId);
2762
+ }
2763
+ const orphanedNotes = remoteNotes.filter((remoteNote) => !allLocalNotesCopy.some((localNote) => localNote.noteId === remoteNote.noteId));
2764
+ await deleteNotes(client, orphanedNotes, dryRun);
2765
+ for (const orphanedNote of orphanedNotes) synced.push({
2766
+ action: "deleted",
2767
+ note: orphanedNote
2768
+ });
2769
+ const liveNotes = [];
2770
+ const deletedNotes = [];
2771
+ for (const entry of synced) if (entry.action === "deleted") deletedNotes.push(entry.note);
2772
+ else liveNotes.push(entry.note);
2773
+ const deletedDecks = await deleteOrphanedDecks(client, liveNotes, remoteNotes, dryRun);
2774
+ let fixedDatabase = false;
2775
+ if (checkDatabase) {
2776
+ const updatedModelRemoteNotes = remoteNotes.filter((remoteNote) => synced.some((localNote) => localNote.action === "updated" && localNote.note.noteId === remoteNote.noteId && localNote.note.modelName !== remoteNote.modelName));
2777
+ if (updatedModelRemoteNotes.length > 0) {
2778
+ const cardIdsToCheck = updatedModelRemoteNotes.flatMap(({ cards }) => cards ?? []);
2779
+ try {
2780
+ await client.card.cardsInfo({ cards: cardIdsToCheck });
2781
+ } catch {
2782
+ fixedDatabase = true;
2783
+ await client.graphical.guiCheckDatabase();
2784
+ }
2785
+ }
2786
+ }
2787
+ const deletedMedia = await deleteUnusedMedia(client, liveNotes, sanitizedNamespace, dryRun);
2788
+ const isChanged = deletedDecks.length > 0 || synced.some((note) => note.action !== "unchanged");
2789
+ if (!dryRun && ankiWeb && (isChanged || SYNC_TO_ANKI_WEB_EVEN_IF_UNCHANGED)) await syncToAnkiWeb(client);
2790
+ return {
2791
+ ankiWeb,
2792
+ deletedDecks,
2793
+ deletedMedia,
2794
+ dryRun,
2795
+ duration: performance.now() - startTime,
2796
+ fixedDatabase,
2797
+ namespace: sanitizedNamespace,
2798
+ synced
2799
+ };
2800
+ }
2801
+ function findNotesWithDuplicateIds(notes, noteId) {
2802
+ return notes.filter((note) => note.noteId === void 0 ? false : note.noteId === noteId);
2803
+ }
2804
+ function selectNoteToKeep(duplicates, remoteNote) {
2805
+ return duplicates.find((duplicate) => duplicate.fields.Front === remoteNote?.fields.Front && duplicate.fields.Back === remoteNote.fields.Back && duplicate.fields.Extra === remoteNote.fields.Extra) ?? duplicates[0];
2806
+ }
2807
+ function findRemoteContentMatchId(localNote, remoteNotes, matchedIds) {
2808
+ return remoteNotes.find((remoteNote) => remoteNote.noteId !== void 0 && !matchedIds.has(remoteNote.noteId) && areNotesEqual(localNote, remoteNote, false))?.noteId ?? void 0;
2809
+ }
2810
+
2811
+ //#endregion
2812
+ //#region src/lib/actions/sync-files.ts
2813
+ const defaultSyncFilesOptions = {
2814
+ ...defaultGlobalOptions,
2815
+ ...defaultSyncNotesOptions
2816
+ };
2817
+ /**
2818
+ * Sync a list of local yanki files to Anki.
2819
+ *
2820
+ * Wraps the syncNotes function to handle file I/O.
2821
+ *
2822
+ * Most importantly, it updates the note IDs in the frontmatter of the local
2823
+ * files.
2824
+ * @param allLocalFilePaths Array of paths to the local markdown files
2825
+ * @returns The synced files (with new IDs where applicable), plus some stats
2826
+ * about the sync
2827
+ * @throws {Error} If syncing fails or file operations encounter an error.
2828
+ */
2829
+ async function syncFiles(allLocalFilePaths, options) {
2830
+ const startTime = performance.now();
2831
+ const { allFilePaths: allFilePathsRaw, ankiConnectOptions, ankiWeb, basePath: basePathRaw, checkDatabase, dryRun, fetchAdapter = getDefaultFetchAdapter(), fileAdapter = await getDefaultFileAdapter(), manageFilenames, maxFilenameLength, namespace: namespaceRaw, obsidianVault, strictLineBreaks, strictMatching, syncMediaAssets } = deepmerge(defaultSyncFilesOptions, options ?? {});
2832
+ const allLocalFilePathsNormalized = allLocalFilePaths.map((file) => normalize(file));
2833
+ const basePath = basePathRaw === void 0 ? void 0 : normalize(basePathRaw);
2834
+ const allFilePaths = allFilePathsRaw.map((file) => normalize(file));
2835
+ const namespace = validateAndSanitizeNamespace(namespaceRaw);
2836
+ const renamedLocalNotes = await renameNotes(await loadLocalNotes(allLocalFilePathsNormalized, {
2837
+ allFilePaths,
2838
+ basePath,
2839
+ fetchAdapter,
2840
+ fileAdapter,
2841
+ namespace,
2842
+ obsidianVault,
2843
+ strictLineBreaks,
2844
+ syncMediaAssets
2845
+ }), {
2846
+ dryRun,
2847
+ fileAdapter,
2848
+ manageFilenames,
2849
+ maxFilenameLength
2850
+ });
2851
+ if (obsidianVault !== void 0) {
2852
+ if (renamedLocalNotes.some((renamedNote) => renamedNote.filePath !== renamedNote.filePathOriginal)) {
2853
+ const reloadedLocalNotes = await loadLocalNotes(renamedLocalNotes.map((note) => note.filePath), {
2854
+ allFilePaths: allFilePaths.map((filePath) => {
2855
+ const renamedNote = renamedLocalNotes.find((renamedNote) => renamedNote.filePathOriginal === filePath);
2856
+ return renamedNote ? renamedNote.filePath : filePath;
2857
+ }),
2858
+ basePath,
2859
+ fetchAdapter,
2860
+ fileAdapter,
2861
+ namespace,
2862
+ obsidianVault,
2863
+ strictLineBreaks,
2864
+ syncMediaAssets
2865
+ });
2866
+ for (const [index, renamedNote] of renamedLocalNotes.entries()) renamedNote.note = reloadedLocalNotes[index].note;
2867
+ }
2868
+ }
2869
+ const { deletedDecks, deletedMedia, fixedDatabase, synced } = await syncNotes(renamedLocalNotes.map((note) => note.note), {
2870
+ ankiConnectOptions,
2871
+ ankiWeb,
2872
+ checkDatabase,
2873
+ dryRun,
2874
+ fileAdapter,
2875
+ namespace,
2876
+ strictMatching
2877
+ });
2878
+ const liveNotes = synced.filter((note) => note.action !== "deleted");
2879
+ for (const [index, loadedAndRenamedNote] of renamedLocalNotes.entries()) {
2880
+ const liveNote = liveNotes[index];
2881
+ if ((loadedAndRenamedNote.note.noteId === void 0 || loadedAndRenamedNote.note.noteId !== liveNote.note.noteId) && liveNote.action !== "ankiUnreachable") {
2882
+ const updatedMarkdown = await setNoteIdInFrontmatter(loadedAndRenamedNote.markdown, liveNote.note.noteId);
2883
+ if (!dryRun) await fileAdapter.writeFile(loadedAndRenamedNote.filePath, updatedMarkdown);
2884
+ }
2885
+ liveNote.filePath = loadedAndRenamedNote.filePath;
2886
+ liveNote.filePathOriginal = loadedAndRenamedNote.filePathOriginal;
2887
+ }
2888
+ const syncedAndSorted = [...synced.filter((note) => note.action === "deleted").map((note) => ({
2889
+ action: "deleted",
2890
+ filePath: void 0,
2891
+ filePathOriginal: void 0,
2892
+ note: note.note
2893
+ })), ...liveNotes].sort((a, b) => (a.filePath ?? "").localeCompare(b.filePath ?? ""));
2894
+ return {
2895
+ ankiWeb,
2896
+ deletedDecks,
2897
+ deletedMedia,
2898
+ dryRun,
2899
+ duration: performance.now() - startTime,
2900
+ fixedDatabase,
2901
+ namespace,
2902
+ synced: syncedAndSorted
2903
+ };
2904
+ }
2905
+ function formatSyncFilesResult(result, verbose = false) {
2906
+ const lines = [];
2907
+ const { synced } = result;
2908
+ const actionCounts = synced.reduce((acc, note) => {
2909
+ acc[note.action] = (acc[note.action] || 0) + 1;
2910
+ return acc;
2911
+ }, {});
2912
+ const totalSynced = synced.filter((note) => note.action !== "deleted").length;
2913
+ const totalRenamed = synced.filter((note) => note.filePath !== note.filePathOriginal).length;
2914
+ const ankiUnreachable = actionCounts.ankiUnreachable > 0;
2915
+ lines.push(`${result.dryRun ? "Will sync" : ankiUnreachable ? "Failed to sync" : "Successfully synced"} ${totalSynced} ${plur("note", totalSynced)} to Anki${result.dryRun ? "" : ` in ${prettyMilliseconds(result.duration)}`}.`);
2916
+ if (verbose) {
2917
+ lines.push("", result.dryRun ? "Sync Plan Summary:" : "Sync Summary:");
2918
+ for (const [action, count] of Object.entries(actionCounts)) lines.push(` ${capitalize(action)}: ${count}`);
2919
+ if (totalRenamed > 0) lines.push("", `Local notes renamed: ${totalRenamed}`);
2920
+ if (result.deletedDecks.length > 0) lines.push("", `Decks pruned: ${result.deletedDecks.length}`);
2921
+ if (result.deletedMedia.length > 0) lines.push("", `Media assets deleted: ${result.deletedMedia.length}`);
2922
+ if (!result.dryRun) lines.push("", `Database automatically fixed: ${result.fixedDatabase ? "Yes" : "No"}`);
2923
+ lines.push("", result.dryRun ? "Sync Plan Details:" : "Sync Details:");
2924
+ for (const { action, filePath, note } of synced) if (filePath === void 0) lines.push(` Note ID ${note.noteId} ${capitalize(action)} (From Anki)`);
2925
+ else lines.push(` Note ID ${note.noteId} ${capitalize(action)} ${filePath}`);
2926
+ }
2927
+ return lines.join("\n");
2928
+ }
39
2929
 
40
- {{Extra}}{{/Extra}}`,Front:`{{Back}}`,YankiNamespace:`{{YankiNamespace}}`}],inOrderFields:[`Front`,`Back`,`Extra`,`YankiNamespace`],modelName:`Yanki - Basic (and reversed card with extra)`}],K=G.map(e=>e.modelName),ct=[`Yanki - Basic (and reversed card)`];async function lt(e,t,n=!1){if(n)return;let r=t.map(e=>e.noteId).filter(e=>e!==void 0);await e.note.deleteNotes({notes:r})}async function q(e,t,n){if(t.noteId!==void 0)throw Error(`Note already has an ID`);if(n)return 0;let r=await e.note.addNote({note:{...t,options:{allowDuplicate:!0}}}).catch(async r=>{if(r instanceof Error){if(r.message===`model was not found: ${t.modelName}`){let r=G.find(e=>e.modelName===t.modelName);if(r===void 0)throw Error(`Model not found: ${t.modelName}`);return await e.model.createModel(r),q(e,t,n)}if(r.message===`deck was not found: ${t.deckName}`){if(t.deckName===``)throw Error(`Deck name is empty`);return await e.deck.createDeck({deck:t.deckName}),q(e,t,n)}throw r}else throw TypeError(`Unknown error`)});if(r===null)throw Error(`Note creation failed`);return await vt(e,t,n),r}async function ut(e,t,n,r){if(t.noteId===void 0)throw Error(`Local note ID is undefined`);if(n.cards===void 0)throw Error(`Remote note cards are undefined`);let i=!1;if(t.deckName!==n.deckName){if(t.deckName===``)throw Error(`Local deck name is empty`);r||await e.deck.changeDeck({cards:n.cards,deck:t.deckName}),i=!0}return(!pt(t.tags??[],n.tags??[])||!dt(t.fields,n.fields)||t.modelName!==n.modelName)&&(r||(await e.note.updateNoteModel({note:{...t,id:t.noteId,tags:t.tags??[]}}).catch(async i=>{if(i instanceof Error){if(i.message===`Model '${t.modelName}' not found`){let i=G.find(e=>e.modelName===t.modelName);if(i===void 0)throw Error(`Model not found: ${t.modelName}`);return await e.model.createModel(i),ut(e,t,n,r)}throw i}else throw TypeError(`Unknown error`)}),await vt(e,t,r)),i=!0),i}function dt(e,t){for(let n of[`Front`,`Back`,`Extra`])if(n in e&&n in t){if(e[n].normalize(`NFC`)!==t[n].normalize(`NFC`))return!1}else if(n in e||n in t)return!1;return!0}function ft(e,t,n=!0){return!(n&&e.noteId!==t.noteId||e.deckName!==t.deckName||e.modelName!==t.modelName||!dt(e.fields,t.fields)||!pt(e.tags??[],t.tags??[]))}function pt(e,t){let n=new Set(e.map(e=>e.normalize(`NFC`).toLowerCase())),r=new Set(t.map(e=>e.normalize(`NFC`).toLowerCase()));return new Set([...n,...r]).size===r.size}async function J(e,t=M.namespace){return await mt(e,await e.note.findNotes({query:`"YankiNamespace:${t}"`}))}async function mt(e,t){let n=await e.note.notesInfo({notes:t}),r=[];if(n.every(e=>e.noteId===void 0))return Array.from({length:n.length}).fill(void 0);let i=n.flatMap(e=>e.cards??[]),a=await e.deck.getDecks({cards:i}),o=new Map;for(let[e,t]of Object.entries(a))for(let n of t)o.set(n,e);let s=new Map,c=new Map;for(let t of n){if(t.noteId===void 0){r.push(void 0);continue}if(![...ct,...K].includes(t.modelName))throw Error(`Unknown model name ${t.modelName} for note ${t.noteId}`);let n=[...new Set(t.cards.map(e=>o.get(e)))];n.length;let i=n.at(0);if(i===void 0)throw Error(`No deck found for cards in note ${t.noteId}`);if(!s.has(i)){let t=!!(await e.deck.getDeckConfig({deck:i})).dyn;if(s.set(i,t),t){let t=await e.deck.deckNames();for(let n of t)if(!s.has(n)){let t=await e.deck.getDeckConfig({deck:n});s.set(n,!!t.dyn)}let n=[...s.entries()].filter(([e,t])=>!t&&e!==`Default`).map(([e])=>e).sort((e,t)=>t.split(`::`).length-e.split(`::`).length);n.push(`Default`);for(let e of n)c.set(e,void 0)}}if(s.get(i)){let n=c.keys(),r=!1;for(let a of n)if(c.get(a)===void 0&&c.set(a,await e.note.findNotes({query:`"deck:${a}"`})),c.get(a)?.includes(t.noteId)){i=a,r=!0;break}if(!r)throw Error(`No matching non-filtered deck found for note ${t.noteId}`)}r.push({cards:t.cards,deckName:i,fields:{Back:t.fields.Back.value??``,...t.fields.Extra!==void 0&&{Extra:t.fields.Extra.value??``},Front:t.fields.Front.value??``,YankiNamespace:t.fields.YankiNamespace.value??``},modelName:t.modelName,noteId:t.noteId,tags:t.tags})}return r}async function ht(e,t,n,r){let i=[...new Set(t.map(e=>e.deckName))].filter(Boolean),a=[...new Set(n.map(e=>e.deckName))].filter(Boolean).filter(e=>!i.includes(e)),o=[];for(let e of a){let t=e.split(`::`);if(t.length!==1)for(;t.length>1;){t.pop();let e=t.join(`::`);if(i.some(t=>e.includes(t)))break;o.push(e)}}let s=[...new Set([...a,...o])].sort(),c=[];for(let i of s){let a=await e.deck.getDeckStats({decks:[i]}),o=Object.values(a);if(o.length>1){console.warn(`Multiple decks found for deck name: ${i}`);continue}let s=o.at(0);if(s===void 0){console.warn(`Deck not found for deck name: ${i}`);continue}let l=Math.max(s.total_in_deck,s.new_count+s.learn_count+s.review_count);if(r){let e=n.filter(e=>e.deckName===i).length,r=t.filter(e=>e.deckName===i).length;l===e&&r===0&&!c.includes(i)&&c.push(i)}else l===0&&!c.includes(i)&&c.push(i)}return r||await e.deck.deleteDecks({cardsToo:!0,decks:c}),c}async function gt(e,t,n,r){let i;try{let{css:n}=await e.model.modelStyling({modelName:t});i=n}catch(i){if(i instanceof Error){if(i.message===`model was not found: ${t}`){let i=G.find(e=>e.modelName===t);if(i===void 0)throw Error(`Model not found: ${t}`);return r?!1:(await e.model.createModel(i),gt(e,i.modelName,n,r))}throw i}else throw TypeError(`Unknown error`)}return i!==void 0&&i===n?!1:(r||await e.model.updateModelStyling({model:{css:n,name:t}}),!0)}async function _t(e,t=K[0]){let{css:n}=await e.model.modelStyling({modelName:t});return n}async function vt(e,t,n){let r=rt(`${t.fields.Front}\n${t.fields.Back}\n${t.fields.Extra}`),i=[];for(let{filename:t,originalSrc:a}of r)if((await e.media.getMediaFilesNames({pattern:t})).length===0){if(!n)try{let n=await e.media.storeMediaFile(V(a)?{deleteExisting:!0,filename:t,url:a}:{deleteExisting:!0,filename:t,path:a});t!==n&&console.warn(`Anki media filename mismatch: Expected: "${t}" -> Received: "${n}"`)}catch(e){console.warn(`Anki could not store media file: "${t}"\n${String(e)}`)}i.push({filename:t,originalSrc:a})}return i}async function yt(e,t,n,r){if(r)return[];let i=De(n),a=[];for(let e of t){let t=rt(`${e.fields.Front}\n${e.fields.Back}\n${e.fields.Extra}`);for(let{filename:e}of t)a.push(e)}let o=await e.media.getMediaFilesNames({pattern:`${i}-*`}),s=[];for(let t of o)a.includes(t)||(await e.media.deleteMediaFile({filename:t}),s.push(t));return s}async function Y(e){try{let{permission:t}=await e.miscellaneous.requestPermission();if(t===`denied`)throw Error(`Permission denied, please add this source to the "webCorsOriginList" in the Anki-Connect add-on configuration options.`);return`granted`}catch(e){if(e instanceof Error&&(e.message===`fetch failed`||e.message===`net::ERR_CONNECTION_REFUSED`))return`ankiUnreachable`;throw e}}async function X(e){try{await e.miscellaneous.sync()}catch{console.warn(`Could not sync to AnkiWeb.`)}}const bt={...M};async function xt(t){let n=performance.now(),{ankiConnectOptions:i,ankiWeb:a,dryRun:o,namespace:s}=e(bt,t??{}),c=F(s,!0),l=new r(i);if(await Y(l)===`ankiUnreachable`)throw Error(`Anki is unreachable. Is Anki running?`);let u=await J(l,c);await lt(l,u,o);let d=await ht(l,[],u,o),f=await yt(l,[],c,o);return u.length>0||d.length,!o&&a&&await X(l),{ankiWeb:a,deletedDecks:d,deletedMedia:f,deletedNotes:u,dryRun:o,duration:performance.now()-n,namespace:c}}function St(e,r=!1){let i=e.deletedDecks.length,a=e.deletedNotes.length,o=e.deletedMedia.length;if(i===0&&a===0&&o===0)return`Nothing to delete`;let s=[`${e.dryRun?`Will`:`Successfully`} deleted ${a} ${t(`note`,a)}, ${i} ${t(`deck`,i)}, and ${o} media ${t(`asset`,o)} from Anki${e.dryRun?``:` in ${n(e.duration)}`}.`];if(r){if(a>0){s.push(``,e.dryRun?`Notes to delete:`:`Deleted notes:`);for(let t of e.deletedNotes){let e=E(W(t.fields.Front),50);s.push(` Note ID ${t.noteId} ${e}`)}}if(i>0){s.push(``,e.dryRun?`Decks to delete:`:`Deleted decks:`);for(let t of e.deletedDecks)s.push(` ${t}`)}if(o>0){s.push(``,e.dryRun?`Media assets to delete:`:`Deleted media assets:`);for(let t of e.deletedMedia)s.push(` ${t}`)}}return s.join(`
41
- `)}const Ct={...M};async function wt(t){let n=performance.now(),{ankiConnectOptions:i,namespace:a}=e(Ct,t??{}),o=F(a,!0),s=new r(i);if(await Y(s)===`ankiUnreachable`)throw Error(`Anki is unreachable. Is Anki running?`);let c=await J(s,o);return{duration:performance.now()-n,namespace:o,notes:c}}function Tt(e){if(e.notes.length===0)return`No notes found.`;let t=[];for(let n of e.notes){let e=E(W(n.fields.Front),50);t.push(`Note ID ${n.noteId} ${e}`)}return t.join(`
42
- `)}function Et(e,t,n){if(t===`off`)throw Error(`manageFilenames must not be off`);switch(e.modelName){case`Yanki - Basic`:case`Yanki - Basic (and reversed card with extra)`:case`Yanki - Basic (type in the answer)`:{let r=D(Z(e.fields.Front).replace(k,``).replace(A,``)),i=D(Z(e.fields.Back).replace(k,``).replace(A,``));switch(t){case`prompt`:return Z(r??i??``,n);case`response`:return Z(i??r??``,n)}}case`Yanki - Cloze`:{let r=nt(e.fields.Front),i=D(r.split(`{{`).at(0)??``),a=D(/\{\{\w\d*\s?:{0,2}([^:}]+)/.exec(r)?.at(1)),o=D(r.split(`}}`).at(1)?.split(`{{`).at(0)??``);switch(t){case`prompt`:return Z(i??o??a??``,n);case`response`:return Z(a??i??o??``,n)}}}}function Z(e,t){let n=te(W(e).trim(),{maxLength:2**53-1,replacement:` `}).replaceAll(/\s+/g,` `).trim();return n.length===0&&(n=A),n=n.normalize(`NFC`),t===void 0?n:E(n,Math.min(t,108))}function Dt(e,t){let n=At(e,1),r=2;for(;t.includes(n.toLowerCase());)n=At(e,r),r++;return n}function Ot(e,t){let n=At(e,2);return t.includes(n.toLowerCase())?e:kt(e)}function kt(e){let t=e.endsWith(`.`)||e.endsWith(`)`)?void 0:y.extname(e),n=y.basename(e,t).replace(/\s\(\d+\)$/,``);return y.join(y.dirname(e),`${n}${t??``}`)}function At(e,t){let n=y.extname(e),r=`${kt(y.basename(e,n))} (${t})`;return y.join(y.dirname(e),`${r}${n}`)}function jt(e){return`${e}-${ne(8)}`}const Mt={allFilePaths:[],basePath:void 0,convertFilePathsToProtocol:`none`,obsidianVaultName:void 0};function Q(t,n){let{allFilePaths:r,basePath:i,convertFilePathsToProtocol:a,cwd:o,obsidianVaultName:s,type:c}=e(Mt,n??{});a===`obsidian`&&s===void 0&&console.warn(`convertFilePathsToProtocol is 'obsidian', but no obsidianVaultName provided`);let l=Le(t)??t;switch(H(l)){case`localFileName`:{let e=Fe(L(l),`md`);return e=Nt(e,o,r??[])??Ae(l,{basePath:i,cwd:o}),H(e)===`localFilePath`?Q(e,{allFilePaths:r,basePath:i,convertFilePathsToProtocol:a,cwd:o,obsidianVaultName:s,type:c}):(console.warn(`Failed to convert local file wiki-style name to path: ${t} --> ${e}`),e)}case`localFilePath`:{let e=Ae(L(l),{basePath:i,cwd:o}),t=Pt(Fe(e,`md`),r??[])??Pt(Ie(e,`md`),r??[])??void 0;if(t!==void 0){if(a!==`none`&&(c===`link`||c===`embed`&&[`.md`,`.pdf`].includes(Pe(t)))){if(a===`obsidian`&&s!==void 0)return It(t,i??``,s);if(a===`file`)return Ft(t)}return z(t)}return z(e)}case`localFileUrl`:{let e=L(Re(t));return H(e)===`localFilePath`?Q(e,{allFilePaths:r,basePath:i,convertFilePathsToProtocol:a,cwd:o,obsidianVaultName:s,type:c}):(console.warn(`Failed to convert file URL to path: ${t} --> ${e}`),e)}case`obsidianVaultUrl`:return t;case`remoteHttpUrl`:return t;case`unsupportedProtocolUrl`:return console.warn(`Unsupported URL protocol: ${t}`),t}}function Nt(e,t,n){if(n.length===0)return;let[r,i]=R(e),a=r.replace(/\.md$/,``).toLowerCase(),o=n.filter(e=>e.replace(/\.md$/,``).toLowerCase().endsWith(a));if(o.length!==0)return o.length===1?`${o[0]}${i??``}`:`${[...o].sort((e,n)=>{if(!r.endsWith(`.md`)||r.includes(y.sep)){let r=e.startsWith(t);if(r!==n.startsWith(t))return r?-1:1}let i=e.split(y.sep).length,a=n.split(y.sep).length;return i===a?e.localeCompare(n):i-a})[0]}${i??``}`}function Pt(e,t){let n=z(e);if(t.some(e=>e.toLowerCase().endsWith(n.toLowerCase())))return e}function Ft(e){return`file://${e}`}function It(e,t,n){let r=je(e,t);return`obsidian://open?vault=${encodeURIComponent(n)}&file=${encodeURIComponent(r)}`}const Lt=function(e){let{allFilePaths:t=[],basePath:n,cwd:r,enabled:i=!0,obsidianVault:a}=e;return function(e){i&&(_(e,`link`,e=>{e.data??={},e.data.hProperties={...e.data?.hProperties,"data-yanki-src-original":e.url};let i=Q(e.url,{allFilePaths:t,basePath:n,convertFilePathsToProtocol:a===void 0?`none`:`obsidian`,cwd:r,obsidianVaultName:a,type:`link`});e.url=V(i)?i:encodeURI(i)}),_(e,`image`,e=>{e.data??={},e.data.hProperties={...e.data?.hProperties,"data-yanki-src-original":e.url};let i=Q(e.url,{allFilePaths:t,basePath:n,convertFilePathsToProtocol:a===void 0?`none`:`obsidian`,cwd:r,obsidianVaultName:a,type:`embed`});e.url=V(i)?i:encodeURI(i)}))}};function Rt(){let e=``,t;return{enter:{wikiEmbed:r,wikiLabel:o,wikiLink:n,wikiUrl:i},exit:{wikiEmbed:l,wikiLabel:s,wikiLink:c,wikiUrl:a}};function n(n){e=``,t=void 0,this.enter({children:[],title:void 0,type:`link`,url:``},n)}function r(n){e=``,t=void 0,this.enter({type:`image`,url:``},n)}function i(){this.buffer()}function a(){e=this.resume()}function o(){this.buffer()}function s(){t=this.resume()}function c(n){let r=this.stack.at(-1);r.url=de(e),r.children=[{type:`text`,value:D((t??``).replaceAll(`|`,``))??e.split(`#`).pop()??e.split(`/`).pop()??e}],this.exit(n)}function l(n){let r=this.stack.at(-1);r.url=de(e),t!==void 0&&(r.alt=t),this.exit(n)}}function zt(){return{text:{33:{name:`wikiEmbed`,tokenize:e},91:{name:`wikiLink`,tokenize:e}}};function e(e,t,n){let r=!1,i=!1,a=0,o=0;return s;function s(t){return t===33?(r=!0,e.enter(`wikiEmbed`),e.enter(`wikiMarker`),e.consume(t),c):(e.enter(`wikiLink`),e.enter(`wikiMarker`),c(t))}function c(t){return t===91?(e.consume(t),l):n(t)}function l(t){return t===91?(e.consume(t),e.exit(`wikiMarker`),u):n(t)}function u(t){return t===-5||t===-4||t===-3||t===null||t===124||t===93?n(t):(e.enter(`wikiUrl`),e.enter(`chunkString`,{contentType:`string`}),e.consume(t),a++,d)}function d(t){return t===-5||t===-4||t===-3||t===null?n(t):t===124?a===1?n(t):x(t):t===92?e.check({partial:!0,tokenize:b},x,f)(t):t===93?h(t):f(t)}function f(t){return e.consume(t),a++,d}function p(t){return t===-5||t===-4||t===-3||t===null?n(t):t===93?h(t):(e.enter(`wikiLabel`),e.enter(`chunkString`,{contentType:`string`}),e.consume(t),o++,m)}function m(t){return t===-5||t===-4||t===-3||t===null?n(t):t===93?h(t):(e.consume(t),o++,m)}function h(t){return t===93?e.check({partial:!0,tokenize:y},g,v)(t):n(t)}function g(t){return t===93?(i?o>0&&(e.exit(`chunkString`),e.exit(`wikiLabel`)):a>0&&(e.exit(`chunkString`),e.exit(`wikiUrl`)),e.enter(`wikiMarker`),e.consume(t),_):n(t)}function _(i){return i!==93||a===0?n(i):(e.consume(i),e.exit(`wikiMarker`),r?e.exit(`wikiEmbed`):e.exit(`wikiLink`),t)}function v(t){return t===93?(e.consume(t),i?(o++,m):(a++,d)):n(t)}function y(e,t,n){return r;function r(r){return r===93?(e.enter(`wikiMarkerTemp`),e.consume(r),e.exit(`wikiMarkerTemp`),t(r)):n(r)}}function b(e,t,n){return r;function r(t){return t===92?(e.enter(`wikiMarkerTemp`),e.consume(t),i):n(t)}function i(r){return r===124?(e.consume(r),e.exit(`wikiMarkerTemp`),t(r)):(e.exit(`wikiMarkerTemp`),n(r))}}function x(t){return e.exit(`chunkString`),e.exit(`wikiUrl`),e.enter(`wikiMarker`),e.consume(t),t===92?C:(e.exit(`wikiMarker`),i=!0,e.check({partial:!0,tokenize:S},h,p)(t))}function S(e,t,n){return r;function r(r){return r===93?(e.enter(`wikiMarkerTemp`),e.consume(r),e.exit(`wikiMarkerTemp`),t(r)):n(r)}}function C(t){return t===124?(e.consume(t),e.exit(`wikiMarker`),i=!0,p):n(t)}}}const Bt=function(){let e=this.data();e.micromarkExtensions=[...e.micromarkExtensions??[],zt()],e.fromMarkdownExtensions=[...e.fromMarkdownExtensions??[],Rt()]},Vt={...M};async function Ht(t,n){let{allFilePaths:r,basePath:i,cwd:a,obsidianVault:o,resolveUrls:s}=e(Vt,n??{}),c=f().use(ce).use(ie,[{anywhere:!1,marker:`-`,type:`yaml`}]).use(Bt).use(ae,{singleTilde:!1}).use(Lt,{allFilePaths:r,basePath:i,cwd:a,enabled:s,obsidianVault:o}).use(se).use(oe).use(w).use(re);return c.run(c.parse(t))}function Ut(e){return e.type===`text`}function Wt(e,t){return _(e,t,(e,t,n)=>{if(n&&t!==void 0)return n.children.splice(t,1),h}),e}function Gt(e){let t=e.at(0);t?.type===`text`&&(t.value=t.value.trimStart(),t.value===``&&e.shift());let n=e.at(-1);return n?.type===`text`&&(n.value=n.value.trimEnd(),n.value===``&&e.pop()),e}function Kt(e){let t=1;return _(e,`delete`,(e,n,r)=>{if(r===void 0||n===void 0||!(`children`in e)||e.children.length===0)return m;if(e.children.length>0&&Ut(e.children[0])){let n=/^[(|]?(\d{1,2})(?:[\s).|]|$)(.*)$/.exec(e.children[0].value);if(n!==null&&(e.children.length>1||(n.at(2)??``).length>0)){let r=Number.parseInt(n.at(1)??``,10);Number.isNaN(r)||(t=r,e.children[0].value=(n.at(2)?.trim().length??0)>0?n.at(2)??``:``)}}let i=e.children.at(-1),a=e.children.length>1&&i?.type===`emphasis`?[p(`text`,`{{c${t}::`),...Gt(e.children.slice(0,-1)),p(`text`,`::`),...Gt(e.children.slice(-1)),p(`text`,`}}`)]:[p(`text`,`{{c${t}::`),...Gt(e.children),p(`text`,`}}`)];r.children.splice(n,1,...a),t+=1}),e}function $(e){let t,n=!1;return _(e,`thematicBreak`,(e,r,i)=>r===void 0||i===void 0?m:(t=r,i.children[r+1]?.type===`thematicBreak`&&(n=!0),h)),t===void 0?[e,void 0]:[{children:e.children.slice(0,t),type:`root`},{children:e.children.slice(t+(n?2:1)),type:`root`}]}function qt(e){let t;if(_(e,e=>{if(e.type===`thematicBreak`)return t=void 0,h;if(e.type===`delete`)return t=`Yanki - Cloze`,h}),t!==void 0)return t;if(!Yt(e)&&Xt(e))return`Yanki - Basic (type in the answer)`;let n;return _(e,(e,r,i)=>{if(i===null||r===null)return m;if(e.type===`thematicBreak`){if(t===void 0)t=`Yanki - Basic`;else if(t===`Yanki - Basic`&&n?.type===`thematicBreak`)return t=`Yanki - Basic (and reversed card with extra)`,h}n=e}),t??`Yanki - Basic`}function Jt(e){let t;if(_(e,`yaml`,e=>`value`in e?(t=e.value,h):m),!t)return{};let n=le(t);if(!n)throw Error(`Could not parse frontmatter`);return n}function Yt(e){let t=!1;return _(e,`thematicBreak`,()=>(t=!0,h)),t}function Xt(e){let t,n=0;return _(e,e=>{if(e.type===`text`&&e.value.trim()!==``)t=e,n++;else if(e.type===`emphasis`&&e.children.some(e=>e.type===`text`&&e.value.trim()!==``))return t=e,n++,g}),t?.type===`emphasis`&&n>1}function Zt(e){let t,n,r;if(_(e,`emphasis`,(e,i,a)=>{if(a===void 0||i===void 0||e.type!==`emphasis`)return m;t=e,n=a,r=i}),n&&t&&typeof r==`number`)return n.children.splice(r,1),t}const Qt={namespaceValidationAndSanitization:!0,...M};async function $t(t,n){let{allFilePaths:r,basePath:i,cwd:a,fetchAdapter:o=P(),fileAdapter:s=await N(),namespace:c,namespaceValidationAndSanitization:l,obsidianVault:u,resolveUrls:d,strictLineBreaks:f,syncMediaAssets:m}=e(Qt,n??{}),h=l?F(c):c,g=await Ht(t,{allFilePaths:r,basePath:i,cwd:a,obsidianVault:u,resolveUrls:d}),_=qt(g),v=Jt(g);g=Wt(g,`yaml`);let y=``,b=``,x;switch(_){case`Yanki - Basic`:case`Yanki - Basic (and reversed card with extra)`:{let[e,t]=$(g),n;if(t!==void 0&&_===`Yanki - Basic (and reversed card with extra)`){x=``;let[e,r]=$(t);t=e,n=r}y=await U(e,{cssClassNames:[O,`namespace-${h}`,`front`,`model-${_}`],fetchAdapter:o,fileAdapter:s,namespace:h,strictLineBreaks:f,syncMediaAssets:m,useEmptyPlaceholder:!0}),b=await U(t,{cssClassNames:[O,`namespace-${h}`,`back`,`model-${_}`],fetchAdapter:o,fileAdapter:s,namespace:h,strictLineBreaks:f,syncMediaAssets:m,useEmptyPlaceholder:!0}),n!==void 0&&(x=await U(n,{cssClassNames:[O,`namespace-${h}`,`extra`,`model-${_}`],fetchAdapter:o,fileAdapter:s,namespace:h,strictLineBreaks:f,syncMediaAssets:m,useEmptyPlaceholder:!1}));break}case`Yanki - Basic (type in the answer)`:{let e=Zt(g);if(e===void 0)throw Error(`Could not find emphasis in Basic (type in the answer) note AST.`);let t=g,n=p(`root`,p(`paragraph`,e.children));y=await U(t,{cssClassNames:[O,`namespace-${h}`,`front`,`model-${_}`],fetchAdapter:o,fileAdapter:s,namespace:h,strictLineBreaks:f,syncMediaAssets:m,useEmptyPlaceholder:!0}),b=await U(n,{cssClassNames:[O,`namespace-${h}`,`back`,`model-${_}`],fetchAdapter:o,fileAdapter:s,namespace:h,strictLineBreaks:f,syncMediaAssets:m,useEmptyPlaceholder:!1});break}case`Yanki - Cloze`:{let[e,t]=$(g);y=await U(Kt(e),{cssClassNames:[O,`namespace-${h}`,`front`,`model-${_}`],fetchAdapter:o,fileAdapter:s,namespace:h,strictLineBreaks:f,syncMediaAssets:m,useEmptyPlaceholder:!0}),b=await U(t,{cssClassNames:[O,`namespace-${h}`,`back`,`model-${_}`],fetchAdapter:o,fileAdapter:s,namespace:h,strictLineBreaks:f,syncMediaAssets:m,useEmptyPlaceholder:!1});break}}return{deckName:``,fields:{Back:b,...x!==void 0&&{Extra:x},Front:y,YankiNamespace:h},modelName:_,noteId:v.noteId??void 0,tags:en(v.tags)}}function en(e){return(typeof e==`string`?[e]:e??[]).map(e=>e.replaceAll(`/`,`::`))}const tn={...M};async function nn(t,n){let{allFilePaths:r,basePath:i,fetchAdapter:a=P(),fileAdapter:o=await N(),namespace:s,obsidianVault:c,strictLineBreaks:l,syncMediaAssets:u}=e(tn,n??{}),d=F(s);t.sort((e,t)=>e.localeCompare(t));let f=an(t);return await Promise.all(t.map(async(e,t)=>{let n=await o.readFile(e),s=await $t(n,{allFilePaths:r,basePath:i,cwd:y.dirname(e),fetchAdapter:a,fileAdapter:o,namespace:d,namespaceValidationAndSanitization:!1,obsidianVault:c,strictLineBreaks:l,syncMediaAssets:u});return s.deckName===``&&(s.deckName=f[t]),{filePath:e,filePathOriginal:e,markdown:n,note:s}}))}const rn={mode:`common-root`};function an(t,n){let{mode:r}=e(rn,n??{});if(t.length===0)return[];let i=t.map(e=>y.dirname(e).split(y.sep)),a=i.reduce((e,t)=>e.filter((e,n)=>e===t[n])),o=i.some(e=>e.at(-1)===a.at(-1)),s=r===`common-parent`||o?1:0;return i.map(e=>e.slice(a.length-s).join(`::`))}const on={...M};async function sn(t,n){let{dryRun:r,fileAdapter:i=await N(),manageFilenames:a,maxFilenameLength:o}=e(on,n??{});if(a!==`off`){let e=[];for(let n of t){let{filePath:t,note:r}=n;if(t===void 0)throw Error(`File path is undefined`);let i=Et(r,a,o),s=Dt(y.join(y.dirname(t),`${i}${y.extname(t)}`),e);n.filePath=s,e.push(s.toLowerCase())}for(let n of t){let{filePath:t}=n;if(t===void 0)throw Error(`File path is undefined`);n.filePath=Ot(t,e)}let n=new Map;for(let e of t){let{filePath:a,filePathOriginal:o}=e;if(o===void 0)throw Error(`Original file path is undefined.`);if(a===void 0)throw Error(`File path is undefined.`);if(a===o)continue;let s=a;t.some(({filePath:e,filePathOriginal:t})=>e!==a&&t?.toLowerCase()===a.toLowerCase())&&(s=jt(a),n.set(s,a)),r||await i.rename(o,s)}for(let[e,t]of n)r||await i.rename(e,t)}return t.sort((e,t)=>e.filePath.localeCompare(t.filePath)),t}const cn={...M};async function ln(t,n){let{allFilePaths:r,basePath:i,dryRun:a,fetchAdapter:o=P(),fileAdapter:s=await N(),manageFilenames:c,maxFilenameLength:l,namespace:u,obsidianVault:d,strictLineBreaks:f,syncMediaAssets:p}=e(cn,n??{}),m=t.map(e=>L(e)),h=i===void 0?void 0:L(i);return{dryRun:a,notes:await sn(await nn(m,{allFilePaths:r.map(e=>L(e)),basePath:h,fetchAdapter:o,fileAdapter:s,namespace:F(u),obsidianVault:d,strictLineBreaks:f,syncMediaAssets:p}),{dryRun:a,fileAdapter:s,manageFilenames:c,maxFilenameLength:l})}}const un={css:_e,...M},dn={...M};async function fn(t){let{ankiConnectOptions:n}=e(un,t??{}),i=new r(n);if(await Y(i)===`ankiUnreachable`)throw Error(`Anki is unreachable. Is Anki running?`);let a=new Set;for(let e of K){let t=await _t(i,e);a.add(t)}if(a.size===0)throw Error(`No CSS found in any Yanki model.`);if(a.size>1)throw Error(`Expected all Yanki models to have identical CSS.`);return[...a][0]}async function pn(t){let n=performance.now(),{ankiConnectOptions:i,ankiWeb:a,css:o,dryRun:s}=e(un,t??{}),c=new r(i);if(await Y(c)===`ankiUnreachable`)throw Error(`Anki is unreachable. Is Anki running?`);let l=[];for(let e of K){let t=await gt(c,e,o,s);l.push({action:t?`updated`:`unchanged`,name:e})}return l.some(e=>e.action!==`unchanged`),!s&&a&&await X(c),{ankiWeb:a,dryRun:s,duration:performance.now()-n,models:l}}function mn(e,r=!1){let i=[],a=e.models.filter(e=>e.action===`unchanged`),o=e.models.filter(e=>e.action===`updated`);if(i.push(`${e.dryRun?`Will`:`Successfully`} update ${o.length} ${t(`model`,o.length)} and left ${a.length} ${t(`model`,a.length)} unchanged${e.dryRun?``:` in ${n(e.duration)}`}.`),r){if(o.length>0){i.push(``,e.dryRun?`Models to update:`:`Updated models:`);for(let e of o)i.push(` ${e.name}`)}if(a.length>0){i.push(``,e.dryRun?`Models unchanged:`:`Unchanged models:`);for(let e of a)i.push(` ${e.name}`)}}return i.join(`
43
- `)}async function hn(e,t){let[n,r]=gn(e),i=e.split(/\r?\n/);if(n===void 0||r===void 0)return t===void 0?e:[`---`,ue({noteId:t}).trim(),`---
44
- `,...i].join(`
45
- `);let a=await le(i.slice(n+1,r).join(`
46
- `))??{};if(t===void 0){if(delete a.noteId,Object.keys(a).length===0){let e=i.slice(r+1);return e[0].trim()===``?e.slice(1).join(`
47
- `):e.join(`
48
- `)}}else a.noteId=t;let o=ue(a,{lineWidth:0}).trim();return[...i.slice(0,n+1),o,...i.slice(r)].join(`
49
- `)}function gn(e){let t=e.split(/\r?\n/);if(!t.join(``).trim().startsWith(`---`))return[void 0,void 0];let n=t.findIndex(e=>e.startsWith(`---`)),r=t.findIndex((e,t)=>t>n&&e.startsWith(`---`));return n===-1||r===-1?[void 0,void 0]:[n,r]}const _n={...M};async function vn(t,n){let i=performance.now(),a=structuredClone(t),{ankiConnectOptions:o,ankiWeb:s,checkDatabase:c,dryRun:l,namespace:u,strictMatching:d}=e(_n,n??{}),f=F(u),p=[],m=new r(o);if(await Y(m)===`ankiUnreachable`)return{ankiWeb:s,deletedDecks:[],deletedMedia:[],dryRun:l,duration:performance.now()-i,fixedDatabase:!1,namespace:f,synced:a.map(e=>({action:`ankiUnreachable`,note:e}))};for(let e of a)e.deckName===``&&(e.deckName=`Yanki`);let h=await J(m,`*`),g=h.filter(e=>e.fields.YankiNamespace===f);for(let e of a){if(e.noteId===void 0)continue;let t=yn(a,e.noteId);if(t.length<=1)continue;let n=bn(t,g.find(t=>t.noteId===e.noteId));for(let e of t)e!==n&&(e.noteId=void 0)}let _=new Set(a.filter(e=>e.noteId!==void 0&&g.some(t=>e.noteId===t.noteId)).map(e=>e.noteId));for(let e of a){let t=h.find(t=>t.noteId===e.noteId);if(t?.fields.YankiNamespace!==f&&(e.noteId=void 0,t=void 0),t===void 0)e.noteId=d?void 0:xn(e,g,_),e.noteId===void 0?(e.noteId=await q(m,{...e,noteId:void 0},l),p.push({action:`created`,note:e})):p.push({action:`matched`,note:e});else{if(t.noteId===void 0)throw Error(`Remote note ID is undefined`);let n=await ut(m,e,t,l);p.push({action:n?`updated`:`unchanged`,note:e})}if(e.noteId===void 0)throw Error(`Note ID is undefined`);_.add(e.noteId)}let v=g.filter(e=>!a.some(t=>t.noteId===e.noteId));await lt(m,v,l);for(let e of v)p.push({action:`deleted`,note:e});let y=[],b=[];for(let e of p)e.action===`deleted`?b.push(e.note):y.push(e.note);let x=await ht(m,y,g,l),S=!1;if(c){let e=g.filter(e=>p.some(t=>t.action===`updated`&&t.note.noteId===e.noteId&&t.note.modelName!==e.modelName));if(e.length>0){let t=e.flatMap(({cards:e})=>e??[]);try{await m.card.cardsInfo({cards:t})}catch{S=!0,await m.graphical.guiCheckDatabase()}}}let C=await yt(m,y,f,l);return x.length>0||p.some(e=>e.action!==`unchanged`),!l&&s&&await X(m),{ankiWeb:s,deletedDecks:x,deletedMedia:C,dryRun:l,duration:performance.now()-i,fixedDatabase:S,namespace:f,synced:p}}function yn(e,t){return e.filter(e=>e.noteId===void 0?!1:e.noteId===t)}function bn(e,t){return e.find(e=>e.fields.Front===t?.fields.Front&&e.fields.Back===t.fields.Back&&e.fields.Extra===t.fields.Extra)??e[0]}function xn(e,t,n){return t.find(t=>t.noteId!==void 0&&!n.has(t.noteId)&&ft(e,t,!1))?.noteId??void 0}const Sn={...M,..._n};async function Cn(t,n){let r=performance.now(),{allFilePaths:i,ankiConnectOptions:a,ankiWeb:o,basePath:s,checkDatabase:c,dryRun:l,fetchAdapter:u=P(),fileAdapter:d=await N(),manageFilenames:f,maxFilenameLength:p,namespace:m,obsidianVault:h,strictLineBreaks:g,strictMatching:_,syncMediaAssets:v}=e(Sn,n??{}),y=t.map(e=>L(e)),b=s===void 0?void 0:L(s),x=i.map(e=>L(e)),S=F(m),C=await sn(await nn(y,{allFilePaths:x,basePath:b,fetchAdapter:u,fileAdapter:d,namespace:S,obsidianVault:h,strictLineBreaks:g,syncMediaAssets:v}),{dryRun:l,fileAdapter:d,manageFilenames:f,maxFilenameLength:p});if(h!==void 0&&C.some(e=>e.filePath!==e.filePathOriginal)){let e=await nn(C.map(e=>e.filePath),{allFilePaths:x.map(e=>{let t=C.find(t=>t.filePathOriginal===e);return t?t.filePath:e}),basePath:b,fetchAdapter:u,fileAdapter:d,namespace:S,obsidianVault:h,strictLineBreaks:g,syncMediaAssets:v});for(let[t,n]of C.entries())n.note=e[t].note}let{deletedDecks:ee,deletedMedia:te,fixedDatabase:ne,synced:re}=await vn(C.map(e=>e.note),{ankiConnectOptions:a,ankiWeb:o,checkDatabase:c,dryRun:l,namespace:S,strictMatching:_}),w=re.filter(e=>e.action!==`deleted`);for(let[e,t]of C.entries()){let n=w[e];if((t.note.noteId===void 0||t.note.noteId!==n.note.noteId)&&n.action!==`ankiUnreachable`){let e=await hn(t.markdown,n.note.noteId);l||await d.writeFile(t.filePath,e)}n.filePath=t.filePath,n.filePathOriginal=t.filePathOriginal}let ie=[...re.filter(e=>e.action===`deleted`).map(e=>({action:`deleted`,filePath:void 0,filePathOriginal:void 0,note:e.note})),...w].sort((e,t)=>(e.filePath??``).localeCompare(t.filePath??``));return{ankiWeb:o,deletedDecks:ee,deletedMedia:te,dryRun:l,duration:performance.now()-r,fixedDatabase:ne,namespace:S,synced:ie}}function wn(e,r=!1){let i=[],{synced:a}=e,o=a.reduce((e,t)=>(e[t.action]=(e[t.action]||0)+1,e),{}),s=a.filter(e=>e.action!==`deleted`).length,c=a.filter(e=>e.filePath!==e.filePathOriginal).length,l=o.ankiUnreachable>0;if(i.push(`${e.dryRun?`Will sync`:l?`Failed to sync`:`Successfully synced`} ${s} ${t(`note`,s)} to Anki${e.dryRun?``:` in ${n(e.duration)}`}.`),r){i.push(``,e.dryRun?`Sync Plan Summary:`:`Sync Summary:`);for(let[e,t]of Object.entries(o))i.push(` ${fe(e)}: ${t}`);c>0&&i.push(``,`Local notes renamed: ${c}`),e.deletedDecks.length>0&&i.push(``,`Decks pruned: ${e.deletedDecks.length}`),e.deletedMedia.length>0&&i.push(``,`Media assets deleted: ${e.deletedMedia.length}`),e.dryRun||i.push(``,`Database automatically fixed: ${e.fixedDatabase?`Yes`:`No`}`),i.push(``,e.dryRun?`Sync Plan Details:`:`Sync Details:`);for(let{action:e,filePath:t,note:n}of a)t===void 0?i.push(` Note ID ${n.noteId} ${fe(e)} (From Anki)`):i.push(` Note ID ${n.noteId} ${fe(e)} ${t}`)}return i.join(`
50
- `)}export{xt as cleanNotes,bt as defaultCleanOptions,Qt as defaultGetNoteFromMarkdownOptions,dn as defaultGetStyleOptions,Ct as defaultListOptions,cn as defaultRenameFilesOptions,un as defaultSetStyleOptions,Sn as defaultSyncFilesOptions,_n as defaultSyncNotesOptions,St as formatCleanResult,Tt as formatListResult,mn as formatSetStyleResult,wn as formatSyncFilesResult,$t as getNoteFromMarkdown,fn as getStyle,Ge as hostAndPortToUrl,wt as listNotes,ln as renameFiles,pn as setStyle,Cn as syncFiles,vn as syncNotes,We as urlToHostAndPort};
2930
+ //#endregion
2931
+ export { cleanNotes, defaultCleanOptions, defaultGetNoteFromMarkdownOptions, defaultGetStyleOptions, defaultListOptions, defaultRenameFilesOptions, defaultSetStyleOptions, defaultSyncFilesOptions, defaultSyncNotesOptions, formatCleanResult, formatListResult, formatSetStyleResult, formatSyncFilesResult, getNoteFromMarkdown, getStyle, hostAndPortToUrl, listNotes, renameFiles, setStyle, syncFiles, syncNotes, urlToHostAndPort };