diff --git a/Down.xcodeproj/project.pbxproj b/Down.xcodeproj/project.pbxproj index e8f479b2..132075e1 100644 --- a/Down.xcodeproj/project.pbxproj +++ b/Down.xcodeproj/project.pbxproj @@ -66,6 +66,33 @@ 8AFAEB091E6E331700E09B68 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D438696B1D00D27700E95A1F /* StringTests.swift */; }; 907C64651EC133780095FEE1 /* TestDownView.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 907C64621EC120530095FEE1 /* TestDownView.bundle */; }; 90A40A9C1EC03292004F2E91 /* Down.framework in Copy Bundled Frameworks */ = {isa = PBXBuildFile; fileRef = 8A569F401E6B3E50008BE2AC /* Down.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + EE469B11226CF3B500C0655D /* BaseNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE469B10226CF3B500C0655D /* BaseNode.swift */; }; + EE5F2BA42262564A00B7C0F3 /* Styler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5F2BA32262564A00B7C0F3 /* Styler.swift */; }; + EE64FEF0225BEB3900A35B34 /* VisitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE64FEEF225BEB3900A35B34 /* VisitorTests.swift */; }; + EEEBEE47225D298D00AE438D /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE46225D298D00AE438D /* Document.swift */; }; + EEEBEE49225D29C200AE438D /* BlockQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE48225D29C200AE438D /* BlockQuote.swift */; }; + EEEBEE4B225D29D800AE438D /* List.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE4A225D29D800AE438D /* List.swift */; }; + EEEBEE4D225D2A0200AE438D /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE4C225D2A0200AE438D /* Item.swift */; }; + EEEBEE4F225D2A1400AE438D /* CodeBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE4E225D2A1400AE438D /* CodeBlock.swift */; }; + EEEBEE51225D2A2900AE438D /* HtmlBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE50225D2A2900AE438D /* HtmlBlock.swift */; }; + EEEBEE53225D2A3C00AE438D /* CustomBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE52225D2A3C00AE438D /* CustomBlock.swift */; }; + EEEBEE55225D2A4E00AE438D /* Paragraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE54225D2A4E00AE438D /* Paragraph.swift */; }; + EEEBEE57225D2A6200AE438D /* Heading.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE56225D2A6200AE438D /* Heading.swift */; }; + EEEBEE59225D2A7000AE438D /* ThematicBreak.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE58225D2A7000AE438D /* ThematicBreak.swift */; }; + EEEBEE5B225D2A7E00AE438D /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE5A225D2A7E00AE438D /* Text.swift */; }; + EEEBEE5D225D2A8F00AE438D /* SoftBreak.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE5C225D2A8F00AE438D /* SoftBreak.swift */; }; + EEEBEE5F225D2AA700AE438D /* LineBreak.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE5E225D2AA700AE438D /* LineBreak.swift */; }; + EEEBEE61225D2AC000AE438D /* Code.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE60225D2AC000AE438D /* Code.swift */; }; + EEEBEE63225D2AD400AE438D /* HtmlInline.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE62225D2AD400AE438D /* HtmlInline.swift */; }; + EEEBEE65225D2AE800AE438D /* CustomInline.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE64225D2AE800AE438D /* CustomInline.swift */; }; + EEEBEE67225D2AF900AE438D /* Emphasis.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE66225D2AF900AE438D /* Emphasis.swift */; }; + EEEBEE69225D2B1200AE438D /* Strong.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE68225D2B1200AE438D /* Strong.swift */; }; + EEEBEE6B225D2B2200AE438D /* Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE6A225D2B2200AE438D /* Link.swift */; }; + EEEBEE6D225D2B3200AE438D /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE6C225D2B3200AE438D /* Image.swift */; }; + EEEBEE70225D2B9D00AE438D /* DebugVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE6F225D2B9D00AE438D /* DebugVisitor.swift */; }; + EEEBEE72225D2F9200AE438D /* AttributedStringVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEBEE71225D2F9200AE438D /* AttributedStringVisitor.swift */; }; + EEF1376F2259E53400D7DDE0 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF1376E2259E53400D7DDE0 /* Node.swift */; }; + EEF137712259E7E700D7DDE0 /* Vistor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF137702259E7E700D7DDE0 /* Vistor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXBuildRule section */ @@ -193,6 +220,33 @@ D4CF88971CFFAC2C00F07FD1 /* DownAttributedStringRenderable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownAttributedStringRenderable.swift; sourceTree = ""; }; D4DC91131CFDED4B0091CE09 /* DownCommonMarkRenderable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownCommonMarkRenderable.swift; sourceTree = ""; }; D4F948DB1D00A4A800C9C0F6 /* NSAttributedStringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSAttributedStringTests.swift; sourceTree = ""; }; + EE469B10226CF3B500C0655D /* BaseNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNode.swift; sourceTree = ""; }; + EE5F2BA32262564A00B7C0F3 /* Styler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styler.swift; sourceTree = ""; }; + EE64FEEF225BEB3900A35B34 /* VisitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitorTests.swift; sourceTree = ""; }; + EEEBEE46225D298D00AE438D /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; + EEEBEE48225D29C200AE438D /* BlockQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockQuote.swift; sourceTree = ""; }; + EEEBEE4A225D29D800AE438D /* List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = List.swift; sourceTree = ""; }; + EEEBEE4C225D2A0200AE438D /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + EEEBEE4E225D2A1400AE438D /* CodeBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeBlock.swift; sourceTree = ""; }; + EEEBEE50225D2A2900AE438D /* HtmlBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlBlock.swift; sourceTree = ""; }; + EEEBEE52225D2A3C00AE438D /* CustomBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBlock.swift; sourceTree = ""; }; + EEEBEE54225D2A4E00AE438D /* Paragraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paragraph.swift; sourceTree = ""; }; + EEEBEE56225D2A6200AE438D /* Heading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Heading.swift; sourceTree = ""; }; + EEEBEE58225D2A7000AE438D /* ThematicBreak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThematicBreak.swift; sourceTree = ""; }; + EEEBEE5A225D2A7E00AE438D /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; + EEEBEE5C225D2A8F00AE438D /* SoftBreak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftBreak.swift; sourceTree = ""; }; + EEEBEE5E225D2AA700AE438D /* LineBreak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineBreak.swift; sourceTree = ""; }; + EEEBEE60225D2AC000AE438D /* Code.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Code.swift; sourceTree = ""; }; + EEEBEE62225D2AD400AE438D /* HtmlInline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlInline.swift; sourceTree = ""; }; + EEEBEE64225D2AE800AE438D /* CustomInline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInline.swift; sourceTree = ""; }; + EEEBEE66225D2AF900AE438D /* Emphasis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emphasis.swift; sourceTree = ""; }; + EEEBEE68225D2B1200AE438D /* Strong.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strong.swift; sourceTree = ""; }; + EEEBEE6A225D2B2200AE438D /* Link.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Link.swift; sourceTree = ""; }; + EEEBEE6C225D2B3200AE438D /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; + EEEBEE6F225D2B9D00AE438D /* DebugVisitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugVisitor.swift; sourceTree = ""; }; + EEEBEE71225D2F9200AE438D /* AttributedStringVisitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringVisitor.swift; sourceTree = ""; }; + EEF1376E2259E53400D7DDE0 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; + EEF137702259E7E700D7DDE0 /* Vistor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vistor.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -263,6 +317,7 @@ children = ( D4CF88961CFF94B300F07FD1 /* Down.h */, D4201EF01CFA59F2008EEC6E /* Down.swift */, + EEF1376D2259E4BA00D7DDE0 /* AST */, D44875E71CFA6CF30037A624 /* Enums & Options */, D43AE5C81CFFAE39006E1522 /* Extensions */, D44875E21CFA6B120037A624 /* Renderers */, @@ -280,6 +335,7 @@ 907C64611EC120530095FEE1 /* Fixtures */, D4F948DB1D00A4A800C9C0F6 /* NSAttributedStringTests.swift */, D438696B1D00D27700E95A1F /* StringTests.swift */, + EE64FEEF225BEB3900A35B34 /* VisitorTests.swift */, ); path = Tests; sourceTree = ""; @@ -370,6 +426,63 @@ path = "Enums & Options"; sourceTree = ""; }; + EE5F2BA22262562F00B7C0F3 /* Stylers */ = { + isa = PBXGroup; + children = ( + EE5F2BA32262564A00B7C0F3 /* Styler.swift */, + ); + path = Stylers; + sourceTree = ""; + }; + EEEBEE45225D297700AE438D /* Nodes */ = { + isa = PBXGroup; + children = ( + EEF1376E2259E53400D7DDE0 /* Node.swift */, + EE469B10226CF3B500C0655D /* BaseNode.swift */, + EEEBEE46225D298D00AE438D /* Document.swift */, + EEEBEE48225D29C200AE438D /* BlockQuote.swift */, + EEEBEE4A225D29D800AE438D /* List.swift */, + EEEBEE4C225D2A0200AE438D /* Item.swift */, + EEEBEE4E225D2A1400AE438D /* CodeBlock.swift */, + EEEBEE50225D2A2900AE438D /* HtmlBlock.swift */, + EEEBEE52225D2A3C00AE438D /* CustomBlock.swift */, + EEEBEE54225D2A4E00AE438D /* Paragraph.swift */, + EEEBEE56225D2A6200AE438D /* Heading.swift */, + EEEBEE58225D2A7000AE438D /* ThematicBreak.swift */, + EEEBEE5A225D2A7E00AE438D /* Text.swift */, + EEEBEE5C225D2A8F00AE438D /* SoftBreak.swift */, + EEEBEE5E225D2AA700AE438D /* LineBreak.swift */, + EEEBEE60225D2AC000AE438D /* Code.swift */, + EEEBEE62225D2AD400AE438D /* HtmlInline.swift */, + EEEBEE64225D2AE800AE438D /* CustomInline.swift */, + EEEBEE66225D2AF900AE438D /* Emphasis.swift */, + EEEBEE68225D2B1200AE438D /* Strong.swift */, + EEEBEE6A225D2B2200AE438D /* Link.swift */, + EEEBEE6C225D2B3200AE438D /* Image.swift */, + ); + path = Nodes; + sourceTree = ""; + }; + EEEBEE6E225D2B8200AE438D /* Visitors */ = { + isa = PBXGroup; + children = ( + EEF137702259E7E700D7DDE0 /* Vistor.swift */, + EEEBEE6F225D2B9D00AE438D /* DebugVisitor.swift */, + EEEBEE71225D2F9200AE438D /* AttributedStringVisitor.swift */, + ); + path = Visitors; + sourceTree = ""; + }; + EEF1376D2259E4BA00D7DDE0 /* AST */ = { + isa = PBXGroup; + children = ( + EEEBEE45225D297700AE438D /* Nodes */, + EEEBEE6E225D2B8200AE438D /* Visitors */, + EE5F2BA22262562F00B7C0F3 /* Stylers */, + ); + path = AST; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -532,12 +645,20 @@ 8A569F761E6B3EE3008BE2AC /* NSAttributedString+HTML.swift in Sources */, 8A569F5A1E6B3ED9008BE2AC /* html.c in Sources */, 8A569F5E1E6B3ED9008BE2AC /* iterator.c in Sources */, + EEEBEE70225D2B9D00AE438D /* DebugVisitor.swift in Sources */, + EEEBEE59225D2A7000AE438D /* ThematicBreak.swift in Sources */, 8A569F481E6B3ED2008BE2AC /* DownView.swift in Sources */, 8A569F591E6B3ED9008BE2AC /* houdini_html_u.c in Sources */, + EEEBEE63225D2AD400AE438D /* HtmlInline.swift in Sources */, 8A569F621E6B3ED9008BE2AC /* node.c in Sources */, + EEEBEE6D225D2B3200AE438D /* Image.swift in Sources */, + EEEBEE4D225D2A0200AE438D /* Item.swift in Sources */, 8A569F6F1E6B3EDE008BE2AC /* DownAttributedStringRenderable.swift in Sources */, + EEEBEE55225D2A4E00AE438D /* Paragraph.swift in Sources */, 8A569F721E6B3EDE008BE2AC /* DownHTMLRenderable.swift in Sources */, 8A569F581E6B3ED9008BE2AC /* houdini_html_e.c in Sources */, + EEEBEE5D225D2A8F00AE438D /* SoftBreak.swift in Sources */, + EEEBEE6B225D2B2200AE438D /* Link.swift in Sources */, 8A569F531E6B3ED9008BE2AC /* commonmark.c in Sources */, 8A569F4A1E6B3ED9008BE2AC /* buffer.c in Sources */, 8A569F771E6B3EE3008BE2AC /* String+ToHTML.swift in Sources */, @@ -545,22 +666,40 @@ 8A569F741E6B3EDE008BE2AC /* DownXMLRenderable.swift in Sources */, 8A569F6B1E6B3ED9008BE2AC /* utf8.c in Sources */, 8A569F791E6B3EE7008BE2AC /* DownOptions.swift in Sources */, + EEEBEE72225D2F9200AE438D /* AttributedStringVisitor.swift in Sources */, + EEEBEE4B225D29D800AE438D /* List.swift in Sources */, + EEEBEE57225D2A6200AE438D /* Heading.swift in Sources */, 8A569F601E6B3ED9008BE2AC /* latex.c in Sources */, 8A569F711E6B3EDE008BE2AC /* DownGroffRenderable.swift in Sources */, + EEEBEE51225D2A2900AE438D /* HtmlBlock.swift in Sources */, 8A569F691E6B3ED9008BE2AC /* scanners.c in Sources */, 8A569F701E6B3EDE008BE2AC /* DownCommonMarkRenderable.swift in Sources */, 8A569F7A1E6B3EEA008BE2AC /* Down.swift in Sources */, + EE5F2BA42262564A00B7C0F3 /* Styler.swift in Sources */, 8A569F731E6B3EDE008BE2AC /* DownLaTeXRenderable.swift in Sources */, 8A569F781E6B3EE7008BE2AC /* DownErrors.swift in Sources */, 8A569F5C1E6B3ED9008BE2AC /* inlines.c in Sources */, 8A569F571E6B3ED9008BE2AC /* houdini_href_e.c in Sources */, 8A569F4D1E6B3ED9008BE2AC /* cmark.c in Sources */, + EEEBEE65225D2AE800AE438D /* CustomInline.swift in Sources */, + EEF1376F2259E53400D7DDE0 /* Node.swift in Sources */, 8A569F6D1E6B3ED9008BE2AC /* xml.c in Sources */, 8A569F6E1E6B3EDE008BE2AC /* DownASTRenderable.swift in Sources */, + EEEBEE5B225D2A7E00AE438D /* Text.swift in Sources */, + EEEBEE4F225D2A1400AE438D /* CodeBlock.swift in Sources */, + EEEBEE5F225D2AA700AE438D /* LineBreak.swift in Sources */, + EEEBEE61225D2AC000AE438D /* Code.swift in Sources */, 8A569F491E6B3ED9008BE2AC /* blocks.c in Sources */, + EEF137712259E7E700D7DDE0 /* Vistor.swift in Sources */, + EEEBEE53225D2A3C00AE438D /* CustomBlock.swift in Sources */, + EEEBEE69225D2B1200AE438D /* Strong.swift in Sources */, + EEEBEE47225D298D00AE438D /* Document.swift in Sources */, 8A569F4F1E6B3ED9008BE2AC /* cmark_ctype.c in Sources */, + EE469B11226CF3B500C0655D /* BaseNode.swift in Sources */, 8A569F671E6B3ED9008BE2AC /* render.c in Sources */, + EEEBEE49225D29C200AE438D /* BlockQuote.swift in Sources */, 8A569F651E6B3ED9008BE2AC /* references.c in Sources */, + EEEBEE67225D2AF900AE438D /* Emphasis.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -569,6 +708,7 @@ buildActionMask = 2147483647; files = ( 8AFAEB091E6E331700E09B68 /* StringTests.swift in Sources */, + EE64FEF0225BEB3900A35B34 /* VisitorTests.swift in Sources */, 8AFAEB071E6E331700E09B68 /* DownViewTests.swift in Sources */, 8AFAEB061E6E331700E09B68 /* BindingTests.swift in Sources */, 8AFAEB081E6E331700E09B68 /* NSAttributedStringTests.swift in Sources */, diff --git a/Source/AST/Nodes/BaseNode.swift b/Source/AST/Nodes/BaseNode.swift new file mode 100644 index 00000000..7fb7e516 --- /dev/null +++ b/Source/AST/Nodes/BaseNode.swift @@ -0,0 +1,37 @@ +// +// BaseNode.swift +// Down +// +// Created by John Nguyen on 21.04.19. +// +// + +import Foundation +import libcmark + +public class BaseNode: Node { + + public let cmarkNode: CMarkNode + + public private(set) lazy var children: [Node] = { + var result: [Node] = [] + var child = cmark_node_first_child(cmarkNode) + + while let raw = child { + + guard let node = raw.wrap() else { + assertionFailure("Couldn't wrap node of type: \(raw.type)") + continue + } + + result.append(node) + child = cmark_node_next(child) + } + + return result + }() + + init(cmarkNode: CMarkNode) { + self.cmarkNode = cmarkNode + } +} diff --git a/Source/AST/Nodes/BlockQuote.swift b/Source/AST/Nodes/BlockQuote.swift new file mode 100644 index 00000000..318c1d6b --- /dev/null +++ b/Source/AST/Nodes/BlockQuote.swift @@ -0,0 +1,21 @@ +// +// BlockQuote.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class BlockQuote: BaseNode {} + + +// MARK: - Debug + +extension BlockQuote: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Block Quote" + } +} diff --git a/Source/AST/Nodes/Code.swift b/Source/AST/Nodes/Code.swift new file mode 100644 index 00000000..73904ffb --- /dev/null +++ b/Source/AST/Nodes/Code.swift @@ -0,0 +1,25 @@ +// +// Code.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Code: BaseNode { + + /// 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/Source/AST/Nodes/CodeBlock.swift b/Source/AST/Nodes/CodeBlock.swift new file mode 100644 index 00000000..6cfdb91a --- /dev/null +++ b/Source/AST/Nodes/CodeBlock.swift @@ -0,0 +1,38 @@ +// +// CodeBlock.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class CodeBlock: BaseNode { + + /// 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. + /// + /// For example: + /// ``` + /// ''' + /// + /// ''' + /// ``` + /// + public private(set) lazy var fenceInfo: String? = cmarkNode.fenceInfo +} + + +// MARK: - Debug + +extension CodeBlock: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Code Block - \(literal ?? "nil"), fenceInfo: \(fenceInfo ?? "nil")" + } +} diff --git a/Source/AST/Nodes/CustomBlock.swift b/Source/AST/Nodes/CustomBlock.swift new file mode 100644 index 00000000..13001242 --- /dev/null +++ b/Source/AST/Nodes/CustomBlock.swift @@ -0,0 +1,25 @@ +// +// CustomBlock.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class CustomBlock: BaseNode { + + /// 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/Source/AST/Nodes/CustomInline.swift b/Source/AST/Nodes/CustomInline.swift new file mode 100644 index 00000000..852a0f04 --- /dev/null +++ b/Source/AST/Nodes/CustomInline.swift @@ -0,0 +1,25 @@ +// +// CustomInline.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class CustomInline: BaseNode { + + /// 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/Source/AST/Nodes/Document.swift b/Source/AST/Nodes/Document.swift new file mode 100644 index 00000000..c5a62a34 --- /dev/null +++ b/Source/AST/Nodes/Document.swift @@ -0,0 +1,32 @@ +// +// Document.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Document: BaseNode { + + deinit { + // Frees the node and all its children. + cmark_node_free(cmarkNode) + } + + /// Accepts the given visitor and return its result. + 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/Source/AST/Nodes/Emphasis.swift b/Source/AST/Nodes/Emphasis.swift new file mode 100644 index 00000000..c6550f98 --- /dev/null +++ b/Source/AST/Nodes/Emphasis.swift @@ -0,0 +1,21 @@ +// +// Emphasis.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Emphasis: BaseNode {} + + +// MARK: - Debug + +extension Emphasis: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Emphasis" + } +} diff --git a/Source/AST/Nodes/Heading.swift b/Source/AST/Nodes/Heading.swift new file mode 100644 index 00000000..82427651 --- /dev/null +++ b/Source/AST/Nodes/Heading.swift @@ -0,0 +1,25 @@ +// +// Heading.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Heading: BaseNode { + + /// 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/Source/AST/Nodes/HtmlBlock.swift b/Source/AST/Nodes/HtmlBlock.swift new file mode 100644 index 00000000..dfa6e3ea --- /dev/null +++ b/Source/AST/Nodes/HtmlBlock.swift @@ -0,0 +1,25 @@ +// +// HtmlBlock.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class HtmlBlock: BaseNode { + + /// The html content, if present. + public private(set) lazy var literal: String? = cmarkNode.literal +} + + +// MARK: - Debug + +extension HtmlBlock: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Html Block - \(literal ?? "nil")" + } +} diff --git a/Source/AST/Nodes/HtmlInline.swift b/Source/AST/Nodes/HtmlInline.swift new file mode 100644 index 00000000..6b63d033 --- /dev/null +++ b/Source/AST/Nodes/HtmlInline.swift @@ -0,0 +1,25 @@ +// +// HtmlInline.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class HtmlInline: BaseNode { + + /// 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/Source/AST/Nodes/Image.swift b/Source/AST/Nodes/Image.swift new file mode 100644 index 00000000..97d15aef --- /dev/null +++ b/Source/AST/Nodes/Image.swift @@ -0,0 +1,46 @@ +// +// Image.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Image: BaseNode { + + /// The title of the image, if present. + /// + /// In the example below, the first line is a reference link, with the reference at the + /// bottom. `` is literal text belonging to children nodes. The title occurs + /// after the url and is optional. + /// + /// ``` + /// ![][] + /// ... + /// []: "" + /// ``` + /// + public private(set) lazy var title: String? = cmarkNode.title + + /// The url of the image, if present. + /// + /// For example: + /// + /// ``` + /// ![<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/Source/AST/Nodes/Item.swift b/Source/AST/Nodes/Item.swift new file mode 100644 index 00000000..27df342b --- /dev/null +++ b/Source/AST/Nodes/Item.swift @@ -0,0 +1,21 @@ +// +// Item.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Item: BaseNode {} + + +// MARK: - Debug + +extension Item: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Item" + } +} diff --git a/Source/AST/Nodes/LineBreak.swift b/Source/AST/Nodes/LineBreak.swift new file mode 100644 index 00000000..8782d25a --- /dev/null +++ b/Source/AST/Nodes/LineBreak.swift @@ -0,0 +1,21 @@ +// +// LineBreak.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class LineBreak: BaseNode {} + + +// MARK: - Debug + +extension LineBreak: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Line Break" + } +} diff --git a/Source/AST/Nodes/Link.swift b/Source/AST/Nodes/Link.swift new file mode 100644 index 00000000..b53d4f6e --- /dev/null +++ b/Source/AST/Nodes/Link.swift @@ -0,0 +1,46 @@ +// +// Link.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Link: BaseNode { + + /// The title of the link, if present. + /// + /// In the example below, the first line is a reference link, with the reference at the + /// bottom. `<text>` is literal text belonging to children nodes. The title occurs + /// after the url and is optional. + /// + /// ``` + /// [<text>][<id>] + /// ... + /// [<id>]: <url> "<title>" + /// ``` + /// + public private(set) lazy var title: String? = cmarkNode.title + + /// The url of the link, if present. + /// + /// For example: + /// + /// ``` + /// [<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/Source/AST/Nodes/List.swift b/Source/AST/Nodes/List.swift new file mode 100644 index 00000000..782dbd6c --- /dev/null +++ b/Source/AST/Nodes/List.swift @@ -0,0 +1,57 @@ +// +// List.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class List: BaseNode { + + public enum ListType: CustomDebugStringConvertible { + case bullet + case ordered(start: Int) + + public var debugDescription: String { + switch self { + case .bullet: return "Bullet" + case .ordered(let start): return "Ordered (start: \(start)" + } + } + + init?(cmarkNode: CMarkNode) { + switch cmarkNode.listType { + case CMARK_BULLET_LIST: self = .bullet + case CMARK_ORDERED_LIST: self = .ordered(start: cmarkNode.listStart) + default: return nil + } + } + } + + /////////////////////////////////////////////////////////////////////////// + + /// 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 +} + + +// MARK: - Debug + +extension List: CustomDebugStringConvertible { + + public var debugDescription: String { + return "List - type: \(listType)" + } +} diff --git a/Source/AST/Nodes/Node.swift b/Source/AST/Nodes/Node.swift new file mode 100644 index 00000000..4924ab6e --- /dev/null +++ b/Source/AST/Nodes/Node.swift @@ -0,0 +1,104 @@ +// +// Node.swift +// Down +// +// Created by John Nguyen on 07.04.19. +// + +import Foundation +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 + } +} + + +// MARK: - Helper extensions + +public typealias CMarkNode = UnsafeMutablePointer<cmark_node> + +public extension UnsafeMutablePointer where Pointee == cmark_node { + + /// Wraps the cmark node referred to by this pointer. + func wrap() -> Node? { + switch type { + case CMARK_NODE_DOCUMENT: return Document(cmarkNode: self) + case CMARK_NODE_BLOCK_QUOTE: return BlockQuote(cmarkNode: self) + case CMARK_NODE_LIST: return List(cmarkNode: self) + case CMARK_NODE_ITEM: return Item(cmarkNode: self) + case CMARK_NODE_CODE_BLOCK: return CodeBlock(cmarkNode: self) + case CMARK_NODE_HTML_BLOCK: return HtmlBlock(cmarkNode: self) + case CMARK_NODE_CUSTOM_BLOCK: return CustomBlock(cmarkNode: self) + case CMARK_NODE_PARAGRAPH: return Paragraph(cmarkNode: self) + case CMARK_NODE_HEADING: return Heading(cmarkNode: self) + case CMARK_NODE_THEMATIC_BREAK: return ThematicBreak(cmarkNode: self) + case CMARK_NODE_TEXT: return Text(cmarkNode: self) + case CMARK_NODE_SOFTBREAK: return SoftBreak(cmarkNode: self) + case CMARK_NODE_LINEBREAK: return LineBreak(cmarkNode: self) + case CMARK_NODE_CODE: return Code(cmarkNode: self) + case CMARK_NODE_HTML_INLINE: return HtmlInline(cmarkNode: self) + case CMARK_NODE_CUSTOM_INLINE: return CustomInline(cmarkNode: self) + case CMARK_NODE_EMPH: return Emphasis(cmarkNode: self) + case CMARK_NODE_STRONG: return Strong(cmarkNode: self) + case CMARK_NODE_LINK: return Link(cmarkNode: self) + case CMARK_NODE_IMAGE: return Image(cmarkNode: self) + default: return nil + } + } + + 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/Source/AST/Nodes/Paragraph.swift b/Source/AST/Nodes/Paragraph.swift new file mode 100644 index 00000000..8a7b34cd --- /dev/null +++ b/Source/AST/Nodes/Paragraph.swift @@ -0,0 +1,21 @@ +// +// Paragraph.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Paragraph: BaseNode {} + + +// MARK: - Debug + +extension Paragraph: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Paragraph" + } +} diff --git a/Source/AST/Nodes/SoftBreak.swift b/Source/AST/Nodes/SoftBreak.swift new file mode 100644 index 00000000..519dfc44 --- /dev/null +++ b/Source/AST/Nodes/SoftBreak.swift @@ -0,0 +1,21 @@ +// +// SoftBreak.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class SoftBreak: BaseNode {} + + +// MARK: - Debug + +extension SoftBreak: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Soft Break" + } +} diff --git a/Source/AST/Nodes/Strong.swift b/Source/AST/Nodes/Strong.swift new file mode 100644 index 00000000..039bfd36 --- /dev/null +++ b/Source/AST/Nodes/Strong.swift @@ -0,0 +1,21 @@ +// +// Strong.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Strong: BaseNode {} + + +// MARK: - Debug + +extension Strong: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Strong" + } +} diff --git a/Source/AST/Nodes/Text.swift b/Source/AST/Nodes/Text.swift new file mode 100644 index 00000000..ef2928e9 --- /dev/null +++ b/Source/AST/Nodes/Text.swift @@ -0,0 +1,25 @@ +// +// Text.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class Text: BaseNode { + + /// 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/Source/AST/Nodes/ThematicBreak.swift b/Source/AST/Nodes/ThematicBreak.swift new file mode 100644 index 00000000..d4831c45 --- /dev/null +++ b/Source/AST/Nodes/ThematicBreak.swift @@ -0,0 +1,21 @@ +// +// ThematicBreak.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation +import libcmark + +public class ThematicBreak: BaseNode {} + + +// MARK: - Debug + +extension ThematicBreak: CustomDebugStringConvertible { + + public var debugDescription: String { + return "Thematic Break" + } +} diff --git a/Source/AST/Stylers/Styler.swift b/Source/AST/Stylers/Styler.swift new file mode 100644 index 00000000..2db207fc --- /dev/null +++ b/Source/AST/Stylers/Styler.swift @@ -0,0 +1,41 @@ +// +// Styler.swift +// Down +// +// Created by John Nguyen on 13.04.19. +// Copyright © 2019 Glazed Donut, LLC. All rights reserved. +// + +import Foundation + +/// A styler is an object that manipulates the appearance of attributed strings generated +/// for each particular markdown node. The attributed string passed into each method is +/// mutable, so attributes (and even text) can be added, set, and removed. +/// +/// 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 { + func style(document str: NSMutableAttributedString) + func style(blockQuote str: NSMutableAttributedString) + func style(list str: NSMutableAttributedString) + func style(item str: NSMutableAttributedString) + func style(codeBlock str: NSMutableAttributedString, fenceInfo: String?) + func style(htmlBlock str: NSMutableAttributedString) + func style(customBlock str: NSMutableAttributedString) + func style(paragraph str: NSMutableAttributedString) + func style(heading str: NSMutableAttributedString, level: Int) + func style(thematicBreak str: NSMutableAttributedString) + func style(text str: NSMutableAttributedString) + func style(softBreak str: NSMutableAttributedString) + func style(lineBreak str: NSMutableAttributedString) + func style(code str: NSMutableAttributedString) + func style(htmlInline str: NSMutableAttributedString) + func style(customInline str: NSMutableAttributedString) + func style(emphasis str: NSMutableAttributedString) + func style(strong str: NSMutableAttributedString) + func style(link str: NSMutableAttributedString, title: String?, url: String?) + func style(image str: NSMutableAttributedString, title: String?, url: String?) + + var listPrefixAttributes: [NSAttributedStringKey: Any] { get } +} diff --git a/Source/AST/Visitors/AttributedStringVisitor.swift b/Source/AST/Visitors/AttributedStringVisitor.swift new file mode 100644 index 00000000..6972453c --- /dev/null +++ b/Source/AST/Visitors/AttributedStringVisitor.swift @@ -0,0 +1,199 @@ +// +// AttributedStringVisitor.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation + +/// This class is used to generated an `NSMutableAttributedString` from the abstract syntax +/// 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 { + + private let styler: Styler + private let options: DownOptions + + /// Creates a new instance with the given styler and options. + /// + /// - 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 + } + + public func visit(blockQuote node: BlockQuote) -> NSMutableAttributedString { + let s = visitChildren(of: node).joined + if node.hasSuccessor { s.append(.blankLine) } + styler.style(blockQuote: s) + return s + } + + public func visit(list node: List) -> NSMutableAttributedString { + let items = visitChildren(of: node) + + // Prepend prefixes to each item. + items.enumerated().forEach { index, item in + let prefix: String + switch node.listType { + case .bullet: prefix = "•\t" + case .ordered(let start): prefix = "\(start + index).\t" + } + + let attrPrefix = NSAttributedString(string: prefix, attributes: styler.listPrefixAttributes) + item.insert(attrPrefix, at: 0) + } + + let s = items.joined + if node.hasSuccessor { s.append(.blankLine) } + styler.style(list: s) + return s + } + + public func visit(item node: Item) -> NSMutableAttributedString { + let s = visitChildren(of: node).joined + if node.hasSuccessor { s.append(.blankLine) } + styler.style(item: s) + return s + } + + public func visit(codeBlock node: CodeBlock) -> NSMutableAttributedString { + guard let s = node.literal?.attributed else { return .empty } + styler.style(codeBlock: s, fenceInfo: node.fenceInfo) + return s + } + + public func visit(htmlBlock node: HtmlBlock) -> NSMutableAttributedString { + guard let s = node.literal?.attributed else { return .empty } + styler.style(htmlBlock: s) + return s + } + + public func visit(customBlock node: CustomBlock) -> NSMutableAttributedString { + guard let s = node.literal?.attributed else { return .empty } + styler.style(customBlock: s) + return s + } + + public func visit(paragraph node: Paragraph) -> NSMutableAttributedString { + let s = visitChildren(of: node).joined + if node.hasSuccessor { s.append(.blankLine) } + styler.style(paragraph: s) + return s + } + + public func visit(heading node: Heading) -> NSMutableAttributedString { + let s = visitChildren(of: node).joined + if node.hasSuccessor { s.append(.blankLine) } + styler.style(heading: s, level: node.headingLevel) + return s + } + + public func visit(thematicBreak node: ThematicBreak) -> NSMutableAttributedString { + let s = "\n".attributed + styler.style(thematicBreak: s) + return s + } + + public func visit(text node: Text) -> NSMutableAttributedString { + guard let s = node.literal?.attributed else { return .empty } + styler.style(text: s) + return s + } + + public func visit(softBreak node: SoftBreak) -> NSMutableAttributedString { + let s = (options.contains(.hardBreaks) ? "\n" : " ").attributed + styler.style(softBreak: s) + return s + } + + public func visit(lineBreak node: LineBreak) -> NSMutableAttributedString { + let s = "\n".attributed + styler.style(lineBreak: s) + return s + } + + public func visit(code node: Code) -> NSMutableAttributedString { + guard let s = node.literal?.attributed else { return .empty } + styler.style(code: s) + return s + } + + public func visit(htmlInline node: HtmlInline) -> NSMutableAttributedString { + guard let s = node.literal?.attributed else { return .empty } + styler.style(htmlInline: s) + return s + } + + public func visit(customInline node: CustomInline) -> NSMutableAttributedString { + guard let s = node.literal?.attributed else { return .empty } + styler.style(customInline: s) + return s + } + + public func visit(emphasis node: Emphasis) -> NSMutableAttributedString { + let s = visitChildren(of: node).joined + styler.style(emphasis: s) + return s + } + + public func visit(strong node: Strong) -> NSMutableAttributedString { + let s = visitChildren(of: node).joined + styler.style(strong: s) + return s + } + + 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 + } + + 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 + } +} + +// MARK: - Helper extentions + +private extension Sequence where Iterator.Element == NSMutableAttributedString { + var joined: NSMutableAttributedString { + return reduce(into: NSMutableAttributedString()) { $0.append($1) } + } +} + +private extension String { + var attributed: NSMutableAttributedString { + return NSMutableAttributedString(string: self) + } +} + +private extension NSAttributedString { + static var blankLine: NSAttributedString { + return "\n".attributed + } +} + +private extension NSMutableAttributedString { + static var empty: NSMutableAttributedString { + return "".attributed + } +} diff --git a/Source/AST/Visitors/DebugVisitor.swift b/Source/AST/Visitors/DebugVisitor.swift new file mode 100644 index 00000000..c381ac78 --- /dev/null +++ b/Source/AST/Visitors/DebugVisitor.swift @@ -0,0 +1,120 @@ +// +// DebugVisitor.swift +// Down +// +// Created by John Nguyen on 09.04.19. +// + +import Foundation + +/// This visitor will generate the debug description of an entire abstract syntax tree, +/// indicating relationships between nodes with indentation. +/// +public class DebugVisitor { + + /// Current depth in the tree. + private var depth = 0 + + /// The amount of indent for the current depth. + private var indent: String { + return String(repeating: " ", count: depth) + } + + /// Debug representation of node. + private func report(_ node: Node) -> String { + return "\(indent)\(node is Document ? "" : "↳ ")\(String(reflecting: node))\n" + } + + /// Debug representation of node including all children. + private func reportWithChildren(_ node: Node) -> String { + let thisNode = report(node) + depth += 1 + let children = visitChildren(of: node).joined() + depth -= 1 + return "\(thisNode)\(children)" + } +} + +extension DebugVisitor: Visitor { + public typealias Result = String + + public func visit(document node: Document) -> String { + return reportWithChildren(node) + } + + public func visit(blockQuote node: BlockQuote) -> String { + return reportWithChildren(node) + } + + public func visit(list node: List) -> String { + return reportWithChildren(node) + } + + public func visit(item node: Item) -> String { + return reportWithChildren(node) + } + + public func visit(codeBlock node: CodeBlock) -> String { + return reportWithChildren(node) + } + + public func visit(htmlBlock node: HtmlBlock) -> String { + return reportWithChildren(node) + } + + public func visit(customBlock node: CustomBlock) -> String { + return reportWithChildren(node) + } + + public func visit(paragraph node: Paragraph) -> String { + return reportWithChildren(node) + } + + public func visit(heading node: Heading) -> String { + return reportWithChildren(node) + } + + public func visit(thematicBreak node: ThematicBreak) -> String { + return report(node) + } + + public func visit(text node: Text) -> String { + return report(node) + } + + public func visit(softBreak node: SoftBreak) -> String { + return report(node) + } + + public func visit(lineBreak node: LineBreak) -> String { + return report(node) + } + + public func visit(code node: Code) -> String { + return report(node) + } + + public func visit(htmlInline node: HtmlInline) -> String { + return report(node) + } + + public func visit(customInline node: CustomInline) -> String { + return report(node) + } + + public func visit(emphasis node: Emphasis) -> String { + return reportWithChildren(node) + } + + public func visit(strong node: Strong) -> String { + return reportWithChildren(node) + } + + public func visit(link node: Link) -> String { + return reportWithChildren(node) + } + + public func visit(image node: Image) -> String { + return reportWithChildren(node) + } +} diff --git a/Source/AST/Visitors/Vistor.swift b/Source/AST/Visitors/Vistor.swift new file mode 100644 index 00000000..4ec63305 --- /dev/null +++ b/Source/AST/Visitors/Vistor.swift @@ -0,0 +1,70 @@ +// +// Vistor.swift +// Down +// +// Created by John Nguyen on 07.04.19. +// + +import Foundation + +/// Visitor describes a type that is able to traverse the abstract syntax tree. It visits +/// 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 + func visit(item node: Item) -> Result + func visit(codeBlock node: CodeBlock) -> Result + func visit(htmlBlock node: HtmlBlock) -> Result + func visit(customBlock node: CustomBlock) -> Result + func visit(paragraph node: Paragraph) -> Result + func visit(heading node: Heading) -> Result + func visit(thematicBreak node: ThematicBreak) -> Result + func visit(text node: Text) -> Result + func visit(softBreak node: SoftBreak) -> Result + func visit(lineBreak node: LineBreak) -> Result + func visit(code node: Code) -> Result + func visit(htmlInline node: HtmlInline) -> Result + func visit(customInline node: CustomInline) -> Result + func visit(emphasis node: Emphasis) -> Result + func visit(strong node: Strong) -> Result + 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.children.compactMap { child in + switch child { + case is Document: return visit(document: child as! Document) + case is BlockQuote: return visit(blockQuote: child as! BlockQuote) + case is List: return visit(list: child as! List) + case is Item: return visit(item: child as! Item) + case is CodeBlock: return visit(codeBlock: child as! CodeBlock) + case is HtmlBlock: return visit(htmlBlock: child as! HtmlBlock) + case is CustomBlock: return visit(customBlock: child as! CustomBlock) + case is Paragraph: return visit(paragraph: child as! Paragraph) + case is Heading: return visit(heading: child as! Heading) + case is ThematicBreak: return visit(thematicBreak: child as! ThematicBreak) + case is Text: return visit(text: child as! Text) + case is SoftBreak: return visit(softBreak: child as! SoftBreak) + case is LineBreak: return visit(lineBreak: child as! LineBreak) + case is Code: return visit(code: child as! Code) + case is HtmlInline: return visit(htmlInline: child as! HtmlInline) + case is CustomInline: return visit(customInline: child as! CustomInline) + case is Emphasis: return visit(emphasis: child as! Emphasis) + case is Strong: return visit(strong: child as! Strong) + case is Link: return visit(link: child as! Link) + case is Image: return visit(image: child as! Image) + default: + assertionFailure("Unexpected child") + return nil + } + } + } +} diff --git a/Source/Renderers/DownASTRenderable.swift b/Source/Renderers/DownASTRenderable.swift index 030328f8..d05fd375 100644 --- a/Source/Renderers/DownASTRenderable.swift +++ b/Source/Renderers/DownASTRenderable.swift @@ -19,7 +19,6 @@ public protocol DownASTRenderable: DownRenderable { - returns: An abstract syntax tree representation of the Markdown input */ - func toAST(_ options: DownOptions) throws -> UnsafeMutablePointer<cmark_node> } @@ -33,7 +32,6 @@ extension DownASTRenderable { - returns: An abstract syntax tree representation of the Markdown input */ - public func toAST(_ options: DownOptions = .default) throws -> UnsafeMutablePointer<cmark_node> { return try DownASTRenderer.stringToAST(markdownString, options: options) } @@ -51,7 +49,6 @@ public struct DownASTRenderer { - returns: An abstract syntax tree representation of the Markdown input */ - public static func stringToAST(_ string: String, options: DownOptions = .default) throws -> UnsafeMutablePointer<cmark_node> { var tree: UnsafeMutablePointer<cmark_node>? string.withCString { diff --git a/Source/Renderers/DownAttributedStringRenderable.swift b/Source/Renderers/DownAttributedStringRenderable.swift index 9fded92a..e3090b0a 100644 --- a/Source/Renderers/DownAttributedStringRenderable.swift +++ b/Source/Renderers/DownAttributedStringRenderable.swift @@ -9,7 +9,7 @@ import Foundation import libcmark -public protocol DownAttributedStringRenderable: DownHTMLRenderable { +public protocol DownAttributedStringRenderable: DownHTMLRenderable, DownASTRenderable { /** Generates an `NSAttributedString` from the `markdownString` property @@ -21,26 +21,70 @@ public protocol DownAttributedStringRenderable: DownHTMLRenderable { - returns: An `NSAttributedString` */ - func toAttributedString(_ options: DownOptions, stylesheet: String?) throws -> NSAttributedString + + /** + Generates an `NSAttributedString` from the `markdownString` property + + - parameter options: `DownOptions` to modify parsing or rendering + + - parameter styler: a `Styler` to use when rendering + + - throws: `DownErrors` depending on the scenario + + - returns: An `NSAttributedString` + */ + func toAttributedString(_ options: DownOptions, styler: Styler) throws -> NSAttributedString } extension DownAttributedStringRenderable { /** Generates an `NSAttributedString` from the `markdownString` property + + 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 + thread. Additionally, it may be slow to render. For an efficient background safe render, + use the `toAttributedString(options: styler:)` method. - parameter options: `DownOptions` to modify parsing or rendering, defaulting to `.default` - - parameter stylesheet: a `String` to use as the CSS stylesheet when rendering, defaulting to a style that uses the `NSAttributedString` default font + - parameter stylesheet: a `String` to use as the CSS stylesheet when rendering, defaulting + to a style that uses the `NSAttributedString` default font - throws: `DownErrors` depending on the scenario - returns: An `NSAttributedString` */ - 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 + + 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. + + - parameter options: `DownOptions` to modify parsing or rendering + + - parameter styler: a `Styler` to use when rendering + + - throws: `DownErrors` depending on the scenario + + - returns: An `NSAttributedString` + */ + public func toAttributedString(_ options: DownOptions = .default, styler: Styler) throws -> NSAttributedString { + let tree = try self.toAST(options) + + guard tree.type == CMARK_NODE_DOCUMENT else { + throw DownErrors.astRenderingError + } + + let document = Document(cmarkNode: tree) + let visitor = AttributedStringVisitor(styler: styler, options: options) + return document.accept(visitor) + } } diff --git a/Source/Renderers/DownCommonMarkRenderable.swift b/Source/Renderers/DownCommonMarkRenderable.swift index 9a9af0d2..52ce72fd 100644 --- a/Source/Renderers/DownCommonMarkRenderable.swift +++ b/Source/Renderers/DownCommonMarkRenderable.swift @@ -20,7 +20,6 @@ public protocol DownCommonMarkRenderable: DownRenderable { - returns: CommonMark Markdown string */ - func toCommonMark(_ options: DownOptions, width: Int32) throws -> String } @@ -35,7 +34,6 @@ extension DownCommonMarkRenderable { - returns: CommonMark Markdown string */ - 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) @@ -57,7 +55,6 @@ public struct DownCommonMarkRenderer { - returns: CommonMark Markdown string */ - public static func astToCommonMark(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default, width: Int32 = 0) throws -> String { diff --git a/Source/Renderers/DownGroffRenderable.swift b/Source/Renderers/DownGroffRenderable.swift index 291106a1..4bdc5043 100644 --- a/Source/Renderers/DownGroffRenderable.swift +++ b/Source/Renderers/DownGroffRenderable.swift @@ -20,7 +20,6 @@ public protocol DownGroffRenderable: DownRenderable { - returns: groff man string */ - func toGroff(_ options: DownOptions, width: Int32) throws -> String } @@ -35,7 +34,6 @@ extension DownGroffRenderable { - returns: groff man string */ - 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) @@ -57,7 +55,6 @@ public struct DownGroffRenderer { - returns: groff man string */ - public static func astToGroff(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default, width: Int32 = 0) throws -> String { diff --git a/Source/Renderers/DownHTMLRenderable.swift b/Source/Renderers/DownHTMLRenderable.swift index bc110622..fbb15177 100644 --- a/Source/Renderers/DownHTMLRenderable.swift +++ b/Source/Renderers/DownHTMLRenderable.swift @@ -19,7 +19,6 @@ public protocol DownHTMLRenderable: DownRenderable { - returns: HTML string */ - func toHTML(_ options: DownOptions) throws -> String } @@ -33,7 +32,6 @@ extension DownHTMLRenderable { - returns: HTML string */ - public func toHTML(_ options: DownOptions = .default) throws -> String { return try markdownString.toHTML(options) } @@ -51,7 +49,6 @@ public struct DownHTMLRenderer { - returns: HTML string */ - public static func astToHTML(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default) throws -> String { guard let cHTMLString = cmark_render_html(ast, options.rawValue) else { throw DownErrors.astRenderingError diff --git a/Source/Renderers/DownLaTeXRenderable.swift b/Source/Renderers/DownLaTeXRenderable.swift index 96bbf8bf..e6472642 100644 --- a/Source/Renderers/DownLaTeXRenderable.swift +++ b/Source/Renderers/DownLaTeXRenderable.swift @@ -20,7 +20,6 @@ public protocol DownLaTeXRenderable: DownRenderable { - returns: LaTeX string */ - func toLaTeX(_ options: DownOptions, width: Int32) throws -> String } @@ -35,7 +34,6 @@ extension DownLaTeXRenderable { - returns: LaTeX string */ - 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) @@ -57,7 +55,6 @@ public struct DownLaTeXRenderer { - returns: LaTeX string */ - public static func astToLaTeX(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default, width: Int32 = 0) throws -> String { diff --git a/Source/Renderers/DownXMLRenderable.swift b/Source/Renderers/DownXMLRenderable.swift index 1dddd727..46bfd2dd 100644 --- a/Source/Renderers/DownXMLRenderable.swift +++ b/Source/Renderers/DownXMLRenderable.swift @@ -19,7 +19,6 @@ public protocol DownXMLRenderable: DownRenderable { - returns: XML string */ - func toXML(_ options: DownOptions) throws -> String } @@ -33,7 +32,6 @@ extension DownXMLRenderable { - returns: XML string */ - public func toXML(_ options: DownOptions = .default) throws -> String { let ast = try DownASTRenderer.stringToAST(markdownString, options: options) let xml = try DownXMLRenderer.astToXML(ast, options: options) @@ -54,7 +52,6 @@ public struct DownXMLRenderer { - returns: XML string */ - public static func astToXML(_ ast: UnsafeMutablePointer<cmark_node>, options: DownOptions = .default) throws -> String { guard let cXMLString = cmark_render_xml(ast, options.rawValue) else { throw DownErrors.astRenderingError diff --git a/Source/Views/DownView.swift b/Source/Views/DownView.swift index 77b032ac..8ddc95e7 100644 --- a/Source/Views/DownView.swift +++ b/Source/Views/DownView.swift @@ -198,7 +198,7 @@ extension DownView: WKNavigationDelegate { fileprivate extension WKNavigationDelegate { /// A wrapper for `UIApplication.shared.openURL` so that an empty default /// implementation is available in app extensions - public func openURL(url: URL) {} + func openURL(url: URL) {} } #endif diff --git a/Tests/VisitorTests.swift b/Tests/VisitorTests.swift new file mode 100644 index 00000000..342849d9 --- /dev/null +++ b/Tests/VisitorTests.swift @@ -0,0 +1,126 @@ +// +// VisitorTests.swift +// DownTests +// +// Created by John Nguyen on 08.04.19. +// + +import XCTest +@testable import Down + +class VisitorTests: XCTestCase { + + func testExample() throws { + // Given + let markdown = """ + # Hello + This is a **test!** + """ + + let down = Down(markdownString: markdown) + let ast = try down.toAST() + let document = Document(cmarkNode: ast) + + // When + let result = document.accept(DebugVisitor()) + + // Then + let expected = """ + Document + ↳ Heading - L1 + ↳ Text - Hello + ↳ Paragraph + ↳ Text - This is a + ↳ Strong + ↳ Text - test! + + """ + + XCTAssertEqual(result, expected) + } + + func testAttributedStringVisitor() throws { + // Given + let markdown = """ + # Heading + + This **is** a *paragraph* with `inline` + elements <p></p> + + This is followed by a hard linebreak\(" ") + This is after the linebreak + + --- + + [this is a link](www.text.com) + ![this is an image](www.text.com) + + > this is a quote + + ``` + code block + code block + ``` + + <html> + block + </html> + + 1. first item + 2. second item + """ + + let down = Down(markdownString: markdown) + let ast = try down.toAST() + print(Document(cmarkNode: ast).accept(DebugVisitor())) + + // When + let result = try down.toAttributedString(styler: EmptyStyler()).string + + // Then + let expected = """ + Heading + This is a paragraph with inline elements <p></p> + This is followed by a hard linebreak + This is after the linebreak + + this is a link this is an image + this is a quote + code block + code block + <html> + block + </html> + 1.\tfirst item + 2.\tsecond item + """ + + XCTAssertEqual(result, expected) + } + +} + +private class EmptyStyler: Styler { + var listPrefixAttributes: [NSAttributedStringKey : Any] = [:] + func style(document str: NSMutableAttributedString) {} + func style(blockQuote str: NSMutableAttributedString) {} + func style(list str: NSMutableAttributedString) {} + func style(item str: NSMutableAttributedString) {} + func style(codeBlock str: NSMutableAttributedString, fenceInfo: String?) {} + func style(htmlBlock str: NSMutableAttributedString) {} + func style(customBlock str: NSMutableAttributedString) {} + func style(paragraph str: NSMutableAttributedString) {} + func style(heading str: NSMutableAttributedString, level: Int) {} + func style(thematicBreak str: NSMutableAttributedString) {} + func style(text str: NSMutableAttributedString) {} + func style(softBreak str: NSMutableAttributedString) {} + func style(lineBreak str: NSMutableAttributedString) {} + func style(code str: NSMutableAttributedString) {} + func style(htmlInline str: NSMutableAttributedString) {} + func style(customInline str: NSMutableAttributedString) {} + func style(emphasis str: NSMutableAttributedString) {} + func style(strong str: NSMutableAttributedString) {} + func style(link str: NSMutableAttributedString, title: String?, url: String?) {} + func style(image str: NSMutableAttributedString, title: String?, url: String?) {} +} +