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.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- 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, "&")
|
|
22
|
+
.replace(/</g, "<")
|
|
23
|
+
.replace(/>/g, ">")
|
|
24
|
+
.replace(/"/g, """);
|
|
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
|