Add documentation

This commit is contained in:
Guille Gonzalez
2020-12-22 19:12:09 +01:00
parent 18b9f76fda
commit db6078d4eb
20 changed files with 493 additions and 364 deletions

View File

@@ -7,11 +7,11 @@ struct AllExamplesView: View {
private var content: some View {
List {
ForEach(examples) { example in
Markdown(example.content)
Markdown(example.document)
.padding()
.documentStyle(
.markdownStyle(
example.useDefaultStyle
? DocumentStyle(font: .system(.body))
? MarkdownStyle(font: .system(.body))
: .alternative
)
}

View File

@@ -1,10 +1,10 @@
import CommonMark
import Foundation
import MarkdownUI
struct Example: Identifiable, Hashable {
var id: String
var title: String
var content: String
var document: Document
var useDefaultStyle = true
}
@@ -15,7 +15,7 @@ extension Example {
static let text = Example(
id: "text",
title: "Text",
content: #"""
document: #"""
It's very easy to make some words **bold** and other words *italic* with Markdown.
**Want to experiment with Markdown?** Play with the [reference CommonMark
@@ -26,7 +26,7 @@ extension Example {
static let lists = Example(
id: "lists",
title: "Lists",
content: #"""
document: #"""
Sometimes you want numbered lists:
1. One
@@ -50,7 +50,7 @@ extension Example {
static let images = Example(
id: "images",
title: "Images",
content: #"""
document: #"""
If you want to embed images, this is how you do it:
![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")
@@ -60,7 +60,7 @@ extension Example {
static let headers = Example(
id: "headers",
title: "Headers & Quotes",
content: #"""
document: #"""
# Structured documents
Sometimes it's useful to have different levels of headings to structure your documents. Start lines with a `#` to create headings. Multiple `##` in a row denote smaller heading sizes.
@@ -79,7 +79,7 @@ extension Example {
static let code = Example(
id: "code",
title: "Code",
content: #"""
document: #"""
There are many different ways to style code with CommonMark. If you have inline code blocks, wrap them in backticks: `var example = true`. If you've got a longer block of code, you can indent with four spaces:
if isAwesome {
@@ -99,15 +99,15 @@ extension Example {
static let style = Example(
id: "style",
title: "Style",
content: #"""
document: #"""
## Document Style
By default, MardownUI renders markdown strings using the system fonts and reasonable defaults for paragraph spacing, indent size, heading size, etc.
If you don't want to use the default style, you can provide a custom `DocumentStyle` by using the `documentStyle()` modifier:
If you don't want to use the default style, you can provide a custom `MarkdownStyle` by using the `markdownStyle()` modifier:
```
.documentStyle(
DocumentStyle(
.markdownStyle(
MarkdownStyle(
font: .system(
.body,
design: .serif

View File

@@ -8,7 +8,7 @@ struct ExampleView: View {
ScrollView {
VStack(spacing: 0) {
HStack {
Text(example.content)
Text(example.document.description)
Spacer()
}
.font(.system(.callout, design: .monospaced))
@@ -17,7 +17,7 @@ struct ExampleView: View {
Divider()
Markdown(example.content)
Markdown(example.document)
.padding()
}
.clipShape(RoundedRectangle(cornerRadius: 4))
@@ -28,7 +28,7 @@ struct ExampleView: View {
.padding()
}
.navigationTitle(example.title)
.documentStyle(example.useDefaultStyle ? DocumentStyle(font: .system(.body)) : .alternative)
.markdownStyle(example.useDefaultStyle ? MarkdownStyle(font: .system(.body)) : .alternative)
}
var body: some View {
@@ -40,9 +40,13 @@ struct ExampleView: View {
}
}
extension DocumentStyle {
static var alternative: DocumentStyle {
DocumentStyle(font: .system(.body, design: .serif), codeFontName: "Menlo")
extension MarkdownStyle {
static var alternative: MarkdownStyle {
MarkdownStyle(
font: .system(.body, design: .serif),
codeFontName: "Menlo",
codeFontSize: .em(0.88)
)
}
}

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2020 Guillermo Gonzalez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,17 +1,35 @@
import cmark
import Foundation
/// A CommonMark document.
public struct Document {
/// A CommonMark document inline.
public enum Inline: Equatable {
/// Plain textual content.
case text(String)
/// Soft line break.
case softBreak
/// Hard line break.
case lineBreak
/// Code span.
case code(String)
/// Raw HTML.
case html(String)
case custom(String)
/// Emphasis.
case emphasis([Inline])
/// Strong emphasis.
case strong([Inline])
/// Link.
case link([Inline], url: String, title: String = "")
/// Image.
case image([Inline], url: String, title: String = "")
init(node: Node) {
@@ -26,8 +44,6 @@ public struct Document {
self = .code(node.literal!)
case CMARK_NODE_HTML_INLINE:
self = .html(node.literal!)
case CMARK_NODE_CUSTOM_INLINE:
self = .custom(node.literal!)
case CMARK_NODE_EMPH:
self = .emphasis(node.children.map(Inline.init))
case CMARK_NODE_STRONG:
@@ -42,7 +58,9 @@ public struct Document {
}
}
/// A CommonMark list.
public struct List: Equatable {
/// List style.
public enum Style: Equatable {
case bullet, ordered
@@ -56,6 +74,7 @@ public struct Document {
}
}
/// A single list item.
public struct Item: Equatable {
public let blocks: [Block]
@@ -69,9 +88,16 @@ public struct Document {
}
}
/// The items in this list.
public let items: [Item]
/// The list style.
public let style: Style
/// The start index in an ordered list.
public let start: Int
/// Whether or not this list has tight or loose spacing between its items.
public let isTight: Bool
public init(items: [Item], style: Style, start: Int, isTight: Bool) {
@@ -91,14 +117,27 @@ public struct Document {
}
}
/// A CommonMark document block.
public enum Block: Equatable {
/// A block quote.
case blockQuote([Block])
/// A list.
case list(List)
/// A code block.
case code(String, language: String = "")
/// A group of lines that is treated as raw HTML.
case html(String)
case custom(String)
/// A paragraph.
case paragraph([Inline])
/// A heading.
case heading([Inline], level: Int)
/// A thematic break.
case thematicBreak
init(node: Node) {
@@ -111,8 +150,6 @@ public struct Document {
self = .code(node.literal!, language: node.fenceInfo ?? "")
case CMARK_NODE_HTML_BLOCK:
self = .html(node.literal!)
case CMARK_NODE_CUSTOM_BLOCK:
self = .custom(node.literal!)
case CMARK_NODE_PARAGRAPH:
self = .paragraph(node.children.map(Inline.init))
case CMARK_NODE_HEADING:
@@ -128,15 +165,21 @@ public struct Document {
}
}
/// The blocks that form this document.
public var blocks: [Block] {
node.children.map(Block.init)
}
/// A set with all the image locations contained in this document.
public var imageURLs: Set<String> {
Set(node.imageURLs)
}
private let node: Node
public init(_ content: String) {
node = Node(content)!
}
}
extension Document: Equatable {
@@ -155,28 +198,27 @@ extension Document: Hashable {
}
}
extension Document: LosslessStringConvertible {
public init?(_ description: String) {
guard let node = Node(description) else {
return nil
}
self.node = node
}
extension Document: CustomStringConvertible {
public var description: String {
node.description
}
}
extension Document: ExpressibleByStringInterpolation {
public init(stringLiteral value: StringLiteralType) {
self.init(value)
}
}
extension Document: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let description = try container.decode(String.self)
let content = try container.decode(String.self)
guard let node = Node(description) else {
guard let node = Node(content) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid document: \(description)"
debugDescription: "Invalid document: \(content)"
)
}

View File

@@ -77,7 +77,7 @@ class Node {
}
convenience init?(_ cmark: String) {
guard let node = cmark_parse_document(cmark, cmark.utf8.count, 0) else {
guard let node = cmark_parse_document(cmark, cmark.utf8.count, CMARK_OPT_SMART) else {
return nil
}
self.init(node)

View File

@@ -3,22 +3,26 @@
import AppKit
public extension NSFont {
/// Create a system font by specifying the size and weight.
static func system(size: CGFloat, weight: Weight = .regular) -> NSFont {
.systemFont(ofSize: size, weight: weight)
}
/// Create a system font by specifying the size, weight, and a type design together.
@available(macOS 10.15, *)
static func system(size: CGFloat, weight: Weight = .regular, design: NSFontDescriptor.SystemDesign) -> NSFont {
let font = NSFont.systemFont(ofSize: size, weight: weight)
return font.withDesign(design) ?? font
}
/// Create a system font with the given style and design.
@available(macOS 11.0, *)
static func system(_ style: TextStyle, design: NSFontDescriptor.SystemDesign = .default) -> NSFont {
let font = NSFont.preferredFont(forTextStyle: style)
return font.withDesign(design) ?? font
}
/// Create a custom font with the given name and a fixed size.
static func custom(_ name: String, size: CGFloat) -> NSFont {
NSFont(name: name, size: size) ?? .system(size: size)
}

View File

@@ -1,21 +1,25 @@
import CoreGraphics
import Foundation
/// A quantity that can be expressed in points or relative to the font size.
public enum Dimension: Equatable {
/// Absolute points.
case points(CGFloat)
/// Relative to the font-size (.em(2) means 2 times the size of the current font)
case em(CGFloat)
var points: CGFloat? {
public var points: CGFloat? {
guard case let .points(value) = self else { return nil }
return value
}
var em: CGFloat? {
public var em: CGFloat? {
guard case let .em(value) = self else { return nil }
return value
}
func resolve(_ parentSize: CGFloat) -> CGFloat {
public func resolve(_ parentSize: CGFloat) -> CGFloat {
switch self {
case let .points(value):
return value

View File

@@ -8,11 +8,20 @@ import CommonMark
public extension NSAttributedString {
#if !os(watchOS)
convenience init(document: Document, attachments: [String: NSTextAttachment] = [:], style: DocumentStyle) {
/// Create an attributed string from a CommonMark document.
/// - Parameters:
/// - document: A CommonMark document.
/// - attachments: A dictionary of text attachments, keyed by the URL strings corresponding to images in the CommonMark document.
/// - style: A document style describing how the document is going to be rendered.
convenience init(document: Document, attachments: [String: NSTextAttachment] = [:], style: MarkdownStyle) {
self.init(attributedString: document.attributedString(attachments: attachments, style: style))
}
#else
convenience init(document: Document, style: DocumentStyle) {
/// Create an attributed string from a CommonMark document.
/// - Parameters:
/// - document: A CommonMark document.
/// - style: A document style describing how the document is going to be rendered.
convenience init(document: Document, style: MarkdownStyle) {
self.init(attributedString: document.attributedString(style: style))
}
#endif
@@ -20,11 +29,18 @@ public extension NSAttributedString {
public extension Document {
#if !os(watchOS)
func attributedString(attachments: [String: NSTextAttachment] = [:], style: DocumentStyle) -> NSAttributedString {
/// Create an attributed string from this document.
/// - Parameters:
/// - attachments: A dictionary of text attachments, keyed by the URL strings corresponding to images in this document.
/// - style: A document style describing how the document is going to be rendered.
func attributedString(attachments: [String: NSTextAttachment] = [:], style: MarkdownStyle) -> NSAttributedString {
blocks.attributedString(context: RenderContext(attachments: attachments, style: style))
}
#else
func attributedString(style: DocumentStyle) -> NSAttributedString {
/// Create an attributed string from this document.
/// - Parameters:
/// - style: A document style describing how the document is going to be rendered.
func attributedString(style: MarkdownStyle) -> NSAttributedString {
blocks.attributedString(context: RenderContext(style: style))
}
#endif
@@ -49,8 +65,6 @@ extension Document.Inline {
return NSAttributedString(string: text, attributes: context.code().attributes)
case let .html(text):
return NSAttributedString(string: text, attributes: context.attributes)
case let .custom(text):
return NSAttributedString(string: text, attributes: context.attributes)
case let .emphasis(inlines):
return inlines.attributedString(context: context.emphasis())
case let .strong(inlines):
@@ -115,8 +129,6 @@ extension Document.Block {
return result
}
return NSAttributedString()
case .custom:
return NSAttributedString()
case let .paragraph(inlines):
return inlines.attributedString(context: context.paragraph())

View File

@@ -4,10 +4,18 @@
import UIKit
#endif
/// A set of values that control the appearance of headings in Markdown views.
public struct HeadingStyle: Equatable {
/// The font size of this heading.
public var fontSize: Dimension
/// The text alignment of this heading.
public var alignment: NSTextAlignment
/// The space after this heading.
public var spacing: Dimension
/// Whether or not this heading uses a bold font.
public var isBold: Bool
public init(

View File

@@ -4,25 +4,26 @@
@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)
public extension View {
/// Sets the markdown style in this view and its children.
@available(macOS 11.0, *)
func documentStyle(_ documentStyle: @autoclosure @escaping () -> DocumentStyle) -> some View {
environment(\.documentStyle, documentStyle)
func markdownStyle(_ markdownStyle: @autoclosure @escaping () -> MarkdownStyle) -> some View {
environment(\.markdownStyle, markdownStyle)
}
}
@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)
extension EnvironmentValues {
@available(macOS 11.0, *)
var documentStyle: () -> DocumentStyle {
get { self[DocumentStyleKey.self] }
set { self[DocumentStyleKey.self] = newValue }
var markdownStyle: () -> MarkdownStyle {
get { self[MarkdownStyleKey.self] }
set { self[MarkdownStyleKey.self] = newValue }
}
}
@available(macOS 11.0, iOS 13.0, tvOS 13.0, *)
private struct DocumentStyleKey: EnvironmentKey {
static let defaultValue: () -> DocumentStyle = {
DocumentStyle(font: .system(.body))
private struct MarkdownStyleKey: EnvironmentKey {
static let defaultValue: () -> MarkdownStyle = {
MarkdownStyle(font: .system(.body))
}
}

View File

@@ -5,30 +5,61 @@
import NetworkImage
import SwiftUI
/// A view that displays Markdown formatted text.
///
/// A Markdown view displays formatted text using the Markdown syntax, fully compliant
/// with the [CommonMark Spec](https://spec.commonmark.org/current/).
///
/// Markdown("It's very easy to make some words **bold** and other words *italic* with Markdown.")
///
/// A Markdown view renders text using a `body` font appropriate for the current platform.
/// You can choose a different font or customize other properties like the foreground color,
/// paragraph spacing, or heading styles using the `markdownStyle(_:)` view modifier.
///
/// Markdown("If you have inline code blocks, wrap them in backticks: `var example = true`.")
/// .markdownStyle(
/// MarkdownStyle(
/// font: .custom("Helvetica Neue", size: 14),
/// foregroundColor: .gray
/// codeFontName: "Menlo"
/// )
/// )
///
/// Use the `accentColor(_:)` view modifier to configure the link color.
///
/// Markdown("Play with the [reference CommonMark implementation](https://spec.commonmark.org/dingus/).")
/// .accentColor(.purple)
///
/// A Markdown view always uses all the available width and adjusts its height to fit its
/// rendered text.
///
/// Use modifiers like `lineLimit(_:)` and `truncationMode(_:)` to configure
/// how the view handles space constraints.
///
/// Markdown("> Knowledge is power, Francis Bacon.")
/// .lineLimit(1)
@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)
public struct Markdown: View {
@Environment(\.sizeCategory) private var sizeCategory: ContentSizeCategory
@Environment(\.documentStyle) private var documentStyle: () -> DocumentStyle
@Environment(\.markdownStyle) private var markdownStyle: () -> MarkdownStyle
@StateObject private var store = MarkdownStore()
private let document: Document?
private let document: Document
public init(_ content: String) {
self.init(document: Document(content))
}
public init(document: Document?) {
/// Creates a Markdown view that displays a CommonMark document.
/// - Parameter document: The CommonMark document to display.
public init(_ document: Document) {
self.document = document
}
public var body: some View {
AttributedText(store.attributedText)
.onChange(of: sizeCategory) { _ in
store.setDocument(document, style: documentStyle())
store.setDocument(document, style: markdownStyle())
}
.onAppear {
store.setDocument(document, style: documentStyle())
store.setDocument(document, style: markdownStyle())
}
}
}

View File

@@ -10,10 +10,10 @@
@Published private(set) var attributedText = NSAttributedString()
private var document: Document?
private var style: DocumentStyle?
private var style: MarkdownStyle?
private var cancellable: AnyCancellable?
func setDocument(_ document: Document?, style: DocumentStyle) {
func setDocument(_ document: Document?, style: MarkdownStyle) {
guard self.document != document || self.style != style else {
return
}

View File

@@ -4,91 +4,7 @@
import UIKit
#endif
public struct DocumentStyle: Equatable {
#if os(macOS)
public typealias Font = NSFont
public typealias Color = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
public typealias Font = UIFont
public typealias Color = UIColor
#endif
public var font: Font
public var foregroundColor: Color
public var alignment: NSTextAlignment
public var lineHeight: Dimension
public var paragraphSpacing: Dimension
public var indentSize: Dimension
public var codeFontName: String?
public var codeFontSize: Dimension
public var headingStyles: [HeadingStyle]
public var thematicBreakStyle: ThematicBreakStyle
public init(
font: Font,
foregroundColor: Color,
alignment: NSTextAlignment = .natural,
lineHeight: Dimension = .em(1),
paragraphSpacing: Dimension = .em(1),
indentSize: Dimension = .em(1),
codeFontName: String? = nil,
codeFontSize: Dimension = .em(0.88),
headingStyles: [HeadingStyle] = HeadingStyle.default,
thematicBreakStyle: ThematicBreakStyle = .default
) {
self.font = font
self.foregroundColor = foregroundColor
self.alignment = alignment
self.lineHeight = lineHeight
self.paragraphSpacing = paragraphSpacing
self.indentSize = indentSize
self.codeFontName = codeFontName
self.codeFontSize = codeFontSize
self.thematicBreakStyle = thematicBreakStyle
self.headingStyles = headingStyles
}
}
@available(macOS 11.0, iOS 13.0, tvOS 13.0, *)
public extension DocumentStyle {
@available(watchOS, unavailable)
init(
font: Font,
alignment: NSTextAlignment = .natural,
lineHeight: Dimension = .em(1),
paragraphSpacing: Dimension = .em(1),
indentSize: Dimension = .em(1),
codeFontName: String? = nil,
codeFontSize: Dimension = .em(0.88),
headingStyles: [HeadingStyle] = HeadingStyle.default,
thematicBreakStyle: ThematicBreakStyle = .default
) {
#if os(watchOS)
fatalError("unavailable!")
#else
#if os(macOS)
let foregroundColor = NSColor.labelColor
#else
let foregroundColor = UIColor.label
#endif
self.init(
font: font,
foregroundColor: foregroundColor,
alignment: alignment,
lineHeight: lineHeight,
paragraphSpacing: paragraphSpacing,
indentSize: indentSize,
codeFontName: codeFontName,
codeFontSize: codeFontSize,
headingStyles: headingStyles,
thematicBreakStyle: thematicBreakStyle
)
#endif
}
}
extension DocumentStyle {
extension MarkdownStyle {
func paragraphStyle(indentLevel: Int, options: ParagraphOptions) -> NSParagraphStyle {
let paragraphStyle = NSMutableParagraphStyle()

View File

@@ -0,0 +1,109 @@
#if os(macOS)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif
/// A set of values that control the appearance of Markdown views.
public struct MarkdownStyle: Equatable {
#if os(macOS)
public typealias Font = NSFont
public typealias Color = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
public typealias Font = UIFont
public typealias Color = UIColor
#endif
/// The base font used to render the document.
public var font: Font
/// The text color.
public var foregroundColor: Color
/// The text alignment.
public var alignment: NSTextAlignment
/// The line height.
public var lineHeight: Dimension
/// The space after the end of the paragraph.
public var paragraphSpacing: Dimension
/// The indent size for quotes, lists and code blocks.
public var indentSize: Dimension
/// The code font name. If `nil` the system monospaced font is used.
public var codeFontName: String?
/// The code font size.
public var codeFontSize: Dimension
/// The heading styles.
public var headingStyles: [HeadingStyle]
/// The thematic break style.
public var thematicBreakStyle: ThematicBreakStyle
public init(
font: Font,
foregroundColor: Color,
alignment: NSTextAlignment = .natural,
lineHeight: Dimension = .em(1),
paragraphSpacing: Dimension = .em(1),
indentSize: Dimension = .em(1),
codeFontName: String? = nil,
codeFontSize: Dimension = .em(0.94),
headingStyles: [HeadingStyle] = HeadingStyle.default,
thematicBreakStyle: ThematicBreakStyle = .default
) {
self.font = font
self.foregroundColor = foregroundColor
self.alignment = alignment
self.lineHeight = lineHeight
self.paragraphSpacing = paragraphSpacing
self.indentSize = indentSize
self.codeFontName = codeFontName
self.codeFontSize = codeFontSize
self.thematicBreakStyle = thematicBreakStyle
self.headingStyles = headingStyles
}
}
@available(macOS 11.0, iOS 13.0, tvOS 13.0, *)
public extension MarkdownStyle {
@available(watchOS, unavailable)
init(
font: Font,
alignment: NSTextAlignment = .natural,
lineHeight: Dimension = .em(1),
paragraphSpacing: Dimension = .em(1),
indentSize: Dimension = .em(1),
codeFontName: String? = nil,
codeFontSize: Dimension = .em(0.94),
headingStyles: [HeadingStyle] = HeadingStyle.default,
thematicBreakStyle: ThematicBreakStyle = .default
) {
#if os(watchOS)
fatalError("unavailable!")
#else
#if os(macOS)
let foregroundColor = NSColor.labelColor
#else
let foregroundColor = UIColor.label
#endif
self.init(
font: font,
foregroundColor: foregroundColor,
alignment: alignment,
lineHeight: lineHeight,
paragraphSpacing: paragraphSpacing,
indentSize: indentSize,
codeFontName: codeFontName,
codeFontSize: codeFontSize,
headingStyles: headingStyles,
thematicBreakStyle: thematicBreakStyle
)
#endif
}
}

View File

@@ -11,17 +11,17 @@ struct RenderContext {
let attachments: [String: NSTextAttachment]
#endif
let style: DocumentStyle
let style: MarkdownStyle
private var paragraphOptions: ParagraphOptions = []
private var indentLevel = 0
private var currentFont: DocumentStyle.Font? {
get { attributes[.font] as? DocumentStyle.Font }
private var currentFont: MarkdownStyle.Font? {
get { attributes[.font] as? MarkdownStyle.Font }
set { attributes[.font] = newValue }
}
#if !os(watchOS)
init(attachments: [String: NSTextAttachment], style: DocumentStyle) {
init(attachments: [String: NSTextAttachment], style: MarkdownStyle) {
self.attachments = attachments
self.style = style
@@ -35,7 +35,7 @@ struct RenderContext {
attachments[url]
}
#else
init(style: DocumentStyle) {
init(style: MarkdownStyle) {
self.style = style
attributes = [.font: style.font]
}

View File

@@ -4,6 +4,7 @@
import UIKit
#endif
/// A set of values that control the appearance of thematic breaks in Markdown views.
public struct ThematicBreakStyle: Equatable {
public var text: String
public var alignment: NSTextAlignment

View File

@@ -3,26 +3,31 @@
import UIKit
public extension UIFont {
/// Create a system font by specifying the size and weight.
static func system(size: CGFloat, weight: Weight = .regular) -> UIFont {
.systemFont(ofSize: size, weight: weight)
}
/// Create a system font by specifying the size, weight, and a type design together.
@available(iOS 13.0, tvOS 13.0, watchOS 7.0, *)
static func system(size: CGFloat, weight: Weight = .regular, design: UIFontDescriptor.SystemDesign) -> UIFont {
let font = UIFont.systemFont(ofSize: size, weight: weight)
return font.withDesign(design) ?? font
}
/// Create a system font with the given style.
static func system(_ style: TextStyle) -> UIFont {
.preferredFont(forTextStyle: style)
}
/// Create a system font with the given style and design.
@available(iOS 13.0, tvOS 13.0, watchOS 7.0, *)
static func system(_ style: TextStyle, design: UIFontDescriptor.SystemDesign) -> UIFont {
let font = UIFont.preferredFont(forTextStyle: style)
return font.withDesign(design) ?? font
}
/// Create a custom font with the given name and size that scales with the body text style.
@available(watchOS 4.0, *)
static func custom(_ name: String, size: CGFloat) -> UIFont {
guard let customFont = UIFont(name: name, size: size) else {

View File

@@ -21,7 +21,19 @@ final class DocumentTests: XCTestCase {
let expected = "# **Hello** *world*\n"
// when
let result = Document(text)?.description
let result = Document(text).description
// then
XCTAssertEqual(result, expected)
}
func testEmptyDocument() {
// given
let content = ""
let expected: [Document.Block] = []
// when
let result = Document(content).blocks
// then
XCTAssertEqual(result, expected)
@@ -43,7 +55,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -93,7 +105,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -144,7 +156,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -166,7 +178,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -180,7 +192,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -194,7 +206,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -212,7 +224,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -233,7 +245,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -251,7 +263,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -269,7 +281,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -289,7 +301,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -307,7 +319,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -325,7 +337,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -343,7 +355,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -361,7 +373,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.blocks
let result = Document(text).blocks
// then
XCTAssertEqual(result, expected)
@@ -384,7 +396,7 @@ final class DocumentTests: XCTestCase {
]
// when
let result = Document(text)?.imageURLs
let result = Document(text).imageURLs
// then
XCTAssertEqual(result, expected)

View File

@@ -11,7 +11,7 @@ import XCTest
import MarkdownUI
final class NSAttributedStringTests: XCTestCase {
private let style = DocumentStyle(
private let style = MarkdownStyle(
font: .custom("Helvetica Neue", size: 17),
foregroundColor: .black,
lineHeight: .em(1),
@@ -32,13 +32,11 @@ final class NSAttributedStringTests: XCTestCase {
#endif
func testParagraph() {
let document = Document(
#"""
The sky above the port was the color of television, tuned to a dead channel.
let document: Document = #"""
The sky above the port was the color of television, tuned to a dead channel.
It was a bright cold day in April, and the clocks were striking thirteen.
"""#
)!
It was a bright cold day in April, and the clocks were striking thirteen.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -46,17 +44,15 @@ final class NSAttributedStringTests: XCTestCase {
}
func testRightAlignedParagraph() {
let document = Document(
#"""
The sky above the port was the color of television, tuned to a dead channel.
let document: Document = #"""
The sky above the port was the color of television, tuned to a dead channel.
It was a bright cold day in April, and the clocks were striking thirteen.
"""#
)!
It was a bright cold day in April, and the clocks were striking thirteen.
"""#
let attributedString = NSAttributedString(
document: document,
style: DocumentStyle(
style: MarkdownStyle(
font: .custom("Helvetica Neue", size: 17),
foregroundColor: .black,
alignment: .right
@@ -67,14 +63,12 @@ final class NSAttributedStringTests: XCTestCase {
}
func testParagraphAndLineBreak() {
let document = Document(
#"""
The sky above the port was the color of television,\
tuned to a dead channel.
let document: Document = #"""
The sky above the port was the color of television,\
tuned to a dead channel.
It was a bright cold day in April, and the clocks were striking thirteen.
"""#
)!
It was a bright cold day in April, and the clocks were striking thirteen.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -82,15 +76,13 @@ final class NSAttributedStringTests: XCTestCase {
}
func testBlockQuote() {
let document = Document(
#"""
The quote
let document: Document = #"""
The quote
> Somewhere, something incredible is waiting to be known
> Somewhere, something incredible is waiting to be known
has been ascribed to Carl Sagan.
"""#
)!
has been ascribed to Carl Sagan.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -98,18 +90,16 @@ final class NSAttributedStringTests: XCTestCase {
}
func testNestedBlockQuote() {
let document = Document(
#"""
Blockquotes can be nested, and can also contain other formatting:
let document: Document = #"""
Blockquotes can be nested, and can also contain other formatting:
> Lorem ipsum dolor sit amet,
> consectetur adipiscing elit.
>
> > Ut enim ad minim **veniam**.
>
> Excepteur sint occaecat cupidatat non proident.
"""#
)!
> Lorem ipsum dolor sit amet,
> consectetur adipiscing elit.
>
> > Ut enim ad minim **veniam**.
>
> Excepteur sint occaecat cupidatat non proident.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -117,30 +107,28 @@ final class NSAttributedStringTests: XCTestCase {
}
func testMultipleParagraphList() {
let document = Document(
#"""
Before
1. List item one.
let document: Document = #"""
Before
1. List item one.
List item one continued with a second paragraph.
List item one continued with a second paragraph.
List item continued with a third paragraph.
List item continued with a third paragraph.
1. List item two continued with an open block.
1. List item two continued with an open block.
This paragraph is part of the preceding list item.
This paragraph is part of the preceding list item.
1. This list is nested and does not require explicit item continuation.
1. This list is nested and does not require explicit item continuation.
This paragraph is part of the preceding list item.
This paragraph is part of the preceding list item.
1. List item b.
1. List item b.
This paragraph belongs to item two of the outer list.
This paragraph belongs to item two of the outer list.
After
"""#
)!
After
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -148,21 +136,19 @@ final class NSAttributedStringTests: XCTestCase {
}
func testTightList() {
let document = Document(
#"""
The following sites and projects have adopted CommonMark:
let document: Document = #"""
The following sites and projects have adopted CommonMark:
* Discourse
* GitHub
* GitLab
* Reddit
* Qt
* Stack Overflow / Stack Exchange
* Swift
* Discourse
* GitHub
* GitLab
* Reddit
* Qt
* Stack Overflow / Stack Exchange
* Swift
❤️
"""#
)!
❤️
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -170,18 +156,16 @@ final class NSAttributedStringTests: XCTestCase {
}
func testLooseList() {
let document = Document(
#"""
Before
1. **one**
- a
let document: Document = #"""
Before
1. **one**
- a
- b
1. two
- b
1. two
After
"""#
)!
After
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -189,22 +173,20 @@ final class NSAttributedStringTests: XCTestCase {
}
func testFencedCodeBlock() {
let document = Document(
#"""
Create arrays and dictionaries using brackets (`[]`), and access their elements by writing the index or key in brackets. A comma is allowed after the last element.
```
var shoppingList = ["catfish", "water", "tulips"]
shoppingList[1] = "bottle of water"
let document: Document = #"""
Create arrays and dictionaries using brackets (`[]`), and access their elements by writing the index or key in brackets. A comma is allowed after the last element.
```
var shoppingList = ["catfish", "water", "tulips"]
shoppingList[1] = "bottle of water"
var occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"
```
Arrays automatically grow as you add elements.
"""#
)!
var occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"
```
Arrays automatically grow as you add elements.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -212,19 +194,17 @@ final class NSAttributedStringTests: XCTestCase {
}
func testHTMLBlock() {
let document = Document(
#"""
<table>
<tr>
<td>
hi
</td>
</tr>
</table>
let document: Document = #"""
<table>
<tr>
<td>
hi
</td>
</tr>
</table>
okay.
"""#
)!
okay.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -232,16 +212,14 @@ final class NSAttributedStringTests: XCTestCase {
}
func testHeadings() {
let document = Document(
#"""
# After the Big Bang
A brief summary of time
## Life on earth
10 billion years
## You reading this
13.7 billion years
"""#
)!
let document: Document = #"""
# After the Big Bang
A brief summary of time
## Life on earth
10 billion years
## You reading this
13.7 billion years
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -249,13 +227,11 @@ final class NSAttributedStringTests: XCTestCase {
}
func testHeadingInsideList() {
let document = Document(
#"""
1. # After the Big Bang
1. ## Life on earth
1. ### You reading this
"""#
)!
let document: Document = #"""
1. # After the Big Bang
1. ## Life on earth
1. ### You reading this
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -263,25 +239,23 @@ final class NSAttributedStringTests: XCTestCase {
}
func testThematicBreak() {
let document = Document(
#"""
HTML is the standard markup language for creating Web pages. HTML describes
the structure of a Web page, and consists of a series of elements.
HTML elements tell the browser how to display the content.
let document: Document = #"""
HTML is the standard markup language for creating Web pages. HTML describes
the structure of a Web page, and consists of a series of elements.
HTML elements tell the browser how to display the content.
---
---
CSS is a language that describes how HTML elements are to be displayed on
screen, paper, or in other media. CSS saves a lot of work, because it can
control the layout of multiple web pages all at once.
CSS is a language that describes how HTML elements are to be displayed on
screen, paper, or in other media. CSS saves a lot of work, because it can
control the layout of multiple web pages all at once.
---
---
JavaScript is the programming language of HTML and the Web. JavaScript can
change HTML content and attribute values. JavaScript can change CSS.
JavaScript can hide and show HTML elements, and more.
"""#
)!
JavaScript is the programming language of HTML and the Web. JavaScript can
change HTML content and attribute values. JavaScript can change CSS.
JavaScript can hide and show HTML elements, and more.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -289,15 +263,13 @@ final class NSAttributedStringTests: XCTestCase {
}
func testInlineHTML() {
let document = Document(
#"""
Before
let document: Document = #"""
Before
<a><bab><c2c>
<a><bab><c2c>
After
"""#
)!
After
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -305,7 +277,7 @@ final class NSAttributedStringTests: XCTestCase {
}
func testInlineCode() {
let document = Document(#"When `x = 3`, that means `x + 2 = 5`"#)!
let document: Document = #"When `x = 3`, that means `x + 2 = 5`"#
let attributedString = NSAttributedString(document: document, style: style)
@@ -313,7 +285,7 @@ final class NSAttributedStringTests: XCTestCase {
}
func testInlineCodeStrong() {
let document = Document(#"When `x = 3`, that means **`x + 2 = 5`**"#)!
let document: Document = #"When `x = 3`, that means **`x + 2 = 5`**"#
let attributedString = NSAttributedString(document: document, style: style)
@@ -321,13 +293,11 @@ final class NSAttributedStringTests: XCTestCase {
}
func testStrong() {
let document = Document(
#"""
The music video for Rihannas song **American Oxygen** depicts various
moments from American history, including the inauguration of Barack
Obama.
"""#
)!
let document: Document = #"""
The music video for Rihannas song **American Oxygen** depicts various
moments from American history, including the inauguration of Barack
Obama.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -335,12 +305,10 @@ final class NSAttributedStringTests: XCTestCase {
}
func testEmphasis() {
let document = Document(
#"""
Why, sometimes Ive believed as many as _six_ impossible things before
breakfast.
"""#
)!
let document: Document = #"""
Why, sometimes Ive believed as many as _six_ impossible things before
breakfast.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -348,11 +316,9 @@ final class NSAttributedStringTests: XCTestCase {
}
func testStrongAndEmphasis() {
let document = Document(
#"""
**Everyone _must_ attend the meeting at 5 oclock today.**
"""#
)!
let document: Document = #"""
**Everyone _must_ attend the meeting at 5 oclock today.**
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -360,13 +326,11 @@ final class NSAttributedStringTests: XCTestCase {
}
func testLink() {
let document = Document(
#"""
[Hurricane](https://en.wikipedia.org/wiki/Tropical_cyclone) Erika was the
strongest and longest-lasting tropical cyclone in the 1997 Atlantic
[hurricane](https://en.wikipedia.org/wiki/Tropical_cyclone) season.
"""#
)!
let document: Document = #"""
[Hurricane](https://en.wikipedia.org/wiki/Tropical_cyclone) Erika was the
strongest and longest-lasting tropical cyclone in the 1997 Atlantic
[hurricane](https://en.wikipedia.org/wiki/Tropical_cyclone) season.
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -374,14 +338,12 @@ final class NSAttributedStringTests: XCTestCase {
}
func testLinkWithTitle() {
let document = Document(
#"""
[Hurricane][1] Erika was the strongest and longest-lasting tropical cyclone
in the 1997 Atlantic [hurricane][1] season.
let document: Document = #"""
[Hurricane][1] Erika was the strongest and longest-lasting tropical cyclone
in the 1997 Atlantic [hurricane][1] season.
[1]:https://en.wikipedia.org/wiki/Tropical_cyclone "Tropical cyclone"
"""#
)!
[1]:https://en.wikipedia.org/wiki/Tropical_cyclone "Tropical cyclone"
"""#
let attributedString = NSAttributedString(document: document, style: style)
@@ -389,13 +351,11 @@ final class NSAttributedStringTests: XCTestCase {
}
func testImage() {
let document = Document(
#"""
CommonMark favicon:
let document: Document = #"""
CommonMark favicon:
![](https://commonmark.org/help/images/favicon.png)
"""#
)!
![](https://commonmark.org/help/images/favicon.png)
"""#
let attributedString = NSAttributedString(
document: document,
@@ -409,13 +369,11 @@ final class NSAttributedStringTests: XCTestCase {
}
func testImageWithoutAttachment() {
let document = Document(
#"""
This image does not have an attachment:
let document: Document = #"""
This image does not have an attachment:
![](https://commonmark.org/help/images/unknown.png)
"""#
)!
![](https://commonmark.org/help/images/unknown.png)
"""#
let attributedString = NSAttributedString(document: document, style: style)