agentic-mermaid 0.1.0 → 0.1.2

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.
Files changed (333) hide show
  1. package/CHANGELOG.md +149 -4
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/README.md +25 -32
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/assets/fonts/FONT-LICENSES.md +9 -5
  6. package/assets/fonts/Inter-Bold.ttf +0 -0
  7. package/assets/fonts/Inter-Medium.ttf +0 -0
  8. package/assets/fonts/Inter-Regular.ttf +0 -0
  9. package/assets/fonts/Inter-SemiBold.ttf +0 -0
  10. package/dist/agent-core.d.ts +463 -0
  11. package/dist/agent-core.js +397 -0
  12. package/dist/agent.d.ts +81 -1822
  13. package/dist/agent.js +336 -36839
  14. package/dist/agentic-mermaid-mcp.js +7 -37908
  15. package/dist/am.js +33638 -36261
  16. package/dist/chunk-4QECACIA.js +4702 -0
  17. package/dist/chunk-CDPKJ2BQ.js +37 -0
  18. package/dist/chunk-QKDLMOJJ.js +3487 -0
  19. package/dist/chunk-WI35L7IR.js +615 -0
  20. package/dist/chunk-WN3YDOBG.js +149 -0
  21. package/dist/chunk-ZYMMKQ3I.js +62637 -0
  22. package/dist/index-D54dqSdC.d.ts +6777 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +310 -32836
  25. package/dist/mcp-cli-SBQSTJFC.js +17 -0
  26. package/docs/README.md +14 -6
  27. package/docs/schemas/style-spec.schema.json +1221 -30
  28. package/examples/styles/catalog.json +54 -0
  29. package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
  30. package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
  31. package/examples/styles/cupertino-prototype.mmd +10 -0
  32. package/examples/styles/cupertino-prototype.style.json +63 -0
  33. package/examples/styles/mid-century-report.style.json +2 -2
  34. package/examples/styles/star-chart-atlas.style.json +2 -1
  35. package/examples/styles/transit-route-map.style.json +2 -2
  36. package/examples/styles/vercel-inspired-prototype.mmd +6 -0
  37. package/examples/styles/vercel-inspired-prototype.style.json +87 -0
  38. package/package.json +70 -43
  39. package/server.json +41 -0
  40. package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
  41. package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
  42. package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
  43. package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
  44. package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
  45. package/AGENT_NATIVE.md +0 -602
  46. package/Instructions_for_agents.md +0 -83
  47. package/TODO.md +0 -662
  48. package/assets/hero.png +0 -0
  49. package/dist/index-x2Cj1dVv.d.ts +0 -1089
  50. package/docs/agent-api-cookbook.md +0 -308
  51. package/docs/agent-mutation-policy.md +0 -104
  52. package/docs/agent-workflow-examples.md +0 -46
  53. package/docs/api.md +0 -203
  54. package/docs/ascii.md +0 -103
  55. package/docs/assets/improvements/box-start-connector.png +0 -0
  56. package/docs/assets/improvements/comment-dropped-lint.png +0 -0
  57. package/docs/assets/improvements/er-content-loss.png +0 -0
  58. package/docs/assets/improvements/fan-in-trunks.png +0 -0
  59. package/docs/assets/improvements/gantt-family.png +0 -0
  60. package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
  61. package/docs/assets/improvements/gantt-verify-seam.png +0 -0
  62. package/docs/assets/improvements/subgraph-container-edges.png +0 -0
  63. package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
  64. package/docs/assets/improvements/wrapper-init-directive.png +0 -0
  65. package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
  66. package/docs/assets/style-cookbook/mid-century-report.png +0 -0
  67. package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
  68. package/docs/assets/style-cookbook/transit-route-map.png +0 -0
  69. package/docs/comparison.md +0 -94
  70. package/docs/config.md +0 -105
  71. package/docs/contributing/adding-diagram-types.md +0 -144
  72. package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
  73. package/docs/contributing/diagram-family-citizenship.md +0 -82
  74. package/docs/contributing/harvesting-upstream-tests.md +0 -126
  75. package/docs/contributing/lessons-learned.md +0 -276
  76. package/docs/contributing/releasing.md +0 -71
  77. package/docs/contributing/visual-review-evidence.md +0 -53
  78. package/docs/custom-style-cookbook.md +0 -154
  79. package/docs/design/families/README.md +0 -26
  80. package/docs/design/families/architecture-beta.md +0 -48
  81. package/docs/design/families/flowchart-parser-conformance.md +0 -22
  82. package/docs/design/families/gantt-research.md +0 -137
  83. package/docs/design/families/gantt.md +0 -329
  84. package/docs/design/families/journey-agentic-cards-before.png +0 -0
  85. package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
  86. package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
  87. package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
  88. package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
  89. package/docs/design/families/journey-migration-parity.md +0 -433
  90. package/docs/design/families/journey-usage-research.md +0 -423
  91. package/docs/design/families/journey.md +0 -125
  92. package/docs/design/families/xychart.md +0 -85
  93. package/docs/design/system/README.md +0 -45
  94. package/docs/design/system/abstraction-audit.md +0 -224
  95. package/docs/design/system/abstraction-recommendations.md +0 -655
  96. package/docs/design/system/architecture.mmd +0 -29
  97. package/docs/design/system/architecture.svg +0 -137
  98. package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
  99. package/docs/design/system/layout-rubric.md +0 -138
  100. package/docs/design/system/route-contracts.md +0 -848
  101. package/docs/design/system/source-preservation-ladder.md +0 -52
  102. package/docs/design/system/styles-rollout.md +0 -216
  103. package/docs/design/system/ugly-layouts.md +0 -119
  104. package/docs/diagram-families.md +0 -193
  105. package/docs/features.md +0 -147
  106. package/docs/fork-differences.md +0 -130
  107. package/docs/getting-started.md +0 -175
  108. package/docs/issue-derived-test-cases.md +0 -156
  109. package/docs/layout-characterization/README.md +0 -245
  110. package/docs/layout-characterization/contact-sheet-families.md +0 -496
  111. package/docs/layout-characterization/contact-sheet.md +0 -638
  112. package/docs/layout-characterization/properties.md +0 -281
  113. package/docs/layout-characterization/visual-quality.md +0 -160
  114. package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
  115. package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
  116. package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
  117. package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
  118. package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
  119. package/docs/layout-characterization/visual-snapshots/journey.svg +0 -118
  120. package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
  121. package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
  122. package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
  123. package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
  124. package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
  125. package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
  126. package/docs/mcp-code-mode-rationale.md +0 -101
  127. package/docs/mcp-http-transport.md +0 -223
  128. package/docs/mermaid-layout-complaints.md +0 -837
  129. package/docs/mutation-testing.md +0 -206
  130. package/docs/project/divergences.md +0 -98
  131. package/docs/project/dogfooding-docs-strategy.md +0 -66
  132. package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
  133. package/docs/project/lessons-learned.md +0 -1019
  134. package/docs/project/remove-role-styling-plan.md +0 -176
  135. package/docs/quality.md +0 -212
  136. package/docs/react.md +0 -97
  137. package/docs/style-authoring.md +0 -133
  138. package/docs/testing-strategy.md +0 -335
  139. package/docs/theming.md +0 -162
  140. package/examples/agent-improve-auth-flow.ts +0 -206
  141. package/examples/agent-loop.ts +0 -53
  142. package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
  143. package/llms.txt +0 -104
  144. package/skill-evals/README.md +0 -77
  145. package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
  146. package/skill-evals/fixtures/class-payment/input.mmd +0 -4
  147. package/skill-evals/fixtures/er-orders/input.mmd +0 -10
  148. package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
  149. package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
  150. package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
  151. package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
  152. package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
  153. package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
  154. package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
  155. package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
  156. package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
  157. package/skill-evals/shared-benchmark.json +0 -510
  158. package/src/agent/analyze.ts +0 -157
  159. package/src/agent/apply.ts +0 -146
  160. package/src/agent/architecture-body.ts +0 -458
  161. package/src/agent/certificates.ts +0 -110
  162. package/src/agent/class-body.ts +0 -319
  163. package/src/agent/core.ts +0 -207
  164. package/src/agent/create.ts +0 -106
  165. package/src/agent/describe.ts +0 -364
  166. package/src/agent/er-body.ts +0 -224
  167. package/src/agent/facts.ts +0 -337
  168. package/src/agent/families-builtin.ts +0 -934
  169. package/src/agent/families.ts +0 -232
  170. package/src/agent/family-layouts.ts +0 -826
  171. package/src/agent/flowchart-body.ts +0 -372
  172. package/src/agent/flowchart-unsupported.ts +0 -243
  173. package/src/agent/gantt-body.ts +0 -495
  174. package/src/agent/index.ts +0 -10
  175. package/src/agent/journey-body.ts +0 -414
  176. package/src/agent/label-metrics.ts +0 -28
  177. package/src/agent/layout-to-rendered.ts +0 -63
  178. package/src/agent/mutate.ts +0 -97
  179. package/src/agent/mutation-ops.ts +0 -51
  180. package/src/agent/op-schema.ts +0 -439
  181. package/src/agent/parse.ts +0 -223
  182. package/src/agent/pie-body.ts +0 -270
  183. package/src/agent/png.ts +0 -117
  184. package/src/agent/quadrant-body.ts +0 -336
  185. package/src/agent/quality.ts +0 -363
  186. package/src/agent/readability-audit.ts +0 -92
  187. package/src/agent/rendered-route-audit.ts +0 -61
  188. package/src/agent/sequence-body.ts +0 -345
  189. package/src/agent/serialize.ts +0 -214
  190. package/src/agent/state-body.ts +0 -589
  191. package/src/agent/structural-count.ts +0 -150
  192. package/src/agent/timeline-body.ts +0 -280
  193. package/src/agent/trace-log.ts +0 -43
  194. package/src/agent/types.ts +0 -979
  195. package/src/agent/verify.ts +0 -571
  196. package/src/agent/xychart-body.ts +0 -472
  197. package/src/architecture/config.ts +0 -282
  198. package/src/architecture/layout.ts +0 -496
  199. package/src/architecture/parser.ts +0 -375
  200. package/src/architecture/renderer.ts +0 -611
  201. package/src/architecture/types.ts +0 -113
  202. package/src/ascii/ansi.ts +0 -423
  203. package/src/ascii/architecture.ts +0 -210
  204. package/src/ascii/canvas.ts +0 -443
  205. package/src/ascii/class-diagram.ts +0 -697
  206. package/src/ascii/converter.ts +0 -475
  207. package/src/ascii/draw.ts +0 -1693
  208. package/src/ascii/edge-bundling.ts +0 -328
  209. package/src/ascii/edge-routing.ts +0 -330
  210. package/src/ascii/er-diagram.ts +0 -439
  211. package/src/ascii/gantt.ts +0 -276
  212. package/src/ascii/grid.ts +0 -747
  213. package/src/ascii/index.ts +0 -196
  214. package/src/ascii/journey.ts +0 -189
  215. package/src/ascii/meta.ts +0 -244
  216. package/src/ascii/multiline-utils.ts +0 -80
  217. package/src/ascii/pathfinder.ts +0 -301
  218. package/src/ascii/pie.ts +0 -67
  219. package/src/ascii/quadrant.ts +0 -158
  220. package/src/ascii/reverse.ts +0 -216
  221. package/src/ascii/sequence.ts +0 -499
  222. package/src/ascii/shapes/circle.ts +0 -27
  223. package/src/ascii/shapes/corners.ts +0 -143
  224. package/src/ascii/shapes/diamond.ts +0 -27
  225. package/src/ascii/shapes/hexagon.ts +0 -27
  226. package/src/ascii/shapes/index.ts +0 -106
  227. package/src/ascii/shapes/rectangle.ts +0 -168
  228. package/src/ascii/shapes/rounded.ts +0 -27
  229. package/src/ascii/shapes/special.ts +0 -332
  230. package/src/ascii/shapes/stadium.ts +0 -110
  231. package/src/ascii/shapes/state.ts +0 -192
  232. package/src/ascii/shapes/types.ts +0 -73
  233. package/src/ascii/timeline.ts +0 -128
  234. package/src/ascii/types.ts +0 -312
  235. package/src/ascii/validate.ts +0 -120
  236. package/src/ascii/width.ts +0 -81
  237. package/src/ascii/xychart.ts +0 -919
  238. package/src/browser.ts +0 -29
  239. package/src/class/layout.ts +0 -240
  240. package/src/class/parser.ts +0 -332
  241. package/src/class/renderer.ts +0 -638
  242. package/src/class/types.ts +0 -129
  243. package/src/cli/agent-instructions.ts +0 -1
  244. package/src/cli/am-bin.ts +0 -6
  245. package/src/cli/exit-codes.ts +0 -18
  246. package/src/cli/index.ts +0 -1117
  247. package/src/cli/init-agent.ts +0 -147
  248. package/src/color-resolver.ts +0 -152
  249. package/src/elk-instance.ts +0 -114
  250. package/src/er/layout.ts +0 -187
  251. package/src/er/parser.ts +0 -226
  252. package/src/er/renderer.ts +0 -792
  253. package/src/er/types.ts +0 -99
  254. package/src/font-manifest.ts +0 -27
  255. package/src/gantt/layout.ts +0 -365
  256. package/src/gantt/parser.ts +0 -282
  257. package/src/gantt/pipeline.ts +0 -27
  258. package/src/gantt/renderer.ts +0 -414
  259. package/src/gantt/schedule.ts +0 -513
  260. package/src/gantt/types.ts +0 -255
  261. package/src/index.ts +0 -368
  262. package/src/journey/layout.ts +0 -520
  263. package/src/journey/parser.ts +0 -218
  264. package/src/journey/renderer.ts +0 -811
  265. package/src/journey/types.ts +0 -144
  266. package/src/layout/geometry.ts +0 -182
  267. package/src/layout/pass.ts +0 -142
  268. package/src/layout/passes/index.ts +0 -3916
  269. package/src/layout-engine.ts +0 -1565
  270. package/src/layout-rubric.ts +0 -659
  271. package/src/mcp/artifacts.ts +0 -137
  272. package/src/mcp/dynamic-harness.ts +0 -31
  273. package/src/mcp/dynamic-modules.d.ts +0 -7
  274. package/src/mcp/facade.ts +0 -608
  275. package/src/mcp/harness-runtime.ts +0 -203
  276. package/src/mcp/hosted-server.ts +0 -468
  277. package/src/mcp/mcp-bin.ts +0 -3
  278. package/src/mcp/mcp-cli.ts +0 -82
  279. package/src/mcp/protocol.ts +0 -13
  280. package/src/mcp/sandbox.ts +0 -195
  281. package/src/mcp/sdk-decl.ts +0 -387
  282. package/src/mcp/server.ts +0 -362
  283. package/src/mcp/tool-surface.ts +0 -146
  284. package/src/mermaid-source.ts +0 -655
  285. package/src/multiline-utils.ts +0 -228
  286. package/src/parser.ts +0 -1031
  287. package/src/pie/layout.ts +0 -168
  288. package/src/pie/parser.ts +0 -136
  289. package/src/pie/renderer.ts +0 -234
  290. package/src/pie/types.ts +0 -82
  291. package/src/quadrant/layout.ts +0 -191
  292. package/src/quadrant/parser.ts +0 -183
  293. package/src/quadrant/renderer.ts +0 -322
  294. package/src/quadrant/types.ts +0 -113
  295. package/src/render-family-hooks.ts +0 -244
  296. package/src/renderer.ts +0 -1099
  297. package/src/route-contracts.ts +0 -2723
  298. package/src/scene/backend.ts +0 -76
  299. package/src/scene/fidelity.ts +0 -183
  300. package/src/scene/hybrid-backend.ts +0 -174
  301. package/src/scene/ir.ts +0 -181
  302. package/src/scene/marks.ts +0 -127
  303. package/src/scene/rough-backend.ts +0 -378
  304. package/src/scene/seed.ts +0 -46
  305. package/src/scene/style-registry.ts +0 -556
  306. package/src/sequence/layout.ts +0 -456
  307. package/src/sequence/parser.ts +0 -255
  308. package/src/sequence/renderer.ts +0 -604
  309. package/src/sequence/types.ts +0 -154
  310. package/src/shape-clipping.ts +0 -351
  311. package/src/shared/accessibility-directives.ts +0 -22
  312. package/src/shared/batched.ts +0 -61
  313. package/src/shared/color-math.ts +0 -72
  314. package/src/shared/svg-a11y.ts +0 -29
  315. package/src/shared/syntax-error.ts +0 -17
  316. package/src/shared/unicode-ranges.ts +0 -135
  317. package/src/styles.ts +0 -318
  318. package/src/svg-paths.ts +0 -28
  319. package/src/text-metrics.ts +0 -248
  320. package/src/theme.ts +0 -639
  321. package/src/timeline/layout.ts +0 -381
  322. package/src/timeline/parser.ts +0 -215
  323. package/src/timeline/renderer.ts +0 -538
  324. package/src/timeline/types.ts +0 -105
  325. package/src/types.ts +0 -454
  326. package/src/website-worker-modules.d.ts +0 -18
  327. package/src/xychart/axis-utils.ts +0 -109
  328. package/src/xychart/colors.ts +0 -124
  329. package/src/xychart/config.ts +0 -88
  330. package/src/xychart/layout.ts +0 -656
  331. package/src/xychart/parser.ts +0 -504
  332. package/src/xychart/renderer.ts +0 -640
  333. package/src/xychart/types.ts +0 -282
