Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions Sources/BeautifulMermaid/Layout/SequenceLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
6 changes: 5 additions & 1 deletion Sources/BeautifulMermaid/Parser/SequenceParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -410,4 +410,8 @@ public struct SequenceParser {
afterIndex: afterIndex
)
}

private func normalizeHtmlBreaks(in text: String) -> String {
text.replacingOccurrences(of: #"(?i)<br\s*/?>"#, with: "\n", options: .regularExpression)
}
}
10 changes: 9 additions & 1 deletion Sources/BeautifulMermaid/Render/LabelRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions Sources/BeautifulMermaid/Render/SequenceRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Expand Down
38 changes: 38 additions & 0 deletions Tests/BeautifulMermaidTests/SequenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<br/>line 2<BR>line 3<br />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("<br"))
}

func testImplicitParticipants() {
let source = """
sequenceDiagram
Expand Down Expand Up @@ -383,6 +397,30 @@ final class SequenceTests: XCTestCase {
XCTAssertTrue(positioned.height > 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<br/>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() {
Expand Down