diff --git a/Examples/Demo/Demo/HeadingsView.swift b/Examples/Demo/Demo/HeadingsView.swift index fd97066d..9447c3e1 100644 --- a/Examples/Demo/Demo/HeadingsView.swift +++ b/Examples/Demo/Demo/HeadingsView.swift @@ -25,8 +25,8 @@ struct HeadingsView: View { Section("Customization Example") { Markdown("# One Big Header") } - .markdownBlockStyle(\.heading1) { label in - label + .markdownBlockStyle(\.heading1) { configuration in + configuration.label .markdownMargin(top: .em(1), bottom: .em(1)) .markdownTextStyle { FontFamily(.custom("Trebuchet MS")) diff --git a/Examples/Demo/Demo/ImagesView.swift b/Examples/Demo/Demo/ImagesView.swift index 2d03e484..3b8fab36 100644 --- a/Examples/Demo/Demo/ImagesView.swift +++ b/Examples/Demo/Demo/ImagesView.swift @@ -43,8 +43,8 @@ struct ImagesView: View { Section("Customization Example") { Markdown(self.content) } - .markdownBlockStyle(\.image) { label in - label + .markdownBlockStyle(\.image) { configuration in + configuration.label .clipShape(RoundedRectangle(cornerRadius: 8)) .shadow(radius: 8, y: 8) .markdownMargin(top: .em(1.6), bottom: .em(1.6)) diff --git a/Examples/Demo/Demo/QuotesView.swift b/Examples/Demo/Demo/QuotesView.swift index c0f2082e..6f6abc70 100644 --- a/Examples/Demo/Demo/QuotesView.swift +++ b/Examples/Demo/Demo/QuotesView.swift @@ -18,8 +18,8 @@ struct QuotesView: View { Section("Customization Example") { Markdown(self.content) } - .markdownBlockStyle(\.blockquote) { label in - label + .markdownBlockStyle(\.blockquote) { configuration in + configuration.label .padding() .markdownTextStyle { FontCapsVariant(.lowercaseSmallCaps) diff --git a/README.md b/README.md index d9cc368f..5e1da356 100644 --- a/README.md +++ b/README.md @@ -226,13 +226,14 @@ extension Theme { ForegroundColor(.purple) } // More text styles... - .paragraph { label in - label + .paragraph { configuration in + configuration.label .relativeLineSpacing(.em(0.25)) .markdownMargin(top: 0, bottom: 16) } - .listItem { label in - label.markdownMargin(top: .em(0.25)) + .listItem { configuration in + configuration.label + .markdownMargin(top: .em(0.25)) } // More block styles... } diff --git a/Sources/MarkdownUI/DSL/Blocks/MarkdownContent.swift b/Sources/MarkdownUI/DSL/Blocks/MarkdownContent.swift index 07da9cc9..65a60068 100644 --- a/Sources/MarkdownUI/DSL/Blocks/MarkdownContent.swift +++ b/Sources/MarkdownUI/DSL/Blocks/MarkdownContent.swift @@ -60,6 +60,16 @@ public protocol MarkdownContentProtocol { /// } /// ``` public struct MarkdownContent: Equatable, MarkdownContentProtocol { + /// Returns a Markdown content value with the sum of the contents of all the container blocks + /// present in this content. + /// + /// You can use this property to access the contents of a blockquote or a list. Returns `nil` if + /// there are no container blocks. + public var childContent: MarkdownContent? { + let children = self.blocks.map(\.children).flatMap { $0 } + return children.isEmpty ? nil : .init(blocks: children) + } + public var _markdownContent: MarkdownContent { self } let blocks: [BlockNode] @@ -67,6 +77,10 @@ public struct MarkdownContent: Equatable, MarkdownContentProtocol { self.blocks = blocks } + init(block: BlockNode) { + self.init(blocks: [block]) + } + init(_ components: [MarkdownContentProtocol]) { self.init(blocks: components.map(\._markdownContent).flatMap(\.blocks)) } @@ -88,4 +102,10 @@ public struct MarkdownContent: Equatable, MarkdownContentProtocol { let result = self.blocks.renderMarkdown() return result.hasSuffix("\n") ? String(result.dropLast()) : result } + + /// Renders this Markdown content value as plain text. + public func renderPlainText() -> String { + let result = self.blocks.renderPlainText() + return result.hasSuffix("\n") ? String(result.dropLast()) : result + } } diff --git a/Sources/MarkdownUI/Documentation.docc/Articles/GettingStarted.md b/Sources/MarkdownUI/Documentation.docc/Articles/GettingStarted.md index 35657e69..d50a5858 100644 --- a/Sources/MarkdownUI/Documentation.docc/Articles/GettingStarted.md +++ b/Sources/MarkdownUI/Documentation.docc/Articles/GettingStarted.md @@ -183,13 +183,14 @@ extension Theme { ForegroundColor(.purple) } // More text styles... - .paragraph { label in - label + .paragraph { configuration in + configuration.label .relativeLineSpacing(.em(0.25)) .markdownMargin(top: 0, bottom: 16) } - .listItem { label in - label.markdownMargin(top: .em(0.25)) + .listItem { configuration in + configuration.label + .markdownMargin(top: .em(0.25)) } // More block styles... } diff --git a/Sources/MarkdownUI/Parser/BlockNode.swift b/Sources/MarkdownUI/Parser/BlockNode.swift index 9fb93aa7..4686106d 100644 --- a/Sources/MarkdownUI/Parser/BlockNode.swift +++ b/Sources/MarkdownUI/Parser/BlockNode.swift @@ -14,6 +14,21 @@ enum BlockNode: Hashable { } extension BlockNode { + var children: [BlockNode] { + switch self { + case .blockquote(let children): + return children + case .bulletedList(_, let items): + return items.map(\.children).flatMap { $0 } + case .numberedList(_, _, let items): + return items.map(\.children).flatMap { $0 } + case .taskList(_, let items): + return items.map(\.children).flatMap { $0 } + default: + return [] + } + } + var isParagraph: Bool { guard case .paragraph = self else { return false } return true diff --git a/Sources/MarkdownUI/Parser/MarkdownParser.swift b/Sources/MarkdownUI/Parser/MarkdownParser.swift index 059d0a38..45c8165b 100644 --- a/Sources/MarkdownUI/Parser/MarkdownParser.swift +++ b/Sources/MarkdownUI/Parser/MarkdownParser.swift @@ -14,6 +14,12 @@ extension Array where Element == BlockNode { String(cString: cmark_render_commonmark(document, CMARK_OPT_DEFAULT, 0)) } ?? "" } + + func renderPlainText() -> String { + UnsafeNode.makeDocument(self) { document in + String(cString: cmark_render_plaintext(document, CMARK_OPT_DEFAULT, 0)) + } ?? "" + } } extension BlockNode { diff --git a/Sources/MarkdownUI/Theme/BlockStyle/BlockConfiguration.swift b/Sources/MarkdownUI/Theme/BlockStyle/BlockConfiguration.swift index 8c5f00d9..0ee2a56d 100644 --- a/Sources/MarkdownUI/Theme/BlockStyle/BlockConfiguration.swift +++ b/Sources/MarkdownUI/Theme/BlockStyle/BlockConfiguration.swift @@ -15,23 +15,14 @@ public struct BlockConfiguration { public let body: AnyView } - /// The Markdown block content. + /// The Markdown block view. public let label: Label -} -extension BlockStyle where Configuration == BlockConfiguration { - /// Creates a block style that customizes a block by applying the given body. - /// - Parameter body: A view builder that returns the customized block. - public init( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body - ) { - self.init { configuration in - body(configuration.label) - } - } - - /// Creates a block style that returns the block content without applying any customization. - public init() { - self.init { $0 } - } + /// The content of the Markdown block. + /// + /// This property provides access to different representations of the block content. + /// For example, you can use ``MarkdownContent/renderMarkdown()`` + /// to get the Markdown formatted text or ``MarkdownContent/renderPlainText()`` + /// to get the plain text of the block content. + public let content: MarkdownContent } diff --git a/Sources/MarkdownUI/Theme/BlockStyle/TableCellConfiguration.swift b/Sources/MarkdownUI/Theme/BlockStyle/TableCellConfiguration.swift index 59cd6c52..64440316 100644 --- a/Sources/MarkdownUI/Theme/BlockStyle/TableCellConfiguration.swift +++ b/Sources/MarkdownUI/Theme/BlockStyle/TableCellConfiguration.swift @@ -20,6 +20,9 @@ public struct TableCellConfiguration { /// The table cell column index. public let column: Int - /// The table cell content. + /// The table cell view. public let label: Label + + /// The table cell content. + public let content: MarkdownContent } diff --git a/Sources/MarkdownUI/Theme/Theme+Basic.swift b/Sources/MarkdownUI/Theme/Theme+Basic.swift index 1aaf15f3..68071135 100644 --- a/Sources/MarkdownUI/Theme/Theme+Basic.swift +++ b/Sources/MarkdownUI/Theme/Theme+Basic.swift @@ -19,61 +19,61 @@ extension Theme { FontFamilyVariant(.monospaced) FontSize(.em(0.94)) } - .heading1 { label in - label + .heading1 { configuration in + configuration.label .markdownMargin(top: .rem(1.5), bottom: .rem(1)) .markdownTextStyle { FontWeight(.semibold) FontSize(.em(2)) } } - .heading2 { label in - label + .heading2 { configuration in + configuration.label .markdownMargin(top: .rem(1.5), bottom: .rem(1)) .markdownTextStyle { FontWeight(.semibold) FontSize(.em(1.5)) } } - .heading3 { label in - label + .heading3 { configuration in + configuration.label .markdownMargin(top: .rem(1.5), bottom: .rem(1)) .markdownTextStyle { FontWeight(.semibold) FontSize(.em(1.17)) } } - .heading4 { label in - label + .heading4 { configuration in + configuration.label .markdownMargin(top: .rem(1.5), bottom: .rem(1)) .markdownTextStyle { FontWeight(.semibold) FontSize(.em(1)) } } - .heading5 { label in - label + .heading5 { configuration in + configuration.label .markdownMargin(top: .rem(1.5), bottom: .rem(1)) .markdownTextStyle { FontWeight(.semibold) FontSize(.em(0.83)) } } - .heading6 { label in - label + .heading6 { configuration in + configuration.label .markdownMargin(top: .rem(1.5), bottom: .rem(1)) .markdownTextStyle { FontWeight(.semibold) FontSize(.em(0.67)) } } - .paragraph { label in - label + .paragraph { configuration in + configuration.label .relativeLineSpacing(.em(0.15)) .markdownMargin(top: .zero, bottom: .em(1)) } - .blockquote { label in - label + .blockquote { configuration in + configuration.label .markdownTextStyle { FontStyle(.italic) } @@ -92,8 +92,9 @@ extension Theme { } .markdownMargin(top: .zero, bottom: .em(1)) } - .table { label in - label.markdownMargin(top: .zero, bottom: .em(1)) + .table { configuration in + configuration.label + .markdownMargin(top: .zero, bottom: .em(1)) } .tableCell { configuration in configuration.label diff --git a/Sources/MarkdownUI/Theme/Theme+DocC.swift b/Sources/MarkdownUI/Theme/Theme+DocC.swift index ccee0060..5d7f61c4 100644 --- a/Sources/MarkdownUI/Theme/Theme+DocC.swift +++ b/Sources/MarkdownUI/Theme/Theme+DocC.swift @@ -21,16 +21,16 @@ extension Theme { .link { ForegroundColor(.link) } - .heading1 { label in - label + .heading1 { configuration in + configuration.label .markdownMargin(top: .em(0.8), bottom: .zero) .markdownTextStyle { FontWeight(.semibold) FontSize(.em(2)) } } - .heading2 { label in - label + .heading2 { configuration in + configuration.label .relativeLineSpacing(.em(0.0625)) .markdownMargin(top: .em(1.6), bottom: .zero) .markdownTextStyle { @@ -38,8 +38,8 @@ extension Theme { FontSize(.em(1.88235)) } } - .heading3 { label in - label + .heading3 { configuration in + configuration.label .relativeLineSpacing(.em(0.07143)) .markdownMargin(top: .em(1.6), bottom: .zero) .markdownTextStyle { @@ -47,8 +47,8 @@ extension Theme { FontSize(.em(1.64706)) } } - .heading4 { label in - label + .heading4 { configuration in + configuration.label .relativeLineSpacing(.em(0.083335)) .markdownMargin(top: .em(1.6), bottom: .zero) .markdownTextStyle { @@ -56,8 +56,8 @@ extension Theme { FontSize(.em(1.41176)) } } - .heading5 { label in - label + .heading5 { configuration in + configuration.label .relativeLineSpacing(.em(0.09091)) .markdownMargin(top: .em(1.6), bottom: .zero) .markdownTextStyle { @@ -65,21 +65,21 @@ extension Theme { FontSize(.em(1.29412)) } } - .heading6 { label in - label + .heading6 { configuration in + configuration.label .relativeLineSpacing(.em(0.235295)) .markdownMargin(top: .em(1.6), bottom: .zero) .markdownTextStyle { FontWeight(.semibold) } } - .paragraph { label in - label + .paragraph { configuration in + configuration.label .relativeLineSpacing(.em(0.235295)) .markdownMargin(top: .em(0.8), bottom: .zero) } - .blockquote { label in - label + .blockquote { configuration in + configuration.label .relativePadding(length: .rem(0.94118)) .frame(maxWidth: .infinity, alignment: .leading) .background { @@ -107,21 +107,22 @@ extension Theme { .clipShape(.container) .markdownMargin(top: .em(0.8), bottom: .zero) } - .image { label in - label + .image { configuration in + configuration.label .frame(maxWidth: .infinity) .markdownMargin(top: .em(1.6), bottom: .em(1.6)) } - .listItem { label in - label.markdownMargin(top: .em(0.8)) + .listItem { configuration in + configuration.label + .markdownMargin(top: .em(0.8)) } .taskListMarker { _ in // DocC renders task lists as bullet lists ListBullet.disc .relativeFrame(minWidth: .em(1.5), alignment: .trailing) } - .table { label in - label + .table { configuration in + configuration.label .markdownTableBorderStyle(.init(.horizontalBorders, color: .grid)) .markdownMargin(top: .em(1.6), bottom: .zero) } diff --git a/Sources/MarkdownUI/Theme/Theme+GitHub.swift b/Sources/MarkdownUI/Theme/Theme+GitHub.swift index 1a073f9c..9be9b978 100644 --- a/Sources/MarkdownUI/Theme/Theme+GitHub.swift +++ b/Sources/MarkdownUI/Theme/Theme+GitHub.swift @@ -31,9 +31,9 @@ extension Theme { .link { ForegroundColor(.link) } - .heading1 { label in + .heading1 { configuration in VStack(alignment: .leading, spacing: 0) { - label + configuration.label .relativePadding(.bottom, length: .em(0.3)) .relativeLineSpacing(.em(0.125)) .markdownMargin(top: 24, bottom: 16) @@ -44,9 +44,9 @@ extension Theme { Divider().overlay(Color.divider) } } - .heading2 { label in + .heading2 { configuration in VStack(alignment: .leading, spacing: 0) { - label + configuration.label .relativePadding(.bottom, length: .em(0.3)) .relativeLineSpacing(.em(0.125)) .markdownMargin(top: 24, bottom: 16) @@ -57,8 +57,8 @@ extension Theme { Divider().overlay(Color.divider) } } - .heading3 { label in - label + .heading3 { configuration in + configuration.label .relativeLineSpacing(.em(0.125)) .markdownMargin(top: 24, bottom: 16) .markdownTextStyle { @@ -66,16 +66,16 @@ extension Theme { FontSize(.em(1.25)) } } - .heading4 { label in - label + .heading4 { configuration in + configuration.label .relativeLineSpacing(.em(0.125)) .markdownMargin(top: 24, bottom: 16) .markdownTextStyle { FontWeight(.semibold) } } - .heading5 { label in - label + .heading5 { configuration in + configuration.label .relativeLineSpacing(.em(0.125)) .markdownMargin(top: 24, bottom: 16) .markdownTextStyle { @@ -83,8 +83,8 @@ extension Theme { FontSize(.em(0.875)) } } - .heading6 { label in - label + .heading6 { configuration in + configuration.label .relativeLineSpacing(.em(0.125)) .markdownMargin(top: 24, bottom: 16) .markdownTextStyle { @@ -93,17 +93,17 @@ extension Theme { ForegroundColor(.tertiaryText) } } - .paragraph { label in - label + .paragraph { configuration in + configuration.label .relativeLineSpacing(.em(0.25)) .markdownMargin(top: 0, bottom: 16) } - .blockquote { label in + .blockquote { configuration in HStack(spacing: 0) { RoundedRectangle(cornerRadius: 6) .fill(Color.border) .relativeFrame(width: .em(0.2)) - label + configuration.label .markdownTextStyle { ForegroundColor(.secondaryText) } .relativePadding(.horizontal, length: .em(1)) } @@ -123,8 +123,9 @@ extension Theme { .clipShape(RoundedRectangle(cornerRadius: 6)) .markdownMargin(top: 0, bottom: 16) } - .listItem { label in - label.markdownMargin(top: .em(0.25)) + .listItem { configuration in + configuration.label + .markdownMargin(top: .em(0.25)) } .taskListMarker { configuration in Image(systemName: configuration.isCompleted ? "checkmark.square.fill" : "square") @@ -133,8 +134,8 @@ extension Theme { .imageScale(.small) .relativeFrame(minWidth: .em(1.5), alignment: .trailing) } - .table { label in - label + .table { configuration in + configuration.label .markdownTableBorderStyle(.init(color: .border)) .markdownTableBackgroundStyle( .alternatingRows(Color.background, Color.secondaryBackground) diff --git a/Sources/MarkdownUI/Theme/Theme.swift b/Sources/MarkdownUI/Theme/Theme.swift index a4bae55a..b5796066 100644 --- a/Sources/MarkdownUI/Theme/Theme.swift +++ b/Sources/MarkdownUI/Theme/Theme.swift @@ -88,13 +88,14 @@ import SwiftUI /// ForegroundColor(.purple) /// } /// // More text styles... -/// .paragraph { label in -/// label +/// .paragraph { configuration in +/// configuration.label /// .relativeLineSpacing(.em(0.25)) /// .markdownMargin(top: 0, bottom: 16) /// } -/// .listItem { label in -/// label.markdownMargin(top: .em(0.25)) +/// .listItem { configuration in +/// configuration.label +/// .markdownMargin(top: .em(0.25)) /// } /// // More block styles... /// ``` @@ -117,7 +118,10 @@ public struct Theme { /// The link style. public var link: TextStyle = EmptyTextStyle() - var headings = Array(repeating: BlockStyle(), count: 6) + var headings = Array( + repeating: BlockStyle { $0.label }, + count: 6 + ) /// The level 1 heading style. public var heading1: BlockStyle { @@ -156,22 +160,22 @@ public struct Theme { } /// The paragraph style. - public var paragraph = BlockStyle() + public var paragraph = BlockStyle { $0.label } /// The blockquote style. - public var blockquote = BlockStyle() + public var blockquote = BlockStyle { $0.label } /// The code block style. - public var codeBlock: BlockStyle = BlockStyle { $0.label } + public var codeBlock = BlockStyle { $0.label } /// The image style. - public var image = BlockStyle() + public var image = BlockStyle { $0.label } /// The list style. - public var list = BlockStyle() + public var list = BlockStyle { $0.label } /// The list item style. - public var listItem = BlockStyle() + public var listItem = BlockStyle { $0.label } /// The task list marker style. public var taskListMarker = BlockStyle.checkmarkSquare @@ -183,10 +187,10 @@ public struct Theme { public var numberedListMarker = BlockStyle.decimal /// The table style. - public var table = BlockStyle() + public var table = BlockStyle { $0.label } /// The table cell style. - public var tableCell: BlockStyle = BlockStyle { $0.label } + public var tableCell = BlockStyle { $0.label } /// The thematic break style. public var thematicBreak = BlockStyle { Divider() } @@ -249,7 +253,7 @@ extension Theme { /// Adds a level 1 heading style to the theme. /// - Parameter body: A view builder that returns a customized level 1 heading. public func heading1( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ configuration: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.heading1 = .init(body: body) @@ -259,7 +263,7 @@ extension Theme { /// Adds a level 2 heading style to the theme. /// - Parameter body: A view builder that returns a customized level 2 heading. public func heading2( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.heading2 = .init(body: body) @@ -269,7 +273,7 @@ extension Theme { /// Adds a level 3 heading style to the theme. /// - Parameter body: A view builder that returns a customized level 3 heading. public func heading3( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.heading3 = .init(body: body) @@ -279,7 +283,7 @@ extension Theme { /// Adds a level 4 heading style to the theme. /// - Parameter body: A view builder that returns a customized level 4 heading. public func heading4( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.heading4 = .init(body: body) @@ -289,7 +293,7 @@ extension Theme { /// Adds a level 5 heading style to the theme. /// - Parameter body: A view builder that returns a customized level 5 heading. public func heading5( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.heading5 = .init(body: body) @@ -299,7 +303,7 @@ extension Theme { /// Adds a level 6 heading style to the theme. /// - Parameter body: A view builder that returns a customized level 6 heading. public func heading6( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.heading6 = .init(body: body) @@ -309,7 +313,7 @@ extension Theme { /// Adds a paragraph style to the theme. /// - Parameter body: A view builder that returns a customized paragraph. public func paragraph( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.paragraph = .init(body: body) @@ -319,7 +323,7 @@ extension Theme { /// Adds a blockquote style to the theme. /// - Parameter body: A view builder that returns a customized blockquote. public func blockquote( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.blockquote = .init(body: body) @@ -339,7 +343,7 @@ extension Theme { /// Adds an image style to the theme. /// - Parameter body: A view builder that returns a customized image. public func image( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.image = .init(body: body) @@ -349,7 +353,7 @@ extension Theme { /// Adds a list style to the theme. /// - Parameter body: A view builder that returns a customized list. public func list( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.list = .init(body: body) @@ -359,7 +363,7 @@ extension Theme { /// Adds a list item style to the theme. /// - Parameter body: A view builder that returns a customized list item. public func listItem( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.listItem = .init(body: body) @@ -427,7 +431,7 @@ extension Theme { /// Adds a table style to the theme. /// - Parameter body: A view builder that returns a customized table. public func table( - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + @ViewBuilder body: @escaping (_ label: BlockConfiguration) -> Body ) -> Theme { var theme = self theme.table = .init(body: body) diff --git a/Sources/MarkdownUI/Utility/Deprecations.swift b/Sources/MarkdownUI/Utility/Deprecations.swift index 1a17aa07..a0229ca4 100644 --- a/Sources/MarkdownUI/Utility/Deprecations.swift +++ b/Sources/MarkdownUI/Utility/Deprecations.swift @@ -2,11 +2,55 @@ import SwiftUI // MARK: - Deprecated after 2.0.2: +extension BlockStyle where Configuration == BlockConfiguration { + @available( + *, + deprecated, + message: "Use the initializer that takes a closure receiving a 'Configuration' value." + ) + public init( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) { + self.init { configuration in + body(configuration.label) + } + } + + @available( + *, + deprecated, + message: "Use the initializer that takes a closure receiving a 'Configuration' value." + ) + public init() { + self.init { $0 } + } +} + extension View { @available( - *, deprecated, + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a generic 'Configuration' + value. + """ + ) + public func markdownBlockStyle( + _ keyPath: WritableKeyPath>, + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> some View { + self.environment((\EnvironmentValues.theme).appending(path: keyPath), .init(body: body)) + } + + @available( + *, + deprecated, message: - "Use the version of this function that takes a closure receiving a generic 'Configuration' value." + """ + Use the version of this function that takes a closure receiving a generic 'Configuration' + value. + """ ) public func markdownBlockStyle( _ keyPath: WritableKeyPath>, @@ -23,9 +67,149 @@ extension View { extension Theme { @available( - *, deprecated, + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func heading1( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.heading1 = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func heading2( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.heading2 = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func heading3( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.heading3 = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func heading4( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.heading4 = .init(body: body) + return theme + } + + @available( + *, + deprecated, message: - "Use the version of this function that takes a closure receiving a 'CodeBlockConfiguration' value." + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func heading5( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.heading5 = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func heading6( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.heading6 = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func paragraph( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.paragraph = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func blockquote( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.blockquote = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'CodeBlockConfiguration' + value. + """ ) public func codeBlock( @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body @@ -36,6 +220,74 @@ extension Theme { } return theme } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func image( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.image = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func list( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.list = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func listItem( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.listItem = .init(body: body) + return theme + } + + @available( + *, + deprecated, + message: + """ + Use the version of this function that takes a closure receiving a 'BlockConfiguration' + value. + """ + ) + public func table( + @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body + ) -> Theme { + var theme = self + theme.table = .init(body: body) + return theme + } } // MARK: - Unavailable after 1.1.1: diff --git a/Sources/MarkdownUI/Views/Blocks/ApplyBlockStyle.swift b/Sources/MarkdownUI/Views/Blocks/ApplyBlockStyle.swift deleted file mode 100644 index acfa6051..00000000 --- a/Sources/MarkdownUI/Views/Blocks/ApplyBlockStyle.swift +++ /dev/null @@ -1,28 +0,0 @@ -import SwiftUI - -struct ApplyBlockStyle: View { - @Environment private var blockStyle: BlockStyle - - private let configuration: Configuration - - init(_ keyPath: KeyPath>, configuration: Configuration) { - self._blockStyle = Environment((\EnvironmentValues.theme).appending(path: keyPath)) - self.configuration = configuration - } - - var body: some View { - self.blockStyle.makeBody(configuration: self.configuration) - } -} - -extension ApplyBlockStyle where Configuration == BlockConfiguration { - init(_ keyPath: KeyPath>, to label: Label) { - self.init(keyPath, configuration: .init(label: .init(label))) - } -} - -extension ApplyBlockStyle where Configuration == Void { - init(_ keyPath: KeyPath>) { - self.init(keyPath, configuration: ()) - } -} diff --git a/Sources/MarkdownUI/Views/Blocks/BlockNode+View.swift b/Sources/MarkdownUI/Views/Blocks/BlockNode+View.swift index 6ab45fe0..b445dbc4 100644 --- a/Sources/MarkdownUI/Views/Blocks/BlockNode+View.swift +++ b/Sources/MarkdownUI/Views/Blocks/BlockNode+View.swift @@ -4,36 +4,27 @@ extension BlockNode: View { var body: some View { switch self { case .blockquote(let children): - ApplyBlockStyle(\.blockquote, to: BlockSequence(children)) + BlockquoteView(children: children) case .bulletedList(let isTight, let items): - ApplyBlockStyle(\.list, to: BulletedListView(isTight: isTight, items: items)) + BulletedListView(isTight: isTight, items: items) case .numberedList(let isTight, let start, let items): - ApplyBlockStyle(\.list, to: NumberedListView(isTight: isTight, start: start, items: items)) + NumberedListView(isTight: isTight, start: start, items: items) case .taskList(let isTight, let items): - ApplyBlockStyle(\.list, to: TaskListView(isTight: isTight, items: items)) + TaskListView(isTight: isTight, items: items) case .codeBlock(let fenceInfo, let content): CodeBlockView(fenceInfo: fenceInfo, content: content) case .htmlBlock(let content): - ApplyBlockStyle(\.paragraph, to: HTMLBlockView(content: content)) + ParagraphView(content: content) case .paragraph(let content): - if let imageView = ImageView(content) { - ApplyBlockStyle(\.paragraph, to: imageView) - } else if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *), - let imageFlow = ImageFlow(content) - { - ApplyBlockStyle(\.paragraph, to: imageFlow) - } else { - ApplyBlockStyle(\.paragraph, to: InlineText(content)) - } + ParagraphView(content: content) case .heading(let level, let content): - ApplyBlockStyle(\.headings[level - 1], to: InlineText(content)) - .id(content.renderPlainText().kebabCased()) + HeadingView(level: level, content: content) case .table(let columnAlignments, let rows): if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { - ApplyBlockStyle(\.table, to: TableView(columnAlignments: columnAlignments, rows: rows)) + TableView(columnAlignments: columnAlignments, rows: rows) } case .thematicBreak: - ApplyBlockStyle(\.thematicBreak) + ThematicBreakView() } } } diff --git a/Sources/MarkdownUI/Views/Blocks/BlockquoteView.swift b/Sources/MarkdownUI/Views/Blocks/BlockquoteView.swift new file mode 100644 index 00000000..48f1b39e --- /dev/null +++ b/Sources/MarkdownUI/Views/Blocks/BlockquoteView.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct BlockquoteView: View { + @Environment(\.theme.blockquote) private var blockquote + + private let children: [BlockNode] + + init(children: [BlockNode]) { + self.children = children + } + + var body: some View { + self.blockquote.makeBody( + configuration: .init( + label: .init(BlockSequence(self.children)), + content: .init(block: .blockquote(children: self.children)) + ) + ) + } +} diff --git a/Sources/MarkdownUI/Views/Blocks/BulletedListView.swift b/Sources/MarkdownUI/Views/Blocks/BulletedListView.swift index e231356d..9d523724 100644 --- a/Sources/MarkdownUI/Views/Blocks/BulletedListView.swift +++ b/Sources/MarkdownUI/Views/Blocks/BulletedListView.swift @@ -1,6 +1,7 @@ import SwiftUI struct BulletedListView: View { + @Environment(\.theme.list) private var list @Environment(\.theme.bulletedListMarker) private var bulletedListMarker @Environment(\.listLevel) private var listLevel @@ -13,6 +14,15 @@ struct BulletedListView: View { } var body: some View { + self.list.makeBody( + configuration: .init( + label: .init(self.label), + content: .init(block: .bulletedList(isTight: self.isTight, items: self.items)) + ) + ) + } + + private var label: some View { ListItemSequence(items: self.items, markerStyle: self.bulletedListMarker) .environment(\.listLevel, self.listLevel + 1) .environment(\.tightSpacingEnabled, self.isTight) diff --git a/Sources/MarkdownUI/Views/Blocks/CodeBlockView.swift b/Sources/MarkdownUI/Views/Blocks/CodeBlockView.swift index 27cf26c7..86f6d941 100644 --- a/Sources/MarkdownUI/Views/Blocks/CodeBlockView.swift +++ b/Sources/MarkdownUI/Views/Blocks/CodeBlockView.swift @@ -13,7 +13,7 @@ struct CodeBlockView: View { } var body: some View { - codeBlock.makeBody( + self.codeBlock.makeBody( configuration: .init( language: self.fenceInfo, content: self.content, diff --git a/Sources/MarkdownUI/Views/Blocks/HTMLBlockView.swift b/Sources/MarkdownUI/Views/Blocks/HTMLBlockView.swift deleted file mode 100644 index 83504c5f..00000000 --- a/Sources/MarkdownUI/Views/Blocks/HTMLBlockView.swift +++ /dev/null @@ -1,14 +0,0 @@ -import SwiftUI - -struct HTMLBlockView: View { - private let content: String - - init(content: String) { - self.content = content.hasSuffix("\n") ? String(content.dropLast()) : content - } - - var body: some View { - Text(self.content) - .textStyleFont() - } -} diff --git a/Sources/MarkdownUI/Views/Blocks/HeadingView.swift b/Sources/MarkdownUI/Views/Blocks/HeadingView.swift new file mode 100644 index 00000000..7c25150a --- /dev/null +++ b/Sources/MarkdownUI/Views/Blocks/HeadingView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct HeadingView: View { + @Environment(\.theme.headings) private var headings + + private let level: Int + private let content: [InlineNode] + + init(level: Int, content: [InlineNode]) { + self.level = level + self.content = content + } + + var body: some View { + self.headings[self.level - 1].makeBody( + configuration: .init( + label: .init(InlineText(self.content)), + content: .init(block: .heading(level: self.level, content: self.content)) + ) + ) + .id(content.renderPlainText().kebabCased()) + } +} diff --git a/Sources/MarkdownUI/Views/Blocks/ListItemSequence.swift b/Sources/MarkdownUI/Views/Blocks/ListItemSequence.swift index 142b39eb..2139172c 100644 --- a/Sources/MarkdownUI/Views/Blocks/ListItemSequence.swift +++ b/Sources/MarkdownUI/Views/Blocks/ListItemSequence.swift @@ -20,14 +20,11 @@ struct ListItemSequence: View { var body: some View { BlockSequence(self.items) { index, item in - ApplyBlockStyle( - \.listItem, - to: ListItemView( - item: item, - number: self.start + index, - markerStyle: self.markerStyle, - markerWidth: self.markerWidth - ) + ListItemView( + item: item, + number: self.start + index, + markerStyle: self.markerStyle, + markerWidth: self.markerWidth ) } .labelStyle(.titleAndIcon) diff --git a/Sources/MarkdownUI/Views/Blocks/ListItemView.swift b/Sources/MarkdownUI/Views/Blocks/ListItemView.swift index fe9a9c4d..11643ac1 100644 --- a/Sources/MarkdownUI/Views/Blocks/ListItemView.swift +++ b/Sources/MarkdownUI/Views/Blocks/ListItemView.swift @@ -1,6 +1,7 @@ import SwiftUI struct ListItemView: View { + @Environment(\.theme.listItem) private var listItem @Environment(\.listLevel) private var listLevel private let item: RawListItem @@ -21,6 +22,15 @@ struct ListItemView: View { } var body: some View { + self.listItem.makeBody( + configuration: .init( + label: .init(self.label), + content: .init(blocks: item.children) + ) + ) + } + + private var label: some View { Label { BlockSequence(self.item.children) } icon: { diff --git a/Sources/MarkdownUI/Views/Blocks/NumberedListView.swift b/Sources/MarkdownUI/Views/Blocks/NumberedListView.swift index 99a07f16..fb3098ef 100644 --- a/Sources/MarkdownUI/Views/Blocks/NumberedListView.swift +++ b/Sources/MarkdownUI/Views/Blocks/NumberedListView.swift @@ -1,6 +1,7 @@ import SwiftUI struct NumberedListView: View { + @Environment(\.theme.list) private var list @Environment(\.theme.numberedListMarker) private var numberedListMarker @Environment(\.listLevel) private var listLevel @@ -17,6 +18,21 @@ struct NumberedListView: View { } var body: some View { + self.list.makeBody( + configuration: .init( + label: .init(self.label), + content: .init( + block: .numberedList( + isTight: self.isTight, + start: self.start, + items: self.items + ) + ) + ) + ) + } + + private var label: some View { ListItemSequence( items: self.items, start: self.start, diff --git a/Sources/MarkdownUI/Views/Blocks/ParagraphView.swift b/Sources/MarkdownUI/Views/Blocks/ParagraphView.swift new file mode 100644 index 00000000..7d5e4acc --- /dev/null +++ b/Sources/MarkdownUI/Views/Blocks/ParagraphView.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct ParagraphView: View { + @Environment(\.theme.paragraph) private var paragraph + + private let content: [InlineNode] + + init(content: String) { + self.init( + content: [ + .text(content.hasSuffix("\n") ? String(content.dropLast()) : content) + ] + ) + } + + init(content: [InlineNode]) { + self.content = content + } + + var body: some View { + self.paragraph.makeBody( + configuration: .init( + label: .init(self.label), + content: .init(block: .paragraph(content: self.content)) + ) + ) + } + + @ViewBuilder private var label: some View { + if let imageView = ImageView(content) { + imageView + } else if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *), + let imageFlow = ImageFlow(content) + { + imageFlow + } else { + InlineText(content) + } + } +} diff --git a/Sources/MarkdownUI/Views/Blocks/TableCell.swift b/Sources/MarkdownUI/Views/Blocks/TableCell.swift index a75634cd..b3ea865d 100644 --- a/Sources/MarkdownUI/Views/Blocks/TableCell.swift +++ b/Sources/MarkdownUI/Views/Blocks/TableCell.swift @@ -2,6 +2,8 @@ import SwiftUI @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) struct TableCell: View { + @Environment(\.theme.tableCell) private var tableCell + private let row: Int private let column: Int private let cell: RawTableCell @@ -13,18 +15,18 @@ struct TableCell: View { } var body: some View { - ApplyBlockStyle( - \.tableCell, + self.tableCell.makeBody( configuration: .init( row: self.row, column: self.column, - label: .init(self.content) + label: .init(self.label), + content: .init(block: .paragraph(content: cell.content)) ) ) .tableCellBounds(forRow: self.row, column: self.column) } - @ViewBuilder private var content: some View { + @ViewBuilder private var label: some View { if let imageFlow = ImageFlow(self.cell.content) { imageFlow } else { diff --git a/Sources/MarkdownUI/Views/Blocks/TableView.swift b/Sources/MarkdownUI/Views/Blocks/TableView.swift index 140655ee..4d0c3467 100644 --- a/Sources/MarkdownUI/Views/Blocks/TableView.swift +++ b/Sources/MarkdownUI/Views/Blocks/TableView.swift @@ -2,23 +2,33 @@ import SwiftUI @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) struct TableView: View { + @Environment(\.theme.table) private var table @Environment(\.tableBorderStyle.strokeStyle.lineWidth) private var borderWidth - private let columnAlignments: [HorizontalAlignment] + private let columnAlignments: [RawTableColumnAlignment] private let rows: [RawTableRow] init(columnAlignments: [RawTableColumnAlignment], rows: [RawTableRow]) { - self.columnAlignments = columnAlignments.map(HorizontalAlignment.init) + self.columnAlignments = columnAlignments self.rows = rows } var body: some View { + self.table.makeBody( + configuration: .init( + label: .init(self.label), + content: .init(block: .table(columnAlignments: self.columnAlignments, rows: self.rows)) + ) + ) + } + + private var label: some View { Grid(horizontalSpacing: self.borderWidth, verticalSpacing: self.borderWidth) { ForEach(0..( - _ keyPath: WritableKeyPath>, - @ViewBuilder body: @escaping (_ label: BlockConfiguration.Label) -> Body - ) -> some View { - self.environment((\EnvironmentValues.theme).appending(path: keyPath), .init(body: body)) - } - /// Replaces a specific block style on the current ``Theme`` with a block style initialized with the given body closure. /// - Parameters: /// - keyPath: The ``Theme`` key path to the block style to replace. diff --git a/Sources/MarkdownUI/Views/Inlines/ImageView.swift b/Sources/MarkdownUI/Views/Inlines/ImageView.swift index 4a8f6e4a..90f97e0a 100644 --- a/Sources/MarkdownUI/Views/Inlines/ImageView.swift +++ b/Sources/MarkdownUI/Views/Inlines/ImageView.swift @@ -1,6 +1,7 @@ import SwiftUI struct ImageView: View { + @Environment(\.theme.image) private var image @Environment(\.imageProvider) private var imageProvider @Environment(\.imageBaseURL) private var baseURL @@ -11,12 +12,35 @@ struct ImageView: View { } var body: some View { - ApplyBlockStyle( - \.image, - to: self.imageProvider.makeImage(url: self.url) - .link(destination: self.data.destination) + self.image.makeBody( + configuration: .init( + label: .init(self.label), + content: .init(block: self.content) + ) ) - .accessibilityLabel(self.data.alt) + } + + private var label: some View { + self.imageProvider.makeImage(url: self.url) + .link(destination: self.data.destination) + .accessibilityLabel(self.data.alt) + } + + private var content: BlockNode { + if let destination = self.data.destination { + return .paragraph( + content: [ + .link( + destination: destination, + children: [.image(source: self.data.source, children: [.text(self.data.alt)])] + ) + ] + ) + } else { + return .paragraph( + content: [.image(source: self.data.source, children: [.text(self.data.alt)])] + ) + } } private var url: URL? { diff --git a/Tests/MarkdownUITests/MarkdownContentTests.swift b/Tests/MarkdownUITests/MarkdownContentTests.swift index 1d674675..b55f42f2 100644 --- a/Tests/MarkdownUITests/MarkdownContentTests.swift +++ b/Tests/MarkdownUITests/MarkdownContentTests.swift @@ -219,7 +219,7 @@ final class MarkdownContentTests: XCTestCase { XCTAssertEqual( MarkdownContent { TextTable { - TextTableColumn(title: "Default", value: \.[0]) + TextTableColumn<[String]>(title: "Default", value: \.[0]) TextTableColumn(alignment: .leading, title: "Leading", value: \.[1]) TextTableColumn(alignment: .center, title: "Center", value: \.[2]) TextTableColumn(alignment: .trailing, title: "Trailing", value: \.[3]) diff --git a/Tests/MarkdownUITests/MarkdownTableTests.swift b/Tests/MarkdownUITests/MarkdownTableTests.swift index 80a08357..67dc616b 100644 --- a/Tests/MarkdownUITests/MarkdownTableTests.swift +++ b/Tests/MarkdownUITests/MarkdownTableTests.swift @@ -142,8 +142,8 @@ } .padding() .border(Color.accentColor) - .markdownBlockStyle(\.table) { label in - label + .markdownBlockStyle(\.table) { configuration in + configuration.label .markdownMargin(top: .zero, bottom: .em(1)) .markdownTableBackgroundStyle( .alternatingRows(Color.clear, Color(.secondarySystemBackground), header: .mint) @@ -172,8 +172,8 @@ } .padding() .border(Color.accentColor) - .markdownBlockStyle(\.table) { label in - label + .markdownBlockStyle(\.table) { configuration in + configuration.label .markdownMargin(top: .zero, bottom: .em(1)) .markdownTableBorderStyle( .init( diff --git a/Tests/MarkdownUITests/MarkdownTests.swift b/Tests/MarkdownUITests/MarkdownTests.swift index 9d982cd6..e2e7fd9c 100644 --- a/Tests/MarkdownUITests/MarkdownTests.swift +++ b/Tests/MarkdownUITests/MarkdownTests.swift @@ -166,8 +166,9 @@ } .border(Color.accentColor) .padding() - .markdownBlockStyle(\.paragraph) { label in - label.markdownMargin(bottom: .zero) + .markdownBlockStyle(\.paragraph) { configuration in + configuration.label + .markdownMargin(bottom: .zero) } assertSnapshot(matching: view, as: .image(layout: layout)) diff --git a/Tests/MarkdownUITests/__Snapshots__/MarkdownTableTests/testTableBorder.1.png b/Tests/MarkdownUITests/__Snapshots__/MarkdownTableTests/testTableBorder.1.png index cbf8bec4..aa60536c 100644 Binary files a/Tests/MarkdownUITests/__Snapshots__/MarkdownTableTests/testTableBorder.1.png and b/Tests/MarkdownUITests/__Snapshots__/MarkdownTableTests/testTableBorder.1.png differ