@zenuml/core 4.2.0 → 4.3.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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import antlr4 from 'antlr4';
3
3
  import { marked } from 'marked';
4
- import { mkdirSync, writeFileSync, readFileSync, statSync, watch } from 'node:fs';
5
- import { resolve, dirname, extname, basename, join } from 'node:path';
4
+ import { readFileSync, statSync, mkdirSync, writeFileSync, watch } from 'node:fs';
5
+ import { resolve, join, dirname, basename, extname } from 'node:path';
6
6
 
7
7
  // Generated from java-escape by ANTLR 4.11.1
8
8
  // jshint ignore: start
@@ -9075,7 +9075,7 @@ ToCollector.enterRet = function (ctx) {
9075
9075
  returnTo && participants.Add(returnTo);
9076
9076
  };
9077
9077
 
9078
- const walker$2 = antlr4.tree.ParseTreeWalker.DEFAULT;
9078
+ const walker$1 = antlr4.tree.ParseTreeWalker.DEFAULT;
9079
9079
 
9080
9080
  // The walk is a pure function of the (immutable) parse tree and consumers only
9081
9081
  // read the returned Participants, so results are cached per context node.
@@ -9086,80 +9086,11 @@ ToCollector.getParticipants = function (context) {
9086
9086
  const cached = participantsCache.get(context);
9087
9087
  if (cached) return cached;
9088
9088
  participants = new Participants$1();
9089
- walker$2.walk(this, context);
9089
+ walker$1.walk(this, context);
9090
9090
  participantsCache.set(context, participants);
9091
9091
  return participants;
9092
9092
  };
9093
9093
 
9094
- const walker$1 = antlr4.tree.ParseTreeWalker.DEFAULT;
9095
-
9096
- let ChildFragmentDetector = new sequenceParserListener();
9097
-
9098
- let cursor = 0;
9099
- let max = 0;
9100
-
9101
- ChildFragmentDetector.enterTcf = function () {
9102
- cursor++;
9103
- };
9104
- ChildFragmentDetector.enterOpt = function () {
9105
- cursor++;
9106
- };
9107
- ChildFragmentDetector.enterPar = function () {
9108
- cursor++;
9109
- };
9110
- ChildFragmentDetector.enterAlt = function () {
9111
- cursor++;
9112
- };
9113
- ChildFragmentDetector.enterLoop = function () {
9114
- cursor++;
9115
- };
9116
- ChildFragmentDetector.enterSection = function () {
9117
- cursor++;
9118
- };
9119
- ChildFragmentDetector.enterCritical = function () {
9120
- cursor++;
9121
- };
9122
-
9123
- ChildFragmentDetector.exitTcf = function () {
9124
- max = Math.max(max, cursor);
9125
- cursor--;
9126
- };
9127
- ChildFragmentDetector.exitOpt = function () {
9128
- max = Math.max(max, cursor);
9129
- cursor--;
9130
- };
9131
- ChildFragmentDetector.exitPar = function () {
9132
- max = Math.max(max, cursor);
9133
- cursor--;
9134
- };
9135
- ChildFragmentDetector.exitAlt = function () {
9136
- max = Math.max(max, cursor);
9137
- cursor--;
9138
- };
9139
- ChildFragmentDetector.exitLoop = function () {
9140
- max = Math.max(max, cursor);
9141
- cursor--;
9142
- };
9143
- ChildFragmentDetector.exitSection = function () {
9144
- max = Math.max(max, cursor);
9145
- cursor--;
9146
- };
9147
- ChildFragmentDetector.exitCritical = function () {
9148
- max = Math.max(max, cursor);
9149
- cursor--;
9150
- };
9151
-
9152
- ChildFragmentDetector.depth = function (me) {
9153
- return function (context) {
9154
- cursor = 0;
9155
- max = 0;
9156
- context.children.map(function (child) {
9157
- walker$1.walk(me, child);
9158
- });
9159
- return max;
9160
- };
9161
- };
9162
-
9163
9094
  const seqParser$9 = sequenceParser;
9164
9095
  const TitleContext = seqParser$9.TitleContext;
