@zenuml/core 4.2.1 → 4.3.1

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.
@@ -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;
@@ -9823,6 +9723,7 @@ const PARTICIPANT_ICON_WIDTH = 40;
9823
9723
  const LIFELINE_WIDTH = 1;
9824
9724
  const PARTICIPANT_TOP_SPACE_FOR_GROUP = 20;
9825
9725
  const OCCURRENCE_EMPTY_HEIGHT = 24;
9726
+ const DIAGRAM_HEADER_HEIGHT = 24 + 4 * 2 + 1;
9826
9727
 
9827
9728
  var TextType = /* @__PURE__ */ ((TextType2) => {
9828
9729
  TextType2[TextType2["MessageContent"] = 0] = "MessageContent";
@@ -9956,39 +9857,6 @@ function OrderedParticipants(rootContext) {
9956
9857
  return result;
9957
9858
  }
9958
9859
 
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
9860
  function Dual(p, v) {
9993
9861
  return { position: p, velocity: v };
9994
9862
  }
@@ -10129,7 +9997,6 @@ class Coordinates {
10129
9997
  }
10130
9998
  const leftGap = this.getParticipantGap(this.participantModels[0]);
10131
9999
  const position = leftGap + this.positionsCache[pIndex];
10132
- rootLogger.debug(`Position of ${participantName} is ${position}`);
10133
10000
  return position;
10134
10001
  }
10135
10002
  walkThrough() {
@@ -10176,17 +10043,11 @@ class Coordinates {
10176
10043
  }
10177
10044
  const leftIndex = Math.min(indexFrom, indexTo);
10178
10045
  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
- }
10046
+ const messageWidth = this.getMessageWidth(message);
10047
+ this.m[leftIndex][rightIndex] = Math.max(
10048
+ messageWidth + ARROW_HEAD_WIDTH + OCCURRENCE_WIDTH,
10049
+ this.m[leftIndex][rightIndex]
10050
+ );
10190
10051
  }
10191
10052
  }
10192
10053
  withParticipantGaps(participantModels) {
@@ -10216,9 +10077,6 @@ class Coordinates {
10216
10077
  );
10217
10078
  const participantWidth = Math.max(labelWidth + iconWidth + emojiWidth, MIN_PARTICIPANT_WIDTH) + MARGIN;
10218
10079
  this.participantWidthCache.set(participant.name, participantWidth);
10219
- rootLogger.debug(
10220
- `Width of ${participant.name} is ${participantWidth}; labelWidth: ${labelWidth}`
10221
- );
10222
10080
  return participantWidth;
10223
10081
  }
10224
10082
  }
@@ -10247,6 +10105,24 @@ class NodeVM {
10247
10105
  }
10248
10106
  }
10249
10107
 
10108
+ const SPACING_UNIT = 4;
10109
+ const tw = (value) => value * SPACING_UNIT;
10110
+ const DEFAULT_LAYOUT_METRICS = {
10111
+ statementMarginY: tw(4),
10112
+ // .statement-container .my-4
10113
+ fragmentHeaderHeight: 25,
10114
+ // .fragment .leading-4 => line-height: 1rem
10115
+ fragmentPaddingBottom: 10
10116
+ // .zenuml .fragment =>padding-bottom: 10px
10117
+ };
10118
+ const MESSAGE_HEIGHT = 16;
10119
+ const SELF_MESSAGE_HEIGHT = 30;
10120
+ const ASYNC_SELF_MESSAGE_HEIGHT = 44;
10121
+ const CREATION_MESSAGE_HEIGHT = 40;
10122
+ const COMMENT_LINE_HEIGHT = tw(5);
10123
+ const FRAGMENT_BORDER_WIDTH = 1;
10124
+ const FRAGMENT_GUARD_HEIGHT = 28;
10125
+
10250
10126
  function getLocalParticipantNames(ctx) {
10251
10127
  if (!ctx) return [];
10252
10128
  return [ctx.Origin() || _STARTER_, ...Participants(ctx).Names()];
@@ -10677,7 +10553,7 @@ class MarkdownMeasurer {
10677
10553
  return 0;
10678
10554
  }
10679
10555
  const lines = _text.trim().split("\n");
10680
- return lines.length * 20;
10556
+ return lines.length * COMMENT_LINE_HEIGHT;
10681
10557
  }
10682
10558
  }
10683
10559
 
@@ -10689,57 +10565,12 @@ class StatementVM extends NodeVM {
10689
10565
  if (!context?.getComment || !context.getComment()) return 0;
10690
10566
  return new MarkdownMeasurer().measure(context.getComment());
10691
10567
  }
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
10568
  findLeftParticipant(ctx, fallbackOrigin) {
10702
10569
  if (!ctx) return void 0;
10703
10570
  const local = getLocalParticipantNames(ctx) || [];
10704
10571
  const ordered = this.runtime.participants;
10705
10572
  return ordered.find((name) => local.includes(name)) || local[0] || fallbackOrigin || void 0;
10706
10573
  }
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
10574
  }
10744
10575
 
