brane 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +362 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +91 -0
- package/DESIGN.md +402 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +205 -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 +116 -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 +332 -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 +1094 -0
- package/dist/commands/token.js +153 -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 +6349 -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 +633 -0
- package/dist/db/tokens.js +583 -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/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 +82 -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 +197 -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/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 +49 -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 +262 -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 +85 -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 +137 -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 +115 -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/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +587 -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 +581 -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/tls.js +148 -0
- package/dist/tmux.js +202 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +141 -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 +53 -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 +211 -0
- package/docs/token-rotation.md +413 -0
- package/docs/view-urls.md +114 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -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 +48 -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,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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Protocol-command linkification: turns `/ramify` in prose into a link that
|
|
2
|
+
* opens the protocol document, the same way `markdown-nids.ts` turns
|
|
3
|
+
* `claude-4c7289d1` into a link to that narrative.
|
|
4
|
+
*
|
|
5
|
+
* Only commands naming a protocol that actually exists are linked, so the
|
|
6
|
+
* slashes that pepper ordinary agent chatter — paths, dates, `and/or` — stay
|
|
7
|
+
* prose. That known-set check does most of the work; the boundary rule below
|
|
8
|
+
* handles the rest.
|
|
9
|
+
*/
|
|
10
|
+
import { refLinks } from "./markdown-refs.js";
|
|
11
|
+
/** A slash command: `/` then a name of the shape protocol files carry
|
|
12
|
+
* (`ramify`, `LIVE_loop`, `reviewer.draft`). */
|
|
13
|
+
export const PROTOCOL_PATTERN = "/([A-Za-z][A-Za-z0-9_.-]*)";
|
|
14
|
+
export function braneProtocolUrl(name) {
|
|
15
|
+
return `brane://protocol/${name}`;
|
|
16
|
+
}
|
|
17
|
+
/** Trailing characters a sentence lends to a command it ends: `read /ramify.`
|
|
18
|
+
* matches `/ramify.`, which is not a protocol. Shave them one at a time
|
|
19
|
+
* rather than up front, because `reviewer.draft` needs its dot. */
|
|
20
|
+
const TRAILING = ".,:;!?-";
|
|
21
|
+
/** Rejects a candidate by what sits next to it. A command stands alone: it
|
|
22
|
+
* follows the start of a run or a space/bracket, never a path segment
|
|
23
|
+
* (`src/memory`, `~/memory`, `and/or`), and nothing path-like follows it
|
|
24
|
+
* (`/memory/index.ts`). Lookbehind would express the first half, but Safari
|
|
25
|
+
* only learned it in 16.4 and this ships to phones. */
|
|
26
|
+
function boundary(before, after) {
|
|
27
|
+
if (before && /[A-Za-z0-9_./\\~-]/.test(before))
|
|
28
|
+
return false;
|
|
29
|
+
if (after === "/")
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/** Rewrites `/protocol` commands into links, as an mdast-util-from-markdown
|
|
34
|
+
* extension. `hrefFor` defaults to the `brane://protocol/<name>` URL the app's
|
|
35
|
+
* markdown renderer matches on. */
|
|
36
|
+
export function protocolLinks(knownProtocols, hrefFor = braneProtocolUrl) {
|
|
37
|
+
if (knownProtocols.size === 0)
|
|
38
|
+
return { transforms: [] };
|
|
39
|
+
return refLinks({
|
|
40
|
+
pattern: PROTOCOL_PATTERN,
|
|
41
|
+
boundary,
|
|
42
|
+
resolve: (match) => {
|
|
43
|
+
let name = match[1];
|
|
44
|
+
while (name && !knownProtocols.has(name) && TRAILING.includes(name[name.length - 1])) {
|
|
45
|
+
name = name.slice(0, -1);
|
|
46
|
+
}
|
|
47
|
+
if (!name || !knownProtocols.has(name))
|
|
48
|
+
return null;
|
|
49
|
+
return { text: `/${name}`, href: hrefFor(name) };
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=markdown-protocols.js.map
|