diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml new file mode 100644 index 00000000..212b7587 --- /dev/null +++ b/.github/workflows/swiftlint.yml @@ -0,0 +1,17 @@ +name: SwiftLint + +on: + pull_request: + paths: + - '.github/workflows/swiftlint.yml' + - '.swiftlint.yml' + - '**/*.swift' + +jobs: + SwiftLint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: GitHub Action for SwiftLint + uses: norio-nomura/action-swiftlint@3.2.1 + diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..60722168 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,10 @@ +included: + - Sources/Down + - Tests + +large_tuple: + warning: 3 + error: 4 + +cyclomatic_complexity: + ignores_case_statements: true diff --git a/Down.xcodeproj/project.pbxproj b/Down.xcodeproj/project.pbxproj index 05d3aecf..c2b7cced 100644 --- a/Down.xcodeproj/project.pbxproj +++ b/Down.xcodeproj/project.pbxproj @@ -77,8 +77,8 @@ EE3E7E702604010700170A52 /* StylerTestSuite.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE59C30122ECF0BD006EE8A8 /* StylerTestSuite.swift */; }; EE3E7E812604019300170A52 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = EE3E7E802604019300170A52 /* SnapshotTesting */; }; EE3E7E88260401F000170A52 /* VisitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE64FEEF225BEB3900A35B34 /* VisitorTests.swift */; }; - EE408A39230338B600E5278A /* CGPoint_TranslateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A38230338B600E5278A /* CGPoint_TranslateTests.swift */; }; - EE408A3B2303399B00E5278A /* CGRect_HelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A3A2303399B00E5278A /* CGRect_HelpersTests.swift */; }; + EE408A39230338B600E5278A /* CGPointTranslateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A38230338B600E5278A /* CGPointTranslateTests.swift */; }; + EE408A3B2303399B00E5278A /* CGRectHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A3A2303399B00E5278A /* CGRectHelpersTests.swift */; }; EE408A3D23033B6B00E5278A /* ListItemPrefixGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE408A3C23033B6B00E5278A /* ListItemPrefixGeneratorTests.swift */; }; EE44848B2301E51C0065C836 /* CodeBlockOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE44848A2301E51C0065C836 /* CodeBlockOptions.swift */; }; EE4484912301F2920065C836 /* CGPoint+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4484902301F2920065C836 /* CGPoint+Translate.swift */; }; @@ -267,8 +267,9 @@ D4F948DB1D00A4A800C9C0F6 /* NSAttributedStringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAttributedStringTests.swift; sourceTree = ""; }; EE0E54F72300800E0070C83F /* BlockBackgroundColorAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBackgroundColorAttribute.swift; sourceTree = ""; }; EE335C4922EDC85900648842 /* ListItemStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItemStyleTests.swift; sourceTree = ""; }; - EE408A38230338B600E5278A /* CGPoint_TranslateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPoint_TranslateTests.swift; sourceTree = ""; }; - EE408A3A2303399B00E5278A /* CGRect_HelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRect_HelpersTests.swift; sourceTree = ""; }; + EE39B28026063E0D002C4F8D /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; + EE408A38230338B600E5278A /* CGPointTranslateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPointTranslateTests.swift; sourceTree = ""; }; + EE408A3A2303399B00E5278A /* CGRectHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRectHelpersTests.swift; sourceTree = ""; }; EE408A3C23033B6B00E5278A /* ListItemPrefixGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItemPrefixGeneratorTests.swift; sourceTree = ""; }; EE44848A2301E51C0065C836 /* CodeBlockOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeBlockOptions.swift; sourceTree = ""; }; EE4484902301F2920065C836 /* CGPoint+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Translate.swift"; sourceTree = ""; }; @@ -297,8 +298,6 @@ EEA5C02822F58A0900B91D60 /* DownTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownTextView.swift; sourceTree = ""; }; EEA5C02A22F58B8000B91D60 /* ThematicBreakSyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThematicBreakSyleTests.swift; sourceTree = ""; }; EEA5C02C22F5C96B00B91D60 /* QuoteStripeAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteStripeAttribute.swift; sourceTree = ""; }; - EEBA153C2344845500B54ECB /* DownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownSnapshotTests.swift; sourceTree = ""; }; - EEBA153E2344845500B54ECB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EEBE62EF25E28F3D005CCAD6 /* BundleHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BundleHelper.swift; sourceTree = ""; }; EED8DA8D22BE404F00E54492 /* DownStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownStyler.swift; sourceTree = ""; }; EED8DA8F22BECBAE00E54492 /* NSAttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Helpers.swift"; sourceTree = ""; }; @@ -388,10 +387,10 @@ D4201E761CFA5151008EEC6E = { isa = PBXGroup; children = ( + EE39B28026063E0D002C4F8D /* .swiftlint.yml */, D4201E9B1CFA59A5008EEC6E /* Sources */, D41689B41CFFE6BB00E5802B /* Supporting Files */, D4201EC41CFA59A5008EEC6E /* Tests */, - EEBA153B2344845500B54ECB /* DownSnapshotTests */, D4201E811CFA5151008EEC6E /* Products */, EE54F96D22EB9CE400628683 /* Frameworks */, ); @@ -513,8 +512,8 @@ EE408A3E23033DFE00E5278A /* Helpers */ = { isa = PBXGroup; children = ( - EE408A3A2303399B00E5278A /* CGRect_HelpersTests.swift */, - EE408A38230338B600E5278A /* CGPoint_TranslateTests.swift */, + EE408A3A2303399B00E5278A /* CGRectHelpersTests.swift */, + EE408A38230338B600E5278A /* CGPointTranslateTests.swift */, EE97254222C14B79004D3B3A /* NSAttributedString+HelpersTests.swift */, EE97253D22C130D8004D3B3A /* NSMutableAttributedString+AttributesTests.swift */, ); @@ -621,15 +620,6 @@ path = Styling; sourceTree = ""; }; - EEBA153B2344845500B54ECB /* DownSnapshotTests */ = { - isa = PBXGroup; - children = ( - EEBA153C2344845500B54ECB /* DownSnapshotTests.swift */, - EEBA153E2344845500B54ECB /* Info.plist */, - ); - path = DownSnapshotTests; - sourceTree = ""; - }; EEC752BE22C4AE1300EC729C /* AST */ = { isa = PBXGroup; children = ( @@ -785,6 +775,7 @@ 8A569F3D1E6B3E50008BE2AC /* Headers */, 14C5E33421877CDC00D5380C /* Resources */, 14CEAC38218774BC00039EDF /* Replace Bundle for macOS Platform */, + EED185ED26054F800051E616 /* Swiftlint */, ); buildRules = ( 8AE66BE41F848C3900ED4C98 /* PBXBuildRule */, @@ -904,6 +895,24 @@ shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nRESOURCE_PATH=$SRCROOT/Sources/Down/Resources\n\nFILENAME_IN_BUNDLE=DownView.bundle\n\nBUILD_APP_DIR=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework/Resources\n\necho \"$RESOURCE_PATH\"\necho \"$BUILD_APP_DIR\"\n\nif [ \"$PLATFORM_NAME\" == \"macosx\" ]; then\n echo $BUILD_APP_DIR\n rm -r \"$BUILD_APP_DIR/$FILENAME_IN_BUNDLE/\"\n cp -R \"$RESOURCE_PATH/DownView (macOS).bundle\" \"$BUILD_APP_DIR/$FILENAME_IN_BUNDLE/\"\nfi\n"; showEnvVarsInLog = 0; }; + EED185ED26054F800051E616 /* Swiftlint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = Swiftlint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1016,8 +1025,8 @@ EE8F38CC22BFB2420056270E /* NodeTests.swift in Sources */, EE3E7E3F260400EE00170A52 /* LinkStyleTests.swift in Sources */, EE3E7E38260400E800170A52 /* DownDebugLayoutManagerTests.swift in Sources */, - EE408A39230338B600E5278A /* CGPoint_TranslateTests.swift in Sources */, - EE408A3B2303399B00E5278A /* CGRect_HelpersTests.swift in Sources */, + EE408A39230338B600E5278A /* CGPointTranslateTests.swift in Sources */, + EE408A3B2303399B00E5278A /* CGRectHelpersTests.swift in Sources */, EE3E7E88260401F000170A52 /* VisitorTests.swift in Sources */, 8AFAEB071E6E331700E09B68 /* DownViewTests.swift in Sources */, 8AFAEB061E6E331700E09B68 /* BindingTests.swift in Sources */, diff --git a/Sources/Down/AST/Nodes/BaseNode.swift b/Sources/Down/AST/Nodes/BaseNode.swift index 87aba1e7..00e2b772 100644 --- a/Sources/Down/AST/Nodes/BaseNode.swift +++ b/Sources/Down/AST/Nodes/BaseNode.swift @@ -10,7 +10,9 @@ import Foundation import libcmark public class BaseNode: Node { - + + // MARK: - Properties + public let cmarkNode: CMarkNode public private(set) lazy var children: [Node] = Array(childSequence) @@ -25,9 +27,11 @@ public class BaseNode: Node { } return depth }() - + + // MARK: - Life cycle + init(cmarkNode: CMarkNode) { self.cmarkNode = cmarkNode } - + } diff --git a/Sources/Down/AST/Nodes/BlockQuote.swift b/Sources/Down/AST/Nodes/BlockQuote.swift index 2152f7b8..70caac3a 100644 --- a/Sources/Down/AST/Nodes/BlockQuote.swift +++ b/Sources/Down/AST/Nodes/BlockQuote.swift @@ -13,8 +13,9 @@ public class BlockQuote: BaseNode {} // MARK: - Debug extension BlockQuote: CustomDebugStringConvertible { - + public var debugDescription: String { return "Block Quote" } + } diff --git a/Sources/Down/AST/Nodes/ChildSequence.swift b/Sources/Down/AST/Nodes/ChildSequence.swift index d306c9ad..326a0d1f 100644 --- a/Sources/Down/AST/Nodes/ChildSequence.swift +++ b/Sources/Down/AST/Nodes/ChildSequence.swift @@ -7,13 +7,34 @@ import libcmark -/// Sequence of child nodes +/// Sequence of child nodes. + public struct ChildSequence: Sequence { + + // MARK: - Properties + let node: CMarkNode - public struct Iterator: IteratorProtocol { + // MARK: - Methods + + public func makeIterator() -> Iterator { + return Iterator(node: cmark_node_first_child(node)) + } + +} + +// MARK: - Iterator + +public extension ChildSequence { + + struct Iterator: IteratorProtocol { + + // MARK: - Properties + var node: CMarkNode? + // MARK: - Methods + public mutating func next() -> Node? { guard let node = node else { return nil } defer { self.node = cmark_node_next(node) } @@ -25,10 +46,7 @@ public struct ChildSequence: Sequence { return result } - } - public func makeIterator() -> Iterator { - return Iterator(node: cmark_node_first_child(node)) } -} +} diff --git a/Sources/Down/AST/Nodes/Code.swift b/Sources/Down/AST/Nodes/Code.swift index da2b9e51..6a7c858b 100644 --- a/Sources/Down/AST/Nodes/Code.swift +++ b/Sources/Down/AST/Nodes/Code.swift @@ -9,17 +9,21 @@ import Foundation import libcmark public class Code: BaseNode { - + + // MARK: - Properties + /// The code content, if present. + public private(set) lazy var literal: String? = cmarkNode.literal - + } // MARK: - Debug extension Code: CustomDebugStringConvertible { - + public var debugDescription: String { return "Code - \(literal ?? "nil")" } + } diff --git a/Sources/Down/AST/Nodes/CodeBlock.swift b/Sources/Down/AST/Nodes/CodeBlock.swift index 2ee306a4..8e64b97a 100644 --- a/Sources/Down/AST/Nodes/CodeBlock.swift +++ b/Sources/Down/AST/Nodes/CodeBlock.swift @@ -9,10 +9,13 @@ import Foundation import libcmark public class CodeBlock: BaseNode { - + + // MARK: - Properties + /// The code content, if present. + public private(set) lazy var literal: String? = cmarkNode.literal - + /// The fence info is an optional string that trails the opening sequence of backticks. /// It can be used to provide some contextual information about the block, such as /// the name of a programming language. @@ -24,16 +27,18 @@ public class CodeBlock: BaseNode { /// ''' /// ``` /// + public private(set) lazy var fenceInfo: String? = cmarkNode.fenceInfo - + } // MARK: - Debug extension CodeBlock: CustomDebugStringConvertible { - + public var debugDescription: String { let content = (literal ?? "nil").replacingOccurrences(of: "\n", with: "\\n") return "Code Block - fenceInfo: \(fenceInfo ?? "nil"), content: \(content)" } + } diff --git a/Sources/Down/AST/Nodes/CustomBlock.swift b/Sources/Down/AST/Nodes/CustomBlock.swift index 2ea85db9..799e2862 100644 --- a/Sources/Down/AST/Nodes/CustomBlock.swift +++ b/Sources/Down/AST/Nodes/CustomBlock.swift @@ -9,17 +9,21 @@ import Foundation import libcmark public class CustomBlock: BaseNode { - + + // MARK: - Properfies + /// The custom content, if present. + public private(set) lazy var literal: String? = cmarkNode.literal - + } // MARK: - Debug extension CustomBlock: CustomDebugStringConvertible { - + public var debugDescription: String { return "Custom Block - \(literal ?? "nil")" } + } diff --git a/Sources/Down/AST/Nodes/CustomInline.swift b/Sources/Down/AST/Nodes/CustomInline.swift index ac524c68..23110934 100644 --- a/Sources/Down/AST/Nodes/CustomInline.swift +++ b/Sources/Down/AST/Nodes/CustomInline.swift @@ -9,16 +9,20 @@ import Foundation import libcmark public class CustomInline: BaseNode { - + + // MARK: - Properties + /// The custom content, if present. + public private(set) lazy var literal: String? = cmarkNode.literal } // MARK: - Debug extension CustomInline: CustomDebugStringConvertible { - + public var debugDescription: String { return "Custom Inline - \(literal ?? "nil")" } + } diff --git a/Sources/Down/AST/Nodes/Document.swift b/Sources/Down/AST/Nodes/Document.swift index 747513f3..fe7574d3 100644 --- a/Sources/Down/AST/Nodes/Document.swift +++ b/Sources/Down/AST/Nodes/Document.swift @@ -9,26 +9,30 @@ import Foundation import libcmark public class Document: BaseNode { - + + // MARK: - Life cycle + deinit { - // Frees the node and all its children. cmark_node_free(cmarkNode) } - + + // MARK: - Methods + /// Accepts the given visitor and return its result. + @discardableResult public func accept(_ visitor: T) -> T.Result { return visitor.visit(document: self) } - -} +} // MARK: - Debug extension Document: CustomDebugStringConvertible { - + public var debugDescription: String { return "Document" } + } diff --git a/Sources/Down/AST/Nodes/Emphasis.swift b/Sources/Down/AST/Nodes/Emphasis.swift index 518f29f1..378113dc 100644 --- a/Sources/Down/AST/Nodes/Emphasis.swift +++ b/Sources/Down/AST/Nodes/Emphasis.swift @@ -13,8 +13,9 @@ public class Emphasis: BaseNode {} // MARK: - Debug extension Emphasis: CustomDebugStringConvertible { - + public var debugDescription: String { return "Emphasis" } + } diff --git a/Sources/Down/AST/Nodes/Heading.swift b/Sources/Down/AST/Nodes/Heading.swift index 0c4df7b9..78ca2f9c 100644 --- a/Sources/Down/AST/Nodes/Heading.swift +++ b/Sources/Down/AST/Nodes/Heading.swift @@ -9,16 +9,20 @@ import Foundation import libcmark public class Heading: BaseNode { - + + // MARK: - Properties + /// The level of the heading, a value between 1 and 6. + public private(set) lazy var headingLevel: Int = cmarkNode.headingLevel } // MARK: - Debug extension Heading: CustomDebugStringConvertible { - + public var debugDescription: String { return "Heading - L\(headingLevel)" } + } diff --git a/Sources/Down/AST/Nodes/HtmlBlock.swift b/Sources/Down/AST/Nodes/HtmlBlock.swift index 181c5ea2..427201d8 100644 --- a/Sources/Down/AST/Nodes/HtmlBlock.swift +++ b/Sources/Down/AST/Nodes/HtmlBlock.swift @@ -9,18 +9,22 @@ import Foundation import libcmark public class HtmlBlock: BaseNode { - + + // MARK: - Properties + /// The html content, if present. + public private(set) lazy var literal: String? = cmarkNode.literal - + } // MARK: - Debug extension HtmlBlock: CustomDebugStringConvertible { - + public var debugDescription: String { let content = (literal ?? "nil").replacingOccurrences(of: "\n", with: "\\n") return "Html Block - content: \(content)" } + } diff --git a/Sources/Down/AST/Nodes/HtmlInline.swift b/Sources/Down/AST/Nodes/HtmlInline.swift index c5be36c7..367d75bc 100644 --- a/Sources/Down/AST/Nodes/HtmlInline.swift +++ b/Sources/Down/AST/Nodes/HtmlInline.swift @@ -9,17 +9,21 @@ import Foundation import libcmark public class HtmlInline: BaseNode { - + + // MARK: - Properties + /// The html tag, if present. + public private(set) lazy var literal: String? = cmarkNode.literal - + } // MARK: - Debug extension HtmlInline: CustomDebugStringConvertible { - + public var debugDescription: String { return "Html Inline - \(literal ?? "nil")" } + } diff --git a/Sources/Down/AST/Nodes/Image.swift b/Sources/Down/AST/Nodes/Image.swift index 07c57f07..15049fd2 100644 --- a/Sources/Down/AST/Nodes/Image.swift +++ b/Sources/Down/AST/Nodes/Image.swift @@ -9,7 +9,9 @@ import Foundation import libcmark public class Image: BaseNode { - + + // MARK: - Properties + /// The title of the image, if present. /// /// In the example below, the first line is a reference link, with the reference at the @@ -21,9 +23,9 @@ public class Image: BaseNode { /// ... /// []: "" /// ``` - /// + public private(set) lazy var title: String? = cmarkNode.title - + /// The url of the image, if present. /// /// For example: @@ -31,16 +33,17 @@ public class Image: BaseNode { /// ``` /// ![<text>](<url>) /// ``` - /// + public private(set) lazy var url: String? = cmarkNode.url - + } // MARK: - Debug extension Image: CustomDebugStringConvertible { - + public var debugDescription: String { return "Image - title: \(title ?? "nil"), url: \(url ?? "nil"))" } + } diff --git a/Sources/Down/AST/Nodes/Item.swift b/Sources/Down/AST/Nodes/Item.swift index 272592c1..e483c311 100644 --- a/Sources/Down/AST/Nodes/Item.swift +++ b/Sources/Down/AST/Nodes/Item.swift @@ -13,8 +13,9 @@ public class Item: BaseNode {} // MARK: - Debug extension Item: CustomDebugStringConvertible { - + public var debugDescription: String { return "Item" } + } diff --git a/Sources/Down/AST/Nodes/LineBreak.swift b/Sources/Down/AST/Nodes/LineBreak.swift index 675227e3..c2ced684 100644 --- a/Sources/Down/AST/Nodes/LineBreak.swift +++ b/Sources/Down/AST/Nodes/LineBreak.swift @@ -13,8 +13,9 @@ public class LineBreak: BaseNode {} // MARK: - Debug extension LineBreak: CustomDebugStringConvertible { - + public var debugDescription: String { return "Line Break" } + } diff --git a/Sources/Down/AST/Nodes/Link.swift b/Sources/Down/AST/Nodes/Link.swift index 84a1758d..398bf9c6 100644 --- a/Sources/Down/AST/Nodes/Link.swift +++ b/Sources/Down/AST/Nodes/Link.swift @@ -9,7 +9,9 @@ import Foundation import libcmark public class Link: BaseNode { - + + // MARK: - Properties + /// The title of the link, if present. /// /// In the example below, the first line is a reference link, with the reference at the @@ -21,9 +23,9 @@ public class Link: BaseNode { /// ... /// [<id>]: <url> "<title>" /// ``` - /// + public private(set) lazy var title: String? = cmarkNode.title - + /// The url of the link, if present. /// /// For example: @@ -31,16 +33,17 @@ public class Link: BaseNode { /// ``` /// [<text>](<url>) /// ``` - /// + public private(set) lazy var url: String? = cmarkNode.url - + } // MARK: - Debug extension Link: CustomDebugStringConvertible { - + public var debugDescription: String { return "Link - title: \(title ?? "nil"), url: \(url ?? "nil"))" } + } diff --git a/Sources/Down/AST/Nodes/List.swift b/Sources/Down/AST/Nodes/List.swift index 37c3a889..aa5dbd79 100644 --- a/Sources/Down/AST/Nodes/List.swift +++ b/Sources/Down/AST/Nodes/List.swift @@ -10,17 +10,21 @@ import libcmark public class List: BaseNode { + // MARK: - Properties + /// The type of the list, either bullet or ordered. + public lazy var listType: ListType = { guard let type = ListType(cmarkNode: cmarkNode) else { assertionFailure("Unsupported or missing list type. Defaulting to .bullet.") return .bullet } - + return type }() - + /// The number of items in the list. + public lazy var numberOfItems: Int = children.count /// Whether the list is "tight". @@ -29,6 +33,7 @@ public class List: BaseNode { /// a hint to render the list with more (loose) or less (tight) spacing between items. public lazy var isTight: Bool = cmark_node_get_list_tight(cmarkNode) == 1 + } // MARK: - List Type @@ -39,6 +44,8 @@ public extension List { case bullet case ordered(start: Int) + // MARK: - Properties + public var debugDescription: String { switch self { case .bullet: return "Bullet" @@ -46,6 +53,8 @@ public extension List { } } + // MARK: - Life cycle + init?(cmarkNode: CMarkNode) { switch cmarkNode.listType { case CMARK_BULLET_LIST: self = .bullet @@ -53,14 +62,16 @@ public extension List { default: return nil } } + } } // MARK: - Debug extension List: CustomDebugStringConvertible { - + public var debugDescription: String { return "List - type: \(listType), isTight: \(isTight)" } + } diff --git a/Sources/Down/AST/Nodes/Node.swift b/Sources/Down/AST/Nodes/Node.swift index 947c2cd7..e0d9fa22 100644 --- a/Sources/Down/AST/Nodes/Node.swift +++ b/Sources/Down/AST/Nodes/Node.swift @@ -10,33 +10,43 @@ import libcmark /// A node is a wrapper of a raw `CMarkNode` belonging to the abstract syntax tree /// generated by cmark. + public protocol Node { + /// The wrapped node. + var cmarkNode: CMarkNode { get } - + /// The wrapped child nodes. + var children: [Node] { get } + } public extension Node { + /// True iff the node has a sibling that succeeds it. + var hasSuccessor: Bool { return cmark_node_next(cmarkNode) != nil } /// Sequence of wrapped child nodes. + var childSequence: ChildSequence { return ChildSequence(node: cmarkNode) } + } // MARK: - Helper extensions public typealias CMarkNode = UnsafeMutablePointer<cmark_node> -public extension UnsafeMutablePointer where Pointee == cmark_node { - +public extension CMarkNode { + /// Wraps the cmark node referred to by this pointer. + func wrap() -> Node? { switch type { case CMARK_NODE_DOCUMENT: return Document(cmarkNode: self) @@ -70,42 +80,43 @@ public extension UnsafeMutablePointer where Pointee == cmark_node { var type: cmark_node_type { return cmark_node_get_type(self) } - + var literal: String? { return String(cString: cmark_node_get_literal(self)) } - + var fenceInfo: String? { return String(cString: cmark_node_get_fence_info(self)) } - + var headingLevel: Int { return Int(cmark_node_get_heading_level(self)) } - + var listType: cmark_list_type { return cmark_node_get_list_type(self) } - + var listStart: Int { return Int(cmark_node_get_list_start(self)) } - + var url: String? { return String(cString: cmark_node_get_url(self)) } - + var title: String? { return String(cString: cmark_node_get_title(self)) } } private extension String { - + init?(cString: UnsafePointer<Int8>?) { guard let unwrapped = cString else { return nil } let result = String(cString: unwrapped) guard !result.isEmpty else { return nil } self = result } + } diff --git a/Sources/Down/AST/Nodes/Paragraph.swift b/Sources/Down/AST/Nodes/Paragraph.swift index d710d6af..488458c5 100644 --- a/Sources/Down/AST/Nodes/Paragraph.swift +++ b/Sources/Down/AST/Nodes/Paragraph.swift @@ -13,8 +13,9 @@ public class Paragraph: BaseNode {} // MARK: - Debug extension Paragraph: CustomDebugStringConvertible { - + public var debugDescription: String { return "Paragraph" } + } diff --git a/Sources/Down/AST/Nodes/SoftBreak.swift b/Sources/Down/AST/Nodes/SoftBreak.swift index 1c61f4e9..031a17a2 100644 --- a/Sources/Down/AST/Nodes/SoftBreak.swift +++ b/Sources/Down/AST/Nodes/SoftBreak.swift @@ -13,8 +13,9 @@ public class SoftBreak: BaseNode {} // MARK: - Debug extension SoftBreak: CustomDebugStringConvertible { - + public var debugDescription: String { return "Soft Break" } + } diff --git a/Sources/Down/AST/Nodes/Strong.swift b/Sources/Down/AST/Nodes/Strong.swift index 74ab339c..e04e3b1f 100644 --- a/Sources/Down/AST/Nodes/Strong.swift +++ b/Sources/Down/AST/Nodes/Strong.swift @@ -13,8 +13,9 @@ public class Strong: BaseNode {} // MARK: - Debug extension Strong: CustomDebugStringConvertible { - + public var debugDescription: String { return "Strong" } + } diff --git a/Sources/Down/AST/Nodes/Text.swift b/Sources/Down/AST/Nodes/Text.swift index dd8bb55c..878c8ba7 100644 --- a/Sources/Down/AST/Nodes/Text.swift +++ b/Sources/Down/AST/Nodes/Text.swift @@ -9,17 +9,21 @@ import Foundation import libcmark public class Text: BaseNode { - + + // MARK: - Properties + /// The text content, if present. + public private(set) lazy var literal: String? = cmarkNode.literal - + } // MARK: - Debug extension Text: CustomDebugStringConvertible { - + public var debugDescription: String { return "Text - \(literal ?? "nil")" } + } diff --git a/Sources/Down/AST/Nodes/ThematicBreak.swift b/Sources/Down/AST/Nodes/ThematicBreak.swift index df20d795..e1672719 100644 --- a/Sources/Down/AST/Nodes/ThematicBreak.swift +++ b/Sources/Down/AST/Nodes/ThematicBreak.swift @@ -13,8 +13,9 @@ public class ThematicBreak: BaseNode {} // MARK: - Debug extension ThematicBreak: CustomDebugStringConvertible { - + public var debugDescription: String { return "Thematic Break" } + } diff --git a/Sources/Down/AST/Styling/Attribute Collections/ColorCollection.swift b/Sources/Down/AST/Styling/Attribute Collections/ColorCollection.swift index 16d05776..3da1e5bb 100644 --- a/Sources/Down/AST/Styling/Attribute Collections/ColorCollection.swift +++ b/Sources/Down/AST/Styling/Attribute Collections/ColorCollection.swift @@ -36,10 +36,13 @@ public protocol ColorCollection { var thematicBreak: DownColor { get } var listItemPrefix: DownColor { get } var codeBlockBackground: DownColor { get } + } public struct StaticColorCollection: ColorCollection { + // MARK: - Properties + public var heading1: DownColor public var heading2: DownColor public var heading3: DownColor @@ -55,6 +58,8 @@ public struct StaticColorCollection: ColorCollection { public var listItemPrefix: DownColor public var codeBlockBackground: DownColor + // MARK: - Life cycle + public init( heading1: DownColor = .black, heading2: DownColor = .black, @@ -86,6 +91,7 @@ public struct StaticColorCollection: ColorCollection { self.listItemPrefix = listItemPrefix self.codeBlockBackground = codeBlockBackground } + } #endif diff --git a/Sources/Down/AST/Styling/Attribute Collections/FontCollection.swift b/Sources/Down/AST/Styling/Attribute Collections/FontCollection.swift index 26ef66d6..e6bdfdef 100644 --- a/Sources/Down/AST/Styling/Attribute Collections/FontCollection.swift +++ b/Sources/Down/AST/Styling/Attribute Collections/FontCollection.swift @@ -31,10 +31,13 @@ public protocol FontCollection { var body: DownFont { get } var code: DownFont { get } var listItemPrefix: DownFont { get } + } public struct StaticFontCollection: FontCollection { + // MARK: - Properties + public var heading1: DownFont public var heading2: DownFont public var heading3: DownFont @@ -45,6 +48,8 @@ public struct StaticFontCollection: FontCollection { public var code: DownFont public var listItemPrefix: DownFont + // MARK: - Life cycle + public init( heading1: DownFont = .boldSystemFont(ofSize: 28), heading2: DownFont = .boldSystemFont(ofSize: 24), @@ -66,6 +71,7 @@ public struct StaticFontCollection: FontCollection { self.code = code self.listItemPrefix = listItemPrefix } + } #endif diff --git a/Sources/Down/AST/Styling/Attribute Collections/ParagraphStyleCollection.swift b/Sources/Down/AST/Styling/Attribute Collections/ParagraphStyleCollection.swift index 808bcc10..eece3888 100644 --- a/Sources/Down/AST/Styling/Attribute Collections/ParagraphStyleCollection.swift +++ b/Sources/Down/AST/Styling/Attribute Collections/ParagraphStyleCollection.swift @@ -28,10 +28,13 @@ public protocol ParagraphStyleCollection { var heading6: NSParagraphStyle { get } var body: NSParagraphStyle { get } var code: NSParagraphStyle { get } + } public struct StaticParagraphStyleCollection: ParagraphStyleCollection { + // MARK: - Properties + public var heading1: NSParagraphStyle public var heading2: NSParagraphStyle public var heading3: NSParagraphStyle @@ -41,6 +44,8 @@ public struct StaticParagraphStyleCollection: ParagraphStyleCollection { public var body: NSParagraphStyle public var code: NSParagraphStyle + // MARK: - Life cycle + public init() { let headingStyle = NSMutableParagraphStyle() headingStyle.paragraphSpacing = 8 @@ -63,6 +68,7 @@ public struct StaticParagraphStyleCollection: ParagraphStyleCollection { body = bodyStyle code = codeStyle } + } #endif diff --git a/Sources/Down/AST/Styling/Custom Attributes/BlockBackgroundColorAttribute.swift b/Sources/Down/AST/Styling/Custom Attributes/BlockBackgroundColorAttribute.swift index d5ff2f76..10a2716e 100644 --- a/Sources/Down/AST/Styling/Custom Attributes/BlockBackgroundColorAttribute.swift +++ b/Sources/Down/AST/Styling/Custom Attributes/BlockBackgroundColorAttribute.swift @@ -20,13 +20,17 @@ import AppKit struct BlockBackgroundColorAttribute { + // MARK: - Properties + var color: DownColor var inset: CGFloat + } extension NSAttributedString.Key { static let blockBackgroundColor = NSAttributedString.Key("blockBackgroundColor") + } #endif diff --git a/Sources/Down/AST/Styling/Custom Attributes/QuoteStripeAttribute.swift b/Sources/Down/AST/Styling/Custom Attributes/QuoteStripeAttribute.swift index e2b2ca22..9feddb34 100644 --- a/Sources/Down/AST/Styling/Custom Attributes/QuoteStripeAttribute.swift +++ b/Sources/Down/AST/Styling/Custom Attributes/QuoteStripeAttribute.swift @@ -20,6 +20,8 @@ import AppKit struct QuoteStripeAttribute { + // MARK: - Properties + var color: DownColor var thickness: CGFloat var spacingAfter: CGFloat @@ -28,25 +30,35 @@ struct QuoteStripeAttribute { var layoutWidth: CGFloat { return thickness + spacingAfter } -} -extension QuoteStripeAttribute { + // MARK: - Life cycle + + init(color: DownColor, thickness: CGFloat, spacingAfter: CGFloat, locations: [CGFloat]) { + self.color = color + self.thickness = thickness + self.spacingAfter = spacingAfter + self.locations = locations + } init(level: Int, color: DownColor, options: QuoteStripeOptions) { self.init(color: color, thickness: options.thickness, spacingAfter: options.spacingAfter, locations: []) locations = (0..<level).map { CGFloat($0) * layoutWidth } } + // MARK: - Methods + func indented(by indentation: CGFloat) -> QuoteStripeAttribute { var copy = self copy.locations = locations.map { $0 + indentation } return copy } + } extension NSAttributedString.Key { - + static let quoteStripe = NSAttributedString.Key(rawValue: "quoteStripe") + } #endif diff --git a/Sources/Down/AST/Styling/Custom Attributes/ThematicBreakAttribute.swift b/Sources/Down/AST/Styling/Custom Attributes/ThematicBreakAttribute.swift index 2c280d60..5111d2b0 100644 --- a/Sources/Down/AST/Styling/Custom Attributes/ThematicBreakAttribute.swift +++ b/Sources/Down/AST/Styling/Custom Attributes/ThematicBreakAttribute.swift @@ -20,13 +20,17 @@ import AppKit struct ThematicBreakAttribute { + // MARK: - Properties + var thickness: CGFloat var color: DownColor + } extension NSAttributedString.Key { - + static let thematicBreak = NSAttributedString.Key(rawValue: "thematicBreak") + } #endif diff --git a/Sources/Down/AST/Styling/Helpers/Extensions/CGPoint+Translate.swift b/Sources/Down/AST/Styling/Helpers/Extensions/CGPoint+Translate.swift index 2ecb5a00..dd4ca4b5 100644 --- a/Sources/Down/AST/Styling/Helpers/Extensions/CGPoint+Translate.swift +++ b/Sources/Down/AST/Styling/Helpers/Extensions/CGPoint+Translate.swift @@ -23,6 +23,7 @@ extension CGPoint { func translated(by point: CGPoint) -> CGPoint { return CGPoint(x: x + point.x, y: y + point.y) } + } #endif diff --git a/Sources/Down/AST/Styling/Helpers/Extensions/CGRect+Helpers.swift b/Sources/Down/AST/Styling/Helpers/Extensions/CGRect+Helpers.swift index 57b2f2c3..daa0bc84 100644 --- a/Sources/Down/AST/Styling/Helpers/Extensions/CGRect+Helpers.swift +++ b/Sources/Down/AST/Styling/Helpers/Extensions/CGRect+Helpers.swift @@ -27,6 +27,7 @@ extension CGRect { func translated(by point: CGPoint) -> CGRect { return CGRect(origin: origin.translated(by: point), size: size) } + } #endif diff --git a/Sources/Down/AST/Styling/Helpers/Extensions/NSAttributedString+Helpers.swift b/Sources/Down/AST/Styling/Helpers/Extensions/NSAttributedString+Helpers.swift index 643f71bb..07958bcd 100644 --- a/Sources/Down/AST/Styling/Helpers/Extensions/NSAttributedString+Helpers.swift +++ b/Sources/Down/AST/Styling/Helpers/Extensions/NSAttributedString+Helpers.swift @@ -12,6 +12,8 @@ extension NSAttributedString { typealias Attributes = [NSAttributedString.Key: Any] + // MARK: - Ranges + var wholeRange: NSRange { return NSRange(location: 0, length: length) } @@ -32,11 +34,11 @@ extension NSAttributedString { return ranges(for: key, in: range, where: { $0 == nil }) } - private func ranges(for key: Key, in range: NSRange, where p: (Any?) -> Bool) -> [NSRange] { + private func ranges(for key: Key, in range: NSRange, where predicate: (Any?) -> Bool) -> [NSRange] { var ranges = [NSRange]() enumerateAttribute(key, in: range, options: []) { value, attrRange, _ in - if p(value) { + if predicate(value) { ranges.append(attrRange) } } @@ -60,6 +62,8 @@ extension NSAttributedString { return result.filter { $0.length > 1 } } + // MARK: - Enumerate attributes + func enumerateAttributes<A>(for key: Key, block: (_ attr: A, _ range: NSRange) -> Void) { enumerateAttributes(for: key, in: wholeRange, block: block) } @@ -71,4 +75,5 @@ extension NSAttributedString { } } } + } diff --git a/Sources/Down/AST/Styling/Helpers/Extensions/NSMutableAttributedString+Attributes.swift b/Sources/Down/AST/Styling/Helpers/Extensions/NSMutableAttributedString+Attributes.swift index 70453060..fdd85082 100644 --- a/Sources/Down/AST/Styling/Helpers/Extensions/NSMutableAttributedString+Attributes.swift +++ b/Sources/Down/AST/Styling/Helpers/Extensions/NSMutableAttributedString+Attributes.swift @@ -35,14 +35,14 @@ extension NSMutableAttributedString { addAttribute(key, value: value, range: range) } - func updateExistingAttributes<A>(for key: Key, using f: (A) -> A) { - updateExistingAttributes(for: key, in: wholeRange, using: f) + func updateExistingAttributes<A>(for key: Key, using transform: (A) -> A) { + updateExistingAttributes(for: key, in: wholeRange, using: transform) } - func updateExistingAttributes<A>(for key: Key, in range: NSRange, using f: (A) -> A) { + func updateExistingAttributes<A>(for key: Key, in range: NSRange, using transform: (A) -> A) { var existingValues = [(value: A, range: NSRange)]() enumerateAttributes(for: key, in: range) { existingValues.append(($0, $1)) } - existingValues.forEach { addAttribute(key, value: f($0.0), range: $0.1) } + existingValues.forEach { addAttribute(key, value: transform($0.0), range: $0.1) } } func addAttributeInMissingRanges<A>(for key: Key, value: A) { @@ -54,4 +54,5 @@ extension NSMutableAttributedString { addAttribute(key, value: value, range: $0) } } + } diff --git a/Sources/Down/AST/Styling/Helpers/Extensions/UIFont+Traits.swift b/Sources/Down/AST/Styling/Helpers/Extensions/UIFont+Traits.swift index 72dc0564..8102dbfd 100644 --- a/Sources/Down/AST/Styling/Helpers/Extensions/UIFont+Traits.swift +++ b/Sources/Down/AST/Styling/Helpers/Extensions/UIFont+Traits.swift @@ -66,6 +66,7 @@ extension DownFont { private func contains(_ trait: DownFontDescriptor.SymbolicTraits) -> Bool { return fontDescriptor.symbolicTraits.contains(trait) } + } #if canImport(UIKit) @@ -75,6 +76,7 @@ private extension DownFontDescriptor.SymbolicTraits { static let strong = DownFontDescriptor.SymbolicTraits.traitBold static let emphasis = DownFontDescriptor.SymbolicTraits.traitItalic static let monoSpace = DownFontDescriptor.SymbolicTraits.traitMonoSpace + } #elseif canImport(AppKit) @@ -84,6 +86,7 @@ private extension DownFontDescriptor.SymbolicTraits { static let strong = DownFontDescriptor.SymbolicTraits.bold static let emphasis = DownFontDescriptor.SymbolicTraits.italic static let monoSpace = DownFontDescriptor.SymbolicTraits.monoSpace + } #endif diff --git a/Sources/Down/AST/Styling/Helpers/ListItemParagraphStyler.swift b/Sources/Down/AST/Styling/Helpers/ListItemParagraphStyler.swift index 1dd37c21..e8d21fea 100644 --- a/Sources/Down/AST/Styling/Helpers/ListItemParagraphStyler.swift +++ b/Sources/Down/AST/Styling/Helpers/ListItemParagraphStyler.swift @@ -20,13 +20,17 @@ import AppKit /// A convenient class used to format lists, such that list item prefixes /// are right aligned and list item content left aligns. + public class ListItemParagraphStyler { + // MARK: - Properties + public var indentation: CGFloat { return largestPrefixWidth + options.spacingAfterPrefix } /// The paragraph style intended for all paragraphs excluding the first. + public var trailingParagraphStyle: NSParagraphStyle { let contentIndentation = indentation let style = baseStyle @@ -45,15 +49,19 @@ public class ListItemParagraphStyler { return style } + // MARK: - Life cycle + public init(options: ListItemOptions, prefixFont: DownFont) { self.options = options self.largestPrefixWidth = prefixFont.widthOfNumberedPrefix(digits: options.maxPrefixDigits) } + // MARK: - Methods /// The paragraph style intended for the first paragraph of the list item. /// /// - Parameter prefixWidth: the width (in points) of the list item prefix. + public func leadingParagraphStyle(prefixWidth: CGFloat) -> NSParagraphStyle { let contentIndentation = indentation let prefixIndentation: CGFloat = contentIndentation - options.spacingAfterPrefix - prefixWidth @@ -70,6 +78,7 @@ public class ListItemParagraphStyler { private func tabStop(at location: CGFloat) -> NSTextTab { return NSTextTab(textAlignment: .left, location: location, options: [:]) } + } // MARK: - Helpers @@ -91,6 +100,7 @@ private extension DownFont { .size() .width } + } private extension Int { @@ -98,6 +108,7 @@ private extension Int { static var decimalDigits: [Int] { return Array(0...9) } + } #endif diff --git a/Sources/Down/AST/Styling/Layout Managers/DownDebugLayoutManager.swift b/Sources/Down/AST/Styling/Layout Managers/DownDebugLayoutManager.swift index e96835dd..97d6f401 100644 --- a/Sources/Down/AST/Styling/Layout Managers/DownDebugLayoutManager.swift +++ b/Sources/Down/AST/Styling/Layout Managers/DownDebugLayoutManager.swift @@ -31,15 +31,18 @@ import AppKit /// of a `DownStyler`. /// /// Insert this into a TextKit stack manually, or use the provided `DownDebugTextView`. + public class DownDebugLayoutManager: DownLayoutManager { + // MARK: - Drawing + override public func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin) drawLineFragments(forGlyphRange: glyphsToShow, at: origin) } private func drawLineFragments(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { - enumerateLineFragments(forGlyphRange: glyphsToShow) { rect, usedRect, textContainer, glyphRange, _ in + enumerateLineFragments(forGlyphRange: glyphsToShow) { rect, usedRect, _, _, _ in [(usedRect, DownColor.blue), (rect, DownColor.red)].forEach { rectToDraw, color in let adjustedRect = rectToDraw.translated(by: origin) self.drawRect(adjustedRect, color: color.cgColor) @@ -55,6 +58,7 @@ public class DownDebugLayoutManager: DownLayoutManager { context.setStrokeColor(color) context.stroke(rect) } + } #endif diff --git a/Sources/Down/AST/Styling/Layout Managers/DownLayoutManager.swift b/Sources/Down/AST/Styling/Layout Managers/DownLayoutManager.swift index dea1078e..6b08ec07 100644 --- a/Sources/Down/AST/Styling/Layout Managers/DownLayoutManager.swift +++ b/Sources/Down/AST/Styling/Layout Managers/DownLayoutManager.swift @@ -21,8 +21,11 @@ import AppKit /// A layout manager capable of drawing the custom attributes set by the `DownStyler`. /// /// Insert this into a TextKit stack manually, or use the provided `DownTextView`. + public class DownLayoutManager: NSLayoutManager { + // MARK: - Graphic context + #if canImport(UIKit) var context: CGContext? { return UIGraphicsGetCurrentContext() @@ -51,13 +54,15 @@ public class DownLayoutManager: NSLayoutManager { #endif + // MARK: - Drawing + override public func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { drawCustomBackgrounds(forGlyphRange: glyphsToShow, at: origin) super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin) drawCustomAttributes(forGlyphRange: glyphsToShow, at: origin) } - private func drawCustomBackgrounds(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { + private func drawCustomBackgrounds(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { guard let context = context else { return } push(context: context) defer { popContext() } @@ -66,17 +71,19 @@ public class DownLayoutManager: NSLayoutManager { let characterRange = self.characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil) - textStorage.enumerateAttributes(for: .blockBackgroundColor, in: characterRange) { (attr: BlockBackgroundColorAttribute, blockRange) in - + textStorage.enumerateAttributes(for: .blockBackgroundColor, + in: characterRange) { (attr: BlockBackgroundColorAttribute, blockRange) in let inset = attr.inset context.setFillColor(attr.color.cgColor) - let allBlockColorRanges = glyphRanges(for: .blockBackgroundColor, in: textStorage, inCharacterRange: blockRange) - let blockColorGlyphRange = glyphRange(forCharacterRange: blockRange, actualCharacterRange: nil) + let allBlockColorRanges = glyphRanges(for: .blockBackgroundColor, + in: textStorage, + inCharacterRange: blockRange) - enumerateLineFragments(forGlyphRange: blockColorGlyphRange) { lineRect, lineUsedRect, container, lineGlyphRange, _ in + let glyphRange = self.glyphRange(forCharacterRange: blockRange, actualCharacterRange: nil) + enumerateLineFragments(forGlyphRange: glyphRange) { lineRect, lineUsedRect, container, lineGlyphRange, _ in let isLineStartOfBlock = allBlockColorRanges.contains { lineGlyphRange.overlapsStart(of: $0) } @@ -89,7 +96,7 @@ public class DownLayoutManager: NSLayoutManager { let maxX = lineRect.maxX let minY = isLineStartOfBlock ? lineUsedRect.minY - inset : lineRect.minY let maxY = isLineEndOfBlock ? lineUsedRect.maxY + inset : lineUsedRect.maxY - let blockRect = CGRect(minX: minX, minY: minY, maxX: maxX, maxY: maxY).translated(by: origin) + let blockRect = CGRect(minX: minX, minY: minY, maxX: maxX, maxY: maxY).translated(by: origin) context.fill(blockRect) } @@ -107,7 +114,9 @@ public class DownLayoutManager: NSLayoutManager { push(context: context) defer { popContext() } - textStorage?.enumerateAttributes(for: .thematicBreak, in: characterRange) { (attr: ThematicBreakAttribute, range) in + textStorage?.enumerateAttributes(for: .thematicBreak, + in: characterRange) { (attr: ThematicBreakAttribute, range) in + let firstGlyphIndex = glyphIndexForCharacter(at: range.lowerBound) let lineRect = lineFragmentRect(forGlyphAt: firstGlyphIndex, effectiveRange: nil) @@ -115,7 +124,10 @@ public class DownLayoutManager: NSLayoutManager { let lineStart = usedRect.minX + fragmentPadding(forGlyphAt: firstGlyphIndex) - let boundingRect = CGRect(x: lineStart, y: lineRect.minY, width: lineRect.width - lineStart, height: lineRect.height) + let width = lineRect.width - lineStart + let height = lineRect.height + + let boundingRect = CGRect(x: lineStart, y: lineRect.minY, width: width, height: height) let adjustedLineRect = boundingRect.translated(by: origin) drawThematicBreak(with: context, in: adjustedLineRect, attr: attr) @@ -140,7 +152,9 @@ public class DownLayoutManager: NSLayoutManager { push(context: context) defer { popContext() } - textStorage?.enumerateAttributes(for: .quoteStripe, in: characterRange) { (attr: QuoteStripeAttribute, quoteRange) in + textStorage?.enumerateAttributes(for: .quoteStripe, + in: characterRange) { (attr: QuoteStripeAttribute, quoteRange) in + context.setFillColor(attr.color.cgColor) let glyphRangeOfQuote = self.glyphRange(forCharacterRange: quoteRange, actualCharacterRange: nil) @@ -165,7 +179,10 @@ public class DownLayoutManager: NSLayoutManager { } } - private func glyphRanges(for key: NSAttributedString.Key, in storage: NSTextStorage, inCharacterRange range: NSRange) -> [NSRange] { + private func glyphRanges(for key: NSAttributedString.Key, + in storage: NSTextStorage, + inCharacterRange range: NSRange) -> [NSRange] { + return storage .ranges(of: key, in: range) .map { self.glyphRange(forCharacterRange: $0, actualCharacterRange: nil) } @@ -184,6 +201,7 @@ private extension NSRange { func overlapsEnd(of range: NSRange) -> Bool { return lowerBound < range.upperBound && upperBound >= range.upperBound } + } private extension Array where Element == NSRange { @@ -207,6 +225,7 @@ private extension Array where Element == NSRange { return result } + } #endif diff --git a/Sources/Down/AST/Styling/Options/CodeBlockOptions.swift b/Sources/Down/AST/Styling/Options/CodeBlockOptions.swift index b0a25bd2..15c6bf71 100644 --- a/Sources/Down/AST/Styling/Options/CodeBlockOptions.swift +++ b/Sources/Down/AST/Styling/Options/CodeBlockOptions.swift @@ -20,11 +20,16 @@ import AppKit public struct CodeBlockOptions { + // MARK: - Properties + public var containerInset: CGFloat + // MARK: - Life cycle + public init(containerInset: CGFloat = 8) { self.containerInset = containerInset } + } #endif diff --git a/Sources/Down/AST/Styling/Options/ListItemOptions.swift b/Sources/Down/AST/Styling/Options/ListItemOptions.swift index d34c1b94..5aeae6f9 100644 --- a/Sources/Down/AST/Styling/Options/ListItemOptions.swift +++ b/Sources/Down/AST/Styling/Options/ListItemOptions.swift @@ -20,21 +20,26 @@ import AppKit public struct ListItemOptions { + // MARK: - Properties + public var maxPrefixDigits: UInt public var spacingAfterPrefix: CGFloat public var spacingAbove: CGFloat public var spacingBelow: CGFloat + // MARK: - Life cycle + public init(maxPrefixDigits: UInt = 2, spacingAfterPrefix: CGFloat = 8, spacingAbove: CGFloat = 4, - spacingBelow: CGFloat = 8 - ) { + spacingBelow: CGFloat = 8) { + self.maxPrefixDigits = maxPrefixDigits self.spacingAfterPrefix = spacingAfterPrefix self.spacingAbove = spacingAbove self.spacingBelow = spacingBelow } + } #endif diff --git a/Sources/Down/AST/Styling/Options/QuoteStripeOptions.swift b/Sources/Down/AST/Styling/Options/QuoteStripeOptions.swift index 9dd70a92..057aa951 100644 --- a/Sources/Down/AST/Styling/Options/QuoteStripeOptions.swift +++ b/Sources/Down/AST/Styling/Options/QuoteStripeOptions.swift @@ -20,13 +20,18 @@ import AppKit public struct QuoteStripeOptions { + // MARK: - Properties + public var thickness: CGFloat public var spacingAfter: CGFloat + // MARK: - Life cycle + public init(thickness: CGFloat = 2, spacingAfter: CGFloat = 8) { self.thickness = thickness self.spacingAfter = spacingAfter } + } #endif diff --git a/Sources/Down/AST/Styling/Options/ThematicBreakOptions.swift b/Sources/Down/AST/Styling/Options/ThematicBreakOptions.swift index 4e901b55..56be3477 100644 --- a/Sources/Down/AST/Styling/Options/ThematicBreakOptions.swift +++ b/Sources/Down/AST/Styling/Options/ThematicBreakOptions.swift @@ -20,13 +20,18 @@ import AppKit public struct ThematicBreakOptions { + // MARK: - Properties + public var thickness: CGFloat public var indentation: CGFloat + // MARK: - Life cycle + public init(thickness: CGFloat = 1, indentation: CGFloat = 0) { self.thickness = thickness self.indentation = indentation } + } #endif diff --git a/Sources/Down/AST/Styling/Stylers/DownStyler.swift b/Sources/Down/AST/Styling/Stylers/DownStyler.swift index 1e15e64a..7ae3611a 100644 --- a/Sources/Down/AST/Styling/Stylers/DownStyler.swift +++ b/Sources/Down/AST/Styling/Stylers/DownStyler.swift @@ -21,6 +21,7 @@ import AppKit /// A default `Styler` implementation that supports a variety of configurable /// properties for font, text color and paragraph styling, as well as formatting /// of nested lists and quotes. + open class DownStyler: Styler { // MARK: - Properties @@ -35,12 +36,12 @@ open class DownStyler: Styler { private let itemParagraphStyler: ListItemParagraphStyler - private var listPrefixAttributes: [NSAttributedString.Key : Any] {[ + private var listPrefixAttributes: [NSAttributedString.Key: Any] {[ .font: fonts.listItemPrefix, .foregroundColor: colors.listItemPrefix] } - // MARK: - Init + // MARK: - Life cycle public init(configuration: DownStylerConfiguration = DownStylerConfiguration()) { fonts = configuration.fonts @@ -49,7 +50,8 @@ open class DownStyler: Styler { quoteStripeOptions = configuration.quoteStripeOptions thematicBreakOptions = configuration.thematicBreakOptions codeBlockOptions = configuration.codeBlockOptions - itemParagraphStyler = ListItemParagraphStyler(options: configuration.listItemOptions, prefixFont: fonts.listItemPrefix) + itemParagraphStyler = ListItemParagraphStyler(options: configuration.listItemOptions, + prefixFont: fonts.listItemPrefix) } // MARK: - Styling @@ -59,7 +61,9 @@ open class DownStyler: Styler { } open func style(blockQuote str: NSMutableAttributedString, nestDepth: Int) { - let stripeAttribute = QuoteStripeAttribute(level: nestDepth + 1, color: colors.quoteStripe, options: quoteStripeOptions) + let stripeAttribute = QuoteStripeAttribute(level: nestDepth + 1, + color: colors.quoteStripe, + options: quoteStripeOptions) str.updateExistingAttributes(for: .paragraphStyle) { (style: NSParagraphStyle) in style.indented(by: stripeAttribute.layoutWidth) @@ -82,7 +86,7 @@ open class DownStyler: Styler { guard let leadingParagraphRange = paragraphRanges.first else { return } - indentListItemLeadingParagraph(in: str, prefixLength: prefixLength, inRange: leadingParagraphRange) + indentListItemLeadingParagraph(in: str, prefixLength: prefixLength, in: leadingParagraphRange) paragraphRanges.dropFirst().forEach { indentListItemTrailingParagraph(in: str, inRange: $0) @@ -111,15 +115,15 @@ open class DownStyler: Styler { str.updateExistingAttributes(for: .font) { (currentFont: DownFont) in var newFont = font - if (currentFont.isMonospace) { + if currentFont.isMonospace { newFont = newFont.monospace } - - if (currentFont.isEmphasized) { + + if currentFont.isEmphasized { newFont = newFont.emphasis } - if (currentFont.isStrong) { + if currentFont.isStrong { newFont = newFont.strong } @@ -229,7 +233,10 @@ open class DownStyler: Styler { } } - private func indentListItemLeadingParagraph(in str: NSMutableAttributedString, prefixLength: Int, inRange range: NSRange) { + private func indentListItemLeadingParagraph(in str: NSMutableAttributedString, + prefixLength: Int, + in range: NSRange) { + str.updateExistingAttributes(for: .paragraphStyle, in: range) { (existingStyle: NSParagraphStyle) in existingStyle.indented(by: itemParagraphStyler.indentation) } @@ -265,7 +272,7 @@ open class DownStyler: Styler { private extension NSParagraphStyle { func indented(by indentation: CGFloat) -> NSParagraphStyle { - let result = mutableCopy() as! NSMutableParagraphStyle + guard let result = mutableCopy() as? NSMutableParagraphStyle else { return self } result.firstLineHeadIndent += indentation result.headIndent += indentation @@ -277,7 +284,7 @@ private extension NSParagraphStyle { } func inset(by amount: CGFloat) -> NSParagraphStyle { - let result = mutableCopy() as! NSMutableParagraphStyle + guard let result = mutableCopy() as? NSMutableParagraphStyle else { return self } result.paragraphSpacingBefore += amount result.paragraphSpacing += amount result.firstLineHeadIndent += amount @@ -285,6 +292,7 @@ private extension NSParagraphStyle { result.tailIndent = -amount return result } + } private extension NSAttributedString { @@ -292,8 +300,9 @@ private extension NSAttributedString { func prefix(with length: Int) -> NSAttributedString { guard length <= self.length else { return self } guard length > 0 else { return NSAttributedString() } - return attributedSubstring(from: NSMakeRange(0, length)) + return attributedSubstring(from: NSRange(location: 0, length: length)) } + } #endif diff --git a/Sources/Down/AST/Styling/Stylers/DownStylerConfiguration.swift b/Sources/Down/AST/Styling/Stylers/DownStylerConfiguration.swift index 8f42491e..fc960253 100644 --- a/Sources/Down/AST/Styling/Stylers/DownStylerConfiguration.swift +++ b/Sources/Down/AST/Styling/Stylers/DownStylerConfiguration.swift @@ -9,17 +9,22 @@ #if !os(watchOS) && !os(Linux) /// A configuration object used to initialze the `DownStyler`. + public struct DownStylerConfiguration { - + + // MARK: - Properties + public var fonts: FontCollection public var colors: ColorCollection public var paragraphStyles: ParagraphStyleCollection - + public var listItemOptions: ListItemOptions public var quoteStripeOptions: QuoteStripeOptions public var thematicBreakOptions: ThematicBreakOptions public var codeBlockOptions: CodeBlockOptions + // MARK: - Life cycle + public init(fonts: FontCollection = StaticFontCollection(), colors: ColorCollection = StaticColorCollection(), paragraphStyles: ParagraphStyleCollection = StaticParagraphStyleCollection(), @@ -36,6 +41,7 @@ public struct DownStylerConfiguration { self.thematicBreakOptions = thematicBreakOptions self.codeBlockOptions = codeBlockOptions } + } #endif diff --git a/Sources/Down/AST/Styling/Stylers/Styler.swift b/Sources/Down/AST/Styling/Stylers/Styler.swift index df5bdb4c..ef61398f 100644 --- a/Sources/Down/AST/Styling/Stylers/Styler.swift +++ b/Sources/Down/AST/Styling/Stylers/Styler.swift @@ -18,147 +18,172 @@ import Foundation /// /// A styler is used in conjunction with an instance of `AttributedStringVisitor` in order /// to generate an NSAttributedString from an abstract syntax tree. + public protocol Styler { /// Styles the content of the document in the given string. /// - /// - Parameter str: the document content. - func style(document str: NSMutableAttributedString) + /// - Parameters: + /// - str: the document content. + func style(document str: NSMutableAttributedString) /// Styles the content of the block quote contained in the given string. /// - /// - Parameter str: the quote content. - /// - Parameter nestDepth: the zero indexed nesting depth of the block quote node. - func style(blockQuote str: NSMutableAttributedString, nestDepth: Int) + /// - Parameters: + /// - str: the quote content. + /// - nestDepth: the zero indexed nesting depth of the block quote node. + func style(blockQuote str: NSMutableAttributedString, nestDepth: Int) /// Styles the content of the list contained in the given string. /// - /// - Parameter str: the list content. - /// - Parameter nestDepth: the zero indexed nesting depth of the list node. - func style(list str: NSMutableAttributedString, nestDepth: Int) + /// - Parameters: + /// - str: the list content. + /// - nestDepth: the zero indexed nesting depth of the list node. + func style(list str: NSMutableAttributedString, nestDepth: Int) /// Styles the number or bullet list item prefix. /// - /// - Parameter str: the list item prefix. - func style(listItemPrefix str: NSMutableAttributedString) + /// - Parameters: + /// - str: the list item prefix. + func style(listItemPrefix str: NSMutableAttributedString) /// Styles the content of the list item contained in the given string, including the /// number or bullet prefix. /// - /// - Parameter str: the item content. - /// - Parameter prefixLength: the character length of the number or bullet prefix. - func style(item str: NSMutableAttributedString, prefixLength: Int) + /// - Parameters: + /// - str: the item content. + /// - prefixLength: the character length of the number or bullet prefix. + func style(item str: NSMutableAttributedString, prefixLength: Int) /// Styles the content of the code block in the given string. /// /// An example use case for `fenceInfo` is to specify a programming language name, /// which could be used to support syntax highlighting. /// - /// - Parameter str: the code content. - /// - Parameter fenceInfo: the string that trails the initial ``` ticks. - func style(codeBlock str: NSMutableAttributedString, fenceInfo: String?) + /// - Parameters: + /// - str: the code content. + /// - fenceInfo: the string that trails the initial \`\`\` ticks. + func style(codeBlock str: NSMutableAttributedString, fenceInfo: String?) /// Styles the content of the html block contained in the given string. /// - /// - Parameter str: the html content. - func style(htmlBlock str: NSMutableAttributedString) + /// - Parameters: + /// - str: the html content. + func style(htmlBlock str: NSMutableAttributedString) /// Styles the content of the custom block contained in the given string. /// - /// - Parameter str: the content. - func style(customBlock str: NSMutableAttributedString) + /// - Parameters: + /// - str: the content. + func style(customBlock str: NSMutableAttributedString) /// Styles the content of the paragraph in the given string. /// - /// - Parameter str: the paragraph content. - func style(paragraph str: NSMutableAttributedString) + /// - Parameters: + /// - str: the paragraph content. + func style(paragraph str: NSMutableAttributedString) /// Styles the content of the heading in the given string. /// - /// - Parameter str: the heading content. - /// - Parameter level: the heading level [1, 6] - func style(heading str: NSMutableAttributedString, level: Int) + /// - Parameters: + /// - str: the heading content. + /// - level: the heading level [1, 6] + func style(heading str: NSMutableAttributedString, level: Int) /// Styles the content of the thematic break in the given string. /// - /// - Parameter str: the thematic break. - func style(thematicBreak str: NSMutableAttributedString) + /// - Parameters: + /// - str: the thematic break. + func style(thematicBreak str: NSMutableAttributedString) /// Styles the content of the inline text node in the given string. /// /// The text nodes are always the leaves of the AST, thus they /// contain the base style upon which other nodes can work with. /// - /// - Parameter str: the text content. - func style(text str: NSMutableAttributedString) + /// - Parameters: + /// - str: the text content. + func style(text str: NSMutableAttributedString) /// Styles the content of the soft break in the given string. /// - /// - Parameter str: the soft break. - func style(softBreak str: NSMutableAttributedString) + /// - Parameters: + /// - str: the soft break. + func style(softBreak str: NSMutableAttributedString) /// Styles the content of the line break in the given string. /// - /// - Parameter str: the line break. - func style(lineBreak str: NSMutableAttributedString) + /// - Parameters: + /// - str: the line break. + func style(lineBreak str: NSMutableAttributedString) /// Styles the content of the inline code in the given string. /// - /// - Parameter str: the code content. - func style(code str: NSMutableAttributedString) + /// - Parameters: + /// - str: the code content. + func style(code str: NSMutableAttributedString) /// Styles the content of the inline html tags in the given string. /// /// Note, the content does not include text between matching tags. /// - /// - Parameter str: the html content. - func style(htmlInline str: NSMutableAttributedString) + /// - Parameters: + /// - str: the html content. + func style(htmlInline str: NSMutableAttributedString) /// Styles the content of the inline custom node in the given string. /// - /// - Parameter str: the custom content. - func style(customInline str: NSMutableAttributedString) + /// - Parameters: + /// - str: the custom content. + func style(customInline str: NSMutableAttributedString) /// Styles the content of the inline emphasis node in the given string. /// - /// - Parameter str: the ephasized content. - func style(emphasis str: NSMutableAttributedString) + /// - Parameters: + /// - str: the ephasized content. + func style(emphasis str: NSMutableAttributedString) /// Styles the content of the inline strong node in the given string. /// - /// - Parameter str: the strong content. - func style(strong str: NSMutableAttributedString) + /// - Parameters: + /// - str: the strong content. + func style(strong str: NSMutableAttributedString) /// Styles the content of the inline link node in the given string. /// - /// - Parameter str: the link content. - /// - Parameter title: the link title. - /// - Parameter url: the linked url. + /// - Parameters: + /// - str: the link content. + /// - title: the link title. + /// - url: the linked url. + func style(link str: NSMutableAttributedString, title: String?, url: String?) /// Styles the content of the inline image node in the given string. /// - /// - Parameter str: the link content. - /// - Parameter title: the link title. - /// - Parameter url: the linked url. + /// - Parameters: + /// - str: the link content. + /// - title: the link title. + /// - url: the linked url. + func style(image str: NSMutableAttributedString, title: String?, url: String?) + } diff --git a/Sources/Down/AST/Styling/Text Views/DownDebugTextView.swift b/Sources/Down/AST/Styling/Text Views/DownDebugTextView.swift index 8c97b647..948f1d28 100644 --- a/Sources/Down/AST/Styling/Text Views/DownDebugTextView.swift +++ b/Sources/Down/AST/Styling/Text Views/DownDebugTextView.swift @@ -21,8 +21,11 @@ import AppKit /// A text view capable of parsing and rendering markdown via the AST, as well as line fragments. /// /// See `DownDebugLayoutManager`. + public class DownDebugTextView: DownTextView { + // MARK: - Life cycle + public init(frame: CGRect, styler: Styler = DownStyler()) { super.init(frame: frame, styler: styler, layoutManager: DownDebugLayoutManager()) } @@ -30,6 +33,7 @@ public class DownDebugTextView: DownTextView { required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + } #endif diff --git a/Sources/Down/AST/Styling/Text Views/DownTextView.swift b/Sources/Down/AST/Styling/Text Views/DownTextView.swift index e4eb8bd8..4c238a4b 100644 --- a/Sources/Down/AST/Styling/Text Views/DownTextView.swift +++ b/Sources/Down/AST/Styling/Text Views/DownTextView.swift @@ -21,6 +21,7 @@ public typealias TextView = NSTextView #endif /// A text view capable of parsing and rendering markdown via the AST. + open class DownTextView: TextView { // MARK: - Properties @@ -40,15 +41,14 @@ open class DownTextView: TextView { open override var string: String { didSet { - guard oldValue != string else { return } + guard oldValue != string else { return } try? render() } } #endif - - // MARK: - Init + // MARK: - Life cycle public convenience init(frame: CGRect, styler: Styler = DownStyler()) { self.init(frame: frame, styler: styler, layoutManager: DownLayoutManager()) @@ -90,6 +90,7 @@ open class DownTextView: TextView { #endif } + } #endif diff --git a/Sources/Down/AST/Visitors/AttributedStringVisitor.swift b/Sources/Down/AST/Visitors/AttributedStringVisitor.swift index 5134e0a8..fd156da1 100644 --- a/Sources/Down/AST/Visitors/AttributedStringVisitor.swift +++ b/Sources/Down/AST/Visitors/AttributedStringVisitor.swift @@ -13,8 +13,11 @@ import Foundation /// tree produced by a markdown string. It traverses the tree to construct substrings /// represented at each node and uses an instance of `Styler` to apply the visual attributes. /// These substrings are joined together to produce the final result. + public class AttributedStringVisitor { - + + // MARK: - Properties + private let styler: Styler private let options: DownOptions private var listPrefixGenerators = [ListItemPrefixGenerator]() @@ -24,26 +27,29 @@ public class AttributedStringVisitor { /// - parameters: /// - styler: used to style the markdown elements. /// - options: may be used to modify rendering. + public init(styler: Styler, options: DownOptions = .default) { self.styler = styler self.options = options } + } extension AttributedStringVisitor: Visitor { + public typealias Result = NSMutableAttributedString - + public func visit(document node: Document) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined - styler.style(document: s) - return s + let result = visitChildren(of: node).joined + styler.style(document: result) + return result } - + public func visit(blockQuote node: BlockQuote) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined - if node.hasSuccessor { s.append(.paragraphSeparator) } - styler.style(blockQuote: s, nestDepth: node.nestDepth) - return s + let result = visitChildren(of: node).joined + if node.hasSuccessor { result.append(.paragraphSeparator) } + styler.style(blockQuote: result, nestDepth: node.nestDepth) + return result } public func visit(list node: List) -> NSMutableAttributedString { @@ -52,125 +58,125 @@ extension AttributedStringVisitor: Visitor { let items = visitChildren(of: node) - let s = items.joined - if node.hasSuccessor { s.append(.paragraphSeparator) } - styler.style(list: s, nestDepth: node.nestDepth) - return s + let result = items.joined + if node.hasSuccessor { result.append(.paragraphSeparator) } + styler.style(list: result, nestDepth: node.nestDepth) + return result } - + public func visit(item node: Item) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined + let result = visitChildren(of: node).joined let prefix = listPrefixGenerators.last?.next() ?? "•" let attributedPrefix = "\(prefix)\t".attributed styler.style(listItemPrefix: attributedPrefix) - s.insert(attributedPrefix, at: 0) + result.insert(attributedPrefix, at: 0) - if node.hasSuccessor { s.append(.paragraphSeparator) } - styler.style(item: s, prefixLength: (prefix as NSString).length) - return s + if node.hasSuccessor { result.append(.paragraphSeparator) } + styler.style(item: result, prefixLength: (prefix as NSString).length) + return result } - + public func visit(codeBlock node: CodeBlock) -> NSMutableAttributedString { guard let literal = node.literal else { return .empty } - let s = literal.replacingNewlinesWithLineSeparators().attributed - if node.hasSuccessor { s.append(.paragraphSeparator) } - styler.style(codeBlock: s, fenceInfo: node.fenceInfo) - return s + let result = literal.replacingNewlinesWithLineSeparators().attributed + if node.hasSuccessor { result.append(.paragraphSeparator) } + styler.style(codeBlock: result, fenceInfo: node.fenceInfo) + return result } - + public func visit(htmlBlock node: HtmlBlock) -> NSMutableAttributedString { guard let literal = node.literal else { return .empty } - let s = literal.replacingNewlinesWithLineSeparators().attributed - if node.hasSuccessor { s.append(.paragraphSeparator) } - styler.style(htmlBlock: s) - return s + let result = literal.replacingNewlinesWithLineSeparators().attributed + if node.hasSuccessor { result.append(.paragraphSeparator) } + styler.style(htmlBlock: result) + return result } - + public func visit(customBlock node: CustomBlock) -> NSMutableAttributedString { - guard let s = node.literal?.attributed else { return .empty } - styler.style(customBlock: s) - return s + guard let result = node.literal?.attributed else { return .empty } + styler.style(customBlock: result) + return result } - + public func visit(paragraph node: Paragraph) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined - if node.hasSuccessor { s.append(.paragraphSeparator) } - styler.style(paragraph: s) - return s + let result = visitChildren(of: node).joined + if node.hasSuccessor { result.append(.paragraphSeparator) } + styler.style(paragraph: result) + return result } - + public func visit(heading node: Heading) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined - if node.hasSuccessor { s.append(.paragraphSeparator) } - styler.style(heading: s, level: node.headingLevel) - return s + let result = visitChildren(of: node).joined + if node.hasSuccessor { result.append(.paragraphSeparator) } + styler.style(heading: result, level: node.headingLevel) + return result } - + public func visit(thematicBreak node: ThematicBreak) -> NSMutableAttributedString { - let s = "\(String.zeroWidthSpace)\n".attributed - styler.style(thematicBreak: s) - return s + let result = "\(String.zeroWidthSpace)\n".attributed + styler.style(thematicBreak: result) + return result } - + public func visit(text node: Text) -> NSMutableAttributedString { - guard let s = node.literal?.attributed else { return .empty } - styler.style(text: s) - return s + guard let result = node.literal?.attributed else { return .empty } + styler.style(text: result) + return result } - + public func visit(softBreak node: SoftBreak) -> NSMutableAttributedString { - let s = (options.contains(.hardBreaks) ? String.lineSeparator : " ").attributed - styler.style(softBreak: s) - return s + let result = (options.contains(.hardBreaks) ? String.lineSeparator : " ").attributed + styler.style(softBreak: result) + return result } - + public func visit(lineBreak node: LineBreak) -> NSMutableAttributedString { - let s = String.lineSeparator.attributed - styler.style(lineBreak: s) - return s + let result = String.lineSeparator.attributed + styler.style(lineBreak: result) + return result } - + public func visit(code node: Code) -> NSMutableAttributedString { - guard let s = node.literal?.attributed else { return .empty } - styler.style(code: s) - return s + guard let result = node.literal?.attributed else { return .empty } + styler.style(code: result) + return result } - + public func visit(htmlInline node: HtmlInline) -> NSMutableAttributedString { - guard let s = node.literal?.attributed else { return .empty } - styler.style(htmlInline: s) - return s + guard let result = node.literal?.attributed else { return .empty } + styler.style(htmlInline: result) + return result } - + public func visit(customInline node: CustomInline) -> NSMutableAttributedString { - guard let s = node.literal?.attributed else { return .empty } - styler.style(customInline: s) - return s + guard let result = node.literal?.attributed else { return .empty } + styler.style(customInline: result) + return result } - + public func visit(emphasis node: Emphasis) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined - styler.style(emphasis: s) - return s + let result = visitChildren(of: node).joined + styler.style(emphasis: result) + return result } - + public func visit(strong node: Strong) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined - styler.style(strong: s) - return s + let result = visitChildren(of: node).joined + styler.style(strong: result) + return result } - + public func visit(link node: Link) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined - styler.style(link: s, title: node.title, url: node.url) - return s + let result = visitChildren(of: node).joined + styler.style(link: result, title: node.title, url: node.url) + return result } - + public func visit(image node: Image) -> NSMutableAttributedString { - let s = visitChildren(of: node).joined - styler.style(image: s, title: node.title, url: node.url) - return s + let result = visitChildren(of: node).joined + styler.style(image: result, title: node.title, url: node.url) + return result } } @@ -181,6 +187,7 @@ private extension Sequence where Iterator.Element == NSMutableAttributedString { var joined: NSMutableAttributedString { return reduce(into: NSMutableAttributedString()) { $0.append($1) } } + } private extension NSMutableAttributedString { @@ -188,6 +195,7 @@ private extension NSMutableAttributedString { static var empty: NSMutableAttributedString { return "".attributed } + } private extension NSAttributedString { @@ -195,6 +203,7 @@ private extension NSAttributedString { static var paragraphSeparator: NSAttributedString { return String.paragraphSeparator.attributed } + } private extension String { @@ -204,11 +213,13 @@ private extension String { } // This codepoint marks the end of a paragraph and the start of the next. + static var paragraphSeparator: String { return "\u{2029}" } // This code point allows line breaking, without starting a new paragraph. + static var lineSeparator: String { return "\u{2028}" } @@ -222,5 +233,7 @@ private extension String { let lines = trimmed.components(separatedBy: .newlines) return lines.joined(separator: .lineSeparator) } + } + #endif // !os(Linux) diff --git a/Sources/Down/AST/Visitors/DebugVisitor.swift b/Sources/Down/AST/Visitors/DebugVisitor.swift index 9554b693..733f73e8 100644 --- a/Sources/Down/AST/Visitors/DebugVisitor.swift +++ b/Sources/Down/AST/Visitors/DebugVisitor.swift @@ -9,16 +9,23 @@ import Foundation /// This visitor will generate the debug description of an entire abstract syntax tree, /// indicating relationships between nodes with indentation. + public class DebugVisitor: Visitor { - + + // MARK: - Properties + private var depth = 0 private var indent: String { return String(repeating: " ", count: depth) } + // MARK: - Life cycle + public init() {} + // MARK: - Helpers + private func report(_ node: Node) -> String { return "\(indent)\(node is Document ? "" : "↳ ")\(String(reflecting: node))\n" } @@ -114,5 +121,5 @@ public class DebugVisitor: Visitor { public func visit(image node: Image) -> String { return reportWithChildren(node) } -} +} diff --git a/Sources/Down/AST/Visitors/ListItemPrefixGenerator.swift b/Sources/Down/AST/Visitors/ListItemPrefixGenerator.swift index 4bc14648..0a0b9c9c 100644 --- a/Sources/Down/AST/Visitors/ListItemPrefixGenerator.swift +++ b/Sources/Down/AST/Visitors/ListItemPrefixGenerator.swift @@ -10,8 +10,12 @@ import Foundation class ListItemPrefixGenerator { + // MARK: - Properties + private var prefixes: IndexingIterator<[String]> + // MARK: - Life cycle + convenience init(list: List) { self.init(listType: list.listType, numberOfItems: list.numberOfItems) } @@ -29,7 +33,10 @@ class ListItemPrefixGenerator { } } + // MARK: - Methods + func next() -> String? { prefixes.next() } + } diff --git a/Sources/Down/AST/Visitors/Visitor.swift b/Sources/Down/AST/Visitors/Visitor.swift index 0c9072c2..461c419e 100644 --- a/Sources/Down/AST/Visitors/Visitor.swift +++ b/Sources/Down/AST/Visitors/Visitor.swift @@ -11,8 +11,11 @@ import Foundation /// each node of the tree and produces some result for that node. A visitor is "accepted" by /// the root node (of type `Document`), which will start the traversal by first invoking /// `visit(document:)`. + public protocol Visitor { + associatedtype Result + func visit(document node: Document) -> Result func visit(blockQuote node: BlockQuote) -> Result func visit(list node: List) -> Result @@ -34,9 +37,11 @@ public protocol Visitor { func visit(link node: Link) -> Result func visit(image node: Image) -> Result func visitChildren(of node: Node) -> [Result] + } extension Visitor { + public func visitChildren(of node: Node) -> [Result] { return node.childSequence.compactMap { child in switch child { @@ -66,4 +71,5 @@ extension Visitor { } } } + } diff --git a/Sources/Down/Down.swift b/Sources/Down/Down.swift index 22d695ec..00814d37 100644 --- a/Sources/Down/Down.swift +++ b/Sources/Down/Down.swift @@ -13,7 +13,8 @@ public struct Down: DownASTRenderable, DownHTMLRenderable, DownXMLRenderable, /// A string containing CommonMark Markdown public var markdownString: String - /// Initializes the container with a CommonMark Markdown string which can then be rendered depending on protocol conformance + /// Initializes the container with a CommonMark Markdown string which can then be + /// rendered depending on protocol conformance. /// /// - Parameter markdownString: A string containing CommonMark Markdown public init(markdownString: String) { diff --git a/Sources/Down/Enums & Options/DownErrors.swift b/Sources/Down/Enums & Options/DownErrors.swift index 4d215dc5..f7daff77 100644 --- a/Sources/Down/Enums & Options/DownErrors.swift +++ b/Sources/Down/Enums & Options/DownErrors.swift @@ -9,19 +9,28 @@ import Foundation public enum DownErrors: Error { - /// Thrown when there was an issue converting the Markdown into an abstract syntax tree + + /// Thrown when there was an issue converting the Markdown into an abstract syntax tree. + case markdownToASTError - /// Thrown when the abstract syntax tree could not be rendered into another format + /// Thrown when the abstract syntax tree could not be rendered into another format. + case astRenderingError - /// Thrown when an HTML string cannot be converted into an `NSData` representation + /// Thrown when an HTML string cannot be converted into an `NSData` representation. + case htmlDataConversionError #if os(macOS) + /// Thrown when a custom template bundle has a non-standard bundle format. /// - /// Specifically, the file URL of the bundle’s subdirectory containing resource files could not be found (i.e. the bundle's `resourceURL` property is nil). + /// Specifically, the file URL of the bundle’s subdirectory containing resource files could + /// not be found (i.e. the bundle's `resourceURL` property is nil). + case nonStandardBundleFormatError + #endif + } diff --git a/Sources/Down/Enums & Options/DownOptions.swift b/Sources/Down/Enums & Options/DownOptions.swift index 52d73a84..713a87dd 100644 --- a/Sources/Down/Enums & Options/DownOptions.swift +++ b/Sources/Down/Enums & Options/DownOptions.swift @@ -10,18 +10,27 @@ import Foundation import libcmark public struct DownOptions: OptionSet { + + // MARK: - Properties + public let rawValue: Int32 + + // MARK: - Life cycle + public init(rawValue: Int32) { self.rawValue = rawValue } - /// Default options + /// Default options. + public static let `default` = DownOptions(rawValue: CMARK_OPT_DEFAULT) // MARK: - Rendering Options - /// Include a `data-sourcepos` attribute on all block elements + /// Include a `data-sourcepos` attribute on all block elements. + public static let sourcePos = DownOptions(rawValue: CMARK_OPT_SOURCEPOS) /// Render `softbreak` elements as hard line breaks. + public static let hardBreaks = DownOptions(rawValue: CMARK_OPT_HARDBREAKS) /// Suppress raw HTML and unsafe links (`javascript:`, `vbscript:`, @@ -32,8 +41,9 @@ public struct DownOptions: OptionSet { /// /// Note: this is the default option as of cmark v0.29.0. Use `unsafe` /// to disable this behavior. + public static let safe = DownOptions(rawValue: CMARK_OPT_SAFE) - + /// Render raw HTML and unsafe links (`javascript:`, `vbscript:`, /// `file:`, and `data:`, except for `image/png`, `image/gif`, /// `image/jpeg`, or `image/webp` mime types). By default, @@ -41,23 +51,28 @@ public struct DownOptions: OptionSet { /// links are replaced by empty strings. /// /// Note: `safe` is the default as of cmark v0.29.0 + public static let unsafe = DownOptions(rawValue: CMARK_OPT_UNSAFE) // MARK: - Parsing Options /// Normalize tree by consolidating adjacent text nodes. + public static let normalize = DownOptions(rawValue: CMARK_OPT_NORMALIZE) /// Validate UTF-8 in the input before parsing, replacing illegal /// sequences with the replacement character U+FFFD. + public static let validateUTF8 = DownOptions(rawValue: CMARK_OPT_VALIDATE_UTF8) /// Convert straight quotes to curly, --- to em dashes, -- to en dashes. + public static let smart = DownOptions(rawValue: CMARK_OPT_SMART) - + // MARK: - Combo Options - + /// Combines 'unsafe' and 'smart' to render raw HTML and produce smart typography. + public static let smartUnsafe = DownOptions(rawValue: CMARK_OPT_SMART + CMARK_OPT_UNSAFE) } diff --git a/Sources/Down/Extensions/NSAttributedString+HTML.swift b/Sources/Down/Extensions/NSAttributedString+HTML.swift index c4807572..fafd80e5 100644 --- a/Sources/Down/Extensions/NSAttributedString+HTML.swift +++ b/Sources/Down/Extensions/NSAttributedString+HTML.swift @@ -9,18 +9,25 @@ #if !os(Linux) #if os(macOS) - import AppKit + +import AppKit + #else - import UIKit -#endif +import UIKit + +#endif extension NSAttributedString { /// Instantiates an attributed string with the given HTML string /// - /// - Parameter htmlString: An HTML string - /// - Throws: `HTMLDataConversionError` or an instantiation error + /// - Parameters: + /// - htmlString: An HTML string. + /// + /// - Throws: + /// `HTMLDataConversionError` or an instantiation error. + convenience init(htmlString: String) throws { guard let data = htmlString.data(using: String.Encoding.utf8) else { throw DownErrors.htmlDataConversionError @@ -30,8 +37,10 @@ extension NSAttributedString { .documentType: NSAttributedString.DocumentType.html, .characterEncoding: NSNumber(value: String.Encoding.utf8.rawValue) ] + try self.init(data: data, options: options, documentAttributes: nil) } } + #endif // !os(Linux) diff --git a/Sources/Down/Extensions/String+ToHTML.swift b/Sources/Down/Extensions/String+ToHTML.swift index 3691c8be..88508848 100644 --- a/Sources/Down/Extensions/String+ToHTML.swift +++ b/Sources/Down/Extensions/String+ToHTML.swift @@ -11,11 +11,16 @@ import libcmark extension String { - /// Generates an HTML string from the contents of the string (self), which should contain CommonMark Markdown + /// Generates an HTML string from the contents of the string (self), which should contain CommonMark Markdown. /// - /// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - Returns: HTML string - /// - Throws: `DownErrors` depending on the scenario + /// - Parameters: + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - Returns: + /// An HTML string. + /// + /// - Throws: + /// `DownErrors` depending on the scenario. + public func toHTML(_ options: DownOptions = .default) throws -> String { let ast = try DownASTRenderer.stringToAST(self, options: options) let html = try DownHTMLRenderer.astToHTML(ast, options: options) diff --git a/Sources/Down/Renderers/DownASTRenderable.swift b/Sources/Down/Renderers/DownASTRenderable.swift index 43e60799..c057dfce 100644 --- a/Sources/Down/Renderers/DownASTRenderable.swift +++ b/Sources/Down/Renderers/DownASTRenderable.swift @@ -10,24 +10,39 @@ import Foundation import libcmark public protocol DownASTRenderable: DownRenderable { - func toAST(_ options: DownOptions) throws -> UnsafeMutablePointer<cmark_node> + + func toAST(_ options: DownOptions) throws -> CMarkNode + } extension DownASTRenderable { - /// Generates an abstract syntax tree from the `markdownString` property + + /// Generates an abstract syntax tree from the `markdownString` property. + /// + /// - Parametera: + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// + /// - Returns: + /// An abstract syntax tree representation of the Markdown input. /// - /// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - Returns: An abstract syntax tree representation of the Markdown input - /// - Throws: `MarkdownToASTError` if conversion fails - public func toAST(_ options: DownOptions = .default) throws -> UnsafeMutablePointer<cmark_node> { + /// - Throws: + /// `MarkdownToASTError` if conversion fails. + + public func toAST(_ options: DownOptions = .default) throws -> CMarkNode { return try DownASTRenderer.stringToAST(markdownString, options: options) } /// Parses the `markdownString` property into an abstract syntax tree and returns the root `Document` node. /// - /// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - Returns: The root Document node for the abstract syntax tree representation of the Markdown input - /// - Throws: `MarkdownToASTError` if conversion fails + /// - Parameters: + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// + /// - Returns: + /// The root Document node for the abstract syntax tree representation of the Markdown input. + /// + /// - Throws: + /// `MarkdownToASTError` if conversion fails. + public func toDocument(_ options: DownOptions = .default) throws -> Document { let tree = try toAST(options) @@ -37,20 +52,27 @@ extension DownASTRenderable { return Document(cmarkNode: tree) } + } public struct DownASTRenderer { - /// Generates an abstract syntax tree from the given CommonMark Markdown string + + /// Generates an abstract syntax tree from the given CommonMark Markdown string. /// - /// **Important:** It is the caller's responsibility to call `cmark_node_free(ast)` on the returned value + /// **Important:** It is the caller's responsibility to call `cmark_node_free(ast)` on the returned value. /// /// - Parameters: - /// - string: A string containing CommonMark Markdown - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - Returns: An abstract syntax tree representation of the Markdown input - /// - Throws: `MarkdownToASTError` if conversion fails - public static func stringToAST(_ string: String, options: DownOptions = .default) throws -> UnsafeMutablePointer<cmark_node> { - var tree: UnsafeMutablePointer<cmark_node>? + /// - string: A string containing CommonMark Markdown. + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// + /// - Returns: + /// An abstract syntax tree representation of the Markdown input. + /// + /// - Throws: + /// `MarkdownToASTError` if conversion fails. + public static func stringToAST(_ string: String, options: DownOptions = .default) throws -> CMarkNode { + var tree: CMarkNode? + string.withCString { let stringLength = Int(strlen($0)) tree = cmark_parse_document($0, stringLength, options.rawValue) @@ -59,6 +81,8 @@ public struct DownASTRenderer { guard let ast = tree else { throw DownErrors.markdownToASTError } + return ast } + } diff --git a/Sources/Down/Renderers/DownAttributedStringRenderable.swift b/Sources/Down/Renderers/DownAttributedStringRenderable.swift index e36174ab..3d655af8 100644 --- a/Sources/Down/Renderers/DownAttributedStringRenderable.swift +++ b/Sources/Down/Renderers/DownAttributedStringRenderable.swift @@ -7,16 +7,20 @@ // #if !os(Linux) + import Foundation import libcmark public protocol DownAttributedStringRenderable: DownHTMLRenderable, DownASTRenderable { + func toAttributedString(_ options: DownOptions, stylesheet: String?) throws -> NSAttributedString func toAttributedString(_ options: DownOptions, styler: Styler) throws -> NSAttributedString + } extension DownAttributedStringRenderable { - /// Generates an `NSAttributedString` from the `markdownString` property + + /// Generates an `NSAttributedString` from the `markdownString` property. /// /// **Note:** The attributed string is constructed and rendered via WebKit from html generated from the /// abstract syntax tree. This process is not background safe and must be executed on the main @@ -24,31 +28,46 @@ extension DownAttributedStringRenderable { /// use the `toAttributedString(options: styler:)` method below. /// /// - Parameters: - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - stylesheet: a `String` to use as the CSS stylesheet when rendering, defaulting to a style that uses the `NSAttributedString` default font - /// - Returns: An `NSAttributedString` - /// - Throws: `DownErrors` depending on the scenario - public func toAttributedString(_ options: DownOptions = .default, stylesheet: String? = nil) throws -> NSAttributedString { + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - stylesheet: a `String` to use as the CSS stylesheet when rendering, defaulting + /// to a style that uses the `NSAttributedString` default font. + /// + /// - Returns: + /// An `NSAttributedString`. + /// - Throws: + /// `DownErrors` depending on the scenario. + + public func toAttributedString(_ options: DownOptions = .default, + stylesheet: String? = nil) throws -> NSAttributedString { + let html = try self.toHTML(options) let defaultStylesheet = "* {font-family: Helvetica } code, pre { font-family: Menlo }" return try NSAttributedString(htmlString: "<style>" + (stylesheet ?? defaultStylesheet) + "</style>" + html) } - - /// Generates an `NSAttributedString` from the `markdownString` property + + /// Generates an `NSAttributedString` from the `markdownString` property. /// /// **Note:** The attributed string is constructed directly by traversing the abstract syntax tree. It is /// much faster than the `toAttributedString(options: stylesheet)` method and it can be also be /// rendered in a background thread. /// /// - Parameters: - /// - options: `DownOptions` to modify parsing or rendering - /// - styler: a class/struct conforming to `Styler` to use when rendering the various elements of the attributed string - /// - Returns: An `NSAttributedString` - /// - Throws: `DownErrors` depending on the scenario + /// - options: `DownOptions` to modify parsing or rendering. + /// - styler: a class/struct conforming to `Styler` to use when rendering the various + /// elements of the attributed string + /// + /// - Returns: + /// An `NSAttributedString`. + /// + /// - Throws: + /// `DownErrors` depending on the scenario. + public func toAttributedString(_ options: DownOptions = .default, styler: Styler) throws -> NSAttributedString { let document = try self.toDocument(options) let visitor = AttributedStringVisitor(styler: styler, options: options) return document.accept(visitor) } + } + #endif // !os(Linux) diff --git a/Sources/Down/Renderers/DownCommonMarkRenderable.swift b/Sources/Down/Renderers/DownCommonMarkRenderable.swift index cf0f2ccf..0dbdeadb 100644 --- a/Sources/Down/Renderers/DownCommonMarkRenderable.swift +++ b/Sources/Down/Renderers/DownCommonMarkRenderable.swift @@ -10,48 +10,68 @@ import Foundation import libcmark public protocol DownCommonMarkRenderable: DownRenderable { + func toCommonMark(_ options: DownOptions, width: Int32) throws -> String + } extension DownCommonMarkRenderable { - /// Generates a CommonMark Markdown string from the `markdownString` property + + /// Generates a CommonMark Markdown string from the `markdownString` property. /// /// - Parameters: - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - width: The width to break on, defaulting to 0 - /// - Returns: CommonMark Markdown string - /// - Throws: `DownErrors` depending on the scenario + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - width: The width to break on, defaulting to 0. + /// + /// - Returns: + /// A CommonMark Markdown string. + /// + /// - Throws: + /// `DownErrors` depending on the scenario. + public func toCommonMark(_ options: DownOptions = .default, width: Int32 = 0) throws -> String { let ast = try DownASTRenderer.stringToAST(markdownString, options: options) let commonMark = try DownCommonMarkRenderer.astToCommonMark(ast, options: options, width: width) cmark_node_free(ast) return commonMark } + } public struct DownCommonMarkRenderer { - /// Generates a CommonMark Markdown string from the given abstract syntax tree + + /// Generates a CommonMark Markdown string from the given abstract syntax tree. /// - /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns + /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns. /// /// - Parameters: - /// - ast: The `cmark_node` representing the abstract syntax tree - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - width: The width to break on, defaulting to 0 - /// - Returns: CommonMark Markdown string - /// - Throws: `ASTRenderingError` if the AST could not be converted - public static func astToCommonMark(_ ast: UnsafeMutablePointer<cmark_node>, + /// - ast: The `cmark_node` representing the abstract syntax tree. + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - width: The width to break on, defaulting to 0. + /// + /// - Returns: + /// A CommonMark Markdown string. + /// + /// - Throws: + /// `ASTRenderingError` if the AST could not be converted. + + public static func astToCommonMark(_ ast: CMarkNode, options: DownOptions = .default, width: Int32 = 0) throws -> String { + guard let cCommonMarkString = cmark_render_commonmark(ast, options.rawValue, width) else { throw DownErrors.astRenderingError } - defer { free(cCommonMarkString) } - + + defer { + free(cCommonMarkString) + } + guard let commonMarkString = String(cString: cCommonMarkString, encoding: String.Encoding.utf8) else { throw DownErrors.astRenderingError } - + return commonMarkString } + } diff --git a/Sources/Down/Renderers/DownGroffRenderable.swift b/Sources/Down/Renderers/DownGroffRenderable.swift index 11383827..8c45c569 100644 --- a/Sources/Down/Renderers/DownGroffRenderable.swift +++ b/Sources/Down/Renderers/DownGroffRenderable.swift @@ -10,48 +10,68 @@ import Foundation import libcmark public protocol DownGroffRenderable: DownRenderable { + func toGroff(_ options: DownOptions, width: Int32) throws -> String + } extension DownGroffRenderable { - /// Generates a groff man string from the `markdownString` property + + /// Generates a groff man string from the `markdownString` property. /// /// - Parameters: - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - width: The width to break on, defaulting to 0 - /// - Returns: groff man string - /// - Throws: `DownErrors` depending on the scenario + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - width: The width to break on, defaulting to 0. + /// + /// - Returns: + /// A groff man string. + /// + /// - Throws: + /// `DownErrors` depending on the scenario. + public func toGroff(_ options: DownOptions = .default, width: Int32 = 0) throws -> String { let ast = try DownASTRenderer.stringToAST(markdownString, options: options) let groff = try DownGroffRenderer.astToGroff(ast, options: options, width: width) cmark_node_free(ast) return groff } + } public struct DownGroffRenderer { - /// Generates a groff man string from the given abstract syntax tree + + /// Generates a groff man string from the given abstract syntax tree. /// - /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns + /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns. /// /// - Parameters: - /// - ast: The `cmark_node` representing the abstract syntax tree - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - width: The width to break on, defaulting to 0 - /// - Returns: groff man string - /// - Throws: `ASTRenderingError` if the AST could not be converted - public static func astToGroff(_ ast: UnsafeMutablePointer<cmark_node>, + /// - ast: The `cmark_node` representing the abstract syntax tree. + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - width: The width to break on, defaulting to 0. + /// + /// - Returns: + /// A groff man string. + /// + /// - Throws: + /// `ASTRenderingError` if the AST could not be converted. + + public static func astToGroff(_ ast: CMarkNode, options: DownOptions = .default, width: Int32 = 0) throws -> String { + guard let cGroffString = cmark_render_man(ast, options.rawValue, width) else { throw DownErrors.astRenderingError } - defer { free(cGroffString) } - + + defer { + free(cGroffString) + } + guard let groffString = String(cString: cGroffString, encoding: String.Encoding.utf8) else { throw DownErrors.astRenderingError } return groffString } + } diff --git a/Sources/Down/Renderers/DownHTMLRenderable.swift b/Sources/Down/Renderers/DownHTMLRenderable.swift index 96902108..7df04b9c 100644 --- a/Sources/Down/Renderers/DownHTMLRenderable.swift +++ b/Sources/Down/Renderers/DownHTMLRenderable.swift @@ -10,40 +10,60 @@ import Foundation import libcmark public protocol DownHTMLRenderable: DownRenderable { + func toHTML(_ options: DownOptions) throws -> String + } extension DownHTMLRenderable { - /// Generates an HTML string from the `markdownString` property + + /// Generates an HTML string from the `markdownString` property. + /// + /// - Parameters: + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// + /// - Returns: + /// An HTML string. /// - /// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - Returns: HTML string - /// - Throws: `DownErrors` depending on the scenario + /// - Throws: + /// `DownErrors` depending on the scenario. + public func toHTML(_ options: DownOptions = .default) throws -> String { return try markdownString.toHTML(options) } + } public struct DownHTMLRenderer { - /// Generates an HTML string from the given abstract syntax tree + + /// Generates an HTML string from the given abstract syntax tree. /// - /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns + /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns. /// /// - Parameters: - /// - ast: The `cmark_node` representing the abstract syntax tree - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - Returns: HTML string - /// - Throws: `ASTRenderingError` if the AST could not be converted - public static func astToHTML(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default) throws -> String { + /// - ast: The `cmark_node` representing the abstract syntax tree. + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// + /// - Returns: + /// An HTML string. + /// + /// - Throws: + /// `ASTRenderingError` if the AST could not be converted. + + public static func astToHTML(_ ast: CMarkNode, options: DownOptions = .default) throws -> String { guard let cHTMLString = cmark_render_html(ast, options.rawValue) else { throw DownErrors.astRenderingError } - defer { free(cHTMLString) } - + + defer { + free(cHTMLString) + } + guard let htmlString = String(cString: cHTMLString, encoding: String.Encoding.utf8) else { throw DownErrors.astRenderingError } return htmlString } + } diff --git a/Sources/Down/Renderers/DownLaTeXRenderable.swift b/Sources/Down/Renderers/DownLaTeXRenderable.swift index 78f65e36..ee4622eb 100644 --- a/Sources/Down/Renderers/DownLaTeXRenderable.swift +++ b/Sources/Down/Renderers/DownLaTeXRenderable.swift @@ -10,48 +10,68 @@ import Foundation import libcmark public protocol DownLaTeXRenderable: DownRenderable { + func toLaTeX(_ options: DownOptions, width: Int32) throws -> String + } extension DownLaTeXRenderable { - /// Generates a LaTeX string from the `markdownString` property + + /// Generates a LaTeX string from the `markdownString` property. /// /// - Parameters: - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - width: The width to break on, defaulting to 0 - /// - Returns: LaTeX string - /// - Throws: `DownErrors` depending on the scenario + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - width: The width to break on, defaulting to 0. + /// + /// - Returns: + /// A LaTeX string. + /// + /// - Throws: + /// `DownErrors` depending on the scenario. + public func toLaTeX(_ options: DownOptions = .default, width: Int32 = 0) throws -> String { let ast = try DownASTRenderer.stringToAST(markdownString, options: options) let latex = try DownLaTeXRenderer.astToLaTeX(ast, options: options, width: width) cmark_node_free(ast) return latex } + } public struct DownLaTeXRenderer { - /// Generates a LaTeX string from the given abstract syntax tree + + /// Generates a LaTeX string from the given abstract syntax tree. /// - /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns + /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns. /// /// - Parameters: - /// - ast: The `cmark_node` representing the abstract syntax tree - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - width: The width to break on, defaulting to 0 - /// - Returns: LaTeX string - /// - Throws: `ASTRenderingError` if the AST could not be converted - public static func astToLaTeX(_ ast: UnsafeMutablePointer<cmark_node>, + /// - ast: The `cmark_node` representing the abstract syntax tree. + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - width: The width to break on, defaulting to 0. + /// + /// - Returns: + /// A LaTeX string. + /// + /// - Throws: + /// `ASTRenderingError` if the AST could not be converted. + + public static func astToLaTeX(_ ast: CMarkNode, options: DownOptions = .default, width: Int32 = 0) throws -> String { + guard let cLatexString = cmark_render_latex(ast, options.rawValue, width) else { throw DownErrors.astRenderingError } - defer { free(cLatexString) } - + + defer { + free(cLatexString) + } + guard let latexString = String(cString: cLatexString, encoding: String.Encoding.utf8) else { throw DownErrors.astRenderingError } - + return latexString } + } diff --git a/Sources/Down/Renderers/DownRenderable.swift b/Sources/Down/Renderers/DownRenderable.swift index c5daec92..3d8423e0 100644 --- a/Sources/Down/Renderers/DownRenderable.swift +++ b/Sources/Down/Renderers/DownRenderable.swift @@ -9,6 +9,9 @@ import Foundation public protocol DownRenderable { - /// A string containing CommonMark Markdown + + /// A string containing CommonMark Markdown. + var markdownString: String { get set } + } diff --git a/Sources/Down/Renderers/DownXMLRenderable.swift b/Sources/Down/Renderers/DownXMLRenderable.swift index a2cbf5cb..2dde0ffb 100644 --- a/Sources/Down/Renderers/DownXMLRenderable.swift +++ b/Sources/Down/Renderers/DownXMLRenderable.swift @@ -10,43 +10,63 @@ import Foundation import libcmark public protocol DownXMLRenderable: DownRenderable { + func toXML(_ options: DownOptions) throws -> String + } extension DownXMLRenderable { - /// Generates an XML string from the `markdownString` property + + /// Generates an XML string from the `markdownString` property. + /// + /// - Parameters: + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// + /// - Returns: + /// An XML string. /// - /// - Parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - Returns: XML string - /// - Throws: `DownErrors` depending on the scenario + /// - Throws: + /// `DownErrors` depending on the scenario. + public func toXML(_ options: DownOptions = .default) throws -> String { let ast = try DownASTRenderer.stringToAST(markdownString, options: options) let xml = try DownXMLRenderer.astToXML(ast, options: options) cmark_node_free(ast) return xml } + } public struct DownXMLRenderer { + /// Generates an XML string from the given abstract syntax tree /// - /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns + /// **Note:** caller is responsible for calling `cmark_node_free(ast)` after this returns. /// /// - Parameters: - /// - ast: The `cmark_node` representing the abstract syntax tree - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - Returns: XML string - /// - Throws: `ASTRenderingError` if the AST could not be converted - public static func astToXML(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default) throws -> String { + /// - ast: The `cmark_node` representing the abstract syntax tree. + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// + /// - Returns: + /// An XML string. + /// + /// - Throws: + /// `ASTRenderingError` if the AST could not be converted. + + public static func astToXML(_ ast: CMarkNode, options: DownOptions = .default) throws -> String { guard let cXMLString = cmark_render_xml(ast, options.rawValue) else { throw DownErrors.astRenderingError } - defer { free(cXMLString) } - + + defer { + free(cXMLString) + } + guard let xmlString = String(cString: cXMLString, encoding: String.Encoding.utf8) else { throw DownErrors.astRenderingError } return xmlString } + } diff --git a/Sources/Down/Views/BundleHelper.swift b/Sources/Down/Views/BundleHelper.swift index 6d0dcdb0..7c163c56 100644 --- a/Sources/Down/Views/BundleHelper.swift +++ b/Sources/Down/Views/BundleHelper.swift @@ -19,7 +19,7 @@ extension Foundation.Bundle { Bundle(for: BundleFinder.self).resourceURL, // For command-line tools. - Bundle.main.bundleURL, + Bundle.main.bundleURL ] for candidate in candidates { diff --git a/Sources/Down/Views/DownView.swift b/Sources/Down/Views/DownView.swift index ca33beec..13b36abd 100644 --- a/Sources/Down/Views/DownView.swift +++ b/Sources/Down/Views/DownView.swift @@ -7,30 +7,48 @@ // #if !os(Linux) + #if os(tvOS) || os(watchOS) - // Sorry, not available for tvOS nor watchOS + +// Sorry, not available for tvOS nor watchOS + #else + import WebKit // MARK: - Public API -public typealias DownViewClosure = () -> () +public typealias DownViewClosure = () -> Void open class DownView: WKWebView { - /// Initializes a web view with the results of rendering a CommonMark Markdown string + // MARK: - Life cycle + + /// Initializes a web view with the results of rendering a CommonMark Markdown string. /// /// - Parameters: - /// - frame: The frame size of the web view - /// - markdownString: A string containing CommonMark Markdown - /// - openLinksInBrowser: Whether or not to open links using an external browser - /// - templateBundle: Optional custom template bundle. Leaving this as `nil` will use the bundle included with Down. - /// - configuration: Optional custom web view configuration. - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - didLoadSuccessfully: Optional callback for when the web content has loaded successfully - /// - writableBundle: Whether or not the bundle folder is writable. - /// - Throws: `DownErrors` depending on the scenario - public init(frame: CGRect, markdownString: String, openLinksInBrowser: Bool = true, templateBundle: Bundle? = nil, writableBundle: Bool = false, configuration: WKWebViewConfiguration? = nil, options: DownOptions = .default, didLoadSuccessfully: DownViewClosure? = nil) throws { + /// - frame: The frame size of the web view + /// - markdownString: A string containing CommonMark Markdown + /// - openLinksInBrowser: Whether or not to open links using an external browser + /// - templateBundle: Optional custom template bundle. Leaving this as `nil` will use the bundle included + /// with Down. + /// - configuration: Optional custom web view configuration. + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` + /// - didLoadSuccessfully: Optional callback for when the web content has loaded successfully + /// - writableBundle: Whether or not the bundle folder is writable. + /// + /// - Throws: + /// `DownErrors` depending on the scenario. + + public init(frame: CGRect, + markdownString: String, + openLinksInBrowser: Bool = true, + templateBundle: Bundle? = nil, + writableBundle: Bool = false, + configuration: WKWebViewConfiguration? = nil, + options: DownOptions = .default, + didLoadSuccessfully: DownViewClosure? = nil) throws { + self.options = options self.didLoadSuccessfully = didLoadSuccessfully self.writableBundle = writableBundle @@ -46,7 +64,7 @@ open class DownView: WKWebView { super.init(frame: frame, configuration: configuration ?? WKWebViewConfiguration()) #if os(macOS) - setupMacEnvironment() + setupMacEnvironment() #endif if openLinksInBrowser || didLoadSuccessfully != nil { navigationDelegate = self } @@ -62,22 +80,29 @@ open class DownView: WKWebView { clearTemporaryDirectory() } #endif - + // MARK: - API - - /// Renders the given CommonMark Markdown string into HTML and updates the DownView while keeping the style intact + + /// Renders the given CommonMark Markdown string into HTML and updates the DownView while keeping the style intact. /// /// - Parameters: - /// - markdownString: A string containing CommonMark Markdown - /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - /// - didLoadSuccessfully: Optional callback for when the web content has loaded successfully - /// - Throws: `DownErrors` depending on the scenario - public func update(markdownString: String, options: DownOptions? = nil, didLoadSuccessfully: DownViewClosure? = nil) throws { + /// - markdownString: A string containing CommonMark Markdown. + /// - options: `DownOptions` to modify parsing or rendering, defaulting to `.default`. + /// - didLoadSuccessfully: Optional callback for when the web content has loaded successfully. + /// + /// - Throws: + /// `DownErrors` depending on the scenario. + + public func update(markdownString: String, + options: DownOptions? = nil, + didLoadSuccessfully: DownViewClosure? = nil) throws { + // Note: As the init method sets this initially, we only overwrite them if - // a non-nil value is passed in + // a non-nil value is passed in. if let options = options { self.options = options } + if let didLoadSuccessfully = didLoadSuccessfully { self.didLoadSuccessfully = didLoadSuccessfully } @@ -96,15 +121,11 @@ open class DownView: WKWebView { }() #if os(macOS) - private lazy var temporaryDirectoryURL: URL = { - return try! FileManager.default.url(for: .itemReplacementDirectory, - in: .userDomainMask, - appropriateFor: URL(fileURLWithPath: NSTemporaryDirectory()), - create: true).appendingPathComponent("Down", isDirectory: true) - }() + private var temporaryDirectoryURL: URL? #endif - + private var didLoadSuccessfully: DownViewClosure? + } // MARK: - Private API @@ -116,15 +137,15 @@ private extension DownView { let pageHTMLString = try htmlFromTemplate(htmlString) #if os(iOS) - if writableBundle { - let newIndexUrl = try writeTempIndexFile(pageHTMLString: pageHTMLString) - loadFileURL(newIndexUrl, allowingReadAccessTo: newIndexUrl.deletingLastPathComponent()) - } else { - loadHTMLString(pageHTMLString, baseURL: baseURL) - } + if writableBundle { + let newIndexUrl = try writeTempIndexFile(pageHTMLString: pageHTMLString) + loadFileURL(newIndexUrl, allowingReadAccessTo: newIndexUrl.deletingLastPathComponent()) + } else { + loadHTMLString(pageHTMLString, baseURL: baseURL) + } #elseif os(macOS) - let indexURL = try createTemporaryBundle(pageHTMLString: pageHTMLString) - loadFileURL(indexURL, allowingReadAccessTo: indexURL.deletingLastPathComponent()) + let indexURL = try createTemporaryBundle(pageHTMLString: pageHTMLString) + loadFileURL(indexURL, allowingReadAccessTo: indexURL.deletingLastPathComponent()) #endif } @@ -143,8 +164,19 @@ private extension DownView { #if os(macOS) func createTemporaryBundle(pageHTMLString: String) throws -> URL { - guard let bundleResourceURL = bundle.resourceURL - else { throw DownErrors.nonStandardBundleFormatError } + guard let bundleResourceURL = bundle.resourceURL else { + throw DownErrors.nonStandardBundleFormatError + } + + let fileManager = FileManager.default + + let temporaryDirectoryURL = try fileManager.url(for: .itemReplacementDirectory, + in: .userDomainMask, + appropriateFor: URL(fileURLWithPath: NSTemporaryDirectory()), + create: true).appendingPathComponent("Down", isDirectory: true) + + self.temporaryDirectoryURL = temporaryDirectoryURL + let indexURL = temporaryDirectoryURL.appendingPathComponent("index.html", isDirectory: false) // If updating markdown contents, no need to re-copy bundle. @@ -168,7 +200,9 @@ private extension DownView { @objc func clearTemporaryDirectory() { - try? FileManager.default.removeItem(at: temporaryDirectoryURL) + if let temporaryDirectoryURL = temporaryDirectoryURL { + try? FileManager.default.removeItem(at: temporaryDirectoryURL) + } } #endif @@ -177,11 +211,18 @@ private extension DownView { // MARK: - WKNavigationDelegate extension DownView: WKNavigationDelegate { - public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationResponse: WKNavigationResponse, + decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + decisionHandler(.allow) } - public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { return decisionHandler(.allow) } switch navigationAction.navigationType { @@ -208,18 +249,21 @@ extension DownView: WKNavigationDelegate { NSWorkspace.shared.open(url) #endif } - + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { didLoadSuccessfully?() } - + } private extension WKNavigationDelegate { + /// A wrapper for `UIApplication.shared.openURL` so that an empty default /// implementation is available in app extensions func openURL(url: URL) {} + } #endif + #endif // !os(Linux) diff --git a/Tests/DownTests/AST/ListItemPrefixGeneratorTests.swift b/Tests/DownTests/AST/ListItemPrefixGeneratorTests.swift index f74f24a4..a00f60b1 100644 --- a/Tests/DownTests/AST/ListItemPrefixGeneratorTests.swift +++ b/Tests/DownTests/AST/ListItemPrefixGeneratorTests.swift @@ -32,4 +32,5 @@ class ListItemPrefixGeneratorTests: XCTestCase { XCTAssertEqual("•", sut.next()) XCTAssertNil(sut.next()) } + } diff --git a/Tests/DownTests/AST/NodeTests.swift b/Tests/DownTests/AST/NodeTests.swift index 129c95dc..c6100a48 100644 --- a/Tests/DownTests/AST/NodeTests.swift +++ b/Tests/DownTests/AST/NodeTests.swift @@ -30,6 +30,7 @@ class NodeTests: XCTestCase { // Then XCTAssertEqual(sut.listNestDepthResults, [0, 1, 2]) } + } // MARK: - Helpers @@ -41,11 +42,11 @@ extension NodeTests { let document = try Down(markdownString: markdown).toDocument() document.accept(visitor) } catch { - XCTFail() + XCTFail("Failed to generate document.") } } -} +} private class NodeVisitor: DebugVisitor { @@ -55,4 +56,5 @@ private class NodeVisitor: DebugVisitor { listNestDepthResults.append(node.nestDepth) return super.visit(list: node) } + } diff --git a/Tests/DownTests/AST/VisitorTests.swift b/Tests/DownTests/AST/VisitorTests.swift index 193ec4ce..146246d5 100644 --- a/Tests/DownTests/AST/VisitorTests.swift +++ b/Tests/DownTests/AST/VisitorTests.swift @@ -11,18 +11,18 @@ import SnapshotTesting class VisitorTests: XCTestCase { - func result(for markdown: String) -> String { + func result(for markdown: String) throws -> String { let down = Down(markdownString: markdown) - return try! down.toAttributedString(styler: EmptyStyler()).string + return try down.toAttributedString(styler: EmptyStyler()).string } - func debugResult(for markdown: String) -> String { + func debugResult(for markdown: String) throws -> String { let down = Down(markdownString: markdown) - let document = try! down.toDocument() + let document = try down.toDocument() return document.accept(DebugVisitor()) } - func testBlockQuote() { + func testBlockQuote() throws { // Given let markdown = """ Text text. @@ -35,11 +35,11 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testList() { + func testList() throws { // Given let markdown = """ Text text. @@ -54,11 +54,11 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testCodeBlock() { + func testCodeBlock() throws { // Given let markdown = """ Text text. @@ -72,11 +72,11 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testHtmlBlock() { + func testHtmlBlock() throws { // Given let markdown = """ Text text. @@ -89,11 +89,11 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testParagraph() { + func testParagraph() throws { // Given let markdown = """ Text text. @@ -104,11 +104,11 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testHeading() { + func testHeading() throws { // Given let markdown = """ Text text. @@ -119,11 +119,11 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testThematicBreak() { + func testThematicBreak() throws { // Given let markdown = """ Text text. @@ -134,11 +134,11 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testSoftBreak() { + func testSoftBreak() throws { // Given let markdown = """ Text text @@ -146,11 +146,11 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testLineBreak() { + func testLineBreak() throws { // Given let markdown = """ Text text.\\ @@ -158,35 +158,36 @@ class VisitorTests: XCTestCase { """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testInline() { + func testInline() throws { // Given let markdown = """ Text **strong _emphasis `code` <html>_** """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } - func testLink() { + func testLink() throws { // Given let markdown = """ Text [link](www.example.com) text ![image](www.example.com) """ // Then - assertSnapshot(matching: result(for: markdown), as: .lines) - assertSnapshot(matching: debugResult(for: markdown), as: .lines) + assertSnapshot(matching: try result(for: markdown), as: .lines) + assertSnapshot(matching: try debugResult(for: markdown), as: .lines) } } private class EmptyStyler: Styler { - var listPrefixAttributes: [NSAttributedString.Key : Any] = [:] + + var listPrefixAttributes: [NSAttributedString.Key: Any] = [:] func style(document str: NSMutableAttributedString) {} func style(blockQuote str: NSMutableAttributedString, nestDepth: Int) {} func style(list str: NSMutableAttributedString, nestDepth: Int) {} @@ -208,4 +209,5 @@ private class EmptyStyler: Styler { func style(strong str: NSMutableAttributedString) {} func style(link str: NSMutableAttributedString, title: String?, url: String?) {} func style(image str: NSMutableAttributedString, title: String?, url: String?) {} + } diff --git a/Tests/DownTests/BindingTests.swift b/Tests/DownTests/BindingTests.swift index 0640d4b4..7faf480c 100644 --- a/Tests/DownTests/BindingTests.swift +++ b/Tests/DownTests/BindingTests.swift @@ -7,45 +7,40 @@ // import XCTest +import SnapshotTesting @testable import Down class BindingTests: XCTestCase { - let down = Down(markdownString: "## [Down](https://github.com/iwasrobbed/Down)") + let down = Down(markdownString: "## [Down](https://github.com/johnxnnguyen/Down)") - func testASTBindingsWork() { - let ast = try? down.toAST() - XCTAssertNotNil(ast) + func testASTBindingsWork() throws { + _ = try down.toAST() } - func testHTMLBindingsWork() { - let html = try? down.toHTML() - XCTAssertNotNil(html) - XCTAssertTrue(html == "<h2><a href=\"https://github.com/iwasrobbed/Down\">Down</a></h2>\n") + func testHTMLBindingsWork() throws { + let html = try down.toHTML() + assertSnapshot(matching: html, as: .lines) } - func testXMLBindingsWork() { - let xml = try? down.toXML() - XCTAssertNotNil(xml) - XCTAssertTrue(xml == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE document SYSTEM \"CommonMark.dtd\">\n<document xmlns=\"http://commonmark.org/xml/1.0\">\n <heading level=\"2\">\n <link destination=\"https://github.com/iwasrobbed/Down\" title=\"\">\n <text xml:space=\"preserve\">Down</text>\n </link>\n </heading>\n</document>\n") + func testXMLBindingsWork() throws { + let xml = try down.toXML() + assertSnapshot(matching: xml, as: .lines) } - func testGroffBindingsWork() { - let man = try? down.toGroff() - XCTAssertNotNil(man) - XCTAssertTrue(man == ".SS\nDown (https://github.com/iwasrobbed/Down)\n") + func testGroffBindingsWork() throws { + let man = try down.toGroff() + assertSnapshot(matching: man, as: .lines) } - func testLaTeXBindngsWork() { - let latex = try? down.toLaTeX() - XCTAssertNotNil(latex) - XCTAssertTrue(latex == "\\subsection{\\href{https://github.com/iwasrobbed/Down}{Down}}\n") + func testLaTeXBindngsWork() throws { + let latex = try down.toLaTeX() + assertSnapshot(matching: latex, as: .lines) } - func testCommonMarkBindngsWork() { - let commonMark = try? down.toCommonMark() - XCTAssertNotNil(commonMark) - XCTAssertTrue(commonMark == "## [Down](https://github.com/iwasrobbed/Down)\n") + func testCommonMarkBindngsWork() throws { + let commonMark = try down.toCommonMark() + assertSnapshot(matching: commonMark, as: .lines) } - + } diff --git a/Tests/DownTests/DownViewTests.swift b/Tests/DownTests/DownViewTests.swift index 3402ca43..cad19602 100644 --- a/Tests/DownTests/DownViewTests.swift +++ b/Tests/DownTests/DownViewTests.swift @@ -18,22 +18,26 @@ class DownViewTests: XCTestCase { func testInstantiation() { let expect1 = expectation(description: "DownView sets the html and validates the html is correct") var downView: DownView? - downView = try? DownView(frame: .zero, markdownString: "## [Down](https://github.com/iwasrobbed/Down)", didLoadSuccessfully: { + + downView = try? DownView(frame: .zero, + markdownString: "## [Down](https://github.com/iwasrobbed/Down)", + didLoadSuccessfully: { + self._pageContents(for: downView!) { htmlString in XCTAssertTrue(htmlString!.contains("css/down.min.css")) XCTAssertTrue(htmlString!.contains("https://github.com/iwasrobbed/Down")) - + expect1.fulfill() } }) - + waitForExpectations(timeout: 10) { (error: Error?) in if let error = error { XCTFail("waitForExpectationsWithTimeout errored: \(error)") } } } - + func testUpdatingMarkdown() { let expect1 = expectation(description: "DownView sets the html and validates the html is correct") var downView: DownView? @@ -41,27 +45,27 @@ class DownViewTests: XCTestCase { self._pageContents(for: downView!) { htmlString in XCTAssertTrue(htmlString!.contains("css/down.min.css")) XCTAssertTrue(htmlString!.contains("https://github.com/iwasrobbed/Down")) - + expect1.fulfill() } } - + waitForExpectations(timeout: 10) { (error: Error?) in if let error = error { XCTFail("waitForExpectationsWithTimeout errored: \(error)") } } - + let expect2 = expectation(description: "DownView sets the html and validates the html is correct") - try? downView?.update(markdownString: "## [Google](https://google.com)") { + try? downView?.update(markdownString: "## [Google](https://google.com)") { self._pageContents(for: downView!) { htmlString in XCTAssertTrue(htmlString!.contains("css/down.min.css")) XCTAssertTrue(htmlString!.contains("https://google.com")) - + expect2.fulfill() } } - + waitForExpectations(timeout: 10) { (error: Error?) in if let error = error { XCTFail("waitForExpectationsWithTimeout errored: \(error)") @@ -80,7 +84,11 @@ class DownViewTests: XCTestCase { } var downView: DownView? - downView = try? DownView(frame: .zero, markdownString: "## [Down](https://github.com/iwasrobbed/Down)", templateBundle: templateBundle, didLoadSuccessfully: { + downView = try? DownView(frame: .zero, + markdownString: "## [Down](https://github.com/iwasrobbed/Down)", + templateBundle: templateBundle, + didLoadSuccessfully: { + self._pageContents(for: downView!) { htmlString in XCTAssertTrue(htmlString!.contains("css/down.min.css")) XCTAssertTrue(htmlString!.contains("https://github.com/iwasrobbed/Down")) @@ -98,7 +106,9 @@ class DownViewTests: XCTestCase { } func testInstantiationWithCustomWritableTemplateBundle() { - let expect1 = expectation(description: "DownView accepts and loads custom bundle files from a user writable location") + let expect1 = expectation( + description: "DownView accepts and loads custom bundle files from a user writable location" + ) guard let bundle = Bundle(for: type(of: self)).url(forResource: "TestDownView", withExtension: "bundle"), @@ -108,13 +118,20 @@ class DownViewTests: XCTestCase { return } - let markdownString = """ -```swift -let x = 1 -``` -""" + let markdown = """ + ```swift + let x = 1 + ``` + """ + var downView: DownView? - downView = try? DownView(frame: .zero, markdownString: markdownString, templateBundle: templateBundle, writableBundle: true, didLoadSuccessfully: { + + downView = try? DownView(frame: .zero, + markdownString: markdown, + templateBundle: templateBundle, + writableBundle: true, + didLoadSuccessfully: { + self._pageContents(for: downView!) { htmlString in XCTAssertTrue(htmlString!.contains("css/down.min.css")) XCTAssertTrue(htmlString!.contains("hljs-keyword")) @@ -132,21 +149,21 @@ let x = 1 } func testDownOptions() { - let markdownString = "## [Down](https://github.com/iwasrobbed/Down)\n\n<strong>I'm strong!</strong>" + let markdown = "## [Down](https://github.com/iwasrobbed/Down)\n\n<strong>I'm strong!</strong>" let renderedHTML = "<strong>I'm strong!</strong>" // Set this view to initially be HTML safe let safeExpect = expectation(description: "DownView default init strips unsafe HTML") let toggleSafeExpect = expectation(description: "DownView update to unsafe does not strip unsafe HTML") var safeDownView: DownView? - safeDownView = try? DownView(frame: .zero, markdownString: markdownString, didLoadSuccessfully: { + safeDownView = try? DownView(frame: .zero, markdownString: markdown, didLoadSuccessfully: { self._pageContents(for: safeDownView!) { htmlString in XCTAssertTrue(safeDownView?.options == .default) XCTAssertFalse(htmlString!.contains(renderedHTML)) safeExpect.fulfill() // Then change it to HTML unsafe options and ensure it's changed - try? safeDownView?.update(markdownString: markdownString, options: .unsafe, didLoadSuccessfully: { + try? safeDownView?.update(markdownString: markdown, options: .unsafe, didLoadSuccessfully: { XCTAssertTrue(safeDownView?.options == .unsafe) self._pageContents(for: safeDownView!) { htmlString in XCTAssertTrue(htmlString!.contains(renderedHTML)) @@ -160,14 +177,15 @@ let x = 1 let unsafeExpect = expectation(description: "DownView unsafe init does not strip unsafe HTML") let toggleUnsafeExpect = expectation(description: "DownView update to safe strips unsafe HTML") var unsafeDownView: DownView? - unsafeDownView = try? DownView(frame: .zero, markdownString: markdownString, options: .unsafe, didLoadSuccessfully: { + + unsafeDownView = try? DownView(frame: .zero, markdownString: markdown, options: .unsafe, didLoadSuccessfully: { self._pageContents(for: unsafeDownView!) { htmlString in XCTAssertTrue(unsafeDownView?.options == .unsafe) XCTAssertTrue(htmlString!.contains(renderedHTML)) unsafeExpect.fulfill() // And then toggle it to be HTML safe and ensure it's changed - try? unsafeDownView?.update(markdownString: markdownString, options: .default, didLoadSuccessfully: { + try? unsafeDownView?.update(markdownString: markdown, options: .default, didLoadSuccessfully: { XCTAssertTrue(unsafeDownView?.options == .default) self._pageContents(for: unsafeDownView!) { htmlString in XCTAssertFalse(htmlString!.contains(renderedHTML)) @@ -222,7 +240,11 @@ let x = 1 let configuration = WKWebViewConfiguration() configuration.setURLSchemeHandler(mockURLSchemeHandler, forURLScheme: mockURLScheme) - downView = try? DownView(frame: .zero, markdownString: "[Link](\(mockURL.absoluteString))", openLinksInBrowser: true, configuration: configuration, didLoadSuccessfully: didLoadSuccessfully) + downView = try? DownView(frame: .zero, + markdownString: "[Link](\(mockURL.absoluteString))", + openLinksInBrowser: true, + configuration: configuration, + didLoadSuccessfully: didLoadSuccessfully) waitForExpectations(timeout: 10) { (error: Error?) in if let error = error { @@ -230,15 +252,17 @@ let x = 1 } } } + } private extension DownViewTests { - - func _pageContents(for downView: DownView, completion: @escaping (_ htmlString: String?) -> ()) { + + func _pageContents(for downView: DownView, completion: @escaping (_ htmlString: String?) -> Void) { downView.evaluateJavaScript("document.documentElement.outerHTML.toString()") { (html: Any?, _) in completion(html as? String) } } - + } + #endif diff --git a/Tests/DownTests/NSAttributedStringTests.swift b/Tests/DownTests/NSAttributedStringTests.swift index 67d10575..fdc4497d 100644 --- a/Tests/DownTests/NSAttributedStringTests.swift +++ b/Tests/DownTests/NSAttributedStringTests.swift @@ -12,7 +12,8 @@ import XCTest class NSAttributedStringTests: XCTestCase { func testAttributedStringBindingsWork() { - let attributedString = try? Down(markdownString: "## [Down](https://github.com/iwasrobbed/Down)").toAttributedString() + let markdown = "## [Down](https://github.com/johnxnguyen/Down)" + let attributedString = try? Down(markdownString: markdown).toAttributedString() XCTAssertNotNil(attributedString) XCTAssertTrue(attributedString!.string == "Down\n") } diff --git a/Tests/DownTests/Styler/BlockQuoteStyleTests.swift b/Tests/DownTests/Styler/BlockQuoteStyleTests.swift index 1e9e304f..fb669c6e 100644 --- a/Tests/DownTests/Styler/BlockQuoteStyleTests.swift +++ b/Tests/DownTests/Styler/BlockQuoteStyleTests.swift @@ -10,11 +10,10 @@ class BlockQuoteStyleTests: StylerTestSuite { - /// # Important - /// - /// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise - /// the comparison may fail. These tests were recorded on the **iPhone 12** simulator. - /// + // # Important + // + // Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise + // the comparison may fail. These tests were recorded on the **iPhone 12** simulator. // MARK: - Alignment @@ -206,6 +205,7 @@ class BlockQuoteStyleTests: StylerTestSuite { // Then assertStyle(for: markdown, width: .wide) } + } #endif diff --git a/Tests/DownTests/Styler/CodeBlockStyleTests.swift b/Tests/DownTests/Styler/CodeBlockStyleTests.swift index 4defc36a..31637172 100644 --- a/Tests/DownTests/Styler/CodeBlockStyleTests.swift +++ b/Tests/DownTests/Styler/CodeBlockStyleTests.swift @@ -52,6 +52,7 @@ class CodeBlockStyleTests: StylerTestSuite { // Then assertStyle(for: markdown, width: .wide) } + } #endif diff --git a/Tests/DownTests/Styler/HeadingStyleTests.swift b/Tests/DownTests/Styler/HeadingStyleTests.swift index 37869ad8..5d607cbe 100644 --- a/Tests/DownTests/Styler/HeadingStyleTests.swift +++ b/Tests/DownTests/Styler/HeadingStyleTests.swift @@ -10,11 +10,10 @@ class HeadingStyleTests: StylerTestSuite { - /// # Important - /// - /// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise - /// the comparison may fail. These tests were recorded on the **iPhone 12** simulator. - /// + // # Important + // + // Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise + // the comparison may fail. These tests were recorded on the **iPhone 12** simulator. // MARK: - Heading Levels @@ -105,6 +104,7 @@ class HeadingStyleTests: StylerTestSuite { // Then assertStyle(for: markdown, width: .wide) } + } #endif diff --git a/Tests/DownTests/Styler/Helpers/CGPoint_TranslateTests.swift b/Tests/DownTests/Styler/Helpers/CGPointTranslateTests.swift similarity index 90% rename from Tests/DownTests/Styler/Helpers/CGPoint_TranslateTests.swift rename to Tests/DownTests/Styler/Helpers/CGPointTranslateTests.swift index 25dabc31..2a7f2bed 100644 --- a/Tests/DownTests/Styler/Helpers/CGPoint_TranslateTests.swift +++ b/Tests/DownTests/Styler/Helpers/CGPointTranslateTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import Down -class CGPoint_TranslateTests: XCTestCase { +class CGPointTranslateTests: XCTestCase { func testPointTranslation() { // Given @@ -21,4 +21,5 @@ class CGPoint_TranslateTests: XCTestCase { // Then XCTAssertEqual(CGPoint(x: 4, y: 6), result) } + } diff --git a/Tests/DownTests/Styler/Helpers/CGRect_HelpersTests.swift b/Tests/DownTests/Styler/Helpers/CGRectHelpersTests.swift similarity index 94% rename from Tests/DownTests/Styler/Helpers/CGRect_HelpersTests.swift rename to Tests/DownTests/Styler/Helpers/CGRectHelpersTests.swift index 7d393723..a4470bf4 100644 --- a/Tests/DownTests/Styler/Helpers/CGRect_HelpersTests.swift +++ b/Tests/DownTests/Styler/Helpers/CGRectHelpersTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import Down -class CGRect_HelpersTests: XCTestCase { +class CGRectHelpersTests: XCTestCase { func testRectInitializationWithBoundaries() { // When @@ -29,4 +29,5 @@ class CGRect_HelpersTests: XCTestCase { // Then XCTAssertEqual(CGRect(x: 6, y: 8, width: 3, height: 4), result) } + } diff --git a/Tests/DownTests/Styler/Helpers/NSAttributedString+HelpersTests.swift b/Tests/DownTests/Styler/Helpers/NSAttributedString+HelpersTests.swift index 470b755f..718b5bd2 100644 --- a/Tests/DownTests/Styler/Helpers/NSAttributedString+HelpersTests.swift +++ b/Tests/DownTests/Styler/Helpers/NSAttributedString+HelpersTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import Down -class NSAttributedString_HelpersTests: XCTestCase { +class NSAttributedStringHelpersTests: XCTestCase { let dummyKey = NSAttributedString.Key(rawValue: "key") let dummyValue = "value" @@ -91,8 +91,7 @@ class NSAttributedString_HelpersTests: XCTestCase { XCTAssertEqual(result[1], NSRange(location: 19, length: 4)) // "you " } - // MARK: - Missing Attribute Ranges - + // MARK: - Missing Attribute Ranges func testRangesMissingAttribute_None() { // Given @@ -165,7 +164,7 @@ class NSAttributedString_HelpersTests: XCTestCase { func testParagraphRanges() { // Given - let sut = NSAttributedString(string:"Hello\nhello\nworld") + let sut = NSAttributedString(string: "Hello\nhello\nworld") // When let result = sut.paragraphRanges() @@ -180,7 +179,7 @@ class NSAttributedString_HelpersTests: XCTestCase { func testParagraphRangesOfStringThatHasParagraphSeparators() { // Given let separator = "\u{2029}" - let sut = NSAttributedString(string:"Hello\(separator)hello\(separator)world") + let sut = NSAttributedString(string: "Hello\(separator)hello\(separator)world") // When let result = sut.paragraphRanges() @@ -194,7 +193,7 @@ class NSAttributedString_HelpersTests: XCTestCase { func testParagraphRangesOfStringWithLargeBreaks() { // Given - let sut = NSAttributedString(string:"Hello\n\nhello\n\n\nworld") + let sut = NSAttributedString(string: "Hello\n\nhello\n\n\nworld") // When let result = sut.paragraphRanges() @@ -206,28 +205,29 @@ class NSAttributedString_HelpersTests: XCTestCase { XCTAssertEqual(result[2], NSRange(location: 15, length: 5)) // "world } - // MARK: - Enumeration - - func testEnumerationOfAttributes() { - // Given - let sut = NSMutableAttributedString() - sut.append(make("Hello ", attributed: true)) - sut.append(make("world ", attributed: true)) - sut.append(make("how do ")) - sut.append(make("you ", attributed: true)) - sut.append(make("do?")) - - // When - var result = [(String, NSRange)]() - sut.enumerateAttributes(for: dummyKey) { (attr: String, range) in - result.append((attr, range)) + // MARK: - Enumeration + + func testEnumerationOfAttributes() { + // Given + let sut = NSMutableAttributedString() + sut.append(make("Hello ", attributed: true)) + sut.append(make("world ", attributed: true)) + sut.append(make("how do ")) + sut.append(make("you ", attributed: true)) + sut.append(make("do?")) + + // When + var result = [(String, NSRange)]() + sut.enumerateAttributes(for: dummyKey) { (attr: String, range) in + result.append((attr, range)) + } + + // Then + XCTAssertEqual(result.count, 2) + XCTAssertEqual(result[0].0, "value") + XCTAssertEqual(result[0].1, NSRange(location: 0, length: 12)) // "Hello world " + XCTAssertEqual(result[1].0, "value") + XCTAssertEqual(result[1].1, NSRange(location: 19, length: 4)) // "you " } - // Then - XCTAssertEqual(result.count, 2) - XCTAssertEqual(result[0].0, "value") - XCTAssertEqual(result[0].1, NSRange(location: 0, length: 12)) // "Hello world " - XCTAssertEqual(result[1].0, "value") - XCTAssertEqual(result[1].1, NSRange(location: 19, length: 4)) // "you " - } } diff --git a/Tests/DownTests/Styler/Helpers/NSMutableAttributedString+AttributesTests.swift b/Tests/DownTests/Styler/Helpers/NSMutableAttributedString+AttributesTests.swift index 048609fd..2c83979c 100644 --- a/Tests/DownTests/Styler/Helpers/NSMutableAttributedString+AttributesTests.swift +++ b/Tests/DownTests/Styler/Helpers/NSMutableAttributedString+AttributesTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import Down -class NSMutableAttributedString_AttributesTests: XCTestCase { +class NSMutableAttributedStringAttributesTests: XCTestCase { private let key1 = NSAttributedString.Key("dummyKey1") private let key2 = NSAttributedString.Key("dummyKey2") @@ -31,7 +31,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { attributeRanges = sut.ranges(of: key2) XCTAssertEqual(attributeRanges, [sut.wholeRange]) - XCTAssertTrue(value(for: key2, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) + XCTAssertTrue(value(for: key2, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) } func testAddingAttributes() { @@ -44,7 +44,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { // Then let attributeRanges = sut.ranges(of: key1) XCTAssertEqual(attributeRanges, [sut.wholeRange]) - XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) + XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) } func testAddingAttribute() { @@ -57,7 +57,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { // Then let attributeRanges = sut.ranges(of: key1) XCTAssertEqual(attributeRanges, [sut.wholeRange]) - XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) + XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) } func testRemovingAttribute() { @@ -90,7 +90,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { let attributeRanges = sut.ranges(of: .foregroundColor) XCTAssertEqual(attributeRanges.count, 1) XCTAssertEqual(attributeRanges.first, NSRange(location: 0, length: 12)) - XCTAssertTrue(value(for: .foregroundColor, inRange: attributeRanges.first!, isEqualTo: DownColor.yellow, sut: sut)) + XCTAssertTrue(value(for: .foregroundColor, in: attributeRanges.first!, isEqualTo: DownColor.yellow, sut: sut)) } func testUpdatingAttribute() { @@ -105,7 +105,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { // Then let attributeRanges = sut.ranges(of: key1) XCTAssertEqual(attributeRanges, [sut.wholeRange]) - XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue.uppercased(), sut: sut)) + XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue.uppercased(), sut: sut)) } func testUpdatingAttributeInRange() { @@ -115,7 +115,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { let rangeOfSecondWord = NSRange(location: 6, length: 5) // When - sut.updateExistingAttributes(for: key1, in: rangeOfFirstWord) { (value: String) in + sut.updateExistingAttributes(for: key1, in: rangeOfFirstWord) { _ in "some new value" } @@ -123,7 +123,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { let attributeRanges = sut.ranges(of: key1) XCTAssertEqual(attributeRanges.count, 2) XCTAssertEqual(attributeRanges, [rangeOfFirstWord, rangeOfSecondWord]) - XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: "some new value", sut: sut)) + XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: "some new value", sut: sut)) } func testUpdatingAttributeThatDidNotExistInRangeDoesNothing() { @@ -132,7 +132,7 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { let rangeOfFirstWord = NSRange(location: 0, length: 6) // When - sut.updateExistingAttributes(for: key1, in: rangeOfFirstWord) { (value: String) in + sut.updateExistingAttributes(for: key1, in: rangeOfFirstWord) { _ in "some new value" } @@ -156,8 +156,8 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { let attributeRanges = sut.ranges(of: key1) XCTAssertEqual(attributeRanges.count, 2) XCTAssertEqual(attributeRanges, [rangeOfFirstWord, rangeOfSecondWord]) - XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) - XCTAssertTrue(value(for: key1, inRange: attributeRanges.last!, isEqualTo: "some new value", sut: sut)) + XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) + XCTAssertTrue(value(for: key1, in: attributeRanges.last!, isEqualTo: "some new value", sut: sut)) } func testAdddingAttributeInMissingRangesDoesNothingIfNoMissingRanges() { @@ -171,19 +171,25 @@ class NSMutableAttributedString_AttributesTests: XCTestCase { let attributeRanges = sut.ranges(of: key1) XCTAssertEqual(attributeRanges.count, 1) XCTAssertEqual(attributeRanges.first!, sut.wholeRange) - XCTAssertTrue(value(for: key1, inRange: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) + XCTAssertTrue(value(for: key1, in: attributeRanges.first!, isEqualTo: dummyValue, sut: sut)) } + } -private extension NSMutableAttributedString_AttributesTests { +private extension NSMutableAttributedStringAttributesTests { func countAttribute(_ name: NSAttributedString.Key, in str: NSAttributedString) -> Int { str.ranges(of: name).count } - func value<A: Equatable>(for name: NSAttributedString.Key, inRange: NSRange, isEqualTo aValue: A, sut: NSMutableAttributedString) -> Bool { + func value<A: Equatable>(for name: NSAttributedString.Key, + in range: NSRange, + isEqualTo aValue: A, + sut: NSMutableAttributedString) -> Bool { + var effectiveRange = NSRange() - let value = sut.attribute(name, at: inRange.location, effectiveRange: &effectiveRange) as? A - return value == aValue && effectiveRange == inRange + let value = sut.attribute(name, at: range.location, effectiveRange: &effectiveRange) as? A + return value == aValue && effectiveRange == range } + } diff --git a/Tests/DownTests/Styler/InlineStyleTests.swift b/Tests/DownTests/Styler/InlineStyleTests.swift index c01d6805..18e4677c 100644 --- a/Tests/DownTests/Styler/InlineStyleTests.swift +++ b/Tests/DownTests/Styler/InlineStyleTests.swift @@ -10,11 +10,10 @@ class InlineStyleTests: StylerTestSuite { - /// # Important - /// - /// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise - /// the comparison may fail. These tests were recorded on the **iPhone 12** simulator. - /// + // # Important + // + // Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise + // the comparison may fail. These tests were recorded on the **iPhone 12** simulator. // MARK: - Simple @@ -93,6 +92,7 @@ class InlineStyleTests: StylerTestSuite { // Then assertStyle(for: markdown, width: .wide) } + } #endif diff --git a/Tests/DownTests/Styler/LinkStyleTests.swift b/Tests/DownTests/Styler/LinkStyleTests.swift index 8daa75c7..46a78418 100644 --- a/Tests/DownTests/Styler/LinkStyleTests.swift +++ b/Tests/DownTests/Styler/LinkStyleTests.swift @@ -19,7 +19,7 @@ class LinkStyleTests: StylerTestSuite { func testThat_Link_IsStyled() { // Given let markdown = """ - Praesent facilisis [pellentesque](www.example.com) ipsum at pulvinar. Sed consectetur augue vel mattis hendrerit. + Praesent facilisis [pellentesque](www.example.com) ipsum at pulvinar. Sed consectetur augue. """ // Then @@ -35,6 +35,7 @@ class LinkStyleTests: StylerTestSuite { // Then assertStyle(for: markdown, width: .narrow) } + } #endif diff --git a/Tests/DownTests/Styler/ListItemStyleTests.swift b/Tests/DownTests/Styler/ListItemStyleTests.swift index 759b8eb4..91a62192 100644 --- a/Tests/DownTests/Styler/ListItemStyleTests.swift +++ b/Tests/DownTests/Styler/ListItemStyleTests.swift @@ -10,11 +10,10 @@ class ListItemStyleTests: StylerTestSuite { - /// # Important - /// - /// Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise - /// the comparison may fail. These tests were recorded on the **iPhone 12** simulator. - /// + // # Important + // + // Snapshot tests must be run on the same simulator used to record the reference snapshots, otherwise + // the comparison may fail. These tests were recorded on the **iPhone 12** simulator. // MARK: - Prefix Alignment @@ -264,6 +263,7 @@ class ListItemStyleTests: StylerTestSuite { // Then assertStyle(for: markdown, width: .narrow) } + } #endif diff --git a/Tests/DownTests/Styler/StylerTestSuite.swift b/Tests/DownTests/Styler/StylerTestSuite.swift index 9594c5e8..6a62ecea 100644 --- a/Tests/DownTests/Styler/StylerTestSuite.swift +++ b/Tests/DownTests/Styler/StylerTestSuite.swift @@ -20,7 +20,7 @@ class StylerTestSuite: XCTestCase { var textContainerInset: UIEdgeInsets! - // MARK: - Lifecycle + // MARK: - Life cycle override func setUp() { super.setUp() @@ -44,33 +44,52 @@ class StylerTestSuite: XCTestCase { testName: String = #function, line: UInt = #line) { - let view = self.view(for: markdown, width: width, configuration: configuration, showLineFragments: showLineFragments) + let maybeView = try? self.view(for: markdown, + width: width, + configuration: configuration, + showLineFragments: showLineFragments) - let failure = verifySnapshot(matching: view, as: .image, record: recording, file: file, testName: testName, line: line) + guard let view = maybeView else { + return XCTFail("Failed to generate markdown view.", file: file, line: line) + } + + let failure = verifySnapshot(matching: view, + as: .image, + record: recording, + file: file, + testName: testName, + line: line) guard let message = failure else { return } + XCTFail(message, file: file, line: line) } - func view(for markdown: String, width: Width, configuration: DownStylerConfiguration?, showLineFragments: Bool = false) -> DownTextView { + func view(for markdown: String, + width: Width, + configuration: DownStylerConfiguration?, + showLineFragments: Bool = false) throws -> DownTextView { + // To make the snapshots the same size of the text content, we set a huge height then resize the view // to the content size. let frame = CGRect(x: 0, y: 0, width: width.rawValue, height: 5000) let textView = showLineFragments ? DownDebugTextView(frame: frame) : DownTextView(frame: frame) textView.textContainerInset = textContainerInset - textView.attributedText = attributedString(for: markdown, configuration: configuration) + textView.attributedText = try attributedString(for: markdown, configuration: configuration) textView.layoutIfNeeded() textView.resizeToContentSize() return textView } - private func attributedString(for markdown: String, configuration: DownStylerConfiguration?) -> NSAttributedString { + private func attributedString(for markdown: String, + configuration: DownStylerConfiguration?) throws -> NSAttributedString { + let down = Down(markdownString: markdown) let styler = DownStyler(configuration: configuration ?? .testConfiguration) - return try! down.toAttributedString(styler: styler) + return try down.toAttributedString(styler: styler) } -} +} extension StylerTestSuite { @@ -78,14 +97,15 @@ extension StylerTestSuite { case narrow = 300 case wide = 600 } -} +} private extension DownTextView { func resizeToContentSize() { frame = .init(origin: frame.origin, size: .init(width: contentSize.width, height: contentSize.height)) } + } private extension DownStylerConfiguration { @@ -162,6 +182,7 @@ private extension DownStylerConfiguration { return configuration } + } #endif diff --git a/Tests/DownTests/Styler/ThematicBreakSyleTests.swift b/Tests/DownTests/Styler/ThematicBreakSyleTests.swift index a9550ba0..3779d875 100644 --- a/Tests/DownTests/Styler/ThematicBreakSyleTests.swift +++ b/Tests/DownTests/Styler/ThematicBreakSyleTests.swift @@ -66,6 +66,7 @@ class ThematicBreakSyleTests: StylerTestSuite { // Then assertStyle(for: markdown, width: .wide, configuration: configuration) } + } #endif diff --git a/Tests/DownTests/Styler/__Snapshots__/LinkStyleTests/testThat_Link_IsStyled.1.png b/Tests/DownTests/Styler/__Snapshots__/LinkStyleTests/testThat_Link_IsStyled.1.png index f032b6f9..f3bb3577 100644 Binary files a/Tests/DownTests/Styler/__Snapshots__/LinkStyleTests/testThat_Link_IsStyled.1.png and b/Tests/DownTests/Styler/__Snapshots__/LinkStyleTests/testThat_Link_IsStyled.1.png differ diff --git a/Tests/DownTests/__Snapshots__/BindingTests/testCommonMarkBindngsWork.1.txt b/Tests/DownTests/__Snapshots__/BindingTests/testCommonMarkBindngsWork.1.txt new file mode 100644 index 00000000..a8cad904 --- /dev/null +++ b/Tests/DownTests/__Snapshots__/BindingTests/testCommonMarkBindngsWork.1.txt @@ -0,0 +1 @@ +## [Down](https://github.com/johnxnnguyen/Down) diff --git a/Tests/DownTests/__Snapshots__/BindingTests/testGroffBindingsWork.1.txt b/Tests/DownTests/__Snapshots__/BindingTests/testGroffBindingsWork.1.txt new file mode 100644 index 00000000..cd902484 --- /dev/null +++ b/Tests/DownTests/__Snapshots__/BindingTests/testGroffBindingsWork.1.txt @@ -0,0 +1,2 @@ +.SS +Down (https://github.com/johnxnnguyen/Down) diff --git a/Tests/DownTests/__Snapshots__/BindingTests/testHTMLBindingsWork.1.txt b/Tests/DownTests/__Snapshots__/BindingTests/testHTMLBindingsWork.1.txt new file mode 100644 index 00000000..b126e02d --- /dev/null +++ b/Tests/DownTests/__Snapshots__/BindingTests/testHTMLBindingsWork.1.txt @@ -0,0 +1 @@ +<h2><a href="https://github.com/johnxnnguyen/Down">Down</a></h2> diff --git a/Tests/DownTests/__Snapshots__/BindingTests/testLaTeXBindngsWork.1.txt b/Tests/DownTests/__Snapshots__/BindingTests/testLaTeXBindngsWork.1.txt new file mode 100644 index 00000000..a0a8ff1c --- /dev/null +++ b/Tests/DownTests/__Snapshots__/BindingTests/testLaTeXBindngsWork.1.txt @@ -0,0 +1 @@ +\subsection{\href{https://github.com/johnxnnguyen/Down}{Down}} diff --git a/Tests/DownTests/__Snapshots__/BindingTests/testXMLBindingsWork.1.txt b/Tests/DownTests/__Snapshots__/BindingTests/testXMLBindingsWork.1.txt new file mode 100644 index 00000000..3be9f3a9 --- /dev/null +++ b/Tests/DownTests/__Snapshots__/BindingTests/testXMLBindingsWork.1.txt @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE document SYSTEM "CommonMark.dtd"> +<document xmlns="http://commonmark.org/xml/1.0"> + <heading level="2"> + <link destination="https://github.com/johnxnnguyen/Down" title=""> + <text xml:space="preserve">Down</text> + </link> + </heading> +</document>