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