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
package/src/gantt/layout.ts
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Gantt layout — schedule → deterministic geometry (docs/design/families/gantt.md §4).
|
|
3
|
-
//
|
|
4
|
-
// One layout model feeds both renderers: the SVG renderer consumes the pixel
|
|
5
|
-
// geometry directly; the ASCII renderer reuses tick *selection* (the resolved
|
|
6
|
-
// tick instants) and the row model with its own column math. Bars never
|
|
7
|
-
// leave the plot area; `vert` markers consume no task row; compact mode packs
|
|
8
|
-
// non-overlapping tasks of a section into shared rows deterministically
|
|
9
|
-
// (first-fit by source order).
|
|
10
|
-
// ============================================================================
|
|
11
|
-
|
|
12
|
-
import type {
|
|
13
|
-
GanttModel, GanttSchedule, GanttLayoutResult, GanttBarLayout, GanttRowLayout,
|
|
14
|
-
GanttSectionBand, GanttVertLayout, GanttTick, GanttTickUnit, EpochMs,
|
|
15
|
-
} from './types.ts'
|
|
16
|
-
import { DAY_MS, dayOfWeek, WEEKDAY_INDEX, formatGanttInstant, startOfDay } from './schedule.ts'
|
|
17
|
-
import { applyTextTransform, estimateTextWidth, resolveRenderStyle, STROKE_WIDTHS } from '../styles.ts'
|
|
18
|
-
import type { RenderStyleDefaults, ResolvedRenderStyle } from '../styles.ts'
|
|
19
|
-
import type { RenderOptions } from '../types.ts'
|
|
20
|
-
|
|
21
|
-
export const GANTT_MAX_TICKS = 120
|
|
22
|
-
|
|
23
|
-
export interface GanttLayoutOptions {
|
|
24
|
-
/** Total drawing width; the plot shrinks to fit. */
|
|
25
|
-
width?: number
|
|
26
|
-
compact?: boolean
|
|
27
|
-
barHeight?: number
|
|
28
|
-
/** Resolved clock instant (schedule.today). */
|
|
29
|
-
today?: EpochMs
|
|
30
|
-
/** Render options whose style roles affect label and axis geometry. */
|
|
31
|
-
renderOptions?: RenderOptions
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const GL = {
|
|
35
|
-
plotWidth: 600,
|
|
36
|
-
padding: 16,
|
|
37
|
-
titleHeight: 34,
|
|
38
|
-
axisHeight: 24,
|
|
39
|
-
rowGap: 8,
|
|
40
|
-
sectionGap: 6,
|
|
41
|
-
barHeight: 22,
|
|
42
|
-
labelGap: 12,
|
|
43
|
-
labelFontSize: 13,
|
|
44
|
-
sectionFontSize: 13,
|
|
45
|
-
} as const
|
|
46
|
-
|
|
47
|
-
export const GANTT_STYLE_DEFAULTS: RenderStyleDefaults = {
|
|
48
|
-
nodeLabelFontSize: GL.labelFontSize,
|
|
49
|
-
edgeLabelFontSize: 11,
|
|
50
|
-
groupHeaderFontSize: GL.sectionFontSize,
|
|
51
|
-
nodeLabelFontWeight: 500,
|
|
52
|
-
edgeLabelFontWeight: 500,
|
|
53
|
-
groupHeaderFontWeight: 600,
|
|
54
|
-
nodePaddingX: 0,
|
|
55
|
-
nodePaddingY: 0,
|
|
56
|
-
nodeLineWidth: STROKE_WIDTHS.innerBox,
|
|
57
|
-
edgeLineWidth: STROKE_WIDTHS.connector,
|
|
58
|
-
groupCornerRadius: 0,
|
|
59
|
-
groupPaddingX: 0,
|
|
60
|
-
groupPaddingY: 0,
|
|
61
|
-
groupLineWidth: STROKE_WIDTHS.outerBox,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const TICK_UNIT_MS: Record<GanttTickUnit, number> = {
|
|
65
|
-
millisecond: 1,
|
|
66
|
-
second: 1000,
|
|
67
|
-
minute: 60_000,
|
|
68
|
-
hour: 3_600_000,
|
|
69
|
-
day: DAY_MS,
|
|
70
|
-
week: 7 * DAY_MS,
|
|
71
|
-
month: 30 * DAY_MS, // month ticks step by calendar months; this is the sizing estimate
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Auto-tick candidates: (stepMs, defaultFormat) pairs from minutes to years.
|
|
75
|
-
const AUTO_TICKS: Array<{ unit: GanttTickUnit; count: number; format: string }> = [
|
|
76
|
-
{ unit: 'minute', count: 1, format: '%H:%M' },
|
|
77
|
-
{ unit: 'minute', count: 15, format: '%H:%M' },
|
|
78
|
-
{ unit: 'hour', count: 1, format: '%H:%M' },
|
|
79
|
-
{ unit: 'hour', count: 6, format: '%H:%M' },
|
|
80
|
-
{ unit: 'day', count: 1, format: '%m-%d' },
|
|
81
|
-
{ unit: 'week', count: 1, format: '%m-%d' },
|
|
82
|
-
{ unit: 'week', count: 2, format: '%m-%d' },
|
|
83
|
-
{ unit: 'month', count: 1, format: '%b %Y' },
|
|
84
|
-
{ unit: 'month', count: 3, format: '%b %Y' },
|
|
85
|
-
{ unit: 'month', count: 12, format: '%Y' },
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
interface TickPlan {
|
|
89
|
-
unit: GanttTickUnit
|
|
90
|
-
count: number
|
|
91
|
-
format: string
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const INLINE_FORMAT_TAG = /<\/?(?:b|strong|i|em|u|s|del)\s*>/gi
|
|
95
|
-
|
|
96
|
-
export function resolveGanttRenderStyle(options: RenderOptions = {}): ResolvedRenderStyle {
|
|
97
|
-
return resolveRenderStyle(options, GANTT_STYLE_DEFAULTS)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function ganttTitleFontSize(style: ResolvedRenderStyle): number {
|
|
101
|
-
return Math.max(17, style.groupHeaderFontSize)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function ganttTitleY(style: ResolvedRenderStyle): number {
|
|
105
|
-
const titleHeight = ganttTitleHeight(style)
|
|
106
|
-
return titleHeight === GL.titleHeight ? 18 : titleHeight / 2
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function ganttAxisLabelOffset(style: ResolvedRenderStyle): number {
|
|
110
|
-
return Math.max(10, style.edgeLabelFontSize * 0.75)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function ganttMeasureTextWidth(
|
|
114
|
-
text: string,
|
|
115
|
-
fontSize: number,
|
|
116
|
-
fontWeight: number,
|
|
117
|
-
letterSpacing = 0,
|
|
118
|
-
): number {
|
|
119
|
-
const plain = text.replace(INLINE_FORMAT_TAG, '')
|
|
120
|
-
const codepoints = [...plain].length
|
|
121
|
-
const tracking = Math.max(0, codepoints - 1) * letterSpacing
|
|
122
|
-
return Math.max(0, estimateTextWidth(plain, fontSize, fontWeight) + tracking)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function ganttTitleHeight(style: ResolvedRenderStyle): number {
|
|
126
|
-
return Math.max(GL.titleHeight, Math.ceil(ganttTitleFontSize(style) * 1.3) + 8)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function ganttAxisHeight(style: ResolvedRenderStyle): number {
|
|
130
|
-
return Math.max(GL.axisHeight, Math.ceil(ganttAxisLabelOffset(style) + style.edgeLabelFontSize * 0.65 + 4))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function ganttRowHeight(style: ResolvedRenderStyle, barHeight: number): number {
|
|
134
|
-
const labelLineHeight = Math.max(style.nodeLabelFontSize, style.groupHeaderFontSize) * 1.3
|
|
135
|
-
return Math.ceil(Math.max(barHeight, labelLineHeight)) + GL.rowGap
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Pick the tick plan: the explicit `tickInterval` when it stays under the
|
|
140
|
-
* GANTT_MAX_TICKS bound (mermaid PR #7197 regression: tiny intervals must not
|
|
141
|
-
* generate unbounded ticks), otherwise the smallest auto candidate that fits.
|
|
142
|
-
*/
|
|
143
|
-
export function planTicks(schedule: GanttSchedule, model: GanttModel, targetTicks = 8): TickPlan {
|
|
144
|
-
const span = schedule.timeMax - schedule.timeMin
|
|
145
|
-
if (model.tickInterval) {
|
|
146
|
-
const step = TICK_UNIT_MS[model.tickInterval.unit] * model.tickInterval.count
|
|
147
|
-
if (span / step <= GANTT_MAX_TICKS) {
|
|
148
|
-
return { ...model.tickInterval, format: model.axisFormat ?? defaultFormatFor(model.tickInterval.unit) }
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
for (const cand of AUTO_TICKS) {
|
|
152
|
-
const step = TICK_UNIT_MS[cand.unit] * cand.count
|
|
153
|
-
if (span / step <= targetTicks) {
|
|
154
|
-
return { unit: cand.unit, count: cand.count, format: model.axisFormat ?? cand.format }
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const last = AUTO_TICKS[AUTO_TICKS.length - 1]!
|
|
158
|
-
// Beyond the largest candidate: scale year ticks up until bounded.
|
|
159
|
-
let count = last.count
|
|
160
|
-
while ((span / (TICK_UNIT_MS.month * count)) > GANTT_MAX_TICKS) count *= 2
|
|
161
|
-
return { unit: 'month', count, format: model.axisFormat ?? last.format }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function defaultFormatFor(unit: GanttTickUnit): string {
|
|
165
|
-
switch (unit) {
|
|
166
|
-
case 'millisecond': case 'second': return '%H:%M:%S'
|
|
167
|
-
case 'minute': case 'hour': return '%H:%M'
|
|
168
|
-
case 'day': case 'week': return '%m-%d'
|
|
169
|
-
case 'month': return '%b %Y'
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** First tick instant at or before timeMin, aligned to the unit (weeks align
|
|
174
|
-
* to the model's weekStart; months to the 1st; days to midnight UTC). */
|
|
175
|
-
function alignTickStart(timeMin: EpochMs, plan: TickPlan, weekStart: number): EpochMs {
|
|
176
|
-
if (plan.unit === 'week') {
|
|
177
|
-
let t = startOfDay(timeMin)
|
|
178
|
-
let steps = 0
|
|
179
|
-
while (dayOfWeek(t) !== weekStart && steps++ < 7) t -= DAY_MS
|
|
180
|
-
return t
|
|
181
|
-
}
|
|
182
|
-
if (plan.unit === 'month') {
|
|
183
|
-
const d = new Date(timeMin)
|
|
184
|
-
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1)
|
|
185
|
-
}
|
|
186
|
-
if (plan.unit === 'day') return startOfDay(timeMin)
|
|
187
|
-
const step = TICK_UNIT_MS[plan.unit] * plan.count
|
|
188
|
-
return Math.floor(timeMin / step) * step
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function nextTick(t: EpochMs, plan: TickPlan): EpochMs {
|
|
192
|
-
if (plan.unit === 'month') {
|
|
193
|
-
const d = new Date(t)
|
|
194
|
-
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + plan.count, 1)
|
|
195
|
-
}
|
|
196
|
-
return t + TICK_UNIT_MS[plan.unit] * plan.count
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Resolve the tick instants for a schedule (shared by SVG and ASCII).
|
|
201
|
-
* Bounded by GANTT_MAX_TICKS regardless of inputs.
|
|
202
|
-
*/
|
|
203
|
-
export function resolveTicks(schedule: GanttSchedule, model: GanttModel, targetTicks = 8): Array<{ time: EpochMs; label: string }> {
|
|
204
|
-
const plan = planTicks(schedule, model, targetTicks)
|
|
205
|
-
const weekStart = WEEKDAY_INDEX[model.weekStart]
|
|
206
|
-
const out: Array<{ time: EpochMs; label: string }> = []
|
|
207
|
-
let t = alignTickStart(schedule.timeMin, plan, weekStart)
|
|
208
|
-
let guard = 0
|
|
209
|
-
while (t <= schedule.timeMax && guard++ <= GANTT_MAX_TICKS) {
|
|
210
|
-
if (t >= schedule.timeMin) out.push({ time: t, label: formatGanttInstant(t, plan.format) })
|
|
211
|
-
t = nextTick(t, plan)
|
|
212
|
-
}
|
|
213
|
-
// A coarse explicit interval over a short span (e.g. `tickInterval 1month`
|
|
214
|
-
// on a 2-day chart) can leave zero in-range ticks; an axis must never be
|
|
215
|
-
// empty, so fall back to a single tick at the range start.
|
|
216
|
-
if (out.length === 0) {
|
|
217
|
-
out.push({ time: schedule.timeMin, label: formatGanttInstant(schedule.timeMin, plan.format) })
|
|
218
|
-
}
|
|
219
|
-
return out
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Compact-mode lane packing: within one section, tasks (source order) go to
|
|
224
|
-
* the first lane whose latest end is <= the task's start. Deterministic and
|
|
225
|
-
* overlap-free by construction.
|
|
226
|
-
*/
|
|
227
|
-
export function packCompactLanes(tasks: Array<{ start: EpochMs; end: EpochMs }>): number[] {
|
|
228
|
-
const laneEnds: EpochMs[] = []
|
|
229
|
-
const laneOf: number[] = []
|
|
230
|
-
for (const t of tasks) {
|
|
231
|
-
let lane = laneEnds.findIndex(end => end <= t.start)
|
|
232
|
-
if (lane === -1) {
|
|
233
|
-
lane = laneEnds.length
|
|
234
|
-
laneEnds.push(t.end)
|
|
235
|
-
} else {
|
|
236
|
-
laneEnds[lane] = t.end
|
|
237
|
-
}
|
|
238
|
-
laneOf.push(lane)
|
|
239
|
-
}
|
|
240
|
-
return laneOf
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function layoutGantt(model: GanttModel, schedule: GanttSchedule, options: GanttLayoutOptions = {}): GanttLayoutResult {
|
|
244
|
-
const style = resolveGanttRenderStyle(options.renderOptions)
|
|
245
|
-
const compact = options.compact ?? (model.displayMode === 'compact')
|
|
246
|
-
const barHeight = options.barHeight ?? model.barHeight ?? GL.barHeight
|
|
247
|
-
const rowH = ganttRowHeight(style, barHeight)
|
|
248
|
-
|
|
249
|
-
const vertTasks = schedule.tasks.filter(t => t.tags.includes('vert'))
|
|
250
|
-
const rowTasks = schedule.tasks.filter(t => !t.tags.includes('vert'))
|
|
251
|
-
|
|
252
|
-
// ---- label column ---------------------------------------------------------
|
|
253
|
-
let labelColumnWidth = 0
|
|
254
|
-
for (const t of rowTasks) {
|
|
255
|
-
const label = applyTextTransform(t.label, style.nodeTextTransform)
|
|
256
|
-
labelColumnWidth = Math.max(labelColumnWidth, ganttMeasureTextWidth(label, style.nodeLabelFontSize, style.nodeLabelFontWeight, style.nodeLetterSpacing))
|
|
257
|
-
}
|
|
258
|
-
for (const s of model.sections) {
|
|
259
|
-
if (s.label) {
|
|
260
|
-
const label = applyTextTransform(s.label, style.groupTextTransform)
|
|
261
|
-
labelColumnWidth = Math.max(labelColumnWidth, ganttMeasureTextWidth(label, style.groupHeaderFontSize, style.groupHeaderFontWeight, style.groupLetterSpacing))
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
labelColumnWidth = Math.ceil(labelColumnWidth) + GL.labelGap
|
|
265
|
-
|
|
266
|
-
const width = options.width ?? (GL.padding * 2 + labelColumnWidth + GL.plotWidth)
|
|
267
|
-
const plotX = GL.padding + labelColumnWidth
|
|
268
|
-
const plotW = Math.max(120, width - plotX - GL.padding)
|
|
269
|
-
|
|
270
|
-
const titleH = model.title ? ganttTitleHeight(style) : 0
|
|
271
|
-
const axisH = ganttAxisHeight(style)
|
|
272
|
-
const topAxisH = model.topAxis ? axisH : 0
|
|
273
|
-
const plotY = GL.padding + titleH + topAxisH
|
|
274
|
-
|
|
275
|
-
const span = schedule.timeMax - schedule.timeMin
|
|
276
|
-
const xOf = (t: EpochMs): number => plotX + ((t - schedule.timeMin) / span) * plotW
|
|
277
|
-
|
|
278
|
-
// ---- rows + bars (vert tasks never consume a row) --------------------------
|
|
279
|
-
const bars: GanttBarLayout[] = []
|
|
280
|
-
const rows: GanttRowLayout[] = []
|
|
281
|
-
const sections: GanttSectionBand[] = []
|
|
282
|
-
let y = plotY
|
|
283
|
-
|
|
284
|
-
for (let si = 0; si < model.sections.length; si++) {
|
|
285
|
-
const section = model.sections[si]!
|
|
286
|
-
const sectionTasks = rowTasks.filter(t => t.sectionIndex === si)
|
|
287
|
-
if (sectionTasks.length === 0 && section.label === undefined) continue
|
|
288
|
-
// Bars/rows reference the BAND index in the emitted sections array (empty
|
|
289
|
-
// implicit sections are skipped, so it can differ from the model index).
|
|
290
|
-
const bandIndex = sections.length
|
|
291
|
-
const bandStartY = y
|
|
292
|
-
const rowStart = rows.length
|
|
293
|
-
if (section.label !== undefined) y += rowH // the section header row
|
|
294
|
-
|
|
295
|
-
const lanes = compact ? packCompactLanes(sectionTasks) : sectionTasks.map((_, i) => i)
|
|
296
|
-
const laneCount = sectionTasks.length === 0 ? 0 : Math.max(...lanes) + 1
|
|
297
|
-
const laneRows: GanttRowLayout[] = []
|
|
298
|
-
for (let li = 0; li < laneCount; li++) {
|
|
299
|
-
laneRows.push({ barIndexes: [], sectionIndex: bandIndex, y: y + li * rowH, h: barHeight })
|
|
300
|
-
}
|
|
301
|
-
sectionTasks.forEach((t, i) => {
|
|
302
|
-
const lane = lanes[i]!
|
|
303
|
-
const row = laneRows[lane]!
|
|
304
|
-
const x = xOf(t.start)
|
|
305
|
-
const w = Math.max(t.end > t.start ? 2 : 0, xOf(t.end) - x)
|
|
306
|
-
const isMilestone = t.tags.includes('milestone')
|
|
307
|
-
// Milestone diamonds extend ±barHeight/2 around their center; clamp the
|
|
308
|
-
// center so a milestone at the range edge stays inside the plot band
|
|
309
|
-
// (the ASCII renderer clamps its grid column the same way — issue #26:
|
|
310
|
-
// shared geometry, not per-renderer patch-ups).
|
|
311
|
-
let milestoneX: number | undefined
|
|
312
|
-
if (isMilestone) {
|
|
313
|
-
const r = barHeight / 2
|
|
314
|
-
const lo = plotX + r
|
|
315
|
-
const hi = Math.max(lo, plotX + plotW - r)
|
|
316
|
-
milestoneX = Math.min(hi, Math.max(lo, x + w / 2))
|
|
317
|
-
}
|
|
318
|
-
bars.push({
|
|
319
|
-
taskIndex: t.index, id: t.id, label: t.label, tags: t.tags, sectionIndex: bandIndex,
|
|
320
|
-
x, y: row.y, w, h: barHeight,
|
|
321
|
-
milestoneX,
|
|
322
|
-
rowIndex: rowStart + (section.label !== undefined ? 1 : 0) + lane,
|
|
323
|
-
start: t.start, end: t.end,
|
|
324
|
-
})
|
|
325
|
-
row.barIndexes.push(bars.length - 1)
|
|
326
|
-
})
|
|
327
|
-
rows.push(...laneRows)
|
|
328
|
-
y += laneCount * rowH
|
|
329
|
-
sections.push({
|
|
330
|
-
label: section.label, y: bandStartY, h: y - bandStartY,
|
|
331
|
-
rowStart, rowEnd: rows.length,
|
|
332
|
-
})
|
|
333
|
-
y += GL.sectionGap
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const plotH = Math.max(rowH, y - plotY)
|
|
337
|
-
const height = plotY + plotH + axisH + GL.padding
|
|
338
|
-
|
|
339
|
-
// ---- ticks / markers --------------------------------------------------------
|
|
340
|
-
const ticks: GanttTick[] = resolveTicks(schedule, model).map(t => ({
|
|
341
|
-
time: t.time, x: xOf(t.time), label: t.label,
|
|
342
|
-
}))
|
|
343
|
-
|
|
344
|
-
const verts: GanttVertLayout[] = vertTasks.map(t => ({
|
|
345
|
-
taskIndex: t.index, label: t.label,
|
|
346
|
-
x: xOf(t.start + (t.end - t.start) / 2), time: t.start,
|
|
347
|
-
}))
|
|
348
|
-
|
|
349
|
-
const todayX = options.today !== undefined && options.today >= schedule.timeMin && options.today <= schedule.timeMax
|
|
350
|
-
? xOf(options.today)
|
|
351
|
-
: undefined
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
title: model.title,
|
|
355
|
-
width, height,
|
|
356
|
-
plot: { x: plotX, y: plotY, w: plotW, h: plotH },
|
|
357
|
-
labelColumnWidth,
|
|
358
|
-
rows, sections, bars, verts, ticks,
|
|
359
|
-
topAxis: model.topAxis,
|
|
360
|
-
todayX,
|
|
361
|
-
timeMin: schedule.timeMin, timeMax: schedule.timeMax,
|
|
362
|
-
dateOnly: schedule.dateOnly,
|
|
363
|
-
compact, barHeight,
|
|
364
|
-
}
|
|
365
|
-
}
|
package/src/gantt/parser.ts
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Gantt syntax parser — Mermaid-compatible `gantt` source → GanttModel.
|
|
3
|
-
//
|
|
4
|
-
// Renderer-grade parser with error semantics (docs/design/families/gantt.md §Parser
|
|
5
|
-
// rules): never silently drop a line; duplicate task ids are errors; invalid
|
|
6
|
-
// directives/tasks are errors with line numbers. The agent-grade structured-
|
|
7
|
-
// or-opaque parser lives in src/agent/gantt-body.ts and shares the task-line
|
|
8
|
-
// helpers exported here so the two cannot drift.
|
|
9
|
-
//
|
|
10
|
-
// Input is the normalized line list from mermaid-source.ts (trimmed, comments
|
|
11
|
-
// stripped, header first). Frontmatter-derived config (displayMode, barHeight)
|
|
12
|
-
// is merged by the caller via applyGanttFrontmatterConfig.
|
|
13
|
-
// ============================================================================
|
|
14
|
-
|
|
15
|
-
import type {
|
|
16
|
-
GanttModel, GanttModelTask, GanttModelSection, GanttTaskTag,
|
|
17
|
-
GanttCalendarToken, GanttWeekday, GanttStartExpr, GanttEndExpr, GanttTickUnit,
|
|
18
|
-
} from './types.ts'
|
|
19
|
-
import { GanttError } from './types.ts'
|
|
20
|
-
import type { MermaidFrontmatterMap } from '../mermaid-source.ts'
|
|
21
|
-
import { getFrontmatterMap, getFrontmatterScalar } from '../mermaid-source.ts'
|
|
22
|
-
|
|
23
|
-
export const GANTT_TASK_TAGS: readonly GanttTaskTag[] = ['active', 'done', 'crit', 'milestone', 'vert']
|
|
24
|
-
|
|
25
|
-
const WEEKDAYS: readonly GanttWeekday[] = [
|
|
26
|
-
'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday',
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
export const GANTT_DURATION_RE = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w|M|y)$/
|
|
30
|
-
const TICK_INTERVAL_RE = /^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$/
|
|
31
|
-
|
|
32
|
-
export interface ParsedTaskMeta {
|
|
33
|
-
tags: GanttTaskTag[]
|
|
34
|
-
id?: string
|
|
35
|
-
start?: GanttStartExpr
|
|
36
|
-
end: GanttEndExpr
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parse the metadata half of a task line (after the `:`), following Mermaid's
|
|
41
|
-
* comma-split convention: leading items are status tags; the remainder is
|
|
42
|
-
* `[id,] [start,] end`. One item = end only (start inherited from the previous
|
|
43
|
-
* task); two = start + end; three = id + start + end. Returns null with no
|
|
44
|
-
* side effects when the shape is invalid — callers decide error vs opaque.
|
|
45
|
-
*/
|
|
46
|
-
export function parseGanttTaskMeta(rawMeta: string): ParsedTaskMeta | null {
|
|
47
|
-
const items = rawMeta.split(',').map(s => s.trim())
|
|
48
|
-
if (items.some(s => s.length === 0)) return null
|
|
49
|
-
const tags: GanttTaskTag[] = []
|
|
50
|
-
let i = 0
|
|
51
|
-
while (i < items.length && (GANTT_TASK_TAGS as readonly string[]).includes(items[i]!)) {
|
|
52
|
-
const tag = items[i]! as GanttTaskTag
|
|
53
|
-
if (tags.includes(tag)) return null // repeated tag is malformed
|
|
54
|
-
tags.push(tag)
|
|
55
|
-
i++
|
|
56
|
-
}
|
|
57
|
-
const rest = items.slice(i)
|
|
58
|
-
if (rest.length === 0 || rest.length > 3) return null
|
|
59
|
-
|
|
60
|
-
const classifyStart = (raw: string): GanttStartExpr => {
|
|
61
|
-
const after = raw.match(/^after\s+(.+)$/)
|
|
62
|
-
if (after) return { kind: 'after', refs: after[1]!.split(/\s+/).filter(Boolean) }
|
|
63
|
-
return { kind: 'date', raw }
|
|
64
|
-
}
|
|
65
|
-
const classifyEnd = (raw: string): GanttEndExpr => {
|
|
66
|
-
const until = raw.match(/^until\s+(.+)$/)
|
|
67
|
-
if (until) return { kind: 'until', refs: until[1]!.split(/\s+/).filter(Boolean) }
|
|
68
|
-
if (GANTT_DURATION_RE.test(raw)) return { kind: 'duration', raw }
|
|
69
|
-
return { kind: 'date', raw }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (rest.length === 1) return { tags, end: classifyEnd(rest[0]!) }
|
|
73
|
-
if (rest.length === 2) return { tags, start: classifyStart(rest[0]!), end: classifyEnd(rest[1]!) }
|
|
74
|
-
// Three items: Mermaid treats the first as the task id unconditionally.
|
|
75
|
-
const id = rest[0]!
|
|
76
|
-
if (!/^[\w-]+$/.test(id)) return null
|
|
77
|
-
return { tags, id, start: classifyStart(rest[1]!), end: classifyEnd(rest[2]!) }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Canonical re-emission of a parsed task metadata (used by the agent body
|
|
81
|
-
* serializer and the parser↔body differential tests). */
|
|
82
|
-
export function renderGanttTaskMeta(meta: ParsedTaskMeta): string {
|
|
83
|
-
const parts: string[] = [...meta.tags]
|
|
84
|
-
if (meta.id !== undefined) parts.push(meta.id)
|
|
85
|
-
if (meta.start) parts.push(meta.start.kind === 'after' ? `after ${meta.start.refs.join(' ')}` : meta.start.raw)
|
|
86
|
-
parts.push(meta.end.kind === 'until' ? `until ${meta.end.refs.join(' ')}` : meta.end.raw)
|
|
87
|
-
return parts.join(', ')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function parseCalendarTokens(raw: string): GanttCalendarToken[] {
|
|
91
|
-
// Mermaid accepts comma- or whitespace-separated entries.
|
|
92
|
-
return raw.split(/[,\s]+/).filter(Boolean).map((tok): GanttCalendarToken => {
|
|
93
|
-
const lower = tok.toLowerCase()
|
|
94
|
-
if (lower === 'weekends') return { kind: 'weekends' }
|
|
95
|
-
if ((WEEKDAYS as readonly string[]).includes(lower)) return { kind: 'weekday', day: lower as GanttWeekday }
|
|
96
|
-
return { kind: 'date', raw: tok }
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const DIRECTIVE_RES = {
|
|
101
|
-
title: /^title\s+(.+)$/i,
|
|
102
|
-
dateFormat: /^dateFormat\s+(.+)$/i,
|
|
103
|
-
axisFormat: /^axisFormat\s+(.+)$/i,
|
|
104
|
-
tickInterval: /^tickInterval\s+(.+)$/i,
|
|
105
|
-
excludes: /^excludes\s+(.+)$/i,
|
|
106
|
-
includes: /^includes\s+(.+)$/i,
|
|
107
|
-
todayMarker: /^todayMarker\s+(.+)$/i,
|
|
108
|
-
weekday: /^weekday\s+(.+)$/i,
|
|
109
|
-
weekend: /^weekend\s+(.+)$/i,
|
|
110
|
-
section: /^section\s+(.+)$/i,
|
|
111
|
-
click: /^click\s+([\w-]+)\s+(href|call)\s+(.+)$/i,
|
|
112
|
-
accTitle: /^accTitle\s*:\s*(.+)$/i,
|
|
113
|
-
accDescrInline: /^accDescr\s*:\s*(.+)$/i,
|
|
114
|
-
accDescrBlock: /^accDescr\s*\{\s*$/i,
|
|
115
|
-
} as const
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Parse normalized Mermaid gantt lines (header included) into a GanttModel.
|
|
119
|
-
* Throws GanttError with a code and 1-based line number on the first invalid
|
|
120
|
-
* construct. Lines is the normalized list, so `lineNo` refers to it.
|
|
121
|
-
*/
|
|
122
|
-
export function parseGanttModel(lines: string[]): GanttModel {
|
|
123
|
-
const header = (lines[0] ?? '').trim()
|
|
124
|
-
if (!/^gantt\s*$/i.test(header)) {
|
|
125
|
-
throw new GanttError('GANTT_BAD_DIRECTIVE', `Expected "gantt" header, got "${header}"`, 1)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const model: GanttModel = {
|
|
129
|
-
dateFormat: 'YYYY-MM-DD',
|
|
130
|
-
inclusiveEndDates: false,
|
|
131
|
-
topAxis: false,
|
|
132
|
-
excludes: [],
|
|
133
|
-
includes: [],
|
|
134
|
-
weekendStart: 'saturday',
|
|
135
|
-
weekStart: 'sunday',
|
|
136
|
-
sections: [{ taskIndexes: [] }],
|
|
137
|
-
tasks: [],
|
|
138
|
-
clicks: [],
|
|
139
|
-
}
|
|
140
|
-
const seenIds = new Set<string>()
|
|
141
|
-
let currentSection = 0
|
|
142
|
-
let inAccDescr = false
|
|
143
|
-
const accDescrBuf: string[] = []
|
|
144
|
-
|
|
145
|
-
for (let i = 1; i < lines.length; i++) {
|
|
146
|
-
const line = lines[i]!.trim()
|
|
147
|
-
const lineNo = i + 1
|
|
148
|
-
if (!line || line.startsWith('%%')) continue
|
|
149
|
-
|
|
150
|
-
if (inAccDescr) {
|
|
151
|
-
if (/^\}\s*$/.test(line)) {
|
|
152
|
-
model.accDescr = accDescrBuf.join(' ').trim()
|
|
153
|
-
inAccDescr = false
|
|
154
|
-
} else {
|
|
155
|
-
accDescrBuf.push(line)
|
|
156
|
-
}
|
|
157
|
-
continue
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let m: RegExpMatchArray | null
|
|
161
|
-
|
|
162
|
-
if ((m = line.match(DIRECTIVE_RES.title))) { model.title = m[1]!.trim(); continue }
|
|
163
|
-
if ((m = line.match(DIRECTIVE_RES.dateFormat))) { model.dateFormat = m[1]!.trim(); continue }
|
|
164
|
-
if ((m = line.match(DIRECTIVE_RES.axisFormat))) { model.axisFormat = m[1]!.trim(); continue }
|
|
165
|
-
if ((m = line.match(DIRECTIVE_RES.tickInterval))) {
|
|
166
|
-
// Lenient like Mermaid: values outside the documented
|
|
167
|
-
// `count(millisecond|second|minute|hour|day|week|month)` shape are
|
|
168
|
-
// ignored and the axis falls back to auto ticks — mermaid's own docs
|
|
169
|
-
// contain `tickInterval 1decade`. The PR #7197 safety property lives in
|
|
170
|
-
// bounded tick GENERATION (layout.ts), not in rejecting the directive.
|
|
171
|
-
const tm = m[1]!.trim().match(TICK_INTERVAL_RE)
|
|
172
|
-
if (tm) model.tickInterval = { count: Number(tm[1]), unit: tm[2] as GanttTickUnit }
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
if (/^inclusiveEndDates\s*$/i.test(line)) { model.inclusiveEndDates = true; continue }
|
|
176
|
-
if (/^topAxis\s*$/i.test(line)) { model.topAxis = true; continue }
|
|
177
|
-
if ((m = line.match(DIRECTIVE_RES.excludes))) {
|
|
178
|
-
// Multiple excludes lines accumulate (mermaid PR #7772).
|
|
179
|
-
model.excludes.push(...parseCalendarTokens(m[1]!.trim()))
|
|
180
|
-
continue
|
|
181
|
-
}
|
|
182
|
-
if ((m = line.match(DIRECTIVE_RES.includes))) {
|
|
183
|
-
model.includes.push(...parseCalendarTokens(m[1]!.trim()))
|
|
184
|
-
continue
|
|
185
|
-
}
|
|
186
|
-
if ((m = line.match(DIRECTIVE_RES.todayMarker))) {
|
|
187
|
-
const v = m[1]!.trim()
|
|
188
|
-
model.todayMarker = v.toLowerCase() === 'off' ? { off: true } : { off: false, style: v }
|
|
189
|
-
continue
|
|
190
|
-
}
|
|
191
|
-
if ((m = line.match(DIRECTIVE_RES.weekday))) {
|
|
192
|
-
const day = m[1]!.trim().toLowerCase()
|
|
193
|
-
if (!(WEEKDAYS as readonly string[]).includes(day)) {
|
|
194
|
-
throw new GanttError('GANTT_BAD_DIRECTIVE', `Invalid weekday "${m[1]!.trim()}"`, lineNo)
|
|
195
|
-
}
|
|
196
|
-
model.weekStart = day as GanttWeekday
|
|
197
|
-
continue
|
|
198
|
-
}
|
|
199
|
-
if ((m = line.match(DIRECTIVE_RES.weekend))) {
|
|
200
|
-
const day = m[1]!.trim().toLowerCase()
|
|
201
|
-
if (day !== 'friday' && day !== 'saturday') {
|
|
202
|
-
throw new GanttError('GANTT_BAD_DIRECTIVE', `Invalid weekend "${m[1]!.trim()}" (friday or saturday)`, lineNo)
|
|
203
|
-
}
|
|
204
|
-
model.weekendStart = day
|
|
205
|
-
continue
|
|
206
|
-
}
|
|
207
|
-
if ((m = line.match(DIRECTIVE_RES.click))) {
|
|
208
|
-
model.clicks.push({ taskId: m[1]!, action: m[2]!.toLowerCase() as 'href' | 'call', rest: m[3]!.trim(), line: lineNo })
|
|
209
|
-
continue
|
|
210
|
-
}
|
|
211
|
-
if ((m = line.match(DIRECTIVE_RES.accTitle))) { model.accTitle = m[1]!.trim(); continue }
|
|
212
|
-
if (DIRECTIVE_RES.accDescrBlock.test(line)) { inAccDescr = true; continue }
|
|
213
|
-
if ((m = line.match(DIRECTIVE_RES.accDescrInline))) { model.accDescr = m[1]!.trim(); continue }
|
|
214
|
-
if ((m = line.match(DIRECTIVE_RES.section))) {
|
|
215
|
-
model.sections.push({ label: m[1]!.trim(), taskIndexes: [], line: lineNo })
|
|
216
|
-
currentSection = model.sections.length - 1
|
|
217
|
-
continue
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Task line: `<label> : <metadata>` — label is everything before the LAST
|
|
221
|
-
// colon-separated metadata block. Mermaid's grammar splits on the first
|
|
222
|
-
// colon (labels cannot contain `:`); `#`/`;` are allowed in labels since
|
|
223
|
-
// mermaid PR #5095 (our comment stripping only removes `%%` lines).
|
|
224
|
-
const colon = line.indexOf(':')
|
|
225
|
-
if (colon > 0 && colon < line.length - 1) {
|
|
226
|
-
const label = line.slice(0, colon).trim()
|
|
227
|
-
const rawMeta = line.slice(colon + 1).trim()
|
|
228
|
-
const meta = parseGanttTaskMeta(rawMeta)
|
|
229
|
-
if (!meta) throw new GanttError('GANTT_BAD_TASK', `Invalid task metadata "${rawMeta}"`, lineNo)
|
|
230
|
-
if (!label) throw new GanttError('GANTT_BAD_TASK', 'Task label is empty', lineNo)
|
|
231
|
-
if (meta.id !== undefined) {
|
|
232
|
-
if (seenIds.has(meta.id)) throw new GanttError('GANTT_DUPLICATE_TASK_ID', `Duplicate task id "${meta.id}"`, lineNo)
|
|
233
|
-
seenIds.add(meta.id)
|
|
234
|
-
}
|
|
235
|
-
const task: GanttModelTask = {
|
|
236
|
-
index: model.tasks.length,
|
|
237
|
-
id: meta.id,
|
|
238
|
-
label,
|
|
239
|
-
tags: meta.tags,
|
|
240
|
-
start: meta.start,
|
|
241
|
-
end: meta.end,
|
|
242
|
-
sectionIndex: currentSection,
|
|
243
|
-
line: lineNo,
|
|
244
|
-
}
|
|
245
|
-
model.tasks.push(task)
|
|
246
|
-
model.sections[currentSection]!.taskIndexes.push(task.index)
|
|
247
|
-
continue
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
throw new GanttError('GANTT_BAD_DIRECTIVE', `Unrecognized gantt line "${line}"`, lineNo)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (inAccDescr) throw new GanttError('GANTT_BAD_DIRECTIVE', 'Unclosed accDescr block')
|
|
254
|
-
return model
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Merge frontmatter/init config into the model: Mermaid accepts
|
|
259
|
-
* `displayMode: compact` at the top level and `gantt: { displayMode,
|
|
260
|
-
* barHeight, topAxis }` under config. mermaid-source.ts already folds the
|
|
261
|
-
* `config:` root into the top-level map.
|
|
262
|
-
*/
|
|
263
|
-
export function applyGanttFrontmatterConfig(model: GanttModel, frontmatter: MermaidFrontmatterMap | undefined): GanttModel {
|
|
264
|
-
if (!frontmatter) return model
|
|
265
|
-
const topLevelMode = getFrontmatterScalar<string>(frontmatter, ['displayMode'])
|
|
266
|
-
const ganttMap = getFrontmatterMap(frontmatter, ['gantt'])
|
|
267
|
-
const ganttMode = ganttMap ? getFrontmatterScalar<string>(frontmatter, ['gantt', 'displayMode']) : undefined
|
|
268
|
-
const mode = ganttMode ?? topLevelMode
|
|
269
|
-
if (typeof mode === 'string' && mode.toLowerCase() === 'compact') model.displayMode = 'compact'
|
|
270
|
-
const barHeight = getFrontmatterScalar<number>(frontmatter, ['gantt', 'barHeight'])
|
|
271
|
-
if (typeof barHeight === 'number' && Number.isFinite(barHeight) && barHeight > 0) model.barHeight = barHeight
|
|
272
|
-
const topAxis = getFrontmatterScalar<boolean>(frontmatter, ['gantt', 'topAxis'])
|
|
273
|
-
if (topAxis === true) model.topAxis = true
|
|
274
|
-
const axisFormat = getFrontmatterScalar<string>(frontmatter, ['gantt', 'axisFormat'])
|
|
275
|
-
if (typeof axisFormat === 'string' && model.axisFormat === undefined) model.axisFormat = axisFormat
|
|
276
|
-
const tickInterval = getFrontmatterScalar<string>(frontmatter, ['gantt', 'tickInterval'])
|
|
277
|
-
if (typeof tickInterval === 'string' && model.tickInterval === undefined) {
|
|
278
|
-
const tm = tickInterval.match(TICK_INTERVAL_RE)
|
|
279
|
-
if (tm) model.tickInterval = { count: Number(tm[1]), unit: tm[2] as GanttTickUnit }
|
|
280
|
-
}
|
|
281
|
-
return model
|
|
282
|
-
}
|
package/src/gantt/pipeline.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { MermaidFrontmatterMap } from '../mermaid-source.ts'
|
|
2
|
-
import { applyGanttFrontmatterConfig, parseGanttModel } from './parser.ts'
|
|
3
|
-
import { resolveGanttSchedule } from './schedule.ts'
|
|
4
|
-
import { layoutGantt, type GanttLayoutOptions } from './layout.ts'
|
|
5
|
-
import type { GanttClock, GanttLayoutResult, GanttModel, GanttSchedule } from './types.ts'
|
|
6
|
-
|
|
7
|
-
export interface GanttRenderPipelineOptions {
|
|
8
|
-
clock?: GanttClock
|
|
9
|
-
layout?: Omit<GanttLayoutOptions, 'today'>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface GanttRenderPipeline {
|
|
13
|
-
model: GanttModel
|
|
14
|
-
schedule: GanttSchedule
|
|
15
|
-
positioned: GanttLayoutResult
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function buildGanttRenderPipeline(
|
|
19
|
-
lines: string[],
|
|
20
|
-
frontmatter: MermaidFrontmatterMap | undefined,
|
|
21
|
-
options: GanttRenderPipelineOptions = {},
|
|
22
|
-
): GanttRenderPipeline {
|
|
23
|
-
const model = applyGanttFrontmatterConfig(parseGanttModel(lines), frontmatter)
|
|
24
|
-
const schedule = resolveGanttSchedule(model, options.clock)
|
|
25
|
-
const positioned = layoutGantt(model, schedule, { ...options.layout, today: schedule.today })
|
|
26
|
-
return { model, schedule, positioned }
|
|
27
|
-
}
|