@hardikg/mdview 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +52 -10
  3. package/bin/chunk-RBYD2HWV.mjs +196 -0
  4. package/bin/chunk-WWM6JNAS.mjs +209 -0
  5. package/bin/chunk-XFRQFZZ4.mjs +20 -0
  6. package/bin/mdview.mjs +521 -1323
  7. package/bin/regex-worker.mjs +52 -0
  8. package/bin/server-KALIU7HM.mjs +1854 -0
  9. package/bin/shiki-5SFVA32N.mjs +13 -0
  10. package/dist/client/assets/23I4SOAX-BN9xvzna.js +2 -0
  11. package/dist/client/assets/26H2HVXD-Cu1OTMxT.js +2 -0
  12. package/dist/client/assets/2IZ5MNOH-DMjMO3_9.js +1 -0
  13. package/dist/client/assets/2P3CZEXH-D-4BP7dg.js +2 -0
  14. package/dist/client/assets/3E5HRNYT-DUYblpdl.js +5 -0
  15. package/dist/client/assets/3JIRLDUO-C1CwW1Ha.js +1 -0
  16. package/dist/client/assets/5MJ3UKSG-DtT76qKq.js +2 -0
  17. package/dist/client/assets/5THPTBWS-Ce_hS451.js +1 -0
  18. package/dist/client/assets/7WLXJQOR-D6QVYUu9.js +1 -0
  19. package/dist/client/assets/A255FCTW-woaH7Mj_.js +1 -0
  20. package/dist/client/assets/AM43MQ64-CsWC_OQH.js +2 -0
  21. package/dist/client/assets/AOZTNTMC-BIehUQHD.js +5 -0
  22. package/dist/client/assets/AQNLM6KG-DIUuJgPm.js +4 -0
  23. package/dist/client/assets/BNU2RHQB-DeXpwU5k.js +2 -0
  24. package/dist/client/assets/C5HTQT3I-7U13x70S.js +4 -0
  25. package/dist/client/assets/CPKRWH5G-CRxaqyOs.js +12 -0
  26. package/dist/client/assets/CUVCMPQX-JTGMOfG2.js +1 -0
  27. package/dist/client/assets/D4V2MWCI-CytzCr6g.js +1 -0
  28. package/dist/client/assets/DHKBVIA3-CVnlqiEq.js +1 -0
  29. package/dist/client/assets/DKTLEL5E-DciMwFJ1.js +1 -0
  30. package/dist/client/assets/DUZNPW7S-D3pifVNb.js +2 -0
  31. package/dist/client/assets/E3FWOBN4-D5m5o2g0.js +4 -0
  32. package/dist/client/assets/E65FQMJL-Bg5F0Zo8.js +1 -0
  33. package/dist/client/assets/FTXIJQGB-CggtIYAM.js +1 -0
  34. package/dist/client/assets/G3MDBVQH-BZmnMkFw.js +1 -0
  35. package/dist/client/assets/G3OATVW2-VpVvT5i2.js +8 -0
  36. package/dist/client/assets/IV52FOAS-DgPxxRI4.js +5 -0
  37. package/dist/client/assets/JTJEEBDN-Bl0iUT_r.js +2 -0
  38. package/dist/client/assets/K7FUICZ3-DgoihkiR.js +2 -0
  39. package/dist/client/assets/LHH2HHRB-BXHTnBD0.js +2 -0
  40. package/dist/client/assets/MSPQPZZY-DQg7F6a8.js +4 -0
  41. package/dist/client/assets/OB6ZFVKF-UHmC8m-y.js +2 -0
  42. package/dist/client/assets/PLTZDMI2-C1ZAoc-e.js +1 -0
  43. package/dist/client/assets/R7EPXMMD-ChI1SqjJ.js +5 -0
  44. package/dist/client/assets/RVARI57O-CgA1WoRv.js +1 -0
  45. package/dist/client/assets/SPBWZ4VS-EqfCsJ5V.js +1 -0
  46. package/dist/client/assets/SR7OOV3Q-CFnFs0kL.js +4 -0
  47. package/dist/client/assets/STAXBNH4-D4c13Xlj.js +2 -0
  48. package/dist/client/assets/SXFARSFO-CkFSkgdi.js +1 -0
  49. package/dist/client/assets/TC77ISAL--WP0GBo0.js +2 -0
  50. package/dist/client/assets/TYIQQHVC-B67pCpxM.js +13 -0
  51. package/dist/client/assets/TZUXETIT-BFD9W5hl.js +2 -0
  52. package/dist/client/assets/U7N72BGE-Do3829la.js +6 -0
  53. package/dist/client/assets/UFPIB6EZ-DTs_MyQv.js +3 -0
  54. package/dist/client/assets/UO52YEYL-DN1qbGwF.js +3 -0
  55. package/dist/client/assets/VIK3ONSK-DC8mR2sZ.js +2 -0
  56. package/dist/client/assets/VVVO4ZVP-DAtGLbxR.js +2 -0
  57. package/dist/client/assets/WRGXZO53-D30IuREw.js +3 -0
  58. package/dist/client/assets/X5SG7MGC-CdjdoL_s.js +2 -0
  59. package/dist/client/assets/XLHPVVFU-BFP9ID9p.js +2 -0
  60. package/dist/client/assets/XXYPNCD5-DqCewu_S.js +5 -0
  61. package/dist/client/assets/Z6WRYFJC-BEt1KCoU.js +7 -0
  62. package/dist/client/assets/ZI2QNEGF-VzC4uHcW.js +8 -0
  63. package/dist/client/assets/index-B96jWJTi.css +1 -0
  64. package/dist/client/assets/index-DCU09WmS.js +2 -0
  65. package/dist/client/assets/katex-C0xaXAD4.js +257 -0
  66. package/dist/client/assets/katex.min-3X1qOa03.js +1 -0
  67. package/dist/client/assets/katex.min-DMey5pMQ.css +1 -0
  68. package/dist/client/assets/lazy-DaMZI1F7.js +45 -0
  69. package/dist/client/index.html +2 -2
  70. package/package.json +32 -27
  71. package/dist/client/assets/2DXBBFLN-BxPqHNSN.js +0 -5
  72. package/dist/client/assets/46IJ22JX-u_jvQJNg.js +0 -2
  73. package/dist/client/assets/4TTDE5I4-BgbNAVNj.js +0 -3
  74. package/dist/client/assets/4UF3HTLV-DNbPxlOE.js +0 -2
  75. package/dist/client/assets/5THPTBWS-D57irc_4.js +0 -1
  76. package/dist/client/assets/7WLXJQOR-CHhKreq0.js +0 -1
  77. package/dist/client/assets/AAXQQPKT-CsAJqrOC.js +0 -5
  78. package/dist/client/assets/AKE5O7KZ-MSuBOjCG.js +0 -2
  79. package/dist/client/assets/BNMLP6AG-Ca3zyKQY.js +0 -3
  80. package/dist/client/assets/DKTLEL5E-B7ILCVTH.js +0 -1
  81. package/dist/client/assets/DR6PKOQU-Bw7cHvYp.js +0 -2
  82. package/dist/client/assets/E4RJ7UJB-CYB5C4K7.js +0 -2
  83. package/dist/client/assets/ELSXBIJH-DG0MW5dC.js +0 -1
  84. package/dist/client/assets/ERTIBQVS-BMWumWfk.js +0 -2
  85. package/dist/client/assets/F653G4FO-FI8M9Mc0.js +0 -12
  86. package/dist/client/assets/FKCDD66M-BPFkyrwI.js +0 -1
  87. package/dist/client/assets/FP7KDXVC-BLEBNkCO.js +0 -2
  88. package/dist/client/assets/FT5I3RHX-j5YoMicq.js +0 -6
  89. package/dist/client/assets/FTXIJQGB-nrHdCfp7.js +0 -1
  90. package/dist/client/assets/G3MDBVQH-EkdejvdW.js +0 -1
  91. package/dist/client/assets/GY35JXOZ-3ovTGxgz.js +0 -2
  92. package/dist/client/assets/H5EEJPP2-CNVQBEB3.js +0 -1
  93. package/dist/client/assets/HWDLEDTA-C_JcdBMO.js +0 -1
  94. package/dist/client/assets/JZWSX6AR-OwVPptYv.js +0 -1
  95. package/dist/client/assets/KQOEJWJY-DKTXya68.js +0 -2
  96. package/dist/client/assets/KS3BB3KE-QkpqVdmA.js +0 -1
  97. package/dist/client/assets/KXJRRZHI-BQRjeGWj.js +0 -2
  98. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  99. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  100. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  101. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  102. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  103. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  104. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  105. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  106. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  107. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  108. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  109. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  110. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  111. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  112. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  113. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  114. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  115. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  116. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  117. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  118. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  119. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  120. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  121. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  122. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  123. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  124. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  125. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  126. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  127. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  128. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  129. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  130. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  131. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  132. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  133. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  134. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  135. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  136. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  137. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  138. package/dist/client/assets/MFVI5XKI-DrRaRE9X.js +0 -1
  139. package/dist/client/assets/MWY2J62P-AbEgJnzK.js +0 -12
  140. package/dist/client/assets/MXGNCBL2-DG3CZR8g.js +0 -2
  141. package/dist/client/assets/N6DAJLIZ-BsZQKTyT.js +0 -3
  142. package/dist/client/assets/NU5M23IW-CvtpIovv.js +0 -2
  143. package/dist/client/assets/NV4XNBIS-nS8wAx3h.js +0 -8
  144. package/dist/client/assets/OGLU4SIR-DoTUWhkz.js +0 -2
  145. package/dist/client/assets/OUNS5N76-BwVLonT4.js +0 -8
  146. package/dist/client/assets/PU4ISKXV-BYX7eA_r.js +0 -2
  147. package/dist/client/assets/PUZGS23X-n6IKOr7S.js +0 -1
  148. package/dist/client/assets/Q3IYVLG5-hWt1rVyh.js +0 -4
  149. package/dist/client/assets/QYHQDLKZ-CjGeJM7R.js +0 -4
  150. package/dist/client/assets/RVARI57O-CXmrUIiS.js +0 -1
  151. package/dist/client/assets/RZSVICHU-BiwjruZZ.js +0 -4
  152. package/dist/client/assets/UEXXS2NB-Dkz_tz-N.js +0 -2
  153. package/dist/client/assets/UHTQJXKL-lvl-7QeT.js +0 -5
  154. package/dist/client/assets/URA2L2QI-B8pYKxOk.js +0 -1
  155. package/dist/client/assets/UXCHDUKG-BGy3ZRLM.js +0 -2
  156. package/dist/client/assets/VKNV3BUX-DZnoX4IV.js +0 -5
  157. package/dist/client/assets/VMV7XPHP-6QkisQwm.js +0 -4
  158. package/dist/client/assets/WEVWDWVA-nohBBhTA.js +0 -2
  159. package/dist/client/assets/XRRJ35LF-BE7gSN1k.js +0 -2
  160. package/dist/client/assets/YFKBLVVN-CVTPpei6.js +0 -6
  161. package/dist/client/assets/YHRE5QGZ-Bs7dUKjj.js +0 -4
  162. package/dist/client/assets/ZF3OVT4B-6LaC6iLs.js +0 -3
  163. package/dist/client/assets/index-392ivruk.js +0 -2
  164. package/dist/client/assets/index-CGQuEJ_w.css +0 -1
  165. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  166. package/dist/client/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
  167. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  168. package/dist/client/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
  169. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  170. package/dist/client/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  171. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  172. package/dist/client/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
  173. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  174. package/dist/client/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
  175. package/dist/client/assets/katex-C5jXJg4s.js +0 -257
  176. package/dist/client/assets/katex.min-BYZTj3zW.css +0 -1
  177. package/dist/client/assets/katex.min-DXaO1tdm.js +0 -1
  178. package/dist/client/assets/lazy-CKqR1774.js +0 -43
