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
package/dist/folds.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registering a fold — the one implementation behind `POST /api/folds`,
|
|
3
|
+
* `brane fold`, and `brane absorb --fold`.
|
|
4
|
+
*
|
|
5
|
+
* Two callers, one rule with one switch. A phone on the tailnet must not be
|
|
6
|
+
* able to make `/` or `/etc` a spawn point, so a remote client's folds are
|
|
7
|
+
* confined to the operator's home directory. The operator's own shell is
|
|
8
|
+
* not so limited — `brane fold /Volumes/scratch/project` has always worked —
|
|
9
|
+
* and now that the CLI reaches this through the daemon, "the operator's own
|
|
10
|
+
* shell" has to be recognized on the wire. It is recognized as **loopback**:
|
|
11
|
+
* the CLI always talks to `127.0.0.1` (`buildDaemonBaseUrl`), and nothing
|
|
12
|
+
* outside this machine can. The route decides `confineToHome` from
|
|
13
|
+
* `req.socket.remoteAddress`; this module only applies it.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, statSync } from "node:fs";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { fromPortablePath, toPortablePath, getBraneId } from "./db/schema.js";
|
|
19
|
+
import { getConfig } from "./commands/config.js";
|
|
20
|
+
/** A registration the daemon refuses. `status` is the HTTP status the route
|
|
21
|
+
* answers with — 400 for a bad path, 403 for one outside the caller's reach. */
|
|
22
|
+
export class FoldRegistrationError extends Error {
|
|
23
|
+
status;
|
|
24
|
+
constructor(message, status) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.status = status;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The pure decision: is `absolute` a path this caller may register? Exported
|
|
30
|
+
* so the rule can be pinned without a database. Returns the refusal, or
|
|
31
|
+
* `null` when the path is allowed. */
|
|
32
|
+
export function foldPathRefusal(absolute, opts) {
|
|
33
|
+
const home = opts.home ?? homedir();
|
|
34
|
+
if (opts.confineToHome) {
|
|
35
|
+
if (absolute !== home && !absolute.startsWith(home + "/")) {
|
|
36
|
+
return new FoldRegistrationError(`Folds must live under your home directory (${home})`, 403);
|
|
37
|
+
}
|
|
38
|
+
if (absolute === home) {
|
|
39
|
+
return new FoldRegistrationError("Refusing to fold your home directory itself; pick a subdirectory", 403);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
|
|
43
|
+
return new FoldRegistrationError(`Not a directory: ${absolute}`, 400);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** Register `rawPath` (absolute or `~/portable`) as a fold. Idempotent:
|
|
48
|
+
* re-registering an existing fold un-archives it and, when `braneId` is
|
|
49
|
+
* given, rebinds it; otherwise it is a no-op. Never deletes. */
|
|
50
|
+
export function registerFold(db, rawPath, opts) {
|
|
51
|
+
const absolute = resolve(fromPortablePath(rawPath.trim()));
|
|
52
|
+
const refusal = foldPathRefusal(absolute, opts);
|
|
53
|
+
if (refusal)
|
|
54
|
+
throw refusal;
|
|
55
|
+
const portable = toPortablePath(absolute);
|
|
56
|
+
const existing = db.prepare("SELECT * FROM folds WHERE path = ?").get(portable);
|
|
57
|
+
if (existing) {
|
|
58
|
+
if (existing.archived) {
|
|
59
|
+
db.prepare("UPDATE folds SET archived = 0 WHERE path = ?").run(portable);
|
|
60
|
+
}
|
|
61
|
+
if (opts.braneId && opts.braneId !== existing.brane_id) {
|
|
62
|
+
db.prepare("UPDATE folds SET brane_id = ? WHERE path = ?").run(opts.braneId, portable);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const braneId = opts.braneId ?? getConfig("default.brane", db) ?? getBraneId(db);
|
|
67
|
+
db.prepare("INSERT INTO folds (path, created_at, brane_id, archived) VALUES (?, ?, ?, 0)").run(portable, Date.now(), braneId);
|
|
68
|
+
}
|
|
69
|
+
const fold = db.prepare("SELECT * FROM folds WHERE path = ?").get(portable);
|
|
70
|
+
return { fold, created: !existing };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=folds.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// TextMate grammar for EBNF / BNF-style rule definitions.
|
|
2
|
+
// Targets ISO EBNF (`rule = ... ;`, `(* comment *)`, `'x'` / `"x"` terminals) and
|
|
3
|
+
// W3C-style EBNF used in XML/HTML specs (`Rule ::= ...`, `#x20`, `[#x0-#x7F]`).
|
|
4
|
+
// BNF angle-bracket non-terminals (`<name>`) are highlighted too.
|
|
5
|
+
export const ebnf = {
|
|
6
|
+
name: "ebnf",
|
|
7
|
+
scopeName: "source.ebnf",
|
|
8
|
+
displayName: "EBNF",
|
|
9
|
+
aliases: ["bnf"],
|
|
10
|
+
patterns: [
|
|
11
|
+
{ include: "#comment" },
|
|
12
|
+
{ include: "#rule-definition" },
|
|
13
|
+
{ include: "#angle-nonterminal" },
|
|
14
|
+
{ include: "#char-class" },
|
|
15
|
+
{ include: "#string" },
|
|
16
|
+
{ include: "#hex-number" },
|
|
17
|
+
{ include: "#number" },
|
|
18
|
+
{ include: "#operator" },
|
|
19
|
+
{ include: "#identifier" },
|
|
20
|
+
],
|
|
21
|
+
repository: {
|
|
22
|
+
comment: {
|
|
23
|
+
name: "comment.block.ebnf",
|
|
24
|
+
begin: "\\(\\*",
|
|
25
|
+
end: "\\*\\)",
|
|
26
|
+
},
|
|
27
|
+
"rule-definition": {
|
|
28
|
+
match: "^\\s*(<[A-Za-z_][^<>\\s]*>|[A-Za-z_][A-Za-z0-9_-]*)\\s*(::=|=)(?!=)",
|
|
29
|
+
captures: {
|
|
30
|
+
"1": { name: "entity.name.function.ebnf" },
|
|
31
|
+
"2": { name: "keyword.operator.definition.ebnf" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"angle-nonterminal": {
|
|
35
|
+
name: "entity.name.tag.ebnf",
|
|
36
|
+
match: "<[A-Za-z_][A-Za-z0-9_-]*>",
|
|
37
|
+
},
|
|
38
|
+
// Regex-style character classes (`[a-zA-Z_]`, `[^']`, `[#x0-#x7F]`).
|
|
39
|
+
// Swallowed as a single scope so the identifier/number/operator/string
|
|
40
|
+
// patterns don't fire on the interior — crucial because a bare `'` or
|
|
41
|
+
// `"` inside `[^'…]` / `[^"…]` would otherwise open a stray string scope
|
|
42
|
+
// that runs away until the next matching quote anywhere on the line.
|
|
43
|
+
"char-class": {
|
|
44
|
+
name: "constant.other.character-class.ebnf",
|
|
45
|
+
begin: "\\[",
|
|
46
|
+
beginCaptures: { "0": { name: "punctuation.definition.character-class.ebnf" } },
|
|
47
|
+
end: "\\]",
|
|
48
|
+
endCaptures: { "0": { name: "punctuation.definition.character-class.ebnf" } },
|
|
49
|
+
patterns: [
|
|
50
|
+
{ name: "constant.character.escape.ebnf", match: "\\\\." },
|
|
51
|
+
{ name: "constant.numeric.hex.ebnf", match: "#x[0-9A-Fa-f]+" },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
string: {
|
|
55
|
+
patterns: [
|
|
56
|
+
{
|
|
57
|
+
name: "string.quoted.double.ebnf",
|
|
58
|
+
begin: '"',
|
|
59
|
+
end: '"',
|
|
60
|
+
patterns: [
|
|
61
|
+
{ name: "constant.character.escape.ebnf", match: "\\\\." },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "string.quoted.single.ebnf",
|
|
66
|
+
begin: "'",
|
|
67
|
+
end: "'",
|
|
68
|
+
patterns: [
|
|
69
|
+
{ name: "constant.character.escape.ebnf", match: "\\\\." },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
"hex-number": {
|
|
75
|
+
name: "constant.numeric.hex.ebnf",
|
|
76
|
+
match: "#x[0-9A-Fa-f]+",
|
|
77
|
+
},
|
|
78
|
+
number: {
|
|
79
|
+
name: "constant.numeric.decimal.ebnf",
|
|
80
|
+
match: "\\b[0-9]+\\b",
|
|
81
|
+
},
|
|
82
|
+
operator: {
|
|
83
|
+
name: "keyword.operator.ebnf",
|
|
84
|
+
match: "::=|[|,;.*+?()\\[\\]{}=\\-→]",
|
|
85
|
+
},
|
|
86
|
+
identifier: {
|
|
87
|
+
name: "entity.name.tag.ebnf",
|
|
88
|
+
match: "\\b[A-Za-z_][A-Za-z0-9_]*\\b",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=ebnf.js.map
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createHighlighter, bundledLanguages } from "shiki";
|
|
4
|
+
import { ebnf as ebnfGrammar } from "./grammars/ebnf.js";
|
|
5
|
+
export const HIGHLIGHT_THEME = "github-dark-default";
|
|
6
|
+
// Custom (non-bundled) grammars shipped in-repo. Loaded eagerly at highlighter
|
|
7
|
+
// init so the "is this language known?" check below accepts them alongside
|
|
8
|
+
// shiki's bundled set.
|
|
9
|
+
const CUSTOM_GRAMMARS = [ebnfGrammar];
|
|
10
|
+
const CUSTOM_LANG_NAMES = new Set(CUSTOM_GRAMMARS.flatMap((g) => [g.name, ...(g.aliases ?? [])]));
|
|
11
|
+
function isKnownLang(lang) {
|
|
12
|
+
return lang in bundledLanguages || CUSTOM_LANG_NAMES.has(lang);
|
|
13
|
+
}
|
|
14
|
+
// File extension → shiki bundled language id, for the cases where the two
|
|
15
|
+
// differ (`.rs` → rust, `.py` → python). Extensions that already *are* the
|
|
16
|
+
// language id (`.lean`, `.zig`, `.nix`) need no entry — detectLanguage falls
|
|
17
|
+
// back to the bare extension and lets shiki's own registry decide.
|
|
18
|
+
const EXT_LANG = {
|
|
19
|
+
".ts": "typescript",
|
|
20
|
+
".tsx": "tsx",
|
|
21
|
+
".mts": "typescript",
|
|
22
|
+
".cts": "typescript",
|
|
23
|
+
".js": "javascript",
|
|
24
|
+
".jsx": "jsx",
|
|
25
|
+
".mjs": "javascript",
|
|
26
|
+
".cjs": "javascript",
|
|
27
|
+
".json": "json",
|
|
28
|
+
".jsonc": "jsonc",
|
|
29
|
+
".sh": "bash",
|
|
30
|
+
".bash": "bash",
|
|
31
|
+
".zsh": "bash",
|
|
32
|
+
".fish": "fish",
|
|
33
|
+
".py": "python",
|
|
34
|
+
".rb": "ruby",
|
|
35
|
+
".go": "go",
|
|
36
|
+
".rs": "rust",
|
|
37
|
+
".c": "c",
|
|
38
|
+
".h": "c",
|
|
39
|
+
".cpp": "cpp",
|
|
40
|
+
".cc": "cpp",
|
|
41
|
+
".hpp": "cpp",
|
|
42
|
+
".java": "java",
|
|
43
|
+
".kt": "kotlin",
|
|
44
|
+
".swift": "swift",
|
|
45
|
+
".m": "objective-c",
|
|
46
|
+
".mm": "objective-cpp",
|
|
47
|
+
".cs": "csharp",
|
|
48
|
+
".scala": "scala",
|
|
49
|
+
".clj": "clojure",
|
|
50
|
+
".ex": "elixir",
|
|
51
|
+
".exs": "elixir",
|
|
52
|
+
".erl": "erlang",
|
|
53
|
+
".hs": "haskell",
|
|
54
|
+
".lua": "lua",
|
|
55
|
+
".php": "php",
|
|
56
|
+
".dart": "dart",
|
|
57
|
+
".vue": "vue",
|
|
58
|
+
".svelte": "svelte",
|
|
59
|
+
".astro": "astro",
|
|
60
|
+
".css": "css",
|
|
61
|
+
".scss": "scss",
|
|
62
|
+
".less": "less",
|
|
63
|
+
".html": "html",
|
|
64
|
+
".htm": "html",
|
|
65
|
+
".xml": "xml",
|
|
66
|
+
".svg": "xml",
|
|
67
|
+
".yaml": "yaml",
|
|
68
|
+
".yml": "yaml",
|
|
69
|
+
".toml": "toml",
|
|
70
|
+
".ini": "ini",
|
|
71
|
+
".sql": "sql",
|
|
72
|
+
".graphql": "graphql",
|
|
73
|
+
".gql": "graphql",
|
|
74
|
+
".proto": "proto",
|
|
75
|
+
".nix": "nix",
|
|
76
|
+
".zig": "zig",
|
|
77
|
+
".r": "r",
|
|
78
|
+
".jl": "julia",
|
|
79
|
+
".ebnf": "ebnf",
|
|
80
|
+
".bnf": "ebnf",
|
|
81
|
+
};
|
|
82
|
+
// Extensions the *client* renders itself rather than as highlighted source —
|
|
83
|
+
// FilesView routes these to MarkdownContent. Shiki does bundle `md`/`mdx`
|
|
84
|
+
// grammars, so without this the extension fallback below would resolve them and
|
|
85
|
+
// we'd ship a full token payload nobody looks at.
|
|
86
|
+
const CLIENT_RENDERED_EXTS = new Set([".md", ".mdx", ".markdown"]);
|
|
87
|
+
const BASENAME_LANG = {
|
|
88
|
+
"Dockerfile": "docker",
|
|
89
|
+
"Makefile": "make",
|
|
90
|
+
"CMakeLists.txt": "cmake",
|
|
91
|
+
".gitignore": "shellscript",
|
|
92
|
+
".env": "shellscript",
|
|
93
|
+
};
|
|
94
|
+
export function detectLanguage(filePath) {
|
|
95
|
+
const base = filePath.split("/").pop() ?? filePath;
|
|
96
|
+
for (const [needle, lang] of Object.entries(BASENAME_LANG)) {
|
|
97
|
+
// Exact match or "needle.<suffix>" (e.g. Dockerfile.dev, .env.local) — avoids
|
|
98
|
+
// bare `startsWith` over-matching ".env" into ".environmental".
|
|
99
|
+
if (base === needle || base.startsWith(needle + "."))
|
|
100
|
+
return lang;
|
|
101
|
+
}
|
|
102
|
+
const ext = extname(base).toLowerCase();
|
|
103
|
+
if (!ext)
|
|
104
|
+
return null;
|
|
105
|
+
if (EXT_LANG[ext])
|
|
106
|
+
return EXT_LANG[ext];
|
|
107
|
+
if (CLIENT_RENDERED_EXTS.has(ext))
|
|
108
|
+
return null;
|
|
109
|
+
// Long tail: for a great many languages the extension and shiki's language id
|
|
110
|
+
// are the same string (lean, zig, nix, tex, diff, …). Rather than grow the map
|
|
111
|
+
// one bug report at a time, ask shiki whether the bare extension names a
|
|
112
|
+
// grammar it has. The explicit map above still wins wherever they disagree.
|
|
113
|
+
const bare = ext.slice(1);
|
|
114
|
+
if (bare && isKnownLang(bare))
|
|
115
|
+
return bare;
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// Fence tags that name no shiki grammar and aren't extension-shaped either, so
|
|
119
|
+
// the extension fallback in resolveLang can't reach them. Kept deliberately
|
|
120
|
+
// short: the map below is for words people type in fences, not a second
|
|
121
|
+
// language registry.
|
|
122
|
+
const FENCE_LANG = {
|
|
123
|
+
golang: "go",
|
|
124
|
+
plist: "xml",
|
|
125
|
+
xaml: "xml",
|
|
126
|
+
patch: "diff",
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* A fenced block's info string → a grammar shiki can actually load, or null to
|
|
130
|
+
* render the block as plain text.
|
|
131
|
+
*
|
|
132
|
+
* The info string is whatever the author typed, and the two obvious sources of
|
|
133
|
+
* it disagree about vocabulary. Shiki's `bundledLanguages` is generous with
|
|
134
|
+
* aliases (`js`, `sh`, `yml`, `c++` all resolve) but has no SVG grammar, so
|
|
135
|
+
* ```svg fell through to plain text — while the *same bytes* opened from the
|
|
136
|
+
* Files tab highlighted fine, because that path resolves `.svg` through
|
|
137
|
+
* EXT_LANG instead. Rather than keep two vocabularies, a tag shiki doesn't know
|
|
138
|
+
* is retried as if it were a file extension, which inherits EXT_LANG whole and
|
|
139
|
+
* every entry added to it later.
|
|
140
|
+
*/
|
|
141
|
+
export function resolveLang(lang) {
|
|
142
|
+
if (!lang)
|
|
143
|
+
return null;
|
|
144
|
+
const tag = lang.trim().toLowerCase();
|
|
145
|
+
if (!tag)
|
|
146
|
+
return null;
|
|
147
|
+
if (isKnownLang(tag))
|
|
148
|
+
return tag;
|
|
149
|
+
if (FENCE_LANG[tag])
|
|
150
|
+
return FENCE_LANG[tag];
|
|
151
|
+
return detectLanguage(`x.${tag}`);
|
|
152
|
+
}
|
|
153
|
+
let highlighterP = null;
|
|
154
|
+
async function getHighlighter() {
|
|
155
|
+
if (!highlighterP) {
|
|
156
|
+
highlighterP = createHighlighter({ themes: [HIGHLIGHT_THEME], langs: CUSTOM_GRAMMARS });
|
|
157
|
+
}
|
|
158
|
+
return highlighterP;
|
|
159
|
+
}
|
|
160
|
+
// Memoize per-language load promises so concurrent first-loads of the same
|
|
161
|
+
// language collapse to a single underlying loadLanguage call.
|
|
162
|
+
const langLoads = new Map();
|
|
163
|
+
async function ensureLangLoaded(hl, lang) {
|
|
164
|
+
if (hl.getLoadedLanguages().includes(lang))
|
|
165
|
+
return true;
|
|
166
|
+
let p = langLoads.get(lang);
|
|
167
|
+
if (!p) {
|
|
168
|
+
p = hl.loadLanguage(lang).then(() => undefined);
|
|
169
|
+
langLoads.set(lang, p);
|
|
170
|
+
p.catch(() => {
|
|
171
|
+
// Failed load — drop the memo so a later retry can try again
|
|
172
|
+
langLoads.delete(lang);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
await p;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.warn(`[highlight] failed to load grammar for ${lang}:`, err);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const CACHE_MAX = 200;
|
|
185
|
+
const cache = new Map();
|
|
186
|
+
function cacheGet(key) {
|
|
187
|
+
const v = cache.get(key);
|
|
188
|
+
if (v !== undefined) {
|
|
189
|
+
cache.delete(key);
|
|
190
|
+
cache.set(key, v);
|
|
191
|
+
}
|
|
192
|
+
return v;
|
|
193
|
+
}
|
|
194
|
+
function cacheSet(key, value) {
|
|
195
|
+
cache.set(key, value);
|
|
196
|
+
if (cache.size > CACHE_MAX) {
|
|
197
|
+
const firstKey = cache.keys().next().value;
|
|
198
|
+
if (firstKey !== undefined)
|
|
199
|
+
cache.delete(firstKey);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function compactTokens(tokens) {
|
|
203
|
+
return tokens.map((line) => line.map((tok) => {
|
|
204
|
+
const out = { c: tok.content };
|
|
205
|
+
if (tok.color)
|
|
206
|
+
out.f = tok.color;
|
|
207
|
+
if (tok.fontStyle && tok.fontStyle > 0)
|
|
208
|
+
out.s = tok.fontStyle;
|
|
209
|
+
return out;
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Shiki-rendered HTML for a code snippet. Returns null if the tag resolves to
|
|
214
|
+
* no grammar, or the grammar fails to load — caller falls back to raw text.
|
|
215
|
+
*/
|
|
216
|
+
export async function highlightHtml(content, lang) {
|
|
217
|
+
const resolved = resolveLang(lang);
|
|
218
|
+
if (!resolved)
|
|
219
|
+
return null;
|
|
220
|
+
const hl = await getHighlighter();
|
|
221
|
+
if (!(await ensureLangLoaded(hl, resolved)))
|
|
222
|
+
return null;
|
|
223
|
+
return hl.codeToHtml(content, { lang: resolved, theme: HIGHLIGHT_THEME });
|
|
224
|
+
}
|
|
225
|
+
export async function highlight(content, lang) {
|
|
226
|
+
// The resolved id is what gets cached and reported back, so ```svg and
|
|
227
|
+
// ```xml share one cache entry rather than tokenizing the same bytes twice.
|
|
228
|
+
const resolved = resolveLang(lang);
|
|
229
|
+
if (!resolved)
|
|
230
|
+
return null;
|
|
231
|
+
const hl = await getHighlighter();
|
|
232
|
+
if (!(await ensureLangLoaded(hl, resolved)))
|
|
233
|
+
return null;
|
|
234
|
+
const sha = createHash("sha1").update(content).digest("hex");
|
|
235
|
+
const key = `${sha}:${resolved}:${HIGHLIGHT_THEME}`;
|
|
236
|
+
const cached = cacheGet(key);
|
|
237
|
+
if (cached)
|
|
238
|
+
return cached;
|
|
239
|
+
const result = hl.codeToTokens(content, { lang: resolved, theme: HIGHLIGHT_THEME });
|
|
240
|
+
const out = {
|
|
241
|
+
lang: resolved,
|
|
242
|
+
theme: HIGHLIGHT_THEME,
|
|
243
|
+
bg: result.bg ?? "#0f0f0f",
|
|
244
|
+
fg: result.fg ?? "#e0e0e0",
|
|
245
|
+
tokens: compactTokens(result.tokens),
|
|
246
|
+
};
|
|
247
|
+
cacheSet(key, out);
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=highlight.js.map
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a crypto-random string of `len` lowercase base36 characters
|
|
4
|
+
* (`0-9a-z`). Used for the collision-resistant suffixes of narrative ids
|
|
5
|
+
* (`<agent>-<8>`), brane ids (`brane-<6>`), and server-minted external ids
|
|
6
|
+
* (`external-<8>`).
|
|
7
|
+
*/
|
|
8
|
+
export function randomBase36(len) {
|
|
9
|
+
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
10
|
+
// Rejection-sample to avoid modulo bias: 256 is not a multiple of 36, so a
|
|
11
|
+
// plain `byte % 36` would map the high bytes (252-255) onto 0-3, skewing
|
|
12
|
+
// those four characters. Discard bytes >= 252 (the largest multiple of 36)
|
|
13
|
+
// and redraw, so every character is uniform over the 36-symbol alphabet.
|
|
14
|
+
let s = "";
|
|
15
|
+
while (s.length < len) {
|
|
16
|
+
const bytes = randomBytes(len - s.length);
|
|
17
|
+
for (let i = 0; i < bytes.length && s.length < len; i++) {
|
|
18
|
+
if (bytes[i] < 252)
|
|
19
|
+
s += alphabet[bytes[i] % 36];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return s;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ids.js.map
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/** What a narration is about to look like, told to the agent writing it.
|
|
2
|
+
*
|
|
3
|
+
* Every renderer in this codebase fails *quietly* by design: unparseable
|
|
4
|
+
* LaTeX comes back as its own source rather than as KaTeX's red error markup
|
|
5
|
+
* (`renderMathHtml`), a `$…$` run that trips the currency guard is escaped
|
|
6
|
+
* into prose (`prepareMathMarkdown`), an unclosed fence simply swallows the
|
|
7
|
+
* rest of the message. That is the right behaviour for the *reader*, who
|
|
8
|
+
* wants a document rather than a diagnostic — and exactly wrong for the
|
|
9
|
+
* *writer*, who never finds out that the equation they wrote did not set.
|
|
10
|
+
*
|
|
11
|
+
* This module closes that gap without reopening it for the reader: the
|
|
12
|
+
* rendering is unchanged, and the reasoning the renderers were already doing
|
|
13
|
+
* is reported back through `narrate` instead of being discarded.
|
|
14
|
+
*
|
|
15
|
+
* Two properties make the report trustworthy enough to act on:
|
|
16
|
+
*
|
|
17
|
+
* **It is the real parse, not a lookalike.** The math run is found by the
|
|
18
|
+
* same `prepareMathMarkdown` and rendered by the same micromark pass the
|
|
19
|
+
* export uses, so a warning fires if and only if the operator's view differs
|
|
20
|
+
* from what was intended. A second scanner written to approximate the first
|
|
21
|
+
* would eventually disagree with it, and a linter that cries wolf is worse
|
|
22
|
+
* than no linter.
|
|
23
|
+
*
|
|
24
|
+
* **Every warning carries its fallback** — the literal text that will appear
|
|
25
|
+
* in place of the intended rendering. Whether a failure *matters* is a
|
|
26
|
+
* judgment only the author can make, and it turns almost entirely on that
|
|
27
|
+
* text: `$E = mc^2$` shown as source still communicates; a matrix shown as
|
|
28
|
+
* source is line noise. Reporting the parser's complaint without the fallback
|
|
29
|
+
* would leave that call unmakeable.
|
|
30
|
+
*/
|
|
31
|
+
import { micromark } from "micromark";
|
|
32
|
+
import { gfmTable, gfmTableHtml } from "micromark-extension-gfm-table";
|
|
33
|
+
import { gfmAutolinkLiteral, gfmAutolinkLiteralHtml } from "micromark-extension-gfm-autolink-literal";
|
|
34
|
+
import { gfmTaskListItem, gfmTaskListItemHtml } from "micromark-extension-gfm-task-list-item";
|
|
35
|
+
import { math } from "micromark-extension-math";
|
|
36
|
+
import { mathHtmlExtension } from "./math.js";
|
|
37
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
38
|
+
import { inspectDiagram } from "./diagram/index.js";
|
|
39
|
+
import { mathContentRejection, prepareMathMarkdown, } from "./shared/markdown-math.js";
|
|
40
|
+
/** Does this rejected `$…$` run look like a failed equation rather than prose?
|
|
41
|
+
*
|
|
42
|
+
* The guard in `mathContentRejection` throws out far more currency and shell
|
|
43
|
+
* syntax than it does mathematics, and warning on every one of those would
|
|
44
|
+
* bury the real findings. A backslash command or a sub/superscript that opens
|
|
45
|
+
* a group is the signal that someone was writing TeX: `$\\theta$`, `$x_{i+1}$`.
|
|
46
|
+
* Bare `$PATH` and `$5 to $10` stay silent, which is the whole point.
|
|
47
|
+
*/
|
|
48
|
+
const TEX_ISH = /\\[a-zA-Z]|[\^_]\s*[{(]/;
|
|
49
|
+
/** Cheap enough to run on every narration: a source with no math token at all
|
|
50
|
+
* never reaches micromark, and the scan that finds fence and delimiter
|
|
51
|
+
* problems is the same single pass `prepareMathMarkdown` already makes. */
|
|
52
|
+
const HAS_MATH_TOKEN = /(?:^|[^\\])\$/;
|
|
53
|
+
/** How many warnings one narration can produce. A message that trips more than
|
|
54
|
+
* this is broken in a way the first few already explain. */
|
|
55
|
+
const MAX_DIAGNOSTICS = 10;
|
|
56
|
+
export function diagnoseMarkdown(md) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const issues = [];
|
|
59
|
+
const prepared = prepareMathMarkdown(md, (issue) => issues.push(issue));
|
|
60
|
+
for (const issue of issues) {
|
|
61
|
+
const line = lineOf(md, issue.index);
|
|
62
|
+
switch (issue.kind) {
|
|
63
|
+
case "dollar_escaped": {
|
|
64
|
+
const reason = mathContentRejection(issue.content, issue.after);
|
|
65
|
+
if (!reason || !TEX_ISH.test(issue.content))
|
|
66
|
+
break;
|
|
67
|
+
out.push({
|
|
68
|
+
kind: "math_demoted",
|
|
69
|
+
message: `\`$${clamp(issue.content, 60)}$\` looks like math, but ${reason} — so it is not treated as math and renders as literal text.`,
|
|
70
|
+
fallback: `$${clamp(issue.content, 120)}$`,
|
|
71
|
+
line,
|
|
72
|
+
});
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "tex_unmatched":
|
|
76
|
+
out.push({
|
|
77
|
+
kind: "math_unclosed",
|
|
78
|
+
message: `${capitalize(issue.reason)} — the LaTeX renders as literal text.`,
|
|
79
|
+
fallback: excerptAt(md, issue.index),
|
|
80
|
+
line,
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
case "display_unclosed":
|
|
84
|
+
out.push({
|
|
85
|
+
kind: "math_unclosed",
|
|
86
|
+
message: "`$$` is never closed — everything after it is swallowed into one display-math block.",
|
|
87
|
+
fallback: excerptAt(md, issue.index),
|
|
88
|
+
line,
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
case "fence_unclosed":
|
|
92
|
+
out.push({
|
|
93
|
+
kind: "fence_unclosed",
|
|
94
|
+
message: "A code fence is never closed — everything after it renders as one code block.",
|
|
95
|
+
fallback: excerptAt(md, issue.index),
|
|
96
|
+
line,
|
|
97
|
+
});
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (HAS_MATH_TOKEN.test(prepared)) {
|
|
102
|
+
// Rendered and thrown away: the HTML is the export's business, and all we
|
|
103
|
+
// want from this pass is which runs KaTeX refused.
|
|
104
|
+
micromark(prepared, {
|
|
105
|
+
extensions: [gfmTable(), gfmAutolinkLiteral(), gfmTaskListItem(), math()],
|
|
106
|
+
htmlExtensions: [
|
|
107
|
+
gfmTableHtml(),
|
|
108
|
+
gfmAutolinkLiteralHtml(),
|
|
109
|
+
gfmTaskListItemHtml(),
|
|
110
|
+
mathHtmlExtension((message, fallback) => {
|
|
111
|
+
out.push({
|
|
112
|
+
kind: "math_parse_error",
|
|
113
|
+
message,
|
|
114
|
+
fallback: clamp(fallback, 160),
|
|
115
|
+
// Best-effort: the LaTeX reaching KaTeX has been normalized, so it
|
|
116
|
+
// is not always findable in the source the author typed. A line
|
|
117
|
+
// that might be wrong would be worse than none, so it is omitted
|
|
118
|
+
// rather than guessed.
|
|
119
|
+
line: lineOfValue(md, fallback),
|
|
120
|
+
});
|
|
121
|
+
}),
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
for (const d of diagnoseDiagramFences(md))
|
|
126
|
+
out.push(d);
|
|
127
|
+
return out.slice(0, MAX_DIAGNOSTICS);
|
|
128
|
+
}
|
|
129
|
+
/** ```mermaid fences that will not draw.
|
|
130
|
+
*
|
|
131
|
+
* Same contract as the math warnings: it is the *real* decision, taken by the
|
|
132
|
+
* same `diagnoseDiagram` the renderers call, so a warning fires iff the
|
|
133
|
+
* operator sees source where a diagram was meant.
|
|
134
|
+
*
|
|
135
|
+
* Which is why the fences come from **a markdown parse and not a regex**. The
|
|
136
|
+
* first draft scanned for the fence with a pattern, three lines under a
|
|
137
|
+
* comment forbidding exactly that, and it disagreed with the renderers in
|
|
138
|
+
* both directions inside a week: a mermaid fence quoted *inside* a longer
|
|
139
|
+
* ```` fence — which is how an agent documents this feature — warned about a
|
|
140
|
+
* diagram that was never going to draw, while a fence inside a blockquote
|
|
141
|
+
* renders and was never diagnosed. `fromMarkdown` sees what micromark sees,
|
|
142
|
+
* because it is micromark.
|
|
143
|
+
*
|
|
144
|
+
* `fallback` is the source, because that is literally what appears — the
|
|
145
|
+
* highlighted code block a mermaid fence renders as today. Judging whether it
|
|
146
|
+
* matters is the author's call and turns on reading it: a four-line flowchart
|
|
147
|
+
* shown as source still communicates; a thirty-line one does not.
|
|
148
|
+
*/
|
|
149
|
+
function diagnoseDiagramFences(md) {
|
|
150
|
+
const out = [];
|
|
151
|
+
// Cheap gate so an ordinary narration never pays for a parse. Deliberately
|
|
152
|
+
// just the word: anything narrower has to model where a fence may start, and
|
|
153
|
+
// a `>`-prefixed one inside a blockquote is exactly what the first attempt
|
|
154
|
+
// at that missed — which is the bug this function was being rewritten for.
|
|
155
|
+
if (!/mermaid/i.test(md))
|
|
156
|
+
return out;
|
|
157
|
+
let tree;
|
|
158
|
+
try {
|
|
159
|
+
tree = fromMarkdown(md);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
const walk = (node) => {
|
|
165
|
+
if (!node || typeof node !== "object")
|
|
166
|
+
return;
|
|
167
|
+
if (node.type === "code" && typeof node.lang === "string" &&
|
|
168
|
+
node.lang.toLowerCase() === "mermaid") {
|
|
169
|
+
const body = typeof node.value === "string" ? node.value : "";
|
|
170
|
+
if (body.trim()) {
|
|
171
|
+
const { refusal, literal } = inspectDiagram(body);
|
|
172
|
+
// mdast gives the fence's own line; the body starts on the next one.
|
|
173
|
+
const bodyLine = (node.position?.start?.line ?? 0) + 1;
|
|
174
|
+
// The diagram draws, but some label shows its markup as text. The
|
|
175
|
+
// fallback is the statement's source line, since that is where the
|
|
176
|
+
// characters the operator sees come from.
|
|
177
|
+
const bodyLines = body.split("\n");
|
|
178
|
+
for (const note of literal) {
|
|
179
|
+
out.push({
|
|
180
|
+
kind: "diagram_label_literal",
|
|
181
|
+
message: `In this \`mermaid\` diagram, ${note.message}.`,
|
|
182
|
+
fallback: clamp((bodyLines[note.line - 1] ?? "").trim(), 160),
|
|
183
|
+
line: bodyLine + note.line - 1,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (refusal) {
|
|
187
|
+
out.push({
|
|
188
|
+
kind: refusal.kind === "unsupported" ? "diagram_unsupported" : "diagram_parse_error",
|
|
189
|
+
message: `This \`mermaid\` diagram will not draw: ${refusal.message}. It renders as its own source.`,
|
|
190
|
+
fallback: clamp(body.trim(), 280),
|
|
191
|
+
line: refusal.line === undefined ? bodyLine : bodyLine + refusal.line - 1,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (const child of (node.children ?? []))
|
|
198
|
+
walk(child);
|
|
199
|
+
};
|
|
200
|
+
walk(tree);
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
/** The line a failed run sits on, found by its own source, or undefined. */
|
|
204
|
+
function lineOfValue(md, fallback) {
|
|
205
|
+
// Delimiters and all first: the same LaTeX may appear earlier as a code
|
|
206
|
+
// listing, and a listing is not the run that failed.
|
|
207
|
+
let at = md.indexOf(fallback);
|
|
208
|
+
if (at === -1) {
|
|
209
|
+
const value = fallback.replace(/^\$\$?|\$\$?$/g, "");
|
|
210
|
+
at = value ? md.indexOf(value) : -1;
|
|
211
|
+
}
|
|
212
|
+
return at === -1 ? undefined : lineOf(md, at);
|
|
213
|
+
}
|
|
214
|
+
function lineOf(md, index) {
|
|
215
|
+
let line = 1;
|
|
216
|
+
for (let i = 0; i < index && i < md.length; i++)
|
|
217
|
+
if (md[i] === "\n")
|
|
218
|
+
line++;
|
|
219
|
+
return line;
|
|
220
|
+
}
|
|
221
|
+
/** The rest of the line starting at `index`, capped — enough to recognize the
|
|
222
|
+
* spot without pasting a paragraph back at the author. */
|
|
223
|
+
function excerptAt(md, index, max = 80) {
|
|
224
|
+
const end = md.indexOf("\n", index);
|
|
225
|
+
return clamp(md.slice(index, end === -1 ? md.length : end), max);
|
|
226
|
+
}
|
|
227
|
+
function clamp(s, max) {
|
|
228
|
+
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
|
229
|
+
}
|
|
230
|
+
function capitalize(s) {
|
|
231
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=markdown-diagnostics.js.map
|