10745
10576
  class AsyncMessageStatementVM extends StatementVM {
@@ -10754,7 +10585,7 @@ class AsyncMessageStatementVM extends StatementVM {
10754
10585
  const source = asyncContext?.From?.() || asyncContext?.ProvidedFrom?.() || asyncContext?.Origin?.() || origin;
10755
10586
  const target = asyncContext?.Owner?.() || asyncContext?.to?.()?.getFormattedText?.() || source;
10756
10587
  const isSelf = source === target;
10757
- const messageHeight = isSelf ? 44 : 16;
10588
+ const messageHeight = isSelf ? ASYNC_SELF_MESSAGE_HEIGHT : MESSAGE_HEIGHT;
10758
10589
  return {
10759
10590
  top,
10760
10591
  height: commentHeight + messageHeight,
@@ -10763,7 +10594,6 @@ class AsyncMessageStatementVM extends StatementVM {
10763
10594
  }
10764
10595
  }
10765
10596
 
10766
- const CREATION_MESSAGE_HEIGHT = 40;
10767
10597
  class CreationStatementVM extends StatementVM {
10768
10598
  constructor(statement, creation, runtime) {
10769
10599
  super(statement, runtime);
@@ -10787,7 +10617,6 @@ class CreationStatementVM extends StatementVM {
10787
10617
  }
10788
10618
  const assignment = this.creation?.Assignment?.();
10789
10619
  if (assignment) {
10790
- rootLogger.info(`creation::assignment::${participant}`);
10791
10620
  cursor += 12;
10792
10621
  }
10793
10622
  const height = cursor - top;
@@ -10813,26 +10642,7 @@ class EmptyStatementVM extends StatementVM {
10813
10642
  }
10814
10643
  }
10815
10644
 
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 {
10645
+ class FragmentAltVM extends StatementVM {
10836
10646
  constructor(statement, alt, runtime) {
10837
10647
  super(statement, runtime);
10838
10648
  this.alt = alt;
@@ -10844,7 +10654,7 @@ class FragmentAltVM extends FragmentVM {
10844
10654
  let cursor = top + 1 + this.metrics.fragmentHeaderHeight + commentHeight;
10845
10655
  const ifBlock = this.alt?.ifBlock?.();
10846
10656
  if (ifBlock) {
10847
- cursor += 20;
10657
+ cursor += FRAGMENT_GUARD_HEIGHT;
10848
10658
  cursor = this.layoutBlock(
10849
10659
  ifBlock.braceBlock()?.block(),
10850
10660
  leftParticipant,
@@ -10852,7 +10662,7 @@ class FragmentAltVM extends FragmentVM {
10852
10662
  );
10853
10663
  }
10854
10664
  this.alt?.elseIfBlock?.()?.forEach((block) => {
10855
- cursor += 20;
10665
+ cursor += FRAGMENT_GUARD_HEIGHT;
10856
10666
  cursor += 8;
10857
10667
  cursor += 1;
10858
10668
  cursor = this.layoutBlock(
@@ -10863,7 +10673,7 @@ class FragmentAltVM extends FragmentVM {
10863
10673
  });
10864
10674
  const elseBlock = this.alt?.elseBlock?.()?.braceBlock?.()?.block?.();
10865
10675
  if (elseBlock) {
10866
- cursor += 20;
10676
+ cursor += FRAGMENT_GUARD_HEIGHT;
10867
10677
  cursor += 8;
10868
10678
  cursor += 1;
10869
10679
  cursor = this.layoutBlock(elseBlock, leftParticipant, cursor);
@@ -10877,7 +10687,7 @@ class FragmentAltVM extends FragmentVM {
10877
10687
  }
10878
10688
  }
10879
10689
 
10880
- class FragmentRefVM extends FragmentVM {
10690
+ class FragmentRefVM extends StatementVM {
10881
10691
  kind = "ref";
10882
10692
  measure(top) {
10883
10693
  const context = this.context?.ref?.() || this.context;
@@ -10888,7 +10698,7 @@ class FragmentRefVM extends FragmentVM {
10888
10698
  }
10889
10699
  }
10890
10700
 
10891
- class FragmentSingleBlockVM extends FragmentVM {
10701
+ class FragmentSingleBlockVM extends StatementVM {
10892
10702
  constructor(statement, fragment, runtime, kind) {
10893
10703
  super(statement, runtime);
10894
10704
  this.fragment = fragment;
@@ -10896,15 +10706,15 @@ class FragmentSingleBlockVM extends FragmentVM {
10896
10706
  }
10897
10707
  measure(top, origin) {
10898
10708
  const commentHeight = this.measureComment(this.fragment);
10899
- let cursor = top + 1 + this.metrics.fragmentHeaderHeight + commentHeight;
10709
+ let cursor = top + FRAGMENT_BORDER_WIDTH + this.metrics.fragmentHeaderHeight + commentHeight;
10900
10710
  const hasCondition = Boolean(this.fragment?.parExpr?.()?.condition?.());
10901
- if (hasCondition) cursor += 20;
10711
+ if (hasCondition) cursor += FRAGMENT_GUARD_HEIGHT;
10902
10712
  const block = this.fragment?.braceBlock?.()?.block?.();
10903
10713
  if (block) {
10904
10714
  const fragmentOrigin = this.findLeftParticipant(this.fragment, origin) || origin;
10905
10715
  cursor = this.layoutBlock(block, fragmentOrigin, cursor, this.kind);
10906
10716
  }
10907
- cursor += this.metrics.fragmentPaddingBottom + 1;
10717
+ cursor += this.metrics.fragmentPaddingBottom + FRAGMENT_BORDER_WIDTH;
10908
10718
  return {
10909
10719
  top,
10910
10720
  height: cursor - top,
@@ -10913,7 +10723,7 @@ class FragmentSingleBlockVM extends FragmentVM {
10913
10723
  }
10914
10724
  }
10915
10725
 
10916
- class FragmentTryCatchVM extends FragmentVM {
10726
+ class FragmentTryCatchVM extends StatementVM {
10917
10727
  constructor(statement, tcf, runtime) {
10918
10728
  super(statement, runtime);
10919
10729
  this.tcf = tcf;
@@ -10992,6 +10802,14 @@ class ReturnStatementVM extends StatementVM {
10992
10802
  }
10993
10803
  }
10994
10804
 
10805
+ const SINGLE_BLOCK_FRAGMENT_KINDS = [
10806
+ "loop",
10807
+ "opt",
10808
+ "par",
10809
+ "section",
10810
+ "critical"
10811
+ ];
10812
+
10995
10813
  class SyncMessageStatementVM extends StatementVM {
10996
10814
  constructor(statement, message, runtime) {
10997
10815
  super(statement, runtime);
@@ -11006,7 +10824,7 @@ class SyncMessageStatementVM extends StatementVM {
11006
10824
  const block = this.message.braceBlock?.()?.block?.();
11007
10825
  const assignee = this.message.Assignment?.();
11008
10826
  const isSelf = source === target;
11009
- const messageHeight = isSelf ? 30 : 16;
10827
+ const messageHeight = isSelf ? SELF_MESSAGE_HEIGHT : MESSAGE_HEIGHT;
11010
10828
  cursor += messageHeight;
11011
10829
  if (block) {
11012
10830
  const fragmentOrigin = this.findLeftParticipant(this.message, origin) || origin;
@@ -11042,7 +10860,7 @@ const createStatementVM = (statement, runtime) => {
11042
10860
  if (statement.divider?.()) {
11043
10861
  return new DividerStatementVM(statement, runtime);
11044
10862
  }
11045
- for (const kind of ["loop", "opt", "par", "section", "critical"]) {
10863
+ for (const kind of SINGLE_BLOCK_FRAGMENT_KINDS) {
11046
10864
  const fragment = statement[kind]?.();
11047
10865
  if (fragment) {
11048
10866
  return new FragmentSingleBlockVM(statement, fragment, runtime, kind);
@@ -11083,17 +10901,6 @@ class BlockVM extends NodeVM {
11083
10901
  }
11084
10902
  }
11085
10903
 
11086
- const SPACING_UNIT = 4;
11087
- const tw = (value) => value * SPACING_UNIT;
11088
- const DEFAULT_LAYOUT_METRICS = {
11089
- statementMarginY: tw(4),
11090
- // .statement-container .my-4
11091
- fragmentHeaderHeight: 25,
11092
- // .fragment .leading-4 => line-height: 1rem
11093
- fragmentPaddingBottom: 10
11094
- // .zenuml .fragment =>padding-bottom: 10px
11095
- };
11096
-
11097
10904
  class VerticalCoordinates {
11098
10905
  statementMap = /* @__PURE__ */ new Map();
11099
10906
  creationTops = /* @__PURE__ */ new Map();
@@ -11105,16 +10912,13 @@ class VerticalCoordinates {
11105
10912
  const originParticipant = messages.length === 0 ? _STARTER_ : messages[0].from || _STARTER_;
11106
10913
  const runtime = {
11107
10914
  metrics: DEFAULT_LAYOUT_METRICS,
11108
- rootBlock,
11109
10915
  participants,
11110
- originParticipant,
11111
10916
  recordCoordinate: (statement, coordinate) => {
11112
10917
  const key = createStatementKey(statement);
11113
10918
  this.statementMap.set(key, coordinate);
11114
10919
  },
11115
10920
  updateCreationTop: (participant, top) => {
11116
10921
  const paddingTop = top - 8;
11117
- rootLogger.info(`[VerticalCoordinates] updateCreationTop participant="${participant}" raw=${top} paddingTop=${paddingTop}`);
11118
10922
  this.creationTops.set(participant, paddingTop);
11119
10923
  }
11120
10924
  };
@@ -11127,19 +10931,13 @@ class VerticalCoordinates {
11127
10931
  getStatementCoordinate(key) {
11128
10932
  return this.statementMap.get(key);
11129
10933
  }
11130
- getStatementCoordinateFor(statement) {
11131
- return this.statementMap.get(createStatementKey(statement));
11132
- }
11133
- entries() {
11134
- return Array.from(this.statementMap.entries());
11135
- }
11136
10934
  getTotalHeight() {
11137
10935
  return this.totalHeight;
11138
10936
  }
11139
10937
  }
11140
10938
 
11141
10939
  let dic = {};
11142
- const persistDic = {};
10940
+ let persistDic = {};
11143
10941
  const getCache = (key) => {
11144
10942
  if (key != null) {
11145
10943
  const cacheValue = dic[key] ?? persistDic[key];
@@ -11153,78 +10951,92 @@ const setCache = (key, value, persist = false) => {
11153
10951
  persistDic[key] = value;
11154
10952
  }
11155
10953
  };
10954
+ const clearPersistentCache = () => {
10955
+ persistDic = {};
10956
+ dic = {};
10957
+ };
11156
10958
 
11157
10959
  const FONT_FAMILY = "Helvetica, Verdana, serif";
11158
- const FONT_SIZE_PARTICIPANT = "16px";
10960
+ const FONT_SIZE_PARTICIPANT = "14px";
10961
+ const FONT_SIZE_MESSAGE = "15px";
11159
10962
  const FONT_SIZE_FRAGMENT = "14px";
11160
- function getFontSpec() {
11161
- return `${FONT_SIZE_PARTICIPANT} ${FONT_FAMILY}`;
10963
+ function getFontSize(type) {
10964
+ return type === TextType.MessageContent ? FONT_SIZE_MESSAGE : FONT_SIZE_PARTICIPANT;
10965
+ }
10966
+ function getFontSpec(type) {
10967
+ return `${getFontSize(type)} ${FONT_FAMILY}`;
11162
10968
  }
11163
10969
  let canvasCtx = null;
11164
10970
  function setCanvasContext(ctx) {
10971
+ if (ctx === canvasCtx) return;
11165
10972
  canvasCtx = ctx;
10973
+ clearPersistentCache();
11166
10974
  }
11167
10975
  function getCanvasContext() {
11168
10976
  if (canvasCtx) return canvasCtx;
11169
10977
  try {
11170
10978
  if (typeof OffscreenCanvas !== "undefined") {
11171
10979
  canvasCtx = new OffscreenCanvas(1, 1).getContext("2d");
11172
- rootLogger.debug("[ZenUML] WidthProviderOnCanvas: using OffscreenCanvas");
11173
10980
  } else if (typeof document !== "undefined") {
11174
10981
  canvasCtx = document.createElement("canvas").getContext("2d");
11175
- rootLogger.debug("[ZenUML] WidthProviderOnCanvas: using <canvas> element");
11176
- } else {
11177
- rootLogger.debug("[ZenUML] WidthProviderOnCanvas: no canvas available, using character estimate fallback");
11178
10982
  }
11179
10983
  } catch {
11180
- rootLogger.debug("[ZenUML] WidthProviderOnCanvas: canvas creation failed, using character estimate fallback");
11181
10984
  }
11182
10985
  return canvasCtx;
11183
10986
  }
11184
10987
  function WidthProviderOnCanvas(text, type) {
11185
10988
  const measured = text.trim();
11186
- const cacheKey = `WidthProviderOnCanvas_${measured}_${type}`;
10989
+ const cacheKey = `WidthProviderOnCanvas_${getFontSpec(type)}_${measured}_${type}`;
11187
10990
  const cacheValue = getCache(cacheKey);
11188
10991
  if (cacheValue != null) {
11189
10992
  return cacheValue;
11190
10993
  }
11191
10994
  const ctx = getCanvasContext();
11192
10995
  if (!ctx) {
11193
- const width2 = Math.ceil(measured.length * 16 * 0.6);
11194
- setCache(cacheKey, width2, true);
10996
+ const width2 = Math.ceil(
10997
+ measured.length * Number.parseFloat(getFontSize(type)) * 0.6
10998
+ );
10999
+ setCache(cacheKey, width2);
11195
11000
  return width2;
11196
11001
  }
11197
- ctx.font = getFontSpec();
11002
+ ctx.font = getFontSpec(type);
11003
+ ctx.fontVariantCaps = "normal";
11198
11004
  const width = Math.round(ctx.measureText(measured).width);
11199
11005
  setCache(cacheKey, width, true);
11200
11006
  return width;
11201
11007
  }
11202
- const EMOJI_PATTERN = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}]/u;
11203
- function measureWithSvg(text, fontSize) {
11008
+ const EMOJI_PATTERN = (
11009
+ // eslint-disable-next-line no-misleading-character-class
11010
+ /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}]/u
11011
+ );
11012
+ function measureWithSvg(text, fontSize, caps = "normal") {
11204
11013
  if (typeof document === "undefined") return null;
11205
11014
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
11206
11015
  svg.setAttribute("style", "position:absolute;left:-9999px;top:-9999px");
11207
11016
  const textEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
11208
11017
  textEl.setAttribute("font-family", FONT_FAMILY);
11209
11018
  textEl.setAttribute("font-size", fontSize);
11019
+ textEl.style.fontVariantCaps = caps;
11020
+ if (typeof textEl.getBBox !== "function") return null;
11210
11021
  textEl.textContent = text;
11211
11022
  svg.appendChild(textEl);
11212
11023
  document.body.appendChild(svg);
11213
11024
  const width = textEl.getBBox().width;
11214
11025
  document.body.removeChild(svg);
11215
- return width;
11026
+ return Number.isFinite(width) && width > 0 ? width : null;
11216
11027
  }
11217
- function measureTextWithFont(text, fontSize) {
11028
+ function measureTextWithFont(text, fontSize, caps = "normal") {
11218
11029
  const measured = text.trim();
11219
11030
  if (!measured) return 0;
11220
11031
  const hasEmoji = EMOJI_PATTERN.test(measured);
11221
- const cacheKey = hasEmoji ? `measureTextWithFont_svg_${fontSize}_${measured}` : `measureTextWithFont_${fontSize}_${measured}`;
11032
+ const font = `${fontSize} ${FONT_FAMILY}`;
11033
+ const cacheKey = (hasEmoji ? `measureTextWithFont_svg_${font}_${measured}` : `measureTextWithFont_${font}_${measured}`) + `_${caps}`;
11222
11034
  const cacheValue = getCache(cacheKey);
11223
11035
  if (cacheValue != null) {
11224
11036
  return cacheValue;
11225
11037
  }
11226
- if (hasEmoji) {
11227
- const svgWidth = measureWithSvg(measured, fontSize);
11038
+ if (hasEmoji || caps !== "normal") {
11039
+ const svgWidth = measureWithSvg(measured, fontSize, caps);
11228
11040
  if (svgWidth != null) {
11229
11041
  setCache(cacheKey, svgWidth, true);
11230
11042
  return svgWidth;
@@ -11234,11 +11046,19 @@ function measureTextWithFont(text, fontSize) {
11234
11046
  if (!ctx) {
11235
11047
  const px = Number.parseFloat(fontSize) || 14;
11236
11048
  const width2 = Math.ceil(measured.length * px * 0.6);
11237
- setCache(cacheKey, width2, true);
11049
+ setCache(cacheKey, width2);
11238
11050
  return width2;
11239
11051
  }
11240
- ctx.font = `${fontSize} ${FONT_FAMILY}`;
11241
- const width = ctx.measureText(measured).width;
11052
+ ctx.font = font;
11053
+ ctx.fontVariantCaps = "normal";
11054
+ const normalWidth = ctx.measureText(measured).width;
11055
+ let width = normalWidth;
11056
+ if (caps !== "normal") {
11057
+ const normalProbe = ctx.measureText("iiii").width;
11058
+ ctx.fontVariantCaps = caps;
11059
+ width = Math.abs(ctx.measureText("iiii").width - normalProbe) > 0.01 ? ctx.measureText(measured).width : Math.max(normalWidth, ctx.measureText(measured.toUpperCase()).width);
11060
+ ctx.fontVariantCaps = "normal";
11061
+ }
11242
11062
  setCache(cacheKey, width, true);
11243
11063
  return width;
11244
11064
  }
@@ -11249,6 +11069,11 @@ function measureSvgParticipantLabelWidth(text) {
11249
11069
  return measureTextWithFont(text, FONT_SIZE_PARTICIPANT);
11250
11070
  }
11251
11071
 
11072
+ const fragmentGuardKeyword = (kind, sourceKeyword) => kind === "alt" || kind === "opt" ? "if" : kind === "loop" ? sourceKeyword ?? "loop" : void 0;
11073
+ const fragmentGuardWidth = (condition, keyword) => Math.ceil(
11074
+ 10 + measureTextWithFont(resolveEmojiInText(condition), "14px") + (keyword ? measureTextWithFont(keyword, "12px", "all-small-caps") + (condition ? 4 : 0) : 0)
11075
+ );
11076
+
11252
11077
  const walker = antlr4.tree.ParseTreeWalker.DEFAULT;
11253
11078
  class FrameBuilder extends sequenceParserListener {
11254
11079
  _orderedParticipants;
@@ -11358,22 +11183,59 @@ function FrameBorder(frame) {
11358
11183
  return { left: 0, right: 0 };
11359
11184
  }
11360
11185
  return {
11361
- type: frame.type,
11362
11186
  left: FRAGMENT_PADDING_X * longestPath(frame, "LEFT" /* LEFT */),
11363
11187
  right: FRAGMENT_PADDING_X * longestPath(frame, "RIGHT" /* RIGHT */)
11364
11188
  };
11365
11189
  }
11366
11190
 
11191
+ const fragmentHeaderLabel = (kind) => {
11192
+ const labels = {
11193
+ alt: "Alt",
11194
+ loop: "Loop",
11195
+ opt: "Opt",
11196
+ par: "Par",
11197
+ critical: "Critical",
11198
+ section: "Section",
11199
+ tcf: "Try",
11200
+ ref: "Ref"
11201
+ };
11202
+ return labels[kind] ?? kind.charAt(0).toUpperCase() + kind.slice(1);
11203
+ };
11204
+ const fragmentHeaderWidth = (label, number) => Math.ceil(
11205
+ 32 + measureTextWithFont(label, "14px", "all-small-caps") * 1.1 + (number ? measureTextWithFont(number, "12px") + 15 : 0)
11206
+ );
11207
+
11208
+ function TotalWidth(ctx, coordinates) {
11209
+ const allParticipants = coordinates.orderedParticipantNames();
11210
+ if (!ctx || allParticipants.length === 0) return 0;
11211
+ const localParticipants = getLocalParticipantNames(ctx);
11212
+ const leftParticipant = allParticipants.find((p) => localParticipants.includes(p)) || "";
11213
+ const rightParticipant = allParticipants.slice().reverse().find((p) => localParticipants.includes(p)) || "";
11214
+ const frameBuilder = new FrameBuilder(allParticipants);
11215
+ const frame = frameBuilder.getFrame(ctx);
11216
+ const border = FrameBorder(frame);
11217
+ const extraWidth = extraWidthDueToSelfMessage(
11218
+ ctx,
11219
+ rightParticipant,
11220
+ coordinates
11221
+ );
11222
+ const participantWidth = coordinates.distance(leftParticipant, rightParticipant) + coordinates.half(leftParticipant) + coordinates.half(rightParticipant);
11223
+ return Math.max(participantWidth, FRAGMENT_MIN_WIDTH) + border.left + border.right + extraWidth;
11224
+ }
11225
+ function extraWidthDueToSelfMessage(ctx, rightParticipant, coordinates) {
11226
+ const allMessages = AllMessages(ctx);
11227
+ const widths = allMessages.filter((m) => m.from === m.to).map(
11228
+ (m) => coordinates.getMessageWidth(m) - coordinates.distance(m.from || _STARTER_, rightParticipant) - coordinates.half(rightParticipant)
11229
+ );
11230
+ return Math.max.apply(null, [0, ...widths]);
11231
+ }
11232
+
11367
11233
  const PARTICIPANT_TOP_SPACE = PARTICIPANT_TOP_SPACE_FOR_GROUP + 8;
11368
11234
  const PARTICIPANT_BOX_PADDING = 16;
11369
- const PARTICIPANT_BOX_PADDING_ASSIGNEE = 16;
11370
11235
  const PARTICIPANT_ICON_ROW_WIDTH = 28;
11371
11236
  const PARTICIPANT_EMOJI_WIDTH = 20;
11372
11237
  const PARTICIPANT_VISUAL_HEIGHT = 40;
11373
11238
  const PARTICIPANT_MAX_WIDTH = 250;
11374
- function snapX$1(x) {
11375
- return x;
11376
- }
11377
11239
  function cssToSvgStyle(css) {
11378
11240
  const result = {};
11379
11241
  let hasKeys = false;
@@ -11392,24 +11254,32 @@ function cssToSvgStyle(css) {
11392
11254
 
11393
11255
  function buildParticipants(models, coordinates, verticalCoordinates, measureText) {
11394
11256
  return models.map((m) => {
11395
- const centerX = snapX$1(coordinates.getPosition(m.name));
11257
+ const centerX = coordinates.getPosition(m.name);
11396
11258
  const halfWidth = coordinates.half(m.name);
11397
- const isAssignee = m.name.includes(":") && m.getDisplayName() === m.name;
11398
11259
  let width;
11399
11260
  let measuredTextWidth;
11400
11261
  let measuredStereotypeWidth;
11401
11262
  if (measureText && m.name !== _STARTER_) {
11402
11263
  const textWidth = measureSvgParticipantLabelWidth(m.getDisplayName());
11403
11264
  measuredTextWidth = textWidth;
11404
- const padding = isAssignee ? PARTICIPANT_BOX_PADDING_ASSIGNEE : PARTICIPANT_BOX_PADDING;
11265
+ const padding = PARTICIPANT_BOX_PADDING;
11405
11266
  const iconWidth = m.hasIcon() ? PARTICIPANT_ICON_ROW_WIDTH : 0;
11406
11267
  const emojiWidth = m.emoji ? PARTICIPANT_EMOJI_WIDTH : 0;
11407
11268
  const STEREOTYPE_BOX_OVERHEAD = 8;
11408
11269
  if (m.stereotype) {
11409
- measuredStereotypeWidth = measureSvgParticipantLabelWidth(`«${m.stereotype}»`);
11270
+ measuredStereotypeWidth = measureSvgParticipantLabelWidth(
11271
+ `«${m.stereotype}»`
11272
+ );
11410
11273
  }
11411
11274
  const stereotypeBoxWidth = measuredStereotypeWidth != null ? measuredStereotypeWidth + STEREOTYPE_BOX_OVERHEAD : 0;
11412
- width = Math.min(Math.max(textWidth + padding + iconWidth + emojiWidth, stereotypeBoxWidth, MIN_PARTICIPANT_WIDTH), PARTICIPANT_MAX_WIDTH);
11275
+ width = Math.min(
11276
+ Math.max(
11277
+ textWidth + padding + iconWidth + emojiWidth,
11278
+ stereotypeBoxWidth,
11279
+ MIN_PARTICIPANT_WIDTH
11280
+ ),
11281
+ PARTICIPANT_MAX_WIDTH
11282
+ );
11413
11283
  } else {
11414
11284
  width = Math.min(halfWidth * 2 - MARGIN, PARTICIPANT_MAX_WIDTH);
11415
11285
  }
@@ -11488,39 +11358,49 @@ function buildGroups(participants, diagramHeight) {
11488
11358
  return groups;
11489
11359
  }
11490
11360
 
11491
- function snapX(x) {
11492
- return x;
11361
+ class Anchor2 {
11362
+ constructor(position, layers) {
11363
+ this.position = position;
11364
+ this.layers = layers;
11365
+ }
11366
+ /**
11367
+ * centerToEdge is used for translateX calculations.
11368
+ */
11369
+ centerToEdge(other) {
11370
+ return other.rightEdgeOfRightWall() - this.centerOfRightWall();
11371
+ }
11372
+ centerToCenter(other) {
11373
+ return other.centerOfRightWall() - this.centerOfRightWall();
11374
+ }
11375
+ /**
11376
+ * edgeOffset is used for interactionWidth calculations.
11377
+ */
11378
+ edgeOffset(other) {
11379
+ const isRightToLeft = other.position < this.position;
11380
+ const rightEdgeOfLeftAnchor = isRightToLeft ? other.rightEdgeOfRightWall() : this.rightEdgeOfRightWall();
11381
+ const leftEdgeOfRightAnchor = isRightToLeft ? this.leftEdgeOfRightWall() : other.leftEdgeOfRightWall();
11382
+ const distance = leftEdgeOfRightAnchor - rightEdgeOfLeftAnchor - LIFELINE_WIDTH;
11383
+ return isRightToLeft ? distance * -1 : distance;
11384
+ }
11385
+ centerOfRightWall() {
11386
+ return this.layers <= 1 ? this.position : this.position + OCCURRENCE_BAR_SIDE_WIDTH * (this.layers - 1);
11387
+ }
11388
+ rightEdgeOfRightWall() {
11389
+ return this.position + OCCURRENCE_BAR_SIDE_WIDTH * this.layers;
11390
+ }
11391
+ leftEdgeOfRightWall() {
11392
+ return this.layers === 0 ? this.position : this.centerOfRightWall() - OCCURRENCE_BAR_SIDE_WIDTH;
11393
+ }
11493
11394
  }
11395
+
11494
11396
  const COMMENT_FONT_ASCENT = 15;
11495
11397
  function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, allParticipants, _measureText, commentHeight = 0, commentText, commentStyle, commentYBase) {
11496
11398
  const statNode = info.statNode;
11497
- if (!statNode) return null;
11498
11399
  const fragmentCtx = findFragmentContext(statNode);
11499
- if (!fragmentCtx) {
11500
- return {
11501
- fragment: {
11502
- kind: info.fragmentKind,
11503
- label: info.fragmentLabel || "",
11504
- labelWidth: info.fragmentLabel ? measureSvgFragmentLabelWidth(resolveEmojiInText(info.fragmentLabel)) : void 0,
11505
- x: 0,
11506
- y: coord.top,
11507
- width: coordinates.getWidth(),
11508
- height: coord.height,
11509
- headerY: coord.top + 1 + commentHeight,
11510
- sections: [],
11511
- number: info.number,
11512
- depth: info.depth
11513
- },
11514
- comment: commentText && commentYBase != null ? {
11515
- x: 1,
11516
- y: commentYBase + 1 + COMMENT_FONT_ASCENT,
11517
- text: commentText,
11518
- style: commentStyle,
11519
- fragmentComment: true
11520
- } : void 0
11521
- };
11522
- }
11523
- const localNames = getDeepParticipantNames(fragmentCtx, info.fragmentSections);
11400
+ const localNames = getDeepParticipantNames(
11401
+ fragmentCtx,
11402
+ info.fragmentSections
11403
+ );
11524
11404
  const leftParticipant = allParticipants.find((p) => localNames.includes(p)) || "";
11525
11405
  const rightParticipant = allParticipants.slice().reverse().find((p) => localNames.includes(p)) || "";
11526
11406
  const frameBuilder = new FrameBuilder(allParticipants);
@@ -11529,14 +11409,8 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11529
11409
  let fragWidth;
11530
11410
  let fragX;
11531
11411
  if (leftParticipant && rightParticipant) {
11532
- const participantWidth = coordinates.distance(leftParticipant, rightParticipant) + coordinates.half(leftParticipant) + coordinates.half(rightParticipant);
11533
- const selfMessages = AllMessages(statNode).filter((m) => m.from === m.to);
11534
- const extraWidths = selfMessages.map(
11535
- (m) => coordinates.getMessageWidth(m) - coordinates.distance(m.from || _STARTER_, rightParticipant) - coordinates.half(rightParticipant)
11536
- );
11537
- const extraWidth = Math.max(0, ...extraWidths);
11538
- fragWidth = Math.max(participantWidth, FRAGMENT_MIN_WIDTH) + fragBorder.left + fragBorder.right + extraWidth;
11539
- fragX = snapX(coordinates.getPosition(leftParticipant)) - coordinates.half(leftParticipant);
11412
+ fragWidth = TotalWidth(statNode, coordinates);
11413
+ fragX = coordinates.getPosition(leftParticipant) - coordinates.half(leftParticipant);
11540
11414
  } else {
11541
11415
  fragWidth = Math.max(FRAGMENT_MIN_WIDTH, coordinates.getWidth());
11542
11416
  fragX = 0;
@@ -11551,11 +11425,12 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11551
11425
  if (sectionBlock) {
11552
11426
  const innerStats = sectionBlock.stat?.() || [];
11553
11427
  if (innerStats.length > 0) {
11554
- const firstStatKey = createStatementKeyFromStat(innerStats[0]);
11428
+ const firstStatKey = createStatementKey(innerStats[0]);
11555
11429
  if (firstStatKey) {
11556
11430
  const innerCoord = verticalCoordinates.getStatementCoordinate(firstStatKey);
11557
11431
  if (innerCoord) {
11558
- sectionY = innerCoord.top - 20 - 8 - 8 - 1;
11432
+ const labelHeight = info.fragmentKind === "alt" ? FRAGMENT_GUARD_HEIGHT : 20;
11433
+ sectionY = innerCoord.top - labelHeight - 8 - 8 - 1;
11559
11434
  }
11560
11435
  }
11561
11436
  }
@@ -11564,11 +11439,16 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11564
11439
  const sectionHeight = i < info.fragmentSections.length - 1 ? 0 : coord.top + coord.height - sectionY;
11565
11440
  sections.push({
11566
11441
  label: section.label,
11442
+ guardKeyword: section.guardKeyword,
11567
11443
  y: sectionY,
11568
11444
  height: sectionHeight,
11569
11445
  labelWidth: section.label ? measureSvgFragmentLabelWidth(resolveEmojiInText(section.label)) : void 0,
11570
- innerLabel: /^\[\s*.*\s*\]$/.test(section.label) ? section.label.slice(1, -1).trim() : void 0,
11571
- innerLabelWidth: /^\[\s*.*\s*\]$/.test(section.label) ? measureSvgFragmentLabelWidth(resolveEmojiInText(section.label.slice(1, -1).trim())) : void 0,
11446
+ innerLabel: section.condition ?? (/^\[\s*.*\s*\]$/.test(section.label) ? section.label.slice(1, -1).trim() : void 0),
11447
+ innerLabelWidth: section.condition !== void 0 ? measureSvgFragmentLabelWidth(
11448
+ resolveEmojiInText(section.condition)
11449
+ ) : /^\[\s*.*\s*\]$/.test(section.label) ? measureSvgFragmentLabelWidth(
11450
+ resolveEmojiInText(section.label.slice(1, -1).trim())
11451
+ ) : void 0,
11572
11452
  keyword: (() => {
11573
11453
  const spaceIdx = section.label.indexOf(" ");
11574
11454
  if (spaceIdx > 0 && !section.label.startsWith("finally") && !section.label.startsWith("[")) {
@@ -11579,7 +11459,9 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11579
11459
  keywordWidth: (() => {
11580
11460
  const spaceIdx = section.label.indexOf(" ");
11581
11461
  if (spaceIdx > 0 && !section.label.startsWith("finally") && !section.label.startsWith("[")) {
11582
- return measureSvgFragmentLabelWidth(section.label.substring(0, spaceIdx));
11462
+ return measureSvgFragmentLabelWidth(
11463
+ section.label.substring(0, spaceIdx)
11464
+ );
11583
11465
  }
11584
11466
  return void 0;
11585
11467
  })(),
@@ -11593,7 +11475,9 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11593
11475
  detailWidth: (() => {
11594
11476
  const spaceIdx = section.label.indexOf(" ");
11595
11477
  if (spaceIdx > 0 && !section.label.startsWith("finally") && !section.label.startsWith("[")) {
11596
- return measureSvgFragmentLabelWidth(section.label.substring(spaceIdx + 1));
11478
+ return measureSvgFragmentLabelWidth(
11479
+ section.label.substring(spaceIdx + 1)
11480
+ );
11597
11481
  }
11598
11482
  return void 0;
11599
11483
  })()
@@ -11606,7 +11490,7 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11606
11490
  if (innerStats.length > 1) {
11607
11491
  sections.push({ label: "", y: coord.top, height: 0 });
11608
11492
  for (let i = 1; i < innerStats.length; i++) {
11609
- const statKey = createStatementKeyFromStat(innerStats[i]);
11493
+ const statKey = createStatementKey(innerStats[i]);
11610
11494
  if (!statKey) continue;
11611
11495
  const innerCoord = verticalCoordinates.getStatementCoordinate(statKey);
11612
11496
  if (!innerCoord) continue;
@@ -11633,13 +11517,17 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11633
11517
  return {
11634
11518
  fragment: {
11635
11519
  kind: info.fragmentKind,
11520
+ headerLabel: info.fragmentHeaderLabel,
11636
11521
  label: info.fragmentLabel || "",
11522
+ guardKeyword: info.guardKeyword,
11637
11523
  labelWidth: info.fragmentLabel ? measureSvgFragmentLabelWidth(resolveEmojiInText(info.fragmentLabel)) : void 0,
11638
11524
  x: fragX,
11639
11525
  y: coord.top,
11640
11526
  width: fragWidth,
11641
11527
  height: coord.height,
11642
- headerY: coord.top + 1 + commentHeight,
11528
+ // The header sits just inside the fragment's 1px top border, below any
11529
+ // inline comment — the same offset FragmentSingleBlockVM.measure() applies.
11530
+ headerY: coord.top + FRAGMENT_BORDER_WIDTH + commentHeight,
11643
11531
  sections,
11644
11532
  number: info.number,
11645
11533
  depth: info.depth
@@ -11648,7 +11536,7 @@ function buildFragmentGeometry(info, coord, coordinates, verticalCoordinates, al
11648
11536
  };
11649
11537
  }
11650
11538
  function findFragmentContext(stat) {
11651
- for (const kind of ["loop", "opt", "par", "critical", "section"]) {
11539
+ for (const kind of SINGLE_BLOCK_FRAGMENT_KINDS) {
11652
11540
  const frag = stat[kind]?.();
11653
11541
  if (frag) return frag;
11654
11542
  }
@@ -11660,10 +11548,6 @@ function findFragmentContext(stat) {
11660
11548
  if (ref) return ref;
11661
11549
  return null;
11662
11550
  }
11663
- function createStatementKeyFromStat(statement) {
11664
- if (!statement?.start || !statement?.stop) return "";
11665
- return `${statement.start.start}-${statement.stop.stop}`;
11666
- }
11667
11551
  function getDeepParticipantNames(fragmentCtx, sections) {
11668
11552
  const names = new Set(getLocalParticipantNames(fragmentCtx));
11669
11553
  if (sections) {
@@ -11701,11 +11585,37 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11701
11585
  const to = message.Owner?.() || _STARTER_;
11702
11586
  const label = normalizeLabel(message.SignatureText?.() || "");
11703
11587
  const nestedBlock = message.braceBlock?.()?.block?.();
11704
- 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 });
11588
+ results.push({
11589
+ key,
11590
+ kind: "sync",
11591
+ from,
11592
+ to,
11593
+ label,
11594
+ isSelf: from === to,
11595
+ hasBlock: !!nestedBlock,
11596
+ comment,
11597
+ statNode: stat,
11598
+ senderOccurrenceDepth: activeOccurrences.get(from) || 0,
11599
+ targetHasOccurrence: activeOccurrences.has(to),
11600
+ targetOccurrenceDepth: activeOccurrences.get(to) || 0,
11601
+ number,
11602
+ depth,
11603
+ parentBlockKind
11604
+ });
11705
11605
  if (nestedBlock) {
11706
11606
  const innerOccs = new Map(activeOccurrences);
11707
11607
  innerOccs.set(to, (innerOccs.get(to) || 0) + 1);
11708
- results.push(...walkBlock(nestedBlock, to, innerOccs, number, depth + 1, "sync", 0));
11608
+ results.push(
11609
+ ...walkBlock(
11610
+ nestedBlock,
11611
+ to,
11612
+ innerOccs,
11613
+ number,
11614
+ depth + 1,
11615
+ "sync",
11616
+ 0
11617
+ )
11618
+ );
11709
11619
  }
11710
11620
  continue;
11711
11621
  }
@@ -11713,8 +11623,23 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11713
11623
  if (asyncMsg) {
11714
11624
  const from = asyncMsg.From?.() || asyncMsg.ProvidedFrom?.() || asyncMsg.Origin?.() || currentOrigin;
11715
11625
  const to = asyncMsg.Owner?.() || asyncMsg.to?.()?.getFormattedText?.() || from;
11716
- const label = normalizeLabel(asyncMsg.content?.()?.getText?.() || asyncMsg.SignatureText?.() || "");
11717
- 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 });
11626
+ const label = normalizeLabel(
11627
+ asyncMsg.content?.()?.getText?.() || asyncMsg.SignatureText?.() || ""
11628
+ );
11629
+ results.push({
11630
+ key,
11631
+ kind: "async",
11632
+ from,
11633
+ to,
11634
+ label,
11635
+ isSelf: from === to,
11636
+ hasBlock: false,
11637
+ comment,
11638
+ senderOccurrenceDepth: activeOccurrences.get(from) || 0,
11639
+ targetHasOccurrence: activeOccurrences.has(to),
11640
+ number,
11641
+ depth
11642
+ });
11718
11643
  continue;
11719
11644
  }
11720
11645
  const creation = stat.creation?.();
@@ -11723,11 +11648,36 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11723
11648
  const to = creation.Owner?.() || "";
11724
11649
  const label = normalizeLabel(creation.SignatureText?.() || "«create»");
11725
11650
  const creationBlock = creation.braceBlock?.()?.block?.();
11726
- 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 });
11651
+ results.push({
11652
+ key,
11653
+ kind: "creation",
11654
+ from,
11655
+ to,
11656
+ label,
11657
+ isSelf: false,
11658
+ hasBlock: !!creationBlock,
11659
+ comment,
11660
+ statNode: stat,
11661
+ senderOccurrenceDepth: activeOccurrences.get(from) || 0,
11662
+ targetHasOccurrence: activeOccurrences.has(to),
11663
+ number,
11664
+ depth,
11665
+ parentBlockKind
11666
+ });
11727
11667
  if (creationBlock) {
11728
11668
  const innerOccs = new Map(activeOccurrences);
11729
11669
  innerOccs.set(to, (innerOccs.get(to) || 0) + 1);
11730
- results.push(...walkBlock(creationBlock, to || currentOrigin, innerOccs, number, depth + 1, "creation", 0));
11670
+ results.push(
11671
+ ...walkBlock(
11672
+ creationBlock,
11673
+ to || currentOrigin,
11674
+ innerOccs,
11675
+ number,
11676
+ depth + 1,
11677
+ "creation",
11678
+ 0
11679
+ )
11680
+ );
11731
11681
  }
11732
11682
  continue;
11733
11683
  }
@@ -11737,13 +11687,40 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11737
11687
  const asyncMessage = ret?.asyncMessage?.();
11738
11688
  const from = asyncMessage?.From?.() || ret?.From?.() || currentOrigin;
11739
11689
  const to = asyncMessage?.to?.()?.getFormattedText?.() || ret?.ReturnTo?.() || _STARTER_;
11740
- 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 });
11690
+ results.push({
11691
+ key,
11692
+ kind: "return",
11693
+ from,
11694
+ to,
11695
+ label,
11696
+ isSelf: from === to,
11697
+ hasBlock: false,
11698
+ comment,
11699
+ senderOccurrenceDepth: activeOccurrences.get(from) || 0,
11700
+ targetOccurrenceDepth: activeOccurrences.get(to) || 0,
11701
+ number,
11702
+ depth,
11703
+ parentBlockKind
11704
+ });
11741
11705
  continue;
11742
11706
  }
11743
11707
  const divider = stat.divider?.();
11744
11708
  if (divider) {
11745
- const label = normalizeLabel(divider.getFormattedText?.() || divider.getText?.() || "");
11746
- results.push({ key, kind: "divider", from: "", to: "", label, isSelf: false, hasBlock: false, senderOccurrenceDepth: 0, number, depth });
11709
+ const label = normalizeLabel(
11710
+ divider.getFormattedText?.() || divider.getText?.() || ""
11711
+ );
11712
+ results.push({
11713
+ key,
11714
+ kind: "divider",
11715
+ from: "",
11716
+ to: "",
11717
+ label,
11718
+ isSelf: false,
11719
+ hasBlock: false,
11720
+ senderOccurrenceDepth: 0,
11721
+ number,
11722
+ depth
11723
+ });
11747
11724
  continue;
11748
11725
  }
11749
11726
  const fragmentInfo = extractFragmentInfo(stat);
@@ -11758,6 +11735,8 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11758
11735
  hasBlock: false,
11759
11736
  fragmentKind: fragmentInfo.fragmentKind,
11760
11737
  fragmentLabel: fragmentInfo.label,
11738
+ fragmentHeaderLabel: fragmentInfo.headerLabel,
11739
+ guardKeyword: fragmentInfo.guardKeyword,
11761
11740
  fragmentSections: fragmentInfo.sections,
11762
11741
  comment,
11763
11742
  statNode: stat,
@@ -11765,19 +11744,28 @@ function walkBlock(block, currentOrigin, activeOccurrences, parentNumber, depth,
11765
11744
  number,
11766
11745
  depth
11767
11746
  });
11768
- walkFragmentBlocks(stat, currentOrigin, results, activeOccurrences, number, depth);
11747
+ walkFragmentBlocks(
11748
+ stat,
11749
+ currentOrigin,
11750
+ results,
11751
+ activeOccurrences,
11752
+ number,
11753
+ depth
11754
+ );
11769
11755
  }
11770
11756
  return results;
11771
11757
  }
11772
11758
  function extractFragmentInfo(stat) {
11773
- for (const kind of ["loop", "opt", "par", "critical", "section"]) {
11759
+ for (const kind of SINGLE_BLOCK_FRAGMENT_KINDS) {
11774
11760
  const frag = stat[kind]?.();
11775
11761
  if (frag) {
11776
11762
  const condition = frag.parExpr?.()?.condition?.();
11777
11763
  const label = condition?.getFormattedText?.() || "";
11778
11764
  return {
11779
11765
  fragmentKind: kind,
11766
+ headerLabel: frag.atom?.()?.getFormattedText?.(),
11780
11767
  label,
11768
+ guardKeyword: kind === "loop" ? frag.WHILE?.()?.getText?.() : void 0,
11781
11769
  sections: [{ label, blockNode: frag.braceBlock?.()?.block?.() }]
11782
11770
  };
11783
11771
  }
@@ -11787,16 +11775,30 @@ function extractFragmentInfo(stat) {
11787
11775
  const sections = [];
11788
11776
  const ifBlock = alt.ifBlock?.();
11789
11777
  if (ifBlock) {
11790
- sections.push({ label: "Alt", blockNode: ifBlock.braceBlock?.()?.block?.() });
11778
+ sections.push({
11779
+ label: "Alt",
11780
+ guardKeyword: "if",
11781
+ condition: ifBlock.parExpr?.()?.condition?.()?.getFormattedText?.() || "",
11782
+ blockNode: ifBlock.braceBlock?.()?.block?.()
11783
+ });
11791
11784
  }
11792
11785
  for (const elseIf of alt.elseIfBlock?.() || []) {
11793
11786
  const condition = elseIf.parExpr?.()?.condition?.();
11794
11787
  const label = condition?.getFormattedText?.() || "";
11795
- sections.push({ label: `[ ${label} ]`, blockNode: elseIf.braceBlock?.()?.block?.() });
11788
+ sections.push({
11789
+ label: `else if ${label}`,
11790
+ guardKeyword: "else if",
11791
+ condition: label,
11792
+ blockNode: elseIf.braceBlock?.()?.block?.()
11793
+ });
11796
11794
  }
11797
11795
  const elseBlock = alt.elseBlock?.();
11798
11796
  if (elseBlock) {
11799
- sections.push({ label: "[else]", blockNode: elseBlock.braceBlock?.()?.block?.() });
11797
+ sections.push({
11798
+ label: "else",
11799
+ guardKeyword: "else",
11800
+ blockNode: elseBlock.braceBlock?.()?.block?.()
11801
+ });
11800
11802
  }
11801
11803
  const ifCondition = alt.ifBlock?.()?.parExpr?.()?.condition?.();
11802
11804
  const firstLabel = ifCondition?.getFormattedText?.() || "";
@@ -11807,15 +11809,24 @@ function extractFragmentInfo(stat) {
11807
11809
  const sections = [];
11808
11810
  const tryBlock = tcf.tryBlock?.();
11809
11811
  if (tryBlock) {
11810
- sections.push({ label: "Try", blockNode: tryBlock.braceBlock?.()?.block?.() });
11812
+ sections.push({
11813
+ label: "Try",
11814
+ blockNode: tryBlock.braceBlock?.()?.block?.()
11815
+ });
11811
11816
  }
11812
11817
  for (const catchBlock of tcf.catchBlock?.() || []) {
11813
11818
  const exception = catchBlock.invocation?.()?.parameters?.()?.getFormattedText?.() || "";
11814
- sections.push({ label: `catch ${exception}`, blockNode: catchBlock.braceBlock?.()?.block?.() });
11819
+ sections.push({
11820
+ label: `catch ${exception}`,
11821
+ blockNode: catchBlock.braceBlock?.()?.block?.()
11822
+ });
11815
11823
  }
11816
11824
  const finallyBlock = tcf.finallyBlock?.();
11817
11825
  if (finallyBlock) {
11818
- sections.push({ label: "finally", blockNode: finallyBlock.braceBlock?.()?.block?.() });
11826
+ sections.push({
11827
+ label: "finally",
11828
+ blockNode: finallyBlock.braceBlock?.()?.block?.()
11829
+ });
11819
11830
  }
11820
11831
  return { fragmentKind: "tcf", label: "", sections };
11821
11832
  }
@@ -11827,14 +11838,27 @@ function extractFragmentInfo(stat) {
11827
11838
  return null;
11828
11839
  }
11829
11840
  function blockLength(block) {
11830
- return (block?.stat?.() || []).filter((stat) => !!createStatementKey(stat)).length;
11841
+ return (block?.stat?.() || []).filter(
11842
+ (stat) => !!createStatementKey(stat)
11843
+ ).length;
11831
11844
  }
11832
11845
  function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumber, depth) {
11833
- for (const kind of ["loop", "opt", "par", "critical", "section"]) {
11846
+ for (const kind of SINGLE_BLOCK_FRAGMENT_KINDS) {
11834
11847
  const frag = stat[kind]?.();
11835
11848
  if (frag) {
11836
11849
  const block = frag.braceBlock?.()?.block?.();
11837
- if (block) results.push(...walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, 0));
11850
+ if (block)
11851
+ results.push(
11852
+ ...walkBlock(
11853
+ block,
11854
+ origin,
11855
+ activeOccurrences,
11856
+ parentNumber,
11857
+ depth + 1,
11858
+ null,
11859
+ 0
11860
+ )
11861
+ );
11838
11862
  return;
11839
11863
  }
11840
11864
  }
@@ -11845,14 +11869,32 @@ function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumb
11845
11869
  if (ifBlock) {
11846
11870
  const block = ifBlock.braceBlock?.()?.block?.();
11847
11871
  if (block) {
11848
- results.push(...walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset));
11872
+ results.push(
11873
+ ...walkBlock(
11874
+ block,
11875
+ origin,
11876
+ activeOccurrences,
11877
+ parentNumber,
11878
+ depth + 1,
11879
+ null,
11880
+ sectionOffset
11881
+ )
11882
+ );
11849
11883
  sectionOffset += blockLength(block);
11850
11884
  }
11851
11885
  }
11852
11886
  for (const elseIf of alt.elseIfBlock?.() || []) {
11853
11887
  const block = elseIf.braceBlock?.()?.block?.();
11854
11888
  if (block) {
11855
- const sectionStmts = walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset);
11889
+ const sectionStmts = walkBlock(
11890
+ block,
11891
+ origin,
11892
+ activeOccurrences,
11893
+ parentNumber,
11894
+ depth + 1,
11895
+ null,
11896
+ sectionOffset
11897
+ );
11856
11898
  if (sectionStmts.length > 0) sectionStmts[0].sectionReset = true;
11857
11899
  results.push(...sectionStmts);
11858
11900
  sectionOffset += blockLength(block);
@@ -11862,7 +11904,15 @@ function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumb
11862
11904
  if (elseBlock) {
11863
11905
  const block = elseBlock.braceBlock?.()?.block?.();
11864
11906
  if (block) {
11865
- const sectionStmts = walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset);
11907
+ const sectionStmts = walkBlock(
11908
+ block,
11909
+ origin,
11910
+ activeOccurrences,
11911
+ parentNumber,
11912
+ depth + 1,
11913
+ null,
11914
+ sectionOffset
11915
+ );
11866
11916
  if (sectionStmts.length > 0) sectionStmts[0].sectionReset = true;
11867
11917
  results.push(...sectionStmts);
11868
11918
  }
@@ -11876,14 +11926,32 @@ function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumb
11876
11926
  if (tryBlock) {
11877
11927
  const block = tryBlock.braceBlock?.()?.block?.();
11878
11928
  if (block) {
11879
- results.push(...walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset));
11929
+ results.push(
11930
+ ...walkBlock(
11931
+ block,
11932
+ origin,
11933
+ activeOccurrences,
11934
+ parentNumber,
11935
+ depth + 1,
11936
+ null,
11937
+ sectionOffset
11938
+ )
11939
+ );
11880
11940
  sectionOffset += blockLength(block);
11881
11941
  }
11882
11942
  }
11883
11943
  for (const catchBlock of tcf.catchBlock?.() || []) {
11884
11944
  const block = catchBlock.braceBlock?.()?.block?.();
11885
11945
  if (block) {
11886
- const sectionStmts = walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset);
11946
+ const sectionStmts = walkBlock(
11947
+ block,
11948
+ origin,
11949
+ activeOccurrences,
11950
+ parentNumber,
11951
+ depth + 1,
11952
+ null,
11953
+ sectionOffset
11954
+ );
11887
11955
  if (sectionStmts.length > 0) sectionStmts[0].sectionReset = true;
11888
11956
  results.push(...sectionStmts);
11889
11957
  sectionOffset += blockLength(block);
@@ -11893,7 +11961,15 @@ function walkFragmentBlocks(stat, origin, results, activeOccurrences, parentNumb
11893
11961
  if (finallyBlock) {
11894
11962
  const block = finallyBlock.braceBlock?.()?.block?.();
11895
11963
  if (block) {
11896
- const sectionStmts = walkBlock(block, origin, activeOccurrences, parentNumber, depth + 1, null, sectionOffset);
11964
+ const sectionStmts = walkBlock(
11965
+ block,
11966
+ origin,
11967
+ activeOccurrences,
11968
+ parentNumber,
11969
+ depth + 1,
11970
+ null,
11971
+ sectionOffset
11972
+ );
11897
11973
  if (sectionStmts.length > 0) sectionStmts[0].sectionReset = true;
11898
11974
  results.push(...sectionStmts);
11899
11975
  }
@@ -11925,10 +12001,6 @@ function computeReturnDebt(statements, verticalCoordinates, returnHeight) {
11925
12001
  if (ownerKey && occInnerDebt > 0) {
11926
12002
  result.set(`inner:${ownerKey}`, occInnerDebt);
11927
12003
  }
11928
- const hasMixed = hasNonReturnChild[maxDepth] || false;
11929
- if (ownerKey && hasMixed) {
11930
- result.set(`mixed:${ownerKey}`, 1);
11931
- }
11932
12004
  const hasAssign = blockHasAssignment[maxDepth] || false;
11933
12005
  debtByDepth.pop();
11934
12006
  directDebtByDepth.pop();
@@ -12012,10 +12084,6 @@ function computeReturnDebt(statements, verticalCoordinates, returnHeight) {
12012
12084
  if (ownerKey && occInnerDebtEnd > 0) {
12013
12085
  result.set(`inner:${ownerKey}`, occInnerDebtEnd);
12014
12086
  }
12015
- const hasMixedEnd = hasNonReturnChild[maxDepth] || false;
12016
- if (ownerKey && hasMixedEnd) {
12017
- result.set(`mixed:${ownerKey}`, 1);
12018
- }
12019
12087
  const hasAssignEnd = blockHasAssignment[maxDepth] || false;
12020
12088
  debtByDepth.pop();
12021
12089
  directDebtByDepth.pop();
@@ -12031,7 +12099,6 @@ function computeReturnDebt(statements, verticalCoordinates, returnHeight) {
12031
12099
  }
12032
12100
  }
12033
12101
  }
12034
- result.set("__totalDebt__", debtByDepth[0] || 0);
12035
12102
  return result;
12036
12103
  }
12037
12104
 
@@ -12059,7 +12126,11 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12059
12126
  }
12060
12127
  }
12061
12128
  const RETURN_VISUAL_HEIGHT = 16;
12062
- const adjustMap = computeReturnDebt(statements, verticalCoordinates, RETURN_VISUAL_HEIGHT);
12129
+ const adjustMap = computeReturnDebt(
12130
+ statements,
12131
+ verticalCoordinates,
12132
+ RETURN_VISUAL_HEIGHT
12133
+ );
12063
12134
  for (const info of statements) {
12064
12135
  const coord = verticalCoordinates.getStatementCoordinate(info.key);
12065
12136
  if (!coord) continue;
@@ -12075,7 +12146,7 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12075
12146
  const tX = coordinates.getPosition(info.to);
12076
12147
  if (fX > tX) commentParticipant = info.to;
12077
12148
  }
12078
- const commentX = commentParticipant ? snapX$1(coordinates.getPosition(commentParticipant)) : 10;
12149
+ const commentX = commentParticipant ? coordinates.getPosition(commentParticipant) : 10;
12079
12150
  const COMMENT_FONT_ASCENT = 15;
12080
12151
  const commentXOffset = info.senderOccurrenceDepth >= 1 ? OCCURRENCE_BAR_SIDE_WIDTH + 1 : 1;
12081
12152
  const codeSpanPadding = commentObj.text.trimStart().startsWith("`") ? 2 : 0;
@@ -12088,30 +12159,31 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12088
12159
  }
12089
12160
  const messageStyle = commentObj ? cssToSvgStyle(commentObj.messageStyle) : void 0;
12090
12161
  if (info.kind === "sync" || info.kind === "async") {
12091
- let fromX = snapX$1(coordinates.getPosition(info.from));
12092
- const toX = snapX$1(coordinates.getPosition(info.to));
12093
- const messageHeight = info.isSelf ? 30 : 16;
12094
- const msgCommentHeight = commentObj?.text ? (info.comment?.trim().split("\n").length || 0) * 20 : 0;
12162
+ let fromX = coordinates.getPosition(info.from);
12163
+ const toX = coordinates.getPosition(info.to);
12164
+ const isAsync = info.kind === "async";
12165
+ const messageHeight = !info.isSelf ? MESSAGE_HEIGHT : isAsync ? ASYNC_SELF_MESSAGE_HEIGHT : SELF_MESSAGE_HEIGHT;
12166
+ const msgCommentHeight = commentObj?.text ? (info.comment?.trim().split("\n").length || 0) * COMMENT_LINE_HEIGHT : 0;
12095
12167
  const messageY = coord.top + adjust + msgCommentHeight + messageHeight - 0.5;
12096
12168
  if (info.senderOccurrenceDepth >= 1 && !info.isSelf) {
12097
- const depth = info.senderOccurrenceDepth;
12098
- const occOffset = depth * OCCURRENCE_BAR_SIDE_WIDTH;
12169
+ const senderAnchor = new Anchor2(fromX, info.senderOccurrenceDepth);
12099
12170
  const isLTR = fromX < toX;
12100
- fromX = isLTR ? fromX + occOffset : fromX - OCCURRENCE_BAR_SIDE_WIDTH + (depth - 1) * OCCURRENCE_BAR_SIDE_WIDTH;
12171
+ fromX = isLTR ? senderAnchor.rightEdgeOfRightWall() : senderAnchor.leftEdgeOfRightWall();
12101
12172
  }
12102
12173
  const targetDepth = info.targetOccurrenceDepth || (info.targetHasOccurrence ? 1 : 0);
12103
12174
  const nestingOffset = targetDepth * OCCURRENCE_BAR_SIDE_WIDTH;
12104
12175
  if (info.isSelf) {
12105
- const isAsync = info.kind === "async";
12106
12176
  const selfYOffset = 0;
12107
12177
  const selfWidth = isAsync ? 28 : OCCURRENCE_WIDTH;
12108
- const selfHeight = isAsync ? 44 : messageHeight;
12109
- const selfX = info.senderOccurrenceDepth >= 1 ? fromX + OCCURRENCE_BAR_SIDE_WIDTH + (info.senderOccurrenceDepth - 1) * OCCURRENCE_BAR_SIDE_WIDTH : fromX;
12178
+ const selfX = new Anchor2(
12179
+ fromX,
12180
+ info.senderOccurrenceDepth
12181
+ ).rightEdgeOfRightWall();
12110
12182
  selfCalls.push({
12111
12183
  x: selfX,
12112
12184
  y: coord.top + selfYOffset + msgCommentHeight,
12113
12185
  width: selfWidth,
12114
- height: selfHeight,
12186
+ height: messageHeight,
12115
12187
  label: info.label,
12116
12188
  arrowStyle: isAsync ? "open" : "solid",
12117
12189
  number: info.number,
@@ -12183,16 +12255,15 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12183
12255
  continue;
12184
12256
  }
12185
12257
  if (info.kind === "creation") {
12186
- const CREATION_MSG_HEIGHT = 40;
12187
- let fromX = snapX$1(coordinates.getPosition(info.from));
12188
- const toX = snapX$1(coordinates.getPosition(info.to));
12258
+ let fromX = coordinates.getPosition(info.from);
12259
+ const toX = coordinates.getPosition(info.to);
12189
12260
  if (info.senderOccurrenceDepth >= 1) {
12190
12261
  const occOffset = OCCURRENCE_BAR_SIDE_WIDTH + (info.senderOccurrenceDepth - 1) * OCCURRENCE_BAR_SIDE_WIDTH;
12191
12262
  const isLTR = fromX < toX;
12192
12263
  fromX = isLTR ? fromX + occOffset : fromX - occOffset;
12193
12264
  }
12194
12265
  const targetParticipant = participants.find((p) => p.name === info.to);
12195
- const messageY = targetParticipant ? targetParticipant.y + PARTICIPANT_VISUAL_HEIGHT / 2 : coord.top + CREATION_MSG_HEIGHT / 2;
12266
+ const messageY = targetParticipant ? targetParticipant.y + PARTICIPANT_VISUAL_HEIGHT / 2 : coord.top + CREATION_MESSAGE_HEIGHT / 2;
12196
12267
  if (targetParticipant) {
12197
12268
  creations.push({
12198
12269
  participant: targetParticipant,
@@ -12211,7 +12282,7 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12211
12282
  });
12212
12283
  }
12213
12284
  const occX = toX - OCCURRENCE_BAR_SIDE_WIDTH;
12214
- const occY = targetParticipant ? targetParticipant.y + PARTICIPANT_VISUAL_HEIGHT - 2 : coord.top + CREATION_MSG_HEIGHT - 2;
12285
+ const occY = targetParticipant ? targetParticipant.y + PARTICIPANT_VISUAL_HEIGHT - 2 : coord.top + CREATION_MESSAGE_HEIGHT - 2;
12215
12286
  const occHeight = Math.max(
12216
12287
  coord.top + coord.height - occY,
12217
12288
  OCCURRENCE_EMPTY_HEIGHT
@@ -12228,7 +12299,7 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12228
12299
  const creationAssign = creationCtx?.Assignment?.();
12229
12300
  if (creationAssign?.assignee) {
12230
12301
  const occBottom = occY + occHeight - 2;
12231
- const rawFromX = snapX$1(coordinates.getPosition(info.from));
12302
+ const rawFromX = coordinates.getPosition(info.from);
12232
12303
  const isLTR = rawFromX < toX;
12233
12304
  const occHalf = OCCURRENCE_BAR_SIDE_WIDTH;
12234
12305
  const senderNest = Math.max(info.senderOccurrenceDepth - 1, 0) * OCCURRENCE_BAR_SIDE_WIDTH;
@@ -12250,7 +12321,7 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12250
12321
  }
12251
12322
  if (info.kind === "fragment" && info.fragmentKind) {
12252
12323
  const adjustedCoord = { top: coord.top + adjust, height: coord.height };
12253
- const fragmentCommentHeight = commentObj?.text ? (info.comment?.trim().split("\n").length || 0) * 20 : 0;
12324
+ const fragmentCommentHeight = commentObj?.text ? (info.comment?.trim().split("\n").length || 0) * COMMENT_LINE_HEIGHT : 0;
12254
12325
  const fragmentResult = buildFragmentGeometry(
12255
12326
  info,
12256
12327
  adjustedCoord,
@@ -12272,26 +12343,14 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12272
12343
  continue;
12273
12344
  }
12274
12345
  if (info.kind === "return") {
12275
- const rawFromX = snapX$1(coordinates.getPosition(info.from));
12276
- const rawToX = snapX$1(coordinates.getPosition(info.to));
12346
+ const rawFromX = coordinates.getPosition(info.from);
12347
+ const rawToX = coordinates.getPosition(info.to);
12277
12348
  const isReverse = rawToX < rawFromX;
12278
- const fromLayers = info.senderOccurrenceDepth;
12279
- let fromX;
12280
- if (isReverse) {
12281
- fromX = fromLayers === 0 ? rawFromX : (fromLayers <= 1 ? rawFromX : rawFromX + OCCURRENCE_BAR_SIDE_WIDTH * (fromLayers - 1)) - OCCURRENCE_BAR_SIDE_WIDTH;
12282
- } else {
12283
- fromX = rawFromX + OCCURRENCE_BAR_SIDE_WIDTH * fromLayers + 1;
12284
- }
12285
- const toLayers = info.targetOccurrenceDepth || 0;
12286
- let toX;
12287
- if (isReverse) {
12288
- toX = rawToX + OCCURRENCE_BAR_SIDE_WIDTH * toLayers;
12289
- } else {
12290
- toX = toLayers === 0 ? rawToX : (toLayers <= 1 ? rawToX : rawToX + OCCURRENCE_BAR_SIDE_WIDTH * (toLayers - 1)) - OCCURRENCE_BAR_SIDE_WIDTH;
12291
- }
12292
- if (isReverse) {
12293
- toX += LIFELINE_WIDTH;
12294
- }
12349
+ const fromAnchor = new Anchor2(rawFromX, info.senderOccurrenceDepth);
12350
+ const toAnchor = new Anchor2(rawToX, info.targetOccurrenceDepth || 0);
12351
+ const RETURN_STROKE_OFFSET = 1;
12352
+ const fromX = isReverse ? fromAnchor.leftEdgeOfRightWall() : fromAnchor.rightEdgeOfRightWall() + RETURN_STROKE_OFFSET;
12353
+ const toX = isReverse ? toAnchor.rightEdgeOfRightWall() + LIFELINE_WIDTH : toAnchor.leftEdgeOfRightWall();
12295
12354
  const returnOffset = coord.height === 0 ? 16.5 : 15.5;
12296
12355
  const returnY = coord.top + adjust + returnOffset;
12297
12356
  returns.push({
@@ -12313,7 +12372,9 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12313
12372
  y: coord.top + adjust + coord.height / 2,
12314
12373
  width: diagramWidth,
12315
12374
  label: info.label,
12316
- labelWidth: measureSvgFragmentLabelWidth(resolveEmojiInText(cleanLabel))
12375
+ labelWidth: measureSvgFragmentLabelWidth(
12376
+ resolveEmojiInText(cleanLabel)
12377
+ )
12317
12378
  });
12318
12379
  continue;
12319
12380
  }
@@ -12336,11 +12397,23 @@ function buildMessages(rootContext, coordinates, verticalCoordinates, participan
12336
12397
  }
12337
12398
  }
12338
12399
  if (depth > 0) {
12339
- occurrences[i] = { ...inner, x: inner.x + depth * OCCURRENCE_BAR_SIDE_WIDTH };
12400
+ occurrences[i] = {
12401
+ ...inner,
12402
+ x: inner.x + depth * OCCURRENCE_BAR_SIDE_WIDTH
12403
+ };
12340
12404
  }
12341
12405
  }
12342
- const totalReturnDebt = adjustMap.get("__totalDebt__") || 0;
12343
- return { messages, selfCalls, occurrences, creations, fragments, returns, dividers, comments, totalReturnDebt, maxReturnBottom };
12406
+ return {
12407
+ messages,
12408
+ selfCalls,
12409
+ occurrences,
12410
+ creations,
12411
+ fragments,
12412
+ returns,
12413
+ dividers,
12414
+ comments,
12415
+ maxReturnBottom
12416
+ };
12344
12417
  }
12345
12418
 
12346
12419
  function buildGeometry(input) {
@@ -12364,13 +12437,36 @@ function buildGeometry(input) {
12364
12437
  participants,
12365
12438
  measureText
12366
12439
  );
12367
- const { messages, selfCalls, occurrences, creations, fragments, returns, dividers, comments } = buildResult;
12440
+ const {
12441
+ messages,
12442
+ selfCalls,
12443
+ occurrences,
12444
+ creations,
12445
+ fragments,
12446
+ returns,
12447
+ dividers,
12448
+ comments
12449
+ } = buildResult;
12450
+ if (input.enableNumbering === false) {
12451
+ const numberedItems = [
12452
+ ...messages,
12453
+ ...selfCalls,
12454
+ ...fragments,
12455
+ ...returns,
12456
+ ...creations.map((creation) => creation.message)
12457
+ ];
12458
+ for (const numbered of numberedItems) {
12459
+ numbered.number = void 0;
12460
+ }
12461
+ }
12368
12462
  let maxOccBottom = 0;
12369
12463
  let maxOtherY = 0;
12370
- for (const o of occurrences) maxOccBottom = Math.max(maxOccBottom, o.y + o.height);
12464
+ for (const o of occurrences)
12465
+ maxOccBottom = Math.max(maxOccBottom, o.y + o.height);
12371
12466
  for (const m of messages) maxOtherY = Math.max(maxOtherY, m.y);
12372
12467
  for (const s of selfCalls) maxOtherY = Math.max(maxOtherY, s.y + s.height);
12373
- for (const c of creations) maxOtherY = Math.max(maxOtherY, c.message.y + PARTICIPANT_VISUAL_HEIGHT);
12468
+ for (const c of creations)
12469
+ maxOtherY = Math.max(maxOtherY, c.message.y + PARTICIPANT_VISUAL_HEIGHT);
12374
12470
  for (const f of fragments) maxOtherY = Math.max(maxOtherY, f.y + f.height);
12375
12471
  for (const d of dividers) maxOtherY = Math.max(maxOtherY, d.y);
12376
12472
  const diagramHeight = Math.max(
@@ -12380,21 +12476,8 @@ function buildGeometry(input) {
12380
12476
  buildResult.maxReturnBottom
12381
12477
  );
12382
12478
  const lifelineBottom = diagramHeight + PARTICIPANT_VISUAL_HEIGHT - 28;
12383
- const lifelines = buildLifelines(
12384
- participants,
12385
- lifelineBottom
12386
- );
12387
- const localParticipants = getLocalParticipantNames(rootContext);
12388
- const orderedNames = coordinates.orderedParticipantNames();
12389
- const leftParticipant = orderedNames.find((p) => localParticipants.includes(p)) || "";
12390
- const rightParticipant = orderedNames.slice().reverse().find((p) => localParticipants.includes(p)) || "";
12391
- const participantWidth = coordinates.distance(leftParticipant, rightParticipant) + coordinates.half(leftParticipant) + coordinates.half(rightParticipant);
12392
- const selfMessages = AllMessages(rootContext).filter((m) => m.from === m.to);
12393
- const extraWidths = selfMessages.map(
12394
- (m) => coordinates.getMessageWidth(m) - coordinates.distance(m.from || _STARTER_, rightParticipant) - coordinates.half(rightParticipant)
12395
- );
12396
- const extraWidthDueToSelfMessage = Math.max(0, ...extraWidths);
12397
- let diagramWidth = Math.max(participantWidth, FRAGMENT_MIN_WIDTH) + extraWidthDueToSelfMessage;
12479
+ const lifelines = buildLifelines(participants, lifelineBottom);
12480
+ let diagramWidth = TotalWidth(rootContext, coordinates) - frameBorder.left - frameBorder.right;
12398
12481
  if (measureText) {
12399
12482
  const LABEL_PAD = 10;
12400
12483
  for (const msg of messages) {
@@ -12445,6 +12528,42 @@ function buildGeometry(input) {
12445
12528
  f.width = targetRight - f.x;
12446
12529
  }
12447
12530
  }
12531
+ for (const inner of [...fragments].sort((a, b) => a.height - b.height)) {
12532
+ inner.width = Math.max(
12533
+ inner.width,
12534
+ fragmentHeaderWidth(
12535
+ inner.headerLabel ?? fragmentHeaderLabel(inner.kind),
12536
+ inner.number
12537
+ )
12538
+ );
12539
+ if (inner.label)
12540
+ inner.width = Math.max(
12541
+ inner.width,
12542
+ fragmentGuardWidth(
12543
+ inner.label,
12544
+ fragmentGuardKeyword(inner.kind, inner.guardKeyword)
12545
+ )
12546
+ );
12547
+ for (const section of inner.sections) {
12548
+ if (section.guardKeyword)
12549
+ inner.width = Math.max(
12550
+ inner.width,
12551
+ fragmentGuardWidth(section.innerLabel ?? "", section.guardKeyword)
12552
+ );
12553
+ }
12554
+ for (const outer of fragments) {
12555
+ if (outer.y < inner.y && outer.y + outer.height >= inner.y + inner.height && outer.x <= inner.x) {
12556
+ outer.width = Math.max(
12557
+ outer.width,
12558
+ inner.x + inner.width + FRAGMENT_PADDING_X - outer.x
12559
+ );
12560
+ }
12561
+ }
12562
+ diagramWidth = Math.max(
12563
+ diagramWidth,
12564
+ inner.x + inner.width - frameBorder.right
12565
+ );
12566
+ }
12448
12567
  const groups = buildGroups(participants, diagramHeight);
12449
12568
  return {
12450
12569
  width: diagramWidth,
@@ -12543,7 +12662,7 @@ function styleToAttr(style) {
12543
12662
  return Object.entries(style).map(([k, v]) => `${esc(k)}: ${esc(v)}`).join("; ");
12544
12663
  }
12545
12664
 
12546
- const STROKE_WIDTH$1 = 2;
12665
+ const STROKE_WIDTH$1 = 1;
12547
12666
  const HALF_STROKE$1 = STROKE_WIDTH$1 / 2;
12548
12667
  const ICON_SIZE = 24;
12549
12668
  const ICON_MARGIN_RIGHT = 4;
@@ -12551,12 +12670,12 @@ const ICON_PAINT_OFFSET_X = 4;
12551
12670
  const LABEL_PAD_LEFT = 8;
12552
12671
  const LABEL_HORIZONTAL_PADDING = 16;
12553
12672
  const STEREOTYPE_VERTICAL_OFFSET = 8;
12554
- const STEREOTYPE_FONT_SIZE = 16;
12673
+ const STEREOTYPE_FONT_SIZE = 14;
12555
12674
  const BOUNDARY_ICON_VERTICAL_TWEAK = 2.75;
12556
12675
  const PARTICIPANT_TEXT_FILL = "#222";
12557
12676
  function renderParticipant(p) {
12558
12677
  if (p.isStarter) return renderStarterParticipant(p);
12559
- const rx = 3;
12678
+ const rx = 4 - HALF_STROKE$1;
12560
12679
  const x = p.x - p.width / 2 + HALF_STROKE$1;
12561
12680
  const rectY = p.y + HALF_STROKE$1;
12562
12681
  const rectW = p.width - STROKE_WIDTH$1;
@@ -12625,7 +12744,7 @@ function renderStarterParticipant(p) {
12625
12744
  if (!icon) {
12626
12745
  return "";
12627
12746
  }
12628
- const rx = 3;
12747
+ const rx = 4 - HALF_STROKE$1;
12629
12748
  const boxX = p.x - p.width / 2 + HALF_STROKE$1;
12630
12749
  const rectY = p.y + HALF_STROKE$1;
12631
12750
  const rectW = p.width - STROKE_WIDTH$1;
@@ -12665,21 +12784,31 @@ function renderLifeline(l) {
12665
12784
  return `<line x1="${x}" y1="${l.topY}" x2="${x}" y2="${l.bottomY}" class="lifeline" stroke-dasharray="${l.dashed ? "5,5" : "none"}" stroke-dashoffset="${l.dashed ? "5" : "0"}" shape-rendering="crispEdges"/>`;
12666
12785
  }
12667
12786
 
12787
+ function messageNumberWidth(number) {
12788
+ return measureTextWithFont(number, "12px") + 8;
12789
+ }
12790
+ function renderMessageNumber(number, right, baseline) {
12791
+ if (!number) return "";
12792
+ return `<rect x="${right - messageNumberWidth(number)}" y="${baseline - 12}" width="${messageNumberWidth(number)}" height="16" rx="2" class="message-number-bg"/>
12793
+ <text x="${right - 4}" y="${baseline}" text-anchor="end" class="seq-number">${esc(number)}</text>`;
12794
+ }
12795
+
12668
12796
  function renderMessage(m) {
12669
12797
  const isLTR = m.fromX < m.toX;
12670
12798
  const fromX = isLTR ? m.fromX + 1 : m.fromX;
12671
12799
  const toX = isLTR ? m.toX : m.toX + 1;
12672
12800
  const direction = Math.sign(m.toX - m.fromX);
12673
12801
  const labelX = (m.fromX + m.toX) / 2 - direction * 3.5 + 0.5;
12674
- const labelY = m.y - 3.5;
12802
+ const labelY = m.y - 2.5;
12675
12803
  const dashAttr = m.arrowStyle === "dashed" ? ' stroke-dasharray="6,4"' : "";
12676
12804
  const styleAttr = m.style ? ` style="${styleToAttr(m.style)}"` : "";
12677
12805
  const numberX = Math.min(fromX, toX) - 4;
12678
- const numberSvg = m.number ? `<text x="${numberX}" y="${labelY}" text-anchor="end" class="seq-number">${esc(m.number)}</text>` : "";
12679
- const lineY = m.y - 0.5;
12806
+ const numberSvg = renderMessageNumber(m.number, numberX, m.y - 3.5);
12807
+ const headCenterY = m.y - 0.5;
12808
+ const lineY = headCenterY + 0.5;
12680
12809
  return `<g class="message">
12681
12810
  <line x1="${fromX}" y1="${lineY}" x2="${toX}" y2="${lineY}" class="message-line"${dashAttr}/>
12682
- ${renderArrowHead(toX, lineY, m.isReverse, m.arrowStyle)}
12811
+ ${renderArrowHead(toX, headCenterY, m.isReverse, m.arrowStyle)}
12683
12812
  <text x="${labelX}" y="${labelY}" text-anchor="middle" class="message-label"${styleAttr}>${esc(resolveEmojiInText(m.label))}</text>
12684
12813
  ${numberSvg}
12685
12814
  </g>`;
@@ -12690,7 +12819,7 @@ function renderSelfCall(s) {
12690
12819
  const labelX = x1 + 6;
12691
12820
  const labelY = s.y + (isAsync ? 15 : 12);
12692
12821
  const numberY = s.y + 12;
12693
- const numberSvg = s.number ? `<text x="${x1 - 3}" y="${numberY}" text-anchor="end" class="seq-number">${esc(s.number)}</text>` : "";
12822
+ const numberSvg = renderMessageNumber(s.number, x1 - 3, numberY);
12694
12823
  const svgX = x1 + 1;
12695
12824
  const svgY = s.y + (isAsync ? 20 : 14);
12696
12825
  const pathEnd = isAsync ? "L1,15" : "L14,15";
@@ -12699,11 +12828,11 @@ function renderSelfCall(s) {
12699
12828
  const arrowPath = isAsync ? "M1 1.25 L6.15 4.5 L1 7.75" : "M1 1.25 L6.15 4.5 L1 7.75 Z";
12700
12829
  return `<g class="message self-call">
12701
12830
  <svg x="${svgX}" y="${svgY}" width="30" height="24">
12702
- <path d="M0,2 L26,2 Q28,2 28,4 L28,13 Q28,15 26,15 ${pathEnd}" fill="none" stroke="#000" stroke-width="2"/>
12831
+ <path d="M0,2 L26,2 Q28,2 28,4 L28,13 Q28,15 26,15 ${pathEnd}" fill="none" stroke="#000" stroke-width="1"/>
12703
12832
  <g transform="translate(${arrowTx}, 10)">
12704
12833
  <svg height="10" width="7" viewBox="0 0 7 9">
12705
12834
  <g transform="scale(-1, 1) translate(-7, 0)">
12706
- <path d="${arrowPath}" stroke="#000" stroke-linecap="round" fill="${arrowFill}" stroke-width="2"/>
12835
+ <path d="${arrowPath}" stroke="#000" stroke-linecap="round" fill="${arrowFill}" stroke-width="1"/>
12707
12836
  </g>
12708
12837
  </svg>
12709
12838
  </g>
@@ -12721,96 +12850,122 @@ function renderArrowHead(tipX, tipY, pointsLeft, style) {
12721
12850
  const rtlTransform = pointsLeft ? ' transform="scale(-1, 1) translate(-7, 0)"' : "";
12722
12851
  return `<svg x="${svgX}" y="${svgY}" width="7" height="10" viewBox="0 0 7 9" overflow="visible" class="arrow-head${isFilled ? "" : " arrow-open"}">
12723
12852
  <g${rtlTransform}>
12724
- <path d="${pathD}" stroke="#000" stroke-linecap="round" stroke-width="2" fill="${fillAttr}"/>
12853
+ <path d="${pathD}" transform="translate(1 0.5)" stroke="#000" stroke-linecap="round" stroke-width="1" fill="${fillAttr}"/>
12725
12854
  </g>
12726
12855
  </svg>`;
12727
12856
  }
12728
12857
 
12729
12858
  function renderOccurrence(o) {
12730
- const strokeHalf = 1;
12859
+ const strokeHalf = 0.5;
12731
12860
  const rx = o.x + strokeHalf;
12732
- 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)}"/>`;
12861
+ return `<rect x="${rx}" y="${o.y + strokeHalf}" width="${o.width - strokeHalf * 2}" height="${o.height - strokeHalf * 2}" rx="1.5" class="occurrence" data-participant="${esc(o.participantName)}"/>`;
12733
12862
  }
12734
12863
 
12735
- const HEADER_HEIGHT = 25;
12736
- const BRACKET_WIDTH = 3.89;
12864
+ const HEADER_HEIGHT = DEFAULT_LAYOUT_METRICS.fragmentHeaderHeight;
12737
12865
  const TEXT_PAD_X = 4;
12866
+ const NUMBER_PAD_X = 4;
12738
12867
  const STROKE_WIDTH = 1;
12739
12868
  const HALF_STROKE = STROKE_WIDTH / 2;
12740
12869
  function renderFragment(f) {
12741
12870
  const parts = [];
12871
+ const kindLabel = f.headerLabel ?? fragmentHeaderLabel(f.kind);
12872
+ const width = Math.max(f.width, fragmentHeaderWidth(kindLabel, f.number));
12742
12873
  parts.push(
12743
- `<rect x="${f.x + HALF_STROKE}" y="${f.y + HALF_STROKE}" width="${f.width - STROKE_WIDTH}" height="${f.height - STROKE_WIDTH}" rx="4" class="fragment-border"/>`
12874
+ `<rect x="${f.x + HALF_STROKE}" y="${f.y + HALF_STROKE}" width="${width - STROKE_WIDTH}" height="${f.height - STROKE_WIDTH}" rx="${4 - HALF_STROKE}" class="fragment-border"/>`
12744
12875
  );
12745
12876
  const headerX = f.x + 1;
12746
12877
  const headerY = f.headerY;
12747
- const headerW = f.width - 2;
12878
+ const headerW = width - 2;
12748
12879
  parts.push(
12749
- `<rect x="${headerX}" y="${headerY}" width="${headerW}" height="${HEADER_HEIGHT}" class="fragment-header"/>`
12880
+ `<rect x="${headerX}" y="${headerY}" width="${headerW}" height="${HEADER_HEIGHT}" class="fragment-header"${f.kind === "opt" ? "" : ' style="clip-path: inset(0 round 4px 4px 0 0)"'}/>`
12750
12881
  );
12751
- const iconX = headerX + 4;
12882
+ const numberWidth = f.number ? Math.ceil(measureTextWithFont(f.number, "12px") * 64) / 64 : 0;
12883
+ const numberInset = f.number ? numberWidth + 14 : 0;
12884
+ const titleY = headerY + HEADER_HEIGHT / 2 - 0.5;
12885
+ if (f.number) {
12886
+ const numberBoxX = headerX + 4;
12887
+ const numberX = numberBoxX + NUMBER_PAD_X;
12888
+ parts.push(
12889
+ `<rect x="${numberBoxX}" y="${headerY + (HEADER_HEIGHT - 1 - 16) / 2}" width="${numberWidth + NUMBER_PAD_X * 2}" height="16" rx="2" class="fragment-number-bg"/>`,
12890
+ `<text x="${numberX}" y="${titleY}" dominant-baseline="central" class="seq-number">${esc(f.number)}</text>`
12891
+ );
12892
+ }
12893
+ const iconX = headerX + 4 + numberInset;
12752
12894
  const iconY = headerY;
12753
12895
  parts.push(getFragmentIcon(f.kind, iconX, iconY));
12754
- const kindLabel = getKindLabel(f.kind);
12755
- const labelX = headerX + 26;
12896
+ const labelX = headerX + 26 + numberInset;
12756
12897
  parts.push(
12757
12898
  `<text x="${labelX}" y="${headerY + HEADER_HEIGHT / 2 - 0.5}" dominant-baseline="central" class="fragment-label">${esc(kindLabel)}</text>`
12758
12899
  );
12759
- if (f.number) {
12900
+ if (f.label) {
12760
12901
  parts.push(
12761
- `<text x="${f.x - 3}" y="${headerY + HEADER_HEIGHT / 2 - 4.5}" text-anchor="end" dominant-baseline="central" class="seq-number">${esc(f.number)}</text>`
12902
+ renderGuardRow(
12903
+ headerX,
12904
+ headerY + HEADER_HEIGHT,
12905
+ f.label,
12906
+ fragmentGuardKeyword(f.kind, f.guardKeyword)
12907
+ )
12762
12908
  );
12763
12909
  }
12764
- if (f.label) {
12765
- const condY = headerY + HEADER_HEIGHT + 15;
12766
- parts.push(renderBracketedLabel(headerX, condY, f.label, f.labelWidth, "fragment-condition"));
12767
- }
12768
12910
  if (f.sections.length > 1) {
12769
12911
  for (let i = 1; i < f.sections.length; i++) {
12770
12912
  const section = f.sections[i];
12771
12913
  const lineY = section.y;
12772
12914
  const separatorY = lineY + HALF_STROKE;
12773
12915
  const sepX1 = f.x + 1 + (section.contentInsetLeft ?? 0);
12774
- const sepX2 = f.x + f.width - 1;
12916
+ const sepX2 = f.x + width - 1;
12775
12917
  parts.push(
12776
12918
  `<line x1="${sepX1}" y1="${separatorY}" x2="${sepX2}" y2="${separatorY}" class="fragment-separator"/>`
12777
12919
  );
12778
12920
  if (section.label) {
12921
+ if (f.kind === "alt") {
12922
+ parts.push(
12923
+ renderGuardRow(
12924
+ f.x + 1,
12925
+ lineY + 1,
12926
+ section.innerLabel ?? "",
12927
+ section.guardKeyword
12928
+ )
12929
+ );
12930
+ continue;
12931
+ }
12779
12932
  const labelY = lineY + 16;
12780
12933
  const isFinally = section.label.startsWith("finally");
12781
12934
  const isBracketed = !!section.innerLabel && section.label !== "[else]";
12782
12935
  if (isBracketed) {
12783
12936
  const labelX2 = f.x + 1;
12784
- parts.push(
12785
- renderBracketedLabel(labelX2, labelY, section.innerLabel, section.innerLabelWidth, "fragment-section-label")
12786
- );
12937
+ parts.push(renderGuardRow(labelX2, lineY + 1, section.innerLabel));
12787
12938
  continue;
12788
12939
  }
12789
12940
  const spaceIdx = section.label.indexOf(" ");
12790
12941
  if (spaceIdx > 0 && !isFinally) {
12791
12942
  const keyword = section.keyword || section.label.substring(0, spaceIdx);
12792
12943
  const condition = section.detail || section.label.substring(spaceIdx + 1);
12793
- const keywordWidth = section.keywordWidth ?? keyword.length * 7;
12944
+ const keywordWidth = measureTextWithFont(
12945
+ keyword,
12946
+ "14px",
12947
+ "all-small-caps"
12948
+ );
12794
12949
  const keywordX = f.x + 5;
12795
12950
  const conditionX = keywordX + keywordWidth + TEXT_PAD_X * 2;
12796
- const bgWidth = (section.keywordWidth ?? keyword.length * 7) + (section.detailWidth ?? condition.length * 7) + TEXT_PAD_X * 4;
12951
+ const bgWidth = keywordWidth + (section.detailWidth ?? condition.length * 7) + TEXT_PAD_X * 4;
12797
12952
  parts.push(
12798
- `<g opacity="0.65"><rect x="${keywordX - TEXT_PAD_X}" y="${lineY + 1}" width="${bgWidth}" height="20" fill="#fff"/><text x="${keywordX}" y="${labelY}" class="fragment-section-label" fill="#222">${esc(keyword)}</text><text x="${conditionX}" y="${labelY}" class="fragment-section-label" fill="#222">${esc(condition)}</text></g>`
12953
+ `<g opacity="0.65"><rect x="${keywordX - TEXT_PAD_X}" y="${lineY + 1}" width="${bgWidth}" height="20" fill="#fff"/><text x="${keywordX}" y="${labelY}" class="fragment-section-label fragment-section-keyword" fill="#222">${esc(keyword)}</text><text x="${conditionX}" y="${labelY}" class="fragment-section-label" fill="#222">${esc(condition)}</text></g>`
12799
12954
  );
12800
12955
  } else {
12801
12956
  const finallyX = f.x + 5;
12802
12957
  const finallyY = labelY;
12803
12958
  if (section.label === "[else]") {
12804
12959
  parts.push(
12805
- `<text x="${finallyX}" y="${finallyY}" class="fragment-section-label">${esc(section.label)}</text>`
12960
+ `<text x="${finallyX}" y="${finallyY}" class="fragment-section-label fragment-section-keyword">${esc(section.label)}</text>`
12806
12961
  );
12807
12962
  continue;
12808
12963
  }
12809
- const bgWidth = (section.labelWidth ?? section.label.length * 7) + TEXT_PAD_X * 2;
12964
+ const bgWidth = (isFinally ? measureTextWithFont(section.label, "14px", "all-small-caps") : section.labelWidth ?? section.label.length * 7) + TEXT_PAD_X * 2;
12810
12965
  const bgY = lineY + 1;
12811
12966
  const bgHeight = 20;
12812
12967
  parts.push(
12813
- `<g opacity="0.65"><rect x="${finallyX - TEXT_PAD_X}" y="${bgY}" width="${bgWidth}" height="${bgHeight}" fill="#fff"/><text x="${finallyX}" y="${finallyY}" class="fragment-section-label">${esc(section.label)}</text></g>`
12968
+ `<g opacity="0.65"><rect x="${finallyX - TEXT_PAD_X}" y="${bgY}" width="${bgWidth}" height="${bgHeight}" fill="#fff"/><text x="${finallyX}" y="${finallyY}" class="fragment-section-label fragment-section-keyword">${esc(section.label)}</text></g>`
12814
12969
  );
12815
12970
  }
12816
12971
  }
@@ -12820,33 +12975,11 @@ function renderFragment(f) {
12820
12975
  ${parts.join("\n ")}
12821
12976
  </g>`;
12822
12977
  }
12823
- function renderBracketedLabel(x, y, innerText, innerWidth, cls = "fragment-condition") {
12824
- const measuredInnerWidth = innerWidth ?? innerText.length * 7;
12825
- const innerX = x + BRACKET_WIDTH + TEXT_PAD_X;
12826
- const closeX = innerX + measuredInnerWidth + TEXT_PAD_X;
12827
- return `<g><text x="${x}" y="${y}" class="${cls}">[</text><text x="${innerX}" y="${y}" class="${cls}" opacity="0.65">${esc(resolveEmojiInText(innerText))}</text><text x="${closeX}" y="${y}" class="${cls}">]</text></g>`;
12828
- }
12829
- function getKindLabel(kind) {
12830
- switch (kind) {
12831
- case "alt":
12832
- return "Alt";
12833
- case "loop":
12834
- return "Loop";
12835
- case "opt":
12836
- return "Opt";
12837
- case "par":
12838
- return "Par";
12839
- case "critical":
12840
- return "Critical";
12841
- case "section":
12842
- return "Section";
12843
- case "tcf":
12844
- return "Try";
12845
- case "ref":
12846
- return "Ref";
12847
- default:
12848
- return kind.charAt(0).toUpperCase() + kind.slice(1);
12849
- }
12978
+ function renderGuardRow(x, rowY, innerText, keyword) {
12979
+ const baseline = rowY + 19;
12980
+ const textX = x + 4;
12981
+ const conditionX = textX + (keyword ? measureTextWithFont(keyword, "12px", "all-small-caps") + 4 : 0);
12982
+ return `<g class="guard-row">` + (keyword ? `<text x="${textX}" y="${baseline}" class="guard-keyword">${esc(keyword)}</text>` : "") + (innerText ? `<text x="${conditionX}" y="${baseline}" class="fragment-condition" opacity="0.65">${esc(resolveEmojiInText(innerText))}</text>` : "") + `</g>`;
12850
12983
  }
12851
12984
  function getFragmentIcon(kind, x, y) {
12852
12985
  const w = 20;
@@ -12930,7 +13063,7 @@ function renderOpenArrow(tipX, tipY, pointsLeft) {
12930
13063
  const x1 = tipX + dir * w;
12931
13064
  const y1 = tipY - halfH;
12932
13065
  const y2 = tipY + halfH;
12933
- return `<polyline points="${x1},${y1} ${tipX},${tipY} ${x1},${y2}" fill="none" stroke-linecap="round" class="arrow-head arrow-open"/>`;
13066
+ return `<polyline points="${x1},${y1} ${tipX},${tipY} ${x1},${y2}" transform="translate(${pointsLeft ? -1 : 1} 0)" fill="none" stroke-linecap="round" class="arrow-head arrow-open"/>`;
12934
13067
  }
12935
13068
 
12936
13069
  function renderReturn(r) {
@@ -12940,8 +13073,9 @@ function renderReturn(r) {
12940
13073
  const minX = Math.min(r.fromX, r.toX);
12941
13074
  const ARROW_PADDING_HALF = 3.5;
12942
13075
  const labelX = minX + Math.abs(r.toX - r.fromX) / 2 + (r.isReverse ? ARROW_PADDING_HALF : -ARROW_PADDING_HALF);
12943
- const lineY = Math.floor(r.y);
12944
- const labelY = lineY - 3;
13076
+ const snappedY = Math.floor(r.y);
13077
+ const lineY = snappedY + 0.5;
13078
+ const labelY = snappedY - 3;
12945
13079
  const arrowTipX = r.toX;
12946
13080
  const w = 5.15;
12947
13081
  const halfH = 3.25;
@@ -12950,10 +13084,10 @@ function renderReturn(r) {
12950
13084
  const ay1 = lineY - halfH;
12951
13085
  const ay2 = lineY + halfH;
12952
13086
  const numberX = Math.min(r.fromX, r.toX) - 4;
12953
- const numberSvg = r.number ? `<text x="${numberX}" y="${labelY}" text-anchor="end" class="seq-number">${esc(r.number)}</text>` : "";
13087
+ const numberSvg = renderMessageNumber(r.number, numberX, labelY);
12954
13088
  return `<g class="return">
12955
13089
  <line x1="${r.fromX}" y1="${lineY}" x2="${r.toX}" y2="${lineY}" class="return-line"/>
12956
- <polyline points="${ax1},${ay1} ${arrowTipX},${lineY} ${ax1},${ay2}" fill="none" stroke-linecap="round" class="return-arrow"/>
13090
+ <polyline points="${ax1},${ay1} ${arrowTipX},${lineY} ${ax1},${ay2}" transform="translate(${dir} 0)" fill="none" stroke-linecap="round" class="return-arrow"/>
12957
13091
  <text x="${labelX}" y="${labelY}" text-anchor="middle" class="return-label">${esc(resolveEmojiInText(r.label))}</text>
12958
13092
  ${numberSvg}
12959
13093
  </g>`;
@@ -13093,47 +13227,157 @@ function renderGroup(g) {
13093
13227
  </g>`;
13094
13228
  }
13095
13229
 
13096
- const FRAME_HEADER_HEIGHT = 28;
13097
- const FRAME_BORDER_RADIUS = 4;
13098
- const DEFAULT_THEME_STYLES = `
13099
- .frame-border-outer { fill: #666; }
13100
- .frame-border-inner { fill: #ffffff; }
13101
- .frame-header-bg { fill: #ffffff; }
13102
- .frame-header-line { stroke: #666; stroke-width: 1; shape-rendering: crispEdges; }
13103
- .frame-title { font-family: Helvetica, Verdana, serif; font-size: 16px; font-weight: 600; fill: #222; }
13104
- .participant-box { fill: #ffffff; stroke: #666; stroke-width: 2; }
13105
- .participant-label { font-family: Helvetica, Verdana, serif; font-size: 16px; fill: #222; }
13106
- .participant-icon { color: #222; }
13107
- .participant-icon [fill="currentColor"]:not([stroke]) { stroke: #666; stroke-width: 1; }
13108
- .lifeline { stroke: #666; stroke-width: 1; }
13109
- .message-line { stroke: #000; stroke-width: 2; shape-rendering: crispEdges; }
13110
- .message-label { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #222; }
13111
- .arrow-head { fill: #000; stroke: #000; stroke-width: 2; }
13230
+ const HELVETICA = "Helvetica, Verdana, serif";
13231
+ const DEFAULT_PALETTE = {
13232
+ frameBorder: "#666",
13233
+ frameBg: "#ffffff",
13234
+ titleText: "#222",
13235
+ participantBg: "#ffffff",
13236
+ participantBorder: "#666",
13237
+ participantText: "#222",
13238
+ messageArrow: "#000",
13239
+ messageText: "#222",
13240
+ occurrenceBg: "#dedede",
13241
+ occurrenceBorder: "#666",
13242
+ fragmentBorder: "#666",
13243
+ fragmentHeaderBg: "#dedede",
13244
+ fragmentHeaderOpacity: "0.498",
13245
+ fragmentText: "#000",
13246
+ fragmentSeparator: "#e5e7eb",
13247
+ commentText: "#333",
13248
+ fontFamily: HELVETICA
13249
+ };
13250
+ const THEME_PALETTES = {
13251
+ "theme-default": DEFAULT_PALETTE,
13252
+ // `.theme-mermaid` sets only base/text/border/occurrence, and hides the
13253
+ // footer — chrome the SVG output does not draw. Every slot it leaves unset
13254
+ // matches theme-default, so the two render identically.
13255
+ "theme-mermaid": DEFAULT_PALETTE,
13256
+ // src/assets/tailwind.css `.theme-clean-light`.
13257
+ "theme-clean-light": {
13258
+ frameBorder: "#e5e9f0",
13259
+ frameBg: "#ffffff",
13260
+ titleText: "#353748",
13261
+ participantBg: "#edf4fe",
13262
+ participantBorder: "#368eff",
13263
+ participantText: "#368eff",
13264
+ messageArrow: "#368eff",
13265
+ messageText: "#368eff",
13266
+ occurrenceBg: "#ffffff",
13267
+ occurrenceBorder: "#368eff",
13268
+ fragmentBorder: "#c8c9c9",
13269
+ fragmentHeaderBg: "#edf4fe",
13270
+ fragmentHeaderOpacity: "1",
13271
+ fragmentText: "#368eff",
13272
+ fragmentSeparator: "#e5e9f0",
13273
+ commentText: "#353748",
13274
+ fontFamily: HELVETICA
13275
+ },
13276
+ // src/assets/tailwind.css `.theme-clean-dark`.
13277
+ "theme-clean-dark": {
13278
+ frameBorder: "#cecfd2",
13279
+ frameBg: "#111628",
13280
+ titleText: "#cecfd2",
13281
+ participantBg: "#5964f2",
13282
+ participantBorder: "#cecfd2",
13283
+ participantText: "#536fff",
13284
+ messageArrow: "#536fff",
13285
+ messageText: "#cecfd2",
13286
+ occurrenceBg: "#5964f2",
13287
+ occurrenceBorder: "#cecfd2",
13288
+ fragmentBorder: "#cecfd2",
13289
+ fragmentHeaderBg: "#5964f2",
13290
+ fragmentHeaderOpacity: "1",
13291
+ fragmentText: "#cecfd2",
13292
+ fragmentSeparator: "#cecfd2",
13293
+ commentText: "#cecfd2",
13294
+ fontFamily: HELVETICA
13295
+ },
13296
+ // src/assets/tailwind.css `.theme-neon`. The DOM theme also swaps in the
13297
+ // MS Sans Serif webfont (`.zenuml .theme-neon`); the SVG names it first and
13298
+ // keeps the Helvetica stack behind it, since a consumer embedding the SVG
13299
+ // will not have that @font-face rule.
13300
+ "theme-neon": {
13301
+ frameBorder: "#60ff33",
13302
+ frameBg: "#000000",
13303
+ titleText: "#8ffc5b",
13304
+ participantBg: "#000000",
13305
+ participantBorder: "#8ffc5b",
13306
+ participantText: "#8ffc5b",
13307
+ messageArrow: "#8ffc5b",
13308
+ messageText: "#8ffc5b",
13309
+ occurrenceBg: "#8ffc5b",
13310
+ occurrenceBorder: "#8ffc5b",
13311
+ fragmentBorder: "#60ff33",
13312
+ fragmentHeaderBg: "#000000",
13313
+ fragmentHeaderOpacity: "1",
13314
+ fragmentText: "#8ffc5b",
13315
+ fragmentSeparator: "#60ff33",
13316
+ commentText: "#8ffc5b",
13317
+ fontFamily: `"MS Sans Serif", ${HELVETICA}`
13318
+ }
13319
+ };
13320
+ function resolvePalette(theme) {
13321
+ if (theme && theme in THEME_PALETTES) {
13322
+ return THEME_PALETTES[theme];
13323
+ }
13324
+ return DEFAULT_PALETTE;
13325
+ }
13326
+ function buildThemeStyles(p) {
13327
+ return `
13328
+ .frame-border-outer { fill: ${p.frameBorder}; }
13329
+ .frame-border-inner { fill: ${p.frameBg}; }
13330
+ .frame-header-bg { fill: ${p.frameBg}; }
13331
+ .frame-header-line { stroke: ${p.frameBorder}; stroke-width: 1; shape-rendering: crispEdges; }
13332
+ .frame-title { font-family: ${p.fontFamily}; font-size: 14px; font-weight: 400; fill: ${p.titleText}; }
13333
+ .participant-box { fill: ${p.participantBg}; stroke: ${p.participantBorder}; stroke-width: 1; }
13334
+ .participant-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.participantText}; }
13335
+ .participant-icon { color: ${p.participantText}; }
13336
+ .participant-icon [fill="currentColor"]:not([stroke]) { stroke: ${p.participantBorder}; stroke-width: 1; }
13337
+ .lifeline { stroke: ${p.participantBorder}; stroke-width: 1; }
13338
+ .message-line { stroke: ${p.messageArrow}; stroke-width: 1; shape-rendering: crispEdges; }
13339
+ .message-label { font-family: ${p.fontFamily}; font-size: 15px; fill: ${p.messageText}; }
13340
+ .arrow-head { fill: ${p.messageArrow}; stroke: ${p.messageArrow}; stroke-width: 1; }
13112
13341
  .arrow-open { fill: none; }
13113
- .occurrence { fill: #dedede; stroke: #666; stroke-width: 2; shape-rendering: crispEdges; }
13114
- .fragment-border { fill: none; stroke: #666; stroke-width: 1; shape-rendering: crispEdges; }
13115
- .fragment-header { fill: #dedede; fill-opacity: 0.498; stroke: none; shape-rendering: crispEdges; }
13116
- .fragment-label { font-family: Helvetica, Verdana, serif; font-size: 14px; font-weight: 600; fill: #000; }
13117
- .fragment-condition { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #000; }
13118
- .fragment-separator { stroke: #e5e7eb; stroke-width: 1; shape-rendering: crispEdges; }
13119
- .fragment-section-label { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #000; }
13120
- .return-line { stroke: #000; stroke-width: 2; stroke-dasharray: 6,4; shape-rendering: crispEdges; }
13121
- .return-arrow { stroke: #000; stroke-width: 2; fill: none; }
13122
- .return-label { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #222; }
13123
- .return-icon { fill: #222; }
13342
+ .occurrence { fill: ${p.occurrenceBg}; stroke: ${p.occurrenceBorder}; stroke-width: 1; shape-rendering: crispEdges; }
13343
+ .fragment-border { fill: none; stroke: ${p.fragmentBorder}; stroke-width: 1; }
13344
+ .fragment-header { fill: ${p.fragmentHeaderBg}; fill-opacity: ${p.fragmentHeaderOpacity}; stroke: none; shape-rendering: crispEdges; }
13345
+ .fragment-number-bg { fill: #6b7280; fill-opacity: 0.1; stroke: none; }
13346
+ .message-number-bg { fill: #6b7280; fill-opacity: 0.1; stroke: none; }
13347
+ .fragment-label { font-family: ${p.fontFamily}; font-size: 14px; font-weight: 400; font-variant-caps: all-small-caps; fill: ${p.fragmentText}; }
13348
+ .fragment-condition { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.fragmentText}; }
13349
+ .fragment-separator { stroke: ${p.fragmentSeparator}; stroke-width: 1; shape-rendering: crispEdges; }
13350
+ .fragment-section-keyword { font-variant-caps: all-small-caps; }
13351
+ .fragment-section-label { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.fragmentText}; }
13352
+ .return-line { stroke: ${p.messageArrow}; stroke-width: 1; stroke-dasharray: 6,4; shape-rendering: crispEdges; }
13353
+ .return-arrow { stroke: ${p.messageArrow}; stroke-width: 1; fill: none; }
13354
+ .return-label { font-family: ${p.fontFamily}; font-size: 15px; fill: ${p.messageText}; }
13355
+ .return-icon { fill: ${p.messageText}; }
13124
13356
  .divider-line { stroke: #aaaa33; stroke-width: 1; }
13125
13357
  .divider-bg { fill: #fff5ad; stroke: #aaaa33; stroke-width: 1; }
13126
- .divider-label { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #333; }
13127
- .comment-text { font-family: Helvetica, Verdana, serif; font-size: 14px; fill: #333; opacity: 0.5; }
13128
- .seq-number { font-family: Helvetica, Verdana, serif; font-size: 12px; font-weight: 100; fill: #6b7280; }
13129
- .group-outline { fill: none; stroke: #666; }
13130
- .group-title-bg { fill: #ffffff; stroke: none; }
13131
- .group-title-text { font-family: Helvetica, Verdana, serif; font-size: 13px; font-weight: 400; fill: #222; }
13358
+ .divider-label { font-family: ${p.fontFamily}; font-size: 14px; fill: #333; }
13359
+ .comment-text { font-family: ${p.fontFamily}; font-size: 14px; fill: ${p.commentText}; opacity: 0.5; }
13360
+ .guard-keyword { font-family: ${p.fontFamily}; font-size: 12px; font-weight: 400; font-variant-caps: all-small-caps; fill: #6b7280; }
13361
+ .seq-number { font-family: ${p.fontFamily}; font-size: 12px; font-weight: 400; fill: #6b7280; }
13362
+ .group-outline { fill: none; stroke: ${p.participantBorder}; }
13363
+ .group-title-bg { fill: ${p.frameBg}; stroke: none; }
13364
+ .group-title-text { font-family: ${p.fontFamily}; font-size: 13px; font-weight: 400; fill: ${p.participantText}; }
13132
13365
  `;
13366
+ }
13367
+
13368
+ const FRAME_VIEWPORT_HEADER_ALLOWANCE = 28;
13369
+ const FRAME_BORDER_RADIUS = 4;
13133
13370
  function renderToSvg(code, options) {
13134
13371
  const rootContext = RootContext(code);
13135
13372
  if (!rootContext) {
13136
- return { svg: "<svg></svg>", innerSvg: "", width: 0, height: 0, viewBox: "0 0 0 0", geometry: void 0 };
13373
+ return {
13374
+ svg: "<svg></svg>",
13375
+ innerSvg: "",
13376
+ width: 0,
13377
+ height: 0,
13378
+ viewBox: "0 0 0 0",
13379
+ geometry: void 0
13380
+ };
13137
13381
  }
13138
13382
  const coordinates = new Coordinates(rootContext, WidthProviderOnCanvas);
13139
13383
  const verticalCoordinates = new VerticalCoordinates(rootContext);
@@ -13145,14 +13389,41 @@ function renderToSvg(code, options) {
13145
13389
  coordinates,
13146
13390
  verticalCoordinates,
13147
13391
  title,
13148
- measureText: WidthProviderOnCanvas
13392
+ measureText: WidthProviderOnCanvas,
13393
+ enableNumbering: options?.enableNumbering
13149
13394
  });
13150
- return { ...composeSvg(geometry), geometry };
13395
+ return { ...composeSvg(geometry, options), geometry };
13151
13396
  }
13152
13397
  function composeSvg(g, options) {
13153
13398
  const padding = 10;
13154
- const headerH = FRAME_HEADER_HEIGHT;
13155
- const contentLeftMargin = 1 + padding + g.frameBorderLeft;
13399
+ const headerH = FRAME_VIEWPORT_HEADER_ALLOWANCE;
13400
+ let contentLeftMargin = 1 + padding + g.frameBorderLeft;
13401
+ let numberLeft = 0;
13402
+ for (const message of [
13403
+ ...g.messages,
13404
+ ...g.creations.map((creation) => creation.message)
13405
+ ]) {
13406
+ if (!message.number) continue;
13407
+ numberLeft = Math.min(
13408
+ numberLeft,
13409
+ Math.min(message.fromX, message.toX) + 1 - 4 - messageNumberWidth(message.number)
13410
+ );
13411
+ }
13412
+ for (const call of g.selfCalls) {
13413
+ if (call.number)
13414
+ numberLeft = Math.min(
13415
+ numberLeft,
13416
+ call.x - 3 - messageNumberWidth(call.number)
13417
+ );
13418
+ }
13419
+ for (const returned of g.returns) {
13420
+ if (returned.number && !returned.isSelf)
13421
+ numberLeft = Math.min(
13422
+ numberLeft,
13423
+ Math.min(returned.fromX, returned.toX) - 4 - messageNumberWidth(returned.number)
13424
+ );
13425
+ }
13426
+ contentLeftMargin += Math.max(0, padding - contentLeftMargin - numberLeft);
13156
13427
  const viewWidth = g.width + contentLeftMargin + padding + g.frameBorderRight + 1;
13157
13428
  const viewHeight = g.height + padding * 2 + headerH - 1;
13158
13429
  const parts = [];
@@ -13202,14 +13473,13 @@ function composeSvg(g, options) {
13202
13473
  `<rect class="frame-border-outer" x="0" y="0" width="${viewWidth}" height="${viewHeight}" rx="${r}" fill="#666"/>`,
13203
13474
  `<rect class="frame-border-inner" x="1" y="1" width="${viewWidth - 2}" height="${viewHeight - 2}" rx="${Math.max(0, r - 1)}" fill="#fff"/>`
13204
13475
  ].join("\n");
13205
- const contentPaddingTop = 6;
13206
- const headerLineY = headerH + contentPaddingTop;
13476
+ const headerLineY = 1 + DIAGRAM_HEADER_HEIGHT;
13207
13477
  const headerLineDrawY = headerLineY - 0.5;
13208
13478
  const headerLineSvg = `<line class="frame-header-line" x1="1" y1="${headerLineDrawY}" x2="${viewWidth - 1}" y2="${headerLineDrawY}"/>`;
13209
- const titleSvg = g.title ? `<text x="5" y="${headerLineDrawY / 2}" dominant-baseline="central" class="frame-title">${escXml(g.title)}</text>` : "";
13479
+ const titleSvg = g.title ? `<text x="5" y="${headerLineDrawY / 2}" dominant-baseline="central" class="frame-title">${esc(g.title)}</text>` : "";
13210
13480
  const viewBox = `0 0 ${viewWidth} ${viewHeight}`;
13211
13481
  const defs = `<defs>
13212
- <style>${DEFAULT_THEME_STYLES}</style>
13482
+ <style>${buildThemeStyles(resolvePalette(options?.theme))}</style>
13213
13483
  </defs>`;
13214
13484
  const frame = `${frameSvg}
13215
13485
  ${headerLineSvg}
@@ -13225,9 +13495,6 @@ ${innerSvg}
13225
13495
  </svg>`;
13226
13496
  return { svg, innerSvg, width: viewWidth, height: viewHeight, viewBox };
13227
13497
  }
13228
- function escXml(s) {
13229
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13230
- }
13231
13498
 
13232
13499
  function readVersion() {
13233
13500
  const pkgPath = resolve(import.meta.dir, "../../package.json");
@@ -13249,7 +13516,8 @@ Rendering:
13249
13516
  -o, --output <file> Output file (use "-" for stdout; default: <input>.svg)
13250
13517
  -e, --outputFormat <format> Output format: "svg" (default) or "png"
13251
13518
  -s, --scale <factor> Pixel scale factor for PNG (default: 2; ignored for SVG)
13252
- -t, --theme <name> Theme name passed to renderer (e.g. "theme-default")
13519
+ -t, --theme <name> Theme: default, mermaid, clean-light, clean-dark,
13520
+ neon (prefix "theme-"; unknown names use default)
13253
13521
  -c, --configFile <file> JSON config file with { theme, scale, outputFormat }
13254
13522
  --md Markdown mode: render zenuml code blocks and produce output Markdown
13255
13523
 
@@ -13610,7 +13878,6 @@ async function checkOne(input) {
13610
13878
  errors: [{ line: 0, column: 0, msg: err.message }]
13611
13879
  };
13612
13880
  }
13613
- Parser.Errors.length = 0;
13614
13881
  Parser.ErrorDetails.length = 0;
13615
13882
  Parser.RootContext(code);
13616
13883
  const errors = Parser.ErrorDetails.map((e) => ({
@@ -13776,7 +14043,6 @@ async function main() {
13776
14043
  `);
13777
14044
  process.exit(1);
13778
14045
  }
13779
- Parser.Errors.length = 0;
13780
14046
  Parser.ErrorDetails.length = 0;
13781
14047
  const tree = Parser.RootContext(code);
13782
14048
  if (Parser.ErrorDetails.length > 0) {