package/CHANGELOG.md CHANGED
@@ -7,6 +7,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.0] — 2026-10-25
11
+
12
+ A security release. Upgrade if you open markdown you did not write: a file in
13
+ an open folder could read other files on your machine through an SVG and a
14
+ symlink, and several crafted inputs could stall or exhaust the server. This
15
+ release also needs Node 22.12, starts faster, ships 43% smaller, and restarts
16
+ an older background server by itself, so the fixes take effect after
17
+ `npm i -g`.
18
+
19
+ ### Security
20
+ - **Security fixes now take effect after `npm i -g` without `mdview stop`.** `mdview <path>` replaces a background server that runs another version (or, like 0.10.0 and older, reports none) and prints `mdview: restarted the background server (old → new)`. Open folders stay open. `/api/health` now reports `version`.
21
+ - **The registry answer is validated before it reaches your terminal.** The version must be plain semver, the read is capped at 64 KB and 5 s, redirects are refused, and errors never echo the response. A `.mdview.json` in a cloned repo cannot turn on the update check.
22
+ - **KaTeX is updated to 0.19, which fixes a prototype-pollution advisory** that could bypass its `trust` restrictions. KaTeX ships inside the client bundle, so `npm audit --omit=dev` never reported it.
23
+ - **A markdown file can no longer run script through an SVG beside it.** A document could frame `/__asset/…/evil.svg`, or link to it, and the SVG's script ran with no policy and could read other files and send them off the machine. Every `/__asset/*` response and every SVG response now carries a sandbox CSP, `nosniff` and `Cross-Origin-Resource-Policy: same-origin`, including when the route is reached through a percent-encoded URL. The page policy adds `frame-src 'none'` and `object-src 'none'`.
24
+ - **Symlinks can no longer lead out of an open folder.** A link committed into a repo (`notes.md -> /etc/hosts`, `logo.png -> ~/.ssh/id_ed25519`) was served as if it lived in the folder. Paths are now checked again after resolving every link. Links that stay inside the folder still work.
25
+ - **A regex folder search can no longer freeze the server.** A backtracking pattern could block every request for minutes. Regex search now runs in a worker thread that is stopped after 3 s, and the panel shows what it found so far. At most two searches run at once; the rest wait their turn.
26
+ - **`.txt` and `.pdf` are no longer served as assets, and nothing under a dot-folder is.** Only image, video and audio types are served.
27
+ - **The file watcher no longer follows symlinks**, so a repo holding `link -> /` no longer walks the whole disk.
28
+ - **`~/.config/mdview` is now private to your user.** The folder is `0700` and its files `0600`, and existing ones are tightened. They list every folder you have opened.
29
+ - **A link with `%22` in its path can no longer add attributes to the page.**
30
+ - **Front matter is now also bounded by characters.** Aliases to one long string stayed under the 10,000-node limit but expanded to about 100 MB; over 1,000,000 characters is now treated as plain text.
31
+ - **VS Code preview:** the webview now forwards only the extension's own messages into the preview.
32
+ - **A page on another site can no longer frame the VS Code preview.** `--vscode` mode cannot use `frame-ancestors`, so any page could frame the server and talk to the preview. The extension now creates a random token for every launch and passes it in an environment variable, never on the command line. The server answers its page and API only to requests that carry it. The CLI refuses `--vscode` without the token and never prints it.
33
+ - **A crafted markdown file can no longer stall the server while it renders.** An HTML block full of `<a ` or `<img ` with no closing `>` took 12 s at 480 KB and close to a minute at 1 MB, and blocked every other request; any open document could trigger it with an image pointing at `/api/file`. Links and images are now rewritten in one pass over the page: 1 MB takes about 25 ms.
34
+ - **A website can no longer make folder search use a gigabyte of memory.** A search for `a` over a file holding one 15 MB line kept all 15 million matches and took the server past 1 GB. Plain searches now skip lines over 10,000 characters, only count matches past the 20 shown per file, and stop after 1 s. Opening or searching a large file no longer splits it into millions of lines at once, which cost about 500 MB for 15 MB of short lines.
35
+ - **A markdown file can no longer send the tab to another site.** A `<meta http-equiv="refresh">` in a document's HTML navigated the whole tab, and a page's security policy cannot block that. `<meta>` and `<base>` tags are now removed from a document's HTML when it renders.
36
+ - **Opening one file no longer exposes its whole folder.** `mdview ~/README.md` served every image under `$HOME` and watched `$HOME` recursively. A single file is now watched on its own, and only the images and media the document references are served.
37
+ - **Dot-folders are also checked on the real path**, so an in-folder link into `.git` is refused by `/__asset/*`, and so is a Windows short name such as `GIT~1`.
38
+ - **Every response now carries `X-Content-Type-Options: nosniff`**, not only HTML pages, and the `workspace.json` watcher no longer follows symlinks.
39
+ - - **Shipped dependencies are updated to clear advisories:** `markdown-it` 14.3.2 → 15.0.3 (quadratic-time parsing of inline HTML when `html` is enabled, of a multi-line link reference definition, and further quadratic paths fixed in 15.0.1–15.0.3), `fastify` 5.12.5 → 5.12.7 (header validation bypass via case-sensitive `propertyNames`), and `fast-uri` 3.1.8 → 3.1.9 and 4.2.1 → 4.2.3 (scheme spoofing, path equivalence and host confusion in URI handling). The `markdown-it` ones were reachable: the renderer enables `html`, so a crafted markdown file could block the server. The others were not reachable as mdview is configured (no route declares a schema), but they ship in the install. `shiki` 4, `js-yaml` 5 and `open` 11 are also taken.
40
+ - **Folder search can no longer stall the server or exhaust its memory through many parallel requests.** At most two searches run at once across the whole server, literal and regex alike; later ones wait, and one whose page goes away is dropped. File contents read for searches share one 32 MB budget, and literal matching gives way to other requests every 10 ms. Sixteen parallel searches fired from `<img>` tags used to stall every request for 13 s and take the server past 2 GB; other requests now answer within 0.1 s and memory stays under 0.6 GB.
41
+
42
+ ### Removed
43
+ - **Node 20 is no longer supported; mdview now needs Node 22.12 or later.** Node 20 reached end of life on 2026-04-30, and a fresh install of 0.10.0 already failed to start a server on Node 20.0–20.18: `@fastify/static` 10.1.7 pulls `content-disposition` 3, which ships ES modules only, and Node loads those through `require()` without a flag only from 22.12. Run `node --version` before upgrading. The VS Code extension now needs VS Code 1.101 or later, the first release that runs on Node 22.
44
+
45
+ ### Added
46
+ - **`mdview update` checks for a newer release and prints the install command.** It asks registry.npmjs.org once, sends nothing that identifies you, and never installs or runs npm. The command is pinned to that version and uses the npm beside the Node that runs mdview.
47
+ - **An optional daily update notice, off by default.** `mdview config update-check on` makes the background server check about once a day. `mdview <path>` then prints one line to stderr, at most once a day per version, and only in a terminal. Only the global config can turn it on.
48
+ - **`mdview ls` says when the background server runs a different version** than the CLI.
49
+
50
+ ### Changed
51
+ - **`mdview --version`, `ls`, `rm` and `stop` start about five times faster.** They took about 0.2 s because every command loaded the web server, syntax highlighter and file watcher first; they now take about 0.04 s. The server code loads only when a command starts one.
52
+ - **The first document opens faster** (about 250 ms → 80 ms for a page of code). The syntax highlighter warms up right after the server starts listening and loads only the default palette's themes up front. Other palettes load the first time you pick them.
53
+ - **Live reload no longer re-highlights unchanged code blocks.** Highlighted blocks are cached (up to 8 MB, keys included, since a key holds the language name from the file), so saving a document with many large code blocks re-renders only what changed.
54
+ - **Folder search is faster.** Files are read eight at a time and kept in a cache that the file watcher keeps current: a repeated search over 4,000 files drops from about 1 s to about 70 ms. A search stops when you change the query.
55
+ - **The package is about 43% smaller (1.4 MB → 0.8 MB).** JetBrains Mono and KaTeX now ship as woff2 only, which every supported browser already used.
56
+ - **Markdown files over 16 MB are no longer rendered or searched.** Rendering is synchronous, so one huge file stalled every other request.
57
+ - **A single-file target that is itself a symlink is served from the folder its link points into.**
58
+ - **`mdview <file>` shows only that file and the media it references.** An image in the same folder that the document does not reference is no longer served.
59
+ - **Links and images written as raw HTML are now rewritten like markdown ones.** Single-quoted, unquoted or upper-case attributes, and a `>` inside a quoted value, no longer stop a relative `.md` link or image path from resolving. Links inside HTML comments are left alone.
60
+ - **A search that skips a very long line says so** with "More results truncated".
61
+ - **Front matter dates display as written** (`2024-01-02`) instead of as timestamps (`2024-01-02T00:00:00.000Z`). Front matter is now parsed as YAML 1.2 with merge keys (`<<: *defaults`) still applied.
62
+ - **Diagrams render with pele 1.10.** Labels and links in diagrams are sanitised more strictly (entity codes decode once; stray non-characters are stripped before the link scheme check), subgraph diagrams have fewer edge crossings, class diagrams wrap into rows instead of shrinking, CJK labels wrap, and `List<T>` in a label no longer renders as `List`. A `%%` comment no longer deletes the character before it, so a diagram that only parsed because of that now shows a syntax error.
63
+
64
+ ### Fixed
65
+ - **Two CLIs started at the same moment can no longer both spawn a background server.** Taking the spawn lock now re-checks for a server a rival just started.
66
+ - **A front matter block holding only whitespace or comments is still recognised as front matter** and removed from the page.
67
+ - **Navigating between files no longer drops and reopens the live-reload connection.** The watch stream reconnected on every navigation, so a file change could fall into the gap, and the server's connected-tab count briefly dropped to zero, which matters with `MDVIEW_IDLE_TIMEOUT`. One connection now lasts for the life of the page.
68
+ - **A burst of file changes no longer refetches the file tree hundreds of times.** Switching git branches fired one full tree request per added or removed file. Refreshes now wait 150 ms after the last change and run once, and the tree still ends up current.
69
+ - **A markdown file with millions of blank lines after its front matter renders again**, and folder search includes it. It used to overflow the stack and return a 500.
70
+ - **`mdview README.MD` on a case-insensitive disk (macOS, Windows) now live-reloads** when `README.md` is saved.
71
+ - **The VS Code preview keeps working after its frame or the webview reloads.** It used to stop following the editor.
72
+
10
73
  ## [0.10.0] — 2026-10-19