9165
9096
  TitleContext.prototype.content = function () {
@@ -9227,10 +9158,6 @@ CreationContext$6.prototype.Constructor = function () {
9227
9158
  return this.creationBody()?.construct()?.getFormattedText();
9228
9159
  };
9229
9160
 
9230
- CreationContext$6.prototype.To = function () {
9231
- return this.Constructor();
9232
- };
9233
-
9234
9161
  CreationContext$6.prototype.Owner = function () {
9235
9162
  if (!this.Constructor()) {
9236
9163
  return "Missing Constructor";
@@ -9245,10 +9172,6 @@ MessageContext$6.prototype.To = function () {
9245
9172
  return toCtx?.name?.()?.getFormattedText() || toCtx?.getFormattedText();
9246
9173
  };
9247
9174
 
9248
- MessageContext$6.prototype.Owner = function () {
9249
- return this.To() || getOwnerFromAncestor(this.parentCtx);
9250
- };
9251
-
9252
9175
  function getOwnerFromAncestor(ctx) {
9253
9176
  while (ctx) {
9254
9177
  if (ctx instanceof CreationContext$6 || ctx instanceof MessageContext$6) {
@@ -9259,32 +9182,39 @@ function getOwnerFromAncestor(ctx) {
9259
9182
  return undefined;
9260
9183
  }
9261
9184
 
9262
- AsyncMessageContext$2.prototype.To = function () {
9263
- const toCtx = this.to();
9264
- return toCtx?.name?.()?.getFormattedText() || toCtx?.getFormattedText();
9265
- };
9266
-
9267
- AsyncMessageContext$2.prototype.Owner = function () {
9185
+ // MessageContext, AsyncMessageContext, ReturnAsyncMessageContext and
9186
+ // RetContext all resolve Owner the same way: prefer their own To(), else
9187
+ // look up the nearest CreationContext/MessageContext ancestor. Assigning
9188
+ // the same function to each prototype keeps the body single-sourced while
9189
+ // each context class still gets its own Owner property.
9190
+ function ownerFromToOrAncestor() {
9268
9191
  return this.To() || getOwnerFromAncestor(this.parentCtx);
9269
- };
9192
+ }
9193
+
9194
+ [
9195
+ MessageContext$6,
9196
+ AsyncMessageContext$2,
9197
+ ReturnAsyncMessageContext$2,
9198
+ RetMessageContext,
9199
+ ].forEach((ContextClass) => {
9200
+ ContextClass.prototype.Owner = ownerFromToOrAncestor;
9201
+ });
9270
9202
 
9271
- ReturnAsyncMessageContext$2.prototype.To = function () {
9203
+ // AsyncMessageContext and ReturnAsyncMessageContext resolve To() identically
9204
+ // (unlike MessageContext, which reaches its `to` node through
9205
+ // messageBody()?.fromTo(), and RetContext, which delegates to ReturnTo()).
9206
+ function toFromDirectTo() {
9272
9207
  const toCtx = this.to();
9273
9208
  return toCtx?.name?.()?.getFormattedText() || toCtx?.getFormattedText();
9274
- };
9209
+ }
9275
9210
 
9276
- ReturnAsyncMessageContext$2.prototype.Owner = function () {
9277
- return this.To() || getOwnerFromAncestor(this.parentCtx);
9278
- };
9211
+ AsyncMessageContext$2.prototype.To = toFromDirectTo;
9212
+ ReturnAsyncMessageContext$2.prototype.To = toFromDirectTo;
9279
9213
 
9280
9214
  RetMessageContext.prototype.To = function () {
9281
9215
  return this.ReturnTo();
9282
9216
  };
9283
9217
 
9284
- RetMessageContext.prototype.Owner = function () {
9285
- return this.To() || getOwnerFromAncestor(this.parentCtx);
9286
- };
9287
-
9288
9218
  const ProgContext$2 = sequenceParser.ProgContext;
9289
9219
 
9290
9220
  /**
@@ -9397,11 +9327,7 @@ const MessageContext$4 = sequenceParser.MessageContext;
9397
9327
  const CreationContext$4 = sequenceParser.CreationContext;
9398
9328
 
9399
9329
  ParserRuleContext$1.prototype.Origin = function () {
9400
- let ctx = this.parentCtx;
9401
- while (ctx && !(ctx instanceof StatContext$2 || ctx instanceof ProgContext)) {
9402
- ctx = this.parentCtx;
9403
- }
9404
- return ctx.Origin();
9330
+ return this.ClosestAncestorStat()?.Origin();
9405
9331
  };
9406
9332
  // Origin is essentially the 'from' of a message.
9407
9333
  // For example, in `S -> A.m1 {B.m2 {C.m3}}`,
@@ -9482,9 +9408,6 @@ CreationContext$3.prototype.SignatureText = function() {
9482
9408
  const text = this.ParametersText() || "create";
9483
9409
  return `«${text}»`;
9484
9410
  };
9485
- CreationContext$3.prototype.isParamValid = function() {
9486
- return (this.creationBody().parameters()?.parameter()?.length ?? 0) > 0;
9487
- };
9488
9411
  RetContext$1.prototype.SignatureText = function() {
9489
9412
  return this.asyncMessage()?.content()?.getFormattedText() ?? this.returnAsyncMessage()?.content()?.getFormattedText() ?? this.expr()?.getFormattedText() ?? "";
9490
9413
  };
@@ -9575,27 +9498,22 @@ CreationContext.prototype.From = function() {
9575
9498
  }
9576
9499
  return void 0;
9577
9500
  };
9501
+ function fromProvidedOrOrigin() {
9502
+ return this.ProvidedFrom() || this.ClosestAncestorStat()?.Origin();
9503
+ }
9578
9504
  MessageContext.prototype.ProvidedFrom = function() {
9579
9505
  const fromCtx = this.messageBody()?.fromTo?.()?.from?.();
9580
9506
  return fromCtx?.name?.()?.getFormattedText?.() || fromCtx?.getFormattedText?.();
9581
9507
  };
9582
- MessageContext.prototype.From = function() {
9583
- return this.ProvidedFrom() || this.ClosestAncestorStat()?.Origin();
9584
- };
9585
- AsyncMessageContext.prototype.ProvidedFrom = function() {
9586
- const fromCtx = this.from();
9587
- return fromCtx?.name?.()?.getFormattedText?.() || fromCtx?.getFormattedText?.();
9588
- };
9589
- AsyncMessageContext.prototype.From = function() {
9590
- return this.ProvidedFrom() || this.ClosestAncestorStat()?.Origin();
9591
- };
9592
- ReturnAsyncMessageContext.prototype.ProvidedFrom = function() {
9508
+ MessageContext.prototype.From = fromProvidedOrOrigin;
9509
+ function providedFromDirectFrom() {
9593
9510
  const fromCtx = this.from();
9594
9511
  return fromCtx?.name?.()?.getFormattedText?.() || fromCtx?.getFormattedText?.();
9595
- };
9596
- ReturnAsyncMessageContext.prototype.From = function() {
9597
- return this.ProvidedFrom() || this.ClosestAncestorStat()?.Origin();
9598
- };
9512
+ }
9513
+ AsyncMessageContext.prototype.ProvidedFrom = providedFromDirectFrom;
9514
+ AsyncMessageContext.prototype.From = fromProvidedOrOrigin;
9515
+ ReturnAsyncMessageContext.prototype.ProvidedFrom = providedFromDirectFrom;
9516
+ ReturnAsyncMessageContext.prototype.From = fromProvidedOrOrigin;
9599
9517
  RetContext.prototype.From = function() {
9600
9518
  return this.asyncMessage()?.From?.() || this.returnAsyncMessage()?.From?.() || this.ClosestAncestorStat()?.Origin();
9601
9519
  };
@@ -9616,14 +9534,6 @@ ParserRuleContext.prototype.ClosestAncestorStat = function() {
9616
9534
  }
9617
9535
  return void 0;
9618
9536
  };
9619
- ParserRuleContext.prototype.ClosestAncestorBlock = function() {
9620
- const parentCtx = this.ClosestAncestorStat()?.parentCtx;
9621
- if (parentCtx instanceof seqParser.BlockContext) {
9622
- return parentCtx;
9623
- }
9624
- console.warn("Cannot find closest ancestor block for context:", this);
9625
- return void 0;
9626
- };
9627
9537
 
9628
9538
  antlr4.ParserRuleContext.prototype.getAncestors = function(predicate) {
9629
9539
  const ancestors = [];
@@ -9642,11 +9552,9 @@ antlr4.ParserRuleContext.prototype.getAncestors = function(predicate) {
9642
9552
 
9643
9553
  const formatText = (text) => text.replace(/[\n\r]/g, " ").replace(/\s+/g, " ").replace(/\s*([,;.])\s*/g, "$1").replace(/\s*(\()\s*|\s*(\))/g, "$1$2").replace(/\s+$/g, "").replace(/^"(.*)"$/, "$1");
9644
9554
 
9645
- const errors = [];
9646
9555
  const errorDetails = [];
9647
9556
  class SeqErrorListener extends antlr4.error.ErrorListener {
9648
9557
  syntaxError(recognizer, offendingSymbol, line, column, msg) {
9649
- errors.push(`${offendingSymbol} line ${line}, col ${column}: ${msg}`);
9650
9558
  errorDetails.push({
9651
9559
  line,
9652
9560
  column,
@@ -9656,7 +9564,7 @@ class SeqErrorListener extends antlr4.error.ErrorListener {
9656
9564
  }
9657
9565
 
9658
9566
  // Reentrant error listener: collects syntax errors into its OWN instance array
9659
- // instead of the shared module-level `errors`/`errorDetails`. This is what lets
9567
+ // instead of the shared module-level `errorDetails`. This is what lets
9660
9568
  // validate()/parse() below run repeatedly or concurrently (e.g. server-side)
9661
9569
  // without the clear-before/clear-after dance the legacy globals require.
9662
9570
  class CollectingErrorListener extends antlr4.error.ErrorListener {
@@ -9716,7 +9624,7 @@ function rootContext(code) {
9716
9624
 
9717
9625
  const parser = createParser(code);
9718
9626
  parser.addErrorListener(new SeqErrorListener());
9719
- return parser._syntaxErrors ? null : parser.prog();
9627
+ return parser.prog();
9720
9628
  }
9721
9629
 
9722
9630
  antlr4.ParserRuleContext.prototype.getFormattedText = function () {
@@ -9747,7 +9655,7 @@ antlr4.ParserRuleContext.prototype.getComment = function () {
9747
9655
  /**
9748
9656
  * Parse ZenUML DSL with a per-call (reentrant) error listener and return both
9749
9657
  * the error-recovered parse tree and a structured error list. Safe to call
9750
- * repeatedly/concurrently — it does not touch the module-level Errors/ErrorDetails.
9658
+ * repeatedly/concurrently — it does not touch the module-level ErrorDetails.
9751
9659
  *
9752
9660
  * @param {string} code ZenUML DSL source
9753
9661
  * @returns {{ rootContext: object, pass: boolean, errorDetails: { line: number, column: number, msg: string }[] }}
@@ -9801,15 +9709,7 @@ const Parser = {
9801
9709
  },
9802
9710
  parse,
9803
9711
  validate,
9804
- Errors: errors,
9805
9712
  ErrorDetails: errorDetails,
9806
- /**
9807
- * @return {number} how many levels of embedded fragments
9808
- */
9809
- Depth: function (ctx) {
9810
- const childFragmentDetector = ChildFragmentDetector;
9811
- return childFragmentDetector.depth(childFragmentDetector)(ctx);
9812
- },
9813
9713
  };
9814
9714
 
9815
9715
  const FRAGMENT_PADDING_X = 10;
@@ -9956,39 +9856,6 @@ function OrderedParticipants(rootContext) {
9956
9856
  return result;
9957
9857
  }
9958
9858
 
9959
- const LEVELS = ["log", "trace", "debug", "info", "warn", "error"];
9960
- const LEVEL_WEIGHT = {
9961
- trace: 10,
9962
- debug: 20,
9963
- log: 30,
9964
- info: 30,
9965
- warn: 40,
9966
- error: 50
9967
- };
9968
- const THRESHOLD = LEVEL_WEIGHT.warn;
9969
- function consoleFn(level) {
9970
- const fn = console[level];
9971
- return typeof fn === "function" ? fn.bind(console) : console.log.bind(console);
9972
- }
9973
- function createLogger(prefix) {
9974
- const logger = {};
9975
- for (const level of LEVELS) {
9976
- const fn = consoleFn(level);
9977
- logger[level] = (...args) => {
9978
- if (LEVEL_WEIGHT[level] >= THRESHOLD) {
9979
- if (prefix) {
9980
- fn(prefix[0], prefix[1], ...args);
9981
- } else {
9982
- fn(...args);
9983
- }
9984
- }
9985
- };
9986
- }
9987
- logger.child = (opts) => createLogger(["%c" + (opts.name || ""), "color: #00f"]);
9988
- return logger;
9989
- }
9990
- const rootLogger = createLogger();
9991
-
9992
9859
  function Dual(p, v) {
9993
9860
  return { position: p, velocity: v };
9994
9861
  }
@@ -10129,7 +9996,6 @@ class Coordinates {
10129
9996
  }
10130
9997
  const leftGap = this.getParticipantGap(this.participantModels[0]);
10131
9998
  const position = leftGap + this.positionsCache[pIndex];
10132
- rootLogger.debug(`Position of ${participantName} is ${position}`);
10133
9999
  return position;
10134
10000
  }
10135
10001
  walkThrough() {
@@ -10176,17 +10042,11 @@ class Coordinates {
10176
10042
  }
10177
10043
  const leftIndex = Math.min(indexFrom, indexTo);
10178
10044
  const rightIndex = Math.max(indexFrom, indexTo);
10179
- try {
10180
- const messageWidth = this.getMessageWidth(message);
10181
- this.m[leftIndex][rightIndex] = Math.max(
10182
- messageWidth + ARROW_HEAD_WIDTH + OCCURRENCE_WIDTH,
10183
- this.m[leftIndex][rightIndex]
10184
- );
10185
- } catch {
10186
- console.warn(
10187
- `Could not set message gap between ${from} and ${message.to}`
10188
- );
10189
- }
10045
+ const messageWidth = this.getMessageWidth(message);
10046
+ this.m[leftIndex][rightIndex] = Math.max(
10047
+ messageWidth + ARROW_HEAD_WIDTH + OCCURRENCE_WIDTH,
10048
+ this.m[leftIndex][rightIndex]
10049
+ );
10190
10050
  }
10191
10051
  }
10192
10052
  withParticipantGaps(participantModels) {
@@ -10216,9 +10076,6 @@ class Coordinates {
10216
10076
  );
10217
10077
  const participantWidth = Math.max(labelWidth + iconWidth + emojiWidth, MIN_PARTICIPANT_WIDTH) + MARGIN;
10218
10078
  this.participantWidthCache.set(participant.name, participantWidth);
10219
- rootLogger.debug(
10220
- `Width of ${participant.name} is ${participantWidth}; labelWidth: ${labelWidth}`
10221
- );
10222
10079
  return participantWidth;
10223
10080
  }
10224
10081
  }
@@ -10247,6 +10104,23 @@ class NodeVM {
10247
10104
  }
10248
10105
  }
10249
10106
 
10107
+ const SPACING_UNIT = 4;
10108
+ const tw = (value) => value * SPACING_UNIT;
10109
+ const DEFAULT_LAYOUT_METRICS = {
10110
+ statementMarginY: tw(4),
10111
+ // .statement-container .my-4
10112
+ fragmentHeaderHeight: 25,
10113
+ // .fragment .leading-4 => line-height: 1rem
10114
+ fragmentPaddingBottom: 10
10115
+ // .zenuml .fragment =>padding-bottom: 10px
10116
+ };
10117
+ const MESSAGE_HEIGHT = 16;
10118
+ const SELF_MESSAGE_HEIGHT = 30;
10119
+ const ASYNC_SELF_MESSAGE_HEIGHT = 44;
10120
+ const CREATION_MESSAGE_HEIGHT = 40;
10121
+ const COMMENT_LINE_HEIGHT = tw(5);
10122
+ const FRAGMENT_BORDER_WIDTH = 1;
10123
+
10250
10124
  function getLocalParticipantNames(ctx) {
10251
10125
  if (!ctx) return [];
10252
10126
  return [ctx.Origin() || _STARTER_, ...Participants(ctx).Names()];
@@ -10677,7 +10551,7 @@ class MarkdownMeasurer {
10677
10551
  return 0;
10678
10552
  }
10679
10553
  const lines = _text.trim().split("\n");
10680
- return lines.length * 20;
10554
+ return lines.length * COMMENT_LINE_HEIGHT;
10681
10555
  }
10682
10556
  }
10683
10557
 
@@ -10689,57 +10563,12 @@ class StatementVM extends NodeVM {
10689
10563
  if (!context?.getComment || !context.getComment()) return 0;
10690
10564
  return new MarkdownMeasurer().measure(context.getComment());
10691
10565
  }
10692
- resolveFragmentOrigin(fallbackOrigin) {
10693
- try {
10694
- const participants = getLocalParticipantNames(this.context) || [];
10695
- return participants[0] || fallbackOrigin;
10696
- } catch (error) {
10697
- console.warn("Failed to resolve fragment origin", error);
10698
- return fallbackOrigin;
10699
- }
10700
- }
10701
10566
  findLeftParticipant(ctx, fallbackOrigin) {
10702
10567
  if (!ctx) return void 0;
10703
10568
  const local = getLocalParticipantNames(ctx) || [];
10704
10569
  const ordered = this.runtime.participants;
10705
10570
  return ordered.find((name) => local.includes(name)) || local[0] || fallbackOrigin || void 0;
10706
10571
  }
10707
- measureOccurrence(context, top, participant, minHeight = 24, contentInset = 1) {
10708
- const block = context?.braceBlock?.()?.block?.();
10709
- if (!block) {
10710
- return minHeight;
10711
- }
10712
- const offset = this.metrics.statementMarginY - contentInset;
10713
- const blockStart = top - offset;
10714
- const blockEnd = this.layoutBlock(
10715
- block,
10716
- participant || this.runtime.originParticipant,
10717
- blockStart
10718
- );
10719
- const height = blockEnd - blockStart - offset;
10720
- return Math.max(minHeight, height);
10721
- }
10722
- isRootLevelStatement(statCtx) {
10723
- const block = statCtx?.parentCtx;
10724
- return block === this.runtime.rootBlock;
10725
- }
10726
- isFirstStatement(statCtx) {
10727
- const block = statCtx?.parentCtx;
10728
- const statements = block?.stat?.() || [];
10729
- return statements.length > 0 && statements[0] === statCtx;
10730
- }
10731
- altHasMultipleBranches(ctx) {
10732
- if (typeof ctx?.alt !== "function") {
10733
- return false;
10734
- }
10735
- const altContext = ctx.alt();
10736
- if (!altContext) {
10737
- return false;
10738
- }
10739
- const elseIfBlocks = altContext?.elseIfBlock?.() || [];
10740
- const hasElse = Boolean(altContext?.elseBlock?.());
10741
- return elseIfBlocks.length > 0 || hasElse;
10742
- }
10743
10572
  }
10744
10573
 
10745
10574
  class AsyncMessageStatementVM extends StatementVM {
@@ -10754,7 +10583,7 @@ class AsyncMessageStatementVM extends StatementVM {
10754
10583
  const source = asyncContext?.From?.() || asyncContext?.ProvidedFrom?.() || asyncContext?.Origin?.() || origin;
10755
10584
  const target = asyncContext?.Owner?.() || asyncContext?.to?.()?.getFormattedText?.() || source;
10756
10585
  const isSelf = source === target;
10757
- const messageHeight = isSelf ? 44 : 16;
10586
+ const messageHeight = isSelf ? ASYNC_SELF_MESSAGE_HEIGHT : MESSAGE_HEIGHT;
10758
10587
  return {
10759
10588
  top,
10760
10589
  height: commentHeight + messageHeight,
@@ -10763,7 +10592,6 @@ class AsyncMessageStatementVM extends StatementVM {
10763
10592
  }
10764
10593
  }
10765
10594
 
10766
- const CREATION_MESSAGE_HEIGHT = 40;
10767
10595
  class CreationStatementVM extends StatementVM {
10768
10596
  constructor(statement, creation, runtime) {
10769
10597
  super(statement, runtime);
@@ -10787,7 +10615,6 @@ class CreationStatementVM extends StatementVM {
10787
10615
  }
10788
10616
  const assignment = this.creation?.Assignment?.();
10789
10617
  if (assignment) {
10790
- rootLogger.info(`creation::assignment::${participant}`);
10791
10618
  cursor += 12;
10792
10619
  }
10793
10620
  const height = cursor - top;
@@ -10813,26 +10640,7 @@ class EmptyStatementVM extends StatementVM {
10813
10640
  }
10814
10641
  }
10815
10642
 
10816
- class FragmentVM extends StatementVM {
10817
- beginFragment(context, top) {
10818
- const commentHeight = this.measureComment(context);
10819
- const headerHeight = this.metrics.fragmentHeaderHeight;
10820
- return {
10821
- cursor: top + commentHeight + headerHeight,
10822
- commentHeight,
10823
- headerHeight
10824
- };
10825
- }
10826
- finalizeFragment(top, cursor) {
10827
- cursor += this.metrics.fragmentPaddingBottom;
10828
- return {
10829
- top,
10830
- height: cursor - top
10831
- };
10832
- }
10833
- }
10834
-
10835
- class FragmentAltVM extends FragmentVM {
10643
+ class FragmentAltVM extends StatementVM {
10836
10644
  constructor(statement, alt, runtime) {
10837
10645
  super(statement, runtime);
10838
10646
  this.alt = alt;
@@ -10877,14 +10685,26 @@ class FragmentAltVM extends FragmentVM {
10877
10685
  }
10878
10686
  }
10879
10687
 
10880
- class FragmentSingleBlockVM extends FragmentVM {
10881
- constructor(statement, fragment, runtime) {
10688
+ class FragmentRefVM extends StatementVM {
10689
+ kind = "ref";
10690
+ measure(top) {
10691
+ const context = this.context?.ref?.() || this.context;
10692
+ const commentHeight = this.measureComment(context);
10693
+ const headerHeight = this.metrics.fragmentHeaderHeight;
10694
+ const height = commentHeight + headerHeight + this.metrics.fragmentPaddingBottom;
10695
+ return { top, height, kind: this.kind };
10696
+ }
10697
+ }
10698
+
10699
+ class FragmentSingleBlockVM extends StatementVM {
10700
+ constructor(statement, fragment, runtime, kind) {
10882
10701
  super(statement, runtime);
10883
10702
  this.fragment = fragment;
10703
+ this.kind = kind;
10884
10704
  }
10885
10705
  measure(top, origin) {
10886
10706
  const commentHeight = this.measureComment(this.fragment);
10887
- let cursor = top + 1 + this.metrics.fragmentHeaderHeight + commentHeight;
10707
+ let cursor = top + FRAGMENT_BORDER_WIDTH + this.metrics.fragmentHeaderHeight + commentHeight;
10888
10708
  const hasCondition = Boolean(this.fragment?.parExpr?.()?.condition?.());
10889
10709
  if (hasCondition) cursor += 20;
10890
10710
  const block = this.fragment?.braceBlock?.()?.block?.();
@@ -10892,7 +10712,7 @@ class FragmentSingleBlockVM extends FragmentVM {
10892
10712
  const fragmentOrigin = this.findLeftParticipant(this.fragment, origin) || origin;
10893
10713
  cursor = this.layoutBlock(block, fragmentOrigin, cursor, this.kind);
10894
10714
  }
10895
- cursor += this.metrics.fragmentPaddingBottom + 1;
10715
+ cursor += this.metrics.fragmentPaddingBottom + FRAGMENT_BORDER_WIDTH;
10896
10716
  return {
10897
10717
  top,
10898
10718
  height: cursor - top,
@@ -10901,38 +10721,7 @@ class FragmentSingleBlockVM extends FragmentVM {
10901
10721
  }
10902
10722
  }
10903
10723
 
10904
- class FragmentCriticalVM extends FragmentSingleBlockVM {
10905
- kind = "critical";
10906
- }
10907
-
10908
- class FragmentLoopVM extends FragmentSingleBlockVM {
10909
- kind = "loop";
10910
- }
10911
-
10912
- class FragmentOptVM extends FragmentSingleBlockVM {
10913
- kind = "opt";
10914
- }
10915
-
10916
- class FragmentParVM extends FragmentSingleBlockVM {
10917
- kind = "par";
10918
- }
10919
-
10920
- class FragmentRefVM extends FragmentVM {
10921
- kind = "ref";
10922
- measure(top) {
10923
- const context = this.context?.ref?.() || this.context;
10924
- const commentHeight = this.measureComment(context);
10925
- const headerHeight = this.metrics.fragmentHeaderHeight;
10926
- const height = commentHeight + headerHeight + this.metrics.fragmentPaddingBottom;
10927
- return { top, height, kind: this.kind };
10928
- }
10929
- }
10930
-
10931
- class FragmentSectionVM extends FragmentSingleBlockVM {
10932
- kind = "section";
10933
- }
10934
-
10935
- class FragmentTryCatchVM extends FragmentVM {
10724
+ class FragmentTryCatchVM extends StatementVM {
10936
10725
  constructor(statement, tcf, runtime) {
10937
10726
  super(statement, runtime);
10938
10727
  this.tcf = tcf;
@@ -11011,6 +10800,14 @@ class ReturnStatementVM extends StatementVM {
11011
10800
  }
11012
10801
  }
11013
10802
 
10803
+ const SINGLE_BLOCK_FRAGMENT_KINDS = [
10804
+ "loop",
10805
+ "opt",
10806
+ "par",
10807
+ "section",
10808
+ "critical"
10809
+ ];
10810
+
11014
10811
  class SyncMessageStatementVM extends StatementVM {
11015
10812
  constructor(statement, message, runtime) {
11016
10813
  super(statement, runtime);
@@ -11025,7 +10822,7 @@ class SyncMessageStatementVM extends StatementVM {
11025
10822
  const block = this.message.braceBlock?.()?.block?.();
11026
10823
  const assignee = this.message.Assignment?.();
11027
10824
  const isSelf = source === target;
11028
- const messageHeight = isSelf ? 30 : 16;
10825
+ const messageHeight = isSelf ? SELF_MESSAGE_HEIGHT : MESSAGE_HEIGHT;
11029
10826
  cursor += messageHeight;
11030
10827
  if (block) {
11031
10828
  const fragmentOrigin = this.findLeftParticipant(this.message, origin) || origin;
@@ -11061,25 +10858,11 @@ const createStatementVM = (statement, runtime) => {
11061
10858
  if (statement.divider?.()) {
11062
10859
  return new DividerStatementVM(statement, runtime);
11063
10860
  }
11064
- const loop = statement.loop?.();
11065
- if (loop) {
11066
- return new FragmentLoopVM(statement, loop, runtime);
11067
- }
11068
- const opt = statement.opt?.();
11069
- if (opt) {
11070
- return new FragmentOptVM(statement, opt, runtime);
11071
- }
11072
- const par = statement.par?.();
11073
- if (par) {
11074
- return new FragmentParVM(statement, par, runtime);
11075
- }
11076
- const section = statement.section?.();
11077
- if (section) {
11078
- return new FragmentSectionVM(statement, section, runtime);
11079
- }
11080
- const critical = statement.critical?.();
11081
- if (critical) {
11082
- return new FragmentCriticalVM(statement, critical, runtime);
10861
+ for (const kind of SINGLE_BLOCK_FRAGMENT_KINDS) {
10862
+ const fragment = statement[kind]?.();
10863
+ if (fragment) {
10864
+ return new FragmentSingleBlockVM(statement, fragment, runtime, kind);
10865
+ }
11083
10866
  }
11084
10867
  const tcf = statement.tcf?.();
11085
10868
  if (tcf) {
@@ -11116,17 +10899,6 @@ class BlockVM extends NodeVM {
11116
10899
  }
11117
10900
  }
11118
10901
 
11119
- const SPACING_UNIT = 4;
11120
- const tw = (value) => value * SPACING_UNIT;
11121
- const DEFAULT_LAYOUT_METRICS = {
11122
- statementMarginY: tw(4),
11123
- // .statement-container .my-4
11124
- fragmentHeaderHeight: 25,
11125
- // .fragment .leading-4 => line-height: 1rem
11126
- fragmentPaddingBottom: 10
11127
- // .zenuml .fragment =>padding-bottom: 10px
11128
- };
11129
-
11130
10902
  class VerticalCoordinates {
11131
10903
  statementMap = /* @__PURE__ */ new Map();
11132
10904
  creationTops = /* @__PURE__ */ new Map();
@@ -11138,16 +10910,13 @@ class VerticalCoordinates {
11138
10910
  const originParticipant = messages.length === 0 ? _STARTER_ : messages[0].from || _STARTER_;
11139
10911
  const runtime = {
11140
10912
  metrics: DEFAULT_LAYOUT_METRICS,
11141
- rootBlock,
11142
10913
  participants,
11143
- originParticipant,
11144
10914
  recordCoordinate: (statement, coordinate) => {
11145
10915
  const key = createStatementKey(statement);
11146
10916
  this.statementMap.set(key, coordinate);
11147
10917
  },
11148
10918
  updateCreationTop: (participant, top) => {
11149
10919
  const paddingTop = top - 8;
11150
- rootLogger.info(`[VerticalCoordinates] updateCreationTop participant="${participant}" raw=${top} paddingTop=${paddingTop}`);
11151
10920
  this.creationTops.set(participant, paddingTop);
11152
10921
  }
11153
10922
  };
@@ -11160,19 +10929,13 @@ class VerticalCoordinates {
11160
10929
  getStatementCoordinate(key) {
11161
10930
  return this.statementMap.get(key);
11162
10931
  }
11163
- getStatementCoordinateFor(statement) {
11164
- return this.statementMap.get(createStatementKey(statement));
11165
- }
11166
- entries() {
11167
- return Array.from(this.statementMap.entries());
11168
- }
11169
10932
  getTotalHeight() {
11170
10933
  return this.totalHeight;
11171
10934
  }
11172
10935
  }
11173
10936
 
11174
10937
  let dic = {};
11175
- const persistDic = {};
10938
+ let persistDic = {};
11176
10939
  const getCache = (key) => {
11177
10940
  if (key != null) {
11178
10941
  const cacheValue = dic[key] ?? persistDic[key];
@@ -11186,6 +10949,10 @@ const setCache = (key, value, persist = false) => {
11186
10949
  persistDic[key] = value;
11187
10950
  }
11188
10951
  };
10952
+ const clearPersistentCache = () => {
10953
+ persistDic = {};
10954
+ dic = {};
10955
+ };
11189
10956
 
11190
10957
  const FONT_FAMILY = "Helvetica, Verdana, serif";
11191
10958
  const FONT_SIZE_PARTICIPANT = "16px";
@@ -11195,28 +10962,25 @@ function getFontSpec() {
11195
10962
  }
11196
10963
  let canvasCtx = null;
11197
10964
  function setCanvasContext(ctx) {
10965
+ if (ctx === canvasCtx) return;
11198
10966
  canvasCtx = ctx;
10967
+ clearPersistentCache();
11199
10968
  }
11200
10969
  function getCanvasContext() {
11201
10970
  if (canvasCtx) return canvasCtx;
11202
10971
  try {
11203
10972
  if (typeof OffscreenCanvas !== "undefined") {
11204
10973
  canvasCtx = new OffscreenCanvas(1, 1).getContext("2d");
11205
- rootLogger.debug("[ZenUML] WidthProviderOnCanvas: using OffscreenCanvas");
11206
10974
  } else if (typeof document !== "undefined") {
11207
10975
  canvasCtx = document.createElement("canvas").getContext("2d");
11208
- rootLogger.debug("[ZenUML] WidthProviderOnCanvas: using <canvas> element");
11209
- } else {
11210
- rootLogger.debug("[ZenUML] WidthProviderOnCanvas: no canvas available, using character estimate fallback");
11211
10976
  }
11212
10977
  } catch {
11213
- rootLogger.debug("[ZenUML] WidthProviderOnCanvas: canvas creation failed, using character estimate fallback");
11214
10978
  }
11215
10979
  return canvasCtx;
11216
10980
  }
11217
10981
  function WidthProviderOnCanvas(text, type) {
11218
10982
  const measured = text.trim();
11219
- const cacheKey = `WidthProviderOnCanvas_${measured}_${type}`;
10983
+ const cacheKey = `WidthProviderOnCanvas_${getFontSpec()}_${measured}_${type}`;
11220
10984
  const cacheValue = getCache(cacheKey);
11221
10985
  if (cacheValue != null) {
11222
10986
  return cacheValue;
@@ -11224,7 +10988,7 @@ function WidthProviderOnCanvas(text, type) {
11224
10988
  const ctx = getCanvasContext();
11225
10989
  if (!ctx) {
11226
10990
  const width2 = Math.ceil(measured.length * 16 * 0.6);
11227
- setCache(cacheKey, width2, true);
10991
+ setCache(cacheKey, width2);
11228
10992
  return width2;
11229
10993
  }
11230
10994
  ctx.font = getFontSpec();
@@ -11232,7 +10996,10 @@ function WidthProviderOnCanvas(text, type) {
11232
10996
  setCache(cacheKey, width, true);
11233
10997
  return width;
11234
10998
  }
11235
- const EMOJI_PATTERN = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}]/u;
10999
+ const EMOJI_PATTERN = (
11000
+ // eslint-disable-next-line no-misleading-character-class
11001
+ /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}]/u
11002
+ );
11236
11003
  function measureWithSvg(text, fontSize) {
11237
11004
  if (typeof document === "undefined") return null;
11238
11005
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
@@ -11251,7 +11018,8 @@ function measureTextWithFont(text, fontSize) {
11251
11018
  const measured = text.trim();
11252
11019
  if (!measured) return 0;
11253
11020
  const hasEmoji = EMOJI_PATTERN.test(measured);
11254
- const cacheKey = hasEmoji ? `measureTextWithFont_svg_${fontSize}_${measured}` : `measureTextWithFont_${fontSize}_${measured}`;
11021
+ const font = `${fontSize} ${FONT_FAMILY}`;
11022
+ const cacheKey = hasEmoji ? `measureTextWithFont_svg_${font}_${measured}` : `measureTextWithFont_${font}_${measured}`;
11255
11023
  const cacheValue = getCache(cacheKey);
11256
11024
  if (cacheValue != null) {
11257
11025
  return cacheValue;
@@ -11267,10 +11035,10 @@ function measureTextWithFont(text, fontSize) {
11267
11035
  if (!ctx) {
11268
11036
  const px = Number.parseFloat(fontSize) || 14;
11269
11037
  const width2 = Math.ceil(measured.length * px * 0.6);
11270
- setCache(cacheKey, width2, true);
11038
+ setCache(cacheKey, width2);
11271
11039
  return width2;
11272
11040
  }
11273
- ctx.font = `${fontSize} ${FONT_FAMILY}`;
11041
+ ctx.font = font;
11274
11042
  const width = ctx.measureText(measured).width;
11275
11043
  setCache(cacheKey, width, true);
11276
11044
  return width;
@@ -11391,22 +11159,42 @@ function FrameBorder(frame) {
11391
11159
  return { left: 0, right: 0 };
11392
11160
  }
11393
11161
  return {
11394
- type: frame.type,
11395
11162
  left: FRAGMENT_PADDING_X * longestPath(frame, "LEFT" /* LEFT */),
11396
11163
  right: FRAGMENT_PADDING_X * longestPath(frame, "RIGHT" /* RIGHT */)
11397
11164
  };
11398
11165
  }
11399
11166
 
11167
+ function TotalWidth(ctx, coordinates) {
11168
+ const allParticipants = coordinates.orderedParticipantNames();
11169
+ if (!ctx || allParticipants.length === 0) return 0;
11170
+ const localParticipants = getLocalParticipantNames(ctx);
11171
+ const leftParticipant = allParticipants.find((p) => localParticipants.includes(p)) || "";
11172
+ const rightParticipant = allParticipants.slice().reverse().find((p) => localParticipants.includes(p)) || "";
11173
+ const frameBuilder = new FrameBuilder(allParticipants);
11174
+ const frame = frameBuilder.getFrame(ctx);
11175
+ const border = FrameBorder(frame);
11176
+ const extraWidth = extraWidthDueToSelfMessage(
11177
+ ctx,
11178
+ rightParticipant,
11179
+ coordinates
11180
+ );
11181
+ const participantWidth = coordinates.distance(leftParticipant, rightParticipant) + coordinates.half(leftParticipant) + coordinates.half(rightParticipant);
11182
+ return Math.max(participantWidth, FRAGMENT_MIN_WIDTH) + border.left + border.right + extraWidth;
11183
+ }
11184
+ function extraWidthDueToSelfMessage(ctx, rightParticipant, coordinates) {
11185
+ const allMessages = AllMessages(ctx);
11186
+ const widths = allMessages.filter((m) => m.from === m.to).map(
11187
+ (m) => coordinates.getMessageWidth(m) - coordinates.distance(m.from || _STARTER_, rightParticipant) - coordinates.half(rightParticipant)
11188
+ );
11189
+ return Math.max.apply(null, [0, ...widths]);
11190
+ }
11191
+
11400
11192
  const PARTICIPANT_TOP_SPACE = PARTICIPANT_TOP_SPACE_FOR_GROUP + 8;
11401
11193
  const PARTICIPANT_BOX_PADDING = 16;
11402
- const PARTICIPANT_BOX_PADDING_ASSIGNEE = 16;
11403
11194
  const PARTICIPANT_ICON_ROW_WIDTH = 28;
11404
11195
  const PARTICIPANT_EMOJI_WIDTH = 20;
11405
11196
  const PARTICIPANT_VISUAL_HEIGHT = 40;
11406
11197
  const PARTICIPANT_MAX_WIDTH = 250;
11407
- function snapX$1(x) {
11408
- return x;
11409
- }
11410
11198
  function cssToSvgStyle(css) {
11411
11199
  const result = {};
11412
11200
  let hasKeys = false;
@@ -11425,24 +11213,32 @@ function cssToSvgStyle(css) {
11425
11213
 
11426
11214
  function buildParticipants(models, coordinates, verticalCoordinates, measureText) {
11427
11215
  return models.map((m) => {
11428
- const centerX = snapX$1(coordinates.getPosition(m.name));
11216
+ const centerX = coordinates.getPosition(m.name);
11429
11217
  const halfWidth = coordinates.half(m.name);
11430
- const isAssignee = m.name.includes(":") && m.getDisplayName() === m.name;
11431
11218
  let width;
11432
11219
  let measuredTextWidth;
11433
11220
  let measuredStereotypeWidth;
11434
11221
  if (measureText && m.name !== _STARTER_) {
11435
11222
  const textWidth = measureSvgParticipantLabelWidth(m.getDisplayName());
11436
11223
  measuredTextWidth = textWidth;
11437
- const padding = isAssignee ? PARTICIPANT_BOX_PADDING_ASSIGNEE : PARTICIPANT_BOX_PADDING;
11224
+ const padding = PARTICIPANT_BOX_PADDING;
11438
11225
  const iconWidth = m.hasIcon() ? PARTICIPANT_ICON_ROW_WIDTH : 0;
11439
11226
  const emojiWidth = m.emoji ? PARTICIPANT_EMOJI_WIDTH : 0;
11440
11227
  const STEREOTYPE_BOX_OVERHEAD = 8;
11441
11228
  if (m.stereotype) {
11442
- measuredStereotypeWidth = measureSvgParticipantLabelWidth(`«${m.stereotype}»`);
11229
+ measuredStereotypeWidth = measureSvgParticipantLabelWidth(
11230
+ `«${m.stereotype}»`
11231
+ );
11443
11232
  }
11444
11233
  const stereotypeBoxWidth = measuredStereotypeWidth != null ? measuredStereotypeWidth + STEREOTYPE_BOX_OVERHEAD : 0;
11445
- width = Math.min(Math.max(textWidth + padding + iconWidth + emojiWidth, stereotypeBoxWidth, MIN_PARTICIPANT_WIDTH), PARTICIPANT_MAX_WIDTH);
11234
+ width = Math.min(
11235
+ Math.max(
11236
+ textWidth + padding + iconWidth + emojiWidth,
11237
+ stereotypeBoxWidth,
11238
+ MIN_PARTICIPANT_WIDTH
11239
+ ),
11240
+ PARTICIPANT_MAX_WIDTH
11241
+ );
11446
11242
  } else {
11447
11243
  width = Math.min(halfWidth * 2 - MARGIN, PARTICIPANT_MAX_WIDTH);
11448
11244
  }
@@ -11521,39 +11317,49 @@ function buildGroups(participants, diagramHeight) {
11521
11317
  return groups;
11522
11318
  }
11523
11319
 
11524
- function snapX(x) {
11525
- return x;
11320
+ class Anchor2 {
11321
+ constructor(position, layers) {
11322
+ this.position = position;
11323
+ this.layers = layers;
11324
+ }
11325
+ /**
11326
+ * centerToEdge is used for translateX calculations.
11327
+ */
11328
+ centerToEdge(other) {
11329
+ return other.rightEdgeOfRightWall() - this.centerOfRightWall();
11330
+ }
11331
+ centerToCenter(other) {
11332
+ return other.centerOfRightWall() - this.centerOfRightWall();
11333
+ }
11334
+ /**
11335
+ * edgeOffset is used for interactionWidth calculations.
11336
+ */
11337
+ edgeOffset(other) {
11338
+ const isRightToLeft = other.position < this.position;
11339
+ const rightEdgeOfLeftAnchor = isRightToLeft ? other.rightEdgeOfRightWall() : this.rightEdgeOfRightWall();
11340
+ const leftEdgeOfRightAnchor = isRightToLeft ? this.leftEdgeOfRightWall() : other.leftEdgeOfRightWall();
11341
+ const distance = leftEdgeOfRightAnchor - rightEdgeOfLeftAnchor - LIFELINE_WIDTH;
11342
+ return isRightToLeft ? distance * -1 : distance;
11343
+ }
11344
+ centerOfRightWall() {
11345
+ return this.layers <= 1 ? this.position : this.position + OCCURRENCE_BAR_SIDE_WIDTH * (this.layers - 1);
11346
+ }
11347
+ rightEdgeOfRightWall() {
11348
+ return this.position + OCCURRENCE_BAR_SIDE_WIDTH * this.layers;
11349
+ }
11350
+ leftEdgeOfRightWall() {
11351
+ return this.layers === 0 ? this.position : this.centerOfRightWall() - OCCURRENCE_BAR_SIDE_WIDTH;
11352
+ }
11526
11353
  }
11354
+
11527
11355
  const COMMENT_FONT_ASCENT = 15;
11528
11356
  function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, allParticipants, _measureText, commentHeight = 0, commentText, commentStyle, commentYBase) {
11529
11357
  const statNode = info.statNode;
11530
- if (!statNode) return null;
11531
11358
  const fragmentCtx = findFragmentContext(statNode);
11532
- if (!fragmentCtx) {
11533
- return {
11534
- fragment: {
11535
- kind: info.fragmentKind,
11536
- label: info.fragmentLabel || "",
11537
- labelWidth: info.fragmentLabel ? measureSvgFragmentLabelWidth(resolveEmojiInText(info.fragmentLabel)) : void 0,
11538
- x: 0,
11539
- y: coord.top,
11540
- width: coordinates.getWidth(),
11541
- height: coord.height,
11542
- headerY: coord.top + 1 + commentHeight,
11543
- sections: [],
11544
- number: info.number,
11545
- depth: info.depth
11546
- },
11547
- comment: commentText && commentYBase != null ? {
11548
- x: 1,
11549
- y: commentYBase + 1 + COMMENT_FONT_ASCENT,
11550
- text: commentText,
11551
- style: commentStyle,
11552
- fragmentComment: true
11553
- } : void 0
11554
- };
11555
- }
11556
- const localNames = getDeepParticipantNames(fragmentCtx, info.fragmentSections);
11359
+ const localNames = getDeepParticipantNames(
11360
+ fragmentCtx,
11361
+ info.fragmentSections
11362
+ );
11557
11363
  const leftParticipant = allParticipants.find((p) => localNames.includes(p)) || "";
11558
11364
  const rightParticipant = allParticipants.slice().reverse().find((p) => localNames.includes(p)) || "";
11559
11365
  const frameBuilder = new FrameBuilder(allParticipants);
@@ -11562,14 +11368,8 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11562
11368
  let fragWidth;
11563
11369
  let fragX;
11564
11370
  if (leftParticipant && rightParticipant) {
11565
- const participantWidth = coordinates.distance(leftParticipant, rightParticipant) + coordinates.half(leftParticipant) + coordinates.half(rightParticipant);
11566
- const selfMessages = AllMessages(statNode).filter((m) => m.from === m.to);
11567
- const extraWidths = selfMessages.map(
11568
- (m) => coordinates.getMessageWidth(m) - coordinates.distance(m.from || _STARTER_, rightParticipant) - coordinates.half(rightParticipant)
11569
- );
11570
- const extraWidth = Math.max(0, ...extraWidths);
11571
- fragWidth = Math.max(participantWidth, FRAGMENT_MIN_WIDTH) + fragBorder.left + fragBorder.right + extraWidth;
11572
- fragX = snapX(coordinates.getPosition(leftParticipant)) - coordinates.half(leftParticipant);
11371
+ fragWidth = TotalWidth(statNode, coordinates);
11372
+ fragX = coordinates.getPosition(leftParticipant) - coordinates.half(leftParticipant);
11573
11373
  } else {
11574
11374
  fragWidth = Math.max(FRAGMENT_MIN_WIDTH, coordinates.getWidth());
11575
11375
  fragX = 0;
@@ -11584,7 +11384,7 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11584
11384
  if (sectionBlock) {
11585
11385
  const innerStats = sectionBlock.stat?.() || [];
11586
11386
  if (innerStats.length > 0) {
11587
- const firstStatKey = createStatementKeyFromStat(innerStats[0]);
11387
+ const firstStatKey = createStatementKey(innerStats[0]);
11588
11388
  if (firstStatKey) {
11589
11389
  const innerCoord = verticalCoordinates.getStatementCoordinate(firstStatKey);
11590
11390
  if (innerCoord) {
@@ -11601,7 +11401,9 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11601
11401
  height: sectionHeight,
11602
11402
  labelWidth: section.label ? measureSvgFragmentLabelWidth(resolveEmojiInText(section.label)) : void 0,
11603
11403
  innerLabel: /^\[\s*.*\s*\]$/.test(section.label) ? section.label.slice(1, -1).trim() : void 0,
11604
- innerLabelWidth: /^\[\s*.*\s*\]$/.test(section.label) ? measureSvgFragmentLabelWidth(resolveEmojiInText(section.label.slice(1, -1).trim())) : void 0,
11404
+ innerLabelWidth: /^\[\s*.*\s*\]$/.test(section.label) ? measureSvgFragmentLabelWidth(
11405
+ resolveEmojiInText(section.label.slice(1, -1).trim())
11406
+ ) : void 0,
11605
11407
  keyword: (() => {
11606
11408
  const spaceIdx = section.label.indexOf(" ");
11607
11409
  if (spaceIdx > 0 && !section.label.startsWith("finally") && !section.label.startsWith("[")) {
@@ -11612,7 +11414,9 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11612
11414
  keywordWidth: (() => {
11613
11415
  const spaceIdx = section.label.indexOf(" ");
11614
11416
  if (spaceIdx > 0 && !section.label.startsWith("finally") && !section.label.startsWith("[")) {
11615
- return measureSvgFragmentLabelWidth(section.label.substring(0, spaceIdx));
11417
+ return measureSvgFragmentLabelWidth(
11418
+ section.label.substring(0, spaceIdx)
11419
+ );
11616
11420
  }
11617
11421
  return void 0;
11618
11422
  })(),
@@ -11626,7 +11430,9 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11626
11430
  detailWidth: (() => {
11627
11431
  const spaceIdx = section.label.indexOf(" ");
11628
11432
  if (spaceIdx > 0 && !section.label.startsWith("finally") && !section.label.startsWith("[")) {
11629
- return measureSvgFragmentLabelWidth(section.label.substring(spaceIdx + 1));
11433
+ return measureSvgFragmentLabelWidth(
11434
+ section.label.substring(spaceIdx + 1)
11435
+ );
11630
11436
  }
11631
11437
  return void 0;
11632
11438
  })()
@@ -11639,7 +11445,7 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11639
11445
  if (innerStats.length > 1) {
11640
11446
  sections.push({ label: "", y: coord.top, height: 0 });
11641
11447
  for (let i = 1; i < innerStats.length; i++) {
11642
- const statKey = createStatementKeyFromStat(innerStats[i]);
11448
+ const statKey = createStatementKey(innerStats[i]);
11643
11449
  if (!statKey) continue;
11644
11450
  const innerCoord = verticalCoordinates.getStatementCoordinate(statKey);
11645
11451
  if (!innerCoord) continue;
@@ -11672,7 +11478,9 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11672
11478
  y: coord.top,
11673
11479
  width: fragWidth,
11674
11480
  height: coord.height,
11675
- headerY: coord.top + 1 + commentHeight,
11481
+ // The header sits just inside the fragment's 1px top border, below any
11482
+ // inline comment — the same offset FragmentSingleBlockVM.measure() applies.
11483
+ headerY: coord.top + FRAGMENT_BORDER_WIDTH + commentHeight,
11676
11484
  sections,
11677
11485
  number: info.number,
11678
11486
  depth: info.depth
@@ -11681,7 +11489,7 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11681
11489
  };
11682
11490
  }
11683
11491
  function findFragmentContext(stat) {
11684
- for (const kind of ["loop", "opt", "par", "critical", "section"]) {
11492
+ for (const kind of SINGLE_BLOCK_FRAGMENT_KINDS) {
11685
11493
  const frag = stat[kind]?.();
11686
11494
  if (frag) return frag;
11687
11495
  }
@@ -11693,10 +11501,6 @@ function findFragmentContext(stat) {
11693
11501
  if (ref) return ref;
11694
11502
  return null;
11695
11503
  }
11696
- function createStatementKeyFromStat(statement) {
11697
- if (!statement?.start || !statement?.stop) return "";
11698
- return `${statement.start.start}-${statement.stop.stop}`;
11699
- }
11700
11504
  function getDeepParticipantNames(fragmentCtx, sections) {
11701
11505
  const names = new Set(getLocalParticipantNames(fragmentCtx));
11702
11506
  if (sections) {
@@ -11734,11 +11538,37 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11734
11538
  const to = message.Owner?.() || _STARTER_;
11735
11539
  const label = normalizeLabel(message.SignatureText?.() || "");
11736
11540
  const nestedBlock = message.braceBlock?.()?.block?.();
11737
- results.push({ key, kind: "sync", from, to, label, isSelf: from === to, hasBlock: !!nestedBlock, comment, statNode: stat, senderOccurrenceDepth: activeOccurrences.get(from) || 0, targetHasOccurrence: activeOccurrences.has(to), targetOccurrenceDepth: activeOccurrences.get(to) || 0, number, depth, parentBlockKind });
11541
+ results.push({
11542
+ key,
11543
+ kind: "sync",
11544
+ from,
11545
+ to,
11546
+ label,
11547
+ isSelf: from === to,
11548
+ hasBlock: !!nestedBlock,
11549
+ comment,
11550
+ statNode: stat,
11551
+ senderOccurrenceDepth: activeOccurrences.get(from) || 0,
11552
+ targetHasOccurrence: activeOccurrences.has(to),
11553
+ targetOccurrenceDepth: activeOccurrences.get(to) || 0,
11554
+ number,
11555
+ depth,
11556
+ parentBlockKind
11557
+ });
11738
11558
  if (nestedBlock) {
11739
11559
  const innerOccs = new Map(activeOccurrences);
11740
11560
  innerOccs.set(to, (innerOccs.get(to) || 0) + 1);
11741
- results.push(...walkBlock(nestedBlock, to, innerOccs, number, depth + 1, "sync", 0));
11561
+ results.push(
11562
+ ...walkBlock(
11563
+ nestedBlock,
11564
+ to,
11565
+ innerOccs,
11566
+ number,
11567
+ depth + 1,
11568
+ "sync",
11569
+ 0
11570
+ )
11571
+ );
11742
11572
  }
11743
11573
  continue;
11744
11574
  }
@@ -11746,8 +11576,23 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11746
11576
  if (asyncMsg) {
11747
11577
  const from = asyncMsg.From?.() || asyncMsg.ProvidedFrom?.() || asyncMsg.Origin?.() || currentOrigin;
11748
11578
  const to = asyncMsg.Owner?.() || asyncMsg.to?.()?.getFormattedText?.() || from;
11749
- const label = normalizeLabel(asyncMsg.content?.()?.getText?.() || asyncMsg.SignatureText?.() || "");
11750
- results.push({ key, kind: "async", from, to, label, isSelf: from === to, hasBlock: false, comment, senderOccurrenceDepth: activeOccurrences.get(from) || 0, targetHasOccurrence: activeOccurrences.has(to), number, depth });
11579
+ const label = normalizeLabel(
11580
+ asyncMsg.content?.()?.getText?.() || asyncMsg.SignatureText?.() || ""
11581
+ );
11582
+ results.push({
11583
+ key,
11584
+ kind: "async",
11585
+ from,
11586
+ to,
11587
+ label,
11588
+ isSelf: from === to,
11589
+ hasBlock: false,
11590
+ comment,
11591
+ senderOccurrenceDepth: activeOccurrences.get(from) || 0,
11592
+ targetHasOccurrence: activeOccurrences.has(to),
11593
+ number,
11594
+ depth
11595
+ });
11751
11596
  continue;
11752
11597
  }
11753
11598
  const creation = stat.creation?.();
@@ -11756,11 +11601,36 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11756
11601
  const to = creation.Owner?.() || "";
11757
11602
  const label = normalizeLabel(creation.SignatureText?.() || "«create»");
11758
11603
  const creationBlock = creation.braceBlock?.()?.block?.();
11759
- results.push({ key, kind: "creation", from, to, label, isSelf: false, hasBlock: !!creationBlock, comment, statNode: stat, senderOccurrenceDepth: activeOccurrences.get(from) || 0, targetHasOccurrence: activeOccurrences.has(to), number, depth, parentBlockKind });
11604
+ results.push({
11605
+ key,
11606
+ kind: "creation",
11607
+ from,
11608
+ to,
11609
+ label,
11610
+ isSelf: false,
11611
+ hasBlock: !!creationBlock,
11612
+ comment,
11613
+ statNode: stat,
11614
+ senderOccurrenceDepth: activeOccurrences.get(from) || 0,
11615
+ targetHasOccurrence: activeOccurrences.has(to),
11616
+ number,
11617
+ depth,
11618
+ parentBlockKind
11619
+ });
11760
11620
  if (creationBlock) {
11761
11621
  const innerOccs = new Map(activeOccurrences);
11762
11622
  innerOccs.set(to, (innerOccs.get(to) || 0) + 1);
11763
- results.push(...walkBlock(creationBlock, to || currentOrigin, innerOccs, number, depth + 1, "creation", 0));
11623
+ results.push(
11624
+ ...walkBlock(
11625
+ creationBlock,
11626
+ to || currentOrigin,
11627
+ innerOccs,
11628
+ number,
11629
+ depth + 1,
11630
+ "creation",
11631
+ 0
11632
+ )
11633
+ );
11764
11634
  }
11765
11635
  continue;
11766
11636
  }
@@ -11770,13 +11640,40 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11770
11640
  const asyncMessage = ret?.asyncMessage?.();
11771
11641
  const from = asyncMessage?.From?.() || ret?.From?.() || currentOrigin;
11772
11642
  const to = asyncMessage?.to?.()?.getFormattedText?.() || ret?.ReturnTo?.() || _STARTER_;
11773
- results.push({ key, kind: "return", from, to, label, isSelf: from === to, hasBlock: false, comment, senderOccurrenceDepth: activeOccurrences.get(from) || 0, targetOccurrenceDepth: activeOccurrences.get(to) || 0, number, depth, parentBlockKind });
11643
+ results.push({
11644
+ key,
11645
+ kind: "return",
11646
+ from,
11647
+ to,
11648
+ label,
11649
+ isSelf: from === to,
11650
+ hasBlock: false,
11651
+ comment,
11652
+ senderOccurrenceDepth: activeOccurrences.get(from) || 0,
11653
+ targetOccurrenceDepth: activeOccurrences.get(to) || 0,
11654
+ number,
11655
+ depth,
11656
+ parentBlockKind
11657
+ });
11774
11658
  continue;
11775
11659
  }
11776
11660
  const divider = stat.divider?.();
11777
11661
  if (divider) {
11778
- const label = normalizeLabel(divider.getFormattedText?.() || divider.getText?.() || "");
11779
- results.push({ key, kind: "divider", from: "", to: "", label, isSelf: false, hasBlock: false, senderOccurrenceDepth: 0, number, depth });
11662
+ const label = normalizeLabel(
11663
+ divider.getFormattedText?.() || divider.getText?.() || ""
11664
+ );
11665
+ results.push({
11666
+ key,
11667
+ kind: "divider",
11668
+ from: "",
11669
+ to: "",
11670
+ label,
11671
+ isSelf: false,
11672
+ hasBlock: false,
11673
+ senderOccurrenceDepth: 0,
11674
+ number,
11675
+ depth
11676
+ });
11780
11677
  continue;
11781
11678
  }
11782
11679
  const fragmentInfo = extractFragmentInfo(stat);
@@ -11798,12 +11695,19 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11798
11695
  number,
11799
11696
  depth
11800
11697
  });
11801
- walkFragmentBlocks(stat, currentOrigin, results, activeOccurrences, number, depth);
11698
+ walkFragmentBlocks(
11699
+ stat,
11700
+ currentOrigin,
11701
+ results,
11702
+ activeOccurrences,
11703
+ number,
11704
+ depth
11705
+ );
11802
11706
  }
11803
11707
  return results;
11804
11708
  }
11805
11709
  function extractFragmentInfo(stat) {
11806
- for (const kind of ["loop", "opt", "par", "critical", "section"]) {
11710
+ for (const kind of SINGLE_BLOCK_FRAGMENT_KINDS) {
11807
11711
  const frag = stat[kind]?.();
11808
11712
  if (frag) {
11809
11713
  const condition = frag.parExpr?.()?.condition?.();
@@ -11820,16 +11724,25 @@ function extractFragmentInfo(stat) {
11820
11724
  const sections = [];
11821
11725
  const ifBlock = alt.ifBlock?.();
11822
11726
  if (ifBlock) {
11823
- sections.push({ label: "Alt", blockNode: ifBlock.braceBlock?.()?.block?.() });
11727
+ sections.push({
11728
+ label: "Alt",
11729
+ blockNode: ifBlock.braceBlock?.()?.block?.()
11730
+ });
11824
11731
  }
11825
11732
  for (const elseIf of alt.elseIfBlock?.() || []) {
11826
11733
  const condition = elseIf.parExpr?.()?.condition?.();
11827
11734
  const label = condition?.getFormattedText?.() || "";
11828
- sections.push({ label: `[ ${label} ]`, blockNode: elseIf.braceBlock?.()?.block?.() });
11735
+ sections.push({
11736
+ label: `[ ${label} ]`,
11737
+ blockNode: elseIf.braceBlock?.()?.block?.()
11738
+ });
11829
11739
  }
11830
11740
  const elseBlock = alt.elseBlock?.();
11831
11741
  if (elseBlock) {
11832
- sections.push({ label: "[else]", blockNode: elseBlock.braceBlock?.()?.block?.() });
11742
+ sections.push({
11743
+ label: "[else]",
11744
+ blockNode: elseBlock.braceBlock?.()?.block?.()
11745
+ });
11833
11746
  }
11834
11747
  const ifCondition = alt.ifBlock?.()?.parExpr?.()?.condition?.();
11835
11748
  const firstLabel = ifCondition?.getFormattedText?.() || "";
@@ -11840,15 +11753,24 @@ function extractFragmentInfo(stat) {
11840
11753
  const sections = [];
11841
11754
  const tryBlock = tcf.tryBlock?.();
11842
11755
  if (tryBlock) {
11843
- sections.push({ label: "Try", blockNode: tryBlock.braceBlock?.()?.block?.() });
11756
+ sections.push({
11757
+ label: "Try",
11758
+ blockNode: tryBlock.braceBlock?.()?.block?.()
11759
+ });
11844
11760
  }
11845
11761
  for (const catchBlock of tcf.catchBlock?.() || []) {
11846
11762
  const exception = catchBlock.invocation?.()?.parameters?.()?.getFormattedText?.() || "";
11847
- sections.push({ label: `catch ${exception}`, blockNode: catchBlock.braceBlock?.()?.block?.() });
11763
+ sections.push({
11764
+ label: `catch ${exception}`,
11765
+ blockNode: catchBlock.braceBlock?.()?.block?.()
11766
+ });
11848
11767
  }
11849
11768
  const finallyBlock = tcf.finallyBlock?.();
11850
11769
  if (finallyBlock) {
11851
- sections.push({ label: "finally", blockNode: finallyBlock.braceBlock?.()?.block?.() });
11770
+ sections.push({
11771
+ label: "finally",
11772
+ blockNode: finallyBlock.braceBlock?.()?.block?.()
11773
+ });
11852
11774
  }
11853
11775
  return { fragmentKind: "tcf", label: "", sections };
11854
11776
  }
@@ -11860,14 +11782,27 @@ function extractFragmentInfo(stat) {
11860
11782
  return null;
11861
11783
  }
11862
11784
  function blockLength(block) {
11863
- return (block?.stat?.() || []).filter((stat) => !!createStatementKey(stat)).length;
11785
+ return (block?.stat?.() || []).filter(
11786
+ (stat) => !!createStatementKey(stat)
11787
+ ).length;
11864
11788
  }
11865
11789
  function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumber, depth) {
11866
- for (const kind of ["loop", "opt", "par", "critical", "section"]) {
11790
+ for (const kind of SINGLE_BLOCK_FRAGMENT_KINDS) {
11867
11791
  const frag = stat[kind]?.();
11868
11792
  if (frag) {
11869
11793
  const block = frag.braceBlock?.()?.block?.();
11870
- if (block) results.push(...walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, 0));
11794
+ if (block)
11795
+ results.push(
11796
+ ...walkBlock(
11797
+ block,
11798
+ origin,
11799
+ activeOccurrences,
11800
+ parentNumber,
11801
+ depth + 1,
11802
+ null,
11803
+ 0
11804
+ )
11805
+ );
11871
11806
  return;
11872
11807
  }
11873
11808
  }
@@ -11878,14 +11813,32 @@ function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumb
11878
11813
  if (ifBlock) {
11879
11814
  const block = ifBlock.braceBlock?.()?.block?.();
11880
11815
  if (block) {
11881
- results.push(...walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset));
11816
+ results.push(
11817
+ ...walkBlock(
11818
+ block,
11819
+ origin,
11820
+ activeOccurrences,
11821
+ parentNumber,
11822
+ depth + 1,
11823
+ null,
11824
+ sectionOffset
11825
+ )
11826
+ );
11882
11827
  sectionOffset += blockLength(block);
11883
11828
  }
11884
11829
  }
11885
11830
  for (const elseIf of alt.elseIfBlock?.() || []) {
11886
11831
  const block = elseIf.braceBlock?.()?.block?.();
11887
11832
  if (block) {
11888
- const sectionStmts = walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset);
11833
+ const sectionStmts = walkBlock(
11834
+ block,
11835
+ origin,
11836
+ activeOccurrences,
11837
+ parentNumber,
11838
+ depth + 1,
11839
+ null,
11840
+ sectionOffset
11841
+ );
11889
11842
  if (sectionStmts.length > 0) sectionStmts[0].sectionReset = true;
11890
11843
  results.push(...sectionStmts);
11891
11844
  sectionOffset += blockLength(block);
@@ -11895,7 +11848,15 @@ function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumb
11895
11848
  if (elseBlock) {
11896
11849
  const block = elseBlock.braceBlock?.()?.block?.();
11897
11850
  if (block) {
11898
- const sectionStmts = walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset);
11851
+ const sectionStmts = walkBlock(
11852
+ block,
11853
+ origin,
11854
+ activeOccurrences,
11855
+ parentNumber,
11856
+ depth + 1,
11857
+ null,
11858
+ sectionOffset
11859
+ );
11899
11860
  if (sectionStmts.length > 0) sectionStmts[0].sectionReset = true;
11900
11861
  results.push(...sectionStmts);
11901
11862
  }
@@ -11909,14 +11870,32 @@ function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumb
11909
11870
  if (tryBlock) {
11910
11871
  const block = tryBlock.braceBlock?.()?.block?.();
11911
11872
  if (block) {
11912
- results.push(...walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset));
11873
+ results.push(
11874
+ ...walkBlock(
11875
+ block,
11876
+ origin,
11877
+ activeOccurrences,
11878
+ parentNumber,
11879
+ depth + 1,
11880
+ null,
11881
+ sectionOffset
11882
+ )
11883
+ );
11913
11884
  sectionOffset += blockLength(block);
11914
11885
  }
11915
11886
  }
11916
11887
  for (const catchBlock of tcf.catchBlock?.() || []) {
11917
11888
  const block = catchBlock.braceBlock?.()?.block?.();
11918
11889
  if (block) {
11919
- const sectionStmts = walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset);
11890
+ const sectionStmts = walkBlock(
11891
+ block,
11892
+ origin,
11893
+ activeOccurrences,
11894
+ parentNumber,
11895
+ depth + 1,
11896
+ null,
11897
+ sectionOffset
11898
+ );
11920
11899
  if (sectionStmts.length > 0) sectionStmts[0].sectionReset = true;
11921
11900
  results.push(...sectionStmts);
11922
11901
  sectionOffset += blockLength(block);
@@ -11926,7 +11905,15 @@ function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumb
11926
11905
  if (finallyBlock) {
11927
11906
  const block = finallyBlock.braceBlock?.()?.block?.();
11928
11907
  if (block) {
11929
- const sectionStmts = walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset);
11908
+ const sectionStmts = walkBlock(
11909
+ block,
11910
+ origin,
11911
+ activeOccurrences,
11912
+ parentNumber,
11913
+ depth + 1,
11914
+ null,
11915
+ sectionOffset
11916
+ );
11930
11917
  if (sectionStmts.length > 0) sectionStmts[0].sectionReset = true;
11931
11918
  results.push(...sectionStmts);
11932
11919
  }
@@ -11958,10 +11945,6 @@ function computeReturnDebt(statements, verticalCoordinates, returnHeight) {
11958
11945
  if (ownerKey && occInnerDebt > 0) {
11959
11946
  result.set(`inner:${ownerKey}`, occInnerDebt);
11960
11947
  }
11961
- const hasMixed = hasNonReturnChild[maxDepth] || false;
11962
- if (ownerKey && hasMixed) {
11963
- result.set(`mixed:${ownerKey}`, 1);
11964
- }
11965
11948
  const hasAssign = blockHasAssignment[maxDepth] || false;
11966
11949
  debtByDepth.pop();
11967
11950
  directDebtByDepth.pop();
@@ -12045,10 +12028,6 @@ function computeReturnDebt(statements, verticalCoordinates, returnHeight) {
12045
12028
  if (ownerKey && occInnerDebtEnd > 0) {
12046
12029
  result.set(`inner:${ownerKey}`, occInnerDebtEnd);
12047
12030
  }
12048
- const hasMixedEnd = hasNonReturnChild[maxDepth] || false;
12049
- if (ownerKey && hasMixedEnd) {
12050
- result.set(`mixed:${ownerKey}`, 1);
12051
- }
12052
12031
  const hasAssignEnd = blockHasAssignment[maxDepth] || false;
12053
12032
  debtByDepth.pop();
12054
12033
  directDebtByDepth.pop();
@@ -12064,7 +12043,6 @@ function computeReturnDebt(statements, verticalCoordinates, returnHeight) {
12064
12043
  }
12065
12044
  }
12066
12045
  }
12067
- result.set("__totalDebt__", debtByDepth[0] || 0);
12068
12046
  return result;
12069
12047
  }
12070
12048
 
@@ -12092,7 +12070,11 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12092
12070
  }
12093
12071
  }
12094
12072
  const RETURN_VISUAL_HEIGHT = 16;
12095
- const adjustMap = computeReturnDebt(statements, verticalCoordinates, RETURN_VISUAL_HEIGHT);
12073
+ const adjustMap = computeReturnDebt(
12074
+ statements,
12075
+ verticalCoordinates,
12076
+ RETURN_VISUAL_HEIGHT
12077
+ );
12096
12078
  for (const info of statements) {
12097
12079
  const coord = verticalCoordinates.getStatementCoordinate(info.key);
12098
12080
  if (!coord) continue;
@@ -12108,7 +12090,7 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12108
12090
  const tX = coordinates.getPosition(info.to);
12109
12091
  if (fX > tX) commentParticipant = info.to;
12110
12092
  }
12111
- const commentX = commentParticipant ? snapX$1(coordinates.getPosition(commentParticipant)) : 10;
12093
+ const commentX = commentParticipant ? coordinates.getPosition(commentParticipant) : 10;
12112
12094
  const COMMENT_FONT_ASCENT = 15;
12113
12095
  const commentXOffset = info.senderOccurrenceDepth >= 1 ? OCCURRENCE_BAR_SIDE_WIDTH + 1 : 1;
12114
12096
  const codeSpanPadding = commentObj.text.trimStart().startsWith("`") ? 2 : 0;
@@ -12121,30 +12103,31 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12121
12103
  }
12122
12104
  const messageStyle = commentObj ? cssToSvgStyle(commentObj.messageStyle) : void 0;
12123
12105
  if (info.kind === "sync" || info.kind === "async") {
12124
- let fromX = snapX$1(coordinates.getPosition(info.from));
12125
- const toX = snapX$1(coordinates.getPosition(info.to));
12126
- const messageHeight = info.isSelf ? 30 : 16;
12127
- const msgCommentHeight = commentObj?.text ? (info.comment?.trim().split("\n").length || 0) * 20 : 0;
12106
+ let fromX = coordinates.getPosition(info.from);
12107
+ const toX = coordinates.getPosition(info.to);
12108
+ const isAsync = info.kind === "async";
12109
+ const messageHeight = !info.isSelf ? MESSAGE_HEIGHT : isAsync ? ASYNC_SELF_MESSAGE_HEIGHT : SELF_MESSAGE_HEIGHT;
12110
+ const msgCommentHeight = commentObj?.text ? (info.comment?.trim().split("\n").length || 0) * COMMENT_LINE_HEIGHT : 0;
12128
12111
  const messageY = coord.top + adjust + msgCommentHeight + messageHeight - 0.5;
12129
12112
  if (info.senderOccurrenceDepth >= 1 && !info.isSelf) {
12130
- const depth = info.senderOccurrenceDepth;
12131
- const occOffset = depth * OCCURRENCE_BAR_SIDE_WIDTH;
12113
+ const senderAnchor = new Anchor2(fromX, info.senderOccurrenceDepth);
12132
12114
  const isLTR = fromX < toX;
12133
- fromX = isLTR ? fromX + occOffset : fromX - OCCURRENCE_BAR_SIDE_WIDTH + (depth - 1) * OCCURRENCE_BAR_SIDE_WIDTH;
12115
+ fromX = isLTR ? senderAnchor.rightEdgeOfRightWall() : senderAnchor.leftEdgeOfRightWall();
12134
12116
  }
12135
12117
  const targetDepth = info.targetOccurrenceDepth || (info.targetHasOccurrence ? 1 : 0);
12136
12118
  const nestingOffset = targetDepth * OCCURRENCE_BAR_SIDE_WIDTH;
12137
12119
  if (info.isSelf) {
12138
- const isAsync = info.kind === "async";
12139
12120
  const selfYOffset = 0;
12140
12121
  const selfWidth = isAsync ? 28 : OCCURRENCE_WIDTH;
12141
- const selfHeight = isAsync ? 44 : messageHeight;
12142
- const selfX = info.senderOccurrenceDepth >= 1 ? fromX + OCCURRENCE_BAR_SIDE_WIDTH + (info.senderOccurrenceDepth - 1) * OCCURRENCE_BAR_SIDE_WIDTH : fromX;
12122
+ const selfX = new Anchor2(
12123
+ fromX,
12124
+ info.senderOccurrenceDepth
12125
+ ).rightEdgeOfRightWall();
12143
12126
  selfCalls.push({
12144
12127
  x: selfX,
12145
12128
  y: coord.top + selfYOffset + msgCommentHeight,
12146
12129
  width: selfWidth,
12147
- height: selfHeight,
12130
+ height: messageHeight,
12148
12131
  label: info.label,
12149
12132
  arrowStyle: isAsync ? "open" : "solid",
12150
12133
  number: info.number,
@@ -12216,16 +12199,15 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12216
12199
  continue;
12217
12200
  }
12218
12201
  if (info.kind === "creation") {
12219
- const CREATION_MSG_HEIGHT = 40;
12220
- let fromX = snapX$1(coordinates.getPosition(info.from));
12221
- const toX = snapX$1(coordinates.getPosition(info.to));
12202
+ let fromX = coordinates.getPosition(info.from);
12203
+ const toX = coordinates.getPosition(info.to);
12222
12204
  if (info.senderOccurrenceDepth >= 1) {
12223
12205
  const occOffset = OCCURRENCE_BAR_SIDE_WIDTH + (info.senderOccurrenceDepth - 1) * OCCURRENCE_BAR_SIDE_WIDTH;
12224
12206
  const isLTR = fromX < toX;
12225
12207
  fromX = isLTR ? fromX + occOffset : fromX - occOffset;
12226
12208
  }
12227
12209
  const targetParticipant = participants.find((p) => p.name === info.to);
12228
- const messageY = targetParticipant ? targetParticipant.y + PARTICIPANT_VISUAL_HEIGHT / 2 : coord.top + CREATION_MSG_HEIGHT / 2;
12210
+ const messageY = targetParticipant ? targetParticipant.y + PARTICIPANT_VISUAL_HEIGHT / 2 : coord.top + CREATION_MESSAGE_HEIGHT / 2;
12229
12211
  if (targetParticipant) {
12230
12212
  creations.push({
12231
12213
  participant: targetParticipant,
@@ -12244,7 +12226,7 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12244
12226
  });
12245
12227
  }
12246
12228
  const occX = toX - OCCURRENCE_BAR_SIDE_WIDTH;
12247
- const occY = targetParticipant ? targetParticipant.y + PARTICIPANT_VISUAL_HEIGHT - 2 : coord.top + CREATION_MSG_HEIGHT - 2;
12229
+ const occY = targetParticipant ? targetParticipant.y + PARTICIPANT_VISUAL_HEIGHT - 2 : coord.top + CREATION_MESSAGE_HEIGHT - 2;
12248
12230
  const occHeight = Math.max(
12249
12231
  coord.top + coord.height - occY,
12250
12232
  OCCURRENCE_EMPTY_HEIGHT
@@ -12261,7 +12243,7 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12261
12243
  const creationAssign = creationCtx?.Assignment?.();
12262
12244
  if (creationAssign?.assignee) {
12263
12245
  const occBottom = occY + occHeight - 2;
12264
- const rawFromX = snapX$1(coordinates.getPosition(info.from));
12246
+ const rawFromX = coordinates.getPosition(info.from);
12265
12247
  const isLTR = rawFromX < toX;
12266
12248
  const occHalf = OCCURRENCE_BAR_SIDE_WIDTH;
12267
12249
  const senderNest = Math.max(info.senderOccurrenceDepth - 1, 0) * OCCURRENCE_BAR_SIDE_WIDTH;
@@ -12283,7 +12265,7 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12283
12265
  }
12284
12266
  if (info.kind === "fragment" && info.fragmentKind) {
12285
12267
  const adjustedCoord = { top: coord.top + adjust, height: coord.height };
12286
- const fragmentCommentHeight = commentObj?.text ? (info.comment?.trim().split("\n").length || 0) * 20 : 0;
12268
+ const fragmentCommentHeight = commentObj?.text ? (info.comment?.trim().split("\n").length || 0) * COMMENT_LINE_HEIGHT : 0;
12287
12269
  const fragmentResult = buildFragmentGeometry(
12288
12270
  info,
12289
12271
  adjustedCoord,
@@ -12305,26 +12287,14 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12305
12287
  continue;
12306
12288
  }
12307
12289
  if (info.kind === "return") {
12308
- const rawFromX = snapX$1(coordinates.getPosition(info.from));
12309
- const rawToX = snapX$1(coordinates.getPosition(info.to));
12290
+ const rawFromX = coordinates.getPosition(info.from);
12291
+ const rawToX = coordinates.getPosition(info.to);
12310
12292
  const isReverse = rawToX < rawFromX;
12311
- const fromLayers = info.senderOccurrenceDepth;
12312
- let fromX;
12313
- if (isReverse) {
12314
- fromX = fromLayers === 0 ? rawFromX : (fromLayers <= 1 ? rawFromX : rawFromX + OCCURRENCE_BAR_SIDE_WIDTH * (fromLayers - 1)) - OCCURRENCE_BAR_SIDE_WIDTH;
12315
- } else {
12316
- fromX = rawFromX + OCCURRENCE_BAR_SIDE_WIDTH * fromLayers + 1;
12317
- }
12318
- const toLayers = info.targetOccurrenceDepth || 0;
12319
- let toX;
12320
- if (isReverse) {
12321
- toX = rawToX + OCCURRENCE_BAR_SIDE_WIDTH * toLayers;
12322
- } else {
12323
- toX = toLayers === 0 ? rawToX : (toLayers <= 1 ? rawToX : rawToX + OCCURRENCE_BAR_SIDE_WIDTH * (toLayers - 1)) - OCCURRENCE_BAR_SIDE_WIDTH;
12324
- }
12325
- if (isReverse) {
12326
- toX += LIFELINE_WIDTH;
12327
- }
12293
+ const fromAnchor = new Anchor2(rawFromX, info.senderOccurrenceDepth);
12294
+ const toAnchor = new Anchor2(rawToX, info.targetOccurrenceDepth || 0);
12295
+ const RETURN_STROKE_OFFSET = 1;
12296
+ const fromX = isReverse ? fromAnchor.leftEdgeOfRightWall() : fromAnchor.rightEdgeOfRightWall() + RETURN_STROKE_OFFSET;
12297
+ const toX = isReverse ? toAnchor.rightEdgeOfRightWall() + LIFELINE_WIDTH : toAnchor.leftEdgeOfRightWall();
12328
12298
  const returnOffset = coord.height === 0 ? 16.5 : 15.5;
12329
12299
  const returnY = coord.top + adjust + returnOffset;
12330
12300
  returns.push({
@@ -12346,7 +12316,9 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12346
12316
  y: coord.top + adjust + coord.height / 2,
12347
12317
  width: diagramWidth,
12348
12318
  label: info.label,
12349
- labelWidth: measureSvgFragmentLabelWidth(resolveEmojiInText(cleanLabel))
12319
+ labelWidth: measureSvgFragmentLabelWidth(
12320
+ resolveEmojiInText(cleanLabel)
12321
+ )
12350
12322
  });
12351
12323
  continue;
12352
12324
  }
@@ -12369,11 +12341,23 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12369
12341
  }
12370
12342
  }
12371
12343
  if (depth > 0) {
12372
- occurrences[i] = { ...inner, x: inner.x + depth * OCCURRENCE_BAR_SIDE_WIDTH };
12344
+ occurrences[i] = {
12345
+ ...inner,
12346
+ x: inner.x + depth * OCCURRENCE_BAR_SIDE_WIDTH
12347
+ };
12373
12348
  }
12374
12349
  }
12375
- const totalReturnDebt = adjustMap.get("__totalDebt__") || 0;
12376
- return { messages, selfCalls, occurrences, creations, fragments, returns, dividers, comments, totalReturnDebt, maxReturnBottom };
12350
+ return {
12351
+ messages,
12352
+ selfCalls,
12353
+ occurrences,
12354
+ creations,
12355
+ fragments,
12356
+ returns,
12357
+ dividers,
12358
+ comments,
12359
+ maxReturnBottom
12360
+ };
12377
12361
  }
12378
12362
 
12379
12363
  function buildGeometry(input) {
@@ -12417,17 +12401,7 @@ function buildGeometry(input) {
12417
12401
  participants,
12418
12402
  lifelineBottom
12419
12403
  );
12420
- const localParticipants = getLocalParticipantNames(rootContext);
12421
- const orderedNames = coordinates.orderedParticipantNames();
12422
- const leftParticipant = orderedNames.find((p) => localParticipants.includes(p)) || "";
12423
- const rightParticipant = orderedNames.slice().reverse().find((p) => localParticipants.includes(p)) || "";
12424
- const participantWidth = coordinates.distance(leftParticipant, rightParticipant) + coordinates.half(leftParticipant) + coordinates.half(rightParticipant);
12425
- const selfMessages = AllMessages(rootContext).filter((m) => m.from === m.to);
12426
- const extraWidths = selfMessages.map(
12427
- (m) => coordinates.getMessageWidth(m) - coordinates.distance(m.from || _STARTER_, rightParticipant) - coordinates.half(rightParticipant)
12428
- );
12429
- const extraWidthDueToSelfMessage = Math.max(0, ...extraWidths);
12430
- let diagramWidth = Math.max(participantWidth, FRAGMENT_MIN_WIDTH) + extraWidthDueToSelfMessage;
12404
+ let diagramWidth = TotalWidth(rootContext, coordinates) - frameBorder.left - frameBorder.right;
12431
12405
  if (measureText) {
12432
12406
  const LABEL_PAD = 10;
12433
12407
  for (const msg of messages) {
@@ -12765,7 +12739,7 @@ function renderOccurrence(o) {
12765
12739
  return `<rect x="${rx}" y="${o.y + strokeHalf}" width="${o.width - strokeHalf * 2}" height="${o.height - strokeHalf * 2}" rx="1" class="occurrence" data-participant="${esc(o.participantName)}"/>`;
12766
12740
  }
12767
12741
 
12768
- const HEADER_HEIGHT = 25;
12742
+ const HEADER_HEIGHT = DEFAULT_LAYOUT_METRICS.fragmentHeaderHeight;
12769
12743
  const BRACKET_WIDTH = 3.89;
12770
12744
  const TEXT_PAD_X = 4;
12771
12745
  const STROKE_WIDTH = 1;
@@ -13126,47 +13100,153 @@ function renderGroup(g) {
13126
13100
  </g>`;
13127
13101
  }
13128
13102
 
13129
- const FRAME_HEADER_HEIGHT = 28;
13130
- const FRAME_BORDER_RADIUS = 4;
13131
- const DEFAULT_THEME_STYLES = `
13132
- .frame-border-outer { fill: #666; }
13133
- .frame-border-inner { fill: #ffffff; }
13134
- .frame-header-bg { fill: #ffffff; }
13135
- .frame-header-line { stroke: #666; stroke-width: 1; shape-rendering: crispEdges; }
13136
- .frame-title { font-family: Helvetica, Verdana, serif; font-size: 16px; font-weight: 600; fill: #222; }
13137
- .participant-box { fill: #ffffff; stroke: #666; stroke-width: 2; }
13138
- .participant-label { font-family: Helvetica, Verdana, serif; font-size: 16px; fill: #222; }
13139
- .participant-icon { color: #222; }
13140
- .participant-icon [fill="currentColor"]:not([stroke]) { stroke: #666; stroke-width: 1; }
13141
- .lifeline { stroke: #666; stroke-width: 1; }
13142
- .message-line { stroke: #000; stroke-width: 2; shape-rendering: crispEdges; }
13143
- .message-label { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #222; }
13144
- .arrow-head { fill: #000; stroke: #000; stroke-width: 2; }
13103
+ const HELVETICA = "Helvetica, Verdana, serif";
13104
+ const DEFAULT_PALETTE = {
13105
+ frameBorder: "#666",
13106
+ frameBg: "#ffffff",
13107
+ titleText: "#222",
13108
+ participantBg: "#ffffff",
13109
+ participantBorder: "#666",
13110
+ participantText: "#222",
13111
+ messageArrow: "#000",
13112
+ messageText: "#222",
13113
+ occurrenceBg: "#dedede",
13114
+ occurrenceBorder: "#666",
13115
+ fragmentBorder: "#666",
13116
+ fragmentHeaderBg: "#dedede",
13117
+ fragmentHeaderOpacity: "0.498",
13118
+ fragmentText: "#000",
13119
+ fragmentSeparator: "#e5e7eb",
13120
+ commentText: "#333",
13121
+ fontFamily: HELVETICA
13122
+ };
13123
+ const THEME_PALETTES = {
13124
+ "theme-default": DEFAULT_PALETTE,
13125
+ // `.theme-mermaid` sets only base/text/border/occurrence, and hides the
13126
+ // footer — chrome the SVG output does not draw. Every slot it leaves unset
13127
+ // matches theme-default, so the two render identically.
13128
+ "theme-mermaid": DEFAULT_PALETTE,
13129
+ // src/assets/tailwind.css `.theme-clean-light`.
13130
+ "theme-clean-light": {
13131
+ frameBorder: "#e5e9f0",
13132
+ frameBg: "#ffffff",
13133
+ titleText: "#353748",
13134
+ participantBg: "#edf4fe",
13135
+ participantBorder: "#368eff",
13136
+ participantText: "#368eff",
13137
+ messageArrow: "#368eff",
13138
+ messageText: "#368eff",
13139
+ occurrenceBg: "#ffffff",
13140
+ occurrenceBorder: "#368eff",
13141
+ fragmentBorder: "#c8c9c9",
13142
+ fragmentHeaderBg: "#edf4fe",
13143
+ fragmentHeaderOpacity: "1",
13144
+ fragmentText: "#368eff",
13145
+ fragmentSeparator: "#e5e9f0",
13146
+ commentText: "#353748",
13147
+ fontFamily: HELVETICA
13148
+ },
13149
+ // src/assets/tailwind.css `.theme-clean-dark`.
13150
+ "theme-clean-dark": {
13151
+ frameBorder: "#cecfd2",
13152
+ frameBg: "#111628",
13153
+ titleText: "#cecfd2",
13154
+ participantBg: "#5964f2",
13155
+ participantBorder: "#cecfd2",
13156
+ participantText: "#536fff",
13157
+ messageArrow: "#536fff",
13158
+ messageText: "#cecfd2",
13159
+ occurrenceBg: "#5964f2",
13160
+ occurrenceBorder: "#cecfd2",
13161
+ fragmentBorder: "#cecfd2",
13162
+ fragmentHeaderBg: "#5964f2",
13163
+ fragmentHeaderOpacity: "1",
13164
+ fragmentText: "#cecfd2",
13165
+ fragmentSeparator: "#cecfd2",
13166
+ commentText: "#cecfd2",
13167
+ fontFamily: HELVETICA
13168
+ },
13169
+ // src/assets/tailwind.css `.theme-neon`. The DOM theme also swaps in the
13170
+ // MS Sans Serif webfont (`.zenuml .theme-neon`); the SVG names it first and
13171
+ // keeps the Helvetica stack behind it, since a consumer embedding the SVG
13172
+ // will not have that @font-face rule.
13173
+ "theme-neon": {
13174
+ frameBorder: "#60ff33",
13175
+ frameBg: "#000000",
13176
+ titleText: "#8ffc5b",
13177
+ participantBg: "#000000",
13178
+ participantBorder: "#8ffc5b",
13179
+ participantText: "#8ffc5b",
13180
+ messageArrow: "#8ffc5b",
13181
+ messageText: "#8ffc5b",
13182
+ occurrenceBg: "#8ffc5b",
13183
+ occurrenceBorder: "#8ffc5b",
13184
+ fragmentBorder: "#60ff33",
13185
+ fragmentHeaderBg: "#000000",
13186
+ fragmentHeaderOpacity: "1",
13187
+ fragmentText: "#8ffc5b",
13188
+ fragmentSeparator: "#60ff33",
13189
+ commentText: "#8ffc5b",
13190
+ fontFamily: `"MS Sans Serif", ${HELVETICA}`
13191
+ }
13192
+ };
13193
+ function resolvePalette(theme) {
13194
+ if (theme && theme in THEME_PALETTES) {
13195
+ return THEME_PALETTES[theme];
13196
+ }
13197
+ return DEFAULT_PALETTE;
13198
+ }
13199
+ function buildThemeStyles(p) {
13200
+ return `
13201
+ .frame-border-outer { fill: ${p.frameBorder}; }
13202
+ .frame-border-inner { fill: ${p.frameBg}; }
13203
+ .frame-header-bg { fill: ${p.frameBg}; }
13204
+ .frame-header-line { stroke: ${p.frameBorder}; stroke-width: 1; shape-rendering: crispEdges; }
13205
+ .frame-title { font-family: ${p.fontFamily}; font-size: 16px; font-weight: 600; fill: ${p.titleText}; }
13206
+ .participant-box { fill: ${p.participantBg}; stroke: ${p.participantBorder}; stroke-width: 2; }
13207
+ .participant-label { font-family: ${p.fontFamily}; font-size: 16px; fill: ${p.participantText}; }
13208
+ .participant-icon { color: ${p.participantText}; }
13209
+ .participant-icon [fill="currentColor"]:not([stroke]) { stroke: ${p.participantBorder}; stroke-width: 1; }
13210
+ .lifeline { stroke: ${p.participantBorder}; stroke-width: 1; }
13211
+ .message-line { stroke: ${p.messageArrow}; stroke-width: 2; shape-rendering: crispEdges; }
13212
+ .message-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.messageText}; }
13213
+ .arrow-head { fill: ${p.messageArrow}; stroke: ${p.messageArrow}; stroke-width: 2; }
13145
13214
  .arrow-open { fill: none; }
13146
- .occurrence { fill: #dedede; stroke: #666; stroke-width: 2; shape-rendering: crispEdges; }
13147
- .fragment-border { fill: none; stroke: #666; stroke-width: 1; shape-rendering: crispEdges; }
13148
- .fragment-header { fill: #dedede; fill-opacity: 0.498; stroke: none; shape-rendering: crispEdges; }
13149
- .fragment-label { font-family: Helvetica, Verdana, serif; font-size: 14px; font-weight: 600; fill: #000; }
13150
- .fragment-condition { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #000; }
13151
- .fragment-separator { stroke: #e5e7eb; stroke-width: 1; shape-rendering: crispEdges; }
13152
- .fragment-section-label { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #000; }
13153
- .return-line { stroke: #000; stroke-width: 2; stroke-dasharray: 6,4; shape-rendering: crispEdges; }
13154
- .return-arrow { stroke: #000; stroke-width: 2; fill: none; }
13155
- .return-label { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #222; }
13156
- .return-icon { fill: #222; }
13215
+ .occurrence { fill: ${p.occurrenceBg}; stroke: ${p.occurrenceBorder}; stroke-width: 2; shape-rendering: crispEdges; }
13216
+ .fragment-border { fill: none; stroke: ${p.fragmentBorder}; stroke-width: 1; shape-rendering: crispEdges; }
13217
+ .fragment-header { fill: ${p.fragmentHeaderBg}; fill-opacity: ${p.fragmentHeaderOpacity}; stroke: none; shape-rendering: crispEdges; }
13218
+ .fragment-label { font-family: ${p.fontFamily}; font-size: 14px; font-weight: 600; fill: ${p.fragmentText}; }
13219
+ .fragment-condition { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.fragmentText}; }
13220
+ .fragment-separator { stroke: ${p.fragmentSeparator}; stroke-width: 1; shape-rendering: crispEdges; }
13221
+ .fragment-section-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.fragmentText}; }
13222
+ .return-line { stroke: ${p.messageArrow}; stroke-width: 2; stroke-dasharray: 6,4; shape-rendering: crispEdges; }
13223
+ .return-arrow { stroke: ${p.messageArrow}; stroke-width: 2; fill: none; }
13224
+ .return-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.messageText}; }
13225
+ .return-icon { fill: ${p.messageText}; }
13157
13226
  .divider-line { stroke: #aaaa33; stroke-width: 1; }
13158
13227
  .divider-bg { fill: #fff5ad; stroke: #aaaa33; stroke-width: 1; }
13159
- .divider-label { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #333; }
13160
- .comment-text { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #333; opacity: 0.5; }
13161
- .seq-number { font-family: Helvetica, Verdana, serif; font-size: 12px; font-weight: 100; fill: #6b7280; }
13162
- .group-outline { fill: none; stroke: #666; }
13163
- .group-title-bg { fill: #ffffff; stroke: none; }
13164
- .group-title-text { font-family: Helvetica, Verdana, serif; font-size: 13px; font-weight: 400; fill: #222; }
13228
+ .divider-label { font-family: ${p.fontFamily}; font-size: 14px; fill: #333; }
13229
+ .comment-text { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.commentText}; opacity: 0.5; }
13230
+ .seq-number { font-family: ${p.fontFamily}; font-size: 12px; font-weight: 100; fill: #6b7280; }
13231
+ .group-outline { fill: none; stroke: ${p.participantBorder}; }
13232
+ .group-title-bg { fill: ${p.frameBg}; stroke: none; }
13233
+ .group-title-text { font-family: ${p.fontFamily}; font-size: 13px; font-weight: 400; fill: ${p.participantText}; }
13165
13234
  `;
13235
+ }
13236
+
13237
+ const FRAME_HEADER_HEIGHT = 28;
13238
+ const FRAME_BORDER_RADIUS = 4;
13166
13239
  function renderToSvg(code, options) {
13167
13240
  const rootContext = RootContext(code);
13168
13241
  if (!rootContext) {
13169
- return { svg: "<svg></svg>", innerSvg: "", width: 0, height: 0, viewBox: "0 0 0 0", geometry: void 0 };
13242
+ return {
13243
+ svg: "<svg></svg>",
13244
+ innerSvg: "",
13245
+ width: 0,
13246
+ height: 0,
13247
+ viewBox: "0 0 0 0",
13248
+ geometry: void 0
13249
+ };
13170
13250
  }
13171
13251
  const coordinates = new Coordinates(rootContext, WidthProviderOnCanvas);
13172
13252
  const verticalCoordinates = new VerticalCoordinates(rootContext);
@@ -13180,7 +13260,7 @@ function renderToSvg(code, options) {
13180
13260
  title,
13181
13261
  measureText: WidthProviderOnCanvas
13182
13262
  });
13183
- return { ...composeSvg(geometry), geometry };
13263
+ return { ...composeSvg(geometry, options), geometry };
13184
13264
  }
13185
13265
  function composeSvg(g, options) {
13186
13266
  const padding = 10;
@@ -13239,10 +13319,10 @@ function composeSvg(g, options) {
13239
13319
  const headerLineY = headerH + contentPaddingTop;
13240
13320
  const headerLineDrawY = headerLineY - 0.5;
13241
13321
  const headerLineSvg = `<line class="frame-header-line" x1="1" y1="${headerLineDrawY}" x2="${viewWidth - 1}" y2="${headerLineDrawY}"/>`;
13242
- const titleSvg = g.title ? `<text x="5" y="${headerLineDrawY / 2}" dominant-baseline="central" class="frame-title">${escXml(g.title)}</text>` : "";
13322
+ const titleSvg = g.title ? `<text x="5" y="${headerLineDrawY / 2}" dominant-baseline="central" class="frame-title">${esc(g.title)}</text>` : "";
13243
13323
  const viewBox = `0 0 ${viewWidth} ${viewHeight}`;
13244
13324
  const defs = `<defs>
13245
- <style>${DEFAULT_THEME_STYLES}</style>
13325
+ <style>${buildThemeStyles(resolvePalette(options?.theme))}</style>
13246
13326
  </defs>`;
13247
13327
  const frame = `${frameSvg}
13248
13328
  ${headerLineSvg}
@@ -13258,9 +13338,6 @@ ${innerSvg}
13258
13338
  </svg>`;
13259
13339
  return { svg, innerSvg, width: viewWidth, height: viewHeight, viewBox };
13260
13340
  }
13261
- function escXml(s) {
13262
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13263
- }
13264
13341
 
13265
13342
  function readVersion() {
13266
13343
  const pkgPath = resolve(import.meta.dir, "../../package.json");
@@ -13282,7 +13359,8 @@ Rendering:
13282
13359
  -o, --output <file> Output file (use "-" for stdout; default: <input>.svg)
13283
13360
  -e, --outputFormat <format> Output format: "svg" (default) or "png"
13284
13361
  -s, --scale <factor> Pixel scale factor for PNG (default: 2; ignored for SVG)
13285
- -t, --theme <name> Theme name passed to renderer (e.g. "theme-default")
13362
+ -t, --theme <name> Theme: default, mermaid, clean-light, clean-dark,
13363
+ neon (prefix "theme-"; unknown names use default)
13286
13364
  -c, --configFile <file> JSON config file with { theme, scale, outputFormat }
13287
13365
  --md Markdown mode: render zenuml code blocks and produce output Markdown
13288
13366
 
@@ -13643,7 +13721,6 @@ async function checkOne(input) {
13643
13721
  errors: [{ line: 0, column: 0, msg: err.message }]
13644
13722
  };
13645
13723
  }
13646
- Parser.Errors.length = 0;
13647
13724
  Parser.ErrorDetails.length = 0;
13648
13725
  Parser.RootContext(code);
13649
13726
  const errors = Parser.ErrorDetails.map((e) => ({
@@ -13657,6 +13734,69 @@ async function checkOne(input) {
13657
13734
  errors
13658
13735
  };
13659
13736
  }
13737
+ async function renderMarkdownFile(inputArg, args, config) {
13738
+ const mdContent = await readCode(inputArg);
13739
+ const effectiveFormat = args.outputFormat ?? config.configOutputFormat ?? "svg";
13740
+ if (effectiveFormat !== "svg" && effectiveFormat !== "png") {
13741
+ throw new Error(
13742
+ `Unsupported output format: "${effectiveFormat}". Use "svg" or "png".`
13743
+ );
13744
+ }
13745
+ const inputPath = inputArg === "-" ? void 0 : resolve(inputArg);
13746
+ const outputPath = args.output === "-" ? "-" : args.output ? resolve(args.output) : inputPath ? join(
13747
+ dirname(inputPath),
13748
+ `${basename(inputArg, extname(inputArg))}-rendered.md`
13749
+ ) : "-";
13750
+ const imageDir = outputPath !== "-" ? dirname(outputPath) : inputPath ? dirname(inputPath) : process.cwd();
13751
+ const imageStem = inputPath ? basename(inputArg, extname(inputArg)) : outputPath !== "-" ? basename(outputPath, extname(outputPath)) : "diagram";
13752
+ const renderOptions = {};
13753
+ const effectiveTheme = args.theme ?? config.configTheme;
13754
+ if (effectiveTheme) {
13755
+ renderOptions.theme = effectiveTheme;
13756
+ }
13757
+ const blocks = extractZenumlBlocks(mdContent);
13758
+ const imageFiles = /* @__PURE__ */ new Map();
13759
+ for (const block of blocks) {
13760
+ if (block.empty) continue;
13761
+ const imageFilename = `${imageStem}-zenuml-${block.index}.${effectiveFormat}`;
13762
+ const imageFilePath = join(imageDir, imageFilename);
13763
+ let result;
13764
+ try {
13765
+ result = renderToSvg(block.code, renderOptions);
13766
+ } catch (err) {
13767
+ throw new Error(
13768
+ `Failed to render zenuml block ${block.index}: ${err.message}`
13769
+ );
13770
+ }
13771
+ mkdirSync(imageDir, { recursive: true });
13772
+ const image = effectiveFormat === "png" ? await rasterizeToPng(
13773
+ result.svg,
13774
+ result.width,
13775
+ result.height,
13776
+ args.scale ?? config.configScale ?? 2
13777
+ ) : result.svg;
13778
+ writeFileSync(imageFilePath, image);
13779
+ imageFiles.set(block.index, imageFilename);
13780
+ if (!args.quiet && !args.watch) {
13781
+ process.stderr.write(`Wrote ${imageFilePath}
13782
+ `);
13783
+ }
13784
+ }
13785
+ const outputMd = [...blocks].reverse().reduce((markdown, block) => {
13786
+ const replacement = block.empty ? "" : `![${block.title || `diagram ${block.index + 1}`}](${imageFiles.get(block.index)})`;
13787
+ return markdown.replace(block.raw, replacement);
13788
+ }, mdContent);
13789
+ if (outputPath === "-") {
13790
+ process.stdout.write(outputMd);
13791
+ return;
13792
+ }
13793
+ mkdirSync(dirname(outputPath), { recursive: true });
13794
+ writeFileSync(outputPath, outputMd, "utf-8");
13795
+ if (!args.quiet && !args.watch) {
13796
+ process.stderr.write(`Wrote ${outputPath}
13797
+ `);
13798
+ }
13799
+ }
13660
13800
  async function main() {
13661
13801
  if (!globalThis.OffscreenCanvas && typeof document === "undefined") {
13662
13802
  try {
@@ -13746,7 +13886,6 @@ async function main() {
13746
13886
  `);
13747
13887
  process.exit(1);
13748
13888
  }
13749
- Parser.Errors.length = 0;
13750
13889
  Parser.ErrorDetails.length = 0;
13751
13890
  const tree = Parser.RootContext(code);
13752
13891
  if (Parser.ErrorDetails.length > 0) {
@@ -13760,146 +13899,6 @@ async function main() {
13760
13899
  process.stdout.write(JSON.stringify(ast, null, 2) + "\n");
13761
13900
  process.exit(0);
13762
13901
  }
13763
- const isMdMode = args.md || expandedInputs.some((f) => f !== "-" && /\.(?:md|markdown)$/i.test(f));
13764
- if (isMdMode) {
13765
- if (expandedInputs.length > 1) {
13766
- process.stderr.write(
13767
- "Error: --md mode supports only a single input file.\n"
13768
- );
13769
- process.exit(1);
13770
- }
13771
- const inputArg = expandedInputs[0];
13772
- if (args.md && inputArg !== "-") {
13773
- const ext = extname(inputArg).toLowerCase();
13774
- if (ext !== ".md" && ext !== ".markdown") {
13775
- process.stderr.write(
13776
- `Error: --md flag requires a .md or .markdown input file, got: ${inputArg}
13777
- `
13778
- );
13779
- process.exit(1);
13780
- }
13781
- }
13782
- let mdContent;
13783
- try {
13784
- mdContent = await readCode(inputArg);
13785
- } catch (err) {
13786
- process.stderr.write(`Error: ${err.message}
13787
- `);
13788
- process.exit(1);
13789
- }
13790
- const effectiveFormat = args.outputFormat ?? "svg";
13791
- if (effectiveFormat !== "svg" && effectiveFormat !== "png") {
13792
- process.stderr.write(
13793
- `Error: Unsupported output format: "${effectiveFormat}". Use "svg" or "png".
13794
- `
13795
- );
13796
- process.exit(1);
13797
- }
13798
- let imageDir;
13799
- let mdOutputPath;
13800
- if (args.output && args.output !== "-") {
13801
- const resolvedOutput = resolve(args.output);
13802
- imageDir = dirname(resolvedOutput);
13803
- mdOutputPath = resolvedOutput;
13804
- } else if (args.output === "-") {
13805
- if (inputArg !== "-") {
13806
- imageDir = dirname(resolve(inputArg));
13807
- } else {
13808
- imageDir = process.cwd();
13809
- }
13810
- mdOutputPath = "-";
13811
- } else {
13812
- if (inputArg !== "-") {
13813
- const resolvedInput = resolve(inputArg);
13814
- const inputDir = dirname(resolvedInput);
13815
- const ext = extname(inputArg);
13816
- const stem = basename(inputArg, ext);
13817
- imageDir = inputDir;
13818
- mdOutputPath = join(inputDir, `${stem}-rendered.md`);
13819
- } else {
13820
- imageDir = process.cwd();
13821
- mdOutputPath = "-";
13822
- }
13823
- }
13824
- let imageStem;
13825
- if (inputArg !== "-") {
13826
- const ext = extname(inputArg);
13827
- imageStem = basename(inputArg, ext);
13828
- } else if (mdOutputPath !== "-") {
13829
- const ext = extname(mdOutputPath);
13830
- imageStem = basename(mdOutputPath, ext);
13831
- } else {
13832
- imageStem = "diagram";
13833
- }
13834
- const blocks = extractZenumlBlocks(mdContent);
13835
- const effectiveScale = args.scale ?? 2;
13836
- const effectiveTheme = args.theme;
13837
- const renderOptions = {};
13838
- if (effectiveTheme) {
13839
- renderOptions.theme = effectiveTheme;
13840
- }
13841
- const imageFiles = /* @__PURE__ */ new Map();
13842
- for (const block of blocks) {
13843
- if (block.empty) continue;
13844
- const imageFilename = `${imageStem}-zenuml-${block.index}.${effectiveFormat}`;
13845
- const imageFilePath = join(imageDir, imageFilename);
13846
- let svg;
13847
- let svgWidth;
13848
- let svgHeight;
13849
- try {
13850
- const result = renderToSvg(block.code, renderOptions);
13851
- svg = result.svg;
13852
- svgWidth = result.width;
13853
- svgHeight = result.height;
13854
- } catch (err) {
13855
- process.stderr.write(
13856
- `Error: Failed to render zenuml block ${block.index}: ${err.message}
13857
- `
13858
- );
13859
- process.exit(1);
13860
- }
13861
- mkdirSync(imageDir, { recursive: true });
13862
- if (effectiveFormat === "png") {
13863
- const pngBuffer = await rasterizeToPng(
13864
- svg,
13865
- svgWidth,
13866
- svgHeight,
13867
- effectiveScale
13868
- );
13869
- writeFileSync(imageFilePath, pngBuffer);
13870
- } else {
13871
- writeFileSync(imageFilePath, svg, "utf-8");
13872
- }
13873
- if (!args.quiet) {
13874
- process.stderr.write(`Wrote ${imageFilePath}
13875
- `);
13876
- }
13877
- imageFiles.set(block.index, imageFilename);
13878
- }
13879
- let outputMd = mdContent;
13880
- const sortedBlocks = [...blocks].reverse();
13881
- for (const block of sortedBlocks) {
13882
- const altText = block.title || `diagram ${block.index + 1}`;
13883
- if (block.empty) {
13884
- outputMd = outputMd.replace(block.raw, "");
13885
- } else {
13886
- const imageFilename = imageFiles.get(block.index);
13887
- const replacement = `![${altText}](${imageFilename})`;
13888
- outputMd = outputMd.replace(block.raw, replacement);
13889
- }
13890
- }
13891
- if (mdOutputPath === "-") {
13892
- process.stdout.write(outputMd);
13893
- } else {
13894
- mkdirSync(dirname(mdOutputPath), { recursive: true });
13895
- writeFileSync(mdOutputPath, outputMd, "utf-8");
13896
- if (!args.quiet) {
13897
- process.stderr.write(`Wrote ${mdOutputPath}
13898
- `);
13899
- }
13900
- }
13901
- process.exit(0);
13902
- }
13903
13902
  let configScale;
13904
13903
  let configTheme;
13905
13904
  let configOutputFormat;
@@ -13917,99 +13916,30 @@ async function main() {
13917
13916
  );
13918
13917
  process.exit(1);
13919
13918
  }
13920
- if (args.watch) {
13921
- const renderForWatch = async (inputArg) => {
13922
- await renderOneFile(inputArg, args, {
13923
- configScale,
13924
- configTheme,
13925
- configOutputFormat});
13926
- };
13927
- const renderMdForWatch = async (inputArg) => {
13928
- let mdContent;
13929
- try {
13930
- mdContent = readFileSync(resolve(inputArg), "utf-8");
13931
- } catch {
13932
- throw new Error(`Cannot read input file: ${resolve(inputArg)}`);
13933
- }
13934
- const effectiveFormat = args.outputFormat ?? "svg";
13935
- if (effectiveFormat !== "svg" && effectiveFormat !== "png") {
13936
- throw new Error(
13937
- `Unsupported output format: "${effectiveFormat}". Use "svg" or "png".`
13938
- );
13939
- }
13940
- let mdOutputPath;
13941
- let imageDir;
13942
- if (args.output && args.output !== "-") {
13943
- const resolvedOutput = resolve(args.output);
13944
- imageDir = dirname(resolvedOutput);
13945
- mdOutputPath = resolvedOutput;
13946
- } else if (args.output === "-") {
13947
- imageDir = dirname(resolve(inputArg));
13948
- mdOutputPath = "-";
13949
- } else {
13950
- const resolvedInput = resolve(inputArg);
13951
- const inputDir = dirname(resolvedInput);
13952
- const ext2 = extname(inputArg);
13953
- const stem = basename(inputArg, ext2);
13954
- imageDir = inputDir;
13955
- mdOutputPath = join(inputDir, `${stem}-rendered.md`);
13956
- }
13957
- const ext = extname(inputArg);
13958
- const imageStem = basename(inputArg, ext);
13959
- const blocks = extractZenumlBlocks(mdContent);
13960
- const effectiveScale = args.scale ?? configScale ?? 2;
13961
- const imageFiles = /* @__PURE__ */ new Map();
13962
- for (const block of blocks) {
13963
- if (block.empty) continue;
13964
- const imageFilename = `${imageStem}-zenuml-${block.index}.${effectiveFormat}`;
13965
- const imageFilePath = join(imageDir, imageFilename);
13966
- const result = renderToSvg(block.code);
13967
- const { svg, width: svgWidth, height: svgHeight } = result;
13968
- mkdirSync(imageDir, { recursive: true });
13969
- if (effectiveFormat === "png") {
13970
- const pngBuffer = await rasterizeToPng(
13971
- svg,
13972
- svgWidth,
13973
- svgHeight,
13974
- effectiveScale
13975
- );
13976
- writeFileSync(imageFilePath, pngBuffer);
13977
- } else {
13978
- writeFileSync(imageFilePath, svg, "utf-8");
13979
- }
13980
- imageFiles.set(block.index, imageFilename);
13981
- }
13982
- let outputMd = mdContent;
13983
- const sortedBlocks = [...blocks].reverse();
13984
- for (const block of sortedBlocks) {
13985
- const altText = block.title || `diagram ${block.index + 1}`;
13986
- if (block.empty) {
13987
- outputMd = outputMd.replace(block.raw, "");
13988
- } else {
13989
- const imageFilename = imageFiles.get(block.index);
13990
- const replacement = `![${altText}](${imageFilename})`;
13991
- outputMd = outputMd.replace(block.raw, replacement);
13992
- }
13993
- }
13994
- if (mdOutputPath === "-") {
13995
- process.stdout.write(outputMd);
13996
- } else {
13997
- mkdirSync(dirname(mdOutputPath), { recursive: true });
13998
- writeFileSync(mdOutputPath, outputMd, "utf-8");
13999
- }
14000
- };
14001
- const hasMdInputs = expandedInputs.some(
14002
- (f) => /\.(?:md|markdown)$/i.test(f)
13919
+ const config = {
13920
+ configScale,
13921
+ configTheme,
13922
+ configOutputFormat,
13923
+ multipleInputs
13924
+ };
13925
+ const isMdMode = args.md || expandedInputs.some((f) => f !== "-" && /\.(?:md|markdown)$/i.test(f));
13926
+ if (isMdMode && multipleInputs) {
13927
+ process.stderr.write(
13928
+ "Error: --md mode supports only a single input file.\n"
14003
13929
  );
14004
- const renderMdFnForWatch = hasMdInputs ? renderMdForWatch : void 0;
14005
- const watchHandle = await startWatchMode(
14006
- expandedInputs,
14007
- renderForWatch,
14008
- void 0,
14009
- void 0,
14010
- void 0,
14011
- renderMdFnForWatch
13930
+ process.exit(1);
13931
+ }
13932
+ const inputArg = expandedInputs[0];
13933
+ if (args.md && inputArg !== "-" && !/\.(?:md|markdown)$/i.test(inputArg)) {
13934
+ process.stderr.write(
13935
+ `Error: --md flag requires a .md or .markdown input file, got: ${inputArg}
13936
+ `
14012
13937
  );
13938
+ process.exit(1);
13939
+ }
13940
+ if (args.watch) {
13941
+ const renderForWatch = (input) => isMdMode ? renderMarkdownFile(input, args, config) : renderOneFile(input, args, config);
13942
+ const watchHandle = await startWatchMode(expandedInputs, renderForWatch);
14013
13943
  process.on("SIGINT", () => {
14014
13944
  watchHandle.shutdown();
14015
13945
  process.exit(0);
@@ -14018,16 +13948,26 @@ async function main() {
14018
13948
  });
14019
13949
  return;
14020
13950
  }
13951
+ if (isMdMode) {
13952
+ try {
13953
+ await renderMarkdownFile(inputArg, args, config);
13954
+ process.exit(0);
13955
+ } catch (err) {
13956
+ process.stderr.write(`Error: ${err.message}
13957
+ `);
13958
+ process.exit(1);
13959
+ }
13960
+ }
14021
13961
  let rendered = 0;
14022
13962
  let errors = 0;
14023
- for (const inputArg of expandedInputs) {
13963
+ for (const inputArg2 of expandedInputs) {
14024
13964
  if (!args.quiet) {
14025
- const displayName = inputArg === "-" ? "<stdin>" : inputArg;
13965
+ const displayName = inputArg2 === "-" ? "<stdin>" : inputArg2;
14026
13966
  process.stderr.write(`Rendering ${displayName}...
14027
13967
  `);
14028
13968
  }
14029
13969
  try {
14030
- await renderOneFile(inputArg, args, {
13970
+ await renderOneFile(inputArg2, args, {
14031
13971
  configScale,
14032
13972
  configTheme,
14033
13973
  configOutputFormat,
@@ -14036,7 +13976,7 @@ async function main() {
14036
13976
  rendered++;
14037
13977
  } catch (err) {
14038
13978
  errors++;
14039
- process.stderr.write(`Error: ${inputArg}: ${err.message}
13979
+ process.stderr.write(`Error: ${inputArg2}: ${err.message}
14040
13980
  `);
14041
13981
  }
14042
13982
  }