diff --git a/Sources/BeautifulMermaid/Layout/SequenceLayout.swift b/Sources/BeautifulMermaid/Layout/SequenceLayout.swift
index 7f34841..58c2966 100644
--- a/Sources/BeautifulMermaid/Layout/SequenceLayout.swift
+++ b/Sources/BeautifulMermaid/Layout/SequenceLayout.swift
@@ -379,11 +379,16 @@ public struct SequenceLayout: GraphLayoutAlgorithm {
// Port of: lines 263-293
var notes: [PositionedSequenceNote] = []
for note in diagram.notes {
+ let noteMetrics = measureMultilineText(
+ note.text,
+ fontSize: RenderConfig.shared.fontSizeEdgeLabel,
+ fontWeight: RenderConfig.shared.fontWeightEdgeLabel
+ )
let noteW = max(
c.noteWidth,
- measureText(note.text, fontSize: RenderConfig.shared.fontSizeEdgeLabel, fontWeight: RenderConfig.shared.fontWeightEdgeLabel) + c.notePadding * 2
+ noteMetrics.width + c.notePadding * 2
)
- let noteH = RenderConfig.shared.fontSizeEdgeLabel + c.notePadding * 2
+ let noteH = noteMetrics.height + c.notePadding * 2
// Position based on the message after which it appears
let refMsg = note.afterIndex >= 0 && note.afterIndex < messages.count ? messages[note.afterIndex] : nil
@@ -538,9 +543,16 @@ public struct SequenceLayout: GraphLayoutAlgorithm {
return config.estimateTextWidth(text, fontSize: fontSize, fontWeight: fontWeight)
}
- private func measureNoteHeight(_ text: String) -> CGFloat {
- let lineCount = max(1, text.components(separatedBy: "\n").count)
- let lineHeight: CGFloat = 16
- return CGFloat(lineCount) * lineHeight + SequenceConstants.notePadding * 2
+ private func measureMultilineText(_ text: String,
+ fontSize: CGFloat,
+ fontWeight: Int,
+ lineSpacing: CGFloat = 4) -> (width: CGFloat, height: CGFloat) {
+ let lines = text.components(separatedBy: "\n")
+ let nonEmptyLines = lines.isEmpty ? [""] : lines
+ let maxWidth = nonEmptyLines.map { measureText($0, fontSize: fontSize, fontWeight: fontWeight) }.max() ?? 0
+ let lineCount = max(1, nonEmptyLines.count)
+ let lineHeight = ceil(fontSize)
+ let height = CGFloat(lineCount) * lineHeight + CGFloat(max(0, lineCount - 1)) * lineSpacing
+ return (maxWidth, height)
}
}
diff --git a/Sources/BeautifulMermaid/Parser/SequenceParser.swift b/Sources/BeautifulMermaid/Parser/SequenceParser.swift
index 359a16e..3231a8c 100644
--- a/Sources/BeautifulMermaid/Parser/SequenceParser.swift
+++ b/Sources/BeautifulMermaid/Parser/SequenceParser.swift
@@ -401,7 +401,7 @@ public struct SequenceParser {
return nil
}
- let text = textPart.trimmingCharacters(in: .whitespaces)
+ let text = normalizeHtmlBreaks(in: textPart).trimmingCharacters(in: .whitespaces)
return SequenceNote(
actorIds: actorIds,
@@ -410,4 +410,8 @@ public struct SequenceParser {
afterIndex: afterIndex
)
}
+
+ private func normalizeHtmlBreaks(in text: String) -> String {
+ text.replacingOccurrences(of: #"(?i)
"#, with: "\n", options: .regularExpression)
+ }
}
diff --git a/Sources/BeautifulMermaid/Render/LabelRenderer.swift b/Sources/BeautifulMermaid/Render/LabelRenderer.swift
index 03d7ff3..2d4de3d 100644
--- a/Sources/BeautifulMermaid/Render/LabelRenderer.swift
+++ b/Sources/BeautifulMermaid/Render/LabelRenderer.swift
@@ -125,13 +125,21 @@ public class LabelRenderer {
context: CGContext,
color: BMColor,
font: BMFont,
+ alignment: TextAlignment = .center,
lineSpacing: CGFloat = 4
) {
guard !text.isEmpty else { return }
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
- paragraphStyle.alignment = .center
+ switch alignment {
+ case .left:
+ paragraphStyle.alignment = .left
+ case .center:
+ paragraphStyle.alignment = .center
+ case .right:
+ paragraphStyle.alignment = .right
+ }
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
diff --git a/Sources/BeautifulMermaid/Render/SequenceRenderer.swift b/Sources/BeautifulMermaid/Render/SequenceRenderer.swift
index 26bdecc..770fe23 100644
--- a/Sources/BeautifulMermaid/Render/SequenceRenderer.swift
+++ b/Sources/BeautifulMermaid/Render/SequenceRenderer.swift
@@ -315,14 +315,13 @@ public class SequenceRenderer {
if !note.text.isEmpty {
let textRect = bounds.insetBy(dx: SequenceConstants.notePadding, dy: SequenceConstants.notePadding)
let noteFont = RenderConfig.shared.edgeLabelFont()
- labelRenderer.drawText(
+ labelRenderer.drawMultilineText(
note.text,
in: textRect,
context: context,
color: theme.effectiveMuted(),
font: noteFont,
- alignment: .left,
- verticalAlignment: .center
+ alignment: .left
)
}
}
diff --git a/Tests/BeautifulMermaidTests/SequenceTests.swift b/Tests/BeautifulMermaidTests/SequenceTests.swift
index affb4c3..030f51a 100644
--- a/Tests/BeautifulMermaidTests/SequenceTests.swift
+++ b/Tests/BeautifulMermaidTests/SequenceTests.swift
@@ -285,6 +285,20 @@ final class SequenceTests: XCTestCase {
XCTAssertEqual(diagram.notes[0].actorIds, ["A", "B"])
}
+ func testNoteHtmlBreaksNormalizeToNewlines() {
+ let source = """
+ sequenceDiagram
+ Note right of B: line 1
line 2
line 3
line 4
+ A->>B: Hello
+ """
+
+ let diagram = parseSequenceDiagram(source)
+
+ XCTAssertEqual(diagram.notes.count, 1)
+ XCTAssertEqual(diagram.notes[0].text, "line 1\nline 2\nline 3\nline 4")
+ XCTAssertFalse(diagram.notes[0].text.contains("
0)
}
+ func testMultilineNoteIncreasesNoteHeight() {
+ let singleSource = """
+ sequenceDiagram
+ participant A
+ participant B
+ Note right of B: Single line
+ A->>B: Hello
+ """
+ let multiSource = """
+ sequenceDiagram
+ participant A
+ participant B
+ Note right of B: Line 1
Line 2
+ A->>B: Hello
+ """
+
+ let single = layoutSequenceDiagram(parseSequenceDiagram(singleSource))
+ let multi = layoutSequenceDiagram(parseSequenceDiagram(multiSource))
+
+ XCTAssertEqual(single.notes.count, 1)
+ XCTAssertEqual(multi.notes.count, 1)
+ XCTAssertGreaterThan(multi.notes[0].bounds.height, single.notes[0].bounds.height)
+ }
+
// MARK: - Integration Tests
func testFullDiagram() {