Skip to content
Open
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 @@ -26,4 +26,19 @@ enum FontHelpers {
static func cachedFont(from blockStyle: BlockStyle?) -> UIFont? {
blockStyle?.font
}

static func ensureMonospaced(_ font: UIFont?, configFont: UIFont?) -> UIFont? {
if let configFont {
return configFont
}
guard let font else {
return UIFont.monospacedSystemFont(
ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize,
weight: .regular
)
}

let weight: UIFont.Weight = font.fontDescriptor.symbolicTraits.contains(.traitBold) ? .bold : .regular
return UIFont.monospacedSystemFont(ofSize: font.pointSize, weight: weight)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ final class RendererFactory {
return LinkRenderer(factory: self, config: config)
case .lineBreak:
return LineBreakRenderer()
case .code:
return CodeRenderer(factory: self, config: config)
default:
return childrenOnlyRenderer
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import UIKit

final class CodeRenderer: NodeRenderer {
private let factory: RendererFactory
private let config: MarkdownStyleConfig

init(factory: RendererFactory, config: MarkdownStyleConfig) {
self.factory = factory
self.config = config
}

func render(node: MarkdownASTNode, into output: NSMutableAttributedString, context: RenderContext) {
let start = output.length
factory.renderChildren(of: node, into: output, context: context)

let range = RenderContext.rangeForRenderedContent(in: output, start: start)
guard range.length > 0 else { return }

let codeStyle = config.code

output.enumerateAttributes(in: range, options: []) { attributes, subrange, _ in
let currentFont = (attributes[.font] as? UIFont) ?? FontHelpers.cachedFont(from: context.getBlockStyle())
if let resolvedFont = FontHelpers.ensureMonospaced(currentFont, configFont: codeStyle.font) {
output.addAttribute(.font, value: resolvedFont, range: subrange)
}

if let codeColor = codeStyle.foregroundColor {
output.addAttribute(.foregroundColor, value: codeColor, range: subrange)
}

if let backgroundColor = codeStyle.backgroundColor {
output.addAttribute(.backgroundColor, value: backgroundColor, range: subrange)
}

output.addAttribute(MarkdownAttribute.inlineCode, value: true, range: subrange)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ enum DefaultMarkdownTheme {

Strong()
Emphasis()

Code()
.fontDesign(.monospaced)
.foregroundStyle(Semantic.secondary)
.background(Semantic.quaternary)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import SwiftUI

public struct Code: MarkdownThemeElement {
public var fontSpec: ThemeFontSpec?
public var fontWeight: Font.Weight?
public var fontDesign: Font.Design?
public var foregroundColorSpec: ThemeColorSpec?
public var backgroundColorSpec: ThemeColorSpec?
public var marginTop: CGFloat?
public var marginBottom: CGFloat?
public var lineHeight: CGFloat?
public var textAlignment: TextAlignment?

public init() {
fontDesign = .monospaced
}

public func backgroundStyle(_ color: Color) -> Self {
var copy = self
copy.backgroundColorSpec = ThemeColorModifiers.spec(from: color)
return copy
}

public func backgroundStyle(_ semantic: ThemeColorSpec.SemanticColor) -> Self {
var copy = self
copy.backgroundColorSpec = ThemeColorModifiers.spec(from: semantic)
return copy
}

public func background(_ color: Color) -> Self {
backgroundStyle(color)
}

public func background(_ semantic: ThemeColorSpec.SemanticColor) -> Self {
backgroundStyle(semantic)
}

public func apply(to config: inout MarkdownStyleConfig, traitCollection: UITraitCollection) {
applyElementStyle(to: &config.code, traitCollection: traitCollection)
if let backgroundColorSpec {
config.code.backgroundColor = backgroundColorSpec.resolve(traitCollection: traitCollection)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import UIKit
public struct ElementStyle: Equatable, Sendable {
public var font: UIFont?
public var foregroundColor: UIColor?
public var backgroundColor: UIColor?
public var marginTop: CGFloat?
public var marginBottom: CGFloat?
public var lineHeight: CGFloat?
Expand All @@ -12,6 +13,7 @@ public struct ElementStyle: Equatable, Sendable {
public init(
font: UIFont? = nil,
foregroundColor: UIColor? = nil,
backgroundColor: UIColor? = nil,
marginTop: CGFloat? = nil,
marginBottom: CGFloat? = nil,
lineHeight: CGFloat? = nil,
Expand All @@ -20,6 +22,7 @@ public struct ElementStyle: Equatable, Sendable {
) {
self.font = font
self.foregroundColor = foregroundColor
self.backgroundColor = backgroundColor
self.marginTop = marginTop
self.marginBottom = marginBottom
self.lineHeight = lineHeight
Expand All @@ -30,6 +33,7 @@ public struct ElementStyle: Equatable, Sendable {
public mutating func merge(_ other: ElementStyle) {
if let font = other.font { self.font = font }
if let foregroundColor = other.foregroundColor { self.foregroundColor = foregroundColor }
if let backgroundColor = other.backgroundColor { self.backgroundColor = backgroundColor }
if let marginTop = other.marginTop { self.marginTop = marginTop }
if let marginBottom = other.marginBottom { self.marginBottom = marginBottom }
if let lineHeight = other.lineHeight { self.lineHeight = lineHeight }
Expand All @@ -49,6 +53,7 @@ public struct MarkdownStyleConfig: Equatable, Sendable {
public var link: ElementStyle
public var strong: ElementStyle
public var emphasis: ElementStyle
public var code: ElementStyle

public init(
paragraph: ElementStyle = ElementStyle(),
Expand All @@ -60,7 +65,8 @@ public struct MarkdownStyleConfig: Equatable, Sendable {
heading6: ElementStyle = ElementStyle(),
link: ElementStyle = ElementStyle(),
strong: ElementStyle = ElementStyle(),
emphasis: ElementStyle = ElementStyle()
emphasis: ElementStyle = ElementStyle(),
code: ElementStyle = ElementStyle()
) {
self.paragraph = paragraph
self.heading1 = heading1
Expand All @@ -72,6 +78,7 @@ public struct MarkdownStyleConfig: Equatable, Sendable {
self.link = link
self.strong = strong
self.emphasis = emphasis
self.code = code
}

public mutating func merge(_ other: MarkdownStyleConfig) {
Expand All @@ -85,6 +92,7 @@ public struct MarkdownStyleConfig: Equatable, Sendable {
link.merge(other.link)
strong.merge(other.strong)
emphasis.merge(other.emphasis)
code.merge(other.code)
}

public func headingStyle(for level: Int) -> ElementStyle {
Expand All @@ -110,3 +118,5 @@ public struct MarkdownStyleConfig: Equatable, Sendable {
default: heading1 = style
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,82 @@ final class RendererTests: XCTestCase {
let attributes = result.attributes(at: separatorRange.location, effectiveRange: &effectiveRange)
XCTAssertNotNil(attributes[.font] as? UIFont)
}


func testInlineCodeUsesMonospacedFont() {
let result = MarkdownRenderer.render("Use `code` here", config: config)
XCTAssertTrue(result.string.contains("code"))

var foundMonospaced = false
result.enumerateAttribute(.font, in: NSRange(location: 0, length: result.length)) { value, range, _ in
guard let font = value as? UIFont else { return }
if result.string[range] == "code" {
XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitMonoSpace))
foundMonospaced = true
}
}
XCTAssertTrue(foundMonospaced)
}


func testInlineCodeHasInlineCodeAttribute() {
let result = MarkdownRenderer.render("`code`", config: config)

var found = false
result.enumerateAttribute(MarkdownAttribute.inlineCode, in: NSRange(location: 0, length: result.length)) { value, range, _ in
if result.string[range] == "code", value as? Bool == true {
found = true
}
}
XCTAssertTrue(found)
}


func testInlineCodeAppliesBackgroundColor() {
let result = MarkdownRenderer.render("`code`", config: config)

var foundBackground = false
result.enumerateAttribute(.backgroundColor, in: NSRange(location: 0, length: result.length)) { value, range, _ in
if result.string[range] == "code", value != nil {
foundBackground = true
}
}
XCTAssertTrue(foundBackground)
}


func testInlineCodeThemeOverrideAppliesColor() {
var customConfig = config!
customConfig.code.foregroundColor = .systemPink

let result = MarkdownRenderer.render("`code`", config: customConfig)

var foundPink = false
result.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: result.length)) { value, range, _ in
guard let color = value as? UIColor else { return }
if result.string[range] == "code" {
XCTAssertEqual(color, UIColor.systemPink)
foundPink = true
}
}
XCTAssertTrue(foundPink)
}


func testBoldInlineCodePreservesBoldWeight() {
let result = MarkdownRenderer.render("**`code`**", config: config)

var foundBoldMonospaced = false
result.enumerateAttribute(.font, in: NSRange(location: 0, length: result.length)) { value, range, _ in
guard let font = value as? UIFont else { return }
if result.string[range] == "code" {
XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitMonoSpace))
XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitBold))
foundBoldMonospaced = true
}
}
XCTAssertTrue(foundBoldMonospaced)
}
}

private extension String {
Expand Down