diff --git a/Sources/MarkdownView/Customizations/Block Quotes/BlockQuoteStyle.swift b/Sources/MarkdownView/Customizations/Block Quotes/BlockQuoteStyle.swift index f1430836..32f8c08c 100644 --- a/Sources/MarkdownView/Customizations/Block Quotes/BlockQuoteStyle.swift +++ b/Sources/MarkdownView/Customizations/Block Quotes/BlockQuoteStyle.swift @@ -41,12 +41,16 @@ public struct BlockQuoteStyleConfiguration { self.blockQuote = blockQuote } + private var children: [Markup] { + Array(blockQuote.children) + } + @_documentation(visibility: internal) public var body: some View { VStack(alignment: .leading, spacing: configuration.componentSpacing) { - ForEach(Array(blockQuote.children.enumerated()), id: \.offset) { _, child in + ForEach(children.indices, id: \.self) { index in CmarkNodeVisitor(configuration: configuration) - .makeBody(for: child) + .makeBody(for: children[index]) } } } diff --git a/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.swift b/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.swift index ac504ba2..311d70fe 100644 --- a/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.swift +++ b/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.swift @@ -38,8 +38,8 @@ extension MarkdownTableStyleConfiguration.Table: View { if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { Grid(horizontalSpacing: 0, verticalSpacing: 0) { header - ForEach(Array(rows.enumerated()), id: \.offset) { (_, row) in - row + ForEach(rows.indices, id: \.self) { index in + rows[index] } } } else { diff --git a/Sources/MarkdownView/Customizations/Table/DefaultMarkdownTableStyle.swift b/Sources/MarkdownView/Customizations/Table/DefaultMarkdownTableStyle.swift index 1c1fe615..012cfc7f 100644 --- a/Sources/MarkdownView/Customizations/Table/DefaultMarkdownTableStyle.swift +++ b/Sources/MarkdownView/Customizations/Table/DefaultMarkdownTableStyle.swift @@ -42,11 +42,11 @@ fileprivate struct DefaultMarkdownTable: View { if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { Grid(horizontalSpacing: 0, verticalSpacing: 0) { configuration.table.header - ForEach(Array(configuration.table.rows.enumerated()), id: \.offset) { (_, row) in + ForEach(configuration.table.rows.indices, id: \.self) { index in if showsRowSeparators { Divider() } - row + configuration.table.rows[index] } } } else { diff --git a/Sources/MarkdownView/Customizations/Table/GithubMarkdownTableStyle.swift b/Sources/MarkdownView/Customizations/Table/GithubMarkdownTableStyle.swift index 6d981e3c..bddbc534 100644 --- a/Sources/MarkdownView/Customizations/Table/GithubMarkdownTableStyle.swift +++ b/Sources/MarkdownView/Customizations/Table/GithubMarkdownTableStyle.swift @@ -63,9 +63,9 @@ fileprivate struct GithubMarkdownTable: View { Grid(horizontalSpacing: 0, verticalSpacing: 0) { configuration.table.header - ForEach(Array(configuration.table.rows.enumerated()), id: \.offset) { (index, row) in + ForEach(configuration.table.rows.indices, id: \.self) { index in let backgroundStyle = index % 2 == 0 ? AnyShapeStyle(backgroundColor) : AnyShapeStyle(alternativeRowColor) - row + configuration.table.rows[index] .markdownTableRowBackgroundStyle(backgroundStyle) } } diff --git a/Sources/MarkdownView/MarkdownReader.swift b/Sources/MarkdownView/MarkdownReader.swift index 6f9350d2..71a3ce53 100644 --- a/Sources/MarkdownView/MarkdownReader.swift +++ b/Sources/MarkdownView/MarkdownReader.swift @@ -21,25 +21,25 @@ import SwiftUI /// } /// ``` public struct MarkdownReader: View { - @ObservedObject private var content: MarkdownContent + @StateObject private var content: MarkdownContent private var _body: (_ markdownContent: MarkdownContent) -> Content - + public init( _ text: String, @ViewBuilder contents: @escaping (MarkdownContent) -> Content ) { - content = MarkdownContent(text) + _content = StateObject(wrappedValue: MarkdownContent(text)) self._body = contents } - + public init( _ url: URL, @ViewBuilder contents: @escaping (MarkdownContent) -> Content ) { - content = MarkdownContent(url) + _content = StateObject(wrappedValue: MarkdownContent(url)) self._body = contents } - + public var body: some View { _body(content) } diff --git a/Sources/MarkdownView/Renderers/Cmark/CmarkFirstMarkdownViewRenderer.swift b/Sources/MarkdownView/Renderers/Cmark/CmarkFirstMarkdownViewRenderer.swift index b19f04af..fdebb78d 100644 --- a/Sources/MarkdownView/Renderers/Cmark/CmarkFirstMarkdownViewRenderer.swift +++ b/Sources/MarkdownView/Renderers/Cmark/CmarkFirstMarkdownViewRenderer.swift @@ -36,7 +36,7 @@ struct TextViewViewRenderer: MarkdownViewRenderer { if !configuration.allowedBlockDirectiveRenderers.isEmpty { parseOptions.insert(.parseBlockDirectives) } - + let textContent = CmarkTextContentVisitor(configuration: configuration) .makeTextContent(for: content.document(options: parseOptions)) return TextView { diff --git a/Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift b/Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift index 20149015..8a8fcc14 100644 --- a/Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift +++ b/Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift @@ -25,23 +25,21 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { } func visitDocument(_ document: Document) -> MarkdownNodeView { - var renderer = self + var visitor = self let nodeViews = document.children.map { - renderer.visit($0) + visitor.visit($0) } return MarkdownNodeView(nodeViews, layoutPolicy: .linebreak) } - + func defaultVisit(_ markup: Markdown.Markup) -> MarkdownNodeView { descendInto(markup) } - + func descendInto(_ markup: any Markup) -> MarkdownNodeView { - var nodeViews = [MarkdownNodeView]() - for child in markup.children { - var renderer = self - let nodeView = renderer.visit(child) - nodeViews.append(nodeView) + var visitor = self + let nodeViews = markup.children.map { + visitor.visit($0) } return MarkdownNodeView(nodeViews) } @@ -170,11 +168,9 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { } func visitTableCell(_ cell: Markdown.Table.Cell) -> MarkdownNodeView { - var cellViews = [MarkdownNodeView]() - for child in cell.children { - var renderer = CmarkNodeVisitor(configuration: configuration) - let cellView = renderer.visit(child) - cellViews.append(cellView) + var visitor = self + let cellViews = cell.children.map { + visitor.visit($0) } return MarkdownNodeView( cellViews, @@ -193,10 +189,10 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { } func visitEmphasis(_ emphasis: Markdown.Emphasis) -> MarkdownNodeView { + var visitor = self var attributedString = AttributedString() for child in emphasis.children { - var renderer = self - guard let text = renderer.visit(child).asAttributedString else { continue } + guard let text = visitor.visit(child).asAttributedString else { continue } let intent = text.inlinePresentationIntent ?? [] attributedString += text.mergingAttributes( AttributeContainer() @@ -205,12 +201,12 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { } return MarkdownNodeView(attributedString) } - + func visitStrong(_ strong: Strong) -> MarkdownNodeView { + var visitor = self var attributedString = AttributedString() for child in strong.children { - var renderer = self - guard let text = renderer.visit(child).asAttributedString else { continue } + guard let text = visitor.visit(child).asAttributedString else { continue } let intent = text.inlinePresentationIntent ?? [] attributedString += text.mergingAttributes( AttributeContainer() @@ -219,12 +215,12 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { } return MarkdownNodeView(attributedString) } - + func visitStrikethrough(_ strikethrough: Strikethrough) -> MarkdownNodeView { + var visitor = self var attributedString = AttributedString() for child in strikethrough.children { - var renderer = self - guard let text = renderer.visit(child).asAttributedString else { continue } + guard let text = visitor.visit(child).asAttributedString else { continue } let intent = text.inlinePresentationIntent ?? [] attributedString += text.mergingAttributes( AttributeContainer() @@ -241,13 +237,18 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { let nodeView = descendInto(link) let tintColor = configuration.preferredTintColors[.link] ?? .accentColor + let underline = configuration.underlineLinks return if let attributedString = nodeView.asAttributedString { MarkdownNodeView( - attributedString.mergingAttributes( - AttributeContainer() + attributedString.mergingAttributes({ + var container = AttributeContainer() .link(url) .foregroundColor(tintColor) - ) + if underline { + container.underlineStyle = .single + } + return container + }()) ) } else { MarkdownNodeView { @@ -255,6 +256,7 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { nodeView } .foregroundStyle(tintColor) + .underline(underline) } } } diff --git a/Sources/MarkdownView/Renderers/Cmark/CmarkTextContentVisitor.swift b/Sources/MarkdownView/Renderers/Cmark/CmarkTextContentVisitor.swift index 21b6d340..703a5f31 100644 --- a/Sources/MarkdownView/Renderers/Cmark/CmarkTextContentVisitor.swift +++ b/Sources/MarkdownView/Renderers/Cmark/CmarkTextContentVisitor.swift @@ -15,7 +15,7 @@ import RichText @available(iOS 26, macOS 26, *) struct CmarkTextContentVisitor: @preconcurrency MarkupVisitor { var configuration: MarkdownRendererConfiguration - + init(configuration: MarkdownRendererConfiguration) { self.configuration = configuration } @@ -184,29 +184,51 @@ struct CmarkTextContentVisitor: @preconcurrency MarkupVisitor { var attributes = AttributeContainer([.paragraphStyle: paragraphStyle]) let markerString: String? - switch listItem.parent { - case let list as UnorderedList: - let marker = configuration.list.unorderedListMarker - markerString = marker.marker(listDepth: list.listDepth) - attributes = attributes.font((configuration.fonts[.body] ?? .body).monospaced(marker.monospaced)) - case let list as OrderedList: - let marker = configuration.list.orderedListMarker - markerString = marker.marker(at: listItem.indexInParent, listDepth: list.listDepth) - attributes = attributes.font((configuration.fonts[.body] ?? .body).monospaced(marker.monospaced)) - default: - markerString = nil + let hasCheckbox = listItem.checkbox != nil + if hasCheckbox { + markerString = nil + } else { + switch listItem.parent { + case let list as UnorderedList: + let marker = configuration.list.unorderedListMarker + markerString = marker.marker(listDepth: list.listDepth) + attributes = attributes.font((configuration.fonts[.body] ?? .body).monospaced(marker.monospaced)) + case let list as OrderedList: + let marker = configuration.list.orderedListMarker + markerString = marker.marker(at: listItem.indexInParent, listDepth: list.listDepth) + attributes = attributes.font((configuration.fonts[.body] ?? .body).monospaced(marker.monospaced)) + default: + markerString = nil + } } let children = Array(listItem.children) - + let firstChildContent = children.first.map(descendInto) let trailingBlocks = children.dropFirst().map { child in var nestedRenderer = self return nestedRenderer.visit(child) } - + return TextContent { - if let markerString { + if let checkbox = listItem.checkbox { + let checkboxView: some View = Group { + switch checkbox { + case .checked: + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.tint) + case .unchecked: + Image(systemName: "circle") + .foregroundStyle(.secondary) + } + } + let attachment = InlineHostingAttachment( + checkboxView, + id: listItem.range, + replacement: nil + ) + TextContent(.view(attachment)) + } else if let markerString { AttributedString(markerString, attributes: attributes) } Space() @@ -264,16 +286,46 @@ struct CmarkTextContentVisitor: @preconcurrency MarkupVisitor { case 6: MarkdownComponent.h6 default: MarkdownComponent.body } + let foregroundStyle: AnyShapeStyle = switch heading.level { + case 1: configuration.headingStyleGroup.h1 + case 2: configuration.headingStyleGroup.h2 + case 3: configuration.headingStyleGroup.h3 + case 4: configuration.headingStyleGroup.h4 + case 5: configuration.headingStyleGroup.h5 + case 6: configuration.headingStyleGroup.h6 + default: AnyShapeStyle(.foreground) + } + let font = configuration.fonts[component] ?? .body let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.paragraphSpacing = 12 paragraphStyle.paragraphSpacingBefore = 12 let attributes = AttributeContainer([.paragraphStyle : paragraphStyle as NSParagraphStyle]) .presentationIntent(.init(.header(level: heading.level), identity: heading.indexInParent)) .accessibilityHeadingLevel(AttributeScopes.AccessibilityAttributes.HeadingLevelAttribute.HeadingLevel(rawValue: heading.level) ?? .unspecified) - .font(configuration.fonts[component] ?? .body) - + .font(font) + + let replacement = AttributedString(heading.plainText, attributes: attributes) return TextContent { - AttributedString(heading.plainText, attributes: attributes) + inlineViewContent( + for: heading, + replacement: replacement + ) { + SwiftUI.Text(heading.plainText) + .font(font) + .foregroundStyle(foregroundStyle) + .accessibilityHeading({ + switch heading.level { + case 1: .h1 + case 2: .h2 + case 3: .h3 + case 4: .h4 + case 5: .h5 + case 6: .h6 + default: .unspecified + } + }() as AccessibilityHeadingLevel) + .accessibilityAddTraits(.isHeader) + } LineBreak() } } @@ -331,14 +383,15 @@ struct CmarkTextContentVisitor: @preconcurrency MarkupVisitor { let linkContent = descendInto(link) let tintColor = configuration.preferredTintColors[.link] ?? .accentColor - + let underline = configuration.underlineLinks + let contentView = linkContent.fragments.first(byUnwrapping: { if case let .view(attachment) = $0 { return attachment.view } return nil }) - + if let contentView { return inlineViewContent( for: link, @@ -351,16 +404,21 @@ struct CmarkTextContentVisitor: @preconcurrency MarkupVisitor { contentView } .foregroundStyle(tintColor) + .underline(underline) } } else { let attributedString = linkContent.attributedStringIgnoringViews return TextContent( .attributedString( - attributedString.mergingAttributes( - AttributeContainer() + attributedString.mergingAttributes({ + var container = AttributeContainer() .link(url) .foregroundColor(tintColor) - ) + if underline { + container.underlineStyle = .single + } + return container + }()) ) ) } diff --git a/Sources/MarkdownView/Renderers/MarkdownRendererConfiguration.swift b/Sources/MarkdownView/Renderers/MarkdownRendererConfiguration.swift index 7f27d329..c394f4d6 100644 --- a/Sources/MarkdownView/Renderers/MarkdownRendererConfiguration.swift +++ b/Sources/MarkdownView/Renderers/MarkdownRendererConfiguration.swift @@ -31,9 +31,13 @@ public struct MarkdownRendererConfiguration: Equatable, Sendable { public var rendersMath: Bool { math.isEnabled } public internal(set) var preferredTintColors: [MarkdownTintableComponent: Color] = [:] + + public internal(set) var underlineLinks: Bool = false public internal(set) var list = MarkdownListConfiguration() + public internal(set) var headingStyleGroup: AnyHeadingStyleGroup = .init(.automatic) + public internal(set) var allowedImageRenderers: Set = ["https", "http"] public internal(set) var allowedBlockDirectiveRenderers: Set = [] diff --git a/Sources/MarkdownView/Renderers/MarkdownViewRenderer.swift b/Sources/MarkdownView/Renderers/MarkdownViewRenderer.swift index dbfd4261..4b602691 100644 --- a/Sources/MarkdownView/Renderers/MarkdownViewRenderer.swift +++ b/Sources/MarkdownView/Renderers/MarkdownViewRenderer.swift @@ -120,11 +120,11 @@ extension MarkdownViewRenderer { } } -struct MarkdownViewRendererKey: EnvironmentKey { - nonisolated(unsafe) static let defaultValue: any MarkdownViewRenderer = .automatic +public struct MarkdownViewRendererKey: EnvironmentKey { + nonisolated(unsafe) public static let defaultValue: any MarkdownViewRenderer = .automatic } -extension EnvironmentValues { +public extension EnvironmentValues { var markdownViewRenderer: any MarkdownViewRenderer { get { self[MarkdownViewRendererKey.self] } set { self[MarkdownViewRendererKey.self] = newValue } diff --git a/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.swift b/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.swift index 801c35d9..b7c8737c 100644 --- a/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.swift +++ b/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.swift @@ -115,7 +115,7 @@ private func makeMathFirstBody( var extractor = MathFirstMarkdownViewRenderer.ParsingRangesExtractor() extractor.visit(content.document()) - for range in extractor.parsableRanges(in: rawText) { + for range in extractor.parsableRanges(in: rawText).reversed() { let segment = rawText[range] let segmentParser = MathParser(text: segment) for math in segmentParser.mathRepresentations.reversed() where !math.kind.inline { diff --git a/Sources/MarkdownView/Renderers/Node Representations/MarkdownList.swift b/Sources/MarkdownView/Renderers/Node Representations/MarkdownList.swift index aa7fa2a3..7b9c3a54 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/MarkdownList.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/MarkdownList.swift @@ -18,17 +18,18 @@ struct MarkdownList: View { } } + private var listItems: [ListItem] { + Array(listItemsContainer.listItems) + } + var body: some View { VStack(alignment: .leading, spacing: configuration.componentSpacing) { - ForEach( - Array(listItemsContainer.listItems.enumerated()), - id: \.offset - ) { (index, listItem) in + ForEach(listItems.indices, id: \.self) { index in HStack(alignment: .firstTextBaseline) { - CheckboxOrMarker(list: self, listItem: listItem, index: index) + CheckboxOrMarker(list: self, listItem: listItems[index], index: index) .padding(.leading, depth == 0 ? configuration.list.leadingIndentation : 0) CmarkNodeVisitor(configuration: configuration) - .makeBody(for: listItem) + .makeBody(for: listItems[index]) } } } @@ -71,12 +72,16 @@ struct MarkdownList: View { struct MarkdownListItem: View { var listItem: ListItem @Environment(\.markdownRendererConfiguration) private var configuration - + + private var children: [Markup] { + Array(listItem.children) + } + var body: some View { VStack(alignment: .leading, spacing: configuration.componentSpacing) { - ForEach(Array(listItem.children.enumerated()), id: \.offset) { (_, child) in + ForEach(children.indices, id: \.self) { index in CmarkNodeVisitor(configuration: configuration) - .makeBody(for: child) + .makeBody(for: children[index]) } } } diff --git a/Sources/MarkdownView/Renderers/Node Representations/MarkdownNodeView.swift b/Sources/MarkdownView/Renderers/Node Representations/MarkdownNodeView.swift index 9eb6b574..684f80bc 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/MarkdownNodeView.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/MarkdownNodeView.swift @@ -35,15 +35,16 @@ struct MarkdownNodeView: View { } var body: some View { - Group { - if case .left(let attributedString) = storage { - MarkdownText(attributedString) - } else if case .right(let view) = storage { - view - } + switch storage { + case .left(let attributedString): + MarkdownText(attributedString) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + case .right(let view): + view + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) } - .lineLimit(nil) - .fixedSize(horizontal: false, vertical: true) } var asAttributedString: AttributedString? { @@ -89,24 +90,20 @@ extension MarkdownNodeView { } if composedContents.count == 1 { - if let attributedString = composedContents[0].asAttributedString { - storage = .left(attributedString) - } else { - storage = .right(AnyView(composedContents[0].body)) - } + storage = composedContents[0].storage } else { if layoutPolicy == .adaptive, #available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) { let composedView = FlowLayout(verticleSpacing: 8) { ForEach(composedContents.indices, id: \.self) { - composedContents[$0].body + composedContents[$0] } } storage = .right(AnyView(composedView)) } else { let composedView = VStack(alignment: alignment, spacing: 8) { ForEach(composedContents.indices, id: \.self) { - composedContents[$0].body + composedContents[$0] } } storage = .right(AnyView(composedView)) diff --git a/Sources/MarkdownView/Renderers/Node Representations/MarkdownText.swift b/Sources/MarkdownView/Renderers/Node Representations/MarkdownText.swift index f69bd362..43c5d1cd 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/MarkdownText.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/MarkdownText.swift @@ -12,38 +12,44 @@ import SwiftUI /// Convert HTML into `AttributedString` asynchronously to avoid `AttributeGraph` crash. struct MarkdownText: View { var text: AttributedString - @State private var attributedString: AttributedString? - + @State private var resolvedString: AttributedString? + @State private var lastInput: AttributedString? + + private var containsHTML: Bool { + text.runs.contains { $0.isHTML ?? false } + } + init(_ text: AttributedString) { self.text = text } - + var body: some View { - Group { - if let attributedString { - Text(attributedString) - } else { - Text(text) - } - } - .task(id: text) { - var attributedString = text - for run in text.runs.reversed() where (run.isHTML ?? false) { - let range = run.range - - if let htmlAttrString = try? AttributedString( - NSAttributedString( - data: Data(String(text.characters[range]).utf8), - options: [ - .documentType: NSAttributedString.DocumentType.html - ], - documentAttributes: nil - ) - ) { - attributedString.replaceSubrange(range, with: htmlAttrString) + Text(resolvedString ?? text) + .task(id: text) { + guard containsHTML else { + resolvedString = nil + lastInput = text + return + } + guard text != lastInput else { return } + lastInput = text + + var result = text + for run in text.runs.reversed() where (run.isHTML ?? false) { + let range = run.range + if let htmlAttrString = try? AttributedString( + NSAttributedString( + data: Data(String(text.characters[range]).utf8), + options: [ + .documentType: NSAttributedString.DocumentType.html + ], + documentAttributes: nil + ) + ) { + result.replaceSubrange(range, with: htmlAttrString) + } } + resolvedString = result } - self.attributedString = attributedString - } } } diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/MarkdownTable.swift b/Sources/MarkdownView/Renderers/Node Representations/Tables/MarkdownTable.swift index 9fa97e85..bfc00ec5 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/Tables/MarkdownTable.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/Tables/MarkdownTable.swift @@ -23,14 +23,18 @@ extension MarkdownTable { struct MarkdownTableBody: View { var tableBody: Markdown.Table.Body - + @Environment(\.markdownRendererConfiguration) private var configuration - + + private var rows: [Markup] { + Array(tableBody.children) + } + var body: some View { let font = configuration.fonts[.tableBody] ?? .body - ForEach(Array(tableBody.children.enumerated()), id: \.offset) { (_, row) in + ForEach(rows.indices, id: \.self) { index in CmarkNodeVisitor(configuration: configuration) - .makeBody(for: row) + .makeBody(for: rows[index]) .font(font) } } diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/MarkdownTableRow.swift b/Sources/MarkdownView/Renderers/Node Representations/Tables/MarkdownTableRow.swift index 5eac6bfb..9c6971c8 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/Tables/MarkdownTableRow.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/Tables/MarkdownTableRow.swift @@ -22,12 +22,12 @@ struct MarkdownTableRow: View { var body: some View { if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { GridRow { - ForEach(Array(cells.enumerated()), id: \.offset) { (index, cell) in + ForEach(cells.indices, id: \.self) { index in CmarkNodeVisitor(configuration: configuration) - .makeBody(for: cell) - .multilineTextAlignment(cell.textAlignment) - .gridColumnAlignment(cell.horizontalAlignment) - .gridCellColumns(Int(cell.colspan)) + .makeBody(for: cells[index]) + .multilineTextAlignment(cells[index].textAlignment) + .gridColumnAlignment(cells[index].horizontalAlignment) + .gridCellColumns(Int(cells[index].colspan)) ._markdownCellPadding(padding) .modifier( MarkdownTableStylePreferenceSynchronizer( diff --git a/Sources/MarkdownView/View Modifiers/Heading/HeadingStyleModifier.swift b/Sources/MarkdownView/View Modifiers/Heading/HeadingStyleModifier.swift index 1a6fe99d..fbe9c745 100644 --- a/Sources/MarkdownView/View Modifiers/Heading/HeadingStyleModifier.swift +++ b/Sources/MarkdownView/View Modifiers/Heading/HeadingStyleModifier.swift @@ -7,7 +7,7 @@ import SwiftUI -extension View { +extension SwiftUI.View { /// Apply a foreground style group to MarkdownView. /// /// This is useful when you want to completely customize foreground styles. @@ -16,20 +16,11 @@ extension View { nonisolated public func headingStyleGroup( _ group: some HeadingStyleGroup ) -> some View { - environment(\.headingStyleGroup, AnyHeadingStyleGroup(group)) - } - - /// Apply a foreground style group to MarkdownView. - /// - /// This is useful when you want to completely customize foreground styles. - /// - /// - Parameter group: A style set to apply to the MarkdownView. - @available(*, deprecated, renamed: "headingStyleGroup") - nonisolated public func foregroundStyleGroup( - _ group: some HeadingStyleGroup - ) -> some View { - headingStyleGroup(group) + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.headingStyleGroup = AnyHeadingStyleGroup(group) + } } + /// Sets foreground style for the specific component in MarkdownView. /// @@ -52,6 +43,22 @@ extension View { } } } +} + +// MARK: - Deprecated + +extension SwiftUI.View { + /// Apply a foreground style group to MarkdownView. + /// + /// This is useful when you want to completely customize foreground styles. + /// + /// - Parameter group: A style set to apply to the MarkdownView. + @available(*, deprecated, renamed: "headingStyleGroup") + nonisolated public func foregroundStyleGroup( + _ group: some HeadingStyleGroup + ) -> some View { + headingStyleGroup(group) + } /// Sets foreground style for the specific component in MarkdownView. /// diff --git a/Sources/MarkdownView/View Modifiers/Styling/UnderlineLinkModifier.swift b/Sources/MarkdownView/View Modifiers/Styling/UnderlineLinkModifier.swift new file mode 100644 index 00000000..6bc201ed --- /dev/null +++ b/Sources/MarkdownView/View Modifiers/Styling/UnderlineLinkModifier.swift @@ -0,0 +1,17 @@ +// +// UnderlineLinkModifier.swift +// MarkdownView +// + +import SwiftUI + +extension View { + /// Adds an underline decoration to links in the Markdown content. + /// + /// - Parameter isActive: Whether links should be underlined. Defaults to `true`. + nonisolated public func underlineLinks(_ isActive: Bool = true) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.underlineLinks = isActive + } + } +}