@@ -1,154 +0,0 @@
1
- // ============================================================================
2
- // Sequence diagram types
3
- //
4
- // Models the parsed and positioned representations of a Mermaid sequence diagram.
5
- // Sequence diagrams show actor interactions over time (vertical timeline).
6
- // ============================================================================
7
-
8
- import type { PositionedDiagram } from '../types.ts'
9
-
10
- /** Parsed sequence diagram — logical structure from mermaid text */
11
- export interface SequenceDiagram {
12
- /** Optional accessibility title (Mermaid accTitle) */
13
- accessibilityTitle?: string
14
- /** Optional accessibility description (Mermaid accDescr) */
15
- accessibilityDescription?: string
16
- /** Ordered list of actors/participants */
17
- actors: Actor[]
18
- /** Messages between actors in chronological order */
19
- messages: Message[]
20
- /** Structural blocks (loop, alt, opt, par, critical) */
21
- blocks: Block[]
22
- /** Notes attached to actors */
23
- notes: Note[]
24
- }
25
-
26
- export interface Actor {
27
- id: string
28
- label: string
29
- /** 'participant' renders as a box, 'actor' renders as a stick figure */
30
- type: 'participant' | 'actor'
31
- }
32
-
33
- export interface Message {
34
- from: string
35
- to: string
36
- label: string
37
- /** Arrow style: solid line or dashed line */
38
- lineStyle: 'solid' | 'dashed'
39
- /** Arrow head: filled (closed) or open */
40
- arrowHead: 'filled' | 'open'
41
- /** Activate the target lifeline (+) */
42
- activate?: boolean
43
- /** Deactivate the source lifeline (-) */
44
- deactivate?: boolean
45
- }
46
-
47
- export interface Block {
48
- /** Block type keyword */
49
- type: 'loop' | 'alt' | 'opt' | 'par' | 'critical' | 'break' | 'rect'
50
- /** Label for the block header */
51
- label: string
52
- /** Index of the first message inside this block */
53
- startIndex: number
54
- /** Index of the last message inside this block (inclusive) */
55
- endIndex: number
56
- /** For alt/par blocks: indices where "else"/"and" dividers appear (message indices) */
57
- dividers: Array<{ index: number; label: string }>
58
- }
59
-
60
- export interface Note {
61
- /** Which actor(s) the note is attached to */
62
- actorIds: string[]
63
- /** Note text content */
64
- text: string
65
- /** Position relative to the actor(s) */
66
- position: 'left' | 'right' | 'over'
67
- /** Message index after which this note appears */
68
- afterIndex: number
69
- }
70
-
71
- // ============================================================================
72
- // Positioned sequence diagram — ready for SVG rendering
73
- // ============================================================================
74
-
75
- export interface PositionedSequenceDiagram extends PositionedDiagram {
76
- width: number
77
- height: number
78
- accessibilityTitle?: string
79
- accessibilityDescription?: string
80
- actors: PositionedActor[]
81
- lifelines: Lifeline[]
82
- messages: PositionedMessage[]
83
- activations: Activation[]
84
- blocks: PositionedBlock[]
85
- notes: PositionedNote[]
86
- }
87
-
88
- export interface PositionedActor {
89
- id: string
90
- label: string
91
- type: 'participant' | 'actor'
92
- /** Center x of the actor box */
93
- x: number
94
- /** Top y of the actor box */
95
- y: number
96
- width: number
97
- height: number
98
- }
99
-
100
- /** Vertical dashed line from actor to bottom of diagram */
101
- export interface Lifeline {
102
- actorId: string
103
- x: number
104
- topY: number
105
- bottomY: number
106
- }
107
-
108
- export interface PositionedMessage {
109
- from: string
110
- to: string
111
- label: string
112
- lineStyle: 'solid' | 'dashed'
113
- arrowHead: 'filled' | 'open'
114
- /** Start point (from actor's lifeline) */
115
- x1: number
116
- /** End point (to actor's lifeline) */
117
- x2: number
118
- /** Vertical position */
119
- y: number
120
- /** Whether this is a self-message (same actor) */
121
- isSelf: boolean
122
- }
123
-
124
- /** Narrow rectangle on a lifeline showing active processing */
125
- export interface Activation {
126
- actorId: string
127
- x: number
128
- topY: number
129
- bottomY: number
130
- width: number
131
- }
132
-
133
- export interface PositionedBlock {
134
- type: Block['type']
135
- label: string
136
- x: number
137
- y: number
138
- width: number
139
- height: number
140
- /** Divider lines within the block (for alt/par) */
141
- dividers: Array<{ y: number; label: string }>
142
- }
143
-
144
- export interface PositionedNote {
145
- text: string
146
- x: number
147
- y: number
148
- width: number
149
- height: number
150
- /** Actor IDs this note is attached to (for SVG attribution) */
151
- actors?: string[]
152
- /** Note position relative to actors (for SVG attribution) */
153
- position?: 'left' | 'right' | 'over'
154
- }
@@ -1,351 +0,0 @@
1
- /**
2
- * Shape-aware edge clipping utilities.
3
- *
4
- * ELK.js treats all nodes as rectangles for edge routing. For non-rectangular
5
- * shapes like diamonds, this causes edges to terminate at the bounding box
6
- * boundary instead of the actual shape vertices.
7
- *
8
- * This module provides utilities to clip edge endpoints to actual shape
9
- * boundaries after ELK layout is complete.
10
- */
11
-
12
- import type { Point, PositionedNode } from './types.ts'
13
-
14
- /**
15
- * Clip an edge endpoint to the actual shape boundary of a node.
16
- *
17
- * @param points - The edge points array
18
- * @param node - The node to clip to
19
- * @param isStart - True if clipping the start point (source), false for end (target)
20
- * @returns New points array with clipped endpoint
21
- */
22
- export function clipEdgeToShape(
23
- points: Point[],
24
- node: PositionedNode,
25
- isStart: boolean
26
- ): Point[] {
27
- if (points.length < 2) return points
28
-
29
- // Only clip non-rectangular shapes
30
- if (node.shape === 'rectangle' || node.shape === 'service' || node.shape === 'rounded' || node.shape === 'subroutine') {
31
- return points
32
- }
33
-
34
- const result = [...points]
35
- const clip = (endpoint: Point, adjacent: Point): Point => {
36
- switch (node.shape) {
37
- case 'diamond': return clipToDiamond(endpoint, adjacent, node)
38
- case 'circle':
39
- case 'doublecircle':
40
- case 'state-start':
41
- case 'state-end': return clipToEllipse(endpoint, adjacent, node)
42
- case 'stadium': return clipToStadium(endpoint, adjacent, node)
43
- case 'hexagon': return clipToHexagon(endpoint, adjacent, node)
44
- case 'cylinder': return clipToCylinder(endpoint, adjacent, node)
45
- case 'trapezoid':
46
- case 'trapezoid-alt':
47
- case 'lean-r':
48
- case 'lean-l':
49
- case 'asymmetric': return clipToConvexPolygon(endpoint, adjacent, slantedShapePolygon(node))
50
- default: return endpoint
51
- }
52
- }
53
-
54
- if (isStart) {
55
- result[0] = clip(points[0]!, points[1]!)
56
- } else {
57
- const lastIdx = points.length - 1
58
- result[lastIdx] = clip(points[lastIdx]!, points[lastIdx - 1]!)
59
- }
60
-
61
- return result
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // Outline clippers for the symmetric shapes. Each extends the edge's final
66
- // orthogonal segment as a ray and finds where it meets the rendered outline,
67
- // preserving orthogonality — ELK only knows the bounding box, so an endpoint
68
- // that is not at a side midpoint would otherwise float off the curve.
69
- // ---------------------------------------------------------------------------
70
-
71
- function rayInfo(endpoint: Point, adjacent: Point): { vertical: boolean; dir: 1 | -1 } {
72
- const dx = endpoint.x - adjacent.x
73
- const dy = endpoint.y - adjacent.y
74
- const vertical = Math.abs(dx) < Math.abs(dy)
75
- return { vertical, dir: (vertical ? dy : dx) >= 0 ? 1 : -1 }
76
- }
77
-
78
- /** Ellipse inscribed in the bbox (circle when square). */
79
- function clipToEllipse(endpoint: Point, adjacent: Point, node: PositionedNode): Point {
80
- const cx = node.x + node.width / 2
81
- const cy = node.y + node.height / 2
82
- const rx = node.width / 2
83
- const ry = node.height / 2
84
- const { vertical, dir } = rayInfo(endpoint, adjacent)
85
- if (vertical) {
86
- const dx = (endpoint.x - cx) / rx
87
- if (Math.abs(dx) > 1) return endpoint
88
- const span = ry * Math.sqrt(1 - dx * dx)
89
- // Entering downward hits the top half; upward hits the bottom half.
90
- return { x: endpoint.x, y: cy - dir * span }
91
- }
92
- const dy = (endpoint.y - cy) / ry
93
- if (Math.abs(dy) > 1) return endpoint
94
- const span = rx * Math.sqrt(1 - dy * dy)
95
- return { x: cx - dir * span, y: endpoint.y }
96
- }
97
-
98
- /** Flat top/bottom, semicircular ends of radius h/2. */
99
- function clipToStadium(endpoint: Point, adjacent: Point, node: PositionedNode): Point {
100
- const cx = node.x + node.width / 2
101
- const cy = node.y + node.height / 2
102
- const hw = node.width / 2
103
- const r = node.height / 2
104
- const { vertical, dir } = rayInfo(endpoint, adjacent)
105
- if (vertical) {
106
- const dx = Math.abs(endpoint.x - cx)
107
- if (dx <= hw - r) return { x: endpoint.x, y: cy - dir * r }
108
- const ax = dx - (hw - r)
109
- if (ax > r) return endpoint
110
- return { x: endpoint.x, y: cy - dir * Math.sqrt(r * r - ax * ax) }
111
- }
112
- const dy = Math.abs(endpoint.y - cy)
113
- if (dy > r) return endpoint
114
- const span = (hw - r) + Math.sqrt(r * r - dy * dy)
115
- return { x: cx - dir * span, y: endpoint.y }
116
- }
117
-
118
- /** Six-gon: flat top/bottom inset by h/4, pointy E/W tips at mid-height. */
119
- function clipToHexagon(endpoint: Point, adjacent: Point, node: PositionedNode): Point {
120
- const cx = node.x + node.width / 2
121
- const cy = node.y + node.height / 2
122
- const hw = node.width / 2
123
- const hh = node.height / 2
124
- const inset = node.height / 4
125
- const { vertical, dir } = rayInfo(endpoint, adjacent)
126
- if (vertical) {
127
- const dx = Math.abs(endpoint.x - cx)
128
- if (dx <= hw - inset) return { x: endpoint.x, y: cy - dir * hh }
129
- // Slanted corner: |dy| = hh * (1 - (dx - (hw - inset)) / inset)
130
- const t = (dx - (hw - inset)) / inset
131
- if (t > 1) return endpoint
132
- return { x: endpoint.x, y: cy - dir * hh * (1 - t) }
133
- }
134
- const dy = Math.abs(endpoint.y - cy)
135
- if (dy > hh) return endpoint
136
- // |dx| at this height: flat region ends at hw - inset, tip at hw.
137
- const span = dy >= 0 ? hw - inset * (dy / hh) : hw
138
- return { x: cx - dir * span, y: endpoint.y }
139
- }
140
-
141
- /** Vertical walls; elliptical caps of vertical radius 7 (renderer geometry). */
142
- function clipToCylinder(endpoint: Point, adjacent: Point, node: PositionedNode): Point {
143
- const RY = 7
144
- const cx = node.x + node.width / 2
145
- const hw = node.width / 2
146
- const top = node.y + RY
147
- const bottom = node.y + node.height - RY
148
- const { vertical, dir } = rayInfo(endpoint, adjacent)
149
- if (!vertical) {
150
- if (endpoint.y >= top && endpoint.y <= bottom) {
151
- return { x: cx - dir * hw, y: endpoint.y } // straight wall
152
- }
153
- // Above/below the walls the horizontal ray meets a cap ellipse; keep the
154
- // ray's own y so the segment stays orthogonal.
155
- const capCy = endpoint.y < top ? top : bottom
156
- const dyCap = (endpoint.y - capCy) / RY
157
- if (Math.abs(dyCap) > 1) return endpoint
158
- const span = hw * Math.sqrt(1 - dyCap * dyCap)
159
- return { x: cx - dir * span, y: endpoint.y }
160
- }
161
- const dx = (endpoint.x - cx) / hw
162
- if (Math.abs(dx) > 1) return endpoint
163
- const span = RY * Math.sqrt(1 - dx * dx)
164
- return dir > 0
165
- ? { x: endpoint.x, y: top - span } // entering downward: top cap
166
- : { x: endpoint.x, y: bottom + span } // entering upward: bottom cap
167
- }
168
-
169
- /**
170
- * Rendered polygon outlines of the slanted shapes (renderer geometry: the
171
- * trapezoids and parallelograms shear by w * 0.15, the asymmetric flag
172
- * indents its left point by 12px).
173
- */
174
- function slantedShapePolygon(node: PositionedNode): Point[] {
175
- const { x, y, width: w, height: h } = node
176
- const inset = w * 0.15
177
- switch (node.shape) {
178
- case 'trapezoid': // wider bottom
179
- return [{ x: x + inset, y }, { x: x + w - inset, y }, { x: x + w, y: y + h }, { x, y: y + h }]
180
- case 'trapezoid-alt': // wider top
181
- return [{ x, y }, { x: x + w, y }, { x: x + w - inset, y: y + h }, { x: x + inset, y: y + h }]
182
- case 'lean-r': // parallelogram leaning right
183
- return [{ x: x + inset, y }, { x: x + w, y }, { x: x + w - inset, y: y + h }, { x, y: y + h }]
184
- case 'lean-l': // parallelogram leaning left
185
- return [{ x, y }, { x: x + w - inset, y }, { x: x + w, y: y + h }, { x: x + inset, y: y + h }]
186
- case 'asymmetric': // flag with a pointed left edge
187
- return [{ x: x + 12, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x: x + 12, y: y + h }, { x, y: y + h / 2 }]
188
- default:
189
- return [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }]
190
- }
191
- }
192
-
193
- /**
194
- * Generic convex-polygon clipper: extend the edge's final orthogonal segment
195
- * as a ray and keep the polygon-boundary intersection nearest the OUTSIDE
196
- * endpoint (`adjacent`) — the entry point into the shape. Like clipToDiamond
197
- * this preserves orthogonality: the ray's own fixed coordinate is kept and
198
- * only the coordinate along the ray moves onto the outline. Returns the
199
- * endpoint unchanged when the ray misses the polygon.
200
- */
201
- function clipToConvexPolygon(endpoint: Point, adjacent: Point, verts: Point[]): Point {
202
- const { vertical } = rayInfo(endpoint, adjacent)
203
- let best: Point | null = null
204
- let bestDist = Infinity
205
- for (let i = 0; i < verts.length; i++) {
206
- const p1 = verts[i]!
207
- const p2 = verts[(i + 1) % verts.length]!
208
- const hit = vertical
209
- ? intersectVerticalRayWithEdge(endpoint.x, p1, p2)
210
- : intersectHorizontalRayWithEdge(endpoint.y, p1, p2)
211
- if (!hit) continue
212
- const d = Math.hypot(hit.x - adjacent.x, hit.y - adjacent.y)
213
- if (d < bestDist) {
214
- bestDist = d
215
- best = hit
216
- }
217
- }
218
- return best ?? endpoint
219
- }
220
-
221
- /**
222
- * Clip a point to the diamond shape boundary using ray-polygon intersection.
223
- *
224
- * Diamond vertices are at the midpoints of the bounding box sides:
225
- * - Top: (cx, y)
226
- * - Right: (x + w, cy)
227
- * - Bottom: (cx, y + h)
228
- * - Left: (x, cy)
229
- *
230
- * For orthogonal edges, we extend the final segment as a ray and find where
231
- * it intersects the diamond boundary. This preserves orthogonality while
232
- * ensuring the edge terminates at the actual diamond shape.
233
- *
234
- * @param endpoint - The edge endpoint to clip
235
- * @param adjacent - The adjacent point (to determine ray direction)
236
- * @param node - The diamond node
237
- * @returns Clipped point on the diamond boundary
238
- */
239
- function clipToDiamond(endpoint: Point, adjacent: Point, node: PositionedNode): Point {
240
- const cx = node.x + node.width / 2
241
- const cy = node.y + node.height / 2
242
- const halfW = node.width / 2
243
- const halfH = node.height / 2
244
-
245
- // Diamond vertices
246
- const top: Point = { x: cx, y: node.y }
247
- const right: Point = { x: node.x + node.width, y: cy }
248
- const bottom: Point = { x: cx, y: node.y + node.height }
249
- const left: Point = { x: node.x, y: cy }
250
-
251
- // Determine approach direction from adjacent point
252
- const dx = endpoint.x - adjacent.x
253
- const dy = endpoint.y - adjacent.y
254
-
255
- // For orthogonal edges, one of dx or dy will be ~0
256
- const isVertical = Math.abs(dx) < Math.abs(dy)
257
-
258
- if (isVertical) {
259
- // Vertical ray at x = endpoint.x
260
- const rayX = endpoint.x
261
-
262
- if (dy > 0) {
263
- // Coming from above (moving down) → intersect with top half of diamond
264
- // Top half edges: left-top (left → top) and top-right (top → right)
265
- if (rayX <= cx) {
266
- // Intersect with left-top edge (from left vertex to top vertex)
267
- return intersectVerticalRayWithEdge(rayX, left, top) ?? top
268
- } else {
269
- // Intersect with top-right edge (from top vertex to right vertex)
270
- return intersectVerticalRayWithEdge(rayX, top, right) ?? top
271
- }
272
- } else {
273
- // Coming from below (moving up) → intersect with bottom half of diamond
274
- // Bottom half edges: left-bottom (bottom → left) and bottom-right (right → bottom)
275
- if (rayX <= cx) {
276
- // Intersect with bottom-left edge (from bottom vertex to left vertex)
277
- return intersectVerticalRayWithEdge(rayX, bottom, left) ?? bottom
278
- } else {
279
- // Intersect with bottom-right edge (from right vertex to bottom vertex)
280
- return intersectVerticalRayWithEdge(rayX, right, bottom) ?? bottom
281
- }
282
- }
283
- } else {
284
- // Horizontal ray at y = endpoint.y
285
- const rayY = endpoint.y
286
-
287
- if (dx > 0) {
288
- // Coming from left (moving right) → intersect with left half of diamond
289
- // Left half edges: top-left (top → left) and left-bottom (left → bottom)
290
- if (rayY <= cy) {
291
- // Intersect with top-left edge (from top vertex to left vertex)
292
- return intersectHorizontalRayWithEdge(rayY, top, left) ?? left
293
- } else {
294
- // Intersect with left-bottom edge (from left vertex to bottom vertex)
295
- return intersectHorizontalRayWithEdge(rayY, left, bottom) ?? left
296
- }
297
- } else {
298
- // Coming from right (moving left) → intersect with right half of diamond
299
- // Right half edges: top-right (top → right) and right-bottom (right → bottom)
300
- if (rayY <= cy) {
301
- // Intersect with top-right edge (from top vertex to right vertex)
302
- return intersectHorizontalRayWithEdge(rayY, top, right) ?? right
303
- } else {
304
- // Intersect with right-bottom edge (from right vertex to bottom vertex)
305
- return intersectHorizontalRayWithEdge(rayY, right, bottom) ?? right
306
- }
307
- }
308
- }
309
- }
310
-
311
- /**
312
- * Find intersection of a horizontal ray (y = rayY) with a line segment.
313
- * Returns the intersection point or null if no intersection.
314
- */
315
- function intersectHorizontalRayWithEdge(rayY: number, p1: Point, p2: Point): Point | null {
316
- const dy = p2.y - p1.y
317
- if (Math.abs(dy) < 0.001) {
318
- // Edge is horizontal, no single intersection
319
- return null
320
- }
321
-
322
- const t = (rayY - p1.y) / dy
323
- if (t < 0 || t > 1) {
324
- // Intersection outside the edge segment
325
- return null
326
- }
327
-
328
- const x = p1.x + t * (p2.x - p1.x)
329
- return { x, y: rayY }
330
- }
331
-
332
- /**
333
- * Find intersection of a vertical ray (x = rayX) with a line segment.
334
- * Returns the intersection point or null if no intersection.
335
- */
336
- function intersectVerticalRayWithEdge(rayX: number, p1: Point, p2: Point): Point | null {
337
- const dx = p2.x - p1.x
338
- if (Math.abs(dx) < 0.001) {
339
- // Edge is vertical, no single intersection
340
- return null
341
- }
342
-
343
- const t = (rayX - p1.x) / dx
344
- if (t < 0 || t > 1) {
345
- // Intersection outside the edge segment
346
- return null
347
- }
348
-
349
- const y = p1.y + t * (p2.y - p1.y)
350
- return { x: rayX, y }
351
- }
@@ -1,22 +0,0 @@
1
- // Mermaid-universal accessibility directives — accTitle / accDescr single
2
- // lines and accDescr { … } blocks — are legal in every diagram family.
3
- // Families that MODEL them (sequence, timeline) parse them into fields; the
4
- // rest accept-and-skip via this one helper so the tolerance can never drift
5
- // per family again (pie and quadrant each carried a hand-rolled copy).
6
-
7
- /**
8
- * If `lines[i]` starts an accessibility directive, return the index of its
9
- * LAST line (the caller's loop increment then steps past it); otherwise -1.
10
- * Handles single-line `accTitle:`/`accDescr:` and multi-line `accDescr {`
11
- * blocks terminated by a line containing `}`.
12
- */
13
- export function accessibilityDirectiveEnd(lines: readonly string[], i: number): number {
14
- const line = lines[i]!.trim()
15
- if (/^acc(Title|Descr)\s*:/i.test(line)) return i
16
- if (/^accDescr\s*\{/i.test(line)) {
17
- let end = i
18
- while (end < lines.length && !lines[end]!.includes('}')) end++
19
- return end
20
- }
21
- return -1
22
- }
@@ -1,61 +0,0 @@
1
- // ============================================================================
2
- // runBatchedOperations — shared scaffold for "run a handler over each item
3
- // in a list, never abort the batch on a single failure".
4
- //
5
- // Loop 9 M8. Both `cmdBatch` (CLI JSONL runner) and `runWithJudge`
6
- // (LLM-judge fan-out) had near-identical loops: iterate items, try the
7
- // handler, surface the error as a structured entry, keep going.
8
- //
9
- // Signature is sync-or-async — the handler returns O or Promise<O>; the
10
- // result list is awaited as a whole.
11
- // ============================================================================
12
-
13
- export interface BatchSuccess<O> { ok: true; value: O }
14
- export interface BatchFailure { ok: false; error: { code: string; message: string } }
15
- export type BatchEntry<O> = BatchSuccess<O> | BatchFailure
16
-
17
- export interface RunBatchedOptions {
18
- /** Override the code field on caught errors (default 'HANDLER_ERROR'). */
19
- errorCode?: string
20
- }
21
-
22
- export async function runBatchedOperations<I, O>(
23
- items: I[],
24
- handler: (item: I, index: number) => O | Promise<O>,
25
- opts: RunBatchedOptions = {},
26
- ): Promise<BatchEntry<O>[]> {
27
- const errorCode = opts.errorCode ?? 'HANDLER_ERROR'
28
- const out: BatchEntry<O>[] = []
29
- for (let i = 0; i < items.length; i++) {
30
- try {
31
- const value = await handler(items[i]!, i)
32
- out.push({ ok: true, value })
33
- } catch (e) {
34
- const message = e instanceof Error ? e.message : String(e)
35
- out.push({ ok: false, error: { code: errorCode, message } })
36
- }
37
- }
38
- return out
39
- }
40
-
41
- /**
42
- * Synchronous variant for callers whose handler is guaranteed sync.
43
- * Identical semantics to runBatchedOperations sans the await — used by
44
- * cmdBatch in the CLI where the dispatcher returns a number, not a promise.
45
- */
46
- export function collectBatched<I, O>(
47
- items: I[],
48
- handler: (item: I, index: number) => O,
49
- errorCode = 'HANDLER_ERROR',
50
- ): BatchEntry<O>[] {
51
- const out: BatchEntry<O>[] = []
52
- for (let i = 0; i < items.length; i++) {
53
- try {
54
- out.push({ ok: true, value: handler(items[i]!, i) })
55
- } catch (e) {
56
- const message = e instanceof Error ? e.message : String(e)
57
- out.push({ ok: false, error: { code: errorCode, message } })
58
- }
59
- }
60
- return out
61
- }
@@ -1,72 +0,0 @@
1
- // ============================================================================
2
- // Shared hex color math.
3
- //
4
- // The single home for hex parsing, hex serialization, sRGB mixing, and the
5
- // BT.601 luma used for dark/contrast decisions. Thresholds stay at the call
6
- // sites (theme dark-detection, ANSI brightness, contrast text) — only the
7
- // primitives live here, so the arithmetic cannot drift between the SVG,
8
- // ASCII, and chart color paths.
9
- // ============================================================================
10
-
11
- /**
12
- * Parse a hex color to [r, g, b]. Accepts #RGB and #RRGGBB (a longer string
13
- * such as #RRGGBBAA is read as its first six digits; alpha is ignored).
14
- * Assumes a syntactically valid color — use tryParseHex when unsure.
15
- */
16
- export function parseHex(hex: string): [number, number, number] {
17
- const h = hex.replace('#', '')
18
- const full = h.length === 3
19
- ? h[0]! + h[0]! + h[1]! + h[1]! + h[2]! + h[2]!
20
- : h
21
- return [
22
- parseInt(full.slice(0, 2), 16),
23
- parseInt(full.slice(2, 4), 16),
24
- parseInt(full.slice(4, 6), 16),
25
- ]
26
- }
27
-
28
- /** Validating parse: [r, g, b] for #RGB/#RRGGBB/#RRGGBBAA, else null. */
29
- export function tryParseHex(hex: string): [number, number, number] | null {
30
- if (!/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) return null
31
- return parseHex(hex)
32
- }
33
-
34
- /** Serialize r/g/b (clamped and rounded) to #rrggbb. */
35
- export function toHex(r: number, g: number, b: number): string {
36
- return '#' + [r, g, b]
37
- .map(c => Math.round(Math.max(0, Math.min(255, c))).toString(16).padStart(2, '0'))
38
- .join('')
39
- }
40
-
41
- /**
42
- * Mix `pct`% of `fg` over `bg` in sRGB — replicates CSS
43
- * `color-mix(in srgb, fg pct%, bg)`.
44
- */
45
- export function mixHex(fg: string, bg: string, pct: number): string {
46
- const [r1, g1, b1] = parseHex(fg)
47
- const [r2, g2, b2] = parseHex(bg)
48
- const p = pct / 100
49
- return toHex(
50
- r1 * p + r2 * (1 - p),
51
- g1 * p + g2 * (1 - p),
52
- b1 * p + b2 * (1 - p),
53
- )
54
- }
55
-
56
- /** Loose CSS hex form: #RGB, #RGBA, #RRGGBB, or #RRGGBBAA. */
57
- export function isHexColor(s: string): boolean {
58
- return /^#[0-9a-fA-F]{3,8}$/.test(s)
59
- }
60
-
61
- /** Strict 6-digit hex form (#RRGGBB) — what the chart palettes require. */
62
- export function isSixDigitHex(s: string): boolean {
63
- return /^#[0-9a-fA-F]{6}$/.test(s)
64
- }
65
-
66
- /**
67
- * Perceived brightness on a 0–255 scale (ITU-R BT.601 luma:
68
- * `(r·299 + g·587 + b·114) / 1000`). Callers own their thresholds.
69
- */
70
- export function luma255(r: number, g: number, b: number): number {
71
- return (r * 299 + g * 587 + b * 114) / 1000
72
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Shared SVG accessibility root attributes.
3
- *
4
- * One implementation of the contract every family renderer that threads
5
- * accessibility itself must follow: `role="img"` on the root,
6
- * `aria-labelledby` referencing the `<title id>` and `aria-describedby`
7
- * referencing the `<desc id>`. Renderers that do not thread accessibility get
8
- * the same contract from the injectAccessibility post-pass in index.ts —
9
- * which detects these root attributes to know when to leave output alone.
10
- * (Conformance-gated across all families by svg-a11y-conformance.test.ts.)
11
- */
12
- export function buildAccessibilityAttrs(
13
- title: string | undefined,
14
- description: string | undefined,
15
- titleId: string,
16
- descId: string,
17
- roledescription?: string,
18
- ): Record<string, string> {
19
- const attrs: Record<string, string> = {}
20
- if (roledescription) {
21
- attrs['role'] = 'img'
22
- attrs['aria-roledescription'] = roledescription
23
- } else if (title || description) {
24
- attrs['role'] = 'img'
25
- }
26
- if (title) attrs['aria-labelledby'] = titleId
27
- if (description) attrs['aria-describedby'] = descId
28
- return attrs
29
- }