@hardikg/mdview 0.9.1 → 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.
- package/CHANGELOG.md +82 -0
- package/README.md +53 -11
- package/bin/chunk-RBYD2HWV.mjs +196 -0
- package/bin/chunk-WWM6JNAS.mjs +209 -0
- package/bin/chunk-XFRQFZZ4.mjs +20 -0
- package/bin/mdview.mjs +521 -1289
- package/bin/regex-worker.mjs +52 -0
- package/bin/server-KALIU7HM.mjs +1854 -0
- package/bin/shiki-5SFVA32N.mjs +13 -0
- package/dist/client/assets/23I4SOAX-BN9xvzna.js +2 -0
- package/dist/client/assets/26H2HVXD-Cu1OTMxT.js +2 -0
- package/dist/client/assets/2IZ5MNOH-DMjMO3_9.js +1 -0
- package/dist/client/assets/2P3CZEXH-D-4BP7dg.js +2 -0
- package/dist/client/assets/3E5HRNYT-DUYblpdl.js +5 -0
- package/dist/client/assets/3JIRLDUO-C1CwW1Ha.js +1 -0
- package/dist/client/assets/5MJ3UKSG-DtT76qKq.js +2 -0
- package/dist/client/assets/5THPTBWS-Ce_hS451.js +1 -0
- package/dist/client/assets/7WLXJQOR-D6QVYUu9.js +1 -0
- package/dist/client/assets/A255FCTW-woaH7Mj_.js +1 -0
- package/dist/client/assets/AM43MQ64-CsWC_OQH.js +2 -0
- package/dist/client/assets/AOZTNTMC-BIehUQHD.js +5 -0
- package/dist/client/assets/AQNLM6KG-DIUuJgPm.js +4 -0
- package/dist/client/assets/BNU2RHQB-DeXpwU5k.js +2 -0
- package/dist/client/assets/C5HTQT3I-7U13x70S.js +4 -0
- package/dist/client/assets/CPKRWH5G-CRxaqyOs.js +12 -0
- package/dist/client/assets/CUVCMPQX-JTGMOfG2.js +1 -0
- package/dist/client/assets/D4V2MWCI-CytzCr6g.js +1 -0
- package/dist/client/assets/DHKBVIA3-CVnlqiEq.js +1 -0
- package/dist/client/assets/DKTLEL5E-DciMwFJ1.js +1 -0
- package/dist/client/assets/DUZNPW7S-D3pifVNb.js +2 -0
- package/dist/client/assets/E3FWOBN4-D5m5o2g0.js +4 -0
- package/dist/client/assets/E65FQMJL-Bg5F0Zo8.js +1 -0
- package/dist/client/assets/FTXIJQGB-CggtIYAM.js +1 -0
- package/dist/client/assets/G3MDBVQH-BZmnMkFw.js +1 -0
- package/dist/client/assets/G3OATVW2-VpVvT5i2.js +8 -0
- package/dist/client/assets/IV52FOAS-DgPxxRI4.js +5 -0
- package/dist/client/assets/JTJEEBDN-Bl0iUT_r.js +2 -0
- package/dist/client/assets/K7FUICZ3-DgoihkiR.js +2 -0
- package/dist/client/assets/LHH2HHRB-BXHTnBD0.js +2 -0
- package/dist/client/assets/MSPQPZZY-DQg7F6a8.js +4 -0
- package/dist/client/assets/OB6ZFVKF-UHmC8m-y.js +2 -0
- package/dist/client/assets/PLTZDMI2-C1ZAoc-e.js +1 -0
- package/dist/client/assets/R7EPXMMD-ChI1SqjJ.js +5 -0
- package/dist/client/assets/RVARI57O-CgA1WoRv.js +1 -0
- package/dist/client/assets/SPBWZ4VS-EqfCsJ5V.js +1 -0
- package/dist/client/assets/SR7OOV3Q-CFnFs0kL.js +4 -0
- package/dist/client/assets/STAXBNH4-D4c13Xlj.js +2 -0
- package/dist/client/assets/SXFARSFO-CkFSkgdi.js +1 -0
- package/dist/client/assets/TC77ISAL--WP0GBo0.js +2 -0
- package/dist/client/assets/TYIQQHVC-B67pCpxM.js +13 -0
- package/dist/client/assets/TZUXETIT-BFD9W5hl.js +2 -0
- package/dist/client/assets/U7N72BGE-Do3829la.js +6 -0
- package/dist/client/assets/UFPIB6EZ-DTs_MyQv.js +3 -0
- package/dist/client/assets/UO52YEYL-DN1qbGwF.js +3 -0
- package/dist/client/assets/VIK3ONSK-DC8mR2sZ.js +2 -0
- package/dist/client/assets/VVVO4ZVP-DAtGLbxR.js +2 -0
- package/dist/client/assets/WRGXZO53-D30IuREw.js +3 -0
- package/dist/client/assets/X5SG7MGC-CdjdoL_s.js +2 -0
- package/dist/client/assets/XLHPVVFU-BFP9ID9p.js +2 -0
- package/dist/client/assets/XXYPNCD5-DqCewu_S.js +5 -0
- package/dist/client/assets/Z6WRYFJC-BEt1KCoU.js +7 -0
- package/dist/client/assets/ZI2QNEGF-VzC4uHcW.js +8 -0
- package/dist/client/assets/index-B96jWJTi.css +1 -0
- package/dist/client/assets/index-DCU09WmS.js +2 -0
- package/dist/client/assets/katex-C0xaXAD4.js +257 -0
- package/dist/client/assets/katex.min-3X1qOa03.js +1 -0
- package/dist/client/assets/katex.min-DMey5pMQ.css +1 -0
- package/dist/client/assets/lazy-DaMZI1F7.js +45 -0
- package/dist/client/index.html +2 -2
- package/package.json +32 -26
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-BJQEGN-7.js +0 -1
- package/dist/client/assets/arc-C6Oi4WlG.js +0 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CvFdvi3K.js +0 -36
- package/dist/client/assets/blockDiagram-NRAW4CY4-D8P8a3XW.js +0 -129
- package/dist/client/assets/c4Diagram-UCG6FXSJ-WcmmjfHX.js +0 -38
- package/dist/client/assets/channel-UA4WTF8Q.js +0 -1
- package/dist/client/assets/chunk-2Q5K7J3B-LeZsbe9d.js +0 -1
- package/dist/client/assets/chunk-5VM5RSS4-COq9euGx.js +0 -15
- package/dist/client/assets/chunk-F27PBJKO-BxcjnaqW.js +0 -1
- package/dist/client/assets/chunk-G27WJ6UU-CscpPXiS.js +0 -231
- package/dist/client/assets/chunk-JWPE2WC7-DD0M0JL-.js +0 -1
- package/dist/client/assets/chunk-LCL6LL3I-DXhJ59Qq.js +0 -206
- package/dist/client/assets/chunk-POPQ4Y6H-Bj3eKHA8.js +0 -1
- package/dist/client/assets/chunk-SVP7TREG-CDQ9vZv1.js +0 -88
- package/dist/client/assets/chunk-XXDRQBXY-91VBn0TQ.js +0 -1
- package/dist/client/assets/classDiagram-DTDB5LWJ-Ch3MnxRH.js +0 -1
- package/dist/client/assets/classDiagram-v2-JRS7N3AN-Ch3MnxRH.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-BaQN6N6K.js +0 -1
- package/dist/client/assets/cynefin-OW5HDTMX-DFpC3uqz.js +0 -178
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CSeSIxw6.js +0 -62
- package/dist/client/assets/cytoscape.esm-BjAbmLdb.js +0 -331
- package/dist/client/assets/dagre-3AP2YEHR-BBkMnw1s.js +0 -4
- package/dist/client/assets/defaultLocale-DX6XiGOO.js +0 -1
- package/dist/client/assets/diagram-S7CK7UJ4-BRdjuShi.js +0 -30
- package/dist/client/assets/diagram-UQ7AKVKN-D-RaNNET.js +0 -41
- package/dist/client/assets/diagram-VSXAHHWV-eL5_j9pa.js +0 -3
- package/dist/client/assets/diagram-VX7I27RA-nlUjCwlX.js +0 -24
- package/dist/client/assets/diagram-Z3DM3KII-DvhXGz8M.js +0 -24
- package/dist/client/assets/ebnfDiagram-PWID7BFC-LgynLQ4Z.js +0 -1
- package/dist/client/assets/erDiagram-SSCWMZ5O-Bc2j9FRV.js +0 -99
- package/dist/client/assets/flowDiagram-A5DVABFB-ClpDS0y_.js +0 -168
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-_RmNHCQH.js +0 -292
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-F1UGJQ4d.js +0 -106
- package/dist/client/assets/index-BP_8buXC.css +0 -1
- package/dist/client/assets/index-DqIihcNP.js +0 -2
- package/dist/client/assets/infoDiagram-RXCK75RN-BR20W1yx.js +0 -2
- package/dist/client/assets/init-Gi6I4Gst.js +0 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-D6bgpoOj.js +0 -70
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/dist/client/assets/journeyDiagram-EYS64GPL-CBjB_0Bx.js +0 -139
- package/dist/client/assets/kanban-definition-3QL26DDD-Bz0HhYZd.js +0 -89
- package/dist/client/assets/katex-C5jXJg4s.js +0 -257
- package/dist/client/assets/katex.min-BYZTj3zW.css +0 -1
- package/dist/client/assets/katex.min-DXaO1tdm.js +0 -1
- package/dist/client/assets/layout-BmTy4KT7.js +0 -1
- package/dist/client/assets/linear-Cgk2Kw-d.js +0 -1
- package/dist/client/assets/mermaid.core-Dq2piIpp.js +0 -312
- package/dist/client/assets/mindmap-definition-FBJOCRG2-BSbZ3zKk.js +0 -96
- package/dist/client/assets/ordinal-Cboi1Yqb.js +0 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-BLsHBZ_O.js +0 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-BTtaojGC.js +0 -39
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-W-VPz1gZ.js +0 -7
- package/dist/client/assets/railroadDiagram-O6MQD6OU-CstXUJhE.js +0 -1
- package/dist/client/assets/requirementDiagram-EFPCY7ZU-DPpI_Lvd.js +0 -84
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-B3HiW1sY.js +0 -40
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CfpZAY9b.js +0 -162
- package/dist/client/assets/sizeCapture-X5ZJPWSS-Dsdx6W_J.js +0 -1
- package/dist/client/assets/stateDiagram-HBIQ2CUA-DPm-o7Ee.js +0 -1
- package/dist/client/assets/stateDiagram-v2-4QOOHH4V-DDqXMzve.js +0 -1
- package/dist/client/assets/swimlanes-XN3QIQJK-BPdZeRls.js +0 -1
- package/dist/client/assets/swimlanesDiagram-VK2B7HYN-BHORPcrU.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-1bs3iV0k.js +0 -120
- package/dist/client/assets/vennDiagram-4TSXK5OY-vYOlmqTL.js +0 -34
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-DTPUMRNL.js +0 -78
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-M5kqQyt7.js +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,88 @@ 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
|
+
|
|
73
|
+
## [0.10.0] — 2026-10-19
|
|
74
|
+
|
|
75
|
+
A security fix for front matter, and a lighter diagram renderer that follows
|
|
76
|
+
your palette. Upgrade if you open markdown you did not write: in 0.9.1 and
|
|
77
|
+
earlier, a file could run code on your machine.
|
|
78
|
+
|
|
79
|
+
### Security
|
|
80
|
+
- **Front matter is now parsed as YAML only.** Previously a file that began `---js` (or `---javascript`) had its front matter evaluated as JavaScript inside the server process when you opened it, so an untrusted `.md` file could run code on your machine. That path is gone.
|
|
81
|
+
- **YAML aliases can no longer expand a small front matter block into a huge response.** A few hundred bytes of nested aliases could produce hundreds of MB of JSON, or a cycle, and take the server down. The parsed front matter is now bounded at 10,000 nodes; a block over that is treated as plain text.
|
|
82
|
+
- **`gray-matter` is replaced by `js-yaml` 4.** This drops `js-yaml` 3, `argparse` 1 and `sprintf-js`, which `npm audit` flagged, so `npm audit --omit=dev` reports 0 vulnerabilities again.
|
|
83
|
+
- **`img-src` no longer allows `blob:`.** Only the old diagram output needed it.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
- **Only a bare `---` line opens front matter.** A `---js`, `---json` or `---yaml` opening line is now body text. A front matter block that is a list or a single value is still removed from the page, but shows no metadata.
|
|
87
|
+
- **Mermaid diagrams now render with pele instead of the mermaid library.** The lazy diagram code drops from about 899 kB to about 287 kB gzipped across all chunks, and a page loads only the diagram types it uses. Diagrams follow the active palette and light/dark through CSS variables, with no re-render on a switch. Diagrams look different, because pele has its own layout and visual style. A left-to-right flowchart too wide for the text column now switches to a top-down layout at full text size, where it used to shrink to fit.
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
- **External-link marking no longer breaks on SVG links.** An inline SVG anchor has read-only `target` and `rel` properties, so assigning them threw and stopped the marking of every later link. SVG anchors are now skipped.
|
|
91
|
+
|
|
10
92
|
## [0.9.1] — 2026-10-04
|
|
11
93
|
|
|
12
94
|
A file tree that keeps deep paths readable, and a dependency refresh.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](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.
|
|
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 >=
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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**
|
|
@@ -186,11 +194,11 @@ Click the keyboard icon in the header anytime to see the full list.
|
|
|
186
194
|
**Rendering**
|
|
187
195
|
- CommonMark + GFM (tables, task lists, strikethrough, autolinks).
|
|
188
196
|
- Server-side syntax highlighting via Shiki, palette-aware: two variants per token, light and dark, for whichever palette is active. Zero client highlighter bundle, and the light/dark toggle repaints with no request.
|
|
189
|
-
- Mermaid diagrams, lazy-loaded only when a doc contains a `mermaid` fence.
|
|
197
|
+
- Mermaid diagrams, rendered with [pele](https://github.com/obsidianmd/pele) and lazy-loaded only when a doc contains a `mermaid` fence. Colours follow the active palette and light/dark.
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|