diff --git a/Sources/MarkdownUI/DSL/Inlines/SoftBreak.swift b/Sources/MarkdownUI/DSL/Inlines/SoftBreak.swift index b3dbccea..95551353 100644 --- a/Sources/MarkdownUI/DSL/Inlines/SoftBreak.swift +++ b/Sources/MarkdownUI/DSL/Inlines/SoftBreak.swift @@ -11,3 +11,13 @@ public struct SoftBreak: InlineContentProtocol { .init(inlines: [.softBreak]) } } + +extension SoftBreak { + public enum Mode { + /// Treat a soft break as a space + case space + + /// Treat a soft break as a line break + case lineBreak + } +} diff --git a/Sources/MarkdownUI/Renderer/AttributedStringInlineRenderer.swift b/Sources/MarkdownUI/Renderer/AttributedStringInlineRenderer.swift index a6d2dced..b7a1bcde 100644 --- a/Sources/MarkdownUI/Renderer/AttributedStringInlineRenderer.swift +++ b/Sources/MarkdownUI/Renderer/AttributedStringInlineRenderer.swift @@ -4,11 +4,13 @@ extension InlineNode { func renderAttributedString( baseURL: URL?, textStyles: InlineTextStyles, + softBreakMode: SoftBreak.Mode, attributes: AttributeContainer ) -> AttributedString { var renderer = AttributedStringInlineRenderer( baseURL: baseURL, textStyles: textStyles, + softBreakMode: softBreakMode, attributes: attributes ) renderer.render(self) @@ -21,12 +23,19 @@ private struct AttributedStringInlineRenderer { private let baseURL: URL? private let textStyles: InlineTextStyles + private let softBreakMode: SoftBreak.Mode private var attributes: AttributeContainer private var shouldSkipNextWhitespace = false - init(baseURL: URL?, textStyles: InlineTextStyles, attributes: AttributeContainer) { + init( + baseURL: URL?, + textStyles: InlineTextStyles, + softBreakMode: SoftBreak.Mode, + attributes: AttributeContainer + ) { self.baseURL = baseURL self.textStyles = textStyles + self.softBreakMode = softBreakMode self.attributes = attributes } @@ -67,10 +76,13 @@ private struct AttributedStringInlineRenderer { } private mutating func renderSoftBreak() { - if self.shouldSkipNextWhitespace { + switch softBreakMode { + case .space where self.shouldSkipNextWhitespace: self.shouldSkipNextWhitespace = false - } else { + case .space: self.result += .init(" ", attributes: self.attributes) + case .lineBreak: + self.renderLineBreak() } } diff --git a/Sources/MarkdownUI/Renderer/TextInlineRenderer.swift b/Sources/MarkdownUI/Renderer/TextInlineRenderer.swift index 50ae3657..e3f86673 100644 --- a/Sources/MarkdownUI/Renderer/TextInlineRenderer.swift +++ b/Sources/MarkdownUI/Renderer/TextInlineRenderer.swift @@ -5,12 +5,14 @@ extension Sequence where Element == InlineNode { baseURL: URL?, textStyles: InlineTextStyles, images: [String: Image], + softBreakMode: SoftBreak.Mode, attributes: AttributeContainer ) -> Text { var renderer = TextInlineRenderer( baseURL: baseURL, textStyles: textStyles, images: images, + softBreakMode: softBreakMode, attributes: attributes ) renderer.render(self) @@ -24,6 +26,7 @@ private struct TextInlineRenderer { private let baseURL: URL? private let textStyles: InlineTextStyles private let images: [String: Image] + private let softBreakMode: SoftBreak.Mode private let attributes: AttributeContainer private var shouldSkipNextWhitespace = false @@ -31,11 +34,13 @@ private struct TextInlineRenderer { baseURL: URL?, textStyles: InlineTextStyles, images: [String: Image], + softBreakMode: SoftBreak.Mode, attributes: AttributeContainer ) { self.baseURL = baseURL self.textStyles = textStyles self.images = images + self.softBreakMode = softBreakMode self.attributes = attributes } @@ -72,10 +77,14 @@ private struct TextInlineRenderer { } private mutating func renderSoftBreak() { - if self.shouldSkipNextWhitespace { + switch self.softBreakMode { + case .space where self.shouldSkipNextWhitespace: self.shouldSkipNextWhitespace = false - } else { + case .space: self.defaultRender(.softBreak) + case .lineBreak: + self.shouldSkipNextWhitespace = true + self.defaultRender(.lineBreak) } } @@ -104,6 +113,7 @@ private struct TextInlineRenderer { inline.renderAttributedString( baseURL: self.baseURL, textStyles: self.textStyles, + softBreakMode: self.softBreakMode, attributes: self.attributes ) ) diff --git a/Sources/MarkdownUI/Views/Environment/Environment+SoftBreakMode.swift b/Sources/MarkdownUI/Views/Environment/Environment+SoftBreakMode.swift new file mode 100644 index 00000000..52e24687 --- /dev/null +++ b/Sources/MarkdownUI/Views/Environment/Environment+SoftBreakMode.swift @@ -0,0 +1,23 @@ +import SwiftUI + +extension View { + /// Sets the soft break mode for inline texts in a view hierarchy. + /// + /// - parameter softBreakMode: If set to `space`, treats all soft breaks as spaces, keeping sentences whole. If set to `lineBreak`, treats soft breaks as full line breaks + /// + /// - Returns: A view that uses the specified soft break mode for itself and its child views. + public func markdownSoftBreakMode(_ softBreakMode: SoftBreak.Mode) -> some View { + self.environment(\.softBreakMode, softBreakMode) + } +} + +extension EnvironmentValues { + var softBreakMode: SoftBreak.Mode { + get { self[SoftBreakModeKey.self] } + set { self[SoftBreakModeKey.self] = newValue } + } +} + +private struct SoftBreakModeKey: EnvironmentKey { + static let defaultValue: SoftBreak.Mode = .space +} diff --git a/Sources/MarkdownUI/Views/Inlines/InlineText.swift b/Sources/MarkdownUI/Views/Inlines/InlineText.swift index c328a7d3..9da39404 100644 --- a/Sources/MarkdownUI/Views/Inlines/InlineText.swift +++ b/Sources/MarkdownUI/Views/Inlines/InlineText.swift @@ -4,6 +4,7 @@ struct InlineText: View { @Environment(\.inlineImageProvider) private var inlineImageProvider @Environment(\.baseURL) private var baseURL @Environment(\.imageBaseURL) private var imageBaseURL + @Environment(\.softBreakMode) private var softBreakMode @Environment(\.theme) private var theme @State private var inlineImages: [String: Image] = [:] @@ -26,6 +27,7 @@ struct InlineText: View { link: self.theme.link ), images: self.inlineImages, + softBreakMode: self.softBreakMode, attributes: attributes ) } diff --git a/Tests/MarkdownUITests/MarkdownTests.swift b/Tests/MarkdownUITests/MarkdownTests.swift index 0d8ed1a1..488d991c 100644 --- a/Tests/MarkdownUITests/MarkdownTests.swift +++ b/Tests/MarkdownUITests/MarkdownTests.swift @@ -300,5 +300,47 @@ assertSnapshot(of: view, as: .image(layout: layout)) } + + func testSoftBreakModeSpace() { + let view = Markdown { + #""" + # This is a heading + + Item 1 + Item 2 + Item 3 + Item 4 + + I would **very much** like to write + A long paragraph that spans _multiple lines_ + But should ~~render differently~~ based on + soft break mode + """# + } + .markdownSoftBreakMode(.space) + + assertSnapshot(of: view, as: .image(layout: layout)) + } + + func testSoftBreakModeLineBreak() { + let view = Markdown { + #""" + # This is a heading + + Item 1 + Item 2 + Item 3 + Item 4 + + I would **very much** like to write + A long paragraph that spans _multiple lines_ + But should ~~render differently~~ based on + soft break mode + """# + } + .markdownSoftBreakMode(.lineBreak) + + assertSnapshot(of: view, as: .image(layout: layout)) + } } #endif diff --git a/Tests/MarkdownUITests/__Snapshots__/MarkdownTests/testSoftBreakModeLineBreak.1.png b/Tests/MarkdownUITests/__Snapshots__/MarkdownTests/testSoftBreakModeLineBreak.1.png new file mode 100644 index 00000000..ea920e58 Binary files /dev/null and b/Tests/MarkdownUITests/__Snapshots__/MarkdownTests/testSoftBreakModeLineBreak.1.png differ diff --git a/Tests/MarkdownUITests/__Snapshots__/MarkdownTests/testSoftBreakModeSpace.1.png b/Tests/MarkdownUITests/__Snapshots__/MarkdownTests/testSoftBreakModeSpace.1.png new file mode 100644 index 00000000..6f9e62b6 Binary files /dev/null and b/Tests/MarkdownUITests/__Snapshots__/MarkdownTests/testSoftBreakModeSpace.1.png differ