Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/MarkdownView/MarkdownReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,25 @@ import SwiftUI
/// }
/// ```
public struct MarkdownReader<Content: View>: 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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
52 changes: 27 additions & 25 deletions Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -241,20 +237,26 @@ 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 {
Link(destination: url) {
nodeView
}
.foregroundStyle(tintColor)
.underline(underline)
}
}
}
Expand Down
104 changes: 81 additions & 23 deletions Sources/MarkdownView/Renderers/Cmark/CmarkTextContentVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import RichText
@available(iOS 26, macOS 26, *)
struct CmarkTextContentVisitor: @preconcurrency MarkupVisitor {
var configuration: MarkdownRendererConfiguration

init(configuration: MarkdownRendererConfiguration) {
self.configuration = configuration
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
}())
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = ["https", "http"]
public internal(set) var allowedBlockDirectiveRenderers: Set<String> = []

Expand Down
Loading