11
74
 
12
75
  A security fix for front matter, and a lighter diagram renderer that follows
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
6
6
 
7
- A local markdown viewer for reading long, hierarchical docs without losing focus. Run a command, browser opens with a beautifully rendered, navigable view of your file or folder. Edits in your editor show up live. Close terminal to stop.
7
+ A local markdown viewer for reading long, hierarchical docs without losing focus. Run a command, browser opens with a beautifully rendered, navigable view of your file or folder. Edits in your editor show up live. One background server hosts every folder you open; `mdview stop` ends it.
8
8
 
9
9
  ```
10
10
  mdview ./docs # browse a folder
@@ -28,7 +28,7 @@ Reading long markdown docs is hard:
28
28
  npm install -g @hardikg/mdview
29
29
  ```
30
30
 
31
- Requires Node >= 20. The `mdview` binary lands on your `PATH`.
31
+ Requires Node >= 22.12. The `mdview` binary lands on your `PATH`.
32
32
 
33
33
  ## Install from source
34
34
 
@@ -63,10 +63,12 @@ npm install -g ./hardikg-mdview-<version>.tgz
63
63
 
64
64
  ```bash
65
65
  mdview <path> # open a file or folder; the shell comes straight back
66
+ # (a single file shows only that file and the media it references)
66
67
  mdview # current directory
67
68
  mdview ls # what's open, and where the server is
68
69
  mdview rm <name> # stop serving a folder
69
70
  mdview stop # stop the background server
71
+ mdview update # is there a newer release? prints the install command
70
72
  mdview --foreground # run in this terminal instead (Ctrl-C to stop)
71
73
  mdview --no-open # don't auto-launch the browser
72
74
  mdview --port 9000 # one-off server on a specific port (implies --foreground)
@@ -108,7 +110,12 @@ rather it stood down on its own, set `MDVIEW_IDLE_TIMEOUT` to a number of
108
110
  minutes and it will exit that long after the last browser tab disconnects; the
109
111
  next `mdview` starts it again.
110
112
 
111
- Its state lives in `~/.config/mdview/` (`daemon.json`, `workspace.json`,
113
+ After you upgrade mdview, the next `mdview <path>` notices that the background
114
+ server runs an older version, restarts it and prints one line
115
+ (`mdview: restarted the background server (0.10.0 → 0.11.0)`). Your open folders
116
+ stay open. `mdview ls` only reports the mismatch.
117
+
118
+ Its state lives in `~/.config/mdview/` (`daemon.json`, `workspace.json`, `update-check.json`,
112
119
  `daemon.log`) and nowhere else. `workspace.json` is also how the CLI talks to
113
120
  the server: `mdview <path>` writes the file and the server picks the change up,
114
121
  which is why there is no HTTP endpoint that can change what's being served.
@@ -125,8 +132,9 @@ entirely.
125
132
  `kanagawa`, `catppuccin`, or `high-contrast`.
126
133
  It wins over both `.mdview.json` and the global config, and never writes to
127
134
  either. There is also a `--vscode` flag, used by the VS Code extension to run
128
- mdview as a sidecar; it implies `--no-open` and prints a single JSON ready line
129
- instead of the human-readable output.
135
+ mdview as a sidecar; it implies `--no-open`, prints a single JSON ready line
136
+ instead of the human-readable output, and needs the per-launch token the
137
+ extension passes in `MDVIEW_EMBED_TOKEN`.
130
138
 
131
139
  `--port` and `--palette` both override a setting that belongs to a whole server,
132
140
  and the background one is shared with every other folder you have open — so each
@@ -154,7 +162,7 @@ unexpected errors.
154
162
  | `⇧H` / `⇧L` | Previous / next file in folder |
155
163
  | `Ctrl+D` / `Ctrl+U` | Half-page down / up |
156
164
  | `Alt` / `⌥` + scroll | Fast scroll (~4×) in the main pane |
157
- | `f` / `m` | Toggle focus mode / minimap |
165
+ | `f` / `m` / `w` | Toggle focus mode / minimap / wide layout |
158
166
  | `e` / `⇧E` | Expand all / collapse all sections |
159
167
  | `Tab` (in search) | Cycle Doc ↔ Folder scope |
160
168
  | `Enter` / `Shift+Enter` | Next / previous match in search |
@@ -169,10 +177,10 @@ Click the keyboard icon in the header anytime to see the full list.
169
177
  - 3-pane layout: folder tree, content, outline. Both sidebars collapse to a thin label rail; both have drag handles to resize (widths persist).
170
178
  - Editorial typography (serif body, italic accent H1, paper-grain background, JetBrains Mono in code blocks).
171
179
  - Light & dark theme — follows your OS preference, with a manual override that persists.
172
- - Eight built-in palettes: flexoki / paper / solarized / everforest / rose-pine / kanagawa / catppuccin / high-contrast. Pick one from the header palette swatch. Each defines a full light and dark variant and its own ground texture: a dot grain on the paper-like palettes, fibre noise on Everforest and Kanagawa, an accent wash on Rosé Pine and Catppuccin, and none at all on high-contrast. Code blocks follow the palette (Everforest syntax in Everforest, Catppuccin in Catppuccin, etc.).
180
+ - Eight built-in palettes: flexoki / paper / solarized / everforest / rose-pine / kanagawa / catppuccin / high-contrast. Pick one from the View menu (gear icon in the header). Each defines a full light and dark variant and its own ground texture: a dot grain on the paper-like palettes, fibre noise on Everforest and Kanagawa, an accent wash on Rosé Pine and Catppuccin, and none at all on high-contrast. Code blocks follow the palette (Everforest syntax in Everforest, Catppuccin in Catppuccin, etc.).
173
181
  - Reading-progress bar pinned to the bottom of the header.
174
182
  - Doc stats strip below the H1 (reading time, word count, heading count, "Updated N ago").
175
- - Focus mode dims everything except the section at the viewport center.
183
+ - Focus mode dims everything except the section you are reading: the one whose heading last crossed the top-third of the viewport.
176
184
  - Optional minimap rail at the right edge: bars per heading, draggable to scroll, viewport indicator follows.
177
185
 
178
186
  **Navigation**
@@ -190,7 +198,7 @@ Click the keyboard icon in the header anytime to see the full list.
190
198
  - Math via KaTeX (`$inline$` and `$$block$$`), lazy-loaded only when a doc contains math.
191
199
  - Custom-styled task list checkboxes (filled accent when checked, hollow when unchecked).
192
200
  - Front matter detected and shown as a small collapsible metadata block.
193
- - Inline HTML pass-through (your content, your trust).
201
+ - Inline HTML pass-through (your content, your trust), except `<meta>` and `<base>`, which could send the tab to another site.
194
202
  - Images render via a safe `/__asset/*` route with relative-path resolution against the file's folder. Click any image to open in a lightbox.
195
203
  - External links get an `↗` icon and open in a new tab with `noopener noreferrer`.
196
204
 
@@ -225,6 +233,8 @@ Two layered files, both optional, same schema:
225
233
  }
226
234
  ```
227
235
 
236
+ The global file also takes `"updateCheck": true` (see [Updates](#updates)). A per-project file ignores it.
237
+
228
238
  All fields optional. Validated on load — invalid values fall back silently with a warning. Edit the per-project file while the server is running and the SPA picks up palette/font/lineWidth changes live; `ignore` is read once at startup (the file watcher's skip list is frozen — restart mdview after editing).
229
239
 
230
240
  `ignore` extends the built-in skip list (which already covers `node_modules`, `dist`, `build`, `out`, `target`, `coverage`, `vendor`, `__pycache__`, `venv`, `Pods`, `DerivedData`, `_build`, plus every dotfile/dotdir). Add your own basenames here — handy when running mdview at a repo root with non-standard build dirs to avoid `EMFILE: too many open files` from the OS file-watch limit.
@@ -236,11 +246,43 @@ mdview config path # print the global config path
236
246
  mdview config ignore list # show built-in + user-added entries
237
247
  mdview config ignore add deps _site # add one or more basenames
238
248
  mdview config ignore rm deps # remove one or more basenames
249
+ mdview config update-check on # daily check for a newer release (off by default)
250
+ ```
251
+
252
+ ## Updates
253
+
254
+ `mdview update` asks `registry.npmjs.org` for the latest version. When it is
255
+ newer than the one you run, it prints the install command. It does not run it.
256
+
239
257
  ```
258
+ $ mdview update
259
+ mdview 0.11.0 → 0.12.0 is available.
260
+ Install it with:
261
+
262
+ /Users/you/.nvm/versions/node/v22.20.0/bin/npm install -g @hardikg/mdview@0.12.0
263
+
264
+ The background server restarts itself on the next `mdview` run.
265
+ ```
266
+
267
+ The command uses the `npm` that sits next to the Node running mdview, so the
268
+ upgrade lands in the install you use. On Windows the line is written for `cmd`;
269
+ in PowerShell, put `&` in front of a quoted path. The request carries nothing
270
+ about you: no identifier, cookie or custom header.
271
+
272
+ A daily notice is available and **off by default**:
273
+
274
+ ```bash
275
+ mdview config update-check on # `off` to disable, no argument to show
276
+ ```
277
+
278
+ While it is on, the background server checks once a day. `mdview <path>` then
279
+ prints one line to stderr, at most once a day per version, and only in a
280
+ terminal: ``mdview 0.12.0 is available — run `mdview update` for the install command.``
281
+ Only the global config can turn it on. A `.mdview.json` in a cloned repo cannot.
240
282
 
241
283
  ## Stack
242
284
 
243
- Node 20+, TypeScript, Fastify 5, markdown-it 14, Shiki 1, js-yaml 4, chokidar 4, Preact 10 + @preact/signals, KaTeX 0.16, pele (lazy), JetBrains Mono via @fontsource, Vite 6, tsup 8, vitest 2.
285
+ Node 22.12+, TypeScript 7, Fastify 5, markdown-it 15, Shiki 4, js-yaml 5, chokidar 4, Preact 11 + @preact/signals, KaTeX 0.19, pele (lazy), JetBrains Mono via @fontsource, Vite 8, tsup 9, vitest 5.
244
286
 
245
287
  ## Documentation
246
288
 
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_PALETTE
4
+ } from "./chunk-XFRQFZZ4.mjs";
5
+
6
+ // src/render/shiki.ts
7
+ import { createHash } from "crypto";
8
+ import { createHighlighter } from "shiki";
9
+
10
+ // src/render/byte-lru.ts
11
+ var ByteLru = class {
12
+ constructor(maxBytes) {
13
+ this.maxBytes = maxBytes;
14
+ }
15
+ maxBytes;
16
+ entries = /* @__PURE__ */ new Map();
17
+ totalBytes = 0;
18
+ hitCount = 0;
19
+ missCount = 0;
20
+ get(key) {
21
+ const entry = this.entries.get(key);
22
+ if (entry === void 0) {
23
+ this.missCount++;
24
+ return void 0;
25
+ }
26
+ this.hitCount++;
27
+ this.entries.delete(key);
28
+ this.entries.set(key, entry);
29
+ return entry.value;
30
+ }
31
+ set(key, value) {
32
+ const bytes = Buffer.byteLength(key, "utf8") + Buffer.byteLength(value, "utf8");
33
+ this.delete(key);
34
+ if (bytes > this.maxBytes) return;
35
+ this.entries.set(key, { value, bytes });
36
+ this.totalBytes += bytes;
37
+ for (const [oldest, entry] of this.entries) {
38
+ if (this.totalBytes <= this.maxBytes) break;
39
+ this.entries.delete(oldest);
40
+ this.totalBytes -= entry.bytes;
41
+ }
42
+ }
43
+ delete(key) {
44
+ const entry = this.entries.get(key);
45
+ if (entry === void 0) return;
46
+ this.entries.delete(key);
47
+ this.totalBytes -= entry.bytes;
48
+ }
49
+ stats() {
50
+ return {
51
+ entries: this.entries.size,
52
+ bytes: this.totalBytes,
53
+ hits: this.hitCount,
54
+ misses: this.missCount
55
+ };
56
+ }
57
+ };
58
+
59
+ // src/render/shiki.ts
60
+ var COMMON_LANGUAGES = [
61
+ "ts",
62
+ "tsx",
63
+ "js",
64
+ "jsx",
65
+ "python",
66
+ "go",
67
+ "rust",
68
+ "java",
69
+ "kotlin",
70
+ "css",
71
+ "scss",
72
+ "html",
73
+ "json",
74
+ "yaml",
75
+ "toml",
76
+ "bash",
77
+ "shell",
78
+ "sql",
79
+ "md",
80
+ "diff",
81
+ "dockerfile"
82
+ ];
83
+ var PALETTE_THEMES = {
84
+ // Flexoki has no upstream TextMate theme. `min-light` and `vesper` are the
85
+ // closest bundled stand-ins: both are deliberately low-chroma, which is the
86
+ // one property of Flexoki that a louder theme would contradict.
87
+ flexoki: { light: "min-light", dark: "vesper" },
88
+ paper: { light: "min-light", dark: "vitesse-dark" },
89
+ solarized: { light: "solarized-light", dark: "solarized-dark" },
90
+ everforest: { light: "everforest-light", dark: "everforest-dark" },
91
+ "rose-pine": { light: "rose-pine-dawn", dark: "rose-pine" },
92
+ kanagawa: { light: "kanagawa-lotus", dark: "kanagawa-wave" },
93
+ catppuccin: { light: "catppuccin-latte", dark: "catppuccin-mocha" },
94
+ "high-contrast": { light: "github-light-high-contrast", dark: "github-dark-high-contrast" }
95
+ };
96
+ var INITIAL_THEMES = Array.from(new Set(Object.values(PALETTE_THEMES[DEFAULT_PALETTE])));
97
+ var WARM_SNIPPETS = [
98
+ ["ts", "import { a } from './a';\ninterface Row<T> { id: number; value?: T }"],
99
+ ["ts", "export class Box<T> extends Base implements Row<T> {\n private items: Map<string, T[]> = new Map();\n}"],
100
+ ["ts", `async function run(x: number, y = \`n:\${x}\`): Promise<void> {
101
+ const re = /a+b/g; // note
102
+ for (const [k, v] of m) { if (v.length > 0 && k !== "") await f(k); }
103
+ }`],
104
+ ["js", `const f = (a, b) => { /* c */ return a?.b ?? \`\${b}\`; };
105
+ export default class A { static x = 1 }`],
106
+ ["json", '{ "a": [1, 2.5, true, null], "b": { "c": "d" } }'],
107
+ ["bash", '#!/bin/bash\nset -e\nfor f in *.md; do echo "$f" | grep -q x && cat "$f"; done'],
108
+ ["python", 'import os\n@dec\ndef f(x: int = 1) -> str:\n """doc"""\n return f"{x}" if x else None']
109
+ ];
110
+ var HIGHLIGHT_CACHE_BYTES = 8 * 1024 * 1024;
111
+ var highlightCache = new ByteLru(HIGHLIGHT_CACHE_BYTES);
112
+ var highlighterPromise = null;
113
+ var themeLoads = /* @__PURE__ */ new Map();
114
+ function getHighlighter() {
115
+ if (!highlighterPromise) {
116
+ highlighterPromise = createHighlighter({
117
+ themes: INITIAL_THEMES,
118
+ langs: COMMON_LANGUAGES
119
+ }).catch((err) => {
120
+ highlighterPromise = null;
121
+ throw err;
122
+ });
123
+ }
124
+ return highlighterPromise;
125
+ }
126
+ async function ensureThemes(highlighter, palette) {
127
+ const { light, dark } = PALETTE_THEMES[palette];
128
+ const loaded = highlighter.getLoadedThemes();
129
+ const pending = [];
130
+ for (const theme of /* @__PURE__ */ new Set([light, dark])) {
131
+ if (loaded.includes(theme)) continue;
132
+ let load = themeLoads.get(theme);
133
+ if (!load) {
134
+ load = highlighter.loadTheme(theme).then(() => {
135
+ themeLoads.delete(theme);
136
+ }).catch((err) => {
137
+ themeLoads.delete(theme);
138
+ throw err;
139
+ });
140
+ themeLoads.set(theme, load);
141
+ }
142
+ pending.push(load);
143
+ }
144
+ await Promise.all(pending);
145
+ }
146
+ async function warmHighlighter() {
147
+ try {
148
+ const highlighter = await getHighlighter();
149
+ for (const [lang, snippet] of WARM_SNIPPETS) {
150
+ highlighter.codeToHtml(snippet, {
151
+ lang,
152
+ themes: PALETTE_THEMES[DEFAULT_PALETTE],
153
+ defaultColor: false
154
+ });
155
+ await new Promise((resolve) => setImmediate(resolve));
156
+ }
157
+ } catch {
158
+ }
159
+ }
160
+ function escapeHtml(s) {
161
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
162
+ }
163
+ async function highlightCode(code, lang, palette = DEFAULT_PALETTE) {
164
+ const key = `${palette}\0${lang}\0${createHash("sha256").update(code).digest("hex")}`;
165
+ const cached = highlightCache.get(key);
166
+ if (cached !== void 0) return cached;
167
+ const highlighter = await getHighlighter();
168
+ const loaded = highlighter.getLoadedLanguages();
169
+ let effectiveLang = lang;
170
+ if (lang && !loaded.includes(lang) && !["text", "plain", ""].includes(lang)) {
171
+ try {
172
+ await highlighter.loadLanguage(lang);
173
+ } catch {
174
+ effectiveLang = "text";
175
+ }
176
+ }
177
+ try {
178
+ await ensureThemes(highlighter, palette);
179
+ const html = highlighter.codeToHtml(code, {
180
+ lang: effectiveLang || "text",
181
+ themes: PALETTE_THEMES[palette],
182
+ defaultColor: false
183
+ });
184
+ highlightCache.set(key, html);
185
+ return html;
186
+ } catch {
187
+ return `<pre class="shiki shiki-fallback"><code>${escapeHtml(code)}</code></pre>`;
188
+ }
189
+ }
190
+
191
+ export {
192
+ highlightCache,
193
+ warmHighlighter,
194
+ highlightCode
195
+ };
196
+ //# sourceMappingURL=chunk-RBYD2HWV.mjs.map
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PALETTES
4
+ } from "./chunk-XFRQFZZ4.mjs";
5
+
6
+ // src/server/config.ts
7
+ import { readFile } from "fs/promises";
8
+ import os from "os";
9
+ import path from "path";
10
+ var CONFIG_FILENAME = ".mdview.json";
11
+ var GLOBAL_CONFIG_FILENAME = "config.json";
12
+ var FONT_FAMILIES = ["serif", "sans", "mono"];
13
+ var IGNORE_ENTRY_PATTERN = /^[A-Za-z0-9_.\-+]{1,64}$/;
14
+ function isObject(v) {
15
+ return typeof v === "object" && v !== null && !Array.isArray(v);
16
+ }
17
+ function globalConfigPath() {
18
+ const xdg = process.env.XDG_CONFIG_HOME;
19
+ const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
20
+ return path.join(base, "mdview", GLOBAL_CONFIG_FILENAME);
21
+ }
22
+ function validateConfig(raw, source = CONFIG_FILENAME, scope = "project") {
23
+ if (!isObject(raw)) return null;
24
+ const out = {};
25
+ if ("palette" in raw) {
26
+ const p = raw.palette;
27
+ if (typeof p === "string" && PALETTES.includes(p)) {
28
+ out.palette = p;
29
+ } else {
30
+ console.warn(`[mdview] ${source}: ignoring invalid palette: ${String(p)}`);
31
+ }
32
+ }
33
+ if ("fontFamily" in raw) {
34
+ const f = raw.fontFamily;
35
+ if (typeof f === "string" && FONT_FAMILIES.includes(f)) {
36
+ out.fontFamily = f;
37
+ } else {
38
+ console.warn(`[mdview] ${source}: ignoring invalid fontFamily: ${String(f)}`);
39
+ }
40
+ }
41
+ if ("lineWidth" in raw) {
42
+ const w = raw.lineWidth;
43
+ if (typeof w === "string" && w.length > 0 && w.length <= 32 && /^[\w%.-]+$/.test(w)) {
44
+ out.lineWidth = w;
45
+ } else {
46
+ console.warn(`[mdview] ${source}: ignoring invalid lineWidth: ${String(w)}`);
47
+ }
48
+ }
49
+ if ("defaultCollapsed" in raw && isObject(raw.defaultCollapsed)) {
50
+ const dc = raw.defaultCollapsed;
51
+ const collapsed = {};
52
+ if (typeof dc.tree === "boolean") collapsed.tree = dc.tree;
53
+ if (typeof dc.outline === "boolean") collapsed.outline = dc.outline;
54
+ if (collapsed.tree !== void 0 || collapsed.outline !== void 0) {
55
+ out.defaultCollapsed = collapsed;
56
+ }
57
+ }
58
+ if ("ignore" in raw) {
59
+ const ig = raw.ignore;
60
+ if (Array.isArray(ig)) {
61
+ const cleaned = [];
62
+ for (const entry of ig) {
63
+ if (typeof entry === "string" && entry !== "." && entry !== ".." && IGNORE_ENTRY_PATTERN.test(entry)) {
64
+ cleaned.push(entry);
65
+ } else {
66
+ console.warn(`[mdview] ${source}: ignoring invalid ignore entry: ${String(entry)}`);
67
+ }
68
+ }
69
+ if (cleaned.length > 0) out.ignore = cleaned;
70
+ } else {
71
+ console.warn(`[mdview] ${source}: ignoring invalid ignore (expected array of strings)`);
72
+ }
73
+ }
74
+ if ("updateCheck" in raw) {
75
+ if (scope !== "global") {
76
+ console.warn(`[mdview] ${source}: updateCheck is only read from the global config; ignoring`);
77
+ } else if (typeof raw.updateCheck === "boolean") {
78
+ out.updateCheck = raw.updateCheck;
79
+ } else {
80
+ console.warn(`[mdview] ${source}: ignoring invalid updateCheck: ${String(raw.updateCheck)}`);
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+ async function loadConfigFromPath(filePath, source, scope) {
86
+ let raw;
87
+ try {
88
+ raw = await readFile(filePath, "utf8");
89
+ } catch (err) {
90
+ if (err.code === "ENOENT") return null;
91
+ console.warn(`[mdview] failed to read ${source}: ${err.message}`);
92
+ return null;
93
+ }
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(raw);
97
+ } catch (err) {
98
+ console.warn(`[mdview] ${source} is not valid JSON: ${err.message}`);
99
+ return null;
100
+ }
101
+ return validateConfig(parsed, source, scope);
102
+ }
103
+ async function loadProjectConfig(rootAbsPath) {
104
+ return loadConfigFromPath(path.join(rootAbsPath, CONFIG_FILENAME), CONFIG_FILENAME, "project");
105
+ }
106
+ async function loadGlobalConfig() {
107
+ return loadConfigFromPath(
108
+ globalConfigPath(),
109
+ `~/.config/mdview/${GLOBAL_CONFIG_FILENAME}`,
110
+ "global"
111
+ );
112
+ }
113
+ function mergeConfigs(global, project) {
114
+ if (!global && !project) return null;
115
+ const out = {};
116
+ if (global?.palette !== void 0) out.palette = global.palette;
117
+ if (project?.palette !== void 0) out.palette = project.palette;
118
+ if (global?.fontFamily !== void 0) out.fontFamily = global.fontFamily;
119
+ if (project?.fontFamily !== void 0) out.fontFamily = project.fontFamily;
120
+ if (global?.lineWidth !== void 0) out.lineWidth = global.lineWidth;
121
+ if (project?.lineWidth !== void 0) out.lineWidth = project.lineWidth;
122
+ if (global?.defaultCollapsed || project?.defaultCollapsed) {
123
+ out.defaultCollapsed = { ...global?.defaultCollapsed, ...project?.defaultCollapsed };
124
+ }
125
+ const ig = [...global?.ignore ?? [], ...project?.ignore ?? []];
126
+ if (ig.length > 0) {
127
+ out.ignore = Array.from(new Set(ig));
128
+ }
129
+ return out;
130
+ }
131
+ async function loadEffectiveConfig(rootAbsPath) {
132
+ const [global, project] = await Promise.all([
133
+ loadGlobalConfig(),
134
+ loadProjectConfig(rootAbsPath)
135
+ ]);
136
+ return mergeConfigs(global, project);
137
+ }
138
+
139
+ // src/server/workspace.ts
140
+ import path2 from "path";
141
+ var ROOT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
142
+ function isValidRootId(id) {
143
+ return id !== "." && id !== ".." && ROOT_ID_PATTERN.test(id);
144
+ }
145
+ function deriveRootId(absPath, taken) {
146
+ const slug = path2.basename(absPath).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/[-.]+$/, "").slice(0, 64);
147
+ const stem = isValidRootId(slug) ? slug : "root";
148
+ if (!taken.has(stem)) return stem;
149
+ for (let n = 2; ; n++) {
150
+ const candidate = `${stem.slice(0, 60)}-${n}`;
151
+ if (!taken.has(candidate)) return candidate;
152
+ }
153
+ }
154
+ function resolveRootId(spec, taken) {
155
+ if (spec.id && isValidRootId(spec.id) && !taken.has(spec.id)) return spec.id;
156
+ return deriveRootId(spec.absPath, taken);
157
+ }
158
+ function describeRoot(spec, id) {
159
+ return {
160
+ id,
161
+ name: spec.name ?? path2.basename(spec.absPath),
162
+ kind: spec.kind,
163
+ filePath: spec.kind === "file" ? spec.filePath ?? "" : ""
164
+ };
165
+ }
166
+ function parseWorkspacePath(roots, wsPath) {
167
+ const primary = roots[0];
168
+ if (!primary) return null;
169
+ const trimmed = wsPath.replace(/^\/+/, "");
170
+ const slash = trimmed.indexOf("/");
171
+ const head = slash === -1 ? trimmed : trimmed.slice(0, slash);
172
+ const match = roots.find((r) => r.root.id === head);
173
+ if (!match) return { state: primary, rel: trimmed };
174
+ return { state: match, rel: slash === -1 ? "" : trimmed.slice(slash + 1) };
175
+ }
176
+ function toWorkspacePath(rootId, rel) {
177
+ return rel ? `${rootId}/${rel}` : rootId;
178
+ }
179
+
180
+ // src/shared/ignore.ts
181
+ var DEFAULT_IGNORED_DIRS = [
182
+ "node_modules",
183
+ "dist",
184
+ "build",
185
+ "out",
186
+ "target",
187
+ "coverage",
188
+ "vendor",
189
+ "__pycache__",
190
+ "venv",
191
+ "Pods",
192
+ "DerivedData",
193
+ "_build"
194
+ ];
195
+
196
+ export {
197
+ CONFIG_FILENAME,
198
+ globalConfigPath,
199
+ loadGlobalConfig,
200
+ loadEffectiveConfig,
201
+ DEFAULT_IGNORED_DIRS,
202
+ isValidRootId,
203
+ deriveRootId,
204
+ resolveRootId,
205
+ describeRoot,
206
+ parseWorkspacePath,
207
+ toWorkspacePath
208
+ };
209
+ //# sourceMappingURL=chunk-WWM6JNAS.mjs.map
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/shared/types.ts
4
+ var DEFAULT_PALETTE = "flexoki";
5
+ var PALETTES = [
6
+ "flexoki",
7
+ "paper",
8
+ "solarized",
9
+ "everforest",
10
+ "rose-pine",
11
+ "kanagawa",
12
+ "catppuccin",
13
+ "high-contrast"
14
+ ];
15
+
16
+ export {
17
+ DEFAULT_PALETTE,
18
+ PALETTES
19
+ };
20
+ //# sourceMappingURL=chunk-XFRQFZZ4.mjs.map