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
90 changes: 29 additions & 61 deletions Sources/MarkdownUI/Markdown.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,13 @@ public struct Markdown: View {

private struct ViewState {
var attributedString = NSAttributedString()
var environmentHash: Int?
var hashValue: Int?
}

@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
@Environment(\.multilineTextAlignment) private var textAlignment: TextAlignment
@Environment(\.sizeCategory) private var sizeCategory: ContentSizeCategory
@Environment(\.lineSpacing) private var lineSpacing: CGFloat
@Environment(\.markdownStyle) private var style: MarkdownStyle
@Environment(\.openMarkdownLink) private var openMarkdownLink
@State private var viewState = ViewState()
Expand Down Expand Up @@ -194,35 +195,32 @@ public struct Markdown: View {
}

private var viewStatePublisher: AnyPublisher<ViewState, Never> {
struct Environment: Hashable {
var storage: Storage
var baseURL: URL?
var layoutDirection: LayoutDirection
var textAlignment: TextAlignment
var sizeCategory: ContentSizeCategory
var style: MarkdownStyle
struct Input: Hashable {
let storage: Storage
let environment: AttributedStringRenderer.Environment
}

return Just(
// This value helps determine if we need to render the markdown again
Environment(
Input(
storage: self.storage,
baseURL: self.baseURL,
layoutDirection: self.layoutDirection,
textAlignment: self.textAlignment,
sizeCategory: self.sizeCategory,
style: self.style
environment: .init(
baseURL: self.baseURL,
layoutDirection: self.layoutDirection,
alignment: self.textAlignment,
lineSpacing: self.lineSpacing,
sizeCategory: self.sizeCategory,
style: self.style
)
).hashValue
)
.flatMap { environmentHash -> AnyPublisher<ViewState, Never> in
if self.viewState.environmentHash == environmentHash,
!viewState.attributedString.hasMarkdownImages
{
.flatMap { hashValue -> AnyPublisher<ViewState, Never> in
if self.viewState.hashValue == hashValue, !viewState.attributedString.hasMarkdownImages {
return Empty().eraseToAnyPublisher()
} else if self.viewState.environmentHash == environmentHash {
return self.loadMarkdownImages(environmentHash: environmentHash)
} else if self.viewState.hashValue == hashValue {
return self.loadMarkdownImages(hashValue)
} else {
return self.renderAttributedString(environmentHash: environmentHash)
return self.renderAttributedString(hashValue)
}
}
.eraseToAnyPublisher()
Expand All @@ -235,29 +233,29 @@ public struct Markdown: View {
}
}

private func loadMarkdownImages(environmentHash: Int) -> AnyPublisher<ViewState, Never> {
private func loadMarkdownImages(_ hashValue: Int) -> AnyPublisher<ViewState, Never> {
NSAttributedString.loadingMarkdownImages(
from: self.viewState.attributedString,
using: self.imageHandlers
)
.map { ViewState(attributedString: $0, environmentHash: environmentHash) }
.map { ViewState(attributedString: $0, hashValue: hashValue) }
.receive(on: UIScheduler.shared)
.eraseToAnyPublisher()
}

private func renderAttributedString(environmentHash: Int) -> AnyPublisher<ViewState, Never> {
private func renderAttributedString(_ hashValue: Int) -> AnyPublisher<ViewState, Never> {
self.storage.document.renderAttributedString(
baseURL: self.baseURL,
baseWritingDirection: .init(self.layoutDirection),
alignment: .init(
environment: .init(
baseURL: self.baseURL,
layoutDirection: self.layoutDirection,
textAlignment: self.textAlignment
alignment: self.textAlignment,
lineSpacing: self.lineSpacing,
sizeCategory: self.sizeCategory,
style: self.style
),
sizeCategory: self.sizeCategory,
style: self.style,
imageHandlers: self.imageHandlers
)
.map { ViewState(attributedString: $0, environmentHash: environmentHash) }
.map { ViewState(attributedString: $0, hashValue: hashValue) }
.receive(on: UIScheduler.shared)
.eraseToAnyPublisher()
}
Expand Down Expand Up @@ -412,33 +410,3 @@ private struct OpenMarkdownLinkAction {
private struct OpenMarkdownLinkKey: EnvironmentKey {
static let defaultValue: OpenMarkdownLinkAction? = nil
}

extension NSWritingDirection {
fileprivate init(_ layoutDirection: LayoutDirection) {
switch layoutDirection {
case .leftToRight:
self = .leftToRight
case .rightToLeft:
self = .rightToLeft
@unknown default:
self = .natural
}
}
}

extension NSTextAlignment {
fileprivate init(layoutDirection: LayoutDirection, textAlignment: TextAlignment) {
switch (layoutDirection, textAlignment) {
case (_, .leading):
self = .natural
case (_, .center):
self = .center
case (.leftToRight, .trailing):
self = .right
case (.rightToLeft, .trailing):
self = .left
default:
self = .natural
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import SwiftUI

extension AttributedStringRenderer {
struct Environment: Hashable {
let baseURL: URL?
let baseWritingDirection: NSWritingDirection
let alignment: NSTextAlignment
let lineSpacing: CGFloat
let sizeCategory: ContentSizeCategory
let style: MarkdownStyle

init(
baseURL: URL?,
layoutDirection: LayoutDirection,
alignment: TextAlignment,
lineSpacing: CGFloat,
sizeCategory: ContentSizeCategory,
style: MarkdownStyle
) {
self.baseURL = baseURL
self.baseWritingDirection = .init(layoutDirection)
self.alignment = .init(layoutDirection, alignment)
self.lineSpacing = lineSpacing
self.sizeCategory = sizeCategory
self.style = style
}
}
}

extension NSWritingDirection {
fileprivate init(_ layoutDirection: LayoutDirection) {
switch layoutDirection {
case .leftToRight:
self = .leftToRight
case .rightToLeft:
self = .rightToLeft
@unknown default:
self = .natural
}
}
}

extension NSTextAlignment {
fileprivate init(_ layoutDirection: LayoutDirection, _ textAlignment: TextAlignment) {
switch (layoutDirection, textAlignment) {
case (_, .leading):
self = .natural
case (_, .center):
self = .center
case (.leftToRight, .trailing):
self = .right
case (.rightToLeft, .trailing):
self = .left
default:
self = .natural
}
}
}
65 changes: 33 additions & 32 deletions Sources/MarkdownUI/Rendering/AttributedStringRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,15 @@ struct AttributedStringRenderer {
case decimal(Int)
}

let baseURL: URL?
let baseWritingDirection: NSWritingDirection
let alignment: NSTextAlignment
let sizeCategory: ContentSizeCategory
let style: MarkdownStyle
let environment: Environment

func renderDocument(_ document: Document) -> NSAttributedString {
return renderBlocks(
document.blocks,
state: .init(
font: style.font,
foregroundColor: style.foregroundColor,
paragraphSpacing: style.measurements.paragraphSpacing
font: environment.style.font,
foregroundColor: environment.style.foregroundColor,
paragraphSpacing: environment.style.measurements.paragraphSpacing
)
)
}
Expand Down Expand Up @@ -101,8 +97,8 @@ extension AttributedStringRenderer {

var state = state
state.font = state.font.italic()
state.headIndent += style.measurements.headIndentStep
state.tailIndent += style.measurements.tailIndentStep
state.headIndent += environment.style.measurements.headIndentStep
state.tailIndent += environment.style.measurements.tailIndentStep
state.tabStops.append(
.init(textAlignment: .natural, location: state.headIndent)
)
Expand All @@ -129,13 +125,14 @@ extension AttributedStringRenderer {
let result = NSMutableAttributedString()

var itemState = state
itemState.paragraphSpacing = bulletList.tight ? 0 : style.measurements.paragraphSpacing
itemState.headIndent += style.measurements.headIndentStep
itemState.paragraphSpacing =
bulletList.tight ? 0 : environment.style.measurements.paragraphSpacing
itemState.headIndent += environment.style.measurements.headIndentStep
itemState.tabStops.append(
contentsOf: [
.init(
textAlignment: .trailing(baseWritingDirection),
location: itemState.headIndent - style.measurements.listMarkerSpacing
textAlignment: .trailing(environment.baseWritingDirection),
location: itemState.headIndent - environment.style.measurements.listMarkerSpacing
),
.init(textAlignment: .natural, location: itemState.headIndent),
]
Expand Down Expand Up @@ -172,21 +169,24 @@ extension AttributedStringRenderer {
// as the head indent step if higher than the style's head indent step.
let highestNumber = orderedList.start + orderedList.items.count - 1
let headIndentStep = max(
style.measurements.headIndentStep,
environment.style.measurements.headIndentStep,
NSAttributedString(
string: "\(highestNumber).",
attributes: [.font: state.font.monospacedDigit().resolve(sizeCategory: sizeCategory)]
).em() + style.measurements.listMarkerSpacing
attributes: [
.font: state.font.monospacedDigit().resolve(sizeCategory: environment.sizeCategory)
]
).em() + environment.style.measurements.listMarkerSpacing
)

var itemState = state
itemState.paragraphSpacing = orderedList.tight ? 0 : style.measurements.paragraphSpacing
itemState.paragraphSpacing =
orderedList.tight ? 0 : environment.style.measurements.paragraphSpacing
itemState.headIndent += headIndentStep
itemState.tabStops.append(
contentsOf: [
.init(
textAlignment: .trailing(baseWritingDirection),
location: itemState.headIndent - style.measurements.listMarkerSpacing
textAlignment: .trailing(environment.baseWritingDirection),
location: itemState.headIndent - environment.style.measurements.listMarkerSpacing
),
.init(textAlignment: .natural, location: itemState.headIndent),
]
Expand Down Expand Up @@ -258,8 +258,8 @@ extension AttributedStringRenderer {
state: State
) -> NSAttributedString {
var state = state
state.font = state.font.scale(style.measurements.codeFontScale).monospaced()
state.headIndent += style.measurements.headIndentStep
state.font = state.font.scale(environment.style.measurements.codeFontScale).monospaced()
state.headIndent += environment.style.measurements.headIndentStep
state.tabStops.append(
.init(textAlignment: .natural, location: state.headIndent)
)
Expand Down Expand Up @@ -313,14 +313,14 @@ extension AttributedStringRenderer {

var inlineState = state
inlineState.font = inlineState.font.bold().scale(
style.measurements.headingScales[heading.level - 1]
environment.style.measurements.headingScales[heading.level - 1]
)

result.append(renderInlines(heading.text, state: inlineState))

// The paragraph spacing is relative to the parent font
var paragraphState = state
paragraphState.paragraphSpacing = style.measurements.headingSpacing
paragraphState.paragraphSpacing = environment.style.measurements.headingSpacing

result.addAttribute(
.paragraphStyle,
Expand All @@ -342,7 +342,7 @@ extension AttributedStringRenderer {
.init(
string: .nbsp,
attributes: [
.font: state.font.resolve(sizeCategory: sizeCategory),
.font: state.font.resolve(sizeCategory: environment.sizeCategory),
.strikethroughStyle: NSUnderlineStyle.single.rawValue,
.strikethroughColor: PlatformColor.separator,
]
Expand Down Expand Up @@ -425,7 +425,7 @@ extension AttributedStringRenderer {
NSAttributedString(
string: text,
attributes: [
.font: state.font.resolve(sizeCategory: sizeCategory),
.font: state.font.resolve(sizeCategory: environment.sizeCategory),
.foregroundColor: PlatformColor(state.foregroundColor),
]
)
Expand All @@ -441,7 +441,7 @@ extension AttributedStringRenderer {

private func renderInlineCode(_ inlineCode: InlineCode, state: State) -> NSAttributedString {
var state = state
state.font = state.font.scale(style.measurements.codeFontScale).monospaced()
state.font = state.font.scale(environment.style.measurements.codeFontScale).monospaced()
return renderText(inlineCode.code, state: state)
}

Expand All @@ -466,7 +466,7 @@ extension AttributedStringRenderer {
let absoluteURL =
link.url
.map(\.relativeString)
.flatMap { URL(string: $0, relativeTo: baseURL) }
.flatMap { URL(string: $0, relativeTo: environment.baseURL) }
.map(\.absoluteURL)
if let url = absoluteURL {
result.addAttribute(.link, value: url, range: NSRange(0..<result.length))
Expand All @@ -483,19 +483,20 @@ extension AttributedStringRenderer {
private func renderImage(_ image: CommonMark.Image, state: State) -> NSAttributedString {
image.url
.map(\.relativeString)
.flatMap { URL(string: $0, relativeTo: baseURL) }
.flatMap { URL(string: $0, relativeTo: environment.baseURL) }
.map(\.absoluteURL)
.map {
NSAttributedString(markdownImageURL: $0)
} ?? NSAttributedString()
}

private func paragraphStyle(state: State) -> NSParagraphStyle {
let pointSize = state.font.resolve(sizeCategory: sizeCategory).pointSize
let pointSize = state.font.resolve(sizeCategory: environment.sizeCategory).pointSize
let result = NSMutableParagraphStyle()
result.setParagraphStyle(.default)
result.baseWritingDirection = baseWritingDirection
result.alignment = alignment
result.baseWritingDirection = environment.baseWritingDirection
result.alignment = environment.alignment
result.lineSpacing = environment.lineSpacing
result.paragraphSpacing = round(pointSize * state.paragraphSpacing)
result.headIndent = round(pointSize * state.headIndent)
result.tailIndent = round(pointSize * state.tailIndent)
Expand Down
Loading