sfn-diagram 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/dist/index.js ADDED
@@ -0,0 +1,1911 @@
1
+ import dagre from "@dagrejs/dagre";
2
+ import { curveBasis, line, select } from "d3";
3
+ import { JSDOM } from "jsdom";
4
+ import nodeHtmlToImage from "node-html-to-image";
5
+
6
+ //#region src/config/themes.ts
7
+ /**
8
+ * AWS Light Theme - matches the AWS Step Functions console light mode
9
+ */
10
+ const AWS_LIGHT_THEME = {
11
+ background: "#ffffff",
12
+ nodeColors: {
13
+ Pass: {
14
+ fill: "#e1f5fe",
15
+ stroke: "#0277bd"
16
+ },
17
+ Task: {
18
+ fill: "#fff3e0",
19
+ stroke: "#ef6c00"
20
+ },
21
+ Choice: {
22
+ fill: "#f3e5f5",
23
+ stroke: "#7b1fa2"
24
+ },
25
+ Wait: {
26
+ fill: "#e8f5e8",
27
+ stroke: "#388e3c"
28
+ },
29
+ Succeed: {
30
+ fill: "#e8f5e8",
31
+ stroke: "#4caf50"
32
+ },
33
+ Fail: {
34
+ fill: "#ffebee",
35
+ stroke: "#f44336"
36
+ },
37
+ Parallel: {
38
+ fill: "#fce4ec",
39
+ stroke: "#c2185b"
40
+ },
41
+ Map: {
42
+ fill: "#f1f8e9",
43
+ stroke: "#689f38"
44
+ }
45
+ },
46
+ edgeColors: {
47
+ choice: "#7b1fa2",
48
+ default: "#9c27b0",
49
+ error: "#f44336",
50
+ normal: "#546e7a"
51
+ },
52
+ textColor: "#212121",
53
+ fontSize: 14,
54
+ fontFamily: "Arial, sans-serif"
55
+ };
56
+ /**
57
+ * AWS Dark Theme - matches the AWS Step Functions console dark mode
58
+ */
59
+ const AWS_DARK_THEME = {
60
+ background: "#1e1e1e",
61
+ nodeColors: {
62
+ Pass: {
63
+ fill: "#01579b",
64
+ stroke: "#4fc3f7"
65
+ },
66
+ Task: {
67
+ fill: "#e65100",
68
+ stroke: "#ffb74d"
69
+ },
70
+ Choice: {
71
+ fill: "#4a148c",
72
+ stroke: "#ce93d8"
73
+ },
74
+ Wait: {
75
+ fill: "#1b5e20",
76
+ stroke: "#81c784"
77
+ },
78
+ Succeed: {
79
+ fill: "#2e7d32",
80
+ stroke: "#a5d6a7"
81
+ },
82
+ Fail: {
83
+ fill: "#b71c1c",
84
+ stroke: "#ef5350"
85
+ },
86
+ Parallel: {
87
+ fill: "#880e4f",
88
+ stroke: "#f48fb1"
89
+ },
90
+ Map: {
91
+ fill: "#33691e",
92
+ stroke: "#aed581"
93
+ }
94
+ },
95
+ edgeColors: {
96
+ choice: "#ce93d8",
97
+ default: "#ba68c8",
98
+ error: "#ef5350",
99
+ normal: "#90a4ae"
100
+ },
101
+ textColor: "#e0e0e0",
102
+ fontSize: 14,
103
+ fontFamily: "Arial, sans-serif"
104
+ };
105
+ /**
106
+ * Get theme object from theme name or custom theme
107
+ */
108
+ function getTheme(theme, customColors) {
109
+ let baseTheme;
110
+ if (!theme || theme === "light") baseTheme = AWS_LIGHT_THEME;
111
+ else if (theme === "dark") baseTheme = AWS_DARK_THEME;
112
+ else baseTheme = theme;
113
+ if (customColors) return {
114
+ ...baseTheme,
115
+ nodeColors: {
116
+ ...baseTheme.nodeColors,
117
+ ...customColors
118
+ }
119
+ };
120
+ return baseTheme;
121
+ }
122
+
123
+ //#endregion
124
+ //#region src/styles/NodeStyles.ts
125
+ /**
126
+ * Get node style for a given state type
127
+ */
128
+ function getNodeStyle(params) {
129
+ const { stateType, theme = AWS_LIGHT_THEME, customColors, stylePreset = "aws-standard" } = params;
130
+ if (customColors?.[stateType]) return customColors[stateType];
131
+ const themeColor = theme.nodeColors[stateType];
132
+ if (themeColor) return {
133
+ fill: themeColor.fill,
134
+ shape: getShapeForStateType({
135
+ stateType,
136
+ stylePreset
137
+ }),
138
+ stroke: themeColor.stroke,
139
+ strokeWidth: getStrokeWidthForType(stateType)
140
+ };
141
+ return {
142
+ fill: theme.nodeColors.Task.fill,
143
+ shape: "rect",
144
+ stroke: theme.nodeColors.Task.stroke,
145
+ strokeWidth: 2
146
+ };
147
+ }
148
+ /**
149
+ * Get the shape for a state type based on the style preset
150
+ */
151
+ function getShapeForStateType(params) {
152
+ const { stateType, stylePreset } = params;
153
+ if (stylePreset === "aws-standard") return "rect";
154
+ switch (stateType) {
155
+ case "Choice": return "diamond";
156
+ case "Succeed":
157
+ case "Fail": return "circle";
158
+ default: return "rect";
159
+ }
160
+ }
161
+ /**
162
+ * Get stroke width for a state type
163
+ */
164
+ function getStrokeWidthForType(stateType) {
165
+ switch (stateType) {
166
+ case "Succeed":
167
+ case "Fail": return 3;
168
+ default: return 2;
169
+ }
170
+ }
171
+
172
+ //#endregion
173
+ //#region src/constants/labels.ts
174
+ /**
175
+ * Label constants used in diagram generation
176
+ */
177
+ const EDGE_LABELS = {
178
+ BRANCH_PREFIX: "Branch",
179
+ CATCH_PREFIX: "Catch",
180
+ CHOICE_PREFIX: "Choice",
181
+ CONDITION_FALLBACK: "Condition",
182
+ DEFAULT: "Default",
183
+ ERROR_PREFIX: "Error:",
184
+ ITERATOR: "Iterator"
185
+ };
186
+ /**
187
+ * Generates an error label with error types
188
+ */
189
+ function getErrorLabel(errorTypes) {
190
+ const errors = errorTypes?.join(", ") || "Any";
191
+ return `${EDGE_LABELS.ERROR_PREFIX} ${errors}`;
192
+ }
193
+ /**
194
+ * Generates a catch block label based on style preference
195
+ */
196
+ function getCatchLabel(params) {
197
+ const { catchLabelStyle = "error-type", errorTypes, index } = params;
198
+ if (catchLabelStyle === "catch-number") return `${EDGE_LABELS.CATCH_PREFIX} #${index + 1}`;
199
+ return getErrorLabel(errorTypes);
200
+ }
201
+
202
+ //#endregion
203
+ //#region src/services/ServiceDetector.ts
204
+ /**
205
+ * Mapping of AWS service names to their icon filenames in the aws-icons package
206
+ * Icons sourced from: https://github.com/MKAbuMattar/aws-icons
207
+ * URL pattern: https://cdn.jsdelivr.net/npm/aws-icons@latest/icons/architecture-service/{ICON_NAME}.svg
208
+ */
209
+ const SERVICE_ICON_MAP = {
210
+ "apigateway": "AmazonAPIGateway",
211
+ "appflow": "AmazonAppFlow",
212
+ "appsync": "AWSAppSync",
213
+ "eventbridge": "AmazonEventBridge",
214
+ "events": "AmazonEventBridge",
215
+ "mq": "AmazonMQ",
216
+ "sns": "AmazonSimpleNotificationService",
217
+ "sqs": "AmazonSimpleQueueService",
218
+ "stepfunctions": "AWSStepFunctions",
219
+ "sfn": "AWSStepFunctions",
220
+ "states": "AWSStepFunctions",
221
+ "athena": "AmazonAthena",
222
+ "emr": "AmazonEMR",
223
+ "glue": "AWSGlue",
224
+ "kinesis": "AmazonKinesis",
225
+ "kinesisanalytics": "AmazonKinesisDataAnalytics",
226
+ "kinesisfirehose": "AmazonKinesisDataFirehose",
227
+ "redshift": "AmazonRedshift",
228
+ "batch": "AWSBatch",
229
+ "ec2": "AmazonEC2",
230
+ "ecs": "AmazonElasticContainerService",
231
+ "eks": "AmazonElasticKubernetesService",
232
+ "fargate": "AWSFargate",
233
+ "lambda": "AWSLambda",
234
+ "ecr": "AmazonElasticContainerRegistry",
235
+ "aurora": "AmazonAurora",
236
+ "documentdb": "AmazonDocumentDB",
237
+ "dynamodb": "AmazonDynamoDB",
238
+ "elasticache": "AmazonElastiCache",
239
+ "neptune": "AmazonNeptune",
240
+ "rds": "AmazonRDS",
241
+ "timestream": "AmazonTimestream",
242
+ "codebuild": "AWSCodeBuild",
243
+ "codecommit": "AWSCodeCommit",
244
+ "codedeploy": "AWSCodeDeploy",
245
+ "codepipeline": "AWSCodePipeline",
246
+ "bedrock": "AmazonBedrock",
247
+ "comprehend": "AmazonComprehend",
248
+ "forecast": "AmazonForecast",
249
+ "personalize": "AmazonPersonalize",
250
+ "polly": "AmazonPolly",
251
+ "rekognition": "AmazonRekognition",
252
+ "sagemaker": "AmazonSageMaker",
253
+ "textract": "AmazonTextract",
254
+ "transcribe": "AmazonTranscribe",
255
+ "translate": "AmazonTranslate",
256
+ "cloudformation": "AWSCloudFormation",
257
+ "cloudwatch": "AmazonCloudWatch",
258
+ "config": "AWSConfig",
259
+ "systemsmanager": "AWSSystemsManager",
260
+ "ssm": "AWSSystemsManager",
261
+ "kms": "AWSKeyManagementService",
262
+ "secretsmanager": "AWSSecretsManager",
263
+ "waf": "AWSWAF",
264
+ "efs": "AmazonElasticFileSystem",
265
+ "fsx": "AmazonFSx",
266
+ "s3": "AmazonSimpleStorageService",
267
+ "s3glacier": "AmazonS3Glacier"
268
+ };
269
+ /**
270
+ * Extract AWS service name from ARN (Amazon Resource Name)
271
+ *
272
+ * Supports three ARN patterns:
273
+ * 1. Direct service ARNs: arn:aws:SERVICE:region:account:resource
274
+ * 2. Service integrations: arn:aws:states:::SERVICE:action
275
+ * 3. SDK integrations: arn:aws:states:::aws-sdk:SERVICE:action
276
+ *
277
+ * @param params - Parameters containing the ARN to parse
278
+ * @returns Normalized service name or null if parsing fails
279
+ *
280
+ * @example
281
+ * extractServiceFromArn({ arn: 'arn:aws:lambda:us-east-1:123:function:MyFunc' })
282
+ * // Returns: 'lambda'
283
+ *
284
+ * @example
285
+ * extractServiceFromArn({ arn: 'arn:aws:states:::dynamodb:getItem' })
286
+ * // Returns: 'dynamodb'
287
+ */
288
+ function extractServiceFromArn(params) {
289
+ const { arn } = params;
290
+ const directMatch = arn.match(/^arn:aws:([^:]+):/);
291
+ if (directMatch && directMatch[1] !== "states") return normalizeServiceName({ serviceName: directMatch[1] });
292
+ const integrationMatch = arn.match(/^arn:aws:states:::([^:]+):/);
293
+ if (integrationMatch) {
294
+ const service = integrationMatch[1];
295
+ if (service === "aws-sdk") {
296
+ const sdkMatch = arn.match(/^arn:aws:states:::aws-sdk:([^:]+):/);
297
+ if (sdkMatch) return normalizeServiceName({ serviceName: sdkMatch[1] });
298
+ }
299
+ return normalizeServiceName({ serviceName: service });
300
+ }
301
+ return null;
302
+ }
303
+ /**
304
+ * Normalize AWS service name to canonical form
305
+ *
306
+ * Converts service names to lowercase and removes hyphens for consistent mapping
307
+ *
308
+ * @param params - Parameters containing the service name to normalize
309
+ * @returns Normalized service name
310
+ *
311
+ * @example
312
+ * normalizeServiceName({ serviceName: 'Amazon-S3' })
313
+ * // Returns: 'amazons3'
314
+ *
315
+ * @example
316
+ * normalizeServiceName({ serviceName: 'DynamoDB' })
317
+ * // Returns: 'dynamodb'
318
+ */
319
+ function normalizeServiceName(params) {
320
+ const { serviceName } = params;
321
+ return serviceName.toLowerCase().replace(/-/g, "");
322
+ }
323
+ /**
324
+ * Build CDN URL for AWS service icon
325
+ *
326
+ * Constructs jsDelivr CDN URL for icons from the aws-icons npm package
327
+ *
328
+ * @param params - Parameters containing the icon filename
329
+ * @returns Full CDN URL to the icon SVG file
330
+ *
331
+ * @example
332
+ * buildIconUrl({ iconName: 'AWSLambda' })
333
+ * // Returns: 'https://cdn.jsdelivr.net/npm/aws-icons@latest/icons/architecture-service/AWSLambda.svg'
334
+ */
335
+ function buildIconUrl(params) {
336
+ const { iconName } = params;
337
+ return `https://cdn.jsdelivr.net/npm/aws-icons@latest/icons/architecture-service/${iconName}.svg`;
338
+ }
339
+ /**
340
+ * Detect AWS service from ASL Task state and resolve icon URL
341
+ *
342
+ * Analyzes the Resource field of Task states to identify the AWS service,
343
+ * then maps to the corresponding icon URL from the aws-icons CDN
344
+ *
345
+ * @param params - Parameters for service detection
346
+ * @param params.state - ASL state definition to analyze
347
+ * @param params.iconResolver - Optional custom function to resolve icon URLs
348
+ * @returns Service information with name and icon URL, or null for non-Task states
349
+ *
350
+ * @example
351
+ * const taskState = {
352
+ * Type: 'Task',
353
+ * Resource: 'arn:aws:lambda:us-east-1:123456789012:function:MyFunction'
354
+ * };
355
+ *
356
+ * detectService({ state: taskState })
357
+ * // Returns: {
358
+ * // serviceName: 'lambda',
359
+ * // iconUrl: 'https://cdn.jsdelivr.net/npm/aws-icons@latest/icons/arch/Arch_AWS-Lambda_48.svg'
360
+ * // }
361
+ *
362
+ * @example
363
+ * // With custom icon resolver
364
+ * detectService({
365
+ * state: taskState,
366
+ * iconResolver: (service) => {
367
+ * if (service === 'lambda') {
368
+ * return 'https://my-cdn.com/lambda.svg';
369
+ * }
370
+ * return null; // Use default
371
+ * }
372
+ * })
373
+ */
374
+ function detectService(params) {
375
+ const { iconResolver, state } = params;
376
+ if (state.Type !== "Task" || !state.Resource) return null;
377
+ const serviceName = extractServiceFromArn({ arn: state.Resource });
378
+ if (!serviceName) return null;
379
+ if (iconResolver) return {
380
+ iconUrl: iconResolver(serviceName),
381
+ serviceName
382
+ };
383
+ const iconName = SERVICE_ICON_MAP[serviceName];
384
+ if (!iconName) return {
385
+ iconUrl: null,
386
+ serviceName
387
+ };
388
+ return {
389
+ iconUrl: buildIconUrl({ iconName }),
390
+ serviceName
391
+ };
392
+ }
393
+
394
+ //#endregion
395
+ //#region src/AslParser.ts
396
+ /**
397
+ * Error thrown when ASL validation fails
398
+ */
399
+ var AslValidationError = class extends Error {
400
+ constructor(message) {
401
+ super(message);
402
+ this.name = "AslValidationError";
403
+ }
404
+ };
405
+ /** Valid ASL state types */
406
+ const VALID_STATE_TYPES = [
407
+ "Pass",
408
+ "Task",
409
+ "Choice",
410
+ "Wait",
411
+ "Succeed",
412
+ "Fail",
413
+ "Parallel",
414
+ "Map"
415
+ ];
416
+ /**
417
+ * Validates an ASL definition and returns helpful error messages
418
+ *
419
+ * @param params - Validation parameters
420
+ * @throws {AslValidationError} When the ASL definition is invalid
421
+ */
422
+ function validateAsl(params) {
423
+ const { definition } = params;
424
+ if (!definition || typeof definition !== "object") throw new AslValidationError("ASL definition must be a non-null object");
425
+ const asl = definition;
426
+ if (!("StartAt" in asl)) throw new AslValidationError("ASL definition missing required field: StartAt");
427
+ if (typeof asl.StartAt !== "string" || asl.StartAt.trim() === "") throw new AslValidationError("StartAt must be a non-empty string");
428
+ if (!("States" in asl)) throw new AslValidationError("ASL definition missing required field: States");
429
+ if (!asl.States || typeof asl.States !== "object") throw new AslValidationError("States must be a non-null object");
430
+ const states = asl.States;
431
+ const stateNames = Object.keys(states);
432
+ if (stateNames.length === 0) throw new AslValidationError("States object cannot be empty");
433
+ if (!stateNames.includes(asl.StartAt)) throw new AslValidationError(`StartAt references non-existent state: "${asl.StartAt}". Available states: ${stateNames.join(", ")}`);
434
+ for (const [stateName, stateValue] of Object.entries(states)) validateState({
435
+ stateName,
436
+ stateNames,
437
+ stateValue
438
+ });
439
+ }
440
+ /**
441
+ * Validates an individual state within an ASL definition
442
+ */
443
+ function validateState(params) {
444
+ const { stateName, stateNames, stateValue } = params;
445
+ if (!stateValue || typeof stateValue !== "object") throw new AslValidationError(`State "${stateName}" must be a non-null object`);
446
+ const state = stateValue;
447
+ if (!("Type" in state)) throw new AslValidationError(`State "${stateName}" missing required field: Type`);
448
+ const stateType = state.Type;
449
+ if (typeof stateType !== "string" || !VALID_STATE_TYPES.includes(stateType)) throw new AslValidationError(`State "${stateName}" has invalid Type: "${stateType}". Valid types: ${VALID_STATE_TYPES.join(", ")}`);
450
+ if ("Next" in state && state.Next !== void 0) {
451
+ if (typeof state.Next !== "string") throw new AslValidationError(`State "${stateName}": Next must be a string`);
452
+ if (!stateNames.includes(state.Next)) throw new AslValidationError(`State "${stateName}": Next references non-existent state "${state.Next}"`);
453
+ }
454
+ if ("Default" in state && state.Default !== void 0) {
455
+ if (typeof state.Default !== "string") throw new AslValidationError(`State "${stateName}": Default must be a string`);
456
+ if (!stateNames.includes(state.Default)) throw new AslValidationError(`State "${stateName}": Default references non-existent state "${state.Default}"`);
457
+ }
458
+ if ("Choices" in state && Array.isArray(state.Choices)) {
459
+ for (const [index, choice] of state.Choices.entries()) if (choice && typeof choice === "object" && "Next" in choice) {
460
+ const choiceNext = choice.Next;
461
+ if (typeof choiceNext === "string" && !stateNames.includes(choiceNext)) throw new AslValidationError(`State "${stateName}": Choices[${index}].Next references non-existent state "${choiceNext}"`);
462
+ }
463
+ }
464
+ if ("Catch" in state && Array.isArray(state.Catch)) {
465
+ for (const [index, catchBlock] of state.Catch.entries()) if (catchBlock && typeof catchBlock === "object" && "Next" in catchBlock) {
466
+ const catchNext = catchBlock.Next;
467
+ if (typeof catchNext === "string" && !stateNames.includes(catchNext)) throw new AslValidationError(`State "${stateName}": Catch[${index}].Next references non-existent state "${catchNext}"`);
468
+ }
469
+ }
470
+ if (!["Succeed", "Fail"].includes(stateType) && stateType !== "Choice") {
471
+ const hasNext = "Next" in state;
472
+ const hasEnd = "End" in state && state.End === true;
473
+ if (!hasNext && !hasEnd) throw new AslValidationError(`State "${stateName}" (Type: ${stateType}) must have either "Next" or "End: true"`);
474
+ }
475
+ }
476
+ function parseAsl(params) {
477
+ const { definition, options } = params;
478
+ const nodes = [];
479
+ const edges = [];
480
+ validateAsl({ definition });
481
+ extractStatesRecursively({
482
+ definition,
483
+ nodes,
484
+ options
485
+ });
486
+ for (const [stateName, state] of Object.entries(definition.States)) {
487
+ const stateEdges = extractEdgesFromState({
488
+ catchLabelStyle: options?.catchLabelStyle,
489
+ state,
490
+ stateName
491
+ });
492
+ edges.push(...stateEdges);
493
+ }
494
+ extractNestedEdges({
495
+ definition,
496
+ edges,
497
+ options
498
+ });
499
+ return {
500
+ edges,
501
+ nodes
502
+ };
503
+ }
504
+ function createStateNode(params) {
505
+ const { name, options, state, stylePreset } = params;
506
+ const isContainer = state.Type === "Parallel" || state.Type === "Map";
507
+ const baseNode = {
508
+ id: name,
509
+ isContainer,
510
+ label: state.Comment || name,
511
+ style: getNodeStyle({
512
+ stateType: state.Type,
513
+ stylePreset
514
+ }),
515
+ type: state.Type
516
+ };
517
+ if (isContainer) baseNode.children = [];
518
+ if (options?.showIcons && state.Type === "Task") {
519
+ const serviceInfo = detectService({
520
+ iconResolver: options.iconResolver,
521
+ state
522
+ });
523
+ if (serviceInfo) {
524
+ baseNode.serviceType = serviceInfo.serviceName;
525
+ baseNode.iconUrl = serviceInfo.iconUrl || void 0;
526
+ }
527
+ }
528
+ return baseNode;
529
+ }
530
+ function extractEdgesFromState(params) {
531
+ const { catchLabelStyle, state, stateName } = params;
532
+ const edges = [];
533
+ switch (state.Type) {
534
+ case "Choice":
535
+ if (state.Choices) state.Choices.forEach((choice) => {
536
+ const condition = extractConditionLabel(choice);
537
+ edges.push({
538
+ condition,
539
+ from: stateName,
540
+ label: condition,
541
+ to: choice.Next,
542
+ type: "choice"
543
+ });
544
+ });
545
+ if (state.Default) {
546
+ if (!(state.Choices?.map((choice) => choice.Next) || []).includes(state.Default)) edges.push({
547
+ from: stateName,
548
+ label: EDGE_LABELS.DEFAULT,
549
+ to: state.Default,
550
+ type: "default"
551
+ });
552
+ }
553
+ break;
554
+ case "Parallel": break;
555
+ case "Map": break;
556
+ default:
557
+ if (state.Next) edges.push({
558
+ from: stateName,
559
+ to: state.Next,
560
+ type: "normal"
561
+ });
562
+ break;
563
+ }
564
+ if (state.Catch) state.Catch.forEach((catchBlock, index) => {
565
+ if (catchBlock.Next) edges.push({
566
+ from: stateName,
567
+ label: getCatchLabel({
568
+ catchLabelStyle,
569
+ errorTypes: catchBlock.ErrorEquals,
570
+ index
571
+ }),
572
+ to: catchBlock.Next,
573
+ type: "error"
574
+ });
575
+ });
576
+ return edges;
577
+ }
578
+ function extractConditionLabel(choice) {
579
+ const conditions = [];
580
+ const variable = choice.Variable || "";
581
+ if (choice.StringEquals !== void 0) conditions.push(`${variable} == "${choice.StringEquals}"`);
582
+ if (choice.NumericEquals !== void 0) conditions.push(`${variable} == ${choice.NumericEquals}`);
583
+ if (choice.BooleanEquals !== void 0) conditions.push(`${variable} == ${choice.BooleanEquals}`);
584
+ return conditions.join(" AND ") || EDGE_LABELS.CONDITION_FALLBACK;
585
+ }
586
+ /**
587
+ * Recursively extract all states including those nested in Parallel branches and Map iterators
588
+ */
589
+ function extractStatesRecursively(params) {
590
+ const { definition, nodes, options } = params;
591
+ for (const [stateName, state] of Object.entries(definition.States)) {
592
+ const stateNode = createStateNode({
593
+ name: stateName,
594
+ options,
595
+ state,
596
+ stylePreset: options?.stylePreset
597
+ });
598
+ nodes.push(stateNode);
599
+ if (state.Type === "Parallel" && state.Branches) state.Branches.forEach((branch, index) => {
600
+ extractStatesRecursively({
601
+ definition: branch,
602
+ nodes,
603
+ options
604
+ });
605
+ if (nodes.find((node) => node.id === branch.StartAt)) stateNode.children?.push(branch.StartAt);
606
+ const endNodeId = `${stateName}__branch${index}__end`;
607
+ const endNode = {
608
+ id: endNodeId,
609
+ isContainer: false,
610
+ label: "",
611
+ style: {
612
+ fill: "#fff9cc",
613
+ shape: "circle",
614
+ stroke: "#687078",
615
+ strokeWidth: .6
616
+ },
617
+ type: "BranchEnd"
618
+ };
619
+ nodes.push(endNode);
620
+ stateNode.children?.push(endNodeId);
621
+ markBranchStatesAsChildren({
622
+ branch,
623
+ containerNode: stateNode,
624
+ nodes
625
+ });
626
+ });
627
+ if (state.Type === "Map" && state.Iterator) {
628
+ const iterator = state.Iterator;
629
+ extractStatesRecursively({
630
+ definition: iterator,
631
+ nodes,
632
+ options
633
+ });
634
+ if (nodes.find((node) => node.id === iterator.StartAt)) stateNode.children?.push(iterator.StartAt);
635
+ const endNodeId = `${stateName}__iterator__end`;
636
+ const endNode = {
637
+ id: endNodeId,
638
+ isContainer: false,
639
+ label: "",
640
+ style: {
641
+ fill: "#fff9cc",
642
+ shape: "circle",
643
+ stroke: "#687078",
644
+ strokeWidth: .6
645
+ },
646
+ type: "IteratorEnd"
647
+ };
648
+ nodes.push(endNode);
649
+ stateNode.children?.push(endNodeId);
650
+ markBranchStatesAsChildren({
651
+ branch: iterator,
652
+ containerNode: stateNode,
653
+ nodes
654
+ });
655
+ }
656
+ }
657
+ }
658
+ /**
659
+ * Mark all states in a branch as children of the container for bounding box calculation
660
+ */
661
+ function markBranchStatesAsChildren(params) {
662
+ const { branch, containerNode, nodes } = params;
663
+ for (const stateName of Object.keys(branch.States)) if (nodes.find((n) => n.id === stateName) && !containerNode.children?.includes(stateName)) containerNode.children?.push(stateName);
664
+ }
665
+ /**
666
+ * Extract edges from nested state machines (Parallel branches and Map iterators)
667
+ */
668
+ function extractNestedEdges(params) {
669
+ const { definition, edges, options } = params;
670
+ for (const [stateName, state] of Object.entries(definition.States)) {
671
+ if (state.Type === "Parallel" && state.Branches) state.Branches.forEach((branch, index) => {
672
+ const endNodeId = `${stateName}__branch${index}__end`;
673
+ edges.push({
674
+ from: stateName,
675
+ to: branch.StartAt,
676
+ type: "normal",
677
+ visualOnly: true
678
+ });
679
+ for (const [branchStateName, branchState] of Object.entries(branch.States)) {
680
+ const branchEdges = extractEdgesFromState({
681
+ catchLabelStyle: options?.catchLabelStyle,
682
+ state: branchState,
683
+ stateName: branchStateName
684
+ });
685
+ edges.push(...branchEdges);
686
+ if (branchState.End || !branchState.Next && branchState.Type !== "Choice") edges.push({
687
+ from: branchStateName,
688
+ to: endNodeId,
689
+ type: "normal"
690
+ });
691
+ }
692
+ if (state.Next) edges.push({
693
+ from: endNodeId,
694
+ to: state.Next,
695
+ type: "normal"
696
+ });
697
+ if (state.Branches && index === state.Branches.length - 1 && state.Next) edges.push({
698
+ from: stateName,
699
+ to: state.Next,
700
+ type: "normal",
701
+ visualOnly: true
702
+ });
703
+ extractNestedEdges({
704
+ definition: branch,
705
+ edges,
706
+ options
707
+ });
708
+ });
709
+ if (state.Type === "Map" && state.Iterator) {
710
+ const endNodeId = `${stateName}__iterator__end`;
711
+ edges.push({
712
+ from: stateName,
713
+ to: state.Iterator.StartAt,
714
+ type: "normal",
715
+ visualOnly: true
716
+ });
717
+ for (const [iteratorStateName, iteratorState] of Object.entries(state.Iterator.States)) {
718
+ const iteratorEdges = extractEdgesFromState({
719
+ catchLabelStyle: options?.catchLabelStyle,
720
+ state: iteratorState,
721
+ stateName: iteratorStateName
722
+ });
723
+ edges.push(...iteratorEdges);
724
+ if (iteratorState.End || !iteratorState.Next && iteratorState.Type !== "Choice") edges.push({
725
+ from: iteratorStateName,
726
+ to: endNodeId,
727
+ type: "normal"
728
+ });
729
+ }
730
+ if (state.Next) {
731
+ edges.push({
732
+ from: endNodeId,
733
+ to: state.Next,
734
+ type: "normal"
735
+ });
736
+ edges.push({
737
+ from: stateName,
738
+ to: state.Next,
739
+ type: "normal",
740
+ visualOnly: true
741
+ });
742
+ }
743
+ extractNestedEdges({
744
+ definition: state.Iterator,
745
+ edges,
746
+ options
747
+ });
748
+ }
749
+ }
750
+ }
751
+
752
+ //#endregion
753
+ //#region src/layout/DagreLayout.ts
754
+ /**
755
+ * DagreLayout - Calculates node positions and edge routing using Dagre algorithm
756
+ */
757
+ var DagreLayout = class {
758
+ options;
759
+ constructor(options) {
760
+ this.options = options;
761
+ }
762
+ /**
763
+ * Calculate layout positions for nodes and edges
764
+ */
765
+ calculate(nodes, edges) {
766
+ const graph = new dagre.graphlib.Graph();
767
+ graph.setGraph({
768
+ marginx: this.options.padding || 20,
769
+ marginy: this.options.padding || 20,
770
+ nodesep: this.options.nodeSeparation || 50,
771
+ rankdir: this.options.layout || "TB",
772
+ ranksep: this.options.rankSeparation || 50
773
+ });
774
+ graph.setDefaultEdgeLabel(() => ({}));
775
+ const layoutNodes = nodes.filter((node) => !node.isContainer);
776
+ layoutNodes.forEach((node) => {
777
+ const dimensions = this.getNodeDimensions(node);
778
+ graph.setNode(node.id, {
779
+ height: dimensions.height,
780
+ label: node.label,
781
+ shape: node.style?.shape || "rect",
782
+ width: dimensions.width
783
+ });
784
+ });
785
+ edges.filter((edge) => !edge.visualOnly).forEach((edge) => {
786
+ graph.setEdge(edge.from, edge.to, {
787
+ label: edge.label,
788
+ type: edge.type
789
+ });
790
+ });
791
+ dagre.layout(graph);
792
+ const positionedNodes = layoutNodes.map((node) => {
793
+ const dagNode = graph.node(node.id);
794
+ return {
795
+ ...node,
796
+ height: dagNode.height,
797
+ width: dagNode.width,
798
+ x: dagNode.x,
799
+ y: dagNode.y
800
+ };
801
+ });
802
+ const containerNodes = this.calculateContainerBounds({
803
+ containers: nodes.filter((node) => node.isContainer),
804
+ positionedNodes
805
+ });
806
+ const allPositionedNodes = [...positionedNodes, ...containerNodes];
807
+ const routedEdges = edges.map((edge) => {
808
+ if (edge.visualOnly) return {
809
+ ...edge,
810
+ points: this.calculateVisualEdgePoints({
811
+ edge,
812
+ positionedNodes: allPositionedNodes
813
+ })
814
+ };
815
+ const dagEdge = graph.edge(edge.from, edge.to);
816
+ return {
817
+ ...edge,
818
+ points: dagEdge.points
819
+ };
820
+ });
821
+ const graphDims = graph.graph();
822
+ return {
823
+ edges: routedEdges,
824
+ graph: {
825
+ height: this.options.height || (graphDims.height ?? 600),
826
+ width: this.options.width || (graphDims.width ?? 800)
827
+ },
828
+ nodes: allPositionedNodes
829
+ };
830
+ }
831
+ /**
832
+ * Calculate bounding boxes for container nodes based on their children positions
833
+ */
834
+ calculateContainerBounds(params) {
835
+ const { containers, positionedNodes } = params;
836
+ return containers.map((container) => {
837
+ const children = positionedNodes.filter((node) => container.children?.includes(node.id) && node.type !== "BranchEnd" && node.type !== "IteratorEnd");
838
+ if (children.length === 0) return {
839
+ ...container,
840
+ height: 200,
841
+ width: 400,
842
+ x: 0,
843
+ y: 0
844
+ };
845
+ const padding = 40;
846
+ const headerHeight = 50;
847
+ const minX = Math.min(...children.map((child) => (child.x || 0) - (child.width || 0) / 2));
848
+ const maxX = Math.max(...children.map((child) => (child.x || 0) + (child.width || 0) / 2));
849
+ const minY = Math.min(...children.map((child) => (child.y || 0) - (child.height || 0) / 2));
850
+ const maxY = Math.max(...children.map((child) => (child.y || 0) + (child.height || 0) / 2));
851
+ const width = maxX - minX + padding * 2;
852
+ const height = maxY - minY + padding * 2 + headerHeight;
853
+ const x = (minX + maxX) / 2;
854
+ const y = (minY + maxY) / 2 + headerHeight / 2;
855
+ return {
856
+ ...container,
857
+ height,
858
+ width,
859
+ x,
860
+ y
861
+ };
862
+ });
863
+ }
864
+ /**
865
+ * Calculate routing points for visual-only edges
866
+ */
867
+ calculateVisualEdgePoints(params) {
868
+ const { edge, positionedNodes } = params;
869
+ const fromNode = positionedNodes.find((node) => node.id === edge.from);
870
+ const toNode = positionedNodes.find((node) => node.id === edge.to);
871
+ if (!fromNode || !toNode) return [];
872
+ if (fromNode.isContainer && fromNode.children?.includes(edge.to)) {
873
+ const headerHeight = 50;
874
+ const fromX$1 = fromNode.x || 0;
875
+ const toX$1 = toNode.x || 0;
876
+ const fromY$1 = (fromNode.y || 0) - (fromNode.height || 0) / 2 + headerHeight;
877
+ const toY$1 = (toNode.y || 0) - (toNode.height || 0) / 2;
878
+ return [{
879
+ x: fromX$1,
880
+ y: fromY$1
881
+ }, {
882
+ x: toX$1,
883
+ y: toY$1
884
+ }];
885
+ }
886
+ const fromX = fromNode.x || 0;
887
+ const fromY = (fromNode.y || 0) + (fromNode.height || 0) / 2;
888
+ const toX = toNode.x || 0;
889
+ const toY = (toNode.y || 0) - (toNode.height || 0) / 2;
890
+ return [{
891
+ x: fromX,
892
+ y: fromY
893
+ }, {
894
+ x: toX,
895
+ y: toY
896
+ }];
897
+ }
898
+ /**
899
+ * Get node dimensions based on shape and options
900
+ */
901
+ getNodeDimensions(node) {
902
+ const baseWidth = this.options.nodeWidth || 120;
903
+ const baseHeight = this.options.nodeHeight || 60;
904
+ switch (node.style?.shape) {
905
+ case "circle": {
906
+ if (node.type === "BranchEnd" || node.type === "IteratorEnd") return {
907
+ height: 16,
908
+ width: 16
909
+ };
910
+ const terminalDiameter = baseHeight * 1.4;
911
+ return {
912
+ height: terminalDiameter,
913
+ width: terminalDiameter
914
+ };
915
+ }
916
+ case "diamond": return {
917
+ height: baseHeight * 1.2,
918
+ width: baseWidth * 1.2
919
+ };
920
+ default: return {
921
+ height: baseHeight,
922
+ width: baseWidth
923
+ };
924
+ }
925
+ }
926
+ };
927
+
928
+ //#endregion
929
+ //#region src/renderers/SvgRenderer.ts
930
+ /**
931
+ * SvgRenderer - Generates SVG diagrams from positioned nodes and edges
932
+ */
933
+ var SvgRenderer = class {
934
+ options;
935
+ theme;
936
+ constructor(options) {
937
+ this.options = options;
938
+ this.theme = getTheme(options.theme, options.customColors);
939
+ }
940
+ /**
941
+ * Render the diagram to SVG string
942
+ */
943
+ render(layout) {
944
+ const document = new JSDOM("<!DOCTYPE html><html><body></body></html>").window.document;
945
+ const bounds = this.calculateBounds(layout);
946
+ const svgNode = document.createElementNS("http://www.w3.org/2000/svg", "svg");
947
+ const svg = select(svgNode).attr("width", bounds.width).attr("height", bounds.height).attr("xmlns", "http://www.w3.org/2000/svg").attr("viewBox", `${bounds.minX} ${bounds.minY} ${bounds.width} ${bounds.height}`);
948
+ if (this.theme.background && this.theme.background !== "transparent") svg.append("rect").attr("x", bounds.minX).attr("y", bounds.minY).attr("width", bounds.width).attr("height", bounds.height).attr("fill", this.theme.background);
949
+ const defs = svg.append("defs");
950
+ defs.append("marker").attr("id", "arrowhead-normal").attr("markerWidth", 10).attr("markerHeight", 10).attr("refX", 9).attr("refY", 3).attr("orient", "auto").append("polygon").attr("points", "0 0, 10 3, 0 6").attr("fill", this.theme.edgeColors.normal);
951
+ defs.append("marker").attr("id", "arrowhead-error").attr("markerWidth", 10).attr("markerHeight", 10).attr("refX", 9).attr("refY", 3).attr("orient", "auto").append("polygon").attr("points", "0 0, 10 3, 0 6").attr("fill", this.theme.edgeColors.error);
952
+ defs.append("marker").attr("id", "arrowhead-choice").attr("markerWidth", 10).attr("markerHeight", 10).attr("refX", 9).attr("refY", 3).attr("orient", "auto").append("polygon").attr("points", "0 0, 10 3, 0 6").attr("fill", this.theme.edgeColors.choice);
953
+ defs.append("marker").attr("id", "arrowhead-default").attr("markerWidth", 10).attr("markerHeight", 10).attr("refX", 9).attr("refY", 3).attr("orient", "auto").append("polygon").attr("points", "0 0, 10 3, 0 6").attr("fill", this.theme.edgeColors.default);
954
+ const edgesGroup = svg.append("g").attr("class", "edges");
955
+ const containersGroup = svg.append("g").attr("class", "containers");
956
+ const nodesGroup = svg.append("g").attr("class", "nodes");
957
+ const containerNodes = layout.nodes.filter((node) => node.isContainer);
958
+ const regularNodes = layout.nodes.filter((node) => !node.isContainer);
959
+ layout.edges.forEach((edge) => {
960
+ const fromNode = layout.nodes.find((node) => node.id === edge.from);
961
+ if (fromNode && (fromNode.type === "BranchEnd" || fromNode.type === "IteratorEnd")) return;
962
+ this.renderEdge({
963
+ edge,
964
+ group: edgesGroup
965
+ });
966
+ });
967
+ containerNodes.forEach((node) => {
968
+ this.renderContainer({
969
+ group: containersGroup,
970
+ node
971
+ });
972
+ });
973
+ regularNodes.forEach((node) => {
974
+ this.renderNode({
975
+ group: nodesGroup,
976
+ node
977
+ });
978
+ });
979
+ return {
980
+ height: bounds.height,
981
+ metadata: {
982
+ edgeCount: layout.edges.length,
983
+ nodeCount: layout.nodes.length
984
+ },
985
+ svg: svgNode.outerHTML,
986
+ width: bounds.width
987
+ };
988
+ }
989
+ /**
990
+ * Calculate bounding box including all nodes and edge points
991
+ */
992
+ calculateBounds(layout) {
993
+ let minX = Infinity;
994
+ let minY = Infinity;
995
+ let maxX = -Infinity;
996
+ let maxY = -Infinity;
997
+ layout.nodes.forEach((node) => {
998
+ const halfWidth = (node.width || 0) / 2;
999
+ const halfHeight = (node.height || 0) / 2;
1000
+ minX = Math.min(minX, (node.x || 0) - halfWidth);
1001
+ minY = Math.min(minY, (node.y || 0) - halfHeight);
1002
+ maxX = Math.max(maxX, (node.x || 0) + halfWidth);
1003
+ maxY = Math.max(maxY, (node.y || 0) + halfHeight);
1004
+ });
1005
+ layout.edges.forEach((edge) => {
1006
+ if (edge.points) edge.points.forEach((point) => {
1007
+ minX = Math.min(minX, point.x);
1008
+ minY = Math.min(minY, point.y);
1009
+ maxX = Math.max(maxX, point.x);
1010
+ maxY = Math.max(maxY, point.y);
1011
+ });
1012
+ if (edge.label && edge.points && edge.points.length > 0) {
1013
+ const midpoint = this.getPathMidpoint(edge.points);
1014
+ const labelDimensions = this.calculateLabelDimensions(edge.label);
1015
+ const labelMinX = midpoint.x - labelDimensions.width / 2;
1016
+ const labelMaxX = midpoint.x + labelDimensions.width / 2;
1017
+ const labelMinY = midpoint.y - labelDimensions.height / 2;
1018
+ const labelMaxY = midpoint.y + labelDimensions.height / 2;
1019
+ minX = Math.min(minX, labelMinX);
1020
+ minY = Math.min(minY, labelMinY);
1021
+ maxX = Math.max(maxX, labelMaxX);
1022
+ maxY = Math.max(maxY, labelMaxY);
1023
+ }
1024
+ });
1025
+ const padding = this.options.padding || 20;
1026
+ minX -= padding;
1027
+ minY -= padding;
1028
+ maxX += padding;
1029
+ maxY += padding;
1030
+ return {
1031
+ height: maxY - minY,
1032
+ minX,
1033
+ minY,
1034
+ width: maxX - minX
1035
+ };
1036
+ }
1037
+ /**
1038
+ * Render a container node (Parallel/Map) with bounding box
1039
+ */
1040
+ renderContainer(params) {
1041
+ const { group, node } = params;
1042
+ const containerGroup = group.append("g").attr("class", `container container-${node.type}`).attr("transform", `translate(${node.x}, ${node.y})`);
1043
+ const width = node.width || 480;
1044
+ const height = node.height || 180;
1045
+ const headerHeight = 50;
1046
+ containerGroup.append("rect").attr("x", -width / 2).attr("y", -height / 2).attr("width", width).attr("height", height).attr("rx", 7).attr("fill", node.style?.fill || "#fce4ec").attr("stroke", node.style?.stroke || "#c2185b").attr("stroke-width", 2).attr("opacity", .5);
1047
+ containerGroup.append("rect").attr("x", -width / 2).attr("y", -height / 2).attr("width", width).attr("height", headerHeight).attr("rx", 7).attr("fill", node.style?.fill || "#fce4ec").attr("stroke", node.style?.stroke || "#c2185b").attr("stroke-width", 2);
1048
+ containerGroup.append("text").attr("x", 0).attr("y", -height / 2 + headerHeight / 2).attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("fill", this.theme.textColor).attr("font-size", this.theme.fontSize).attr("font-family", this.theme.fontFamily).text(node.label);
1049
+ if (this.options.showStateTypes) containerGroup.append("text").attr("x", 0).attr("y", -height / 2 + headerHeight / 2 + 18).attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("fill", this.theme.textColor).attr("font-size", this.theme.fontSize - 2).attr("opacity", .7).text(`${node.type} state`);
1050
+ }
1051
+ /**
1052
+ * Render a single node
1053
+ */
1054
+ renderNode(params) {
1055
+ const { group, node } = params;
1056
+ const nodeGroup = group.append("g").attr("class", `node node-${node.type}`).attr("transform", `translate(${node.x}, ${node.y})`);
1057
+ const style = node.style;
1058
+ if (!style) return;
1059
+ switch (style.shape) {
1060
+ case "circle":
1061
+ this.renderCircle({
1062
+ group: nodeGroup,
1063
+ node,
1064
+ style
1065
+ });
1066
+ break;
1067
+ case "diamond":
1068
+ this.renderDiamond({
1069
+ group: nodeGroup,
1070
+ node,
1071
+ style
1072
+ });
1073
+ break;
1074
+ default: this.renderRect({
1075
+ group: nodeGroup,
1076
+ node,
1077
+ style
1078
+ });
1079
+ }
1080
+ if (node.iconUrl && this.options.showIcons) this.renderIcon({
1081
+ group: nodeGroup,
1082
+ iconPosition: this.options.iconPosition || "left",
1083
+ iconSize: this.options.iconSize || 24,
1084
+ iconUrl: node.iconUrl,
1085
+ node
1086
+ });
1087
+ const labelX = this.calculateLabelX({
1088
+ hasIcon: !!node.iconUrl && !!this.options.showIcons,
1089
+ iconPosition: this.options.iconPosition || "left",
1090
+ iconSize: this.options.iconSize || 24,
1091
+ node
1092
+ });
1093
+ const labelY = this.calculateLabelY({
1094
+ hasIcon: !!node.iconUrl && !!this.options.showIcons,
1095
+ iconPosition: this.options.iconPosition || "left",
1096
+ iconSize: this.options.iconSize || 24,
1097
+ node
1098
+ });
1099
+ nodeGroup.append("text").attr("x", labelX).attr("y", labelY).attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("fill", this.theme.textColor).attr("font-size", this.theme.fontSize).attr("font-family", this.theme.fontFamily).text(node.label);
1100
+ if (this.options.showStateTypes) nodeGroup.append("text").attr("x", labelX).attr("y", labelY + 20).attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("fill", this.theme.textColor).attr("font-size", this.theme.fontSize - 2).attr("opacity", .7).text(node.type);
1101
+ }
1102
+ /**
1103
+ * Render rectangle node
1104
+ */
1105
+ renderRect(params) {
1106
+ const { group, node, style } = params;
1107
+ const width = node.width || 120;
1108
+ const height = node.height || 60;
1109
+ group.append("rect").attr("x", -width / 2).attr("y", -height / 2).attr("width", width).attr("height", height).attr("rx", 5).attr("fill", style.fill).attr("stroke", style.stroke).attr("stroke-width", style.strokeWidth);
1110
+ }
1111
+ /**
1112
+ * Render circle node
1113
+ */
1114
+ renderCircle(params) {
1115
+ const { group, node, style } = params;
1116
+ const radius = (node.width || 60) / 2;
1117
+ group.append("circle").attr("r", radius).attr("fill", style.fill).attr("stroke", style.stroke).attr("stroke-width", style.strokeWidth);
1118
+ }
1119
+ /**
1120
+ * Render diamond node
1121
+ */
1122
+ renderDiamond(params) {
1123
+ const { group, node, style } = params;
1124
+ const halfWidth = (node.width || 120) / 2;
1125
+ const halfHeight = (node.height || 60) / 2;
1126
+ const path = `M 0,-${halfHeight} L ${halfWidth},0 L 0,${halfHeight} L -${halfWidth},0 Z`;
1127
+ group.append("path").attr("d", path).attr("fill", style.fill).attr("stroke", style.stroke).attr("stroke-width", style.strokeWidth);
1128
+ }
1129
+ /**
1130
+ * Render AWS service icon within node
1131
+ */
1132
+ renderIcon(params) {
1133
+ const { group, iconPosition, iconSize, iconUrl, node } = params;
1134
+ const width = node.width || 120;
1135
+ const height = node.height || 60;
1136
+ const padding = 8;
1137
+ let iconX = 0;
1138
+ let iconY = 0;
1139
+ switch (iconPosition) {
1140
+ case "left":
1141
+ iconX = -width / 2 + padding;
1142
+ iconY = -iconSize / 2;
1143
+ break;
1144
+ case "top":
1145
+ iconX = -iconSize / 2;
1146
+ iconY = -height / 2 + padding;
1147
+ break;
1148
+ case "right":
1149
+ iconX = width / 2 - iconSize - padding;
1150
+ iconY = -iconSize / 2;
1151
+ break;
1152
+ }
1153
+ group.append("image").attr("x", iconX).attr("y", iconY).attr("width", iconSize).attr("height", iconSize).attr("href", iconUrl).attr("preserveAspectRatio", "xMidYMid meet");
1154
+ }
1155
+ /**
1156
+ * Calculate label X position based on icon presence and position
1157
+ */
1158
+ calculateLabelX(params) {
1159
+ const { hasIcon, iconPosition, iconSize } = params;
1160
+ if (!hasIcon) return 0;
1161
+ const padding = 8;
1162
+ const gap = 4;
1163
+ switch (iconPosition) {
1164
+ case "left": return (padding + iconSize + gap) / 2;
1165
+ case "right": return -(padding + iconSize + gap) / 2;
1166
+ default: return 0;
1167
+ }
1168
+ }
1169
+ /**
1170
+ * Calculate label Y position based on icon presence and position
1171
+ */
1172
+ calculateLabelY(params) {
1173
+ const { hasIcon, iconPosition, iconSize } = params;
1174
+ if (!hasIcon || iconPosition !== "top") return 0;
1175
+ return (8 + iconSize + 4) / 2;
1176
+ }
1177
+ /**
1178
+ * Render an edge
1179
+ */
1180
+ renderEdge(params) {
1181
+ const { edge, group } = params;
1182
+ if (!edge.points || edge.points.length < 2) return;
1183
+ const edgeColor = this.theme.edgeColors[edge.type || "normal"];
1184
+ const markerType = edge.type || "normal";
1185
+ const pathGenerator = line().x((d) => d.x).y((d) => d.y);
1186
+ if (this.options.edgeStyle === "curved") pathGenerator.curve(curveBasis);
1187
+ const pathElement = group.append("path").attr("d", pathGenerator(edge.points)).attr("fill", "none").attr("stroke", edgeColor).attr("stroke-width", edge.type === "error" ? 2 : 1.5).attr("marker-end", `url(#arrowhead-${markerType})`);
1188
+ if (edge.type === "error") pathElement.attr("stroke-dasharray", "5,5");
1189
+ else if (edge.type === "default") pathElement.attr("stroke-dasharray", "8,4");
1190
+ if (edge.label) {
1191
+ const midpoint = this.getPathMidpoint(edge.points);
1192
+ const labelDimensions = this.calculateLabelDimensions(edge.label);
1193
+ group.append("rect").attr("x", midpoint.x - labelDimensions.width / 2).attr("y", midpoint.y - labelDimensions.height / 2).attr("width", labelDimensions.width).attr("height", labelDimensions.height).attr("fill", this.theme.background || "#ffffff").attr("stroke", edgeColor).attr("stroke-width", .5).attr("rx", 3);
1194
+ group.append("text").attr("x", midpoint.x).attr("y", midpoint.y).attr("text-anchor", "middle").attr("dominant-baseline", "middle").attr("fill", edgeColor).attr("font-size", this.theme.fontSize - 2).text(edge.label);
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Get the midpoint of a path for label placement
1199
+ */
1200
+ getPathMidpoint(points) {
1201
+ return points[Math.floor(points.length / 2)];
1202
+ }
1203
+ /**
1204
+ * Calculate dimensions for edge label
1205
+ */
1206
+ calculateLabelDimensions(label) {
1207
+ const fontSize = this.theme.fontSize - 2;
1208
+ const avgCharWidth = fontSize * .6;
1209
+ const padding = 8;
1210
+ const verticalPadding = 4;
1211
+ const width = label.length * avgCharWidth + padding * 2;
1212
+ return {
1213
+ height: fontSize + verticalPadding * 2,
1214
+ width
1215
+ };
1216
+ }
1217
+ };
1218
+
1219
+ //#endregion
1220
+ //#region src/renderers/MermaidRenderer.ts
1221
+ /**
1222
+ * MermaidRenderer - Generates Mermaid state diagram syntax from ASL
1223
+ */
1224
+ var MermaidRenderer = class {
1225
+ /**
1226
+ * Render nodes and edges to Mermaid syntax
1227
+ */
1228
+ render(params) {
1229
+ const { asl, edges, nodes } = params;
1230
+ const lines = [];
1231
+ lines.push("stateDiagram-v2");
1232
+ lines.push("");
1233
+ const startState = this.findStartState({
1234
+ asl,
1235
+ edges,
1236
+ nodes
1237
+ });
1238
+ if (startState) lines.push(` [*] --> ${this.sanitizeId(startState)}`);
1239
+ const stateDefinitions = /* @__PURE__ */ new Set();
1240
+ nodes.forEach((node) => {
1241
+ const id = this.sanitizeId(node.id);
1242
+ if (node.label !== node.id && !stateDefinitions.has(id)) {
1243
+ lines.push(` ${id}: ${this.escapeLabel(node.label)}`);
1244
+ stateDefinitions.add(id);
1245
+ }
1246
+ });
1247
+ if (stateDefinitions.size > 0) lines.push("");
1248
+ edges.forEach((edge) => {
1249
+ const from = this.sanitizeId(edge.from);
1250
+ const to = this.sanitizeId(edge.to);
1251
+ if (edge.label || edge.condition) {
1252
+ const label = this.escapeLabel(edge.condition || edge.label || "");
1253
+ lines.push(` ${from} --> ${to}: ${label}`);
1254
+ } else lines.push(` ${from} --> ${to}`);
1255
+ });
1256
+ const endStates = nodes.filter((node) => node.type === "Succeed" || node.type === "Fail");
1257
+ if (endStates.length > 0) {
1258
+ lines.push("");
1259
+ endStates.forEach((node) => {
1260
+ const id = this.sanitizeId(node.id);
1261
+ lines.push(` ${id} --> [*]`);
1262
+ });
1263
+ }
1264
+ lines.push("");
1265
+ lines.push(" classDef successState fill:#e8f5e8,stroke:#4caf50,stroke-width:3px");
1266
+ lines.push(" classDef failState fill:#ffebee,stroke:#f44336,stroke-width:3px");
1267
+ lines.push(" classDef choiceState fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px");
1268
+ lines.push(" classDef taskState fill:#fff3e0,stroke:#ef6c00,stroke-width:2px");
1269
+ lines.push("");
1270
+ nodes.forEach((node) => {
1271
+ const id = this.sanitizeId(node.id);
1272
+ switch (node.type) {
1273
+ case "Succeed":
1274
+ lines.push(` class ${id} successState`);
1275
+ break;
1276
+ case "Fail":
1277
+ lines.push(` class ${id} failState`);
1278
+ break;
1279
+ case "Choice":
1280
+ lines.push(` class ${id} choiceState`);
1281
+ break;
1282
+ case "Task":
1283
+ lines.push(` class ${id} taskState`);
1284
+ break;
1285
+ }
1286
+ });
1287
+ return {
1288
+ code: lines.join("\n"),
1289
+ metadata: {
1290
+ stateCount: nodes.length,
1291
+ edgeCount: edges.length
1292
+ }
1293
+ };
1294
+ }
1295
+ /**
1296
+ * Sanitize state ID for Mermaid (no spaces, special chars)
1297
+ */
1298
+ sanitizeId(id) {
1299
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
1300
+ }
1301
+ /**
1302
+ * Escape label text for Mermaid
1303
+ */
1304
+ escapeLabel(label) {
1305
+ return label.replace(/"/g, "'").replace(/\n/g, " ");
1306
+ }
1307
+ /**
1308
+ * Find the start state from ASL definition or by analyzing edges
1309
+ */
1310
+ findStartState(params) {
1311
+ const { asl, edges, nodes } = params;
1312
+ if (asl?.StartAt) return asl.StartAt;
1313
+ const targetNodes = new Set(edges.map((edge) => edge.to));
1314
+ return nodes.find((node) => !targetNodes.has(node.id))?.id || nodes[0]?.id || null;
1315
+ }
1316
+ };
1317
+
1318
+ //#endregion
1319
+ //#region src/exporters/PngExporter.ts
1320
+ /**
1321
+ * PngExporter - Converts SVG to PNG using headless rendering
1322
+ */
1323
+ var PngExporter = class {
1324
+ options;
1325
+ constructor(options) {
1326
+ this.options = options;
1327
+ }
1328
+ /**
1329
+ * Convert SVG string to PNG buffer
1330
+ *
1331
+ * Note: External images (like AWS service icons from CDN) may not render in PNG export
1332
+ * due to limitations with headless browser rendering. Use SVG output for best results
1333
+ * when showIcons is enabled.
1334
+ */
1335
+ async convert(params) {
1336
+ const { svg, width, height } = params;
1337
+ return {
1338
+ buffer: await nodeHtmlToImage({
1339
+ html: this.wrapSvgInHtml({
1340
+ svg,
1341
+ width,
1342
+ height
1343
+ }),
1344
+ puppeteerArgs: { args: ["--no-sandbox", "--disable-setuid-sandbox"] },
1345
+ quality: this.options.pngQuality || 90,
1346
+ transparent: this.options.backgroundColor === "transparent",
1347
+ type: "png"
1348
+ }),
1349
+ height,
1350
+ metadata: { format: "png" },
1351
+ width
1352
+ };
1353
+ }
1354
+ /**
1355
+ * Wrap SVG in HTML for rendering
1356
+ */
1357
+ wrapSvgInHtml(params) {
1358
+ const { svg, width, height } = params;
1359
+ return `
1360
+ <!DOCTYPE html>
1361
+ <html>
1362
+ <head>
1363
+ <meta charset="UTF-8">
1364
+ <style>
1365
+ * {
1366
+ margin: 0;
1367
+ padding: 0;
1368
+ box-sizing: border-box;
1369
+ }
1370
+ body {
1371
+ width: ${width}px;
1372
+ height: ${height}px;
1373
+ background: ${this.options.backgroundColor || "transparent"};
1374
+ display: flex;
1375
+ align-items: center;
1376
+ justify-content: center;
1377
+ }
1378
+ svg {
1379
+ max-width: 100%;
1380
+ max-height: 100%;
1381
+ }
1382
+ </style>
1383
+ </head>
1384
+ <body>
1385
+ ${svg}
1386
+ </body>
1387
+ </html>
1388
+ `.trim();
1389
+ }
1390
+ };
1391
+
1392
+ //#endregion
1393
+ //#region src/config/defaults.ts
1394
+ /**
1395
+ * Default diagram options - used when options not provided
1396
+ */
1397
+ const DEFAULT_DIAGRAM_OPTIONS = {
1398
+ format: "svg",
1399
+ theme: "light",
1400
+ customColors: void 0,
1401
+ layout: "TB",
1402
+ rankSeparation: 50,
1403
+ nodeSeparation: 50,
1404
+ width: void 0,
1405
+ height: void 0,
1406
+ nodeWidth: 120,
1407
+ nodeHeight: 60,
1408
+ padding: 20,
1409
+ includeComments: true,
1410
+ showStateTypes: false,
1411
+ edgeStyle: "curved",
1412
+ catchLabelStyle: "error-type",
1413
+ stylePreset: "aws-standard",
1414
+ iconPosition: "left",
1415
+ iconResolver: void 0,
1416
+ iconSize: 24,
1417
+ showIcons: false,
1418
+ pngQuality: 90,
1419
+ backgroundColor: "transparent"
1420
+ };
1421
+ /**
1422
+ * Merge user-provided options with defaults
1423
+ */
1424
+ function mergeOptions(options = {}) {
1425
+ return {
1426
+ ...DEFAULT_DIAGRAM_OPTIONS,
1427
+ ...options
1428
+ };
1429
+ }
1430
+
1431
+ //#endregion
1432
+ //#region src/utils/iconEmbedder.ts
1433
+ /** Default timeout for icon fetch operations in milliseconds */
1434
+ const DEFAULT_FETCH_TIMEOUT_MS = 5e3;
1435
+ /**
1436
+ * Fetch a remote resource and convert it to a base64 data URI
1437
+ *
1438
+ * @param params - Parameters containing the URL and optional timeout
1439
+ * @returns Base64 data URI string, or original URL on failure
1440
+ */
1441
+ async function fetchAsDataUri(params) {
1442
+ const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, url } = params;
1443
+ const controller = new AbortController();
1444
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1445
+ try {
1446
+ const response = await fetch(url, { signal: controller.signal });
1447
+ clearTimeout(timeoutId);
1448
+ if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
1449
+ const buffer = await response.arrayBuffer();
1450
+ const base64 = Buffer.from(buffer).toString("base64");
1451
+ return `data:${url.endsWith(".svg") ? "image/svg+xml" : "image/png"};base64,${base64}`;
1452
+ } catch (error) {
1453
+ clearTimeout(timeoutId);
1454
+ const errorMessage = error instanceof Error && error.name === "AbortError" ? `Timeout after ${timeoutMs}ms` : error;
1455
+ console.warn(`Failed to embed icon from ${url}:`, errorMessage);
1456
+ return url;
1457
+ }
1458
+ }
1459
+ /**
1460
+ * Embed external icon URLs in SVG as base64 data URIs
1461
+ *
1462
+ * This function scans the SVG for external image references (href attributes)
1463
+ * and replaces them with inline base64-encoded data URIs. This ensures icons
1464
+ * display correctly in standalone SVG files, PNG exports, and contexts where
1465
+ * external resources are blocked by security policies.
1466
+ *
1467
+ * @param params - Parameters for icon embedding
1468
+ * @param params.svg - SVG string containing external icon URLs
1469
+ * @returns Promise resolving to SVG string with embedded icons
1470
+ *
1471
+ * @example
1472
+ * const svgWithExternalIcons = generateSvg({ aslDefinition, showIcons: true });
1473
+ * const svgWithEmbeddedIcons = await embedIcons({ svg: svgWithExternalIcons.svg });
1474
+ * writeFileSync('diagram.svg', svgWithEmbeddedIcons);
1475
+ *
1476
+ * @example
1477
+ * // Chain with PNG export
1478
+ * const { svg } = generateSvg({ aslDefinition, showIcons: true });
1479
+ * const embeddedSvg = await embedIcons({ svg });
1480
+ * const png = await exportPng({ svg: embeddedSvg });
1481
+ */
1482
+ async function embedIcons(params) {
1483
+ const { svg, timeoutMs } = params;
1484
+ const matches = Array.from(svg.matchAll(/href="(https?:\/\/[^"]+)"/g));
1485
+ if (matches.length === 0) return svg;
1486
+ const uniqueUrls = [...new Set(matches.map((match) => match[1]))];
1487
+ const urlToDataUri = /* @__PURE__ */ new Map();
1488
+ await Promise.all(uniqueUrls.map(async (url) => {
1489
+ const dataUri = await fetchAsDataUri({
1490
+ timeoutMs,
1491
+ url
1492
+ });
1493
+ urlToDataUri.set(url, dataUri);
1494
+ }));
1495
+ let embeddedSvg = svg;
1496
+ urlToDataUri.forEach((dataUri, url) => {
1497
+ embeddedSvg = embeddedSvg.replaceAll(`href="${url}"`, `href="${dataUri}"`);
1498
+ });
1499
+ return embeddedSvg;
1500
+ }
1501
+
1502
+ //#endregion
1503
+ //#region src/index.ts
1504
+ /**
1505
+ * Generate an SVG diagram from an AWS Step Functions ASL definition
1506
+ *
1507
+ * This function parses an ASL definition and renders it as an SVG diagram using D3.js
1508
+ * with automatic graph layout via Dagre. The output is a complete SVG string that can
1509
+ * be saved to a file or embedded in HTML.
1510
+ *
1511
+ * @param params - Configuration object
1512
+ * @param params.aslDefinition - ASL definition as an object or JSON string
1513
+ * @param params.theme - Color theme: 'light' (default), 'dark', or a CustomTheme object
1514
+ * @param params.layout - Layout direction: 'TB' (top-bottom, default), 'LR' (left-right), 'RL', or 'BT'
1515
+ * @param params.nodeWidth - Width of state nodes in pixels (default: 120)
1516
+ * @param params.nodeHeight - Height of state nodes in pixels (default: 60)
1517
+ * @param params.rankSeparation - Vertical separation between ranks in pixels (default: 50)
1518
+ * @param params.nodeSeparation - Horizontal separation between nodes in pixels (default: 50)
1519
+ * @param params.padding - Padding around the diagram in pixels (default: 20)
1520
+ * @param params.edgeStyle - Edge path style: 'curved' (default), 'straight', or 'orthogonal'
1521
+ * @param params.showStateTypes - Whether to display state types on nodes (default: false)
1522
+ * @param params.includeComments - Whether to use state comments as labels (default: true)
1523
+ * @param params.customColors - Override colors for specific state types
1524
+ *
1525
+ * @returns SVG output containing the diagram string, dimensions, and metadata
1526
+ *
1527
+ * @throws {SyntaxError} If params.asl is a string with invalid JSON
1528
+ * @throws {Error} If the ASL definition structure is invalid
1529
+ *
1530
+ * @example
1531
+ * ```typescript
1532
+ * import { generateSvg } from 'sfn-diagram';
1533
+ * import { writeFileSync } from 'fs';
1534
+ *
1535
+ * const asl = {
1536
+ * StartAt: 'HelloWorld',
1537
+ * States: {
1538
+ * HelloWorld: { Type: 'Pass', Result: 'Hello!', End: true }
1539
+ * }
1540
+ * };
1541
+ *
1542
+ * const { svg, width, height } = generateSvg({ aslDefinition: asl, theme: 'dark', layout: 'LR' });
1543
+ * writeFileSync('diagram.svg', svg);
1544
+ * console.log(`Generated ${width}x${height} diagram`);
1545
+ * ```
1546
+ *
1547
+ * @example
1548
+ * ```typescript
1549
+ * // From JSON string
1550
+ * const aslJson = fs.readFileSync('state-machine.json', 'utf-8');
1551
+ * const result = generateSvg({ asl: aslJson, theme: 'light' });
1552
+ * ```
1553
+ *
1554
+ * @example
1555
+ * ```typescript
1556
+ * // With AWS service icons
1557
+ * const { svg } = generateSvg({
1558
+ * aslDefinition: asl,
1559
+ * showIcons: true,
1560
+ * iconPosition: 'left',
1561
+ * nodeWidth: 150
1562
+ * });
1563
+ * ```
1564
+ */
1565
+ function generateSvg(params) {
1566
+ const { aslDefinition, ...options } = params;
1567
+ const aslObj = typeof aslDefinition === "string" ? JSON.parse(aslDefinition) : aslDefinition;
1568
+ const mergedOptions = mergeOptions(options);
1569
+ const { nodes, edges } = parseAsl({
1570
+ definition: aslObj,
1571
+ options: mergedOptions
1572
+ });
1573
+ const positioned = new DagreLayout(mergedOptions).calculate(nodes, edges);
1574
+ return new SvgRenderer(mergedOptions).render(positioned);
1575
+ }
1576
+ /**
1577
+ * Generate Mermaid diagram syntax from an AWS Step Functions ASL definition
1578
+ *
1579
+ * This function converts an ASL definition into Mermaid state diagram syntax,
1580
+ * which can be rendered using the Mermaid library or included in Markdown documentation.
1581
+ * The output includes CSS classes for styling different state types.
1582
+ *
1583
+ * @param params - Configuration object
1584
+ * @param params.aslDefinition - ASL definition as an object or JSON string
1585
+ *
1586
+ * @returns Mermaid output containing the diagram code and metadata
1587
+ *
1588
+ * @throws {SyntaxError} If params.asl is a string with invalid JSON
1589
+ * @throws {Error} If the ASL definition structure is invalid
1590
+ *
1591
+ * @example
1592
+ * ```typescript
1593
+ * import { generateMermaid } from 'sfn-diagram';
1594
+ *
1595
+ * const asl = {
1596
+ * StartAt: 'Process',
1597
+ * States: {
1598
+ * Process: { Type: 'Task', Resource: 'arn:aws:...', Next: 'Done' },
1599
+ * Done: { Type: 'Succeed' }
1600
+ * }
1601
+ * };
1602
+ *
1603
+ * const { code, metadata } = generateMermaid({ asl });
1604
+ * console.log(code);
1605
+ * // Output:
1606
+ * // stateDiagram-v2
1607
+ * // [*] --> Process
1608
+ * // Process --> Done
1609
+ * // Done --> [*]
1610
+ * // ...
1611
+ *
1612
+ * console.log(`Generated diagram with ${metadata.stateCount} states`);
1613
+ * ```
1614
+ *
1615
+ * @example
1616
+ * ```typescript
1617
+ * // Use in Markdown
1618
+ * const { code } = generateMermaid({ asl: myStateMachine });
1619
+ * const markdown = `\`\`\`mermaid\n${code}\n\`\`\``;
1620
+ * fs.writeFileSync('diagram.md', markdown);
1621
+ * ```
1622
+ */
1623
+ function generateMermaid(params) {
1624
+ const { aslDefinition, ...options } = params;
1625
+ const aslObj = typeof aslDefinition === "string" ? JSON.parse(aslDefinition) : aslDefinition;
1626
+ const { nodes, edges } = parseAsl({
1627
+ definition: aslObj,
1628
+ options: mergeOptions(options)
1629
+ });
1630
+ return new MermaidRenderer().render({
1631
+ nodes,
1632
+ edges,
1633
+ asl: aslObj
1634
+ });
1635
+ }
1636
+ /**
1637
+ * Generate a diagram from an ASL definition (auto-detects format from options)
1638
+ *
1639
+ * This is a convenience function that generates either an SVG or Mermaid diagram
1640
+ * based on the `format` option. If no format is specified, defaults to SVG.
1641
+ *
1642
+ * @param params - Configuration object
1643
+ * @param params.aslDefinition - ASL definition as an object or JSON string
1644
+ * @param params.format - Output format: 'svg' (default) or 'mermaid'
1645
+ * @param params.theme - Color theme (SVG only)
1646
+ * @param params.layout - Layout direction (SVG only)
1647
+ * @param ...params - Other options (see generateSvg or generateMermaid)
1648
+ *
1649
+ * @returns SVG output if format is 'svg', Mermaid output if format is 'mermaid'
1650
+ *
1651
+ * @throws {SyntaxError} If params.asl is a string with invalid JSON
1652
+ * @throws {Error} If the ASL definition structure is invalid
1653
+ *
1654
+ * @example
1655
+ * ```typescript
1656
+ * import { generateDiagram } from 'sfn-diagram';
1657
+ *
1658
+ * // Generate SVG (default)
1659
+ * const svgResult = generateDiagram({ asl: myAsl });
1660
+ *
1661
+ * // Generate Mermaid
1662
+ * const mermaidResult = generateDiagram({ asl: myAsl, format: 'mermaid' });
1663
+ * ```
1664
+ */
1665
+ function generateDiagram(params) {
1666
+ const { aslDefinition, format, ...options } = params;
1667
+ if (mergeOptions({
1668
+ format,
1669
+ ...options
1670
+ }).format === "mermaid") return generateMermaid({
1671
+ aslDefinition,
1672
+ ...options
1673
+ });
1674
+ return generateSvg({
1675
+ aslDefinition,
1676
+ ...options
1677
+ });
1678
+ }
1679
+ /**
1680
+ * Export a diagram as PNG (asynchronous)
1681
+ *
1682
+ * This function generates an SVG diagram and converts it to PNG format using
1683
+ * headless browser rendering. The output is a Buffer that can be written to a file.
1684
+ *
1685
+ * **Note:** This function is asynchronous and uses Puppeteer for rendering,
1686
+ * which may take a moment on first run to download the browser binary.
1687
+ *
1688
+ * @param params - Configuration object
1689
+ * @param params.aslDefinition - ASL definition as an object or JSON string
1690
+ * @param params.pngQuality - PNG quality from 1-100 (default: 90)
1691
+ * @param params.backgroundColor - Background color as CSS color or 'transparent' (default: 'transparent')
1692
+ * @param ...params - Other SVG options (see generateSvg)
1693
+ *
1694
+ * @returns Promise resolving to PNG output with buffer and dimensions
1695
+ *
1696
+ * @throws {SyntaxError} If params.asl is a string with invalid JSON
1697
+ * @throws {Error} If the ASL definition structure is invalid or PNG conversion fails
1698
+ *
1699
+ * @example
1700
+ * ```typescript
1701
+ * import { exportPng } from 'sfn-diagram';
1702
+ * import { writeFileSync } from 'fs';
1703
+ *
1704
+ * const asl = { StartAt: 'Hello', States: { Hello: { Type: 'Pass', End: true } } };
1705
+ *
1706
+ * const { buffer, width, height } = await exportPng({
1707
+ * asl,
1708
+ * theme: 'dark',
1709
+ * pngQuality: 95,
1710
+ * backgroundColor: 'white'
1711
+ * });
1712
+ *
1713
+ * writeFileSync('diagram.png', buffer);
1714
+ * console.log(`Saved ${width}x${height} PNG`);
1715
+ * ```
1716
+ */
1717
+ async function exportPng(params) {
1718
+ const { aslDefinition, ...options } = params;
1719
+ const svgOutput = generateSvg({
1720
+ aslDefinition,
1721
+ ...options
1722
+ });
1723
+ return new PngExporter(options).convert({
1724
+ svg: svgOutput.svg,
1725
+ width: svgOutput.width,
1726
+ height: svgOutput.height
1727
+ });
1728
+ }
1729
+ /**
1730
+ * Generate a diagram from an AWS SDK DescribeStateMachine response
1731
+ *
1732
+ * This is a convenience function for integrating with the AWS SDK. It extracts
1733
+ * the ASL definition from the response and generates a diagram.
1734
+ *
1735
+ * @param params - Configuration object
1736
+ * @param params.response - AWS SDK DescribeStateMachine response object
1737
+ * @param params.format - Output format: 'svg' (default) or 'mermaid'
1738
+ * @param ...params - Other options (see generateSvg or generateMermaid)
1739
+ *
1740
+ * @returns SVG output if format is 'svg', Mermaid output if format is 'mermaid'
1741
+ *
1742
+ * @throws {Error} If response.definition is missing
1743
+ * @throws {SyntaxError} If the definition contains invalid JSON
1744
+ * @throws {Error} If the ASL definition structure is invalid
1745
+ *
1746
+ * @example
1747
+ * ```typescript
1748
+ * import { SFNClient, DescribeStateMachineCommand } from '@aws-sdk/client-sfn';
1749
+ * import { generateFromAwsResponse } from 'sfn-diagram';
1750
+ *
1751
+ * const client = new SFNClient({ region: 'us-east-1' });
1752
+ * const response = await client.send(
1753
+ * new DescribeStateMachineCommand({
1754
+ * stateMachineArn: 'arn:aws:states:...'
1755
+ * })
1756
+ * );
1757
+ *
1758
+ * const { svg } = generateFromAwsResponse({
1759
+ * response,
1760
+ * theme: 'light',
1761
+ * layout: 'TB'
1762
+ * });
1763
+ * ```
1764
+ */
1765
+ function generateFromAwsResponse(params) {
1766
+ const { response, ...options } = params;
1767
+ if (!response.definition) throw new Error("No definition found in AWS response");
1768
+ return generateDiagram({
1769
+ aslDefinition: response.definition,
1770
+ ...options
1771
+ });
1772
+ }
1773
+ /**
1774
+ * Class-based API for advanced usage with fluent interface
1775
+ *
1776
+ * This class allows you to configure diagram options once and generate multiple
1777
+ * diagrams with the same settings. Options can be updated using the fluent
1778
+ * `setOptions()` method.
1779
+ *
1780
+ * @example
1781
+ * ```typescript
1782
+ * import { SfnDiagramGenerator } from 'sfn-diagram';
1783
+ *
1784
+ * const generator = new SfnDiagramGenerator({
1785
+ * theme: 'dark',
1786
+ * layout: 'LR',
1787
+ * nodeWidth: 150
1788
+ * });
1789
+ *
1790
+ * // Generate multiple diagrams with the same options
1791
+ * const diagram1 = generator.generateSvg({ asl: stateMachine1 });
1792
+ * const diagram2 = generator.generateSvg({ asl: stateMachine2 });
1793
+ *
1794
+ * // Update options and generate more
1795
+ * generator.setOptions({ theme: 'light' });
1796
+ * const diagram3 = generator.generateSvg({ asl: stateMachine3 });
1797
+ * ```
1798
+ */
1799
+ var SfnDiagramGenerator = class {
1800
+ options;
1801
+ /**
1802
+ * Create a new diagram generator with default options
1803
+ *
1804
+ * @param options - Default diagram options for all subsequent generations
1805
+ *
1806
+ * @example
1807
+ * ```typescript
1808
+ * const generator = new SfnDiagramGenerator({ theme: 'dark', layout: 'LR' });
1809
+ * ```
1810
+ */
1811
+ constructor(options = {}) {
1812
+ this.options = mergeOptions(options);
1813
+ }
1814
+ /**
1815
+ * Generate a diagram (auto-detects format from options)
1816
+ *
1817
+ * @param params - Generation parameters
1818
+ * @param params.asl - ASL definition as an object or JSON string
1819
+ * @returns SVG or Mermaid output based on format option
1820
+ *
1821
+ * @example
1822
+ * ```typescript
1823
+ * const result = generator.generate({ asl: myStateMachine });
1824
+ * ```
1825
+ */
1826
+ generate(params) {
1827
+ return generateDiagram({
1828
+ ...params,
1829
+ ...this.options
1830
+ });
1831
+ }
1832
+ /**
1833
+ * Generate an SVG diagram
1834
+ *
1835
+ * @param params - Generation parameters
1836
+ * @param params.asl - ASL definition as an object or JSON string
1837
+ * @returns SVG output with diagram and metadata
1838
+ *
1839
+ * @example
1840
+ * ```typescript
1841
+ * const { svg } = generator.generateSvg({ asl: myStateMachine });
1842
+ * ```
1843
+ */
1844
+ generateSvg(params) {
1845
+ return generateSvg({
1846
+ ...params,
1847
+ ...this.options
1848
+ });
1849
+ }
1850
+ /**
1851
+ * Generate Mermaid diagram syntax
1852
+ *
1853
+ * @param params - Generation parameters
1854
+ * @param params.asl - ASL definition as an object or JSON string
1855
+ * @returns Mermaid output with code and metadata
1856
+ *
1857
+ * @example
1858
+ * ```typescript
1859
+ * const { code } = generator.generateMermaid({ asl: myStateMachine });
1860
+ * ```
1861
+ */
1862
+ generateMermaid(params) {
1863
+ return generateMermaid({
1864
+ ...params,
1865
+ ...this.options
1866
+ });
1867
+ }
1868
+ /**
1869
+ * Export diagram as PNG
1870
+ *
1871
+ * @param params - Generation parameters
1872
+ * @param params.asl - ASL definition as an object or JSON string
1873
+ * @returns Promise resolving to PNG output
1874
+ *
1875
+ * @example
1876
+ * ```typescript
1877
+ * const { buffer } = await generator.exportPng({ asl: myStateMachine });
1878
+ * ```
1879
+ */
1880
+ async exportPng(params) {
1881
+ return exportPng({
1882
+ ...params,
1883
+ ...this.options
1884
+ });
1885
+ }
1886
+ /**
1887
+ * Update the generator's default options (fluent interface)
1888
+ *
1889
+ * @param options - Partial options to merge with existing options
1890
+ * @returns This generator instance for method chaining
1891
+ *
1892
+ * @example
1893
+ * ```typescript
1894
+ * generator
1895
+ * .setOptions({ theme: 'dark' })
1896
+ * .setOptions({ layout: 'LR' });
1897
+ *
1898
+ * const result = generator.generateSvg({ asl: myAsl });
1899
+ * ```
1900
+ */
1901
+ setOptions(options) {
1902
+ this.options = mergeOptions({
1903
+ ...this.options,
1904
+ ...options
1905
+ });
1906
+ return this;
1907
+ }
1908
+ };
1909
+
1910
+ //#endregion
1911
+ export { AWS_DARK_THEME, AWS_LIGHT_THEME, AslValidationError, PngExporter, SfnDiagramGenerator, embedIcons, exportPng, generateDiagram, generateFromAwsResponse, generateMermaid, generateSvg, validateAsl };