@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
@@ -0,0 +1,1854 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CONFIG_FILENAME,
4
+ DEFAULT_IGNORED_DIRS,
5
+ describeRoot,
6
+ loadEffectiveConfig,
7
+ parseWorkspacePath,
8
+ resolveRootId,
9
+ toWorkspacePath
10
+ } from "./chunk-WWM6JNAS.mjs";
11
+ import {
12
+ highlightCode
13
+ } from "./chunk-RBYD2HWV.mjs";
14
+ import {
15
+ DEFAULT_PALETTE,
16
+ PALETTES
17
+ } from "./chunk-XFRQFZZ4.mjs";
18
+
19
+ // src/server/index.ts
20
+ import { timingSafeEqual } from "crypto";
21
+ import path10 from "path";
22
+ import chokidar2 from "chokidar";
23
+ import Fastify from "fastify";
24
+
25
+ // src/server/fs/grep.ts
26
+ import { readFile, realpath as realpath2, stat } from "fs/promises";
27
+ import path4 from "path";
28
+ import { setImmediate as yieldToEventLoop } from "timers/promises";
29
+
30
+ // src/render/frontmatter.ts
31
+ import { CORE_SCHEMA, load, mergeTag } from "js-yaml";
32
+ var MAX_NODES = 1e4;
33
+ var MAX_STRING_CHARS = 1e6;
34
+ var SCHEMA = CORE_SCHEMA.withTags(mergeTag);
35
+ function unparsed(raw) {
36
+ return { data: null, body: raw, bodyStartLine: 0 };
37
+ }
38
+ function parseFrontmatter(raw) {
39
+ const block = locateBlock(raw);
40
+ if (!block) return unparsed(raw);
41
+ let loaded = {};
42
+ if (!isBlank(block.yaml)) {
43
+ try {
44
+ loaded = load(block.yaml, { schema: SCHEMA });
45
+ } catch {
46
+ return unparsed(raw);
47
+ }
48
+ if (!withinBudget(loaded)) return unparsed(raw);
49
+ }
50
+ return {
51
+ data: isPlainObject(loaded) ? loaded : null,
52
+ body: block.body,
53
+ bodyStartLine: block.bodyStartLine
54
+ };
55
+ }
56
+ function locateBlock(raw) {
57
+ if (!isFenceLine(raw, 0)) return null;
58
+ let next = nextLineStart(raw, 0);
59
+ const yamlStart = next;
60
+ let closingIdx = -1;
61
+ let closingStart = -1;
62
+ for (let i = 1; next >= 0; i++) {
63
+ if (isFenceLine(raw, next)) {
64
+ closingIdx = i;
65
+ closingStart = next;
66
+ break;
67
+ }
68
+ next = nextLineStart(raw, next);
69
+ }
70
+ if (closingIdx < 0) return null;
71
+ let bodyStart = nextLineStart(raw, closingStart);
72
+ let bodyStartLine = closingIdx + 1;
73
+ while (bodyStart >= 0 && lineEnd(raw, bodyStart) === bodyStart) {
74
+ bodyStartLine++;
75
+ bodyStart = nextLineStart(raw, bodyStart);
76
+ }
77
+ return {
78
+ yaml: raw.slice(yamlStart, closingStart).replace(/\r?\n$/, "").replace(/\r\n/g, "\n"),
79
+ body: bodyStart < 0 ? "" : raw.slice(bodyStart),
80
+ bodyStartLine
81
+ };
82
+ }
83
+ function lineEnd(raw, start) {
84
+ const nl = raw.indexOf("\n", start);
85
+ if (nl < 0) return raw.length;
86
+ return nl > start && raw.charCodeAt(nl - 1) === 13 ? nl - 1 : nl;
87
+ }
88
+ function nextLineStart(raw, start) {
89
+ const nl = raw.indexOf("\n", start);
90
+ return nl < 0 ? -1 : nl + 1;
91
+ }
92
+ function isFenceLine(raw, start) {
93
+ return lineEnd(raw, start) - start === 3 && raw.startsWith("---", start);
94
+ }
95
+ function isBlank(yaml) {
96
+ return yaml.split(/\r?\n/).every((line) => /^\s*(?:#.*)?$/.test(line));
97
+ }
98
+ function withinBudget(root) {
99
+ const stack = [root];
100
+ let visited = 0;
101
+ let chars = 0;
102
+ while (stack.length > 0) {
103
+ const node = stack.pop();
104
+ if (++visited > MAX_NODES) return false;
105
+ if (typeof node === "string") {
106
+ chars += node.length;
107
+ } else if (Array.isArray(node)) {
108
+ for (const child of node) stack.push(child);
109
+ } else if (isPlainObject(node)) {
110
+ for (const [key, child] of Object.entries(node)) {
111
+ chars += key.length;
112
+ stack.push(child);
113
+ }
114
+ }
115
+ if (chars > MAX_STRING_CHARS) return false;
116
+ }
117
+ return true;
118
+ }
119
+ function isPlainObject(value) {
120
+ if (typeof value !== "object" || value === null) return false;
121
+ const proto = Object.getPrototypeOf(value);
122
+ return proto === Object.prototype || proto === null;
123
+ }
124
+
125
+ // src/shared/search-pattern.ts
126
+ function escapeRegex(s) {
127
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
128
+ }
129
+ function inert(query, valid) {
130
+ return { test: () => null, matchAll: () => [], source: "", flags: "", query, valid };
131
+ }
132
+ function compilePattern(query, opts) {
133
+ if (!query) return inert(query, true);
134
+ let re;
135
+ if (opts.regex) {
136
+ try {
137
+ re = new RegExp(query, opts.caseSensitive ? "g" : "gi");
138
+ } catch {
139
+ return inert(query, false);
140
+ }
141
+ } else {
142
+ let pat = escapeRegex(query);
143
+ if (opts.wholeWord) pat = `\\b${pat}\\b`;
144
+ re = new RegExp(pat, opts.caseSensitive ? "g" : "gi");
145
+ }
146
+ return {
147
+ query,
148
+ valid: true,
149
+ source: re.source,
150
+ flags: re.flags,
151
+ test(text) {
152
+ re.lastIndex = 0;
153
+ return re.exec(text);
154
+ },
155
+ matchAll(text) {
156
+ re.lastIndex = 0;
157
+ const out = [];
158
+ let m;
159
+ while ((m = re.exec(text)) !== null) {
160
+ if (m[0].length === 0) {
161
+ re.lastIndex++;
162
+ continue;
163
+ }
164
+ out.push({ index: m.index, length: m[0].length });
165
+ }
166
+ return out;
167
+ }
168
+ };
169
+ }
170
+
171
+ // src/shared/tree-utils.ts
172
+ var MD_EXT = /* @__PURE__ */ new Set([".md", ".markdown", ".mdx"]);
173
+ function flattenMdRelPaths(nodes, out = []) {
174
+ for (const n of nodes) {
175
+ if (n.type === "file" && n.isMarkdown) out.push(n.relPath);
176
+ else if (n.type === "dir" && n.children) flattenMdRelPaths(n.children, out);
177
+ }
178
+ return out;
179
+ }
180
+
181
+ // src/server/fs/limiter.ts
182
+ var LimiterAbortError = class extends Error {
183
+ };
184
+ var Limiter = class {
185
+ constructor(capacity) {
186
+ this.capacity = capacity;
187
+ }
188
+ capacity;
189
+ used = 0;
190
+ queue = [];
191
+ /** Units currently held. */
192
+ get inUse() {
193
+ return this.used;
194
+ }
195
+ /** Requests waiting for their turn. */
196
+ get waiting() {
197
+ return this.queue.length;
198
+ }
199
+ acquire(weight = 1, signal) {
200
+ if (signal?.aborted) return Promise.reject(new LimiterAbortError("Aborted while queued"));
201
+ if (this.queue.length === 0 && this.fits(weight)) {
202
+ return Promise.resolve(this.take(weight));
203
+ }
204
+ return new Promise((resolve, reject) => {
205
+ const onAbort = () => {
206
+ const i = this.queue.indexOf(waiter);
207
+ if (i >= 0) this.queue.splice(i, 1);
208
+ reject(new LimiterAbortError("Aborted while queued"));
209
+ this.drain();
210
+ };
211
+ const waiter = {
212
+ weight,
213
+ grant: () => {
214
+ signal?.removeEventListener("abort", onAbort);
215
+ resolve(this.take(weight));
216
+ }
217
+ };
218
+ this.queue.push(waiter);
219
+ signal?.addEventListener("abort", onAbort, { once: true });
220
+ });
221
+ }
222
+ fits(weight) {
223
+ return this.used === 0 || this.used + weight <= this.capacity;
224
+ }
225
+ take(weight) {
226
+ this.used += weight;
227
+ let released = false;
228
+ return () => {
229
+ if (released) return;
230
+ released = true;
231
+ this.used -= weight;
232
+ this.drain();
233
+ };
234
+ }
235
+ drain() {
236
+ for (let head = this.queue[0]; head && this.fits(head.weight); head = this.queue[0]) {
237
+ this.queue.shift();
238
+ head.grant();
239
+ }
240
+ }
241
+ };
242
+
243
+ // src/server/fs/regex-runner.ts
244
+ import { Worker } from "worker_threads";
245
+ var WORKER_URL = new URL(
246
+ import.meta.url.endsWith(".ts") ? "./regex-worker.ts" : "./regex-worker.mjs",
247
+ import.meta.url
248
+ );
249
+ var RegexTimeoutError = class extends Error {
250
+ };
251
+ var RegexAbortError = class extends Error {
252
+ };
253
+ var idle = null;
254
+ var nextId = 1;
255
+ function spawn() {
256
+ const worker = new Worker(WORKER_URL);
257
+ worker.unref();
258
+ worker.on("error", () => void 0);
259
+ worker.once("exit", () => {
260
+ if (idle === worker) idle = null;
261
+ });
262
+ return worker;
263
+ }
264
+ async function closeRegexWorkers() {
265
+ const w = idle;
266
+ idle = null;
267
+ if (w) await w.terminate();
268
+ }
269
+ function openRegexSession(opts) {
270
+ let worker = null;
271
+ let started = false;
272
+ let spentMs = 0;
273
+ function ready() {
274
+ if (started) return;
275
+ started = true;
276
+ worker = idle ?? spawn();
277
+ idle = null;
278
+ }
279
+ function kill() {
280
+ const w = worker;
281
+ worker = null;
282
+ void w?.terminate();
283
+ }
284
+ return {
285
+ async match(files) {
286
+ ready();
287
+ const w = worker;
288
+ if (!w) return Promise.reject(new RegexTimeoutError("Regex worker is gone"));
289
+ const remaining = opts.budgetMs - spentMs;
290
+ if (remaining <= 0) {
291
+ kill();
292
+ return Promise.reject(new RegexTimeoutError("Regex search budget used up"));
293
+ }
294
+ if (opts.signal?.aborted) {
295
+ kill();
296
+ return Promise.reject(new RegexAbortError("Search aborted"));
297
+ }
298
+ const id = nextId++;
299
+ const started2 = performance.now();
300
+ return new Promise((resolve, reject) => {
301
+ const cleanup = () => {
302
+ clearTimeout(timer);
303
+ w.off("message", onMessage);
304
+ w.off("error", onError);
305
+ w.off("exit", onExit);
306
+ opts.signal?.removeEventListener("abort", onAbort);
307
+ spentMs += performance.now() - started2;
308
+ };
309
+ const onMessage = (res) => {
310
+ if (res.id !== id) return;
311
+ cleanup();
312
+ if (res.ok) resolve(res.files);
313
+ else reject(new Error("Regex worker could not run the pattern"));
314
+ };
315
+ const onError = (err) => {
316
+ cleanup();
317
+ worker = null;
318
+ reject(err);
319
+ };
320
+ const onExit = () => {
321
+ cleanup();
322
+ worker = null;
323
+ reject(new Error("Regex worker exited"));
324
+ };
325
+ const onAbort = () => {
326
+ cleanup();
327
+ kill();
328
+ reject(new RegexAbortError("Search aborted"));
329
+ };
330
+ const timer = setTimeout(() => {
331
+ cleanup();
332
+ kill();
333
+ reject(new RegexTimeoutError("Regex search budget used up"));
334
+ }, remaining);
335
+ w.on("message", onMessage);
336
+ w.on("error", onError);
337
+ w.on("exit", onExit);
338
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
339
+ const req = {
340
+ id,
341
+ source: opts.source,
342
+ flags: opts.flags,
343
+ maxLineLen: opts.maxLineLen,
344
+ perFileCap: opts.perFileCap,
345
+ files
346
+ };
347
+ w.postMessage(req);
348
+ });
349
+ },
350
+ release() {
351
+ const w = worker;
352
+ worker = null;
353
+ if (w) {
354
+ if (idle) void w.terminate();
355
+ else idle = w;
356
+ }
357
+ }
358
+ };
359
+ }
360
+
361
+ // src/server/fs/resolve.ts
362
+ import { realpath } from "fs/promises";
363
+ import path from "path";
364
+ var MAX_MARKDOWN_BYTES = 16 * 1024 * 1024;
365
+ var OutsideRootError = class extends Error {
366
+ };
367
+ function isWithin(absRoot, candidate) {
368
+ const rel = path.relative(absRoot, candidate);
369
+ return rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
370
+ }
371
+ function hasDotSegment(rel) {
372
+ return path.normalize(rel).split(/[\\/]/).some((seg) => seg.startsWith(".") && seg !== ".");
373
+ }
374
+ async function resolveSafePath(root, relPath, opts = {}) {
375
+ if (path.isAbsolute(relPath)) {
376
+ throw new OutsideRootError(`Refusing absolute path: ${relPath}`);
377
+ }
378
+ const absRoot = path.resolve(root);
379
+ const candidate = path.resolve(absRoot, relPath);
380
+ if (!isWithin(absRoot, candidate)) {
381
+ throw new OutsideRootError(`Path resolves outside root: ${relPath}`);
382
+ }
383
+ const realRoot = opts.realRoot ?? await realpath(absRoot);
384
+ const realCandidate = await realpath(candidate);
385
+ if (!isWithin(realRoot, realCandidate)) {
386
+ throw new OutsideRootError(`Path resolves outside root: ${relPath}`);
387
+ }
388
+ return realCandidate;
389
+ }
390
+
391
+ // src/server/fs/tree.ts
392
+ import { readdir } from "fs/promises";
393
+ import path3 from "path";
394
+
395
+ // src/server/fs/ignore.ts
396
+ import path2 from "path";
397
+ var DEFAULT_IGNORED_DIRS2 = new Set(DEFAULT_IGNORED_DIRS);
398
+ function buildIgnoreSet(extra = []) {
399
+ return /* @__PURE__ */ new Set([...DEFAULT_IGNORED_DIRS2, ...extra]);
400
+ }
401
+ function isDirIgnored(name, set) {
402
+ return name.startsWith(".") || set.has(name);
403
+ }
404
+ function isPathIgnored(abs, root, set) {
405
+ const rel = path2.relative(root, abs);
406
+ if (rel === "" || rel.startsWith("..")) return false;
407
+ for (const seg of rel.split(path2.sep)) {
408
+ if (!seg) continue;
409
+ if (seg.startsWith(".")) return true;
410
+ if (set.has(seg)) return true;
411
+ }
412
+ return false;
413
+ }
414
+
415
+ // src/server/fs/tree.ts
416
+ async function walkFolder(root, opts = {}) {
417
+ const ignore = opts.ignore ?? DEFAULT_IGNORED_DIRS2;
418
+ return walkInner(root, opts.relBase ?? "", ignore);
419
+ }
420
+ async function walkInner(absDir, relBase, ignore) {
421
+ const entries = await readdir(absDir, { withFileTypes: true });
422
+ const out = [];
423
+ for (const entry of entries) {
424
+ if (entry.isDirectory()) {
425
+ if (isDirIgnored(entry.name, ignore)) continue;
426
+ } else if (entry.name.startsWith(".")) {
427
+ continue;
428
+ }
429
+ const childRel = relBase ? `${relBase}/${entry.name}` : entry.name;
430
+ if (entry.isDirectory()) {
431
+ const children = await walkInner(path3.join(absDir, entry.name), childRel, ignore);
432
+ out.push({
433
+ name: entry.name,
434
+ relPath: childRel,
435
+ type: "dir",
436
+ children
437
+ });
438
+ } else if (entry.isFile()) {
439
+ const ext = path3.extname(entry.name).toLowerCase();
440
+ out.push({
441
+ name: entry.name,
442
+ relPath: childRel,
443
+ type: "file",
444
+ isMarkdown: MD_EXT.has(ext)
445
+ });
446
+ }
447
+ }
448
+ out.sort((a, b) => {
449
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
450
+ return a.name.localeCompare(b.name);
451
+ });
452
+ return out;
453
+ }
454
+
455
+ // src/server/fs/grep.ts
456
+ var READ_CONCURRENCY = 8;
457
+ var LITERAL_SLICE_MS = 10;
458
+ var LITERAL_CHECK_STEPS = 4096;
459
+ var searchSlots = new Limiter(2);
460
+ var searchReadBudget = new Limiter(2 * MAX_MARKDOWN_BYTES);
461
+ var BATCH_FILES = 64;
462
+ var BATCH_CHARS = 1e6;
463
+ var ContentCache = class {
464
+ entries = /* @__PURE__ */ new Map();
465
+ maxChars;
466
+ chars = 0;
467
+ constructor(maxChars = 32 * 1024 * 1024) {
468
+ this.maxChars = maxChars;
469
+ }
470
+ get(key, mtimeMs, size) {
471
+ const e = this.entries.get(key);
472
+ if (!e) return void 0;
473
+ if (e.mtimeMs !== mtimeMs || e.size !== size) {
474
+ this.invalidate(key);
475
+ return void 0;
476
+ }
477
+ this.entries.delete(key);
478
+ this.entries.set(key, e);
479
+ return e.body;
480
+ }
481
+ set(key, mtimeMs, size, body) {
482
+ this.invalidate(key);
483
+ if (body.length > this.maxChars / 4) return;
484
+ this.entries.set(key, { mtimeMs, size, body });
485
+ this.chars += body.length;
486
+ for (const [oldKey] of this.entries) {
487
+ if (this.chars <= this.maxChars) break;
488
+ this.invalidate(oldKey);
489
+ }
490
+ }
491
+ invalidate(key) {
492
+ const e = this.entries.get(key);
493
+ if (!e) return;
494
+ this.chars -= e.body.length;
495
+ this.entries.delete(key);
496
+ }
497
+ clear() {
498
+ this.entries.clear();
499
+ this.chars = 0;
500
+ }
501
+ /** The key a root-relative path is cached under. Shared with the watcher hook. */
502
+ static keyFor(rootAbsPath, relPath) {
503
+ return path4.join(rootAbsPath, relPath);
504
+ }
505
+ };
506
+ function buildSnippet(line, start, end, radius) {
507
+ const from = Math.max(0, start - radius);
508
+ const to = Math.min(line.length, end + radius);
509
+ const prefix = from > 0 ? "\u2026" : "";
510
+ const suffix = to < line.length ? "\u2026" : "";
511
+ const sliced = line.slice(from, to);
512
+ const hlStart = prefix.length + (start - from);
513
+ const hlEnd = hlStart + (end - start);
514
+ return {
515
+ snippet: prefix + sliced + suffix,
516
+ highlight: [hlStart, hlEnd]
517
+ };
518
+ }
519
+ function eachLine(body, visit, from = { index: 0, start: 0 }) {
520
+ let start = from.start;
521
+ for (let index = from.index; ; index++) {
522
+ const nl = body.indexOf("\n", start);
523
+ if (nl < 0) {
524
+ visit(index, start, body.length);
525
+ return null;
526
+ }
527
+ const end = nl > start && body.charCodeAt(nl - 1) === 13 ? nl - 1 : nl;
528
+ const more = visit(index, start, end);
529
+ start = nl + 1;
530
+ if (!more) return { index: index + 1, start };
531
+ }
532
+ }
533
+ function linesAt(body, wanted) {
534
+ const out = /* @__PURE__ */ new Map();
535
+ let w = 0;
536
+ if (wanted.length === 0) return out;
537
+ eachLine(body, (index, start, end) => {
538
+ if (wanted[w] === index) out.set(index, body.slice(start, end));
539
+ while (wanted[w] === index) w++;
540
+ return w < wanted.length;
541
+ });
542
+ return out;
543
+ }
544
+ var HOLDS_NOTHING = () => void 0;
545
+ async function loadBody(t, cache, signal, turn) {
546
+ let release = HOLDS_NOTHING;
547
+ let passed = false;
548
+ try {
549
+ const abs = await resolveSafePath(t.root.absPath, t.relPath, { realRoot: t.realRoot });
550
+ const st = await stat(abs);
551
+ if (!st.isFile() || st.size > MAX_MARKDOWN_BYTES) return { body: null, release };
552
+ const key = ContentCache.keyFor(t.root.absPath, t.relPath);
553
+ const cached = cache.get(key, st.mtimeMs, st.size);
554
+ if (cached !== void 0) return { body: cached, release };
555
+ await turn.prev;
556
+ const granted = searchReadBudget.acquire(st.size, signal);
557
+ passed = true;
558
+ turn.pass();
559
+ release = await granted;
560
+ const { body } = parseFrontmatter(await readFile(abs, "utf8"));
561
+ cache.set(key, st.mtimeMs, st.size, body);
562
+ return { body, release };
563
+ } catch {
564
+ release();
565
+ return { body: null, release: HOLDS_NOTHING };
566
+ } finally {
567
+ if (!passed) void turn.prev.then(turn.pass);
568
+ }
569
+ }
570
+ async function* readAhead(targets, cache, concurrency, signal) {
571
+ const inflight = [];
572
+ let next = 0;
573
+ let queued = Promise.resolve();
574
+ const fill = () => {
575
+ while (inflight.length < concurrency && next < targets.length) {
576
+ const prev = queued;
577
+ let pass = HOLDS_NOTHING;
578
+ queued = new Promise((resolve) => {
579
+ pass = resolve;
580
+ });
581
+ inflight.push(loadBody(targets[next++], cache, signal, { prev, pass }));
582
+ }
583
+ };
584
+ try {
585
+ fill();
586
+ for (const t of targets) {
587
+ const loaded = await inflight.shift();
588
+ fill();
589
+ yield [t, loaded];
590
+ }
591
+ } finally {
592
+ for (const pending of inflight) void pending.then((l) => l.release());
593
+ }
594
+ }
595
+ async function collectTargets(roots, opts) {
596
+ const out = [];
597
+ for (const root of roots) {
598
+ let realRoot;
599
+ try {
600
+ realRoot = await realpath2(root.absPath);
601
+ } catch {
602
+ continue;
603
+ }
604
+ const tree = await walkFolder(root.absPath, { ignore: root.ignore ?? opts.ignore });
605
+ for (const relPath of flattenMdRelPaths(tree)) out.push({ root, realRoot, relPath });
606
+ }
607
+ return out;
608
+ }
609
+ async function grepFiles(roots, query, opts = {}) {
610
+ const perFileCap = opts.perFileCap ?? 20;
611
+ const globalCap = opts.globalCap ?? 200;
612
+ const snippetRadius = opts.snippetRadius ?? 60;
613
+ const maxLineLen = opts.maxLineLen ?? 1e4;
614
+ const regexMode = opts.regex ?? false;
615
+ const cache = opts.cache ?? new ContentCache();
616
+ const signal = opts.signal;
617
+ const empty = { query, results: [], truncated: false, timedOut: false };
618
+ if (!query) return empty;
619
+ const pattern = compilePattern(query, {
620
+ caseSensitive: opts.caseSensitive ?? false,
621
+ wholeWord: opts.wholeWord ?? false,
622
+ regex: regexMode
623
+ });
624
+ if (!pattern.valid) return empty;
625
+ const results = [];
626
+ let totalHits = 0;
627
+ let truncated = false;
628
+ let timedOut = false;
629
+ const record = (t, body, m) => {
630
+ if (m.skipped) truncated = true;
631
+ if (m.total === 0) return;
632
+ const lines = linesAt(body, m.hits.map(([lineIdx]) => lineIdx));
633
+ const hits = [];
634
+ let fileTruncated = m.skipped || m.total > m.hits.length;
635
+ for (const [lineIdx, col, len] of m.hits) {
636
+ if (totalHits >= globalCap) {
637
+ truncated = true;
638
+ fileTruncated = true;
639
+ break;
640
+ }
641
+ const { snippet, highlight } = buildSnippet(
642
+ lines.get(lineIdx) ?? "",
643
+ col,
644
+ col + len,
645
+ snippetRadius
646
+ );
647
+ hits.push({ line: lineIdx + 1, col, snippet, highlight });
648
+ totalHits++;
649
+ }
650
+ if (totalHits >= globalCap) truncated = true;
651
+ if (hits.length > 0) {
652
+ results.push({
653
+ relPath: `${t.root.id}/${t.relPath}`,
654
+ hits,
655
+ total: m.total,
656
+ truncated: fileTruncated
657
+ });
658
+ }
659
+ };
660
+ const literalRe = regexMode ? null : new RegExp(pattern.source, pattern.flags);
661
+ let literalLeftMs = opts.literalBudgetMs ?? 1e3;
662
+ let sliceMs = 0;
663
+ let clock = 0;
664
+ let steps = 0;
665
+ const charge = () => {
666
+ const now = performance.now();
667
+ literalLeftMs -= now - clock;
668
+ sliceMs += now - clock;
669
+ clock = now;
670
+ };
671
+ const checkpoint = async () => {
672
+ steps = 0;
673
+ charge();
674
+ if (literalLeftMs <= 0) return "time";
675
+ if (sliceMs < LITERAL_SLICE_MS) return null;
676
+ sliceMs = 0;
677
+ await yieldToEventLoop();
678
+ clock = performance.now();
679
+ return stop.signal.aborted ? "abort" : null;
680
+ };
681
+ const matchLiteral = async (re, body) => {
682
+ const out = { total: 0, hits: [], skipped: false };
683
+ let stopped = null;
684
+ clock = performance.now();
685
+ const visit = (index, start, end) => {
686
+ if (end - start > maxLineLen) {
687
+ out.skipped = true;
688
+ } else {
689
+ const line = body.slice(start, end);
690
+ re.lastIndex = 0;
691
+ for (let m = re.exec(line); m !== null; m = re.exec(line)) {
692
+ out.total++;
693
+ steps++;
694
+ if (out.hits.length < perFileCap) out.hits.push([index, m.index, m[0].length]);
695
+ }
696
+ }
697
+ return ++steps < LITERAL_CHECK_STEPS;
698
+ };
699
+ for (let at = eachLine(body, visit); at !== null; at = eachLine(body, visit, at)) {
700
+ stopped = await checkpoint();
701
+ if (stopped) break;
702
+ }
703
+ charge();
704
+ if (stopped === "time") out.skipped = true;
705
+ return { m: out, stopped };
706
+ };
707
+ const stop = new AbortController();
708
+ const onCallerAbort = () => stop.abort();
709
+ if (signal?.aborted) stop.abort();
710
+ signal?.addEventListener("abort", onCallerAbort, { once: true });
711
+ let releaseSlot;
712
+ try {
713
+ releaseSlot = await searchSlots.acquire(1, stop.signal);
714
+ } catch (err) {
715
+ signal?.removeEventListener("abort", onCallerAbort);
716
+ if (err instanceof LimiterAbortError) return empty;
717
+ throw err;
718
+ }
719
+ let session = null;
720
+ let batch = [];
721
+ let batchChars = 0;
722
+ const flush = async () => {
723
+ if (!session || batch.length === 0) return;
724
+ const pending = batch;
725
+ batch = [];
726
+ batchChars = 0;
727
+ try {
728
+ const matches = await session.match(pending.map(([, body]) => body));
729
+ for (let i = 0; i < pending.length && totalHits < globalCap; i++) {
730
+ const [t, body] = pending[i];
731
+ record(t, body, matches[i]);
732
+ }
733
+ } finally {
734
+ for (const [, , release] of pending) release();
735
+ }
736
+ };
737
+ try {
738
+ const targets = await collectTargets(roots, opts);
739
+ session = regexMode ? openRegexSession({
740
+ source: pattern.source,
741
+ flags: pattern.flags,
742
+ maxLineLen,
743
+ perFileCap,
744
+ budgetMs: opts.regexBudgetMs ?? 3e3,
745
+ signal: stop.signal
746
+ }) : null;
747
+ for await (const [t, { body, release }] of readAhead(
748
+ targets,
749
+ cache,
750
+ READ_CONCURRENCY,
751
+ stop.signal
752
+ )) {
753
+ if (stop.signal.aborted || totalHits >= globalCap) {
754
+ release();
755
+ break;
756
+ }
757
+ if (body === null) continue;
758
+ if (literalRe) {
759
+ let stopped;
760
+ try {
761
+ const res = await matchLiteral(literalRe, body);
762
+ stopped = res.stopped;
763
+ record(t, body, res.m);
764
+ } finally {
765
+ release();
766
+ }
767
+ if (stopped === "time") truncated = true;
768
+ if (stopped) break;
769
+ continue;
770
+ }
771
+ batch.push([t, body, release]);
772
+ batchChars += body.length;
773
+ if (batch.length >= BATCH_FILES || batchChars >= BATCH_CHARS) await flush();
774
+ }
775
+ if (!stop.signal.aborted && totalHits < globalCap) await flush();
776
+ } catch (err) {
777
+ if (err instanceof RegexTimeoutError) {
778
+ timedOut = true;
779
+ truncated = true;
780
+ } else if (!(err instanceof RegexAbortError)) {
781
+ throw err;
782
+ }
783
+ } finally {
784
+ stop.abort();
785
+ signal?.removeEventListener("abort", onCallerAbort);
786
+ for (const [, , release] of batch) release();
787
+ session?.release();
788
+ releaseSlot();
789
+ }
790
+ if (totalHits >= globalCap) truncated = true;
791
+ return { query, results, truncated, timedOut };
792
+ }
793
+
794
+ // src/server/hosts.ts
795
+ var LOOPBACK_NAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
796
+ var MAX_HOST_LEN = 253;
797
+ function hostOnly(value) {
798
+ const h = value.trim().toLowerCase();
799
+ if (h.startsWith("[")) {
800
+ const end = h.indexOf("]");
801
+ return end === -1 ? h : h.slice(0, end + 1);
802
+ }
803
+ if (h.indexOf(":") !== h.lastIndexOf(":")) return h;
804
+ const colon = h.indexOf(":");
805
+ return colon === -1 ? h : h.slice(0, colon);
806
+ }
807
+ function isAllowedHost(value) {
808
+ if (!value || value.length > MAX_HOST_LEN) return false;
809
+ if (!/^[\x21-\x7e]+$/.test(value)) return false;
810
+ const host = hostOnly(value);
811
+ return LOOPBACK_NAMES.has(host) || /^[a-z0-9][a-z0-9.-]*\.localhost$/.test(host);
812
+ }
813
+
814
+ // src/server/routes/api-asset.ts
815
+ import { createReadStream } from "fs";
816
+ import { realpath as realpath4, stat as stat3 } from "fs/promises";
817
+ import path7 from "path";
818
+
819
+ // src/server/routes/api-file.ts
820
+ import { readFile as readFile2, realpath as realpath3, stat as stat2 } from "fs/promises";
821
+ import path6 from "path";
822
+
823
+ // src/render/links.ts
824
+ import path5 from "path/posix";
825
+
826
+ // src/render/html-tags.ts
827
+ function isSpace(c) {
828
+ return c === 9 || c === 10 || c === 12 || c === 13 || c === 32;
829
+ }
830
+ function isAsciiAlpha(c) {
831
+ return c >= 65 && c <= 90 || c >= 97 && c <= 122;
832
+ }
833
+ var GT = 62;
834
+ var SLASH = 47;
835
+ var EQ = 61;
836
+ function nameEnd(html, from) {
837
+ let j = from;
838
+ while (j < html.length) {
839
+ const c = html.charCodeAt(j);
840
+ if (isSpace(c) || c === SLASH || c === GT) break;
841
+ j++;
842
+ }
843
+ return j;
844
+ }
845
+ function readAttrs(html, from, attrs) {
846
+ const n = html.length;
847
+ let j = from;
848
+ for (; ; ) {
849
+ while (j < n && (isSpace(html.charCodeAt(j)) || html.charCodeAt(j) === SLASH)) j++;
850
+ if (j >= n) return -1;
851
+ if (html.charCodeAt(j) === GT) return j + 1;
852
+ const nameStart = j;
853
+ j++;
854
+ while (j < n) {
855
+ const c = html.charCodeAt(j);
856
+ if (isSpace(c) || c === SLASH || c === GT || c === EQ) break;
857
+ j++;
858
+ }
859
+ const nameStop = j;
860
+ let k = j;
861
+ while (k < n && isSpace(html.charCodeAt(k))) k++;
862
+ if (k >= n || html.charCodeAt(k) !== EQ) {
863
+ attrs?.push({
864
+ name: html.slice(nameStart, nameStop).toLowerCase(),
865
+ value: null,
866
+ start: nameStart,
867
+ end: nameStop
868
+ });
869
+ j = k;
870
+ continue;
871
+ }
872
+ k++;
873
+ while (k < n && isSpace(html.charCodeAt(k))) k++;
874
+ if (k >= n) return -1;
875
+ const q = html[k];
876
+ let valueStart;
877
+ let valueEnd;
878
+ if (q === '"' || q === "'") {
879
+ valueStart = k + 1;
880
+ valueEnd = html.indexOf(q, valueStart);
881
+ if (valueEnd < 0) return -1;
882
+ j = valueEnd + 1;
883
+ } else {
884
+ valueStart = k;
885
+ while (k < n && !isSpace(html.charCodeAt(k)) && html.charCodeAt(k) !== GT) k++;
886
+ valueEnd = k;
887
+ j = k;
888
+ }
889
+ attrs?.push({
890
+ name: html.slice(nameStart, nameStop).toLowerCase(),
891
+ value: html.slice(valueStart, valueEnd),
892
+ start: nameStart,
893
+ end: j
894
+ });
895
+ }
896
+ }
897
+ function rewriteTags(html, names, rewrite) {
898
+ const out = [];
899
+ let copied = 0;
900
+ let i = 0;
901
+ const n = html.length;
902
+ while (i < n) {
903
+ const lt = html.indexOf("<", i);
904
+ if (lt < 0) break;
905
+ const c1 = html.charCodeAt(lt + 1);
906
+ if (isAsciiAlpha(c1)) {
907
+ const ne = nameEnd(html, lt + 1);
908
+ const name = html.slice(lt + 1, ne).toLowerCase();
909
+ const wanted = names.has(name);
910
+ const attrs = wanted ? [] : null;
911
+ const end = readAttrs(html, ne, attrs);
912
+ if (end < 0) break;
913
+ if (wanted) {
914
+ const replaced = rewrite({ name, start: lt, end, attrs });
915
+ if (replaced !== null) {
916
+ out.push(html.slice(copied, lt), replaced);
917
+ copied = end;
918
+ }
919
+ }
920
+ i = end;
921
+ continue;
922
+ }
923
+ if (html.startsWith("<!--", lt)) {
924
+ const close = html.indexOf("-->", lt + 4);
925
+ if (close < 0) break;
926
+ i = close + 3;
927
+ continue;
928
+ }
929
+ if (c1 === SLASH && isAsciiAlpha(html.charCodeAt(lt + 2))) {
930
+ const end = readAttrs(html, nameEnd(html, lt + 2), null);
931
+ if (end < 0) break;
932
+ i = end;
933
+ continue;
934
+ }
935
+ if (c1 === SLASH || c1 === 33 || c1 === 63) {
936
+ const close = html.indexOf(">", lt + 2);
937
+ if (close < 0) break;
938
+ i = close + 1;
939
+ continue;
940
+ }
941
+ i = lt + 1;
942
+ }
943
+ if (copied === 0) return html;
944
+ out.push(html.slice(copied));
945
+ return out.join("");
946
+ }
947
+ function hasNameAt(s, at, name) {
948
+ for (let k = 0; k < name.length; k++) {
949
+ if ((s.charCodeAt(at + k) | 32) !== name.charCodeAt(k)) return false;
950
+ }
951
+ return true;
952
+ }
953
+ var STRIPPED = ["meta", "base"];
954
+ var HAS_STRIPPED = /<(?:meta|base)/i;
955
+ var DANGLING_PREFIX = /<(?:m(?:e(?:ta?)?)?|b(?:a(?:se?)?)?)?$/i;
956
+ function popChars(out, count) {
957
+ let taken = "";
958
+ while (taken.length < count && out.length > 0) {
959
+ const last = out.pop();
960
+ const need = count - taken.length;
961
+ if (last.length > need) {
962
+ out.push(last.slice(0, last.length - need));
963
+ taken = last.slice(last.length - need) + taken;
964
+ } else {
965
+ taken = last + taken;
966
+ }
967
+ }
968
+ return taken;
969
+ }
970
+ function stripMetaAndBase(fragment) {
971
+ if (!HAS_STRIPPED.test(fragment)) return fragment;
972
+ const out = [];
973
+ let i = 0;
974
+ const n = fragment.length;
975
+ while (i < n) {
976
+ const lt = fragment.indexOf("<", i);
977
+ if (lt < 0) break;
978
+ const after = lt + 5;
979
+ const c = fragment.charCodeAt(after);
980
+ const isTarget = STRIPPED.some((name) => hasNameAt(fragment, lt + 1, name)) && (after >= n || isSpace(c) || c === SLASH || c === GT);
981
+ if (!isTarget) {
982
+ out.push(fragment.slice(i, lt + 1));
983
+ i = lt + 1;
984
+ continue;
985
+ }
986
+ if (lt > i) out.push(fragment.slice(i, lt));
987
+ const end = readAttrs(fragment, after, null);
988
+ i = end < 0 ? n : end;
989
+ const tail = popChars(out, 5);
990
+ const dangling = DANGLING_PREFIX.exec(tail);
991
+ if (dangling) {
992
+ out.push(`${tail.slice(0, dangling.index)}&lt;${tail.slice(dangling.index + 1)}`);
993
+ } else if (tail) {
994
+ out.push(tail);
995
+ }
996
+ }
997
+ if (i < n) out.push(fragment.slice(i));
998
+ return out.join("");
999
+ }
1000
+
1001
+ // src/render/links.ts
1002
+ var MD_EXT_RE = /\.(md|markdown|mdx)$/i;
1003
+ var ANCHOR = /* @__PURE__ */ new Set(["a"]);
1004
+ var IMG = /* @__PURE__ */ new Set(["img"]);
1005
+ var ENTITY = {
1006
+ "&amp;": "&",
1007
+ "&lt;": "<",
1008
+ "&gt;": ">",
1009
+ "&quot;": '"',
1010
+ "&#39;": "'"
1011
+ };
1012
+ var ESCAPE = {
1013
+ "&": "&amp;",
1014
+ "<": "&lt;",
1015
+ ">": "&gt;",
1016
+ '"': "&quot;",
1017
+ "'": "&#39;"
1018
+ };
1019
+ function unescapeHtml(s) {
1020
+ return s.replace(/&(?:amp|lt|gt|quot|#39);/g, (e) => ENTITY[e] ?? e);
1021
+ }
1022
+ function escapeAttr(s) {
1023
+ return s.replace(/[&<>"']/g, (c) => ESCAPE[c] ?? c);
1024
+ }
1025
+ function firstAttr(tag, name) {
1026
+ return tag.attrs.find((a) => a.name === name);
1027
+ }
1028
+ function splitHref(href) {
1029
+ const q = href.indexOf("?");
1030
+ const h = href.indexOf("#");
1031
+ if (h >= 0 && (q < 0 || h < q)) return { target: href.slice(0, h), hash: href.slice(h) };
1032
+ if (q < 0) return { target: href };
1033
+ if (h < 0) return { target: href.slice(0, q), query: href.slice(q) };
1034
+ return { target: href.slice(0, q), query: href.slice(q, h), hash: href.slice(h) };
1035
+ }
1036
+ function tagInternalLinks(html, currentRelPath) {
1037
+ const currentDir = path5.dirname(currentRelPath);
1038
+ return rewriteTags(html, ANCHOR, (tag) => {
1039
+ const attr = firstAttr(tag, "href");
1040
+ if (!attr?.value) return null;
1041
+ const { target: rawTarget, query: rawQuery, hash: rawHash } = splitHref(attr.value);
1042
+ if (!rawTarget || /^https?:\/\//i.test(rawTarget) || rawTarget.startsWith("mailto:")) {
1043
+ return null;
1044
+ }
1045
+ if (!MD_EXT_RE.test(rawTarget)) return null;
1046
+ const target = unescapeHtml(rawTarget);
1047
+ const query = rawQuery === void 0 ? void 0 : unescapeHtml(rawQuery);
1048
+ const hash = rawHash === void 0 ? void 0 : unescapeHtml(rawHash);
1049
+ const resolved = path5.normalize(path5.join(currentDir, target));
1050
+ let decoded;
1051
+ try {
1052
+ decoded = decodeURIComponent(resolved);
1053
+ } catch {
1054
+ decoded = resolved;
1055
+ }
1056
+ const href = `?file=${encodeURIComponent(decoded)}${hash ?? ""}`;
1057
+ const internal = `${decoded}${query ?? ""}${hash ?? ""}`;
1058
+ return html.slice(tag.start, attr.start) + `href="${escapeAttr(href)}" data-internal-link="${escapeAttr(internal)}"` + html.slice(attr.end, tag.end);
1059
+ });
1060
+ }
1061
+ function rewriteImageSrc(html, currentRelPath) {
1062
+ const currentDir = path5.dirname(currentRelPath);
1063
+ return rewriteTags(html, IMG, (tag) => {
1064
+ const attr = firstAttr(tag, "src");
1065
+ const src = attr?.value;
1066
+ if (!attr || !src) return null;
1067
+ if (/^(https?:\/\/|data:|\/__asset\/)/i.test(src)) return null;
1068
+ const trimmed = src.replace(/^\/+/, "");
1069
+ const resolved = path5.normalize(path5.join(currentDir, trimmed));
1070
+ if (resolved.startsWith("..")) return null;
1071
+ return html.slice(tag.start, attr.start) + `src="/__asset/${resolved.replaceAll('"', "&quot;")}"` + html.slice(attr.end, tag.end);
1072
+ });
1073
+ }
1074
+
1075
+ // src/render/markdown.ts
1076
+ import MarkdownIt from "markdown-it";
1077
+ import anchor from "markdown-it-anchor";
1078
+ import taskLists from "markdown-it-task-lists";
1079
+
1080
+ // src/render/math.ts
1081
+ var BLOCK_RE = /^\$\$([\s\S]+?)\$\$$/;
1082
+ var INLINE_RE = /\$(?!\s)([^$\n]+?)(?<!\s)\$/g;
1083
+ function blockMathHtml(latex) {
1084
+ const encoded = encodeURIComponent(latex.trim());
1085
+ return `<div class="math-block" data-source="${encoded}"></div>
1086
+ `;
1087
+ }
1088
+ function inlineMathHtml(latex) {
1089
+ const encoded = encodeURIComponent(latex);
1090
+ return `<span class="math-inline" data-source="${encoded}"></span>`;
1091
+ }
1092
+ function convertBlockMath(tokens) {
1093
+ for (let i = 0; i < tokens.length - 2; i++) {
1094
+ if (tokens[i].type === "paragraph_open" && tokens[i + 1].type === "inline" && tokens[i + 2].type === "paragraph_close") {
1095
+ const inline = tokens[i + 1];
1096
+ const trimmed = inline.content.trim();
1097
+ const m = trimmed.match(BLOCK_RE);
1098
+ if (m) {
1099
+ const replacement = new tokens[i].constructor(
1100
+ "html_block",
1101
+ "",
1102
+ 0
1103
+ );
1104
+ replacement.content = blockMathHtml(m[1]);
1105
+ tokens.splice(i, 3, replacement);
1106
+ }
1107
+ }
1108
+ }
1109
+ }
1110
+ function convertInlineMath(tokens) {
1111
+ const TokenCtor = tokens[0]?.constructor;
1112
+ if (!TokenCtor) return;
1113
+ for (const top of tokens) {
1114
+ if (top.type !== "inline" || !top.children) continue;
1115
+ const out = [];
1116
+ for (const child of top.children) {
1117
+ if (child.type !== "text") {
1118
+ out.push(child);
1119
+ continue;
1120
+ }
1121
+ const text = child.content;
1122
+ INLINE_RE.lastIndex = 0;
1123
+ let last = 0;
1124
+ let m;
1125
+ let found = false;
1126
+ while ((m = INLINE_RE.exec(text)) !== null) {
1127
+ found = true;
1128
+ if (m.index > last) {
1129
+ const t = new TokenCtor("text", "", 0);
1130
+ t.content = text.slice(last, m.index);
1131
+ out.push(t);
1132
+ }
1133
+ const mathTok = new TokenCtor("html_inline", "", 0);
1134
+ mathTok.content = inlineMathHtml(m[1]);
1135
+ out.push(mathTok);
1136
+ last = m.index + m[0].length;
1137
+ }
1138
+ if (!found) {
1139
+ out.push(child);
1140
+ } else if (last < text.length) {
1141
+ const t = new TokenCtor("text", "", 0);
1142
+ t.content = text.slice(last);
1143
+ out.push(t);
1144
+ }
1145
+ }
1146
+ top.children = out;
1147
+ }
1148
+ }
1149
+ function mathPlugin(md2) {
1150
+ md2.core.ruler.after("inline", "mdview-math", (state) => {
1151
+ convertBlockMath(state.tokens);
1152
+ convertInlineMath(state.tokens);
1153
+ });
1154
+ }
1155
+
1156
+ // src/render/markdown.ts
1157
+ var md = new MarkdownIt({
1158
+ html: true,
1159
+ linkify: true,
1160
+ typographer: false,
1161
+ breaks: false,
1162
+ highlight: () => ""
1163
+ });
1164
+ md.linkify.set({ fuzzyLink: true });
1165
+ md.use(anchor, {
1166
+ slugify: (s) => s.toLowerCase().trim().replace(/[^\wÀ-ɏ\s-]/g, "").replace(/\s+/g, "-")
1167
+ });
1168
+ md.use(taskLists, { enabled: false, label: false });
1169
+ md.use(mathPlugin);
1170
+ async function renderMarkdown(source, bodyStartLine = 0, palette = DEFAULT_PALETTE) {
1171
+ const tokens = md.parse(source, {});
1172
+ for (const token of tokens) {
1173
+ if (token.type === "heading_open" && token.map) {
1174
+ token.attrSet("data-source-line", String(token.map[0] + bodyStartLine));
1175
+ }
1176
+ if (token.type === "html_block") {
1177
+ token.content = stripMetaAndBase(token.content);
1178
+ } else if (token.type === "inline" && token.children) {
1179
+ for (const child of token.children) {
1180
+ if (child.type === "html_inline") child.content = stripMetaAndBase(child.content);
1181
+ }
1182
+ } else if (token.type === "fence") {
1183
+ const lang = token.info.trim().split(/\s+/)[0] || "text";
1184
+ if (lang === "mermaid") {
1185
+ token.type = "html_block";
1186
+ token.content = `<div class="mermaid-block" data-source="${encodeURIComponent(token.content)}"></div>
1187
+ `;
1188
+ } else {
1189
+ const highlighted = await highlightCode(token.content, lang, palette);
1190
+ token.type = "html_block";
1191
+ token.content = highlighted + "\n";
1192
+ }
1193
+ }
1194
+ }
1195
+ const html = md.renderer.render(tokens, md.options, {});
1196
+ return { html, tokens };
1197
+ }
1198
+
1199
+ // src/render/outline.ts
1200
+ function extractOutline(tokens) {
1201
+ const flat = [];
1202
+ for (let i = 0; i < tokens.length; i++) {
1203
+ const t = tokens[i];
1204
+ if (t.type !== "heading_open") continue;
1205
+ const level = Number(t.tag.slice(1));
1206
+ const inline = tokens[i + 1];
1207
+ const id = (t.attrGet("id") ?? "").toString();
1208
+ const text = inline?.content?.trim() ?? "";
1209
+ if (!id || !text) continue;
1210
+ flat.push({ id, text, level, children: [] });
1211
+ }
1212
+ return nest(flat);
1213
+ }
1214
+ function nest(flat) {
1215
+ const root = [];
1216
+ const stack = [];
1217
+ for (const node of flat) {
1218
+ while (stack.length && stack[stack.length - 1].level >= node.level) {
1219
+ stack.pop();
1220
+ }
1221
+ if (stack.length === 0) {
1222
+ root.push(node);
1223
+ } else {
1224
+ stack[stack.length - 1].children.push(node);
1225
+ }
1226
+ stack.push(node);
1227
+ }
1228
+ return root;
1229
+ }
1230
+
1231
+ // src/server/routes/api-file.ts
1232
+ var MD_FILE_RE = /\.(md|markdown|mdx)$/i;
1233
+ var ASSET_URL_RE = /\/__asset\/([^"'<>\s]*)/g;
1234
+ var ENTITY2 = {
1235
+ "&amp;": "&",
1236
+ "&lt;": "<",
1237
+ "&gt;": ">",
1238
+ "&quot;": '"',
1239
+ "&#39;": "'"
1240
+ };
1241
+ function assetRefKey(rel) {
1242
+ return path6.posix.normalize(rel.replace(/^\/+/, ""));
1243
+ }
1244
+ function collectAssetRefs(html, roots, state) {
1245
+ const refs = /* @__PURE__ */ new Set();
1246
+ for (const m of html.matchAll(ASSET_URL_RE)) {
1247
+ const raw = (m[1] ?? "").replace(/&(?:amp|lt|gt|quot|#39);/g, (e) => ENTITY2[e] ?? e);
1248
+ const bare = raw.split(/[?#]/, 1)[0] ?? "";
1249
+ let decoded;
1250
+ try {
1251
+ decoded = decodeURIComponent(bare);
1252
+ } catch {
1253
+ continue;
1254
+ }
1255
+ const target = parseWorkspacePath(roots, decoded);
1256
+ if (target?.state === state) refs.add(assetRefKey(target.rel));
1257
+ }
1258
+ return refs;
1259
+ }
1260
+ async function renderDocument(raw, wsRel, palette) {
1261
+ const { data, body, bodyStartLine } = parseFrontmatter(raw);
1262
+ const { html: rawHtml, tokens } = await renderMarkdown(body, bodyStartLine, palette);
1263
+ const html = rewriteImageSrc(tagInternalLinks(rawHtml, wsRel), wsRel);
1264
+ const outline = extractOutline(tokens);
1265
+ const title = (typeof data?.title === "string" ? data.title : null) ?? outline[0]?.text ?? null;
1266
+ return { html, outline, frontmatter: data, title };
1267
+ }
1268
+ async function documentAssetRefs(roots, state) {
1269
+ if (state.assetRefs) return state.assetRefs;
1270
+ let refs = /* @__PURE__ */ new Set();
1271
+ try {
1272
+ const absPath = await resolveSafePath(state.absPath, state.root.filePath);
1273
+ const st = await stat2(absPath);
1274
+ if (st.isFile() && st.size <= MAX_MARKDOWN_BYTES && MD_FILE_RE.test(absPath)) {
1275
+ const raw = await readFile2(absPath, "utf8");
1276
+ const wsRel = toWorkspacePath(state.root.id, state.root.filePath);
1277
+ const { html } = await renderDocument(raw, wsRel, DEFAULT_PALETTE);
1278
+ refs = collectAssetRefs(html, roots, state);
1279
+ }
1280
+ } catch {
1281
+ }
1282
+ state.assetRefs = refs;
1283
+ return refs;
1284
+ }
1285
+ function registerApiFile(app, roots) {
1286
+ app.get(
1287
+ "/api/file",
1288
+ async (req, reply) => {
1289
+ const requested = req.query.path?.trim() ?? "";
1290
+ const rawPalette = req.query.palette?.trim() ?? "";
1291
+ const palette = PALETTES.includes(rawPalette) ? rawPalette : DEFAULT_PALETTE;
1292
+ const primary = roots[0];
1293
+ if (!primary) {
1294
+ return reply.code(404).send({ error: "No folders are open" });
1295
+ }
1296
+ let wsPath = requested;
1297
+ if (!wsPath) {
1298
+ if (primary.root.kind !== "file") {
1299
+ return reply.code(400).send({ error: "Missing ?path" });
1300
+ }
1301
+ wsPath = toWorkspacePath(primary.root.id, primary.root.filePath);
1302
+ }
1303
+ const resolved = parseWorkspacePath(roots, wsPath);
1304
+ if (!resolved) {
1305
+ return reply.code(404).send({ error: "File not found" });
1306
+ }
1307
+ const { state, rel } = resolved;
1308
+ const pinned = state.root.kind === "file";
1309
+ if (pinned && rel !== state.root.filePath) {
1310
+ return reply.code(404).send({ error: "File not found in single-file mode" });
1311
+ }
1312
+ if (!MD_FILE_RE.test(rel)) {
1313
+ return reply.code(400).send({ error: "Only markdown files are supported" });
1314
+ }
1315
+ let absPath;
1316
+ let realRoot;
1317
+ try {
1318
+ realRoot = await realpath3(state.absPath);
1319
+ absPath = await resolveSafePath(state.absPath, rel, { realRoot });
1320
+ } catch (err) {
1321
+ if (err instanceof OutsideRootError) {
1322
+ return reply.code(400).send({ error: err.message });
1323
+ }
1324
+ return reply.code(404).send({ error: "File not found" });
1325
+ }
1326
+ if (!MD_FILE_RE.test(absPath)) {
1327
+ return reply.code(400).send({ error: "Only markdown files are supported" });
1328
+ }
1329
+ let raw;
1330
+ let mtimeMs;
1331
+ try {
1332
+ const st = await stat2(absPath);
1333
+ if (!st.isFile()) {
1334
+ return reply.code(404).send({ error: "File not found" });
1335
+ }
1336
+ if (st.size > MAX_MARKDOWN_BYTES) {
1337
+ return reply.code(413).send({ error: "File is too large to render (over 16 MB)" });
1338
+ }
1339
+ raw = await readFile2(absPath, "utf8");
1340
+ mtimeMs = st.mtimeMs;
1341
+ } catch {
1342
+ return reply.code(404).send({ error: "File not found" });
1343
+ }
1344
+ const wsRel = toWorkspacePath(state.root.id, rel);
1345
+ const { html, outline, frontmatter, title } = await renderDocument(raw, wsRel, palette);
1346
+ if (pinned) state.assetRefs = collectAssetRefs(html, roots, state);
1347
+ const result = {
1348
+ relPath: wsRel,
1349
+ html,
1350
+ outline,
1351
+ frontmatter,
1352
+ title,
1353
+ lastModified: mtimeMs
1354
+ };
1355
+ return reply.send(result);
1356
+ }
1357
+ );
1358
+ }
1359
+
1360
+ // src/server/routes/api-asset.ts
1361
+ var MIME = {
1362
+ ".png": "image/png",
1363
+ ".jpg": "image/jpeg",
1364
+ ".jpeg": "image/jpeg",
1365
+ ".gif": "image/gif",
1366
+ ".webp": "image/webp",
1367
+ ".svg": "image/svg+xml",
1368
+ ".avif": "image/avif",
1369
+ ".bmp": "image/bmp",
1370
+ ".ico": "image/x-icon",
1371
+ ".mp4": "video/mp4",
1372
+ ".webm": "video/webm",
1373
+ ".mov": "video/quicktime",
1374
+ ".mp3": "audio/mpeg",
1375
+ ".wav": "audio/wav"
1376
+ };
1377
+ var ASSET_SECURITY_HEADERS = {
1378
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox; frame-ancestors 'none'",
1379
+ "x-content-type-options": "nosniff",
1380
+ "cross-origin-resource-policy": "same-origin"
1381
+ };
1382
+ function registerApiAsset(app, roots) {
1383
+ app.get("/__asset/*", async (req, reply) => {
1384
+ const wsPath = (req.params["*"] ?? "").replace(/^\/+/, "");
1385
+ const resolved = parseWorkspacePath(roots, wsPath);
1386
+ if (!resolved) {
1387
+ return reply.code(404).send({ error: "Asset not found" });
1388
+ }
1389
+ if (resolved.state.root.kind === "file") {
1390
+ const refs = await documentAssetRefs(roots, resolved.state);
1391
+ if (!refs.has(assetRefKey(resolved.rel))) {
1392
+ return reply.code(404).send({ error: "Asset not found" });
1393
+ }
1394
+ }
1395
+ let abs;
1396
+ let realRoot;
1397
+ try {
1398
+ realRoot = await realpath4(resolved.state.absPath);
1399
+ abs = await resolveSafePath(resolved.state.absPath, resolved.rel, { realRoot });
1400
+ } catch (err) {
1401
+ if (err instanceof OutsideRootError) {
1402
+ return reply.code(400).send({ error: err.message });
1403
+ }
1404
+ return reply.code(404).send({ error: "Asset not found" });
1405
+ }
1406
+ if (hasDotSegment(resolved.rel) || hasDotSegment(path7.relative(realRoot, abs))) {
1407
+ return reply.code(404).send({ error: "Asset not found" });
1408
+ }
1409
+ let st;
1410
+ try {
1411
+ st = await stat3(abs);
1412
+ } catch {
1413
+ return reply.code(404).send({ error: "Asset not found" });
1414
+ }
1415
+ if (!st.isFile()) {
1416
+ return reply.code(404).send({ error: "Not a file" });
1417
+ }
1418
+ const mime = MIME[path7.extname(abs).toLowerCase()];
1419
+ if (!mime || !MIME[path7.extname(resolved.rel).toLowerCase()]) {
1420
+ return reply.code(404).send({ error: "Asset type not allowed" });
1421
+ }
1422
+ reply.header("content-type", mime);
1423
+ reply.header("content-length", st.size);
1424
+ reply.header("cache-control", "no-cache");
1425
+ return reply.send(createReadStream(abs));
1426
+ });
1427
+ }
1428
+
1429
+ // src/server/routes/api-search.ts
1430
+ var MAX_QUERY_LEN = 200;
1431
+ function isTrue(v) {
1432
+ return v === "1" || v === "true";
1433
+ }
1434
+ function registerApiSearch(app, roots, opts = {}) {
1435
+ const cache = opts.cache ?? new ContentCache();
1436
+ app.get("/api/search", async (req, reply) => {
1437
+ const q = (req.query.q ?? "").trim();
1438
+ if (q.length === 0) {
1439
+ return reply.send({ query: "", results: [], truncated: false, timedOut: false });
1440
+ }
1441
+ if (q.length > MAX_QUERY_LEN) {
1442
+ return reply.code(400).send({ error: "Query too long" });
1443
+ }
1444
+ const searchable = roots.filter((r) => r.root.kind === "dir").map((r) => ({ absPath: r.absPath, id: r.root.id, ignore: r.ignoreSet }));
1445
+ const ac = new AbortController();
1446
+ const onClose = () => {
1447
+ if (!reply.raw.writableFinished) ac.abort();
1448
+ };
1449
+ reply.raw.once("close", onClose);
1450
+ try {
1451
+ const out = await grepFiles(searchable, q, {
1452
+ caseSensitive: isTrue(req.query.case),
1453
+ wholeWord: isTrue(req.query.word),
1454
+ regex: isTrue(req.query.regex),
1455
+ cache,
1456
+ signal: ac.signal,
1457
+ ...opts.regexBudgetMs !== void 0 ? { regexBudgetMs: opts.regexBudgetMs } : {}
1458
+ });
1459
+ return reply.send(out);
1460
+ } catch {
1461
+ return reply.code(500).send({ error: "Search failed" });
1462
+ } finally {
1463
+ reply.raw.off("close", onClose);
1464
+ }
1465
+ });
1466
+ }
1467
+
1468
+ // src/server/routes/api-tree.ts
1469
+ import path8 from "path";
1470
+ function applyOverrides(config, paletteOverride) {
1471
+ if (!paletteOverride) return config;
1472
+ return { ...config ?? {}, palette: paletteOverride };
1473
+ }
1474
+ async function rootChildren(state) {
1475
+ const { id, kind, filePath } = state.root;
1476
+ if (kind === "file") {
1477
+ return [
1478
+ {
1479
+ name: path8.basename(filePath),
1480
+ relPath: toWorkspacePath(id, filePath),
1481
+ type: "file",
1482
+ isMarkdown: true
1483
+ }
1484
+ ];
1485
+ }
1486
+ return walkFolder(state.absPath, { ignore: state.ignoreSet, relBase: id });
1487
+ }
1488
+ function registerApiTree(app, roots, paletteOverride) {
1489
+ app.get("/api/tree", async (_req, reply) => {
1490
+ const config = applyOverrides(roots[0]?.config ?? null, paletteOverride);
1491
+ if (roots.length === 1) {
1492
+ const only = roots[0];
1493
+ return reply.send({
1494
+ roots: [only.root],
1495
+ tree: await rootChildren(only),
1496
+ config
1497
+ });
1498
+ }
1499
+ const tree = [];
1500
+ for (const state of roots) {
1501
+ tree.push({
1502
+ name: state.root.name,
1503
+ relPath: state.root.id,
1504
+ type: "dir",
1505
+ children: await rootChildren(state)
1506
+ });
1507
+ }
1508
+ return reply.send({ roots: roots.map((r) => r.root), tree, config });
1509
+ });
1510
+ }
1511
+
1512
+ // src/server/routes/health.ts
1513
+ function registerHealth(app, identity, roots) {
1514
+ app.get(
1515
+ "/api/health",
1516
+ async (_req, reply) => reply.send({
1517
+ ok: true,
1518
+ pid: identity.pid,
1519
+ startedAt: identity.startedAt,
1520
+ version: identity.version,
1521
+ roots: roots.map((r) => r.root.id)
1522
+ })
1523
+ );
1524
+ }
1525
+
1526
+ // src/server/routes/sse.ts
1527
+ function registerSse(app, hub, hooks = {}) {
1528
+ let clients = 0;
1529
+ app.get("/api/watch", (req, reply) => {
1530
+ reply.raw.writeHead(200, {
1531
+ "Content-Type": "text/event-stream",
1532
+ "Cache-Control": "no-cache, no-transform",
1533
+ Connection: "keep-alive",
1534
+ "X-Accel-Buffering": "no",
1535
+ // Written straight to the socket, so the onSend hook that adds this to
1536
+ // every other response never sees the stream.
1537
+ "X-Content-Type-Options": "nosniff"
1538
+ });
1539
+ reply.raw.write(": connected\n\n");
1540
+ let closed = false;
1541
+ clients++;
1542
+ hooks.onClientsChanged?.(clients);
1543
+ const cleanup = () => {
1544
+ if (closed) return;
1545
+ closed = true;
1546
+ clearInterval(heartbeat);
1547
+ hub.off("event", send);
1548
+ clients--;
1549
+ hooks.onClientsChanged?.(clients);
1550
+ try {
1551
+ reply.raw.end();
1552
+ } catch {
1553
+ }
1554
+ };
1555
+ const send = (e) => {
1556
+ if (closed) return;
1557
+ try {
1558
+ reply.raw.write(`event: ${e.kind}
1559
+ `);
1560
+ reply.raw.write(`data: ${JSON.stringify(e)}
1561
+
1562
+ `);
1563
+ } catch {
1564
+ cleanup();
1565
+ }
1566
+ };
1567
+ const heartbeat = setInterval(() => {
1568
+ if (closed) return;
1569
+ try {
1570
+ reply.raw.write(": hb\n\n");
1571
+ } catch {
1572
+ cleanup();
1573
+ }
1574
+ }, 15e3);
1575
+ hub.on("event", send);
1576
+ req.raw.on("close", cleanup);
1577
+ req.raw.on("error", cleanup);
1578
+ reply.raw.on("error", cleanup);
1579
+ reply.raw.on("close", cleanup);
1580
+ });
1581
+ }
1582
+
1583
+ // src/server/watcher.ts
1584
+ import { EventEmitter } from "events";
1585
+ import path9 from "path";
1586
+ import chokidar from "chokidar";
1587
+ function createEventHub() {
1588
+ const emitter = new EventEmitter();
1589
+ emitter.setMaxListeners(0);
1590
+ return {
1591
+ on: (event, listener) => emitter.on(event, listener),
1592
+ off: (event, listener) => emitter.off(event, listener),
1593
+ emitSynthetic: (e) => emitter.emit("event", e),
1594
+ close: async () => {
1595
+ emitter.removeAllListeners();
1596
+ }
1597
+ };
1598
+ }
1599
+ function createWatcher(rootAbsPath, opts = {}) {
1600
+ const emitter = new EventEmitter();
1601
+ emitter.setMaxListeners(0);
1602
+ const ignore = opts.ignore ?? DEFAULT_IGNORED_DIRS2;
1603
+ const prefix = opts.prefix ?? "";
1604
+ const only = opts.only === void 0 ? void 0 : path9.join(rootAbsPath, opts.only);
1605
+ const scope = only === void 0 ? { ignored: (p) => isPathIgnored(p, rootAbsPath, ignore) } : { ignored: (p) => p !== rootAbsPath && p !== only, depth: 0 };
1606
+ const watcher = chokidar.watch(rootAbsPath, {
1607
+ ignoreInitial: true,
1608
+ ...scope,
1609
+ persistent: true,
1610
+ // chokidar follows links by default, so a repo holding `link -> /` would
1611
+ // have it walk and watch the whole disk. The server serves nothing through
1612
+ // a link that leaves the root anyway.
1613
+ followSymlinks: false,
1614
+ awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 30 }
1615
+ });
1616
+ watcher.on("error", (err) => {
1617
+ const e = err;
1618
+ if (e?.code === "EMFILE" || e?.code === "ENOSPC") {
1619
+ console.error(
1620
+ `[mdview] file watcher hit ${e.code} while watching ${rootAbsPath}
1621
+ Path: ${e.path ?? "(unknown)"}
1622
+ Likely cause: a heavy directory (build output, deps) wasn't ignored.
1623
+ Fix: add the directory name to "ignore" in ~/.config/mdview/config.json, e.g.
1624
+ { "ignore": ["deps", "_site"] }
1625
+ Built-in skips already cover ${[...DEFAULT_IGNORED_DIRS2].join(", ")}.`
1626
+ );
1627
+ } else {
1628
+ console.error(`[mdview] watcher error: ${e?.message ?? String(err)}`);
1629
+ }
1630
+ });
1631
+ function emit(kind, abs) {
1632
+ const rel = path9.relative(rootAbsPath, abs).split(path9.sep).join("/");
1633
+ if (!rel) return;
1634
+ const relPath = prefix ? `${prefix}/${rel}` : rel;
1635
+ emitter.emit("event", { kind, relPath });
1636
+ }
1637
+ watcher.on("change", (p) => emit("change", p));
1638
+ watcher.on("add", (p) => emit("add", p));
1639
+ watcher.on("unlink", (p) => emit("unlink", p));
1640
+ return {
1641
+ on: (event, listener) => emitter.on(event, listener),
1642
+ off: (event, listener) => emitter.off(event, listener),
1643
+ emitSynthetic: (e) => emitter.emit("event", e),
1644
+ close: () => watcher.close()
1645
+ };
1646
+ }
1647
+
1648
+ // src/server/index.ts
1649
+ function buildCspHtml(embedMode) {
1650
+ const directives = [
1651
+ "default-src 'self'",
1652
+ "script-src 'self'",
1653
+ // KaTeX and the diagram renderer set inline style attributes on their
1654
+ // output; the rest of the policy is strict.
1655
+ "style-src 'self' 'unsafe-inline'",
1656
+ "img-src 'self' data:",
1657
+ "font-src 'self' data:",
1658
+ "connect-src 'self'",
1659
+ // Nothing in the SPA frames or embeds documents. A same-origin frame of an
1660
+ // /__asset/* URL would otherwise run as a document of its own.
1661
+ "frame-src 'none'",
1662
+ "object-src 'none'",
1663
+ "base-uri 'none'",
1664
+ "form-action 'none'"
1665
+ ];
1666
+ if (!embedMode) directives.push("frame-ancestors 'none'");
1667
+ return directives.join("; ");
1668
+ }
1669
+ var EMBED_TOKEN_HEADER = "x-mdview-embed-token";
1670
+ function tokenMatcher(expected) {
1671
+ const want = Buffer.from(expected, "utf8");
1672
+ return (presented) => {
1673
+ if (typeof presented !== "string") return false;
1674
+ const got = Buffer.from(presented, "utf8");
1675
+ return got.length === want.length && timingSafeEqual(got, want);
1676
+ };
1677
+ }
1678
+ function queryToken(req) {
1679
+ return req.query?.token;
1680
+ }
1681
+ async function createServer(opts) {
1682
+ const app = Fastify({ logger: false });
1683
+ const embedToken = opts.embedToken;
1684
+ if (embedToken !== void 0 && embedToken.length === 0) {
1685
+ throw new Error("Embed mode needs a non-empty token");
1686
+ }
1687
+ const embedMode = embedToken !== void 0;
1688
+ const embedTokenMatches = embedMode ? tokenMatcher(embedToken) : () => true;
1689
+ const cspHtml = buildCspHtml(embedMode);
1690
+ app.addHook("onRequest", async (req, reply) => {
1691
+ if (!isAllowedHost(req.headers.host)) {
1692
+ return reply.code(403).type("text/plain; charset=utf-8").send("Forbidden");
1693
+ }
1694
+ if (embedMode && (req.url.startsWith("/api/") || req.routeOptions.url?.startsWith("/api/")) && !embedTokenMatches(req.headers[EMBED_TOKEN_HEADER]) && !embedTokenMatches(queryToken(req))) {
1695
+ return reply.code(403).type("text/plain; charset=utf-8").send("Forbidden");
1696
+ }
1697
+ });
1698
+ app.addHook("onSend", async (req, reply, payload) => {
1699
+ reply.header("x-content-type-options", "nosniff");
1700
+ const ct = String(reply.getHeader("content-type") ?? "");
1701
+ if (embedMode && ct.startsWith("text/html") && !embedTokenMatches(queryToken(req))) {
1702
+ payload?.destroy?.();
1703
+ for (const h of ["content-length", "etag", "last-modified", "accept-ranges"]) {
1704
+ reply.removeHeader(h);
1705
+ }
1706
+ reply.code(403);
1707
+ reply.header("content-type", "text/plain; charset=utf-8");
1708
+ reply.header("cache-control", "no-store");
1709
+ return "Forbidden";
1710
+ }
1711
+ if (req.routeOptions.url === "/__asset/*" || req.url.startsWith("/__asset/") || ct.startsWith("image/svg+xml")) {
1712
+ reply.headers(ASSET_SECURITY_HEADERS);
1713
+ return payload;
1714
+ }
1715
+ if (ct.startsWith("text/html")) {
1716
+ reply.header("content-security-policy", cspHtml);
1717
+ reply.header("referrer-policy", "no-referrer");
1718
+ }
1719
+ return payload;
1720
+ });
1721
+ const roots = [];
1722
+ const configWatchers = /* @__PURE__ */ new Map();
1723
+ const hub = createEventHub();
1724
+ const searchCache = new ContentCache();
1725
+ hub.on("event", (e) => {
1726
+ if (e.kind === "workspace") {
1727
+ searchCache.clear();
1728
+ return;
1729
+ }
1730
+ if (e.kind === "config") return;
1731
+ const target = parseWorkspacePath(roots, e.relPath);
1732
+ if (target) searchCache.invalidate(ContentCache.keyFor(target.state.absPath, target.rel));
1733
+ });
1734
+ async function openRoot(spec, id) {
1735
+ const config = await loadEffectiveConfig(spec.absPath);
1736
+ const ignoreSet = buildIgnoreSet(config?.ignore ?? []);
1737
+ const root = describeRoot(spec, id);
1738
+ const state = {
1739
+ root,
1740
+ absPath: spec.absPath,
1741
+ ignoreSet,
1742
+ config,
1743
+ watcher: createWatcher(spec.absPath, {
1744
+ ignore: ignoreSet,
1745
+ prefix: id,
1746
+ ...root.kind === "file" ? { only: root.filePath } : {}
1747
+ })
1748
+ };
1749
+ state.watcher.on("event", (e) => {
1750
+ if (root.kind === "file") state.assetRefs = void 0;
1751
+ hub.emitSynthetic(e);
1752
+ });
1753
+ const configWatcher = chokidar2.watch(path10.join(spec.absPath, CONFIG_FILENAME), {
1754
+ ignoreInitial: true,
1755
+ persistent: true,
1756
+ // A `.mdview.json` that links to a directory would otherwise be walked
1757
+ // and watched recursively.
1758
+ followSymlinks: false,
1759
+ awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 30 }
1760
+ });
1761
+ const reloadConfig = async () => {
1762
+ state.config = await loadEffectiveConfig(spec.absPath);
1763
+ const event = { kind: "config", relPath: `${id}/${CONFIG_FILENAME}` };
1764
+ hub.emitSynthetic(event);
1765
+ };
1766
+ configWatcher.on("add", () => void reloadConfig());
1767
+ configWatcher.on("change", () => void reloadConfig());
1768
+ configWatcher.on("unlink", () => void reloadConfig());
1769
+ configWatchers.set(id, configWatcher);
1770
+ return state;
1771
+ }
1772
+ async function closeRoot(state) {
1773
+ const cw = configWatchers.get(state.root.id);
1774
+ configWatchers.delete(state.root.id);
1775
+ await Promise.all([state.watcher.close(), cw?.close()]);
1776
+ }
1777
+ async function setRoots(specs) {
1778
+ const taken = /* @__PURE__ */ new Set();
1779
+ const wanted = specs.map((spec) => {
1780
+ const id = resolveRootId(spec, taken);
1781
+ taken.add(id);
1782
+ return { id, spec };
1783
+ });
1784
+ const keep = new Map(
1785
+ roots.filter((r) => wanted.some((w) => w.id === r.root.id && w.spec.absPath === r.absPath)).map((r) => [r.root.id, r])
1786
+ );
1787
+ const dropped = roots.filter((r) => !keep.has(r.root.id));
1788
+ const next = [];
1789
+ for (const { id, spec } of wanted) {
1790
+ const existing = keep.get(id);
1791
+ next.push(existing ?? await openRoot(spec, id));
1792
+ }
1793
+ roots.length = 0;
1794
+ roots.push(...next);
1795
+ await Promise.all(dropped.map(closeRoot));
1796
+ hub.emitSynthetic({ kind: "workspace" });
1797
+ }
1798
+ {
1799
+ const taken = /* @__PURE__ */ new Set();
1800
+ for (const spec of opts.roots) {
1801
+ const id = resolveRootId(spec, taken);
1802
+ taken.add(id);
1803
+ roots.push(await openRoot(spec, id));
1804
+ }
1805
+ }
1806
+ registerApiFile(app, roots);
1807
+ registerApiTree(app, roots, opts.paletteOverride);
1808
+ registerApiAsset(app, roots);
1809
+ registerApiSearch(app, roots, { cache: searchCache });
1810
+ registerSse(app, hub, { ...opts.onSseClientsChanged ? { onClientsChanged: opts.onSseClientsChanged } : {} });
1811
+ if (opts.identity) registerHealth(app, opts.identity, roots);
1812
+ await app.register(import("@fastify/static"), {
1813
+ root: opts.clientDir,
1814
+ prefix: "/"
1815
+ });
1816
+ app.setNotFoundHandler(async (req, reply) => {
1817
+ if (req.url.startsWith("/api/") || req.url.startsWith("/__asset/")) {
1818
+ reply.code(404).send({ error: "Not found" });
1819
+ return;
1820
+ }
1821
+ const qIndex = req.url.indexOf("?");
1822
+ const rawPath = qIndex >= 0 ? req.url.slice(0, qIndex) : req.url;
1823
+ if (/[\r\n]/.test(rawPath)) {
1824
+ reply.code(404).send({ error: "Not found" });
1825
+ return;
1826
+ }
1827
+ if (/\.(md|markdown|mdx)$/i.test(rawPath)) {
1828
+ const relPath = rawPath.replace(/^\/+/, "");
1829
+ const params = new URLSearchParams({ file: relPath });
1830
+ for (const key of ["embed", "token"]) {
1831
+ const value = req.query[key];
1832
+ if (typeof value === "string") params.set(key, value);
1833
+ }
1834
+ const location = `/?${params.toString()}`;
1835
+ reply.code(302).header("Location", location).send();
1836
+ return;
1837
+ }
1838
+ return reply.sendFile("index.html");
1839
+ });
1840
+ app.addHook("onClose", async () => {
1841
+ await Promise.all([
1842
+ ...roots.map((r) => r.watcher.close()),
1843
+ ...[...configWatchers.values()].map((w) => w.close())
1844
+ ]);
1845
+ await hub.close();
1846
+ await closeRegexWorkers();
1847
+ });
1848
+ return { app, setRoots };
1849
+ }
1850
+ export {
1851
+ EMBED_TOKEN_HEADER,
1852
+ createServer
1853
+ };
1854
+ //# sourceMappingURL=server-KALIU7HM.mjs.map