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() {