agentic-mermaid 0.1.1 → 0.2.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/AGENT_NATIVE.md +155 -91
- package/CHANGELOG.md +163 -4
- package/Instructions_for_agents.md +10 -10
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +31 -38
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/hero.png +0 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +73 -1868
- package/dist/agent.js +336 -37325
- package/dist/agentic-mermaid-mcp.js +7 -38399
- package/dist/am.js +33626 -40446
- package/dist/chunk-2P5FKUQC.js +3485 -0
- package/dist/chunk-2S3VSS6I.js +149 -0
- package/dist/chunk-2SILJDW2.js +37 -0
- package/dist/chunk-BJYMBT4I.js +61963 -0
- package/dist/chunk-FDM43XU7.js +615 -0
- package/dist/chunk-MYPDVZCO.js +4702 -0
- package/dist/index-DWPa2MJs.d.ts +6780 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -33014
- package/dist/mcp-cli-5J47WEY3.js +17 -0
- package/docs/README.md +39 -31
- package/docs/agent-api-cookbook.md +44 -35
- package/docs/agent-mutation-policy.md +1 -1
- package/docs/agent-workflow-examples.md +2 -2
- package/docs/api.md +284 -77
- package/docs/ascii.md +26 -16
- package/docs/code-mode-impact.md +214 -0
- package/docs/comparison.md +23 -11
- package/docs/computational-aesthetics-prototype-plan.md +252 -0
- package/docs/config.md +26 -10
- package/docs/custom-fonts.md +265 -0
- package/docs/custom-style-cookbook.md +130 -9
- package/docs/diagram-families.md +48 -23
- package/docs/features.md +59 -52
- package/docs/fork-differences.md +6 -3
- package/docs/getting-started.md +23 -16
- package/docs/issue-derived-test-cases.md +29 -22
- package/docs/mcp-code-mode-rationale.md +13 -6
- package/docs/mcp-http-transport.md +20 -13
- package/docs/mermaid-layout-complaints.md +66 -52
- package/docs/mutation-testing.md +145 -66
- package/docs/quality.md +23 -14
- package/docs/react.md +2 -2
- package/docs/schemas/style-spec.schema.json +1221 -30
- package/docs/style-authoring.md +241 -38
- package/docs/svg-semantic-contract.md +49 -0
- package/docs/testing-strategy.md +166 -58
- package/docs/theming.md +19 -32
- package/examples/styles/catalog.json +54 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
- package/examples/styles/cupertino-prototype.mmd +10 -0
- package/examples/styles/cupertino-prototype.style.json +63 -0
- package/examples/styles/mid-century-report.style.json +2 -2
- package/examples/styles/star-chart-atlas.style.json +2 -1
- package/examples/styles/transit-route-map.style.json +2 -2
- package/examples/styles/vercel-inspired-prototype.mmd +6 -0
- package/examples/styles/vercel-inspired-prototype.style.json +87 -0
- package/package.json +85 -45
- package/server.json +2 -2
- package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
- package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
- package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
- package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
- package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
- package/TODO.md +0 -684
- package/dist/index-wjRjbsP-.d.ts +0 -1102
- package/docs/assets/improvements/box-start-connector.png +0 -0
- package/docs/assets/improvements/comment-dropped-lint.png +0 -0
- package/docs/assets/improvements/er-content-loss.png +0 -0
- package/docs/assets/improvements/fan-in-trunks.png +0 -0
- package/docs/assets/improvements/gantt-family.png +0 -0
- package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
- package/docs/assets/improvements/gantt-verify-seam.png +0 -0
- package/docs/assets/improvements/subgraph-container-edges.png +0 -0
- package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
- package/docs/assets/improvements/wrapper-init-directive.png +0 -0
- package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
- package/docs/assets/style-cookbook/mid-century-report.png +0 -0
- package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
- package/docs/assets/style-cookbook/transit-route-map.png +0 -0
- package/docs/contributing/adding-diagram-types.md +0 -144
- package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
- package/docs/contributing/diagram-family-citizenship.md +0 -82
- package/docs/contributing/harvesting-upstream-tests.md +0 -126
- package/docs/contributing/lessons-learned.md +0 -276
- package/docs/contributing/releasing.md +0 -82
- package/docs/contributing/visual-review-evidence.md +0 -53
- package/docs/design/families/README.md +0 -26
- package/docs/design/families/architecture-beta.md +0 -48
- package/docs/design/families/flowchart-parser-conformance.md +0 -22
- package/docs/design/families/gantt-research.md +0 -137
- package/docs/design/families/gantt.md +0 -329
- package/docs/design/families/journey-agentic-cards-before.png +0 -0
- package/docs/design/families/journey-experience-curve-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
- package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
- package/docs/design/families/journey-migration-parity.md +0 -439
- package/docs/design/families/journey-section-overlap-after.png +0 -0
- package/docs/design/families/journey-section-overlap-before.png +0 -0
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -153
- package/docs/design/families/xychart.md +0 -85
- package/docs/design/system/README.md +0 -45
- package/docs/design/system/abstraction-audit.md +0 -224
- package/docs/design/system/abstraction-recommendations.md +0 -655
- package/docs/design/system/architecture.mmd +0 -29
- package/docs/design/system/architecture.svg +0 -137
- package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
- package/docs/design/system/layout-rubric.md +0 -138
- package/docs/design/system/route-contracts.md +0 -848
- package/docs/design/system/source-preservation-ladder.md +0 -52
- package/docs/design/system/styles-rollout.md +0 -216
- package/docs/design/system/ugly-layouts.md +0 -119
- package/docs/layout-characterization/README.md +0 -245
- package/docs/layout-characterization/contact-sheet-families.md +0 -498
- package/docs/layout-characterization/contact-sheet.md +0 -638
- package/docs/layout-characterization/properties.md +0 -281
- package/docs/layout-characterization/visual-quality.md +0 -160
- package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
- package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
- package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
- package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
- package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/journey.svg +0 -98
- package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
- package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
- package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
- package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
- package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
- package/docs/project/divergences.md +0 -98
- package/docs/project/dogfooding-docs-strategy.md +0 -66
- package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
- package/docs/project/lessons-learned.md +0 -1019
- package/docs/project/remove-role-styling-plan.md +0 -176
- package/examples/agent-improve-auth-flow.ts +0 -206
- package/examples/agent-loop.ts +0 -53
- package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
- package/llms.txt +0 -104
- package/skill-evals/README.md +0 -77
- package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
- package/skill-evals/fixtures/class-payment/input.mmd +0 -4
- package/skill-evals/fixtures/er-orders/input.mmd +0 -10
- package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
- package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
- package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
- package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
- package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
- package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
- package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
- package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
- package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
- package/skill-evals/shared-benchmark.json +0 -510
- package/src/agent/analyze.ts +0 -157
- package/src/agent/apply.ts +0 -146
- package/src/agent/architecture-body.ts +0 -458
- package/src/agent/certificates.ts +0 -110
- package/src/agent/class-body.ts +0 -319
- package/src/agent/core.ts +0 -207
- package/src/agent/create.ts +0 -106
- package/src/agent/describe.ts +0 -368
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -360
- package/src/agent/families-builtin.ts +0 -920
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -832
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/font-coverage.ts +0 -288
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -374
- package/src/agent/label-metrics.ts +0 -28
- package/src/agent/layout-to-rendered.ts +0 -63
- package/src/agent/mutate.ts +0 -97
- package/src/agent/mutation-ops.ts +0 -51
- package/src/agent/op-schema.ts +0 -443
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -171
- package/src/agent/quadrant-body.ts +0 -336
- package/src/agent/quality.ts +0 -363
- package/src/agent/readability-audit.ts +0 -92
- package/src/agent/rendered-route-audit.ts +0 -61
- package/src/agent/sequence-body.ts +0 -345
- package/src/agent/serialize.ts +0 -214
- package/src/agent/state-body.ts +0 -589
- package/src/agent/structural-count.ts +0 -150
- package/src/agent/timeline-body.ts +0 -280
- package/src/agent/trace-log.ts +0 -43
- package/src/agent/types.ts +0 -995
- package/src/agent/verify.ts +0 -603
- package/src/agent/xychart-body.ts +0 -472
- package/src/architecture/config.ts +0 -282
- package/src/architecture/layout.ts +0 -496
- package/src/architecture/parser.ts +0 -375
- package/src/architecture/renderer.ts +0 -611
- package/src/architecture/types.ts +0 -113
- package/src/ascii/ansi.ts +0 -423
- package/src/ascii/architecture.ts +0 -210
- package/src/ascii/canvas.ts +0 -443
- package/src/ascii/class-diagram.ts +0 -697
- package/src/ascii/converter.ts +0 -475
- package/src/ascii/draw.ts +0 -1693
- package/src/ascii/edge-bundling.ts +0 -328
- package/src/ascii/edge-routing.ts +0 -330
- package/src/ascii/er-diagram.ts +0 -439
- package/src/ascii/gantt.ts +0 -276
- package/src/ascii/grid.ts +0 -747
- package/src/ascii/index.ts +0 -196
- package/src/ascii/journey.ts +0 -169
- package/src/ascii/meta.ts +0 -244
- package/src/ascii/multiline-utils.ts +0 -80
- package/src/ascii/pathfinder.ts +0 -301
- package/src/ascii/pie.ts +0 -67
- package/src/ascii/quadrant.ts +0 -158
- package/src/ascii/reverse.ts +0 -216
- package/src/ascii/sequence.ts +0 -499
- package/src/ascii/shapes/circle.ts +0 -27
- package/src/ascii/shapes/corners.ts +0 -143
- package/src/ascii/shapes/diamond.ts +0 -27
- package/src/ascii/shapes/hexagon.ts +0 -27
- package/src/ascii/shapes/index.ts +0 -106
- package/src/ascii/shapes/rectangle.ts +0 -168
- package/src/ascii/shapes/rounded.ts +0 -27
- package/src/ascii/shapes/special.ts +0 -332
- package/src/ascii/shapes/stadium.ts +0 -110
- package/src/ascii/shapes/state.ts +0 -192
- package/src/ascii/shapes/types.ts +0 -73
- package/src/ascii/timeline.ts +0 -141
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- package/src/ascii/wrap.ts +0 -131
- package/src/ascii/xychart.ts +0 -919
- package/src/browser.ts +0 -29
- package/src/class/layout.ts +0 -240
- package/src/class/parser.ts +0 -332
- package/src/class/renderer.ts +0 -638
- package/src/class/types.ts +0 -129
- package/src/cli/agent-instructions.ts +0 -1
- package/src/cli/am-bin.ts +0 -6
- package/src/cli/exit-codes.ts +0 -18
- package/src/cli/index.ts +0 -1132
- package/src/cli/init-agent.ts +0 -147
- package/src/cli/run-entrypoint.ts +0 -10
- package/src/color-resolver.ts +0 -152
- package/src/elk-instance.ts +0 -114
- package/src/er/layout.ts +0 -187
- package/src/er/parser.ts +0 -226
- package/src/er/renderer.ts +0 -792
- package/src/er/types.ts +0 -99
- package/src/family-rubric.ts +0 -425
- package/src/font-manifest.ts +0 -31
- package/src/gantt/layout.ts +0 -365
- package/src/gantt/parser.ts +0 -282
- package/src/gantt/pipeline.ts +0 -27
- package/src/gantt/renderer.ts +0 -414
- package/src/gantt/schedule.ts +0 -513
- package/src/gantt/types.ts +0 -255
- package/src/index.ts +0 -368
- package/src/journey/layout.ts +0 -554
- package/src/journey/parse-core.ts +0 -251
- package/src/journey/parser.ts +0 -101
- package/src/journey/renderer.ts +0 -917
- package/src/journey/types.ts +0 -144
- package/src/layout/geometry.ts +0 -182
- package/src/layout/pass.ts +0 -142
- package/src/layout/passes/index.ts +0 -3916
- package/src/layout-engine.ts +0 -1565
- package/src/layout-rubric.ts +0 -659
- package/src/mcp/artifacts.ts +0 -137
- package/src/mcp/dynamic-harness.ts +0 -31
- package/src/mcp/dynamic-modules.d.ts +0 -7
- package/src/mcp/facade.ts +0 -608
- package/src/mcp/harness-runtime.ts +0 -203
- package/src/mcp/hosted-server.ts +0 -468
- package/src/mcp/mcp-bin.ts +0 -3
- package/src/mcp/mcp-cli.ts +0 -82
- package/src/mcp/protocol.ts +0 -13
- package/src/mcp/sandbox.ts +0 -195
- package/src/mcp/sdk-decl.ts +0 -391
- package/src/mcp/server.ts +0 -362
- package/src/mcp/tool-surface.ts +0 -146
- package/src/mermaid-source.ts +0 -655
- package/src/multiline-utils.ts +0 -228
- package/src/parser.ts +0 -1031
- package/src/pie/layout.ts +0 -168
- package/src/pie/parser.ts +0 -136
- package/src/pie/renderer.ts +0 -234
- package/src/pie/types.ts +0 -82
- package/src/quadrant/layout.ts +0 -191
- package/src/quadrant/parser.ts +0 -183
- package/src/quadrant/renderer.ts +0 -322
- package/src/quadrant/types.ts +0 -113
- package/src/render-family-hooks.ts +0 -244
- package/src/renderer.ts +0 -1099
- package/src/route-contracts.ts +0 -2723
- package/src/scene/backend.ts +0 -76
- package/src/scene/fidelity.ts +0 -183
- package/src/scene/hybrid-backend.ts +0 -174
- package/src/scene/ir.ts +0 -181
- package/src/scene/marks.ts +0 -127
- package/src/scene/rough-backend.ts +0 -378
- package/src/scene/seed.ts +0 -46
- package/src/scene/style-registry.ts +0 -556
- package/src/sequence/layout.ts +0 -456
- package/src/sequence/parser.ts +0 -255
- package/src/sequence/renderer.ts +0 -604
- package/src/sequence/types.ts +0 -154
- package/src/shape-clipping.ts +0 -351
- package/src/shared/accessibility-directives.ts +0 -22
- package/src/shared/batched.ts +0 -61
- package/src/shared/color-math.ts +0 -93
- package/src/shared/svg-a11y.ts +0 -29
- package/src/shared/syntax-error.ts +0 -17
- package/src/shared/unicode-ranges.ts +0 -135
- package/src/styles.ts +0 -318
- package/src/svg-paths.ts +0 -28
- package/src/text-metrics.ts +0 -248
- package/src/theme.ts +0 -710
- package/src/timeline/layout.ts +0 -381
- package/src/timeline/parser.ts +0 -215
- package/src/timeline/renderer.ts +0 -538
- package/src/timeline/types.ts +0 -105
- package/src/types.ts +0 -462
- package/src/website-worker-modules.d.ts +0 -18
- package/src/xychart/axis-utils.ts +0 -109
- package/src/xychart/colors.ts +0 -124
- package/src/xychart/config.ts +0 -88
- package/src/xychart/layout.ts +0 -656
- package/src/xychart/parser.ts +0 -504
- package/src/xychart/renderer.ts +0 -640
- package/src/xychart/types.ts +0 -282
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// XY chart structured body (BUILD-16: promotes xychart from opaque-only
|
|
3
|
-
// fallback semantics to structured mutation, following the BUILD-15 journey and
|
|
4
|
-
// BUILD-17 architecture pilots).
|
|
5
|
-
//
|
|
6
|
-
// Modeled grammar (mirrors the legacy renderer parser, src/xychart/parser.ts):
|
|
7
|
-
// xychart-beta | xychart [horizontal | vertical]
|
|
8
|
-
// title <text>
|
|
9
|
-
// x-axis [<cat>, <cat>, …] — categorical
|
|
10
|
-
// x-axis <name> [<cat>, …] — named categorical
|
|
11
|
-
// x-axis <min> --> <max> — numeric range
|
|
12
|
-
// x-axis <name> <min> --> <max> — named range
|
|
13
|
-
// y-axis <name> — name only
|
|
14
|
-
// y-axis <min> --> <max> — range only
|
|
15
|
-
// y-axis <name> <min> --> <max> — named range
|
|
16
|
-
// bar [<n>, …] / bar <name> [<n>, …]
|
|
17
|
-
// line [<n>, …] / line <name> [<n>, …]
|
|
18
|
-
//
|
|
19
|
-
// Structured-or-opaque: any other non-blank, non-comment line (accTitle,
|
|
20
|
-
// accDescr, multi-statement `;` lines, quoted text, weird tokens) returns null
|
|
21
|
-
// so the caller falls back to a lossless opaque body. We deliberately model
|
|
22
|
-
// ONLY bare (unquoted) text for titles, axis names, series names, and
|
|
23
|
-
// categories so the serializer's canonical output re-parses identically under
|
|
24
|
-
// the legacy parser (no quoting/escaping round-trip hazards). Render support is
|
|
25
|
-
// unchanged — the legacy renderer keeps parsing the canonical source we emit.
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
import { unknownOpMessage } from './mutation-ops.ts'
|
|
29
|
-
import type {
|
|
30
|
-
XyChartBody, XyChartAxis, XyChartSeries, XyChartMutationOp,
|
|
31
|
-
MutationError, Result, LayoutWarning, VerifyOptions,
|
|
32
|
-
} from './types.ts'
|
|
33
|
-
import { ok, err, DEFAULT_LABEL_CHAR_CAP } from './types.ts'
|
|
34
|
-
import { labelOverflowWarning } from './label-metrics.ts'
|
|
35
|
-
|
|
36
|
-
// ---- Number format ----------------------------------------------------------
|
|
37
|
-
//
|
|
38
|
-
// Canonical number format is `String(n)` for finite n. JS `String(number)`
|
|
39
|
-
// emits the shortest round-tripping decimal, and the legacy parser reads values
|
|
40
|
-
// with `parseFloat`, so `parseFloat(String(n)) === n` for every finite n
|
|
41
|
-
// (0.1 → "0.1", -5 → "-5", 2.5 → "2.5", 42 → "42"). We reject non-finite
|
|
42
|
-
// (NaN / Infinity) at every entry point so a structured body never serializes a
|
|
43
|
-
// token the legacy parser would turn into NaN.
|
|
44
|
-
|
|
45
|
-
function formatNumber(n: number): string {
|
|
46
|
-
return String(n)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function isFiniteNumber(n: unknown): n is number {
|
|
50
|
-
return typeof n === 'number' && Number.isFinite(n)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---- Parser -----------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
const NUMBER = String.raw`[+-]?(?:\d+(?:\.\d+)?|\.\d+)`
|
|
56
|
-
const RANGE_RE = new RegExp(`^(${NUMBER})\\s*-->\\s*(${NUMBER})$`)
|
|
57
|
-
const HEADER_RE = /^xychart(?:-beta)?(?:\s+(horizontal|vertical))?$/i
|
|
58
|
-
|
|
59
|
-
// A bare text token must not introduce quoting, brackets, the range operator,
|
|
60
|
-
// or statement separators — anything that wouldn't round-trip through the
|
|
61
|
-
// legacy parser when re-emitted unquoted.
|
|
62
|
-
const BARE_TEXT_RE = /^[^"'\[\];]+$/
|
|
63
|
-
|
|
64
|
-
function normalizeText(value: string): string {
|
|
65
|
-
return value.split(/\r?\n/).map(part => part.trim()).filter(Boolean).join(' ')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** A bare text token is non-empty, has no quote/bracket/semicolon, and doesn't
|
|
69
|
-
* contain the `-->` range operator (which would re-parse as a range). */
|
|
70
|
-
function isBareText(value: string): boolean {
|
|
71
|
-
return value.length > 0 && BARE_TEXT_RE.test(value) && !value.includes('-->')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Strip a single layer of matching surrounding quotes. Mermaid's xychart
|
|
75
|
-
* syntax quotes text (and the family's own example does), so a model that
|
|
76
|
-
* quotes a title/axis/series name must not silently drop the whole chart to an
|
|
77
|
-
* opaque body. We accept quoted text whose inner content is otherwise bare and
|
|
78
|
-
* serialize it back canonically (unquoted); quoted text that still isn't bare
|
|
79
|
-
* after unquoting (embedded quotes/brackets/`-->`) legitimately stays opaque. */
|
|
80
|
-
function unquote(value: string): string {
|
|
81
|
-
const v = value.trim()
|
|
82
|
-
if (v.length >= 2 && ((v[0] === '"' && v.endsWith('"')) || (v[0] === "'" && v.endsWith("'")))) {
|
|
83
|
-
return v.slice(1, -1).trim()
|
|
84
|
-
}
|
|
85
|
-
return v
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Unquote then validate as bare text; returns the bare token or null. */
|
|
89
|
-
function bareToken(value: string): string | null {
|
|
90
|
-
const t = unquote(value)
|
|
91
|
-
return isBareText(t) ? t : null
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function parseNumber(token: string): number | null {
|
|
95
|
-
if (!new RegExp(`^${NUMBER}$`).test(token)) return null
|
|
96
|
-
const n = Number.parseFloat(token)
|
|
97
|
-
return Number.isFinite(n) ? n : null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Parse a `[a, b, c]` category list into bare tokens, or null if any token
|
|
101
|
-
* isn't bare (quoted/empty/bracketed categories fall back to opaque). */
|
|
102
|
-
function parseCategories(inner: string): string[] | null {
|
|
103
|
-
const parts = inner.split(',').map(p => p.trim())
|
|
104
|
-
const out: string[] = []
|
|
105
|
-
for (const p of parts) {
|
|
106
|
-
const t = bareToken(p)
|
|
107
|
-
if (t === null) return null
|
|
108
|
-
out.push(t)
|
|
109
|
-
}
|
|
110
|
-
return out.length > 0 ? out : null
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Parse the value portion of an `x-axis`/`y-axis` directive. axisName === 'x'
|
|
114
|
-
* additionally accepts categorical forms. Returns the axis or null. */
|
|
115
|
-
function parseAxis(rawValue: string, axisName: 'x' | 'y'): XyChartAxis | null {
|
|
116
|
-
const value = rawValue.trim()
|
|
117
|
-
if (value.length === 0) return null
|
|
118
|
-
|
|
119
|
-
// Categorical: `[…]` or `<name> [...]` (x-axis only).
|
|
120
|
-
const catMatch = value.match(/^(.*?)\[([^\[\]]*)\]$/)
|
|
121
|
-
if (catMatch && axisName === 'x') {
|
|
122
|
-
const namePart = catMatch[1]!.trim()
|
|
123
|
-
const categories = parseCategories(catMatch[2]!)
|
|
124
|
-
if (!categories) return null
|
|
125
|
-
if (namePart.length > 0) {
|
|
126
|
-
const name = bareToken(namePart)
|
|
127
|
-
if (name === null) return null
|
|
128
|
-
return { name, categories }
|
|
129
|
-
}
|
|
130
|
-
return { categories }
|
|
131
|
-
}
|
|
132
|
-
if (catMatch) return null // y-axis cannot be categorical
|
|
133
|
-
|
|
134
|
-
// Range only: `<min> --> <max>`.
|
|
135
|
-
const rangeOnly = value.match(RANGE_RE)
|
|
136
|
-
if (rangeOnly) {
|
|
137
|
-
const min = parseNumber(rangeOnly[1]!)
|
|
138
|
-
const max = parseNumber(rangeOnly[2]!)
|
|
139
|
-
if (min === null || max === null) return null
|
|
140
|
-
return { range: { min, max } }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Named range: `<name> <min> --> <max>`.
|
|
144
|
-
const arrowAt = value.indexOf('-->')
|
|
145
|
-
if (arrowAt >= 0) {
|
|
146
|
-
// Split the leading name from `<min> --> <max>`: the min is the last token
|
|
147
|
-
// before the arrow.
|
|
148
|
-
const before = value.slice(0, arrowAt).trim()
|
|
149
|
-
const after = value.slice(arrowAt + 3).trim()
|
|
150
|
-
const max = parseNumber(after)
|
|
151
|
-
const lastSpace = before.lastIndexOf(' ')
|
|
152
|
-
if (lastSpace < 0 || max === null) return null
|
|
153
|
-
const name = bareToken(before.slice(0, lastSpace).trim())
|
|
154
|
-
const min = parseNumber(before.slice(lastSpace + 1).trim())
|
|
155
|
-
if (min === null || name === null) return null
|
|
156
|
-
return { name, range: { min, max } }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Name only (no range, no categories) — legacy accepts this for either axis
|
|
160
|
-
// (y-axis name only, x-axis title only).
|
|
161
|
-
const nameOnly = bareToken(value)
|
|
162
|
-
if (nameOnly !== null) return { name: nameOnly }
|
|
163
|
-
return null
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const SERIES_RE = /^(bar|line)(?:\s+(.+?))?\s+\[([^\[\]]*)\]$/
|
|
167
|
-
|
|
168
|
-
function parseSeries(line: string, idx: number): XyChartSeries | null {
|
|
169
|
-
const m = line.match(SERIES_RE)
|
|
170
|
-
if (!m) return null
|
|
171
|
-
const kind = m[1] as 'bar' | 'line'
|
|
172
|
-
const namePart = m[2]?.trim()
|
|
173
|
-
let name: string | undefined
|
|
174
|
-
if (namePart !== undefined && namePart.length > 0) {
|
|
175
|
-
const t = bareToken(namePart)
|
|
176
|
-
if (t === null) return null
|
|
177
|
-
name = t
|
|
178
|
-
}
|
|
179
|
-
const values: number[] = []
|
|
180
|
-
for (const tok of m[3]!.split(',').map(t => t.trim())) {
|
|
181
|
-
if (tok.length === 0) return null
|
|
182
|
-
const n = parseNumber(tok)
|
|
183
|
-
if (n === null) return null
|
|
184
|
-
values.push(n)
|
|
185
|
-
}
|
|
186
|
-
if (values.length === 0) return null
|
|
187
|
-
return { id: `series-${idx}`, kind, name, values }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Parse xychart body lines (header excluded). Returns a structured body only if
|
|
192
|
-
* EVERY non-blank, non-comment line is a modeled title / x-axis / y-axis /
|
|
193
|
-
* series directive using bare text and finite numbers. Otherwise returns null
|
|
194
|
-
* (opaque fallback). A structured body must contain at least one series (the
|
|
195
|
-
* legacy renderer needs data to render).
|
|
196
|
-
*/
|
|
197
|
-
export function parseXyChartBody(lines: string[]): XyChartBody | null {
|
|
198
|
-
const body: XyChartBody = { kind: 'xychart', series: [] }
|
|
199
|
-
let sIdx = 0
|
|
200
|
-
|
|
201
|
-
for (const raw of lines) {
|
|
202
|
-
const line = raw.trim()
|
|
203
|
-
if (!line) continue
|
|
204
|
-
if (line.startsWith('%%')) continue
|
|
205
|
-
|
|
206
|
-
const titleMatch = line.match(/^title\s+(.+)$/)
|
|
207
|
-
if (titleMatch) {
|
|
208
|
-
const title = bareToken(normalizeText(titleMatch[1]!))
|
|
209
|
-
if (title === null) return null
|
|
210
|
-
body.title = title
|
|
211
|
-
continue
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const xMatch = line.match(/^x-axis\s+(.+)$/)
|
|
215
|
-
if (xMatch) {
|
|
216
|
-
const axis = parseAxis(xMatch[1]!, 'x')
|
|
217
|
-
if (!axis) return null
|
|
218
|
-
body.xAxis = axis
|
|
219
|
-
continue
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const yMatch = line.match(/^y-axis\s+(.+)$/)
|
|
223
|
-
if (yMatch) {
|
|
224
|
-
const axis = parseAxis(yMatch[1]!, 'y')
|
|
225
|
-
if (!axis) return null
|
|
226
|
-
body.yAxis = axis
|
|
227
|
-
continue
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (/^(bar|line)\b/.test(line)) {
|
|
231
|
-
const series = parseSeries(line, sIdx)
|
|
232
|
-
if (!series) return null
|
|
233
|
-
body.series.push(series)
|
|
234
|
-
sIdx++
|
|
235
|
-
continue
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Unmodeled line (accTitle/accDescr/quoted/`;`-joined/anything else) → opaque.
|
|
239
|
-
return null
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// The legacy renderer needs at least one data series to render; model the
|
|
243
|
-
// same floor so a structured body always renders.
|
|
244
|
-
if (body.series.length === 0) return null
|
|
245
|
-
|
|
246
|
-
return body
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ---- Serializer -------------------------------------------------------------
|
|
250
|
-
|
|
251
|
-
function renderAxis(keyword: 'x-axis' | 'y-axis', axis: XyChartAxis): string {
|
|
252
|
-
const parts: string[] = []
|
|
253
|
-
if (axis.name !== undefined) parts.push(axis.name)
|
|
254
|
-
if (axis.range) parts.push(`${formatNumber(axis.range.min)} --> ${formatNumber(axis.range.max)}`)
|
|
255
|
-
if (axis.categories) parts.push(`[${axis.categories.join(', ')}]`)
|
|
256
|
-
return ` ${keyword} ${parts.join(' ')}`
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function renderXyChart(body: XyChartBody): string {
|
|
260
|
-
const lines: string[] = [body.horizontal ? 'xychart-beta horizontal' : 'xychart-beta']
|
|
261
|
-
if (body.title !== undefined) lines.push(` title ${body.title}`)
|
|
262
|
-
if (body.xAxis) lines.push(renderAxis('x-axis', body.xAxis))
|
|
263
|
-
if (body.yAxis) lines.push(renderAxis('y-axis', body.yAxis))
|
|
264
|
-
for (const s of body.series) {
|
|
265
|
-
const namePart = s.name !== undefined ? `${s.name} ` : ''
|
|
266
|
-
lines.push(` ${s.kind} ${namePart}[${s.values.map(formatNumber).join(', ')}]`)
|
|
267
|
-
}
|
|
268
|
-
return lines.join('\n') + '\n'
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ---- Mutator ----------------------------------------------------------------
|
|
272
|
-
|
|
273
|
-
function cloneAxis(a: XyChartAxis): XyChartAxis {
|
|
274
|
-
return {
|
|
275
|
-
name: a.name,
|
|
276
|
-
categories: a.categories ? [...a.categories] : undefined,
|
|
277
|
-
range: a.range ? { ...a.range } : undefined,
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function cloneXyChart(b: XyChartBody): XyChartBody {
|
|
282
|
-
return {
|
|
283
|
-
kind: 'xychart',
|
|
284
|
-
title: b.title,
|
|
285
|
-
horizontal: b.horizontal,
|
|
286
|
-
xAxis: b.xAxis ? cloneAxis(b.xAxis) : undefined,
|
|
287
|
-
yAxis: b.yAxis ? cloneAxis(b.yAxis) : undefined,
|
|
288
|
-
series: b.series.map(s => ({ id: s.id, kind: s.kind, name: s.name, values: [...s.values] })),
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function makeSeriesIdAllocator(body: XyChartBody): () => string {
|
|
293
|
-
const seen = new Set(body.series.map(s => s.id))
|
|
294
|
-
return () => {
|
|
295
|
-
let n = 0
|
|
296
|
-
while (seen.has(`series-${n}`)) n++
|
|
297
|
-
const id = `series-${n}`
|
|
298
|
-
seen.add(id)
|
|
299
|
-
return id
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function validBareText(value: unknown, field: string): Result<string, MutationError> {
|
|
304
|
-
if (typeof value !== 'string') return err({ code: 'INVALID_OP', message: `XY chart ${field} must be a string` })
|
|
305
|
-
const normalized = normalizeText(value)
|
|
306
|
-
if (!isBareText(normalized)) {
|
|
307
|
-
return err({ code: 'INVALID_OP', message: `XY chart ${field} must be non-empty bare text (no quotes, brackets, ; or -->)` })
|
|
308
|
-
}
|
|
309
|
-
return ok(normalized)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function validValues(values: unknown, field: string): Result<number[], MutationError> {
|
|
313
|
-
if (!Array.isArray(values) || values.length === 0) {
|
|
314
|
-
return err({ code: 'INVALID_OP', message: `XY chart ${field} must be a non-empty array of numbers` })
|
|
315
|
-
}
|
|
316
|
-
for (const v of values) {
|
|
317
|
-
if (!isFiniteNumber(v)) return err({ code: 'INVALID_OP', message: `XY chart ${field} must contain only finite numbers, got ${JSON.stringify(v)}` })
|
|
318
|
-
}
|
|
319
|
-
return ok([...values as number[]])
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** Build an axis from a structured op payload (categories XOR range, both
|
|
323
|
-
* optional name). Validates bare text + finite numbers. */
|
|
324
|
-
function buildAxis(
|
|
325
|
-
spec: { name?: string | null; categories?: string[]; range?: { min: number; max: number } },
|
|
326
|
-
axisName: 'x' | 'y',
|
|
327
|
-
): Result<XyChartAxis, MutationError> {
|
|
328
|
-
const axis: XyChartAxis = {}
|
|
329
|
-
if (spec.name !== undefined && spec.name !== null) {
|
|
330
|
-
const n = validBareText(spec.name, `${axisName}-axis name`)
|
|
331
|
-
if (!n.ok) return n
|
|
332
|
-
axis.name = n.value
|
|
333
|
-
}
|
|
334
|
-
const hasCategories = spec.categories !== undefined
|
|
335
|
-
const hasRange = spec.range !== undefined
|
|
336
|
-
if (hasCategories && hasRange) {
|
|
337
|
-
return err({ code: 'INVALID_OP', message: `XY chart ${axisName}-axis cannot have both categories and a range` })
|
|
338
|
-
}
|
|
339
|
-
if (hasCategories) {
|
|
340
|
-
if (axisName === 'y') return err({ code: 'INVALID_OP', message: 'XY chart y-axis cannot be categorical' })
|
|
341
|
-
if (!Array.isArray(spec.categories) || spec.categories.length === 0) {
|
|
342
|
-
return err({ code: 'INVALID_OP', message: 'XY chart x-axis categories must be a non-empty array' })
|
|
343
|
-
}
|
|
344
|
-
const cats: string[] = []
|
|
345
|
-
for (const c of spec.categories) {
|
|
346
|
-
const r = validBareText(c, 'x-axis category')
|
|
347
|
-
if (!r.ok) return r
|
|
348
|
-
cats.push(r.value)
|
|
349
|
-
}
|
|
350
|
-
axis.categories = cats
|
|
351
|
-
}
|
|
352
|
-
if (hasRange) {
|
|
353
|
-
const { min, max } = spec.range!
|
|
354
|
-
if (!isFiniteNumber(min) || !isFiniteNumber(max)) {
|
|
355
|
-
return err({ code: 'INVALID_OP', message: `XY chart ${axisName}-axis range min/max must be finite numbers` })
|
|
356
|
-
}
|
|
357
|
-
axis.range = { min, max }
|
|
358
|
-
}
|
|
359
|
-
if (axis.name === undefined && !hasCategories && !hasRange) {
|
|
360
|
-
return err({ code: 'INVALID_OP', message: `XY chart ${axisName}-axis needs a name, categories, or a range` })
|
|
361
|
-
}
|
|
362
|
-
return ok(axis)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export function mutateXyChart(body: XyChartBody, op: XyChartMutationOp): Result<XyChartBody, MutationError> {
|
|
366
|
-
const next = cloneXyChart(body)
|
|
367
|
-
const nextId = makeSeriesIdAllocator(next)
|
|
368
|
-
|
|
369
|
-
switch (op.kind) {
|
|
370
|
-
case 'set_title': {
|
|
371
|
-
if (op.title === null) { delete next.title; break }
|
|
372
|
-
const t = validBareText(op.title, 'title')
|
|
373
|
-
if (!t.ok) return t
|
|
374
|
-
next.title = t.value
|
|
375
|
-
break
|
|
376
|
-
}
|
|
377
|
-
case 'set_x_axis': {
|
|
378
|
-
if (op.axis === null) { delete next.xAxis; break }
|
|
379
|
-
const axis = buildAxis(op.axis, 'x')
|
|
380
|
-
if (!axis.ok) return axis
|
|
381
|
-
next.xAxis = axis.value
|
|
382
|
-
break
|
|
383
|
-
}
|
|
384
|
-
case 'set_y_axis': {
|
|
385
|
-
if (op.axis === null) { delete next.yAxis; break }
|
|
386
|
-
const axis = buildAxis(op.axis, 'y')
|
|
387
|
-
if (!axis.ok) return axis
|
|
388
|
-
next.yAxis = axis.value
|
|
389
|
-
break
|
|
390
|
-
}
|
|
391
|
-
case 'add_series': {
|
|
392
|
-
if (op.kind2 !== 'bar' && op.kind2 !== 'line') {
|
|
393
|
-
return err({ code: 'INVALID_OP', message: `XY chart series kind must be 'bar' or 'line', got ${JSON.stringify(op.kind2)}` })
|
|
394
|
-
}
|
|
395
|
-
let name: string | undefined
|
|
396
|
-
if (op.name !== undefined && op.name !== null) {
|
|
397
|
-
const n = validBareText(op.name, 'series name')
|
|
398
|
-
if (!n.ok) return n
|
|
399
|
-
name = n.value
|
|
400
|
-
}
|
|
401
|
-
const values = validValues(op.values, 'series values')
|
|
402
|
-
if (!values.ok) return values
|
|
403
|
-
next.series.push({ id: nextId(), kind: op.kind2, name, values: values.value })
|
|
404
|
-
break
|
|
405
|
-
}
|
|
406
|
-
case 'remove_series': {
|
|
407
|
-
if (!next.series[op.index]) return err({ code: 'SERIES_NOT_FOUND', message: `No series at index ${op.index}` })
|
|
408
|
-
next.series.splice(op.index, 1)
|
|
409
|
-
break
|
|
410
|
-
}
|
|
411
|
-
case 'set_series_values': {
|
|
412
|
-
const s = next.series[op.index]
|
|
413
|
-
if (!s) return err({ code: 'SERIES_NOT_FOUND', message: `No series at index ${op.index}` })
|
|
414
|
-
const values = validValues(op.values, 'series values')
|
|
415
|
-
if (!values.ok) return values
|
|
416
|
-
s.values = values.value
|
|
417
|
-
break
|
|
418
|
-
}
|
|
419
|
-
case 'set_series_name': {
|
|
420
|
-
const s = next.series[op.index]
|
|
421
|
-
if (!s) return err({ code: 'SERIES_NOT_FOUND', message: `No series at index ${op.index}` })
|
|
422
|
-
if (op.name === null) { delete s.name; break }
|
|
423
|
-
const n = validBareText(op.name, 'series name')
|
|
424
|
-
if (!n.ok) return n
|
|
425
|
-
s.name = n.value
|
|
426
|
-
break
|
|
427
|
-
}
|
|
428
|
-
case 'reorder_series': {
|
|
429
|
-
const { from, to } = op
|
|
430
|
-
if (!Number.isInteger(from) || !Number.isInteger(to) || !next.series[from] || to < 0 || to >= next.series.length) {
|
|
431
|
-
return err({ code: 'SERIES_NOT_FOUND', message: `reorder_series indices out of range (from=${from}, to=${to})` })
|
|
432
|
-
}
|
|
433
|
-
const [moved] = next.series.splice(from, 1)
|
|
434
|
-
next.series.splice(to, 0, moved!)
|
|
435
|
-
break
|
|
436
|
-
}
|
|
437
|
-
default: {
|
|
438
|
-
const _x: never = op
|
|
439
|
-
return err({ code: 'INVALID_OP', message: unknownOpMessage('xychart', _x) })
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Preserve the structured floor: an xychart must keep at least one series so
|
|
444
|
-
// it always renders. Only mutations that EMPTY a non-empty chart are refused;
|
|
445
|
-
// a body that starts empty (createMermaid/buildMermaid) may stay empty while
|
|
446
|
-
// ops build it up — title/axes first, series after.
|
|
447
|
-
if (body.series.length > 0 && next.series.length === 0) {
|
|
448
|
-
return err({ code: 'INVALID_OP', message: 'XY chart must keep at least one series' })
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return ok(next)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// ---- Verifier (FamilyPlugin.verify hook) ------------------------------------
|
|
455
|
-
|
|
456
|
-
export function verifyXyChart(body: XyChartBody, opts: VerifyOptions): LayoutWarning[] {
|
|
457
|
-
const cap = opts.labelCharCap ?? DEFAULT_LABEL_CHAR_CAP
|
|
458
|
-
const warnings: LayoutWarning[] = []
|
|
459
|
-
const overflow = (target: string, text: string) => {
|
|
460
|
-
const w = labelOverflowWarning(target, text, cap)
|
|
461
|
-
if (w) warnings.push(w)
|
|
462
|
-
}
|
|
463
|
-
if (body.series.length === 0) warnings.push({ code: 'EMPTY_DIAGRAM' })
|
|
464
|
-
if (body.title !== undefined) overflow('title', body.title)
|
|
465
|
-
if (body.xAxis?.name !== undefined) overflow('x-axis', body.xAxis.name)
|
|
466
|
-
for (const c of body.xAxis?.categories ?? []) overflow('x-axis', c)
|
|
467
|
-
if (body.yAxis?.name !== undefined) overflow('y-axis', body.yAxis.name)
|
|
468
|
-
for (const s of body.series) {
|
|
469
|
-
if (s.name !== undefined) overflow(s.id, s.name)
|
|
470
|
-
}
|
|
471
|
-
return warnings
|
|
472
|
-
}
|