brane 0.1.1 → 0.3.1

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 (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +133 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +360 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1101 -0
  37. package/dist/commands/token.js +154 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6353 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +643 -0
  58. package/dist/db/tokens.js +624 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. package/tsconfig.json +0 -16
@@ -0,0 +1,72 @@
1
+ /** Where the app and the HTML export have to agree about lists.
2
+ *
3
+ * The export never calls into here — micromark decides looseness itself, on
4
+ * the way to HTML, and emits `<li><p>…</p></li>` for a loose item against a
5
+ * bare `<li>…</li>` for a tight one. The app has no such luxury: it renders
6
+ * from mdast, where the flags survive but the decision does not, so it has to
7
+ * make the same call the HTML side already made. This is that call, written
8
+ * once and pinned by `src/test/markdown-lists.test.ts` *against micromark's
9
+ * own output* — the only way to know the two still agree.
10
+ */
11
+ /** CommonMark: an item is loose when it holds more than one block, which is
12
+ * what `spread` records. The flag is authoritative when present; the child
13
+ * count is the fallback for a tree built by hand rather than by a parser. */
14
+ function itemIsLoose(item) {
15
+ if (!item)
16
+ return false;
17
+ if (typeof item.spread === "boolean")
18
+ return item.spread;
19
+ return (item.children?.length ?? 0) > 1;
20
+ }
21
+ /** Whether a list renders loose — paragraph margins between items — or tight.
22
+ *
23
+ * Looseness is a property of the **list**, never of the item: CommonMark says
24
+ * a list is loose if *any* of its items are separated by a blank line, and
25
+ * every item then renders loose. So a two-item list whose first item is spread
26
+ * gives its second item paragraph margins too, and reading `item.spread`
27
+ * alone — the obvious shortcut — would space the two differently. */
28
+ export function listIsLoose(list) {
29
+ if (!list)
30
+ return false;
31
+ if (list.spread)
32
+ return true;
33
+ return (list.children ?? []).some(itemIsLoose);
34
+ }
35
+ /** Where a loose list item's top margin has to live.
36
+ *
37
+ * A list item row is two flex siblings — the marker `<Text>` and a `<View>`
38
+ * holding the content — top-aligned. So a margin on the *first paragraph*
39
+ * indents only the content column, and the marker stays parked at the top of
40
+ * the row: a `1.` floating ~8px above the line it numbers, reading as a
41
+ * superscript. Tight lists never showed it because their paragraph margins are
42
+ * zeroed anyway; loose lists show it on every item, because there the spacing
43
+ * is deliberate.
44
+ *
45
+ * CSS has no such failure mode — the `::marker` is aligned to the first line
46
+ * box of the item's principal block, so a `<p>` margin moves the marker with
47
+ * the text — which is why the export renders these correctly and the app does
48
+ * not. The fix is to put the margin somewhere it moves *both* halves of the
49
+ * row: on the row itself.
50
+ *
51
+ * The pair below has to be read together, since the whole point is that they
52
+ * add up to what the paragraph margins used to contribute on their own. */
53
+ /** The margin override for a paragraph, by where it sits in a list. `null`
54
+ * leaves the stylesheet's own `paragraph` margins alone. */
55
+ export function listParagraphOverride(where) {
56
+ if (!where.inListItem)
57
+ return null;
58
+ // A tight list runs its items together: no paragraph margins at all.
59
+ if (!where.loose)
60
+ return { marginVertical: 0 };
61
+ // Loose: the first block's top margin has moved to the row. Later blocks in
62
+ // the same item keep theirs — they sit below the marker, not beside it.
63
+ return where.firstBlock ? { marginTop: 0 } : null;
64
+ }
65
+ /** The row's own top margin — exactly the margin `listParagraphOverride` just
66
+ * took off the first paragraph, so the text lands where it always did and only
67
+ * the marker moves. `paragraphMargin` is the stylesheet's `paragraph`
68
+ * `marginVertical`, passed in rather than duplicated here. */
69
+ export function listItemRowMarginTop(loose, paragraphMargin) {
70
+ return loose ? paragraphMargin : 0;
71
+ }
72
+ //# sourceMappingURL=markdown-lists.js.map
@@ -0,0 +1,485 @@
1
+ /** LaTeX math, shared by the web app and the daemon's HTML export.
2
+ *
3
+ * Both sides hand the *same* LaTeX to the *same* `katex.renderToString`, so an
4
+ * exported narrative isn't an approximation of the live view — it is that view
5
+ * with the renderer run ahead of time. That only holds if the decision of
6
+ * *whether* a `$…$` run is math is shared too — and how a source is normalized
7
+ * before parsing — which is what `prepareMathMarkdown` below is for. If the two
8
+ * sides disagreed, exports would quietly stop matching what the operator saw.
9
+ */
10
+ import katex from "katex";
11
+ /** Delimiters, for the literal fallbacks below. */
12
+ const TEXT_DELIM = "$";
13
+ const FLOW_DELIM = "$$";
14
+ /** A blank line: where a paragraph, and so any code span inside it, ends. */
15
+ const BLANK_LINE = /\n[ \t]*\r?\n/g;
16
+ /** What may precede a `$$` that still opens a math flow fence: indentation,
17
+ * blockquote and list markers — the container prefixes micromark strips. */
18
+ const FLOW_PREFIX = /^(?:[ \t]*(?:>|[-*+]|\d+[.)]))*[ \t]*$/;
19
+ function escapeHtml(s) {
20
+ return s
21
+ .replace(/&/g, "&amp;")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;")
24
+ .replace(/"/g, "&quot;");
25
+ }
26
+ /** Render one run of LaTeX to standalone HTML.
27
+ *
28
+ * `htmlAndMathml` is KaTeX's default and worth its extra bytes: the visible
29
+ * layout is the span tree, and the MathML riding alongside it is what screen
30
+ * readers actually read.
31
+ *
32
+ * Anything KaTeX can't parse falls back to the literal source with its
33
+ * delimiters restored, rather than to KaTeX's red error markup. A run that
34
+ * reached here and failed was almost certainly never math — some prose that
35
+ * slipped past the guard — so showing it as the author typed it is both the
36
+ * honest rendering and the unobtrusive one.
37
+ */
38
+ export function renderMathHtml(value, displayMode, onError) {
39
+ try {
40
+ return katex.renderToString(value, {
41
+ displayMode,
42
+ output: "htmlAndMathml",
43
+ throwOnError: true,
44
+ // Warn-don't-fail on LaTeX-incompatible input: agents write Unicode
45
+ // straight into math mode (`$σ → 0$`) and KaTeX renders it fine.
46
+ strict: false,
47
+ trust: false,
48
+ // `\*` is not a TeX control sequence; it's a model escaping an asterisk
49
+ // so markdown won't read it as emphasis, then leaving the escape inside
50
+ // the math where it no longer means anything. It accounts for half the
51
+ // otherwise-unparseable equations in the absorbed archive.
52
+ macros: { "\\*": "*" },
53
+ });
54
+ }
55
+ catch (err) {
56
+ const d = displayMode ? FLOW_DELIM : TEXT_DELIM;
57
+ const fallback = d + value + d;
58
+ // The fallback *is* the diagnostic. Whether a parse failure matters depends
59
+ // entirely on whether the literal source still reads — `$E = mc^2$` does, a
60
+ // `pmatrix` does not — so a caller collecting these gets the exact text the
61
+ // operator will see, not just the parser's complaint about it.
62
+ onError?.(katexMessage(err), fallback);
63
+ return escapeHtml(fallback);
64
+ }
65
+ }
66
+ /** KaTeX's own complaint, minus the prefix it repeats on every one, and minus
67
+ * the combining underline it draws through the offending characters — which
68
+ * reads as an underline in a browser and as mojibake anywhere a plain string
69
+ * ends up. The message still names the position in words. */
70
+ function katexMessage(err) {
71
+ const raw = err instanceof Error ? err.message : String(err);
72
+ return raw.replace(/^KaTeX parse error:\s*/, "").replace(/\u0332/g, "");
73
+ }
74
+ /** Is this `$…$` run plausibly math?
75
+ *
76
+ * `content` is the text between the delimiters, `after` the single character
77
+ * that follows the closing one (empty at end of input).
78
+ */
79
+ export function isMathContent(content, after) {
80
+ return mathContentRejection(content, after) === null;
81
+ }
82
+ /** Why this run is *not* math, phrased for the author, or null if it is.
83
+ *
84
+ * The first three rules are Pandoc's, and they carry most of the weight — in
85
+ * particular "no digit after the closer", which is what saves `$10k–$500k`
86
+ * from being read as math over `10k–`. The last two are ours, for the two
87
+ * things agents write that Pandoc never had to worry about: shell parameter
88
+ * expansion (`${curcontext%:*:*}`) and quoted snippets pasted outside a code
89
+ * span (`awk '{print $1, $9}'`). Real math contains no raw double quote.
90
+ *
91
+ * The decision and the *explanation* of the decision live in one function on
92
+ * purpose: an author told "a space sits just inside the delimiters" can fix
93
+ * it, where one told only "not math" is left guessing which guard fired — and
94
+ * a second copy of these rules written to phrase them would drift from the
95
+ * copy that enforces them.
96
+ */
97
+ export function mathContentRejection(content, after) {
98
+ if (content.length === 0)
99
+ return "the delimiters enclose nothing";
100
+ if (/^\s|\s$/.test(content))
101
+ return "a space sits just inside the delimiters";
102
+ if (/^[0-9]/.test(after))
103
+ return "a digit follows the closing `$`, which reads as currency";
104
+ if (content.startsWith("{"))
105
+ return "it opens with `{`, which reads as shell parameter expansion";
106
+ if (content.includes('"'))
107
+ return "it contains a double quote, which math never does";
108
+ return null;
109
+ }
110
+ /** Index of the `$` that closes a text-math run opened just before `from`, or
111
+ * -1. Mirrors what micromark-extension-math will accept: escapes don't close,
112
+ * and a run doesn't survive a blank line. */
113
+ function findTextMathClose(md, from) {
114
+ for (let j = from; j < md.length; j++) {
115
+ const c = md[j];
116
+ if (c === "\\") {
117
+ j++;
118
+ continue;
119
+ }
120
+ if (c === "\n" && md[j + 1] === "\n")
121
+ return -1;
122
+ if (c === "$")
123
+ return j;
124
+ }
125
+ return -1;
126
+ }
127
+ /** A fence opening at `i` (a line start): up to three spaces, then three or
128
+ * more backticks or tildes. Returns the fence run, or null. */
129
+ function matchFenceOpen(md, i) {
130
+ const line = md.slice(i, md.indexOf("\n", i) === -1 ? undefined : md.indexOf("\n", i));
131
+ const m = line.match(/^ {0,3}(`{3,}|~{3,})/);
132
+ return m ? m[1] : null;
133
+ }
134
+ /** End index (exclusive) of the fenced block opened at `i` with `fence` —
135
+ * the closing fence line, or end of input if it never closes. */
136
+ function findFenceClose(md, i, fence) {
137
+ const marker = fence[0];
138
+ let j = md.indexOf("\n", i);
139
+ if (j === -1)
140
+ return md.length;
141
+ j += 1;
142
+ while (j < md.length) {
143
+ const lineEnd = md.indexOf("\n", j);
144
+ const line = md.slice(j, lineEnd === -1 ? md.length : lineEnd);
145
+ if (new RegExp(`^ {0,3}${marker === "`" ? "`" : "~"}{${fence.length},}\\s*$`).test(line)) {
146
+ return lineEnd === -1 ? md.length : lineEnd + 1;
147
+ }
148
+ if (lineEnd === -1)
149
+ return md.length;
150
+ j = lineEnd + 1;
151
+ }
152
+ return md.length;
153
+ }
154
+ /** Did the block `findFenceClose` returned actually end at a closing fence?
155
+ * It returns end-of-input both for a fence that never closes and for one that
156
+ * closes on the final line, so the block itself is what tells them apart. An
157
+ * unclosed fence is worth reporting because its damage isn't local: every
158
+ * paragraph after it becomes code. */
159
+ function hasClosingFence(block, fence) {
160
+ return new RegExp(`\\n {0,3}[${fence[0]}]{${fence.length},}[ \\t]*\\r?\\n?$`).test(block);
161
+ }
162
+ /** `\(…\)` or `\[…\]` opening at `i`, or null.
163
+ *
164
+ * TeX's other delimiter pair, and the one OpenAI-family models reach for by
165
+ * default — markdown would otherwise eat both halves as escaped brackets and
166
+ * leave the LaTeX as prose. Unlike `$`, these are unambiguous: nothing else in
167
+ * ordinary writing looks like them, so they need no guard. */
168
+ function matchTexDelimiter(md, i) {
169
+ const open = md.slice(i, i + 2);
170
+ const display = open === "\\[";
171
+ if (!display && open !== "\\(")
172
+ return null;
173
+ const close = display ? "\\]" : "\\)";
174
+ const at = md.indexOf(close, i + 2);
175
+ // Past this point the author has plainly asked for math, so a failure comes
176
+ // back with its reason rather than as a bare null: the caller renders it as
177
+ // prose either way, but only a reason can be reported to whoever wrote it.
178
+ if (at === -1)
179
+ return { ok: false, reason: `\`${open}\` is never closed by \`${close}\`` };
180
+ const value = md.slice(i + 2, at);
181
+ if (!value.trim())
182
+ return { ok: false, reason: `\`${open}${close}\` encloses nothing` };
183
+ // A `$` inside would collide with the delimiters we rewrite to. Vanishingly
184
+ // rare in math; leaving such a run alone beats mangling it.
185
+ if (value.includes("$"))
186
+ return { ok: false, reason: "it contains a `$`, which would collide with the `$` delimiters this rewrites to" };
187
+ // An inline run that swallows a blank line is a runaway, not an equation.
188
+ if (!display && /\n\s*\n/.test(value))
189
+ return { ok: false, reason: "an inline run spanning a blank line is a runaway, not an equation" };
190
+ return { ok: true, value, display, end: at + 2 };
191
+ }
192
+ /** A ```math fence, rewritten to `$$…$$`.
193
+ *
194
+ * GitHub renders that fence as display math, and models that have learned its
195
+ * flavour of markdown emit it. A ```latex fence is left alone: that one is
196
+ * almost always showing the source rather than asking for it to be set. */
197
+ function matchMathFence(block, fence) {
198
+ const firstLineEnd = block.indexOf("\n");
199
+ if (firstLineEnd === -1)
200
+ return null;
201
+ const info = block.slice(0, firstLineEnd).trimStart().slice(fence.length).trim();
202
+ if (info.toLowerCase() !== "math")
203
+ return null;
204
+ const marker = fence[0] === "`" ? "`" : "~";
205
+ const body = block
206
+ .slice(firstLineEnd + 1)
207
+ .replace(new RegExp(`\\n? {0,3}${marker}{${fence.length},}[ \\t]*\\n?$`), "");
208
+ if (!body.trim())
209
+ return null;
210
+ return `${FLOW_DELIM}\n${body.trim()}\n${FLOW_DELIM}\n`;
211
+ }
212
+ /** Normalize a markdown source so both renderers read its math the same way.
213
+ *
214
+ * Two passes in one scan, because both have to respect the same code regions.
215
+ *
216
+ * **Escaping dollars that aren't delimiters.**
217
+ * This runs *before* the markdown is parsed, which is what lets one function
218
+ * serve both renderers: micromark-extension-math is more permissive than
219
+ * Pandoc (it will happily eat `$10k–$500k`), and it exposes no hook for
220
+ * vetoing a run once tokenized. Escaping up front sidesteps that entirely —
221
+ * and `\$` renders as a plain `$`, so the reader sees exactly what was typed.
222
+ *
223
+ * Only the *opening* delimiter is escaped; scanning then resumes inside the
224
+ * rejected run, so its closing `$` gets its own fair hearing as the opener of
225
+ * the next candidate. That's what makes a chain like `$5 and $10 and $x$`
226
+ * come out right.
227
+ *
228
+ * Regions markdown already protects are skipped rather than guarded: fenced
229
+ * blocks, code spans, backslash escapes, `$$…$$` display math (nobody writes
230
+ * a doubled dollar for currency), and lines indented far enough to be code.
231
+ * Skipping is the safe direction — the worst case is micromark's permissive
232
+ * default, where escaping inside a code span would print a literal backslash.
233
+ *
234
+ * **Promoting one-line display math.** micromark-extension-math only treats
235
+ * `$$` as a display *fence*, so `$$x$$` alone on a line parses as inline math
236
+ * with doubled delimiters — centered-block intent, inline rendering, and no
237
+ * `\displaystyle` sizing. That one-line form is how agents actually write
238
+ * display math, so a paragraph that is nothing but `$$…$$` is rewritten into
239
+ * the fenced form the extension wants.
240
+ */
241
+ export function prepareMathMarkdown(md, onIssue) {
242
+ let out = "";
243
+ let i = 0;
244
+ // Indented code is a *block*, not a line: only its first line follows a blank
245
+ // one, so a per-line test lets everything after the first line back in. That
246
+ // is how ASCII art drawn in dollar signs got its `$` escaped and its `$$$$`
247
+ // runs read as empty display math.
248
+ let inIndentedCode = false;
249
+ while (i < md.length) {
250
+ const atLineStart = i === 0 || md[i - 1] === "\n";
251
+ if (atLineStart) {
252
+ const lineEnd = md.indexOf("\n", i);
253
+ const line = md.slice(i, lineEnd === -1 ? md.length : lineEnd);
254
+ const blank = line.trim() === "";
255
+ const indented = /^(?: {4}|\t)/.test(line);
256
+ const prevLineStart = i === 0 ? 0 : md.lastIndexOf("\n", i - 2) + 1;
257
+ const afterBlank = i === 0 || md.slice(prevLineStart, i - 1).trim() === "";
258
+ if (inIndentedCode ? blank || indented : indented && afterBlank) {
259
+ inIndentedCode = true;
260
+ const end = lineEnd === -1 ? md.length : lineEnd + 1;
261
+ out += md.slice(i, end);
262
+ i = end;
263
+ continue;
264
+ }
265
+ inIndentedCode = false;
266
+ const fence = matchFenceOpen(md, i);
267
+ if (fence) {
268
+ const end = findFenceClose(md, i, fence);
269
+ const block = md.slice(i, end);
270
+ if (onIssue && !hasClosingFence(block, fence))
271
+ onIssue({ kind: "fence_unclosed", fence, index: i });
272
+ out += matchMathFence(block, fence) ?? block;
273
+ i = end;
274
+ continue;
275
+ }
276
+ }
277
+ const ch = md[i];
278
+ if (ch === "\\") {
279
+ const tex = matchTexDelimiter(md, i);
280
+ if (tex?.ok) {
281
+ // Rewritten into the `$` forms rather than tokenized separately, so
282
+ // everything downstream — both renderers included — sees one syntax.
283
+ if (!tex.display) {
284
+ // Trimmed, because whitespace is insignificant inside TeX delimiters
285
+ // but decisive inside dollars: `\( i \)` is idiomatic and common,
286
+ // and `$ i $` is exactly what the guard above throws out.
287
+ out += `${TEXT_DELIM}${tex.value.trim()}${TEXT_DELIM}`;
288
+ }
289
+ else {
290
+ const lineStart = md.lastIndexOf("\n", i) + 1;
291
+ const prefix = md.slice(lineStart, i);
292
+ const lineEnd = md.indexOf("\n", tex.end);
293
+ const trailing = md.slice(tex.end, lineEnd === -1 ? md.length : lineEnd);
294
+ // An opener that ends its own line: every interior line already
295
+ // carries whatever prefix its block needs — the `> ` of a blockquote,
296
+ // a list's indent — so swap the delimiters where they stand and touch
297
+ // nothing else. Re-trimming and re-wrapping here is what dropped
298
+ // blockquoted equations into a `$$` fence's info string, where
299
+ // micromark discards them without a word.
300
+ if (/^[ \t>]*$/.test(prefix) && /^[ \t]*\r?\n/.test(tex.value)) {
301
+ out += `${FLOW_DELIM}${tex.value}${FLOW_DELIM}`;
302
+ }
303
+ else if (/^[ \t]*$/.test(prefix) && trailing.trim() === "") {
304
+ // The prefix rides along on the lines we invent. Without it a
305
+ // `\[…\]` sitting three spaces deep in a numbered list opens its
306
+ // fence inside the item and closes it at column 0, which ends the
307
+ // list and leaves the fence hanging.
308
+ out += `${FLOW_DELIM}\n${prefix}${tex.value.trim()}\n${prefix}${FLOW_DELIM}`;
309
+ }
310
+ else {
311
+ // Mid-sentence, or under a prefix a fence can't survive. Inline
312
+ // display math renders in place and can't corrupt the block.
313
+ out += `${FLOW_DELIM}${tex.value.trim()}${FLOW_DELIM}`;
314
+ }
315
+ }
316
+ i = tex.end;
317
+ continue;
318
+ }
319
+ if (tex)
320
+ onIssue?.({ kind: "tex_unmatched", open: md.slice(i, i + 2), reason: tex.reason, index: i });
321
+ out += md.slice(i, i + 2);
322
+ i += 2;
323
+ continue;
324
+ }
325
+ if (ch === "`") {
326
+ let n = 0;
327
+ while (md[i + n] === "`")
328
+ n++;
329
+ const run = "`".repeat(n);
330
+ // A code span closes on a backtick run of exactly the same length — and
331
+ // never past the end of its paragraph. A stray ``` in prose once hunted
332
+ // for its partner across ninety lines and swallowed a real fence opener
333
+ // on the way, leaving every dollar inside the region unguarded.
334
+ BLANK_LINE.lastIndex = i;
335
+ const paragraphEnd = BLANK_LINE.exec(md)?.index ?? md.length;
336
+ let j = i + n;
337
+ let close = -1;
338
+ while (j < paragraphEnd) {
339
+ const next = md.indexOf(run, j);
340
+ if (next === -1 || next >= paragraphEnd)
341
+ break;
342
+ let k = 0;
343
+ while (md[next + run.length + k] === "`")
344
+ k++;
345
+ if (k === 0 && md[next - 1] !== "`") {
346
+ close = next;
347
+ break;
348
+ }
349
+ j = next + run.length + k;
350
+ }
351
+ const end = close === -1 ? i + n : close + n;
352
+ out += md.slice(i, end);
353
+ i = end;
354
+ continue;
355
+ }
356
+ if (ch === "$") {
357
+ if (md.startsWith(FLOW_DELIM, i)) {
358
+ // `$$$` is how people write "expensive", and the extension agrees: a
359
+ // run of three or more is literal unless a run of the same length
360
+ // closes it, which nobody writes. Leave it be.
361
+ if (md[i + 2] === "$") {
362
+ let n = 2;
363
+ while (md[i + n] === "$")
364
+ n++;
365
+ out += md.slice(i, i + n);
366
+ i += n;
367
+ continue;
368
+ }
369
+ const close = md.indexOf(FLOW_DELIM, i + 2);
370
+ const end = close === -1 ? i + 2 : close + 2;
371
+ // An unclosed `$$` only swallows what follows when it opens a line
372
+ // (a math *flow* fence, possibly under a list or quote prefix); in
373
+ // the middle of a sentence it is two literal dollars.
374
+ if (close === -1 && FLOW_PREFIX.test(md.slice(md.lastIndexOf("\n", i) + 1, i))) {
375
+ onIssue?.({ kind: "display_unclosed", index: i });
376
+ }
377
+ if (close !== -1) {
378
+ const content = md.slice(i + 2, close);
379
+ const lineStart = md.lastIndexOf("\n", i) + 1;
380
+ const lineEnd = md.indexOf("\n", end);
381
+ const trailing = md.slice(end, lineEnd === -1 ? md.length : lineEnd);
382
+ // `$$x$$` with nothing else on its line: the author meant a display
383
+ // block, so give the extension the fence it recognizes.
384
+ if (!content.includes("\n") &&
385
+ content.trim() !== "" &&
386
+ md.slice(lineStart, i).trim() === "" &&
387
+ trailing.trim() === "") {
388
+ out += `${FLOW_DELIM}\n${content.trim()}\n${FLOW_DELIM}`;
389
+ i = end;
390
+ continue;
391
+ }
392
+ }
393
+ out += md.slice(i, end);
394
+ i = end;
395
+ continue;
396
+ }
397
+ const close = findTextMathClose(md, i + 1);
398
+ if (close !== -1) {
399
+ const content = md.slice(i + 1, close);
400
+ if (isMathContent(content, md[close + 1] ?? "")) {
401
+ out += md.slice(i, close + 1);
402
+ i = close + 1;
403
+ continue;
404
+ }
405
+ onIssue?.({ kind: "dollar_escaped", content, after: md[close + 1] ?? "", index: i });
406
+ out += "\\$";
407
+ i += 1;
408
+ continue;
409
+ }
410
+ }
411
+ out += ch;
412
+ i += 1;
413
+ }
414
+ return out;
415
+ }
416
+ /** The CSS that makes a display-math block behave, shared by the app and the
417
+ * export for the same reason `renderMathHtml` is: a downloaded narrative has
418
+ * to be the live view, not a lookalike of it. The export writes this into its
419
+ * one `<style>`; the app injects it from `Markdown.tsx`. Neither may keep a
420
+ * private copy.
421
+ *
422
+ * KaTeX gives display math no way to wrap — `.katex-display > .katex` is
423
+ * `white-space: nowrap`, and TeX has no line-breaking for display mode at all
424
+ * — so an equation wider than its container must scroll. `overflow-x: auto`
425
+ * alone does not achieve that, which is the bug this fixes:
426
+ *
427
+ * **`min-width: max-content` is the load-bearing rule.** Left alone, that
428
+ * inner block is exactly the container's width with its content overflowing
429
+ * it *centered* — and centered overflow spills both ways, with the left spill
430
+ * unreachable, since `scrollWidth` doesn't count it. A wide equation is then
431
+ * clipped at *both* ends with nothing to scroll to, and — worse than being
432
+ * cut off — it looks whole. A truncated BBP formula doesn't equal π, but it
433
+ * renders as though it does (`claude-yklrrp1n:16`). Sized to its own content
434
+ * the block overflows rightward only, which is the direction a scroller can
435
+ * actually reach. Below the container's width nothing changes: `width: auto`
436
+ * still wins, so short equations stay centered.
437
+ *
438
+ * **The mask is the overflow hint**, and it dissolves the equation itself
439
+ * rather than washing anything over it: a mask makes the element's own paint
440
+ * transparent, so what comes through at a faded edge is whatever the block
441
+ * sits on — the bubble, in its own color, without this code having to know
442
+ * what that color is.
443
+ *
444
+ * What the mask cannot do on its own is know *when* to fade, and an edge that
445
+ * fades with nothing beyond it lies as badly as one that clips. So the two
446
+ * fade widths are registered custom properties animated on a **scroll
447
+ * timeline over the block itself** — which is a measurement the browser is
448
+ * already making. `animation-range` puts each animation in the first and last
449
+ * 32px of travel, and `both` holds the endpoints in between, so the left edge
450
+ * fades in as soon as you move off the start and the right edge is *crisp
451
+ * once you reach the end*. A block with nothing to scroll has no timeline at
452
+ * all — inactive, so the initial `0px` widths stand and no edge fades.
453
+ *
454
+ * No script, which is what lets the export have this too. Where scroll-driven
455
+ * animations aren't supported the base style is what remains: a scroller with
456
+ * both edges sharp. The equation still scrolls; it just stops advertising it.
457
+ */
458
+ export const MATH_BLOCK_CSS = `
459
+ @property --math-fade-start { syntax: "<length>"; inherits: false; initial-value: 0px; }
460
+ @property --math-fade-end { syntax: "<length>"; inherits: false; initial-value: 0px; }
461
+ @keyframes brane-math-fade-start { from { --math-fade-start: 0px; } to { --math-fade-start: 28px; } }
462
+ @keyframes brane-math-fade-end { from { --math-fade-end: 28px; } to { --math-fade-end: 0px; } }
463
+ .math-display {
464
+ overflow-x: auto;
465
+ overflow-y: hidden;
466
+ padding: 2px 0;
467
+ /* A horizontal pan that runs out of equation shouldn't turn into the
468
+ browser's back-navigation swipe. (The app's tab-swipe is a touch handler
469
+ rather than scroll chaining, so that one needs the JS pan guard too.) */
470
+ overscroll-behavior-x: contain;
471
+ mask-image: linear-gradient(
472
+ to right,
473
+ rgba(0, 0, 0, 0) 0,
474
+ rgba(0, 0, 0, 1) var(--math-fade-start),
475
+ rgba(0, 0, 0, 1) calc(100% - var(--math-fade-end)),
476
+ rgba(0, 0, 0, 0) 100%
477
+ );
478
+ animation: brane-math-fade-start linear both, brane-math-fade-end linear both;
479
+ animation-timeline: scroll(self x), scroll(self x);
480
+ animation-range: 0 32px, calc(100% - 32px) 100%;
481
+ }
482
+ .math-display > .katex-display > .katex { min-width: max-content; }
483
+ .math-inline { overflow-wrap: normal; }
484
+ `;
485
+ //# sourceMappingURL=markdown-math.js.map
@@ -0,0 +1,79 @@
1
+ /** Mention linkification — `@claude-4c7289d1`.
2
+ *
3
+ * A mention *addresses* a narrative; a bare `claude-4c7289d1` merely *cites*
4
+ * one (`markdown-nids.ts`). The `@` is the whole difference, and it has to be,
5
+ * because narratives quote each other's ids constantly: if a bare id notified,
6
+ * every design discussion would page half the brane.
7
+ *
8
+ * This file holds only the spec. The tree walk lives in `markdown-refs.ts`,
9
+ * and the *daemon-side* walk that turns a narration into ledger rows lives in
10
+ * `../mentions-scan.ts` — deliberately not here, because metro resolves a
11
+ * shared module's imports from its own directory and this one must stay free
12
+ * of bare npm imports (see `docs/math.md` on the `katex` alias).
13
+ *
14
+ * One spec, two consumers. The renderer and the scanner agree by construction:
15
+ * a handle in a fenced block is inert in both, and there is no second pattern
16
+ * that could drift out of step with what the operator actually sees.
17
+ */
18
+ import { NID_REF_PATTERN, braneNidUrl } from "./markdown-nids.js";
19
+ import { refLinks } from "./markdown-refs.js";
20
+ /** `@` plus a narrative reference — the `:seq` suffix included, because the
21
+ * common way to write one is to type `@` and paste a citation, and citations
22
+ * carry their seq. Matching only the id would link `@claude-4c7289d1` and
23
+ * leave a bare `:42` stranded beside it. The seq says *where to look*; the
24
+ * mention is recorded against the id alone. */
25
+ export const MENTION_PATTERN = `@${NID_REF_PATTERN}`;
26
+ /** Vetoes a candidate by the character before the `@`.
27
+ *
28
+ * A mention has to start a word. Without this, `ben@claude-4c7289d1` is a
29
+ * mention — and so is the tail of any path or handle that happens to end in
30
+ * one. GFM's autolink extension already claims `@` for e-mail, but only when
31
+ * the domain has a dot, so `ben@claude-4c7289d1` reaches us as plain text and
32
+ * this is the only thing standing in front of it.
33
+ *
34
+ * `before` is `""` at the start of a text node, which is a legal position.
35
+ */
36
+ export function mentionBoundary(before) {
37
+ return before === "" || !/[\w./@-]/.test(before);
38
+ }
39
+ function mentionSpec(hrefFor) {
40
+ return {
41
+ pattern: MENTION_PATTERN,
42
+ boundary: (before) => mentionBoundary(before),
43
+ resolve: (match) => {
44
+ const href = hrefFor(match[1], match[2]);
45
+ // `text` keeps the `@`, so the label reads as an address rather than a
46
+ // citation without needing a second href scheme to tell them apart.
47
+ return href === null ? null : { text: match[0], href };
48
+ },
49
+ };
50
+ }
51
+ /** `mentionSpec` packaged as an mdast-util-from-markdown extension.
52
+ *
53
+ * **Install this before `narrativeIdLinks`.** Each extension's links are
54
+ * opaque to the next, so if the citation spec runs first it consumes the nid
55
+ * and leaves the `@` stranded — which is exactly what brane did before
56
+ * mentions existed, and why `nidSpec` now carries a matching boundary. */
57
+ export function mentionLinks(knownIds, hrefFor = braneNidUrl) {
58
+ if (knownIds.size === 0)
59
+ return { transforms: [] };
60
+ return refLinks(mentionSpec((nid, seq) => (knownIds.has(nid) ? hrefFor(nid, seq) : null)));
61
+ }
62
+ /** The same spec, wired to collect rather than to link.
63
+ *
64
+ * `resolve` always returns null, so the tree is never rewritten — but it is
65
+ * still *called*, including from the code-span path, so what this collects is
66
+ * precisely what `mentionLinks` would have linked. That equivalence is the
67
+ * point: the scanner is the renderer with its output thrown away. */
68
+ export function collectMentionSpec(isKnown, into) {
69
+ return {
70
+ pattern: MENTION_PATTERN,
71
+ boundary: (before) => mentionBoundary(before),
72
+ resolve: (match) => {
73
+ if (isKnown(match[1]))
74
+ into.add(match[1]);
75
+ return null;
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=markdown-mentions.js.map
@@ -0,0 +1,41 @@
1
+ /** Narrative-reference linkification, shared by the live app (expo) and the
2
+ * HTML exporter so both linkify exactly the same things.
3
+ *
4
+ * A narrative reference is `{type}-{8hex}`, optionally suffixed with `:seq`
5
+ * (`claude-4c7289d1`, `claude-4c7289d1:42`). Only references to narratives
6
+ * that actually exist are linked, so arbitrary strings of the same shape
7
+ * don't become dead links.
8
+ *
9
+ * The tree walk itself lives in `markdown-refs.ts`; what's here is the
10
+ * narrative-id spec it runs.
11
+ */
12
+ import { linkifyRefs, refLinks } from "./markdown-refs.js";
13
+ export const NID_PATTERN = "[a-z]+-[0-9a-z]{8}";
14
+ export const NID_REF_PATTERN = `(${NID_PATTERN})(?::(\\d+))?`;
15
+ export function braneNidUrl(nid, seq) {
16
+ return seq ? `brane://nid/${nid}/${seq}` : `brane://nid/${nid}`;
17
+ }
18
+ function nidSpec(hrefFor) {
19
+ return {
20
+ pattern: NID_REF_PATTERN,
21
+ // A citation stops where a mention begins (`markdown-mentions.ts`). Without
22
+ // this veto the citation spec consumes the id and leaves the `@` stranded
23
+ // beside a link, so a mention could never render as one.
24
+ boundary: (before) => before !== "@",
25
+ resolve: (match) => {
26
+ const href = hrefFor(match[1], match[2]);
27
+ return href === null ? null : { text: match[0], href };
28
+ },
29
+ };
30
+ }
31
+ /** Rewrites narrative references into links, in place. */
32
+ export function linkifyNarrativeIds(tree, hrefFor) {
33
+ linkifyRefs(tree, nidSpec(hrefFor));
34
+ }
35
+ /** `linkifyNarrativeIds` packaged as an mdast-util-from-markdown extension. */
36
+ export function narrativeIdLinks(knownIds, hrefFor = braneNidUrl) {
37
+ if (knownIds.size === 0)
38
+ return { transforms: [] };
39
+ return refLinks(nidSpec((nid, seq) => (knownIds.has(nid) ? hrefFor(nid, seq) : null)));
40
+ }
41
+ //# sourceMappingURL=markdown-nids.js.map