Skip to content

Commit

Permalink
Use the environment's line spacing (#95)
Browse files Browse the repository at this point in the history
* Set the environment's line spacing value to the attributed string paragraph style

* Refactor renderer environment
  • Loading branch information
gonzalezreal authored Feb 26, 2022
1 parent b6e88f1 commit 4c16430
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 119 deletions.
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

0 comments on commit 4c16430

Please sign in to comment.