-
Notifications
You must be signed in to change notification settings - Fork 195
/
MarkdownParser.swift
125 lines (108 loc) · 4.36 KB
/
MarkdownParser.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* Ink
* Copyright (c) John Sundell 2019
* MIT license, see LICENSE file for details
*/
///
/// A parser used to convert Markdown text into HTML
///
/// You can use an instance of this type to either convert
/// a Markdown string into an HTML string, or into a `Markdown`
/// value, which also contains any metadata values found in
/// the parsed Markdown text.
///
/// To customize how this parser performs its work, attach
/// a `Modifier` using the `addModifier` method.
public struct MarkdownParser {
private var modifiers: ModifierCollection
/// Initialize an instance, optionally passing an array
/// of modifiers used to customize the parsing process.
public init(modifiers: [Modifier] = []) {
self.modifiers = ModifierCollection(modifiers: modifiers)
}
/// Add a modifier to this parser, which can be used to
/// customize the parsing process. See `Modifier` for more info.
public mutating func addModifier(_ modifier: Modifier) {
modifiers.insert(modifier)
}
/// Convert a Markdown string into HTML, discarding any metadata
/// found in the process. To preserve the Markdown's metadata,
/// use the `parse` method instead.
public func html(from markdown: String) -> String {
parse(markdown).html
}
/// Parse a Markdown string into a `Markdown` value, which contains
/// both the HTML representation of the given string, and also any
/// metadata values found within it.
public func parse(_ markdown: String) -> Markdown {
var reader = Reader(string: markdown)
var fragments = [ParsedFragment]()
var urlsByName = [String : URL]()
var metadata: Metadata?
while !reader.didReachEnd {
reader.discardWhitespacesAndNewlines()
guard !reader.didReachEnd else { break }
do {
if metadata == nil, fragments.isEmpty, reader.currentCharacter == "-" {
if let parsedMetadata = try? Metadata.readOrRewind(using: &reader) {
metadata = parsedMetadata
continue
}
}
guard reader.currentCharacter != "[" else {
let declaration = try URLDeclaration.readOrRewind(using: &reader)
urlsByName[declaration.name] = declaration.url
continue
}
let type = fragmentType(for: reader.currentCharacter,
nextCharacter: reader.nextCharacter)
let fragment = try makeFragment(using: type.readOrRewind, reader: &reader)
fragments.append(fragment)
} catch {
let paragraph = makeFragment(using: Paragraph.read, reader: &reader)
fragments.append(paragraph)
}
}
let urls = NamedURLCollection(urlsByName: urlsByName)
let html = fragments.reduce(into: "") { result, wrapper in
let html = wrapper.fragment.html(
usingURLs: urls,
rawString: wrapper.rawString,
applyingModifiers: modifiers
)
result.append(html)
}
return Markdown(
html: html,
metadata: metadata?.values ?? [:]
)
}
}
private extension MarkdownParser {
struct ParsedFragment {
var fragment: Fragment
var rawString: Substring
}
func makeFragment(using closure: (inout Reader) throws -> Fragment,
reader: inout Reader) rethrows -> ParsedFragment {
let startIndex = reader.currentIndex
let fragment = try closure(&reader)
let rawString = reader.characters(in: startIndex..<reader.currentIndex)
return ParsedFragment(fragment: fragment, rawString: rawString)
}
func fragmentType(for character: Character,
nextCharacter: Character?) -> Fragment.Type {
switch character {
case "#": return Heading.self
case "!": return Image.self
case "<": return HTML.self
case ">": return Blockquote.self
case "`": return CodeBlock.self
case "-" where character == nextCharacter,
"*" where character == nextCharacter:
return HorizontalLine.self
case "-", "*", "+", \.isNumber: return List.self
default: return Paragraph.self
}
}
}