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.
- package/dist/bin/abap-lNEwo1Fh.js +1 -0
- package/dist/bin/actionscript-3-WtXz7Nc9.js +1 -0
- package/dist/bin/ada-CxieIYPC.js +1 -0
- package/dist/bin/andromeeda-CEVX-4Db.js +1 -0
- package/dist/bin/angular-html-CAOiQ-Sh.js +1 -0
- package/dist/bin/angular-html-qzs4hSq7.js +1 -0
- package/dist/bin/angular-ts-CugMBDjk.js +1 -0
- package/dist/bin/apache-zgaAodRu.js +1 -0
- package/dist/bin/apex-BNA6bSzI.js +1 -0
- package/dist/bin/apl-BOFh_hoE.js +1 -0
- package/dist/bin/applescript-D9ApUiQg.js +1 -0
- package/dist/bin/ara-Xn981wPy.js +1 -0
- package/dist/bin/asciidoc-C90J3qhX.js +1 -0
- package/dist/bin/asm-Cvg1-8xg.js +1 -0
- package/dist/bin/astro-Cm_dHcy3.js +1 -0
- package/dist/bin/aurora-x-BLDZZUdf.js +1 -0
- package/dist/bin/awk-BjDw91sP.js +1 -0
- package/dist/bin/ayu-dark-CV3szcXN.js +1 -0
- package/dist/bin/ayu-light-VFwhcb7b.js +1 -0
- package/dist/bin/ayu-mirage-jd_AuG1F.js +1 -0
- package/dist/bin/ballerina-vBN0WIlg.js +1 -0
- package/dist/bin/bat-C7p6Ubpb.js +1 -0
- package/dist/bin/beancount-zb-3xb6T.js +1 -0
- package/dist/bin/berry-B_fAwMGD.js +1 -0
- package/dist/bin/bibtex-AJFrnpSy.js +1 -0
- package/dist/bin/bicep-ZTMcEuYT.js +1 -0
- package/dist/bin/bird2-_hZ3FStw.js +1 -0
- package/dist/bin/blade-U1S3oe2k.js +1 -0
- package/dist/bin/bsl-0ORkkYmh.js +1 -0
- package/dist/bin/c-CdETmdiZ.js +1 -0
- package/dist/bin/c-DIXYfOSP.js +1 -0
- package/dist/bin/c3-CF_7Rl_o.js +1 -0
- package/dist/bin/cadence-BoKQX0Lv.js +1 -0
- package/dist/bin/cairo-kiUMtAsX.js +1 -0
- package/dist/bin/catppuccin-frappe-CHSjY7K5.js +1 -0
- package/dist/bin/catppuccin-latte-BIei23jc.js +1 -0
- package/dist/bin/catppuccin-macchiato-stEzqrNw.js +1 -0
- package/dist/bin/catppuccin-mocha-Csibodaz.js +1 -0
- package/dist/bin/clarity-CF2X0I3d.js +1 -0
- package/dist/bin/cli.js +309 -22
- package/dist/bin/clojure-D4EmLgj3.js +1 -0
- package/dist/bin/cmake-BADnWbaq.js +1 -0
- package/dist/bin/cmake-D8YNkuR-.js +1 -0
- package/dist/bin/cobol-0fKuGiZv.js +1 -0
- package/dist/bin/codeowners-CKcVmpS-.js +1 -0
- package/dist/bin/codeql-aBgygx2C.js +1 -0
- package/dist/bin/coffee-7hoZPzSj.js +1 -0
- package/dist/bin/common-lisp-C0AzPNil.js +1 -0
- package/dist/bin/coq-FNCdvNHe.js +1 -0
- package/dist/bin/cpp-5C3nn41J.js +1 -0
- package/dist/bin/cpp-gdwUInvu.js +1 -0
- package/dist/bin/crystal-Cc66vuxC.js +1 -0
- package/dist/bin/csharp-2Bjq_Rxi.js +1 -0
- package/dist/bin/csharp-CIll8D6u.js +1 -0
- package/dist/bin/css-C-sfFA03.js +1 -0
- package/dist/bin/css-CWs_acR3.js +1 -0
- package/dist/bin/csv-BU_TMxAS.js +1 -0
- package/dist/bin/csv-DxS-HYUf.js +1 -0
- package/dist/bin/cue-DV_1hEYT.js +1 -0
- package/dist/bin/cypher-DrvyYDIr.js +1 -0
- package/dist/bin/d-Dly9ufdX.js +1 -0
- package/dist/bin/dark-plus-B42qAV4C.js +1 -0
- package/dist/bin/dart-C2ROqx0Z.js +1 -0
- package/dist/bin/dax-vDoOL-fx.js +1 -0
- package/dist/bin/desktop-DoZVZZ4h.js +1 -0
- package/dist/bin/diff-By4RszRJ.js +1 -0
- package/dist/bin/diff-DP2jXnqp.js +1 -0
- package/dist/bin/docker-Bp8c7Dqi.js +1 -0
- package/dist/bin/dotenv-CEJeNbhS.js +1 -0
- package/dist/bin/dracula-DmN2zRHi.js +1 -0
- package/dist/bin/dracula-soft-CMTkd8vI.js +1 -0
- package/dist/bin/dream-maker-D3zsoW7J.js +1 -0
- package/dist/bin/edge-DetFnTa9.js +1 -0
- package/dist/bin/elixir-CrM98V7h.js +1 -0
- package/dist/bin/elm-CD-7RX5d.js +1 -0
- package/dist/bin/emacs-lisp-Cj_Ixk9i.js +1 -0
- package/dist/bin/erb-KO-P0SR0.js +1 -0
- package/dist/bin/erlang-BFKCgnPw.js +1 -0
- package/dist/bin/everforest-dark-C1ZUi0Ki.js +1 -0
- package/dist/bin/everforest-light-BiIEIFyN.js +1 -0
- package/dist/bin/fennel-DAD8VWfy.js +1 -0
- package/dist/bin/fish-G6fwZxok.js +1 -0
- package/dist/bin/fluent-BOBTNP2g.js +1 -0
- package/dist/bin/fortran-fixed-form-t-oisds5.js +1 -0
- package/dist/bin/fortran-free-form-By4p5LLA.js +1 -0
- package/dist/bin/fortran-free-form-C3X_sfp9.js +1 -0
- package/dist/bin/fsharp-Dq5iPDb2.js +1 -0
- package/dist/bin/gdresource-BNzPbQHR.js +1 -0
- package/dist/bin/gdscript-DF4DHgCD.js +1 -0
- package/dist/bin/gdscript-bYJURCOy.js +1 -0
- package/dist/bin/gdshader-BPirqxsQ.js +1 -0
- package/dist/bin/gdshader-KOuf2xVk.js +1 -0
- package/dist/bin/genie-DHT45pJu.js +1 -0
- package/dist/bin/gherkin-BOftJ3Qu.js +1 -0
- package/dist/bin/git-commit-Bkc1RnxN.js +1 -0
- package/dist/bin/git-rebase-BfAeU5XI.js +1 -0
- package/dist/bin/github-dark-Dhlw38v_.js +1 -0
- package/dist/bin/github-dark-default-ChurEZVr.js +1 -0
- package/dist/bin/github-dark-dimmed-Bg8moaOR.js +1 -0
- package/dist/bin/github-dark-high-contrast-BuMAw1v5.js +1 -0
- package/dist/bin/github-light-DUFhx0nQ.js +1 -0
- package/dist/bin/github-light-default-DhNuYLfL.js +1 -0
- package/dist/bin/github-light-high-contrast-VDpphmUr.js +1 -0
- package/dist/bin/gleam-BQORp6Ww.js +1 -0
- package/dist/bin/glimmer-js-BZd4ZfVU.js +1 -0
- package/dist/bin/glimmer-ts-UFNn11p7.js +1 -0
- package/dist/bin/glsl-DFAnV4bR.js +1 -0
- package/dist/bin/glsl-r9XJn22U.js +1 -0
- package/dist/bin/gn-BUvkHyO2.js +1 -0
- package/dist/bin/gnuplot-BiNqzIdJ.js +1 -0
- package/dist/bin/go-GcrXPybz.js +1 -0
- package/dist/bin/go-b5i9ZOwA.js +1 -0
- package/dist/bin/graphql-AasBDd3n.js +1 -0
- package/dist/bin/graphql-J1xs8wG6.js +1 -0
- package/dist/bin/groovy-DZM0Oins.js +1 -0
- package/dist/bin/gruvbox-dark-hard-B0-6V_Sa.js +1 -0
- package/dist/bin/gruvbox-dark-medium-B2kDYkf7.js +1 -0
- package/dist/bin/gruvbox-dark-soft-D80qdOIx.js +1 -0
- package/dist/bin/gruvbox-light-hard-np83Fhw4.js +1 -0
- package/dist/bin/gruvbox-light-medium--BtU_nk6.js +1 -0
- package/dist/bin/gruvbox-light-soft-BQQJZmHA.js +1 -0
- package/dist/bin/hack-DTChbFem.js +1 -0
- package/dist/bin/haml-Dz37fDMr.js +1 -0
- package/dist/bin/haml-lACpmly2.js +1 -0
- package/dist/bin/handlebars-B7ppocUa.js +1 -0
- package/dist/bin/haskell-ChFomDou.js +1 -0
- package/dist/bin/haxe-B4EsxHyL.js +1 -0
- package/dist/bin/haxe-Bgb4tX8C.js +1 -0
- package/dist/bin/hcl-B_oQREVc.js +1 -0
- package/dist/bin/hjson-B6rsoOCm.js +1 -0
- package/dist/bin/hlsl-CxhYg9sd.js +1 -0
- package/dist/bin/hlsl-Dzk9GcSJ.js +1 -0
- package/dist/bin/horizon-DC2iZgoY.js +1 -0
- package/dist/bin/horizon-bright-yXhu6egb.js +1 -0
- package/dist/bin/houston-CAkU4Xzv.js +1 -0
- package/dist/bin/html-Crjq5rc3.js +1 -0
- package/dist/bin/html-JSn89dCS.js +1 -0
- package/dist/bin/html-derivative-CsguVNJC.js +1 -0
- package/dist/bin/html-derivative-DhivRM2V.js +1 -0
- package/dist/bin/http-CfCri10f.js +1 -0
- package/dist/bin/hurl-C4sIrBP8.js +1 -0
- package/dist/bin/hxml-BY0JJAFc.js +1 -0
- package/dist/bin/hy-BzSjeCAF.js +1 -0
- package/dist/bin/imba-BCVM_gFu.js +1 -0
- package/dist/bin/ini-CK7-QP3O.js +1 -0
- package/dist/bin/java-BGzTmoXK.js +1 -0
- package/dist/bin/java-Dgsz0B-P.js +1 -0
- package/dist/bin/javascript-BNwJ9fBE.js +1 -0
- package/dist/bin/javascript-DTXNtrg0.js +1 -0
- package/dist/bin/jinja-D4pZPcl3.js +1 -0
- package/dist/bin/jison-CU_JndNM.js +1 -0
- package/dist/bin/json-BadQRMks.js +1 -0
- package/dist/bin/json-Dttibclf.js +1 -0
- package/dist/bin/json5-D-uNcxgL.js +1 -0
- package/dist/bin/jsonc-Mhn2hM5D.js +1 -0
- package/dist/bin/jsonl-DEwECwXy.js +1 -0
- package/dist/bin/jsonnet-aP9lctbz.js +1 -0
- package/dist/bin/jssm-Bzk9BSR5.js +1 -0
- package/dist/bin/jsx-CoE327E5.js +1 -0
- package/dist/bin/jsx-YopGa7XH.js +1 -0
- package/dist/bin/julia-DjiOTm9M.js +1 -0
- package/dist/bin/just-oDTZGZbN.js +1 -0
- package/dist/bin/kanagawa-dragon-DRuqSBxu.js +1 -0
- package/dist/bin/kanagawa-lotus-DFk8eUao.js +1 -0
- package/dist/bin/kanagawa-wave-wqQYYPW0.js +1 -0
- package/dist/bin/kdl-BBdrnnFN.js +1 -0
- package/dist/bin/kotlin-BOAeJ3Zn.js +1 -0
- package/dist/bin/kusto-D9rJ_wbq.js +1 -0
- package/dist/bin/laserwave-po4bDaOp.js +1 -0
- package/dist/bin/latex-B8d5LcJ8.js +1 -0
- package/dist/bin/lean--6SzPv5H.js +1 -0
- package/dist/bin/less-BrjQyxTC.js +1 -0
- package/dist/bin/less-DRwZNTsh.js +1 -0
- package/dist/bin/light-plus-B1SMNL2F.js +1 -0
- package/dist/bin/liquid-DuGPT5mZ.js +1 -0
- package/dist/bin/llvm-7RtiJHjj.js +1 -0
- package/dist/bin/log-KNtIq3vN.js +1 -0
- package/dist/bin/logo-BhWYf_9m.js +1 -0
- package/dist/bin/lua-BTFYnP6K.js +1 -0
- package/dist/bin/lua-mUj-7rV8.js +1 -0
- package/dist/bin/luau-CmcVV_DK.js +1 -0
- package/dist/bin/make-Df5gTOt9.js +1 -0
- package/dist/bin/markdown-BtUDO16i.js +1 -0
- package/dist/bin/markdown-dl1m7YAL.js +1 -0
- package/dist/bin/marko-CEM61Zib.js +1 -0
- package/dist/bin/material-theme-BhPjDEOe.js +1 -0
- package/dist/bin/material-theme-darker-LB0WiHs5.js +1 -0
- package/dist/bin/material-theme-lighter-b9Tsrc9R.js +1 -0
- package/dist/bin/material-theme-ocean-B1e0t2_i.js +1 -0
- package/dist/bin/material-theme-palenight-1CdY1ykl.js +1 -0
- package/dist/bin/matlab-Qa9-GHKp.js +1 -0
- package/dist/bin/mdc-Dya9Bdmr.js +1 -0
- package/dist/bin/mdx-DrGeQtwF.js +1 -0
- package/dist/bin/mermaid-CcPTwP9f.js +1 -0
- package/dist/bin/min-dark-Dujx5q9i.js +1 -0
- package/dist/bin/min-light-wZS1kyfV.js +1 -0
- package/dist/bin/mipsasm-BfvsshAY.js +1 -0
- package/dist/bin/mojo-C2BYzC14.js +1 -0
- package/dist/bin/monokai-w9z2AnJS.js +1 -0
- package/dist/bin/moonbit-DjpzDFB5.js +1 -0
- package/dist/bin/move-Bxr9ef96.js +1 -0
- package/dist/bin/narrat-DcRshums.js +1 -0
- package/dist/bin/nextflow-groovy-COoKx28o.js +1 -0
- package/dist/bin/nextflow-groovy-CpiNfI-c.js +1 -0
- package/dist/bin/nextflow-hLaJXrFj.js +1 -0
- package/dist/bin/nginx-CR6uKwZy.js +1 -0
- package/dist/bin/night-owl-CplppJJB.js +1 -0
- package/dist/bin/night-owl-light-BpOQXyuL.js +1 -0
- package/dist/bin/nim-DceKa9Nn.js +1 -0
- package/dist/bin/nix-CtTYb1HK.js +1 -0
- package/dist/bin/nord-hoPJ5Dy3.js +1 -0
- package/dist/bin/nushell-Cn3fXdgH.js +1 -0
- package/dist/bin/objective-c-BkduaOlQ.js +1 -0
- package/dist/bin/objective-cpp-Cn2j-b6G.js +1 -0
- package/dist/bin/ocaml-DqvuWcoq.js +1 -0
- package/dist/bin/odin-D7ha6-Ni.js +1 -0
- package/dist/bin/one-dark-pro-eMAAhFAk.js +1 -0
- package/dist/bin/one-light-CSbFTW4a.js +1 -0
- package/dist/bin/open-8AenGr4K.js +2 -0
- package/dist/bin/openscad-Dy0BqjE1.js +1 -0
- package/dist/bin/pascal-HLRSlJ2v.js +1 -0
- package/dist/bin/perl-B-LgtJc7.js +1 -0
- package/dist/bin/perl-B6GAvvHF.js +1 -0
- package/dist/bin/php-BAXKJjQn.js +1 -0
- package/dist/bin/php-D9zsHNF7.js +1 -0
- package/dist/bin/pkl-D0LsOQD5.js +1 -0
- package/dist/bin/plastic-DH0ROn14.js +1 -0
- package/dist/bin/plsql-Ckotkn8G.js +1 -0
- package/dist/bin/po-Qix2N2fA.js +1 -0
- package/dist/bin/poimandres-HErFvNSF.js +1 -0
- package/dist/bin/polar-DHAM8Rqb.js +1 -0
- package/dist/bin/postcss-D_yqFB4d.js +1 -0
- package/dist/bin/postcss-hgbxLNuF.js +1 -0
- package/dist/bin/powerquery-sXjCytsm.js +1 -0
- package/dist/bin/powershell-Br9y9LlC.js +1 -0
- package/dist/bin/prisma-COBUUaqE.js +1 -0
- package/dist/bin/prolog-BbJTvtA-.js +1 -0
- package/dist/bin/proto-DiR3FLO1.js +1 -0
- package/dist/bin/pug-F80Qp1DF.js +1 -0
- package/dist/bin/puppet-CNdK406T.js +1 -0
- package/dist/bin/purescript-Cgw06S43.js +1 -0
- package/dist/bin/python-BLuJ3b5I.js +1 -0
- package/dist/bin/python-DRu0B6X_.js +1 -0
- package/dist/bin/qml-Brw1WH4u.js +1 -0
- package/dist/bin/qmldir-tOWyMniz.js +1 -0
- package/dist/bin/qss-CQ_tB_vx.js +1 -0
- package/dist/bin/r-1QWtk6BQ.js +1 -0
- package/dist/bin/r-CeXuGenk.js +1 -0
- package/dist/bin/racket-xM_JlWdJ.js +1 -0
- package/dist/bin/raku-CsjbV0q-.js +1 -0
- package/dist/bin/razor-DKKN_syA.js +1 -0
- package/dist/bin/red-2s42RJtx.js +1 -0
- package/dist/bin/reg-CDO-JwKW.js +1 -0
- package/dist/bin/regexp-Aby82bH7.js +1 -0
- package/dist/bin/regexp-WPJh6EdZ.js +1 -0
- package/dist/bin/rel-Lnk3efq_.js +1 -0
- package/dist/bin/riscv-B6rT4cSG.js +1 -0
- package/dist/bin/ron-BiBOkEWn.js +1 -0
- package/dist/bin/rose-pine-CxwcSun-.js +1 -0
- package/dist/bin/rose-pine-dawn-DkCSMZNl.js +1 -0
- package/dist/bin/rose-pine-moon-CRcpU7ID.js +1 -0
- package/dist/bin/rosmsg-_EpaFX4E.js +1 -0
- package/dist/bin/rst-BHI_EFqA.js +1 -0
- package/dist/bin/ruby-DME9-fO-.js +1 -0
- package/dist/bin/ruby-Go_q6-3o.js +1 -0
- package/dist/bin/rust-Dzeg0wAa.js +1 -0
- package/dist/bin/sas-Dx90SbnG.js +1 -0
- package/dist/bin/sass-CfSmloHa.js +1 -0
- package/dist/bin/scala-BzM4CFnq.js +1 -0
- package/dist/bin/scheme-BqJLyHNb.js +1 -0
- package/dist/bin/scss-ByAjeRJP.js +1 -0
- package/dist/bin/scss-D8wnyi0z.js +1 -0
- package/dist/bin/sdbl-CSGqAXn4.js +1 -0
- package/dist/bin/sdbl-CVVKgb7O.js +1 -0
- package/dist/bin/shaderlab-8CiQ7btM.js +1 -0
- package/dist/bin/shellscript-CED9H1ao.js +1 -0
- package/dist/bin/shellscript-DhB4mnQ-.js +1 -0
- package/dist/bin/shellsession-xr72W2pP.js +1 -0
- package/dist/bin/slack-dark-BLjlSbC0.js +1 -0
- package/dist/bin/slack-ochin-BeSJq7AY.js +1 -0
- package/dist/bin/smalltalk-DrKePzpE.js +1 -0
- package/dist/bin/snazzy-light-hGizAcbG.js +1 -0
- package/dist/bin/solarized-dark-B47oUJnZ.js +1 -0
- package/dist/bin/solarized-light-COfgMzjY.js +1 -0
- package/dist/bin/solidity-93cGmCAT.js +1 -0
- package/dist/bin/soy-Cy2jSWuh.js +1 -0
- package/dist/bin/sparql-Bq74Yvwh.js +1 -0
- package/dist/bin/splunk-C1Hlgf0c.js +1 -0
- package/dist/bin/sql-BfsqdN68.js +1 -0
- package/dist/bin/sql-CWYhyc-w.js +1 -0
- package/dist/bin/ssh-config-mSCJO_Nw.js +1 -0
- package/dist/bin/stata-DawaFFYJ.js +1 -0
- package/dist/bin/stylus-CtR9dQIO.js +1 -0
- package/dist/bin/stylus-Dxd5rS5f.js +1 -0
- package/dist/bin/surrealql-B1iahgCh.js +1 -0
- package/dist/bin/svelte-CHomAVJT.js +1 -0
- package/dist/bin/swift-CmhdkGbJ.js +1 -0
- package/dist/bin/synthwave-84-C8lHUQgz.js +1 -0
- package/dist/bin/system-verilog-C5NPuZ4w.js +1 -0
- package/dist/bin/systemd-DWf13jtC.js +1 -0
- package/dist/bin/talonscript-BHUwvink.js +1 -0
- package/dist/bin/tasl-BqepUa6R.js +1 -0
- package/dist/bin/tcl-Cq_3s_LD.js +1 -0
- package/dist/bin/templ-BRBP2pJf.js +1 -0
- package/dist/bin/terraform-CJUb39F2.js +1 -0
- package/dist/bin/tex-5doNg0Ig.js +1 -0
- package/dist/bin/tex-BLh5zE4g.js +1 -0
- package/dist/bin/tokyo-night-DToTZO1M.js +1 -0
- package/dist/bin/toml-CcAy-xez.js +1 -0
- package/dist/bin/ts-tags-C0IwWXwE.js +1 -0
- package/dist/bin/tsv-2LD76W9Y.js +1 -0
- package/dist/bin/tsx-BMmbqG-q.js +1 -0
- package/dist/bin/tsx-BTd5Sf56.js +1 -0
- package/dist/bin/turtle-BFrhIdAM.js +1 -0
- package/dist/bin/turtle-D-xcuGf_.js +1 -0
- package/dist/bin/twig-D5Nfm9La.js +1 -0
- package/dist/bin/typescript-CPs8nV3S.js +1 -0
- package/dist/bin/typescript-DcBfMx29.js +1 -0
- package/dist/bin/typespec-CJG5em1O.js +1 -0
- package/dist/bin/typst-CdguKrlh.js +1 -0
- package/dist/bin/v-VnqxfSIy.js +1 -0
- package/dist/bin/vala-CPlxddOG.js +1 -0
- package/dist/bin/vb-Do7lrS7D.js +1 -0
- package/dist/bin/verilog-OGZUphCy.js +1 -0
- package/dist/bin/vesper-D6bcMq-9.js +1 -0
- package/dist/bin/vhdl-vQqxP3uc.js +1 -0
- package/dist/bin/viml-CRzBk174.js +1 -0
- package/dist/bin/vitesse-black-DZPzP4WR.js +1 -0
- package/dist/bin/vitesse-dark-Dn_i46a6.js +1 -0
- package/dist/bin/vitesse-light-BYktrL59.js +1 -0
- package/dist/bin/vue-html-D7v7uXO6.js +1 -0
- package/dist/bin/vue-vine-lH9fGYq_.js +1 -0
- package/dist/bin/vue-zYUVH6uJ.js +1 -0
- package/dist/bin/vyper-CxG5w_hf.js +1 -0
- package/dist/bin/wasm-C2OXzu0D.js +1 -0
- package/dist/bin/wasm-DfcgBtd_.js +1 -0
- package/dist/bin/wenyan-CeYhWKkN.js +1 -0
- package/dist/bin/wgsl-gGAb2oSq.js +1 -0
- package/dist/bin/wikitext-BNkXva03.js +1 -0
- package/dist/bin/wit-snpmR1aT.js +1 -0
- package/dist/bin/wolfram-BUWAXgpt.js +1 -0
- package/dist/bin/xml-CCj7o04Q.js +1 -0
- package/dist/bin/xml-CGrMwkeu.js +1 -0
- package/dist/bin/xsl-C-lr_lSi.js +1 -0
- package/dist/bin/yaml-Brl8JesV.js +1 -0
- package/dist/bin/yaml-Dg2Msz_B.js +1 -0
- package/dist/bin/zenscript-DAB1FI6O.js +1 -0
- package/dist/bin/zig-DZTxXyCC.js +1 -0
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +2911 -30
- package/dist/standalone/abap-lNEwo1Fh.js +1 -0
- package/dist/standalone/actionscript-3-WtXz7Nc9.js +1 -0
- package/dist/standalone/ada-CxieIYPC.js +1 -0
- package/dist/standalone/andromeeda-BylpTWnC.js +1 -0
- package/dist/standalone/angular-html-BDfh9ICS.js +1 -0
- package/dist/standalone/angular-ts-WiBasIpj.js +1 -0
- package/dist/standalone/apache-zgaAodRu.js +1 -0
- package/dist/standalone/apex-BNA6bSzI.js +1 -0
- package/dist/standalone/apl-B3nAIz3U.js +1 -0
- package/dist/standalone/applescript-D9ApUiQg.js +1 -0
- package/dist/standalone/ara-Xn981wPy.js +1 -0
- package/dist/standalone/asciidoc-C90J3qhX.js +1 -0
- package/dist/standalone/asm-Cvg1-8xg.js +1 -0
- package/dist/standalone/astro-C0A5Ee0R.js +1 -0
- package/dist/standalone/aurora-x-z2QLMo08.js +1 -0
- package/dist/standalone/awk-L1WqJyjl.js +1 -0
- package/dist/standalone/ayu-dark-DFAYKmY8.js +1 -0
- package/dist/standalone/ayu-light-ChPCIrww.js +1 -0
- package/dist/standalone/ayu-mirage-Be8UDvXu.js +1 -0
- package/dist/standalone/ballerina-DOw61Yuq.js +1 -0
- package/dist/standalone/bat-BJGaXoOz.js +1 -0
- package/dist/standalone/beancount-Cu6FxzuA.js +1 -0
- package/dist/standalone/berry-DiPZpURY.js +1 -0
- package/dist/standalone/bibtex-Dp74FGRo.js +1 -0
- package/dist/standalone/bicep-BwSAodHP.js +1 -0
- package/dist/standalone/bird2-B05GFxQw.js +1 -0
- package/dist/standalone/blade-Bh52-e5e.js +1 -0
- package/dist/standalone/bsl-DrHuh5kR.js +1 -0
- package/dist/standalone/c-C-hboKX2.js +1 -0
- package/dist/standalone/c3-BS3NJrFe.js +1 -0
- package/dist/standalone/cadence-DKUaB06W.js +1 -0
- package/dist/standalone/cairo-D055eoAu.js +1 -0
- package/dist/standalone/catppuccin-frappe-Dq9QR7eZ.js +1 -0
- package/dist/standalone/catppuccin-latte-yUwHFQst.js +1 -0
- package/dist/standalone/catppuccin-macchiato-KYmbHQyq.js +1 -0
- package/dist/standalone/catppuccin-mocha-DFrUhns1.js +1 -0
- package/dist/standalone/clarity-DKB_EYUy.js +1 -0
- package/dist/standalone/clojure-D7D2Zxsu.js +1 -0
- package/dist/standalone/cmake-Bju7ahwU.js +1 -0
- package/dist/standalone/cobol-C9xdim3W.js +1 -0
- package/dist/standalone/codeowners-Di1OvpcW.js +1 -0
- package/dist/standalone/codeql-KhL_IfCA.js +1 -0
- package/dist/standalone/coffee-CWthmHqg.js +1 -0
- package/dist/standalone/common-lisp-DL0ShAR3.js +1 -0
- package/dist/standalone/coq-YuXzTVD8.js +1 -0
- package/dist/standalone/cpp-BP_mYM2q.js +1 -0
- package/dist/standalone/crystal-BP8Y_JHu.js +1 -0
- package/dist/standalone/csharp-X4m7dqIW.js +1 -0
- package/dist/standalone/css-DZpGhzjZ.js +1 -0
- package/dist/standalone/csv-D_FJD-_L.js +1 -0
- package/dist/standalone/cue-Brrvnugl.js +1 -0
- package/dist/standalone/cypher-CieA2-Ow.js +1 -0
- package/dist/standalone/d-BeWU-E1F.js +1 -0
- package/dist/standalone/dark-plus-t8phC8vs.js +1 -0
- package/dist/standalone/dart-CcoouBGm.js +1 -0
- package/dist/standalone/dax-DZs9015r.js +1 -0
- package/dist/standalone/desktop-Bmf3DIqD.js +1 -0
- package/dist/standalone/diff-M4XuV4o4.js +1 -0
- package/dist/standalone/docker-Bf7rtiym.js +1 -0
- package/dist/standalone/dotenv-BwfGDw9z.js +1 -0
- package/dist/standalone/dracula-__iKjS5Q.js +1 -0
- package/dist/standalone/dracula-soft-Cn3YGlZ1.js +1 -0
- package/dist/standalone/dream-maker-BOxs3X-U.js +1 -0
- package/dist/standalone/edge-C7ZV1b67.js +1 -0
- package/dist/standalone/elixir-DaqZx2n-.js +1 -0
- package/dist/standalone/elm-Dop4RWjQ.js +1 -0
- package/dist/standalone/emacs-lisp-C1K9lKmb.js +1 -0
- package/dist/standalone/erb-CYhomfpC.js +1 -0
- package/dist/standalone/erlang-DX8GMz3W.js +1 -0
- package/dist/standalone/everforest-dark-CG-xS58q.js +1 -0
- package/dist/standalone/everforest-light-VnlgGH1r.js +1 -0
- package/dist/standalone/fennel-Bkp2zi6e.js +1 -0
- package/dist/standalone/fish-BSydEoPu.js +1 -0
- package/dist/standalone/fluent-CdeSNonp.js +1 -0
- package/dist/standalone/fortran-fixed-form-Q6JIc71r.js +1 -0
- package/dist/standalone/fortran-free-form-uMmrusbk.js +1 -0
- package/dist/standalone/fsharp-BNMUC2iW.js +1 -0
- package/dist/standalone/gdresource-BlhCtIAh.js +1 -0
- package/dist/standalone/gdscript-CjXE_VF4.js +1 -0
- package/dist/standalone/gdshader-D3GnIQMO.js +1 -0
- package/dist/standalone/genie-DjRp0HIF.js +1 -0
- package/dist/standalone/gherkin-CO-9Xf3O.js +1 -0
- package/dist/standalone/git-commit-BmPljjxo.js +1 -0
- package/dist/standalone/git-rebase-DVpnhtFe.js +1 -0
- package/dist/standalone/github-dark-DW2tvKKC.js +1 -0
- package/dist/standalone/github-dark-default-ChsP8enW.js +1 -0
- package/dist/standalone/github-dark-dimmed-DbObBUjn.js +1 -0
- package/dist/standalone/github-dark-high-contrast-DGQlFkmA.js +1 -0
- package/dist/standalone/github-light-JgN_--pJ.js +1 -0
- package/dist/standalone/github-light-default-Dyse0wyU.js +1 -0
- package/dist/standalone/github-light-high-contrast-CeZ5HGZn.js +1 -0
- package/dist/standalone/gleam-BIhdqQ3b.js +1 -0
- package/dist/standalone/glimmer-js-BxvF_2QK.js +1 -0
- package/dist/standalone/glimmer-ts-AM8hgR0M.js +1 -0
- package/dist/standalone/glsl-CeERM8Pu.js +1 -0
- package/dist/standalone/gn--jXLY5_N.js +1 -0
- package/dist/standalone/gnuplot-Bxet1c3v.js +1 -0
- package/dist/standalone/go-s39PxAzr.js +1 -0
- package/dist/standalone/graphql-B_NxG4GM.js +1 -0
- package/dist/standalone/groovy-CzT_Iae7.js +1 -0
- package/dist/standalone/gruvbox-dark-hard-C1uR-pvB.js +1 -0
- package/dist/standalone/gruvbox-dark-medium-C_NCtEMg.js +1 -0
- package/dist/standalone/gruvbox-dark-soft-DbASFM7u.js +1 -0
- package/dist/standalone/gruvbox-light-hard-BisBk_cZ.js +1 -0
- package/dist/standalone/gruvbox-light-medium-Cqf5GdYM.js +1 -0
- package/dist/standalone/gruvbox-light-soft-R3xWeYua.js +1 -0
- package/dist/standalone/hack-DOi7Q-_3.js +1 -0
- package/dist/standalone/haml-DqnJGs3l.js +1 -0
- package/dist/standalone/handlebars-BD-GTNyX.js +1 -0
- package/dist/standalone/haskell-BU_KScmc.js +1 -0
- package/dist/standalone/haxe-Dq20ysgQ.js +1 -0
- package/dist/standalone/hcl-C-NSNXCz.js +1 -0
- package/dist/standalone/hjson-C1slXyS7.js +1 -0
- package/dist/standalone/hlsl-nviGPNKF.js +1 -0
- package/dist/standalone/horizon-bright-CHsP19AA.js +1 -0
- package/dist/standalone/horizon-pIEktodb.js +1 -0
- package/dist/standalone/houston-DM-vL-Qy.js +1 -0
- package/dist/standalone/html-Dg4T4CWG.js +1 -0
- package/dist/standalone/html-derivative-Dy01fcFN.js +1 -0
- package/dist/standalone/http-Dcg4Y0LN.js +1 -0
- package/dist/standalone/hurl-DLBSghyU.js +1 -0
- package/dist/standalone/hxml-CkY94ZZG.js +1 -0
- package/dist/standalone/hy-CNbyPkG-.js +1 -0
- package/dist/standalone/imba-C3JgZFAu.js +1 -0
- package/dist/standalone/index.d.ts +1607 -0
- package/dist/standalone/index.js +228 -0
- package/dist/standalone/ini-CnSxfbfK.js +1 -0
- package/dist/standalone/java-CzlXKE3t.js +1 -0
- package/dist/standalone/javascript-FKiEkpOl.js +1 -0
- package/dist/standalone/jinja-rn3l-epj.js +1 -0
- package/dist/standalone/jison-Bb1qcUQw.js +1 -0
- package/dist/standalone/json-E5qb7BD2.js +1 -0
- package/dist/standalone/json5-BlfBFW_Y.js +1 -0
- package/dist/standalone/jsonc-BRJexVfP.js +1 -0
- package/dist/standalone/jsonl-D859SsoH.js +1 -0
- package/dist/standalone/jsonnet-sFIVPoTa.js +1 -0
- package/dist/standalone/jssm-BKvxMEP9.js +1 -0
- package/dist/standalone/jsx-D6UqkuWx.js +1 -0
- package/dist/standalone/julia-Bf0YFlEW.js +1 -0
- package/dist/standalone/just-CgnMO4TC.js +1 -0
- package/dist/standalone/kanagawa-dragon-BVeOdnTJ.js +1 -0
- package/dist/standalone/kanagawa-lotus-BPxif4UL.js +1 -0
- package/dist/standalone/kanagawa-wave-yTBcVIG8.js +1 -0
- package/dist/standalone/kdl-DHueRo8f.js +1 -0
- package/dist/standalone/kotlin-Dq1wwHQP.js +1 -0
- package/dist/standalone/kusto-SD8_YGH-.js +1 -0
- package/dist/standalone/laserwave-x5LKCxGn.js +1 -0
- package/dist/standalone/latex-CHYOx8yi.js +1 -0
- package/dist/standalone/lean-xWP4NbYS.js +1 -0
- package/dist/standalone/less-To8q_Luw.js +1 -0
- package/dist/standalone/light-plus-B9N5DQI_.js +1 -0
- package/dist/standalone/liquid-BTKRZY_L.js +1 -0
- package/dist/standalone/llvm-DSlzjbDz.js +1 -0
- package/dist/standalone/log-BUZNzUKc.js +1 -0
- package/dist/standalone/logo-Cdvy4UIe.js +1 -0
- package/dist/standalone/lua-RcKjZiH7.js +1 -0
- package/dist/standalone/luau-ChD5_OIQ.js +1 -0
- package/dist/standalone/make-PTqunrmv.js +1 -0
- package/dist/standalone/markdown-tUO1dt9m.js +1 -0
- package/dist/standalone/marko-CLH1Sh4A.js +1 -0
- package/dist/standalone/material-theme-CZJ_pRWf.js +1 -0
- package/dist/standalone/material-theme-darker-NnzT0yiw.js +1 -0
- package/dist/standalone/material-theme-lighter-B-3DgVxS.js +1 -0
- package/dist/standalone/material-theme-ocean-BZZHghfO.js +1 -0
- package/dist/standalone/material-theme-palenight-B9O9JO4H.js +1 -0
- package/dist/standalone/matlab-Co_sDlVB.js +1 -0
- package/dist/standalone/mdc-C1ZFsTbz.js +1 -0
- package/dist/standalone/mdx-B4AGJXox.js +1 -0
- package/dist/standalone/mermaid-4pySd3sl.js +1 -0
- package/dist/standalone/min-dark-DSigFwoh.js +1 -0
- package/dist/standalone/min-light-b8nqGai7.js +1 -0
- package/dist/standalone/mipsasm-DbZprEQm.js +1 -0
- package/dist/standalone/mojo-4kIBaoam.js +1 -0
- package/dist/standalone/monokai-BKSWoNnW.js +1 -0
- package/dist/standalone/moonbit-ZcSH7VpN.js +1 -0
- package/dist/standalone/move-BAMKTWoZ.js +1 -0
- package/dist/standalone/narrat-DYLRa4xG.js +1 -0
- package/dist/standalone/nextflow-CE20fCoX.js +1 -0
- package/dist/standalone/nextflow-groovy-Brjjw2F-.js +1 -0
- package/dist/standalone/nginx-CJQ7hxI6.js +1 -0
- package/dist/standalone/night-owl-BtS72I6-.js +1 -0
- package/dist/standalone/night-owl-light-fbFrWwTF.js +1 -0
- package/dist/standalone/nim-Czafvbe4.js +1 -0
- package/dist/standalone/nix-DlQjHQ1N.js +1 -0
- package/dist/standalone/nord-Kzx5CafP.js +1 -0
- package/dist/standalone/nushell-B04C_1w9.js +1 -0
- package/dist/standalone/objective-c-DO0dC44X.js +1 -0
- package/dist/standalone/objective-cpp-C6jZz_vb.js +1 -0
- package/dist/standalone/ocaml-D9wp3EJd.js +1 -0
- package/dist/standalone/odin-u5wG6Qd6.js +1 -0
- package/dist/standalone/one-dark-pro-x1jnl7Dq.js +1 -0
- package/dist/standalone/one-light-CHHJSb-V.js +1 -0
- package/dist/standalone/open-CGQvvIUq.js +2 -0
- package/dist/standalone/openscad-H5hW1BA6.js +1 -0
- package/dist/standalone/pascal-h7T9h7AG.js +1 -0
- package/dist/standalone/perl-CChoGwxr.js +1 -0
- package/dist/standalone/php-BW6t2_Q8.js +1 -0
- package/dist/standalone/pkl-Bs_8OmWM.js +1 -0
- package/dist/standalone/plastic-MRrPKBd1.js +1 -0
- package/dist/standalone/plsql-BrbZPaQW.js +1 -0
- package/dist/standalone/po-Diz0LmjW.js +1 -0
- package/dist/standalone/poimandres-D3Al1iQC.js +1 -0
- package/dist/standalone/polar-BCwHILUV.js +1 -0
- package/dist/standalone/postcss-D6Lmyt9A.js +1 -0
- package/dist/standalone/powerquery-CQr4BVoo.js +1 -0
- package/dist/standalone/powershell-B6zzL9QO.js +1 -0
- package/dist/standalone/prisma-yySgczME.js +1 -0
- package/dist/standalone/prolog-CZTkGNiW.js +1 -0
- package/dist/standalone/proto-Bu_Nhf7u.js +1 -0
- package/dist/standalone/pug-DJ5mt9QE.js +1 -0
- package/dist/standalone/puppet-CJffH6Q6.js +1 -0
- package/dist/standalone/purescript-CC7chrMM.js +1 -0
- package/dist/standalone/python-C0zrVFze.js +1 -0
- package/dist/standalone/qml-BxVojlB6.js +1 -0
- package/dist/standalone/qmldir-C14XBFWV.js +1 -0
- package/dist/standalone/qss-B5Hlft_e.js +1 -0
- package/dist/standalone/r-BJ2VNcvN.js +1 -0
- package/dist/standalone/racket-BhgskJzI.js +1 -0
- package/dist/standalone/raku-DsquHgLn.js +1 -0
- package/dist/standalone/razor-Dp2BNdgE.js +1 -0
- package/dist/standalone/red-B7RxoP7i.js +1 -0
- package/dist/standalone/reg-Bax0jtzQ.js +1 -0
- package/dist/standalone/regexp-BTh6yLg7.js +1 -0
- package/dist/standalone/rel-BJ9pN2G2.js +1 -0
- package/dist/standalone/riscv-C3I2jhjQ.js +1 -0
- package/dist/standalone/ron-B4yZ2p1h.js +1 -0
- package/dist/standalone/rose-pine-CesGHTut.js +1 -0
- package/dist/standalone/rose-pine-dawn-H7rm9o-e.js +1 -0
- package/dist/standalone/rose-pine-moon-BLFdnJwM.js +1 -0
- package/dist/standalone/rosmsg-xA4_Syxo.js +1 -0
- package/dist/standalone/rst-DL5K0SsC.js +1 -0
- package/dist/standalone/ruby-DuNhMjQX.js +1 -0
- package/dist/standalone/rust-BwguOFUq.js +1 -0
- package/dist/standalone/sas-CgrIRNnH.js +1 -0
- package/dist/standalone/sass-BZyNoloZ.js +1 -0
- package/dist/standalone/scala-CekfPUXd.js +1 -0
- package/dist/standalone/scheme-B2vB2nlX.js +1 -0
- package/dist/standalone/scss-B5Q0dl0C.js +1 -0
- package/dist/standalone/sdbl-B-nwTmgB.js +1 -0
- package/dist/standalone/shaderlab-B3x1KxeX.js +1 -0
- package/dist/standalone/shellscript-BUzexP2h.js +1 -0
- package/dist/standalone/shellsession-iw5paaXf.js +1 -0
- package/dist/standalone/slack-dark-DCR6M5dd.js +1 -0
- package/dist/standalone/slack-ochin-BxQ3FsyT.js +1 -0
- package/dist/standalone/smalltalk-Cy1j24XO.js +1 -0
- package/dist/standalone/snazzy-light-CjEw4xHM.js +1 -0
- package/dist/standalone/solarized-dark-XlQ9gXgZ.js +1 -0
- package/dist/standalone/solarized-light-isNaysOv.js +1 -0
- package/dist/standalone/solidity-C1mh1SRB.js +1 -0
- package/dist/standalone/soy-BExHEi9F.js +1 -0
- package/dist/standalone/sparql-38UvPuKm.js +1 -0
- package/dist/standalone/splunk-oM69IznW.js +1 -0
- package/dist/standalone/sql-CqT9Lw7j.js +1 -0
- package/dist/standalone/ssh-config-B0ZrS7Sv.js +1 -0
- package/dist/standalone/stata-XyxwKkW7.js +1 -0
- package/dist/standalone/stylus-B9JQrvWs.js +1 -0
- package/dist/standalone/surrealql-DeE9iwmm.js +1 -0
- package/dist/standalone/svelte-DbSBwS6o.js +1 -0
- package/dist/standalone/swift-CaafmPxt.js +1 -0
- package/dist/standalone/synthwave-84-BX-qCxDD.js +1 -0
- package/dist/standalone/system-verilog-J8T_iWHG.js +1 -0
- package/dist/standalone/systemd-DVL5CBpd.js +1 -0
- package/dist/standalone/talonscript-JJfmMcGp.js +1 -0
- package/dist/standalone/tasl-B1QiBRw2.js +1 -0
- package/dist/standalone/tcl-CqllcB5H.js +1 -0
- package/dist/standalone/templ-hC3Z5CVs.js +1 -0
- package/dist/standalone/terraform-wW0EQRMO.js +1 -0
- package/dist/standalone/tex-CpSHM71U.js +1 -0
- package/dist/standalone/tokyo-night-D6xJgjfK.js +1 -0
- package/dist/standalone/toml-BHrgvSPm.js +1 -0
- package/dist/standalone/ts-tags-BiWG72vW.js +1 -0
- package/dist/standalone/tsv-D73M_gRj.js +1 -0
- package/dist/standalone/tsx-D6awiul2.js +1 -0
- package/dist/standalone/turtle-C6QRqIYF.js +1 -0
- package/dist/standalone/twig-BGs9c0m6.js +1 -0
- package/dist/standalone/typescript-CWgyaUtY.js +1 -0
- package/dist/standalone/typespec-oIccER07.js +1 -0
- package/dist/standalone/typst-63_R-dcN.js +1 -0
- package/dist/standalone/v-DNh9rpzt.js +1 -0
- package/dist/standalone/vala-yZrEMgbl.js +1 -0
- package/dist/standalone/vb-CWiXLMNc.js +1 -0
- package/dist/standalone/verilog-D_YgnmVW.js +1 -0
- package/dist/standalone/vesper-BoY7BUOC.js +1 -0
- package/dist/standalone/vhdl-CIOMuzRr.js +1 -0
- package/dist/standalone/viml-KvIzNj-z.js +1 -0
- package/dist/standalone/vitesse-black-BC5aDvua.js +1 -0
- package/dist/standalone/vitesse-dark-C_RcLVAX.js +1 -0
- package/dist/standalone/vitesse-light-CbYhMxH_.js +1 -0
- package/dist/standalone/vue-HXXkW9Ol.js +1 -0
- package/dist/standalone/vue-html-DMGAGeUM.js +1 -0
- package/dist/standalone/vue-vine-CumWe8dY.js +1 -0
- package/dist/standalone/vyper-DQ6Iue0m.js +1 -0
- package/dist/standalone/wasm-BVujMf4j.js +1 -0
- package/dist/standalone/wasm-CttxofWT.js +1 -0
- package/dist/standalone/wenyan-sFr85_Wa.js +1 -0
- package/dist/standalone/wgsl-C8sJ4ScM.js +1 -0
- package/dist/standalone/wikitext-BHbI4LrJ.js +1 -0
- package/dist/standalone/wit-BrulQYCT.js +1 -0
- package/dist/standalone/wolfram-CkTHL-YU.js +1 -0
- package/dist/standalone/xml-OYKGlsbQ.js +1 -0
- package/dist/standalone/xsl-B9zh_nzP.js +1 -0
- package/dist/standalone/yaml-C2UdUGt-.js +1 -0
- package/dist/standalone/zenscript-CvnF8pfB.js +1 -0
- package/dist/standalone/zig-DrTOhqiK.js +1 -0
- package/package.json +22 -11
package/dist/lib/index.js
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
|
-
import{
|
|
2
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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 };
|