brane 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { appendEvent } from "../db/events.js";
|
|
5
|
+
import { exportNarrativeHtml } from "../export.js";
|
|
6
|
+
import { diagnoseMarkdown } from "../markdown-diagnostics.js";
|
|
7
|
+
import { renderDiagram } from "../diagram/index.js";
|
|
8
|
+
import { DIAGRAM_EDGE_HIGHLIGHT_CSS } from "../shared/diagram-css.js";
|
|
9
|
+
const HOST = "claude-aaaaaaaa";
|
|
10
|
+
const PEER = "goose-bbbbbbbb";
|
|
11
|
+
function insertNarrative(db, id) {
|
|
12
|
+
db.prepare(`INSERT INTO narratives (id, brane_id, agent_type, role, created_at, status)
|
|
13
|
+
VALUES (?, 'brane-test', ?, 'worker', 0, 'active')`).run(id, id.split("-")[0]);
|
|
14
|
+
}
|
|
15
|
+
const fence = (body) => "```mermaid\n" + body + "\n```";
|
|
16
|
+
// One renderer, two targets. The export calls the same `renderDiagram` the app
|
|
17
|
+
// reaches through POST /api/diagram, so a downloaded conversation is a snapshot
|
|
18
|
+
// of what the operator saw rather than a lookalike of it. What makes that
|
|
19
|
+
// possible here is that the SVG is static — the export carries no script, and
|
|
20
|
+
// never can.
|
|
21
|
+
describe("the HTML export draws mermaid fences", () => {
|
|
22
|
+
it("emits an inline SVG where the fence was", async () => {
|
|
23
|
+
const db = createTestDb();
|
|
24
|
+
insertNarrative(db, HOST);
|
|
25
|
+
appendEvent(db, HOST, "narration", {
|
|
26
|
+
content: "Here is the loop:\n\n" + fence("flowchart TD\n A[Listen] --> B[Interpret]"),
|
|
27
|
+
});
|
|
28
|
+
const html = (await exportNarrativeHtml(db, HOST));
|
|
29
|
+
assert.match(html, /<div class="diagram"><svg/);
|
|
30
|
+
assert.ok(html.includes(">Listen<"), "the label is in the document as text");
|
|
31
|
+
assert.ok(!/language-mermaid/.test(html), "it should no longer be a code block");
|
|
32
|
+
});
|
|
33
|
+
it("scrolls a wide diagram inside its bubble instead of spilling out", async () => {
|
|
34
|
+
const db = createTestDb();
|
|
35
|
+
insertNarrative(db, HOST);
|
|
36
|
+
appendEvent(db, HOST, "narration", {
|
|
37
|
+
content: fence("flowchart LR\n A --> B --> C --> D --> E --> F --> G --> H"),
|
|
38
|
+
});
|
|
39
|
+
const html = (await exportNarrativeHtml(db, HOST));
|
|
40
|
+
// The SVG carries a fixed width, so without a scroller around it a wide
|
|
41
|
+
// diagram draws past the bubble's edge — the export shows all of it at
|
|
42
|
+
// once, where the app clips and scrolls.
|
|
43
|
+
const rule = /\.content \.diagram\s*\{([^}]*)\}/.exec(html);
|
|
44
|
+
assert.ok(rule, "the stylesheet has a rule for the diagram wrapper");
|
|
45
|
+
assert.match(rule[1], /overflow-x:\s*auto/);
|
|
46
|
+
});
|
|
47
|
+
it("keeps the export scriptless, which is its whole constraint", async () => {
|
|
48
|
+
const db = createTestDb();
|
|
49
|
+
insertNarrative(db, HOST);
|
|
50
|
+
appendEvent(db, HOST, "narration", {
|
|
51
|
+
content: fence("flowchart TD\n A --> B --> C\n A --> C"),
|
|
52
|
+
});
|
|
53
|
+
const html = (await exportNarrativeHtml(db, HOST));
|
|
54
|
+
// Just the diagram. The document around it legitimately carries links.
|
|
55
|
+
const start = html.indexOf('<div class="diagram">');
|
|
56
|
+
const body = html.slice(start, html.indexOf("</svg>", start) + 6);
|
|
57
|
+
assert.ok(body.includes("<svg"), "found the diagram");
|
|
58
|
+
assert.ok(!/<script/i.test(body));
|
|
59
|
+
assert.ok(!/@import/i.test(body));
|
|
60
|
+
assert.ok(!/\son\w+\s*=/i.test(body), "no inline event handlers");
|
|
61
|
+
assert.ok(!/https?:\/\//.test(body.replace(/xmlns="[^"]*"/g, "")), "a diagram must reference nothing over the network");
|
|
62
|
+
});
|
|
63
|
+
it("picks out an arrow and its label on hover or tap, with CSS alone", async () => {
|
|
64
|
+
const db = createTestDb();
|
|
65
|
+
insertNarrative(db, HOST);
|
|
66
|
+
appendEvent(db, HOST, "narration", {
|
|
67
|
+
content: fence("flowchart TD\n A -->|first| B\n A -->|second| C\n B --> C"),
|
|
68
|
+
});
|
|
69
|
+
const html = (await exportNarrativeHtml(db, HOST));
|
|
70
|
+
const start = html.indexOf('<div class="diagram">');
|
|
71
|
+
const svg = html.slice(start, html.indexOf("</svg>", start) + 6);
|
|
72
|
+
const groups = svg.match(/<g class="brane-diagram-edge-hl"[^>]*>/g) ?? [];
|
|
73
|
+
// One per edge for its line, and one more per labelled edge for its label,
|
|
74
|
+
// painted above every line so no later stroke covers an earlier label.
|
|
75
|
+
assert.equal(groups.length, 5, "a line group per edge, a label group per label");
|
|
76
|
+
for (const g of groups) {
|
|
77
|
+
// Hidden by attribute, so a page without the rule shows nothing extra;
|
|
78
|
+
// focusable, so a tap can hold it open without a script.
|
|
79
|
+
assert.match(g, /opacity="0"/);
|
|
80
|
+
assert.match(g, /tabindex="0"/);
|
|
81
|
+
}
|
|
82
|
+
assert.ok(groups.some((g) => /aria-label="first"/.test(g)));
|
|
83
|
+
// Above every edge label, which is what makes it disambiguate; below every
|
|
84
|
+
// node, so the boxes cover the highlighted line's ends.
|
|
85
|
+
const first = svg.indexOf('<g class="brane-diagram-edge-hl"');
|
|
86
|
+
const last = svg.lastIndexOf('<g class="brane-diagram-edge-hl"');
|
|
87
|
+
assert.ok(first > svg.indexOf(">second</text>"), "the highlight layer is painted after the edge labels");
|
|
88
|
+
assert.ok(last < svg.indexOf(">A<") && last < svg.indexOf(">C<"), "and before the nodes");
|
|
89
|
+
assert.ok(html.includes(DIAGRAM_EDGE_HIGHLIGHT_CSS), "the shared rule is in the document verbatim");
|
|
90
|
+
assert.match(DIAGRAM_EDGE_HIGHLIGHT_CSS, /\.brane-diagram-edge-hl:focus\s*\{\s*opacity:\s*1/);
|
|
91
|
+
});
|
|
92
|
+
it("stops a highlighted line short of each arrowhead, and nowhere else", () => {
|
|
93
|
+
const ends = (d) => {
|
|
94
|
+
const n = d.match(/-?\d+(?:\.\d+)?/g).map(Number);
|
|
95
|
+
return { start: [n[0], n[1]], end: [n[n.length - 2], n[n.length - 1]] };
|
|
96
|
+
};
|
|
97
|
+
const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
98
|
+
const paths = (src) => {
|
|
99
|
+
const svg = renderDiagram(src, { edgeHighlight: true }).svg;
|
|
100
|
+
const base = /<path d="([^"]+)" fill="none" stroke="var\(--brane-diagram-line/.exec(svg)[1];
|
|
101
|
+
const hl = /<path d="([^"]+)" fill="none" stroke="var\(--brane-diagram-highlight/.exec(svg)[1];
|
|
102
|
+
return { base: ends(base), hl: ends(hl), svg };
|
|
103
|
+
};
|
|
104
|
+
// A 3px line run under the point blunts it, so it stops one head length
|
|
105
|
+
// back, and the highlight's head is anchored at its base to make up the
|
|
106
|
+
// difference: the tip lands where the base line's does.
|
|
107
|
+
const one = paths("flowchart TD\n A --> B");
|
|
108
|
+
assert.ok(Math.abs(dist(one.base.end, one.hl.end) - 10) < 0.01, "pulled back one head length");
|
|
109
|
+
assert.deepEqual(one.hl.start, one.base.start, "the tail, which has no head, is untouched");
|
|
110
|
+
assert.match(one.svg, /<marker id="ah-hl-[0-9a-f]+"[^>]*refX="0"/);
|
|
111
|
+
const both = paths("flowchart TD\n A <--> B");
|
|
112
|
+
assert.ok(Math.abs(dist(both.base.start, both.hl.start) - 10) < 0.01, "both heads, both ends");
|
|
113
|
+
assert.ok(Math.abs(dist(both.base.end, both.hl.end) - 10) < 0.01);
|
|
114
|
+
// No head, nothing to blunt: the line runs into the box, which covers it.
|
|
115
|
+
const open = paths("flowchart TD\n A --- B");
|
|
116
|
+
assert.deepEqual(open.hl.end, open.base.end);
|
|
117
|
+
assert.deepEqual(open.hl.start, open.base.start);
|
|
118
|
+
});
|
|
119
|
+
it("draws no highlight layer for a caller that did not ask, having no rule to reveal it", () => {
|
|
120
|
+
const drawn = renderDiagram("flowchart TD\n A -->|x| B");
|
|
121
|
+
assert.ok(drawn);
|
|
122
|
+
assert.ok(!drawn.svg.includes("brane-diagram-edge-hl"));
|
|
123
|
+
assert.ok(!drawn.svg.includes("tabindex"));
|
|
124
|
+
});
|
|
125
|
+
it("falls back to the highlighted code block for a diagram it cannot draw", async () => {
|
|
126
|
+
const db = createTestDb();
|
|
127
|
+
insertNarrative(db, HOST);
|
|
128
|
+
appendEvent(db, HOST, "narration", {
|
|
129
|
+
content: fence("gantt\n title A schedule\n section S\n task :a1, 2026-01-01, 30d"),
|
|
130
|
+
});
|
|
131
|
+
const html = (await exportNarrativeHtml(db, HOST));
|
|
132
|
+
assert.ok(!/<div class="diagram">/.test(html), "nothing should have been drawn");
|
|
133
|
+
// The fence loop only substitutes non-null results, so an undrawn diagram
|
|
134
|
+
// lands exactly as the code block it was before this feature existed.
|
|
135
|
+
assert.ok(/gantt/.test(html), "the source is still there for the reader");
|
|
136
|
+
});
|
|
137
|
+
it("leaves other languages alone", async () => {
|
|
138
|
+
const db = createTestDb();
|
|
139
|
+
insertNarrative(db, HOST);
|
|
140
|
+
appendEvent(db, HOST, "narration", {
|
|
141
|
+
content: "```js\nconst a = 1;\n```",
|
|
142
|
+
});
|
|
143
|
+
const html = (await exportNarrativeHtml(db, HOST));
|
|
144
|
+
assert.ok(!/<div class="diagram">/.test(html));
|
|
145
|
+
});
|
|
146
|
+
it("links a node labelled with a narrative that exists, and only that one", async () => {
|
|
147
|
+
const db = createTestDb();
|
|
148
|
+
insertNarrative(db, HOST);
|
|
149
|
+
insertNarrative(db, PEER);
|
|
150
|
+
appendEvent(db, HOST, "narration", {
|
|
151
|
+
content: fence(`flowchart TD\n A[${PEER}] --> B[codex-cccccccc]`),
|
|
152
|
+
});
|
|
153
|
+
const html = (await exportNarrativeHtml(db, HOST));
|
|
154
|
+
const svg = html.slice(html.indexOf('<div class="diagram">'));
|
|
155
|
+
// A sibling `<nid>.html`, the same target prose references already use —
|
|
156
|
+
// *not* `brane://nid/…`, which is the app's in-band scheme and opens in no
|
|
157
|
+
// browser, so it would have been a link that does nothing in a download.
|
|
158
|
+
assert.ok(svg.includes(`<a href="${PEER}.html">`), "the real narrative links");
|
|
159
|
+
assert.ok(!svg.includes("brane://nid/"), "no in-app scheme in a downloaded file");
|
|
160
|
+
assert.equal((svg.match(/<a href="[a-z]+-[0-9a-z]{8}\.html/g) ?? []).length, 1, "the narrative that does not exist stays plain text");
|
|
161
|
+
});
|
|
162
|
+
it("draws several fences in one conversation, each with its own marker ids", async () => {
|
|
163
|
+
const db = createTestDb();
|
|
164
|
+
insertNarrative(db, HOST);
|
|
165
|
+
appendEvent(db, HOST, "narration", { content: fence("flowchart TD\n A --> B") });
|
|
166
|
+
appendEvent(db, HOST, "narration", { content: fence("flowchart LR\n C --> D") });
|
|
167
|
+
const html = (await exportNarrativeHtml(db, HOST));
|
|
168
|
+
const ids = [...html.matchAll(/id="ah-([0-9a-f]+)"/g)].map((m) => m[1]);
|
|
169
|
+
assert.equal(ids.length, 2);
|
|
170
|
+
assert.notEqual(ids[0], ids[1], "two diagrams on a page must not share a marker");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe("an author is told when their diagram will not draw", () => {
|
|
174
|
+
it("warns, with the source as the fallback", () => {
|
|
175
|
+
const out = diagnoseMarkdown("Look:\n\n" + fence("gantt\n title X") + "\n\nafter");
|
|
176
|
+
const d = out.find((w) => w.kind === "diagram_unsupported");
|
|
177
|
+
assert.ok(d, "expected a diagram warning");
|
|
178
|
+
assert.match(d.message, /gantt/);
|
|
179
|
+
assert.ok(d.fallback.includes("gantt"), "the fallback is what the reader will see");
|
|
180
|
+
});
|
|
181
|
+
it("distinguishes a syntax error from an unsupported type", () => {
|
|
182
|
+
const bad = diagnoseMarkdown(fence("flowchart TD\n A --> @@@"));
|
|
183
|
+
assert.equal(bad[0]?.kind, "diagram_parse_error");
|
|
184
|
+
});
|
|
185
|
+
it("points at the line inside the document, not inside the fence", () => {
|
|
186
|
+
const md = "line one\nline two\n\n" + fence("flowchart TD\n A --> B\n click A cb");
|
|
187
|
+
const d = diagnoseMarkdown(md).find((w) => w.kind === "diagram_unsupported");
|
|
188
|
+
assert.ok(d);
|
|
189
|
+
// The fence opens on line 4, so its body starts on line 5 and `click` is
|
|
190
|
+
// the third body line: line 7.
|
|
191
|
+
assert.equal(d.line, 7);
|
|
192
|
+
});
|
|
193
|
+
it("says nothing at all about a diagram that draws", () => {
|
|
194
|
+
const out = diagnoseMarkdown(fence("flowchart TD\n A --> B"));
|
|
195
|
+
assert.deepEqual(out.filter((w) => w.kind.startsWith("diagram_")), []);
|
|
196
|
+
});
|
|
197
|
+
it("says nothing about a fence in another language", () => {
|
|
198
|
+
const out = diagnoseMarkdown("```js\nnot a diagram\n```");
|
|
199
|
+
assert.deepEqual(out.filter((w) => w.kind.startsWith("diagram_")), []);
|
|
200
|
+
});
|
|
201
|
+
it("leaves an unclosed fence to the warning that already covers it", () => {
|
|
202
|
+
// `fence_unclosed` already says everything after it became one code block.
|
|
203
|
+
// A second warning saying the diagram did not draw would be noise about a
|
|
204
|
+
// consequence of the first.
|
|
205
|
+
const out = diagnoseMarkdown("```mermaid\nflowchart TD\n A --> B");
|
|
206
|
+
assert.ok(out.some((w) => w.kind === "fence_unclosed"));
|
|
207
|
+
assert.deepEqual(out.filter((w) => w.kind.startsWith("diagram_")), []);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
//# sourceMappingURL=diagram-export.test.js.map
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { parseDiagram } from "../diagram/parse.js";
|
|
4
|
+
import { layoutDiagram } from "../diagram/layout.js";
|
|
5
|
+
import { renderDiagram, diagnoseDiagram } from "../diagram/index.js";
|
|
6
|
+
import { MARKDOWN_WRAP_WIDTH, parseLabel, wrapRuns } from "../diagram/label.js";
|
|
7
|
+
/** A label line that the test knows is text, not a table. */
|
|
8
|
+
const line = (l) => {
|
|
9
|
+
assert.ok(Array.isArray(l), "expected a line of runs, got a table");
|
|
10
|
+
return l;
|
|
11
|
+
};
|
|
12
|
+
import { FONT_SIZE, measureNode, runWidth, textWidth } from "../diagram/metrics.js";
|
|
13
|
+
import { diagnoseMarkdown } from "../markdown-diagnostics.js";
|
|
14
|
+
// Styled text in diagram labels. Mermaid has two label grammars, and before
|
|
15
|
+
// this brane read only `<br/>` from either: an agent on another daemon tried
|
|
16
|
+
// `<b>`, then a markdown string ``"`**Plan**`"``, then a `font-weight`
|
|
17
|
+
// classDef, and drew literal tags, then literal backticks and asterisks, then
|
|
18
|
+
// nothing, with a clean `narrate` receipt each time (claude-cdouqlgt:12–:24).
|
|
19
|
+
const ok = (src) => {
|
|
20
|
+
const r = parseDiagram(src);
|
|
21
|
+
assert.equal(r.ok, true, `expected a parse, got ${r.ok ? "" : r.refusal.message}`);
|
|
22
|
+
return r.ok ? r.ir : undefined;
|
|
23
|
+
};
|
|
24
|
+
describe("markdown strings", () => {
|
|
25
|
+
it("draws **bold** and *italic*, not the asterisks and backticks", () => {
|
|
26
|
+
const ir = ok('flowchart TD\n A["`**Plan** mixin, *maybe*`"] --> B');
|
|
27
|
+
assert.deepEqual(ir.nodes[0].lines, ["Plan mixin, maybe"]);
|
|
28
|
+
assert.deepEqual(ir.nodes[0].runs, [[
|
|
29
|
+
{ text: "Plan", bold: true },
|
|
30
|
+
{ text: " mixin, " },
|
|
31
|
+
{ text: "maybe", italic: true },
|
|
32
|
+
]]);
|
|
33
|
+
assert.deepEqual(ir.literal, []);
|
|
34
|
+
});
|
|
35
|
+
it("spans lines, and each source line is a drawn line", () => {
|
|
36
|
+
const ir = ok([
|
|
37
|
+
"flowchart TD",
|
|
38
|
+
' Plan["`**Plan**',
|
|
39
|
+
" inspectable before it runs`\"]",
|
|
40
|
+
" Plan --> Intent",
|
|
41
|
+
].join("\n"));
|
|
42
|
+
assert.deepEqual(ir.nodes[0].lines, ["Plan", "inspectable before it runs"]);
|
|
43
|
+
assert.equal(line(ir.nodes[0].runs[0])[0].bold, true);
|
|
44
|
+
assert.deepEqual(ir.edges.map((e) => `${e.from}->${e.to}`), ["Plan->Intent"]);
|
|
45
|
+
});
|
|
46
|
+
it("parses the fence that started this exactly as it was written", () => {
|
|
47
|
+
// claude-cdouqlgt:14, abridged: multi-line markdown strings with bold
|
|
48
|
+
// heads, chained and dotted edges, and a classDef.
|
|
49
|
+
const ir = ok([
|
|
50
|
+
"flowchart TD",
|
|
51
|
+
' Plan["`**Plan**',
|
|
52
|
+
' inspectable before it runs`"]',
|
|
53
|
+
"",
|
|
54
|
+
' Intent["`**IntentPlan**',
|
|
55
|
+
" declare(intent)",
|
|
56
|
+
' planner picks the operations`"]',
|
|
57
|
+
' Effect["`**EffectPlan** mixin',
|
|
58
|
+
' ordering, idempotency, rollback`"]',
|
|
59
|
+
"",
|
|
60
|
+
" Plan --> Intent",
|
|
61
|
+
" Plan -.-> Effect",
|
|
62
|
+
" classDef proposed stroke-dasharray: 5 5",
|
|
63
|
+
" class Effect proposed",
|
|
64
|
+
].join("\n"));
|
|
65
|
+
assert.deepEqual(ir.nodes.map((n) => n.id), ["Plan", "Intent", "Effect"]);
|
|
66
|
+
assert.deepEqual(ir.literal, []);
|
|
67
|
+
for (const n of ir.nodes) {
|
|
68
|
+
assert.equal(line(n.runs[0])[0].bold, true, `${n.id} has a bold head`);
|
|
69
|
+
assert.ok(!n.lines.some((l) => /[`*]/.test(l)), `${n.id} draws no markup characters`);
|
|
70
|
+
}
|
|
71
|
+
// Wrapped at mermaid's width: "planner picks the operations" is two lines.
|
|
72
|
+
assert.deepEqual(ir.nodes[1].lines, ["IntentPlan", "declare(intent)", "planner picks the", "operations"]);
|
|
73
|
+
});
|
|
74
|
+
it("refuses an unclosed markdown string, naming the line it opened on", () => {
|
|
75
|
+
const r = parseDiagram('flowchart TD\n A --> B\n C["`never\n closed\n D --> E');
|
|
76
|
+
assert.equal(r.ok, false);
|
|
77
|
+
assert.equal(!r.ok && r.refusal.kind, "parse_error");
|
|
78
|
+
assert.equal(!r.ok && r.refusal.line, 3);
|
|
79
|
+
});
|
|
80
|
+
it("honours inline <b> and <br> inside a markdown string", () => {
|
|
81
|
+
const { runs } = parseLabel('"`a <b>b</b><br>c`"');
|
|
82
|
+
assert.deepEqual(runs, [[{ text: "a " }, { text: "b", bold: true }], [{ text: "c" }]]);
|
|
83
|
+
});
|
|
84
|
+
it("draws inline code in a monospace run", () => {
|
|
85
|
+
const { runs, literal } = parseLabel('"`call `watch()` first`"');
|
|
86
|
+
assert.deepEqual(runs, [[{ text: "call " }, { text: "watch()", code: true }, { text: " first" }]]);
|
|
87
|
+
assert.deepEqual(literal, []);
|
|
88
|
+
});
|
|
89
|
+
it("draws a link as its text, and says it is not followed", () => {
|
|
90
|
+
const { lines, literal } = parseLabel('"`see [the doc](https://x.test)`"');
|
|
91
|
+
assert.deepEqual(lines, ["see the doc"]);
|
|
92
|
+
assert.equal(literal.length, 1);
|
|
93
|
+
});
|
|
94
|
+
it("works in edge labels and subgraph titles", () => {
|
|
95
|
+
const ir = ok([
|
|
96
|
+
"flowchart LR",
|
|
97
|
+
' subgraph S["`**Stage** one`"]',
|
|
98
|
+
" A",
|
|
99
|
+
" end",
|
|
100
|
+
' A -->|"`**yes**`"| B',
|
|
101
|
+
].join("\n"));
|
|
102
|
+
assert.equal(ir.subgraphs[0].title, "Stage one");
|
|
103
|
+
assert.deepEqual(ir.subgraphs[0].titleRuns, [{ text: "Stage", bold: true }, { text: " one" }]);
|
|
104
|
+
assert.equal(ir.edges[0].label, "yes");
|
|
105
|
+
assert.deepEqual(ir.edges[0].labelRuns, [[{ text: "yes", bold: true }]]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("plain labels", () => {
|
|
109
|
+
it("draws <b>, <i>, <code>, <u> and <s> the way mermaid's HTML labels do", () => {
|
|
110
|
+
const { runs, literal } = parseLabel("<b>B</b> <i>I</i> <code>C</code> <u>U</u> <s>S</s>");
|
|
111
|
+
assert.deepEqual(runs, [[
|
|
112
|
+
{ text: "B", bold: true }, { text: " " },
|
|
113
|
+
{ text: "I", italic: true }, { text: " " },
|
|
114
|
+
{ text: "C", code: true }, { text: " " },
|
|
115
|
+
{ text: "U", underline: true }, { text: " " },
|
|
116
|
+
{ text: "S", strike: true },
|
|
117
|
+
]]);
|
|
118
|
+
assert.deepEqual(literal, []);
|
|
119
|
+
});
|
|
120
|
+
it("keeps nesting straight and lets a stray closer close nothing", () => {
|
|
121
|
+
assert.deepEqual(parseLabel("<b><b>x</b> y</b> z").runs, [[{ text: "x y", bold: true }, { text: " z" }]]);
|
|
122
|
+
assert.deepEqual(parseLabel("a</b> b").runs, [[{ text: "a b" }]]);
|
|
123
|
+
});
|
|
124
|
+
it("still splits on <br/> and drops blank lines, as before", () => {
|
|
125
|
+
assert.deepEqual(parseLabel("one<br/>two<br><br/>three").lines, ["one", "two", "three"]);
|
|
126
|
+
});
|
|
127
|
+
it("leaves **x** literal, as mermaid does, and says how to style it", () => {
|
|
128
|
+
const ir = ok("flowchart TD\n A[**Plan** mixin] --> B");
|
|
129
|
+
assert.deepEqual(ir.nodes[0].lines, ["**Plan** mixin"]);
|
|
130
|
+
assert.equal(ir.literal.length, 1);
|
|
131
|
+
assert.match(ir.literal[0].message, /markdown string/);
|
|
132
|
+
assert.equal(ir.literal[0].line, 2);
|
|
133
|
+
});
|
|
134
|
+
it("shows an unknown tag as text, reported once per tag name", () => {
|
|
135
|
+
const { lines, literal } = parseLabel("<span>q</span>");
|
|
136
|
+
assert.deepEqual(lines, ["<span>q</span>"]);
|
|
137
|
+
assert.equal(literal.length, 1);
|
|
138
|
+
});
|
|
139
|
+
it("never wraps", () => {
|
|
140
|
+
const long = "word ".repeat(40).trim();
|
|
141
|
+
assert.equal(parseLabel(long).lines.length, 1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe("font-weight and font-style in style and classDef", () => {
|
|
145
|
+
it("bolds the whole label, and measures it bold", () => {
|
|
146
|
+
const plain = layoutDiagram(ok("flowchart TD\n A[Weighty label]"));
|
|
147
|
+
const bold = layoutDiagram(ok("flowchart TD\n A[Weighty label]\n classDef key font-weight:bold\n class A key"));
|
|
148
|
+
assert.equal(line(bold.nodes[0].runs[0])[0].bold, true);
|
|
149
|
+
assert.ok(bold.nodes[0].width > plain.nodes[0].width, "a bold label gets a wider box");
|
|
150
|
+
});
|
|
151
|
+
it("drops a value the property cannot take", () => {
|
|
152
|
+
const ir = ok("flowchart TD\n A\n style A font-weight:heavy,font-style:italic");
|
|
153
|
+
assert.deepEqual(ir.nodes[0].style, { "font-style": "italic" });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe("measurement", () => {
|
|
157
|
+
it("uses Helvetica-Bold's own widths, not a stretch", () => {
|
|
158
|
+
const w = (text, bold) => runWidth({ text, bold }, 1000);
|
|
159
|
+
assert.equal(w("r", false), 333);
|
|
160
|
+
assert.equal(w("r", true), 389);
|
|
161
|
+
assert.equal(w("W", true), w("W", false)); // 944 in both faces
|
|
162
|
+
assert.equal(runWidth({ text: "abc" }, FONT_SIZE), textWidth("abc", FONT_SIZE));
|
|
163
|
+
});
|
|
164
|
+
it("gives code a monospace advance", () => {
|
|
165
|
+
assert.equal(runWidth({ text: "iiii", code: true }, 1000), runWidth({ text: "MMMM", code: true }, 1000));
|
|
166
|
+
});
|
|
167
|
+
it("measures a node by its runs", () => {
|
|
168
|
+
const plain = measureNode([[{ text: "Standing mandate" }]]);
|
|
169
|
+
const bold = measureNode([[{ text: "Standing mandate", bold: true }]]);
|
|
170
|
+
assert.ok(bold.width > plain.width);
|
|
171
|
+
assert.deepEqual(measureNode(["Standing mandate"]), plain);
|
|
172
|
+
});
|
|
173
|
+
it("wraps at word boundaries and never breaks a word", () => {
|
|
174
|
+
const lines = wrapRuns([{ text: "a ".repeat(60) + "x".repeat(80) }], MARKDOWN_WRAP_WIDTH);
|
|
175
|
+
for (const l of lines.slice(0, -1)) {
|
|
176
|
+
assert.ok(l.reduce((s, r) => s + runWidth(r, FONT_SIZE), 0) <= MARKDOWN_WRAP_WIDTH);
|
|
177
|
+
}
|
|
178
|
+
assert.equal(lines[lines.length - 1][0].text, "x".repeat(80));
|
|
179
|
+
});
|
|
180
|
+
it("keeps a run boundary inside a word from becoming a break", () => {
|
|
181
|
+
const lines = wrapRuns([{ text: "Plan", bold: true }, { text: "s and more" }], 10_000);
|
|
182
|
+
assert.deepEqual(lines, [[{ text: "Plan", bold: true }, { text: "s and more" }]]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe("SVG", () => {
|
|
186
|
+
it("draws a bold run as a bold tspan", () => {
|
|
187
|
+
const svg = renderDiagram('flowchart TD\n A["`**Plan** mixin`"]').svg;
|
|
188
|
+
assert.match(svg, /<tspan font-weight="bold">Plan<\/tspan> mixin/);
|
|
189
|
+
assert.doesNotMatch(svg, /\*\*|`/);
|
|
190
|
+
});
|
|
191
|
+
it("draws an unstyled label exactly as it did before runs", () => {
|
|
192
|
+
const svg = renderDiagram("flowchart TD\n A[Plain]").svg;
|
|
193
|
+
assert.match(svg, /<tspan x="[\d.]+" dy="0">Plain<\/tspan>/);
|
|
194
|
+
});
|
|
195
|
+
it("escapes markup in every grammar", () => {
|
|
196
|
+
for (const label of ['"`**<script>x</script>**`"', "<b><script>x</script></b>", "<img src=x onerror=y>"]) {
|
|
197
|
+
const svg = renderDiagram(`flowchart TD\n A[${label}]`).svg;
|
|
198
|
+
assert.doesNotMatch(svg, /<script|<img/, label);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
it("stacks a multi-line edge label inside one chip", () => {
|
|
202
|
+
const svg = renderDiagram("flowchart TD\n A -->|one<br/>two| B").svg;
|
|
203
|
+
assert.match(svg, />one<\/tspan><tspan x="[\d.]+" dy="[\d.]+">two</);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe("narrate warns iff label markup shows as typed", () => {
|
|
207
|
+
const fence = (body) => "```mermaid\n" + body + "\n```";
|
|
208
|
+
it("warns on **x** in a plain label, with the source line", () => {
|
|
209
|
+
const ds = diagnoseMarkdown(fence("flowchart TD\n A[**Plan**] --> B"));
|
|
210
|
+
assert.deepEqual(ds.map((d) => d.kind), ["diagram_label_literal"]);
|
|
211
|
+
assert.equal(ds[0].line, 3);
|
|
212
|
+
assert.equal(ds[0].fallback, "A[**Plan**] --> B");
|
|
213
|
+
});
|
|
214
|
+
it("is silent when everything draws", () => {
|
|
215
|
+
const ds = diagnoseMarkdown(fence('flowchart TD\n A["`**Plan**`"] --> B[<b>Query</b><br/>plan]'));
|
|
216
|
+
assert.deepEqual(ds, []);
|
|
217
|
+
});
|
|
218
|
+
it("does not let a literal note make a diagram a refusal", () => {
|
|
219
|
+
assert.equal(diagnoseDiagram("flowchart TD\n A[**x**] --> B"), null);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe("label parsing stays linear on hostile input", () => {
|
|
223
|
+
const time = (src) => {
|
|
224
|
+
const t = process.hrtime.bigint();
|
|
225
|
+
parseDiagram(src);
|
|
226
|
+
return Number(process.hrtime.bigint() - t) / 1e6;
|
|
227
|
+
};
|
|
228
|
+
it("an unclosed markdown string followed by many lines", () => {
|
|
229
|
+
assert.ok(time('flowchart TD\n A["`x\n' + "y\n".repeat(5000)) < 500);
|
|
230
|
+
});
|
|
231
|
+
it("a label full of half-tags and asterisks", () => {
|
|
232
|
+
const label = "<b ".repeat(300) + "**a".repeat(150) + "`b".repeat(150);
|
|
233
|
+
assert.ok(time(`flowchart TD\n A[${label}]`) < 500);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe("tables in node labels", () => {
|
|
237
|
+
// Brane-only: mermaid shows "Unsupported markdown: table" here. Allowed by
|
|
238
|
+
// the dialect rule because the fence is still valid mermaid, and because
|
|
239
|
+
// the drawing is static SVG made on the daemon, so the HTML export carries
|
|
240
|
+
// no rendering logic for it (claude-uwaj0umf:33).
|
|
241
|
+
const TABLE = [
|
|
242
|
+
"flowchart LR",
|
|
243
|
+
' K["`**Plan kinds**',
|
|
244
|
+
" | kind | input | standing |",
|
|
245
|
+
" |:---|:---:|---:|",
|
|
246
|
+
" | QueryPlan | operation | no |",
|
|
247
|
+
" | WatchPlan | *intent* | **yes** |",
|
|
248
|
+
"",
|
|
249
|
+
' the spine`"]',
|
|
250
|
+
].join("\n");
|
|
251
|
+
const tableOf = (l) => {
|
|
252
|
+
assert.ok(!Array.isArray(l), "expected a table");
|
|
253
|
+
return l;
|
|
254
|
+
};
|
|
255
|
+
it("parses a GFM table into a bold header, rows and alignment", () => {
|
|
256
|
+
const ir = ok(TABLE);
|
|
257
|
+
const runs = ir.nodes[0].runs;
|
|
258
|
+
assert.equal(runs.length, 3);
|
|
259
|
+
const t = tableOf(runs[1]);
|
|
260
|
+
assert.deepEqual(t.align, ["left", "center", "right"]);
|
|
261
|
+
assert.deepEqual(t.header, [[{ text: "kind", bold: true }], [{ text: "input", bold: true }], [{ text: "standing", bold: true }]]);
|
|
262
|
+
assert.deepEqual(t.rows[1], [[{ text: "WatchPlan" }], [{ text: "intent", italic: true }], [{ text: "yes", bold: true }]]);
|
|
263
|
+
assert.deepEqual(ir.nodes[0].lines, [
|
|
264
|
+
"Plan kinds", "kind | input | standing", "QueryPlan | operation | no", "WatchPlan | intent | yes", "the spine",
|
|
265
|
+
]);
|
|
266
|
+
assert.deepEqual(ir.literal, []);
|
|
267
|
+
});
|
|
268
|
+
it("pads a short row and drops extra cells, as GFM does", () => {
|
|
269
|
+
const t = tableOf(parseLabel('"`| a | b |\n|---|---|\n| 1 |\n| 1 | 2 | 3 |`"', { tables: true }).runs[0]);
|
|
270
|
+
assert.deepEqual(t.rows, [[[{ text: "1" }], []], [[{ text: "1" }], [{ text: "2" }]]]);
|
|
271
|
+
});
|
|
272
|
+
it("sizes the node to the columns and to every row", () => {
|
|
273
|
+
const placed = layoutDiagram(ok(TABLE));
|
|
274
|
+
const node = placed.nodes[0];
|
|
275
|
+
assert.ok(node.height >= 5 * FONT_SIZE * 1.35, "five line slots: title, header, two rows, trailer");
|
|
276
|
+
const flat = measureNode(["kind | input | standing"]);
|
|
277
|
+
assert.ok(node.width > flat.width, "the gaps between columns cost more than ` | `");
|
|
278
|
+
});
|
|
279
|
+
it("draws cells at their column's edge, and a rule under the header", () => {
|
|
280
|
+
const svg = renderDiagram(TABLE).svg;
|
|
281
|
+
assert.match(svg, /text-anchor="end"><tspan font-weight="bold">standing<\/tspan>/);
|
|
282
|
+
assert.match(svg, /text-anchor="middle"><tspan font-weight="bold">input<\/tspan>/);
|
|
283
|
+
assert.match(svg, /text-anchor="start">QueryPlan</);
|
|
284
|
+
assert.match(svg, /<path d="M[\d.]+,[\d.]+h[\d.]+" stroke="[^"]+" stroke-width="1" opacity="0.6"/);
|
|
285
|
+
});
|
|
286
|
+
it("bolds the cells too when the node is bold", () => {
|
|
287
|
+
const placed = layoutDiagram(ok(TABLE + "\n style K font-weight:bold"));
|
|
288
|
+
const t = tableOf(placed.nodes[0].runs[1]);
|
|
289
|
+
assert.ok(t.rows.flat(2).every((r) => r.bold));
|
|
290
|
+
});
|
|
291
|
+
it("shows a table in an edge label as its source, and says so", () => {
|
|
292
|
+
const ir = ok('flowchart LR\n A -->|"`| a | b |\n|---|---|\n| 1 | 2 |`"| B');
|
|
293
|
+
assert.equal(ir.edges[0].label, "| a | b | |---|---| | 1 | 2 |");
|
|
294
|
+
assert.equal(ir.literal.length, 1);
|
|
295
|
+
assert.match(ir.literal[0].message, /only in node labels/);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
describe("plain-label details", () => {
|
|
299
|
+
it("does not warn about ** inside <code>, where it is meant literally", () => {
|
|
300
|
+
const { runs, literal } = parseLabel("<code>**x**</code>");
|
|
301
|
+
assert.deepEqual(runs, [[{ text: "**x**", code: true }]]);
|
|
302
|
+
assert.deepEqual(literal, []);
|
|
303
|
+
});
|
|
304
|
+
it("decodes character references, as mermaid's HTML labels do", () => {
|
|
305
|
+
assert.deepEqual(parseLabel("x < y && a → b → c").lines, ["x < y && a → b → c"]);
|
|
306
|
+
assert.deepEqual(parseLabel("&bogus; � �").lines, ["&bogus; � �"]);
|
|
307
|
+
});
|
|
308
|
+
it("never reads a decoded < as a tag", () => {
|
|
309
|
+
const { runs } = parseLabel("<b>not bold</b>");
|
|
310
|
+
assert.deepEqual(runs, [[{ text: "<b>not bold</b>" }]]);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
//# sourceMappingURL=diagram-labels.test.js.map
|