@diagrammo/dgmo 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -97,6 +97,7 @@ export interface SequenceNote {
97
97
  position: 'right' | 'left';
98
98
  participantId: string;
99
99
  lineNumber: number;
100
+ endLineNumber: number;
100
101
  }
101
102
 
102
103
  export type SequenceElement =
@@ -165,7 +166,7 @@ const UML_RETURN_PATTERN = /^(\w+\([^)]*\))\s*:\s*(.+)$/;
165
166
 
166
167
  // Note patterns — "note: text", "note right of API: text", "note left of User"
167
168
  const NOTE_SINGLE = /^note(?:\s+(right|left)\s+of\s+(\S+))?\s*:\s*(.+)$/i;
168
- const NOTE_MULTI = /^note(?:\s+(right|left)\s+of\s+(\S+))?\s*$/i;
169
+ const NOTE_MULTI = /^note(?:\s+(right|left)\s+of\s+([^\s:]+))?\s*:?\s*$/i;
169
170
 
170
171
  /**
171
172
  * Extract return label from a message label string.
@@ -647,15 +648,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
647
648
  (noteSingleMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
648
649
  let noteParticipant = noteSingleMatch[2] || null;
649
650
  if (!noteParticipant) {
650
- if (!lastMsgFrom) {
651
- result.error = `Line ${lineNumber}: note requires a preceding message`;
652
- return result;
653
- }
651
+ if (!lastMsgFrom) continue; // incomplete — skip during live typing
654
652
  noteParticipant = lastMsgFrom;
655
653
  }
656
654
  if (!result.participants.some((p) => p.id === noteParticipant)) {
657
- result.error = `Line ${lineNumber}: note references unknown participant '${noteParticipant}'`;
658
- return result;
655
+ continue; // unknown participant skip during live typing
659
656
  }
660
657
  const note: SequenceNote = {
661
658
  kind: 'note',
@@ -663,6 +660,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
663
660
  position: notePosition,
664
661
  participantId: noteParticipant,
665
662
  lineNumber,
663
+ endLineNumber: lineNumber,
666
664
  };
667
665
  currentContainer().push(note);
668
666
  continue;
@@ -675,15 +673,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
675
673
  (noteMultiMatch[1]?.toLowerCase() as 'right' | 'left') || 'right';
676
674
  let noteParticipant = noteMultiMatch[2] || null;
677
675
  if (!noteParticipant) {
678
- if (!lastMsgFrom) {
679
- result.error = `Line ${lineNumber}: note requires a preceding message`;
680
- return result;
681
- }
676
+ if (!lastMsgFrom) continue; // incomplete — skip during live typing
682
677
  noteParticipant = lastMsgFrom;
683
678
  }
684
679
  if (!result.participants.some((p) => p.id === noteParticipant)) {
685
- result.error = `Line ${lineNumber}: note references unknown participant '${noteParticipant}'`;
686
- return result;
680
+ continue; // unknown participant skip during live typing
687
681
  }
688
682
  // Collect indented body lines
689
683
  const noteLines: string[] = [];
@@ -696,16 +690,14 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
696
690
  noteLines.push(nextTrimmed);
697
691
  i++;
698
692
  }
699
- if (noteLines.length === 0) {
700
- result.error = `Line ${lineNumber}: multi-line note has no content — add indented lines or use 'note: text'`;
701
- return result;
702
- }
693
+ if (noteLines.length === 0) continue; // no body yet — skip during live typing
703
694
  const note: SequenceNote = {
704
695
  kind: 'note',
705
696
  text: noteLines.join('\n'),
706
697
  position: notePosition,
707
698
  participantId: noteParticipant,
708
699
  lineNumber,
700
+ endLineNumber: i + 1, // i has advanced past the body lines (1-based)
709
701
  };
710
702
  currentContainer().push(note);
711
703
  continue;
@@ -52,14 +52,16 @@ interface InlineSpan {
52
52
 
53
53
  function parseInlineMarkdown(text: string): InlineSpan[] {
54
54
  const spans: InlineSpan[] = [];
55
- const regex = /\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`|\[(.+?)\]\((.+?)\)|([^*`[]+)/g;
55
+ const regex = /\*\*(.+?)\*\*|__(.+?)__|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)|([^*_`[]+)/g;
56
56
  let match;
57
57
  while ((match = regex.exec(text)) !== null) {
58
- if (match[1]) spans.push({ text: match[1], bold: true });
59
- else if (match[2]) spans.push({ text: match[2], italic: true });
60
- else if (match[3]) spans.push({ text: match[3], code: true });
61
- else if (match[4]) spans.push({ text: match[4], href: match[5] });
62
- else if (match[6]) spans.push({ text: match[6] });
58
+ if (match[1]) spans.push({ text: match[1], bold: true }); // **bold**
59
+ else if (match[2]) spans.push({ text: match[2], bold: true }); // __bold__
60
+ else if (match[3]) spans.push({ text: match[3], italic: true }); // *italic*
61
+ else if (match[4]) spans.push({ text: match[4], italic: true }); // _italic_
62
+ else if (match[5]) spans.push({ text: match[5], code: true }); // `code`
63
+ else if (match[6]) spans.push({ text: match[6], href: match[7] }); // [text](url)
64
+ else if (match[8]) spans.push({ text: match[8] });
63
65
  }
64
66
  return spans;
65
67
  }
@@ -915,6 +917,38 @@ export function renderSequenceDiagram(
915
917
  markBlockSpacing(elements);
916
918
  }
917
919
 
920
+ // Note spacing — add vertical room after messages that have notes attached
921
+ const NOTE_OFFSET_BELOW = 16; // gap between message arrow and top of note box
922
+ const computeNoteHeight = (text: string): number => {
923
+ const lines = wrapTextLines(text, NOTE_CHARS_PER_LINE);
924
+ return lines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
925
+ };
926
+ const markNoteSpacing = (els: SequenceElement[]): void => {
927
+ for (let i = 0; i < els.length; i++) {
928
+ const el = els[i];
929
+ if (isSequenceNote(el)) {
930
+ const noteH = computeNoteHeight(el.text);
931
+ // Find the next non-note element after this note
932
+ const nextIdx =
933
+ i + 1 < els.length ? findFirstMsgIndex([els[i + 1]]) : -1;
934
+ if (nextIdx >= 0) {
935
+ addExtra(nextIdx, noteH + NOTE_OFFSET_BELOW);
936
+ }
937
+ } else if (isSequenceBlock(el)) {
938
+ markNoteSpacing(el.children);
939
+ if (el.elseIfBranches) {
940
+ for (const branch of el.elseIfBranches) {
941
+ markNoteSpacing(branch.children);
942
+ }
943
+ }
944
+ markNoteSpacing(el.elseChildren);
945
+ }
946
+ }
947
+ };
948
+ if (elements && elements.length > 0) {
949
+ markNoteSpacing(elements);
950
+ }
951
+
918
952
  // --- Section-aware Y layout ---
919
953
  // Sections get their own Y positions computed from content above them (not anchored
920
954
  // to messages below). This ensures toggling collapse/expand doesn't move the divider.
@@ -1865,14 +1899,15 @@ export function renderSequenceDiagram(
1865
1899
  const noteX = isRight
1866
1900
  ? px + ACTIVATION_WIDTH + NOTE_GAP
1867
1901
  : px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
1868
- const noteTopY = noteY - noteH / 2;
1902
+ const noteTopY = noteY + NOTE_OFFSET_BELOW;
1869
1903
 
1870
1904
  // Wrap in <g> with data attributes for toggle support
1871
1905
  const noteG = svg
1872
1906
  .append('g')
1873
1907
  .attr('class', 'note')
1874
1908
  .attr('data-note-toggle', '')
1875
- .attr('data-line-number', String(el.lineNumber));
1909
+ .attr('data-line-number', String(el.lineNumber))
1910
+ .attr('data-line-end', String(el.endLineNumber));
1876
1911
 
1877
1912
  // Folded-corner path
1878
1913
  noteG
@@ -1909,35 +1944,32 @@ export function renderSequenceDiagram(
1909
1944
  .attr('stroke-width', 0.75)
1910
1945
  .attr('class', 'note-fold');
1911
1946
 
1912
- // Dashed connector to lifeline
1913
- const connectorNoteX = isRight ? noteX : noteX + noteW;
1914
- const connectorLifeX = isRight
1915
- ? px + ACTIVATION_WIDTH / 2
1916
- : px - ACTIVATION_WIDTH / 2;
1917
- noteG
1918
- .append('line')
1919
- .attr('x1', connectorNoteX)
1920
- .attr('y1', noteY)
1921
- .attr('x2', connectorLifeX)
1922
- .attr('y2', noteY)
1923
- .attr('stroke', palette.textMuted)
1924
- .attr('stroke-width', 0.75)
1925
- .attr('stroke-dasharray', '3 2')
1926
- .attr('class', 'note-connector');
1927
-
1928
1947
  // Render text with inline markdown
1929
1948
  wrappedLines.forEach((line, li) => {
1930
1949
  const textY =
1931
1950
  noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
1951
+ const isBullet = line.startsWith('- ');
1952
+ const bulletIndent = isBullet ? 10 : 0;
1953
+ const displayLine = isBullet ? line.slice(2) : line;
1932
1954
  const textEl = noteG
1933
1955
  .append('text')
1934
- .attr('x', noteX + NOTE_PAD_H)
1956
+ .attr('x', noteX + NOTE_PAD_H + bulletIndent)
1935
1957
  .attr('y', textY)
1936
1958
  .attr('fill', palette.text)
1937
1959
  .attr('font-size', NOTE_FONT_SIZE)
1938
1960
  .attr('class', 'note-text');
1939
1961
 
1940
- const spans = parseInlineMarkdown(line);
1962
+ if (isBullet) {
1963
+ noteG
1964
+ .append('text')
1965
+ .attr('x', noteX + NOTE_PAD_H)
1966
+ .attr('y', textY)
1967
+ .attr('fill', palette.text)
1968
+ .attr('font-size', NOTE_FONT_SIZE)
1969
+ .text('\u2022');
1970
+ }
1971
+
1972
+ const spans = parseInlineMarkdown(displayLine);
1941
1973
  for (const span of spans) {
1942
1974
  if (span.href) {
1943
1975
  const a = textEl