27 |
28 | extension Blocks {
29 |
30 | /// Returns the text of a singleton `Blocks` object. A singleton `Blocks` object contains a
31 | /// single paragraph. This property returns `nil` if this object is not a singleton `Blocks`
32 | /// object.
33 | public var text: Text? {
34 | if self.count == 1,
35 | case .paragraph(let text) = self[0] {
36 | return text
37 | } else {
38 | return nil
39 | }
40 | }
41 |
42 | /// Returns true if this is a singleton `Blocks` object.
43 | public var isSingleton: Bool {
44 | return self.count == 1
45 | }
46 |
47 | /// Returns raw text for this sequence of blocks.
48 | public var string: String {
49 | return self.map { $0.string }.joined(separator: "\n")
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/CustomBlock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomBlock.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 12/05/2021.
6 | // Copyright © 2021 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// Protocol `CustomBlock` defines the interface of custom Markdown elements that are implemented
25 | /// externally (i.e. not by the MarkdownKit framework).
26 | ///
27 | public protocol CustomBlock: CustomStringConvertible, CustomDebugStringConvertible {
28 | var string: String { get }
29 | func equals(to other: CustomBlock) -> Bool
30 | func parse(via parser: InlineParser) -> Block
31 | func generateHtml(via htmlGen: HtmlGenerator, tight: Bool) -> String
32 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
33 | func generateHtml(via htmlGen: HtmlGenerator,
34 | and attGen: AttributedStringGenerator?,
35 | tight: Bool) -> String
36 | #endif
37 | }
38 |
39 | extension CustomBlock {
40 | /// By default, the custom block does not have any raw string content.
41 | public var string: String {
42 | return ""
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/CustomTextFragment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomTextFragment.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 12/05/2021.
6 | // Copyright © 2021 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// Protocol `CustomTextFragment` defines the interface for custom Markdown text fragments
25 | /// that are implemented externally (i.e. not by the MarkdownKit framework).
26 | ///
27 | public protocol CustomTextFragment: CustomStringConvertible, CustomDebugStringConvertible {
28 | func equals(to other: CustomTextFragment) -> Bool
29 | func transform(via transformer: InlineTransformer) -> TextFragment
30 | func generateHtml(via htmlGen: HtmlGenerator) -> String
31 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
32 | func generateHtml(via htmlGen: HtmlGenerator, and attrGen: AttributedStringGenerator?) -> String
33 | #endif
34 | var rawDescription: String { get }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/HTML/HtmlGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HtmlGenerator.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 15/07/2019.
6 | // Copyright © 2019-2021 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// `HtmlGenerator` provides functionality for converting Markdown blocks into HTML. The
25 | /// implementation is extensible allowing subclasses of `HtmlGenerator` to override how
26 | /// individual Markdown structures are converted into HTML.
27 | ///
28 | open class HtmlGenerator {
29 |
30 | public enum Parent {
31 | case none
32 | indirect case block(Block, Parent)
33 | }
34 |
35 | /// Default `HtmlGenerator` implementation
36 | public static let standard = HtmlGenerator()
37 |
38 | public init() {}
39 |
40 | /// `generate` takes a block representing a Markdown document and returns a corresponding
41 | /// representation in HTML as a string.
42 | open func generate(doc: Block) -> String {
43 | guard case .document(let blocks) = doc else {
44 | preconditionFailure("cannot generate HTML from \(doc)")
45 | }
46 | return self.generate(blocks: blocks, parent: .none)
47 | }
48 |
49 | open func generate(blocks: Blocks, parent: Parent, tight: Bool = false) -> String {
50 | var res = ""
51 | for block in blocks {
52 | res += self.generate(block: block, parent: parent, tight: tight)
53 | }
54 | return res
55 | }
56 |
57 | open func generate(block: Block, parent: Parent, tight: Bool = false) -> String {
58 | switch block {
59 | case .document(_):
60 | preconditionFailure("broken block \(block)")
61 | case .blockquote(let blocks):
62 | return "\n" +
63 | self.generate(blocks: blocks, parent: .block(block, parent)) +
64 | " \n"
65 | case .list(let start, let tight, let blocks):
66 | if let startNumber = start {
67 | return "\n" +
68 | self.generate(blocks: blocks, parent: .block(block, parent), tight: tight) +
69 | " \n"
70 | } else {
71 | return "\n" +
72 | self.generate(blocks: blocks, parent: .block(block, parent), tight: tight) +
73 | " \n"
74 | }
75 | case .listItem(_, _, let blocks):
76 | if tight, let text = blocks.text {
77 | return "" + self.generate(text: text) + " \n"
78 | } else {
79 | return "" +
80 | self.generate(blocks: blocks, parent: .block(block, parent), tight: tight) +
81 | " \n"
82 | }
83 | case .paragraph(let text):
84 | if tight {
85 | return self.generate(text: text) + "\n"
86 | } else {
87 | return "" + self.generate(text: text) + "
\n"
88 | }
89 | case .heading(let n, let text):
90 | let tag = "h\(n > 0 && n < 7 ? n : 1)>"
91 | return "<\(tag)\(self.generate(text: text))\(tag)\n"
92 | case .indentedCode(let lines):
93 | return "" +
94 | self.generate(lines: lines).encodingPredefinedXmlEntities() +
95 | "
\n"
96 | case .fencedCode(let lang, let lines):
97 | if let language = lang {
98 | return "" +
99 | self.generate(lines: lines, separator: "").encodingPredefinedXmlEntities() +
100 | "
\n"
101 | } else {
102 | return "" +
103 | self.generate(lines: lines, separator: "").encodingPredefinedXmlEntities() +
104 | "
\n"
105 | }
106 | case .htmlBlock(let lines):
107 | return self.generate(lines: lines)
108 | case .referenceDef(_, _, _):
109 | return ""
110 | case .thematicBreak:
111 | return " \n"
112 | case .table(let header, let align, let rows):
113 | var tagsuffix: [String] = []
114 | for a in align {
115 | switch a {
116 | case .undefined:
117 | tagsuffix.append(">")
118 | case .left:
119 | tagsuffix.append(" align=\"left\">")
120 | case .right:
121 | tagsuffix.append(" align=\"right\">")
122 | case .center:
123 | tagsuffix.append(" align=\"center\">")
124 | }
125 | }
126 | var html = "\n"
127 | var i = 0
128 | for head in header {
129 | html += ""
130 | i += 1
131 | }
132 | html += "\n \n"
133 | for row in rows {
134 | html += ""
135 | i = 0
136 | for cell in row {
137 | html += ""
138 | i += 1
139 | }
140 | html += " \n"
141 | }
142 | html += "
\n"
143 | return html
144 | case .definitionList(let defs):
145 | var html = "\n"
146 | for def in defs {
147 | html += "" + self.generate(text: def.item) + " \n"
148 | for descr in def.descriptions {
149 | if case .listItem(_, _, let blocks) = descr {
150 | if blocks.count == 1,
151 | case .paragraph(let text) = blocks.first! {
152 | html += "" + self.generate(text: text) + " \n"
153 | } else {
154 | html += "" +
155 | self.generate(blocks: blocks, parent: .block(block, parent)) +
156 | " \n"
157 | }
158 | }
159 | }
160 | }
161 | html += " \n"
162 | return html
163 | case .custom(let customBlock):
164 | return customBlock.generateHtml(via: self, tight: tight)
165 | }
166 | }
167 |
168 | open func generate(text: Text) -> String {
169 | var res = ""
170 | for fragment in text {
171 | res += self.generate(textFragment: fragment)
172 | }
173 | return res
174 | }
175 |
176 | open func generate(textFragment fragment: TextFragment) -> String {
177 | switch fragment {
178 | case .text(let str):
179 | return String(str).decodingNamedCharacters().encodingPredefinedXmlEntities()
180 | case .code(let str):
181 | return "" + String(str).encodingPredefinedXmlEntities() + "
"
182 | case .emph(let text):
183 | return "" + self.generate(text: text) + " "
184 | case .strong(let text):
185 | return "" + self.generate(text: text) + " "
186 | case .link(let text, let uri, let title):
187 | let titleAttr = title == nil ? "" : " title=\"\(title!)\""
188 | return "" + self.generate(text: text) + " "
189 | case .autolink(let type, let str):
190 | switch type {
191 | case .uri:
192 | return "\(str) "
193 | case .email:
194 | return "\(str) "
195 | }
196 | case .image(let text, let uri, let title):
197 | let titleAttr = title == nil ? "" : " title=\"\(title!)\""
198 | if let uri = uri {
199 | return " "
200 | } else {
201 | return self.generate(text: text)
202 | }
203 | case .html(let tag):
204 | return "<\(tag.description)>"
205 | case .delimiter(let ch, let n, _):
206 | let char: String
207 | switch ch {
208 | case "<":
209 | char = "<"
210 | case ">":
211 | char = ">"
212 | default:
213 | char = String(ch)
214 | }
215 | var res = char
216 | for _ in 1.. "
224 | case .custom(let customTextFragment):
225 | return customTextFragment.generateHtml(via: self)
226 | }
227 | }
228 |
229 | open func generate(lines: Lines, separator: String = "\n") -> String {
230 | var res = ""
231 | for line in lines {
232 | if res.isEmpty {
233 | res = String(line)
234 | } else {
235 | res += separator + line
236 | }
237 | }
238 | return res
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/HTML/String+Entities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Entities.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 13/02/2021.
6 | // Copyright © 2021 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | extension String {
24 |
25 | public func encodingPredefinedXmlEntities() -> String {
26 | var res = ""
27 | var pos = self.startIndex
28 | // find the first character that requires encoding
29 | while pos < self.endIndex,
30 | let index = self.rangeOfCharacter(from: Self.predefinedEntities,
31 | range: pos..":
45 | res.append(contentsOf: ">")
46 | default:
47 | res.append(self[index.lowerBound])
48 | }
49 | pos = self.index(after: index.lowerBound)
50 | }
51 | if res.isEmpty {
52 | return self
53 | } else {
54 | res.append(contentsOf: self[pos.. String {
60 | var res = ""
61 | for ch in self {
62 | if let charRef = NamedCharacters.characterNameMap[ch] {
63 | res.append(contentsOf: charRef)
64 | } else {
65 | res.append(ch)
66 | }
67 | }
68 | return res
69 | }
70 |
71 | public func decodingNamedCharacters() -> String {
72 | var res = ""
73 | var pos = self.startIndex
74 | // find the next `&`
75 | while let ampPos = self.range(of: "&", range: pos..")
109 | return set
110 | }()
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSHumanReadableCopyright
22 | Copyright © 2019-2025 Google LLC.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/MarkdownKit.h:
--------------------------------------------------------------------------------
1 | //
2 | // MarkdownKit.h
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 03/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import
22 |
23 | //! Project version number for MarkdownKit.
24 | FOUNDATION_EXPORT double MarkdownKitVersionNumber;
25 |
26 | //! Project version string for MarkdownKit.
27 | FOUNDATION_EXPORT const unsigned char MarkdownKitVersionString[];
28 |
29 | // In this header, you should import all the public headers of your framework using statements like #import
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/AtxHeadingParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ATXHeadingParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 01/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// A block parser which parses ATX headings (of the form `## Header`) returning `heading` blocks.
25 | ///
26 | open class AtxHeadingParser: BlockParser {
27 |
28 | public override func parse() -> ParseResult {
29 | guard self.shortLineIndent else {
30 | return .none
31 | }
32 | var i = self.contentStartIndex
33 | var level = 0
34 | while i < self.contentEndIndex && self.line[i] == "#" && level < 7 {
35 | i = self.line.index(after: i)
36 | level += 1
37 | }
38 | guard level > 0 && level < 7 && (i >= self.contentEndIndex || self.line[i] == " ") else {
39 | return .none
40 | }
41 | while i < self.contentEndIndex && self.line[i] == " " {
42 | i = self.line.index(after: i)
43 | }
44 | guard i < self.contentEndIndex else {
45 | let res: Block = .heading(level, Text(self.line[i.. i && self.line[e] == " " {
51 | e = self.line.index(before: e)
52 | }
53 | if e > i && self.line[e] == "#" {
54 | let e0 = e
55 | while e > i && self.line[e] == "#" {
56 | e = self.line.index(before: e)
57 | }
58 | if e >= i && self.line[e] == " " {
59 | while e >= i && self.line[e] == " " {
60 | e = self.line.index(before: e)
61 | }
62 | } else {
63 | e = e0
64 | }
65 | }
66 | let res: Block = .heading(level, Text(self.line[i...e]))
67 | self.readNextLine()
68 | return .block(res)
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/BlockParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlockParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 01/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// A `BlockParser` parses one particular type of Markdown blocks. Class `BlockParser` defines
25 | /// a framework for such block parsers. Every different block type comes with its own subclass
26 | /// of `BlockParser`.
27 | ///
28 | open class BlockParser {
29 |
30 | /// The result of calling the `parse` method.
31 | public enum ParseResult {
32 | case none
33 | case block(Block)
34 | case container((Container) -> Container)
35 | }
36 |
37 | unowned let docParser: DocumentParser
38 |
39 | public required init(docParser: DocumentParser) {
40 | self.docParser = docParser
41 | }
42 |
43 | public var finished: Bool {
44 | return self.docParser.finished
45 | }
46 |
47 | public var prevParagraphLines: Text? {
48 | return self.docParser.prevParagraphLines
49 | }
50 |
51 | public func consumeParagraphLines() {
52 | self.docParser.prevParagraphLines = nil
53 | self.docParser.prevParagraphLinesTight = false
54 | }
55 |
56 | public var line: Substring {
57 | return self.docParser.line
58 | }
59 |
60 | public var contentStartIndex: Substring.Index {
61 | return self.docParser.contentStartIndex
62 | }
63 |
64 | public var contentEndIndex: Substring.Index {
65 | return self.docParser.contentEndIndex
66 | }
67 |
68 | public var lineIndent: Int {
69 | return self.docParser.lineIndent
70 | }
71 |
72 | public var lineEmpty: Bool {
73 | return self.docParser.lineEmpty
74 | }
75 |
76 | public var prevLineEmpty: Bool {
77 | return self.docParser.prevLineEmpty
78 | }
79 |
80 | public var shortLineIndent: Bool {
81 | return self.docParser.shortLineIndent
82 | }
83 |
84 | public var lazyContinuation: Bool {
85 | return self.docParser.lazyContinuation
86 | }
87 |
88 | open func readNextLine() {
89 | self.docParser.readNextLine()
90 | }
91 |
92 | open var mayInterruptParagraph: Bool {
93 | return true
94 | }
95 |
96 | open func parse() -> ParseResult {
97 | return .none
98 | }
99 | }
100 |
101 | ///
102 | /// `RestorableBlockParser` objects are `BlockParser` objects which restore the
103 | /// `DocumentParser` state in case their `parse` method fails (the `ParseResult` is `.none`).
104 | ///
105 | open class RestorableBlockParser: BlockParser {
106 | private var docParserState: DocumentParserState
107 |
108 | public required init(docParser: DocumentParser) {
109 | self.docParserState = DocumentParserState(docParser)
110 | super.init(docParser: docParser)
111 | }
112 |
113 | open override func parse() -> ParseResult {
114 | self.docParser.copyState(&self.docParserState)
115 | let res = self.tryParse()
116 | if case .none = res {
117 | self.docParser.restoreState(self.docParserState)
118 | return .none
119 | } else {
120 | return res
121 | }
122 | }
123 |
124 | open func tryParse() -> ParseResult {
125 | return .none
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/BlockquoteParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlockquoteParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 03/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// A block parser which parses block quotes eturning `blockquote` blocks.
25 | ///
26 | open class BlockquoteParser: BlockParser {
27 |
28 | private final class BlockquoteContainer: NestedContainer {
29 |
30 | public override var indentRequired: Bool {
31 | return true
32 | }
33 |
34 | public override func skipIndent(input: String,
35 | startIndex: String.Index,
36 | endIndex: String.Index) -> String.Index? {
37 | var index = startIndex
38 | var indent = 0
39 | while index < endIndex && input[index] == " " {
40 | indent += 1
41 | index = input.index(after: index)
42 | }
43 | guard indent < 4 && index < endIndex && input[index] == ">" else {
44 | return nil
45 | }
46 | index = input.index(after: index)
47 | if index < endIndex && input[index] == " " {
48 | index = input.index(after: index)
49 | }
50 | return index
51 | }
52 |
53 | public override func makeBlock(_ docParser: DocumentParser) -> Block {
54 | return .blockquote(docParser.bundle(blocks: self.content))
55 | }
56 |
57 | public override var debugDescription: String {
58 | return self.outer.debugDescription + " <- blockquote"
59 | }
60 | }
61 |
62 | public override func parse() -> ParseResult {
63 | guard self.shortLineIndent && self.line[self.contentStartIndex] == ">" else {
64 | return .none
65 | }
66 | let i = self.line.index(after: self.contentStartIndex)
67 | if i < self.contentEndIndex && self.line[i] == " " {
68 | self.docParser.resetLineStart(self.line.index(after: i))
69 | } else {
70 | self.docParser.resetLineStart(i)
71 | }
72 | return .container(BlockquoteContainer.init)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/CodeBlockParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodeBlockParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 01/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// Block parsers for parsing different types of code blocks. `CodeBlockParser` implements
25 | /// shared logic between two concrete implementations, `IndentedCodeBlockParser` and
26 | /// `FencedCodeBlockParser`.
27 | ///
28 | open class CodeBlockParser: BlockParser {
29 |
30 | public func formatIndentedLine(_ n: Int = 4) -> Substring {
31 | var index = self.line.startIndex
32 | var indent = 0
33 | while index < self.line.endIndex && indent < n {
34 | if self.line[index] == " " {
35 | indent += 1
36 | } else if self.line[index] == "\t" {
37 | indent += 4
38 | } else {
39 | break
40 | }
41 | index = self.line.index(after: index)
42 | }
43 | return self.line[index.. ParseResult {
57 | guard !self.shortLineIndent else {
58 | return .none
59 | }
60 | var code: Lines = [self.formatIndentedLine()]
61 | var emptyLines: Lines = []
62 | self.readNextLine()
63 | while !self.finished && self.lineEmpty {
64 | self.readNextLine()
65 | }
66 | while !self.finished && (!self.shortLineIndent || self.lineEmpty) {
67 | if self.lineEmpty {
68 | emptyLines.append(self.formatIndentedLine())
69 | } else {
70 | if emptyLines.count > 0 {
71 | code.append(contentsOf: emptyLines)
72 | emptyLines.removeAll()
73 | }
74 | code.append(self.formatIndentedLine())
75 | }
76 | self.readNextLine()
77 | }
78 | return .block(.indentedCode(code))
79 | }
80 | }
81 |
82 | ///
83 | /// A code block parser which parses fenced code blocks returning `fencedCode` blocks.
84 | ///
85 | public final class FencedCodeBlockParser: CodeBlockParser {
86 |
87 | public override func parse() -> ParseResult {
88 | guard self.shortLineIndent else {
89 | return .none
90 | }
91 | let fenceChar = self.line[self.contentStartIndex]
92 | guard fenceChar == "`" || fenceChar == "~" else {
93 | return .none
94 | }
95 | let fenceIndent = self.lineIndent
96 | var fenceLength = 1
97 | var index = self.line.index(after: self.contentStartIndex)
98 | while index < self.contentEndIndex && self.line[index] == fenceChar {
99 | fenceLength += 1
100 | index = self.line.index(after: index)
101 | }
102 | guard fenceLength >= 3 else {
103 | return .none
104 | }
105 | let info = self.line[index..= fenceLength {
121 | while index < self.contentEndIndex && isUnicodeWhitespace(self.line[index]) {
122 | index = self.line.index(after: index)
123 | }
124 | if index == self.contentEndIndex {
125 | break
126 | }
127 | }
128 | }
129 | code.append(self.formatIndentedLine(fenceIndent))
130 | self.readNextLine()
131 | }
132 | self.readNextLine()
133 | return .block(.fencedCode(info.isEmpty ? nil : info, code))
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/CodeLinkHtmlTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodeLinkHtmlTransformer.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 09/06/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// An inline transformer which extracts code spans, auto-links and html tags and transforms
25 | /// them into `code`, `autolinks`, and `html` text fragments.
26 | ///
27 | open class CodeLinkHtmlTransformer: InlineTransformer {
28 |
29 | public override func transform(_ text: Text) -> Text {
30 | var res: Text = Text()
31 | var iterator = text.makeIterator()
32 | var element = iterator.next()
33 | loop: while let fragment = element {
34 | switch fragment {
35 | case .delimiter("`", let n, []):
36 | var scanner = iterator
37 | var next = scanner.next()
38 | var count = 0
39 | while let lookahead = next {
40 | count += 1
41 | switch lookahead {
42 | case .delimiter("`", n, _):
43 | var scanner2 = iterator
44 | var code = ""
45 | for _ in 1..", n, _):
70 | var scanner2 = iterator
71 | var content = ""
72 | for _ in 1.. Block {
40 | return .document(docParser.bundle(blocks: self.content))
41 | }
42 |
43 | internal func parseIndent(input: String,
44 | startIndex: String.Index,
45 | endIndex: String.Index) -> (String.Index, Container) {
46 | return (startIndex, self)
47 | }
48 |
49 | internal func outermostIndentRequired(upto: Container) -> Container? {
50 | return nil
51 | }
52 |
53 | internal func `return`(to container: Container? = nil, for: DocumentParser) -> Container {
54 | return self
55 | }
56 |
57 | open var debugDescription: String {
58 | return "doc"
59 | }
60 | }
61 |
62 | ///
63 | /// A `NestedContainer` represents a container that has an "outer" container.
64 | ///
65 | open class NestedContainer: Container {
66 | internal let outer: Container
67 |
68 | public init(outer: Container) {
69 | self.outer = outer
70 | }
71 |
72 | open var indentRequired: Bool {
73 | return false
74 | }
75 |
76 | open func skipIndent(input: String,
77 | startIndex: String.Index,
78 | endIndex: String.Index) -> String.Index? {
79 | return startIndex
80 | }
81 |
82 | open override func makeBlock(_ docParser: DocumentParser) -> Block {
83 | preconditionFailure("makeBlock() not defined")
84 | }
85 |
86 | internal final override func parseIndent(input: String,
87 | startIndex: String.Index,
88 | endIndex: String.Index) -> (String.Index, Container) {
89 | let (index, container) = self.outer.parseIndent(input: input,
90 | startIndex: startIndex,
91 | endIndex: endIndex)
92 | guard container === self.outer else {
93 | return (index, container)
94 | }
95 | guard let res = self.skipIndent(input: input, startIndex: index, endIndex: endIndex) else {
96 | return (index, self.outer)
97 | }
98 | return (res, self)
99 | }
100 |
101 | internal final override func outermostIndentRequired(upto container: Container) -> Container? {
102 | if self === container {
103 | return nil
104 | } else if self.indentRequired {
105 | return self.outer.outermostIndentRequired(upto: container) ?? self.outer
106 | } else {
107 | return self.outer.outermostIndentRequired(upto: container)
108 | }
109 | }
110 |
111 | internal final override func `return`(to container: Container? = nil,
112 | for docParser: DocumentParser) -> Container {
113 | if self === container {
114 | return self
115 | } else {
116 | self.outer.append(block: self.makeBlock(docParser), tight: self.density?.isTight ?? true)
117 | return self.outer.return(to: container, for: docParser)
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/DelimiterTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DelimiterTransformer.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 09/06/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// An inline transformer which extracts delimiters into `delimiter` text fragments.
25 | ///
26 | open class DelimiterTransformer: InlineTransformer {
27 |
28 | /// Default emphasis characters
29 | open class var emphasisChars: [Character] {
30 | return ["*", "_"]
31 | }
32 |
33 | var emphasisCharSet: Set = []
34 |
35 | required public init(owner: InlineParser) {
36 | super.init(owner: owner)
37 | for ch in type(of: self).emphasisChars {
38 | self.emphasisCharSet.insert(ch)
39 | }
40 | }
41 |
42 | public override func transform(_ fragment: TextFragment,
43 | from iterator: inout Text.Iterator,
44 | into res: inout Text) -> TextFragment? {
45 | guard case .text(let str) = fragment else {
46 | return super.transform(fragment, from: &iterator, into: &res)
47 | }
48 | var i = str.startIndex
49 | var start = i
50 | var escape = false
51 | var split = false
52 | while i < str.endIndex {
53 | switch str[i] {
54 | case "`":
55 | var n = 1
56 | var j = str.index(after: i)
57 | while j < str.endIndex && str[j] == "`" {
58 | j = str.index(after: j)
59 | n += 1
60 | }
61 | if start < i {
62 | res.append(fragment: .text(str[start..", "[", "]", "(", ")", "\"", "'":
70 | if !escape {
71 | if start < i {
72 | res.append(fragment: .text(str[start.. str.startIndex) {
113 | if start < i {
114 | res.append(fragment: .text(str[start..= str.endIndex ||
133 | isUnicodeWhitespace(str[j]) ||
134 | isUnicodePunctuation(str[j])) {
135 | delimiterRunType.formUnion(.rightFlanking)
136 | if isUnicodePunctuation(str[h]) {
137 | delimiterRunType.formUnion(.leftPunctuation)
138 | }
139 | if j < str.endIndex && isUnicodePunctuation(str[j]) {
140 | delimiterRunType.formUnion(.rightPunctuation)
141 | }
142 | }
143 | } else if j < str.endIndex && !isUnicodeWhitespace(str[j]) {
144 | delimiterRunType.formUnion(.leftFlanking)
145 | }
146 | res.append(fragment: .delimiter(str[i], n,delimiterRunType))
147 | split = true
148 | start = j
149 | i = j
150 | } else {
151 | i = str.index(after: i)
152 | escape = false
153 | }
154 | } else {
155 | i = str.index(after: i)
156 | escape = false
157 | }
158 | }
159 | }
160 | if split {
161 | if start < str.endIndex {
162 | res.append(fragment: .text(str[start...]))
163 | }
164 | } else {
165 | res.append(fragment: fragment)
166 | }
167 | return iterator.next()
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/EmphasisTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmphasisTransformer.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 16/06/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// An inline transformer which extracts emphasis markup and transforms it into `emph` and
25 | /// `strong` text fragments.
26 | ///
27 | open class EmphasisTransformer: InlineTransformer {
28 |
29 | /// Plugin specifying the type of emphasis. `ch` refers to the emphasis character,
30 | /// `special` to whether the charater is used for other use cases (e.g. "*" and "-" should
31 | /// be marked as "special"), and `factory` to a closure constructing the text fragment
32 | /// from two parameters: the first denoting whether it's double usage, and the second
33 | /// referring to the emphasized text.
34 | public struct Emphasis {
35 | public let ch: Character
36 | public let special: Bool
37 | public let factory: (Bool, Text) -> TextFragment
38 |
39 | public init(ch: Character, special: Bool, factory: @escaping (Bool, Text) -> TextFragment) {
40 | self.ch = ch
41 | self.special = special
42 | self.factory = factory
43 | }
44 | }
45 |
46 | /// Emphasis supported by default. Override this property to change what is supported.
47 | open class var supportedEmphasis: [Emphasis] {
48 | let factory = { (double: Bool, text: Text) -> TextFragment in
49 | double ? .strong(text) : .emph(text)
50 | }
51 | return [Emphasis(ch: "*", special: true, factory: factory),
52 | Emphasis(ch: "_", special: false, factory: factory)]
53 | }
54 |
55 | /// The emphasis map, used internally to determine how characters are used for emphasis
56 | /// markup.
57 | private var emphasis: [Character : Emphasis] = [:]
58 |
59 | required public init(owner: InlineParser) {
60 | super.init(owner: owner)
61 | for emph in type(of: self).supportedEmphasis {
62 | self.emphasis[emph.ch] = emph
63 | }
64 | }
65 |
66 | private struct Delimiter: CustomStringConvertible {
67 | let ch: Character
68 | let special: Bool
69 | let runType: DelimiterRunType
70 | var count: Int
71 | var index: Int
72 |
73 | init(_ ch: Character, _ special: Bool, _ rtype: DelimiterRunType, _ count: Int, _ index: Int) {
74 | self.ch = ch
75 | self.special = special
76 | self.runType = rtype
77 | self.count = count
78 | self.index = index
79 | }
80 |
81 | var isOpener: Bool {
82 | return self.runType.contains(.leftFlanking) &&
83 | (self.special ||
84 | !self.runType.contains(.rightFlanking) ||
85 | self.runType.contains(.leftPunctuation))
86 | }
87 |
88 | var isCloser: Bool {
89 | return self.runType.contains(.rightFlanking) &&
90 | (self.special ||
91 | !self.runType.contains(.leftFlanking) ||
92 | self.runType.contains(.rightPunctuation))
93 | }
94 |
95 | var countMultipleOf3: Bool {
96 | return self.count % 3 == 0
97 | }
98 |
99 | func isOpener(for ch: Character) -> Bool {
100 | return self.ch == ch && self.isOpener
101 | }
102 |
103 | func isCloser(for ch: Character) -> Bool {
104 | return self.ch == ch && self.isCloser
105 | }
106 |
107 | var description: String {
108 | return "Delimiter(\(self.ch), \(self.special), \(self.runType), \(self.count), \(self.index))"
109 | }
110 | }
111 |
112 | private typealias DelimiterStack = [Delimiter]
113 |
114 | public override func transform(_ text: Text) -> Text {
115 | // Compute delimiter stack
116 | var res: Text = Text()
117 | var iterator = text.makeIterator()
118 | var element = iterator.next()
119 | var delimiters = DelimiterStack()
120 | while let fragment = element {
121 | switch fragment {
122 | case .delimiter(let ch, let n, let type):
123 | delimiters.append(Delimiter(ch, self.emphasis[ch]?.special ?? false, type, n, res.count))
124 | res.append(fragment: fragment)
125 | element = iterator.next()
126 | default:
127 | element = self.transform(fragment, from: &iterator, into: &res)
128 | }
129 | }
130 | self.processEmphasis(&res, &delimiters)
131 | return res
132 | }
133 |
134 | private func isSupportedEmphasisCloser(_ delimiter: Delimiter) -> Bool {
135 | for ch in self.emphasis.keys {
136 | if delimiter.isCloser(for: ch) {
137 | return true
138 | }
139 | }
140 | return false
141 | }
142 |
143 | private func processEmphasis(_ res: inout Text, _ delimiters: inout DelimiterStack) {
144 | var currentPos = 0
145 | loop: while currentPos < delimiters.count {
146 | var potentialCloser = delimiters[currentPos]
147 | if self.isSupportedEmphasisCloser(potentialCloser) {
148 | var i = currentPos - 1
149 | while i >= 0 {
150 | var potentialOpener = delimiters[i]
151 | if potentialOpener.isOpener(for: potentialCloser.ch) &&
152 | ((!potentialCloser.isOpener && !potentialOpener.isCloser) ||
153 | (potentialCloser.countMultipleOf3 && potentialOpener.countMultipleOf3) ||
154 | ((potentialOpener.count + potentialCloser.count) % 3 != 0)) {
155 | // Deduct counts
156 | let delta = potentialOpener.count > 1 && potentialCloser.count > 1 ? 2 : 1
157 | delimiters[i].count -= delta
158 | delimiters[currentPos].count -= delta
159 | potentialOpener = delimiters[i]
160 | potentialCloser = delimiters[currentPos]
161 | // Collect fragments
162 | var nestedText = Text()
163 | for fragment in res[potentialOpener.index+1.. 0 {
169 | range.append(.delimiter(potentialOpener.ch,
170 | potentialOpener.count,
171 | potentialOpener.runType))
172 | }
173 | if let factory = self.emphasis[potentialOpener.ch]?.factory {
174 | range.append(factory(delta > 1, nestedText))
175 | } else {
176 | for fragment in nestedText {
177 | range.append(fragment)
178 | }
179 | }
180 | if potentialCloser.count > 0 {
181 | range.append(.delimiter(potentialCloser.ch,
182 | potentialCloser.count,
183 | potentialCloser.runType))
184 | }
185 | let shift = range.count - potentialCloser.index + potentialOpener.index - 1
186 | res.replace(from: potentialOpener.index, to: potentialCloser.index, with: range)
187 | // Update delimiter stack
188 | if potentialCloser.count == 0 {
189 | delimiters.remove(at: currentPos)
190 | }
191 | if potentialOpener.count == 0 {
192 | delimiters.remove(at: i)
193 | currentPos -= 1
194 | } else {
195 | i += 1
196 | }
197 | var j = i
198 | while j < currentPos {
199 | delimiters.remove(at: i)
200 | j += 1
201 | }
202 | currentPos = i
203 | while i < delimiters.count {
204 | delimiters[i].index += shift
205 | i += 1
206 | }
207 | continue loop
208 | }
209 | i -= 1
210 | }
211 | if !potentialCloser.isOpener {
212 | delimiters.remove(at: currentPos)
213 | continue loop
214 | }
215 | }
216 | currentPos += 1
217 | }
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/EscapeTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EscapeTransformer.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 18/10/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// An inline transformer which removes backslash escapes.
25 | ///
26 | open class EscapeTransformer: InlineTransformer {
27 |
28 | public override func transform(_ fragment: TextFragment,
29 | from iterator: inout Text.Iterator,
30 | into res: inout Text) -> TextFragment? {
31 | switch fragment {
32 | case .text(let str):
33 | res.append(fragment: .text(self.resolveEscapes(str)))
34 | case .link(let inner, let uri, let title):
35 | res.append(fragment: .link(self.transform(inner), uri, self.resolveEscapes(title)))
36 | case .image(let inner, let uri, let title):
37 | res.append(fragment: .image(self.transform(inner), uri, self.resolveEscapes(title)))
38 | default:
39 | return super.transform(fragment, from: &iterator, into: &res)
40 | }
41 | return iterator.next()
42 | }
43 |
44 | private func resolveEscapes(_ str: String?) -> String? {
45 | if let str = str {
46 | return String(self.resolveEscapes(Substring(str)))
47 | } else {
48 | return nil
49 | }
50 | }
51 |
52 | private func resolveEscapes(_ str: Substring) -> Substring {
53 | guard !str.isEmpty else {
54 | return str
55 | }
56 | var res: String? = nil
57 | var i = str.startIndex
58 | while i < str.endIndex {
59 | if str[i] == "\\" {
60 | if res == nil {
61 | res = String(str[str.startIndex.. Blocks {
32 | // First, bundle lists as previously
33 | let bundled = super.bundle(blocks: blocks)
34 | if bundled.count < 2 {
35 | return bundled
36 | }
37 | // Next, bundle lists of descriptions with their corresponding items into definition lists
38 | var res: Blocks = []
39 | var definitions: Definitions = []
40 | var i = 1
41 | while i < bundled.count {
42 | guard case .paragraph(let text) = bundled[i - 1],
43 | case .list(_, _, let listItems) = bundled[i],
44 | case .some(.listItem(.bullet(":"), _, _)) = listItems.first else {
45 | if definitions.count > 0 {
46 | res.append(.definitionList(definitions))
47 | definitions.removeAll()
48 | }
49 | res.append(bundled[i - 1])
50 | i += 1
51 | continue
52 | }
53 | definitions.append(Definition(item: text, descriptions: listItems))
54 | i += 2
55 | }
56 | if definitions.count > 0 {
57 | res.append(.definitionList(definitions))
58 | definitions.removeAll()
59 | }
60 | if i < bundled.count + 1 {
61 | res.append(bundled[i - 1])
62 | }
63 | return res
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/ExtendedListItemParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtendedListItemParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 19/07/2019.
6 | // Copyright © 2020 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// A block parser for parsing list items. There are two types of list items:
25 | /// _bullet list items_ and _ordered list items_. They are represented using `listItem` blocks
26 | /// using either the `bullet` or the `ordered list type`. `ExtendedListItemParser` also
27 | /// accepts ":" as a bullet. This is used in definition lists.
28 | ///
29 | open class ExtendedListItemParser: ListItemParser {
30 |
31 | public required init(docParser: DocumentParser) {
32 | super.init(docParser: docParser, bulletChars: ["-", "+", "*", ":"])
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/ExtendedMarkdownParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtendedMarkdownParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 17/07/2020.
6 | // Copyright © 2020 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// `ExtendedMarkdownParser` objects are used to parse Markdown text represented as a string
25 | /// using all extensions to the CommonMark specification implemented by MarkdownKit.
26 | ///
27 | /// The `ExtendedMarkdownParser` object itself defines the configuration of the parser.
28 | /// It is stateless in the sense that it can be used for parsing many input strings. This
29 | /// is done via the `parse` function. `parse` returns an abstract syntac tree representing
30 | /// the Markdown text for the given input string.
31 | ///
32 | /// The `parse` method of the `ExtendedMarkdownParser` object delegates parsing of the input
33 | /// string to two types of processors: a `BlockParser` object and an `InlineTransformer`
34 | /// object. A `BlockParser` parses the Markdown block structure returning an abstract
35 | /// syntax tree ignoring inline markup. An `InlineTransformer` object is used to parse
36 | /// a particular type of inline markup within text of Markdown blocks, replacing the
37 | /// matching text with an abstract syntax tree representing the markup.
38 | ///
39 | /// The `parse` method of `ExtendedMarkdownParser` operates in two phases: in the first
40 | /// phase, the block structure of an input string is identified via the `BlockParser`s.
41 | /// In the second phase, the block structure gets traversed and markup within raw text
42 | /// gets replaced with a structured representation.
43 | ///
44 | open class ExtendedMarkdownParser: MarkdownParser {
45 |
46 | /// The default list of block parsers. The order of this list matters.
47 | override open class var defaultBlockParsers: [BlockParser.Type] {
48 | return self.blockParsers
49 | }
50 |
51 | private static let blockParsers: [BlockParser.Type] = MarkdownParser.headingParsers + [
52 | IndentedCodeBlockParser.self,
53 | FencedCodeBlockParser.self,
54 | HtmlBlockParser.self,
55 | LinkRefDefinitionParser.self,
56 | BlockquoteParser.self,
57 | ExtendedListItemParser.self,
58 | TableParser.self
59 | ]
60 |
61 | /// Defines a default implementation
62 | override open class var standard: ExtendedMarkdownParser {
63 | return self.singleton
64 | }
65 |
66 | private static let singleton: ExtendedMarkdownParser = ExtendedMarkdownParser()
67 |
68 | /// Factory method to customize document parsing in subclasses.
69 | open override func documentParser(blockParsers: [BlockParser.Type],
70 | input: String) -> DocumentParser {
71 | return ExtendedDocumentParser(blockParsers: blockParsers, input: input)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/HtmlBlockParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HtmlBlockParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 12/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// `HtmlBlockParser` is a block parser which parses HTML blocks and returns them in form of
25 | /// `htmlBlock` cases of the `Block` enumeration. `HtmlBlockParser` does that with the help
26 | /// of `HtmlBlockParserPlugin` objects to which it delegates detecting of the various HTML
27 | /// block variants that are supported.
28 | ///
29 | open class HtmlBlockParser: BlockParser {
30 |
31 | /// List of supported HTML block parser plugin types; override this computed property in
32 | /// subclasses of `HtmlBlockParser` to create customized versions.
33 | open class var supportedParsers: [HtmlBlockParserPlugin.Type] {
34 | return [ScriptBlockParserPlugin.self,
35 | CommentBlockParserPlugin.self,
36 | ProcessingInstructionBlockParserPlugin.self,
37 | DeclarationBlockParserPlugin.self,
38 | CdataBlockParserPlugin.self,
39 | HtmlTagBlockParserPlugin.self
40 | ]
41 | }
42 |
43 | /// HTML block parser plugins
44 | private var htmlParsers: [HtmlBlockParserPlugin]
45 |
46 | /// Default initializer
47 | public required init(docParser: DocumentParser) {
48 | self.htmlParsers = []
49 | for parserType in type(of: self).supportedParsers {
50 | self.htmlParsers.append(parserType.init())
51 | }
52 | super.init(docParser: docParser)
53 | }
54 |
55 | open override func parse() -> ParseResult {
56 | guard self.shortLineIndent, self.line[self.contentStartIndex] == "<" else {
57 | return .none
58 | }
59 | var cline = self.line[self.contentStartIndex.. Bool {
96 | switch ch {
97 | case " ", "\t", "\n", "\r", "\r\n", "\u{b}", "\u{c}":
98 | return true
99 | default:
100 | return false
101 | }
102 | }
103 |
104 | open func line(_ line: String,
105 | at: String.Index,
106 | startsWith str: String,
107 | endsWith suffix: String? = nil,
108 | htmlTagSuffix: Bool = true) -> Bool {
109 | var strIndex: String.Index = str.startIndex
110 | var index = at
111 | while strIndex < str.endIndex {
112 | guard index < line.endIndex, line[index] == str[strIndex] else {
113 | return false
114 | }
115 | strIndex = str.index(after: strIndex)
116 | index = line.index(after: index)
117 | }
118 | if htmlTagSuffix {
119 | guard index < line.endIndex else {
120 | return true
121 | }
122 | switch line[index] {
123 | case " ", "\t", "\u{b}", "\u{c}":
124 | return true
125 | case "\n", "\r", "\r\n":
126 | return true
127 | case ">":
128 | return true
129 | default:
130 | if let end = suffix {
131 | strIndex = end.startIndex
132 | while strIndex < end.endIndex {
133 | guard index < line.endIndex, line[index] == end[strIndex] else {
134 | return false
135 | }
136 | strIndex = end.index(after: strIndex)
137 | index = line.index(after: index)
138 | }
139 | return true
140 | }
141 | return false
142 | }
143 | } else {
144 | return true
145 | }
146 | }
147 |
148 | open func startCondition(_ line: String) -> Bool {
149 | return false
150 | }
151 |
152 | open func endCondition(_ line: String) -> Bool {
153 | return false
154 | }
155 |
156 | open var emptyLineTerminator: Bool {
157 | return false
158 | }
159 | }
160 |
161 | public final class ScriptBlockParserPlugin: HtmlBlockParserPlugin {
162 |
163 | public override func startCondition(_ line: String) -> Bool {
164 | return self.line(line, at: line.startIndex, startsWith: "") ||
171 | line.contains("") ||
172 | line.contains("")
173 | }
174 | }
175 |
176 | public final class CommentBlockParserPlugin: HtmlBlockParserPlugin {
177 |
178 | public override func startCondition(_ line: String) -> Bool {
179 | return self.line(line, at: line.startIndex, startsWith: "")
184 | }
185 | }
186 |
187 | public final class ProcessingInstructionBlockParserPlugin: HtmlBlockParserPlugin {
188 |
189 | public override func startCondition(_ line: String) -> Bool {
190 | return self.line(line, at: line.startIndex, startsWith: "", htmlTagSuffix: false)
191 | }
192 |
193 | public override func endCondition(_ line: String) -> Bool {
194 | return line.contains("?>")
195 | }
196 | }
197 |
198 | public final class DeclarationBlockParserPlugin: HtmlBlockParserPlugin {
199 |
200 | public override func startCondition(_ line: String) -> Bool {
201 | var index: String.Index = line.startIndex
202 | guard index < line.endIndex && line[index] == "<" else {
203 | return false
204 | }
205 | index = line.index(after: index)
206 | guard index < line.endIndex && line[index] == "!" else {
207 | return false
208 | }
209 | index = line.index(after: index)
210 | guard index < line.endIndex else {
211 | return false
212 | }
213 | switch line[index] {
214 | case "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
215 | "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z":
216 | return true
217 | default:
218 | return false
219 | }
220 | }
221 |
222 | public override func endCondition(_ line: String) -> Bool {
223 | return line.contains(">")
224 | }
225 | }
226 |
227 | public final class CdataBlockParserPlugin: HtmlBlockParserPlugin {
228 |
229 | public override func startCondition(_ line: String) -> Bool {
230 | return self.line(line, at: line.startIndex, startsWith: " Bool {
234 | return line.contains("]]>")
235 | }
236 | }
237 |
238 | public final class HtmlTagBlockParserPlugin: HtmlBlockParserPlugin {
239 | final let htmlTags = ["address", "article", "aside", "base", "basefont", "blockquote", "body",
240 | "caption", "center", "col", "colgroup", "dd", "details", "dialog", "dir",
241 | "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form",
242 | "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header",
243 | "hr", "html", "iframe", "legend", "li", "link", "main", "menu", "menuitem",
244 | "nav", "noframes", "ol", "optgroup", "option", "p", "param", "section",
245 | "source", "summary", "table", "tbody", "td", "tfoot", "th", "thead",
246 | "title", "tr", "track", "ul"]
247 |
248 | public override func startCondition(_ line: String) -> Bool {
249 | var index = line.startIndex
250 | guard index < line.endIndex && line[index] == "<" else {
251 | return false
252 | }
253 | index = line.index(after: index)
254 | if index < line.endIndex && line[index] == "/" {
255 | index = line.index(after: index)
256 | }
257 | for htmlTag in self.htmlTags {
258 | if self.line(line, at: index, startsWith: htmlTag, endsWith: "/>") {
259 | return true
260 | }
261 | }
262 | return false
263 | }
264 |
265 | public override var emptyLineTerminator: Bool {
266 | return true
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/InlineParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InlineParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 30/05/2019.
6 | // Copyright © 2019-2020 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// An `InlineParser` implements Markdown inline text markup parsing given a list of
25 | /// `InlineTransformer` classes as its configuration. `InlineParser` objects are not
26 | /// stateful and can be reused to parse the inline text of many Markdown blocks.
27 | ///
28 | open class InlineParser {
29 |
30 | /// Sequence of inline transformers which implement the inline parsing functionality.
31 | private var inlineTransformers: [InlineTransformer]
32 |
33 | /// Blocks of input document
34 | private let block: Block
35 |
36 | /// Link reference declarations
37 | public private(set) var linkRefDef: [String : (String, String?)]
38 |
39 | /// Initializer
40 | init(inlineTransformers: [InlineTransformer.Type], input: Block) {
41 | self.block = input
42 | self.linkRefDef = [:]
43 | self.inlineTransformers = []
44 | for transformerType in inlineTransformers {
45 | self.inlineTransformers.append(transformerType.init(owner: self))
46 | }
47 | }
48 |
49 | /// Traverses the input block and applies all inline transformers to all text.
50 | open func parse() -> Block {
51 | // First, collect all link reference definitions
52 | self.collectLinkRefDef(self.block)
53 | // Second, apply inline transformers
54 | return self.parse(self.block)
55 | }
56 |
57 | /// Traverses a Markdown block and enters link reference definitions into `linkRefDef`.
58 | public func collectLinkRefDef(_ block: Block) {
59 | switch block {
60 | case .document(let blocks):
61 | self.collectLinkRefDef(blocks)
62 | case .blockquote(let blocks):
63 | self.collectLinkRefDef(blocks)
64 | case .list(_, _, let blocks):
65 | self.collectLinkRefDef(blocks)
66 | case .listItem(_, _, let blocks):
67 | self.collectLinkRefDef(blocks)
68 | case .referenceDef(let label, let dest, let title):
69 | if title.isEmpty {
70 | let canonical = label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
71 | self.linkRefDef[canonical] = (String(dest), nil)
72 | } else {
73 | var str = ""
74 | for line in title {
75 | str += line.description
76 | }
77 | self.linkRefDef[label] = (String(dest), str)
78 | }
79 | default:
80 | break
81 | }
82 | }
83 |
84 | /// Traverses an array of Markdown blocks and enters link reference definitions
85 | /// into `linkRefDef`.
86 | public func collectLinkRefDef(_ blocks: Blocks) {
87 | for block in blocks {
88 | self.collectLinkRefDef(block)
89 | }
90 | }
91 |
92 | /// Parses a Markdown block and returns a new block in which all inline text markup
93 | /// is represented using `TextFragment` objects.
94 | open func parse(_ block: Block) -> Block {
95 | switch block {
96 | case .document(let blocks):
97 | return .document(self.parse(blocks))
98 | case .blockquote(let blocks):
99 | return .blockquote(self.parse(blocks))
100 | case .list(let start, let tight, let blocks):
101 | return .list(start, tight, self.parse(blocks))
102 | case .listItem(let type, let tight, let blocks):
103 | return .listItem(type, tight, self.parse(blocks))
104 | case .paragraph(let lines):
105 | return .paragraph(self.transform(lines))
106 | case .thematicBreak:
107 | return .thematicBreak
108 | case .heading(let level, let lines):
109 | return .heading(level, self.transform(lines))
110 | case .indentedCode(let lines):
111 | return .indentedCode(lines)
112 | case .fencedCode(let info, let lines):
113 | return .fencedCode(info, lines)
114 | case .htmlBlock(let lines):
115 | return .htmlBlock(lines)
116 | case .referenceDef(let label, let dest, let title):
117 | return .referenceDef(label, dest, title)
118 | case .table(let header, let align, let rows):
119 | return .table(self.transform(header), align, self.transform(rows))
120 | case .definitionList(let defs):
121 | return .definitionList(self.transform(defs))
122 | case .custom(let customBlock):
123 | return customBlock.parse(via: self)
124 | }
125 | }
126 |
127 | /// Parses a sequence of Markdown blocks and returns a new sequence in which all inline
128 | /// text markup is represented using `TextFragment` objects.
129 | public func parse(_ blocks: Blocks) -> Blocks {
130 | var res: Blocks = []
131 | for block in blocks {
132 | res.append(self.parse(block))
133 | }
134 | return res
135 | }
136 |
137 | /// Transforms raw Markdown text and returns a new `Text` object in which all inline markup
138 | /// is represented using `TextFragment` objects.
139 | public func transform(_ text: Text) -> Text {
140 | var res = text
141 | for transformer in self.inlineTransformers {
142 | res = transformer.transform(res)
143 | }
144 | return res
145 | }
146 |
147 | /// Transforms raw Markdown rows and returns a new `Row` object in which all inline markup
148 | /// is represented using `TextFragment` objects.
149 | public func transform(_ row: Row) -> Row {
150 | var res = Row()
151 | for cell in row {
152 | res.append(self.transform(cell))
153 | }
154 | return res
155 | }
156 |
157 | /// Transforms raw Markdown tables and returns a new `Rows` object in which all inline markup
158 | /// is represented using `TextFragment` objects.
159 | public func transform(_ rows: Rows) -> Rows {
160 | var res = Rows()
161 | for row in rows {
162 | res.append(self.transform(row))
163 | }
164 | return res
165 | }
166 |
167 | public func transform(_ defs: Definitions) -> Definitions {
168 | var res = Definitions()
169 | for def in defs {
170 | res.append(Definition(item: self.transform(def.item),
171 | descriptions: self.parse(def.descriptions)))
172 | }
173 | return res
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/InlineTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InlineTransformer.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 01/06/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// Class `InlineTransformer` defines a framework for plugins which transform a given
25 | /// unstructured or semi-structured text with Markdown markup into a structured
26 | /// representation which uses `TextFragment` objects. MarkdownKit implements a separate
27 | /// inline transformer plugin for every class of supported inline markup.
28 | ///
29 | open class InlineTransformer {
30 |
31 | public unowned let owner: InlineParser
32 |
33 | required public init(owner: InlineParser) {
34 | self.owner = owner
35 | }
36 |
37 | open func transform(_ text: Text) -> Text {
38 | var res: Text = Text()
39 | var iterator = text.makeIterator()
40 | var element = iterator.next()
41 | while let fragment = element {
42 | element = self.transform(fragment, from: &iterator, into: &res)
43 | }
44 | return res
45 | }
46 |
47 | open func transform(_ fragment: TextFragment,
48 | from iterator: inout Text.Iterator,
49 | into res: inout Text) -> TextFragment? {
50 | switch fragment {
51 | case .text(_):
52 | res.append(fragment: fragment)
53 | case .code(_):
54 | res.append(fragment: fragment)
55 | case .emph(let inner):
56 | res.append(fragment: .emph(self.transform(inner)))
57 | case .strong(let inner):
58 | res.append(fragment: .strong(self.transform(inner)))
59 | case .link(let inner, let uri, let title):
60 | res.append(fragment: .link(self.transform(inner), uri, title))
61 | case .autolink(_, _):
62 | res.append(fragment: fragment)
63 | case .image(let inner, let uri, let title):
64 | res.append(fragment: .image(self.transform(inner), uri, title))
65 | case .html(_):
66 | res.append(fragment: fragment)
67 | case .delimiter(_, _, _):
68 | res.append(fragment: fragment)
69 | case .softLineBreak, .hardLineBreak:
70 | res.append(fragment: fragment)
71 | case .custom(let customTextFragment):
72 | res.append(fragment: customTextFragment.transform(via: self))
73 | }
74 | return iterator.next()
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/LinkRefDefinitionParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LinkRefDefinitionParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 11/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// A block parser which parses link reference definitions returning `referenceDef` blocks.
25 | ///
26 | open class LinkRefDefinitionParser: RestorableBlockParser {
27 |
28 | public override var mayInterruptParagraph: Bool {
29 | return false
30 | }
31 |
32 | public override func parse() -> BlockParser.ParseResult {
33 | guard self.shortLineIndent && self.line[self.contentStartIndex] == "[" else {
34 | return .none
35 | }
36 | return super.parse()
37 | }
38 |
39 | public override func tryParse() -> ParseResult {
40 | var index = self.contentStartIndex
41 | guard let label = self.parseLabel(index: &index),
42 | index < self.contentEndIndex,
43 | self.line[index] == ":" else {
44 | return .none
45 | }
46 | index = self.line.index(after: index)
47 | guard self.skipSpace(index: &index) != nil else {
48 | return .none
49 | }
50 | let destination: Substring
51 | if self.line[index] == "<" {
52 | var prevBackslash = false
53 | index = self.line.index(after: index)
54 | let destStart = index
55 | while index < self.contentEndIndex && (prevBackslash || self.line[index] != ">") {
56 | guard prevBackslash || self.line[index] != "<" else {
57 | return .none
58 | }
59 | prevBackslash = !prevBackslash && self.line[index] == "\\"
60 | index = self.line.index(after: index)
61 | }
62 | guard index < self.contentEndIndex else {
63 | return .none
64 | }
65 | destination = self.line[destStart..= self.contentEndIndex || isWhitespace(self.line[index]) else {
81 | return .none
82 | }
83 | let onNewLine = self.skipSpace(index: &index)
84 | guard onNewLine != nil else {
85 | return .block(.referenceDef(label, destination, []))
86 | }
87 | let title: Lines
88 | switch self.line[index] {
89 | case "\"":
90 | title = self.parseMultiLine(index: &index, closing: "\"", requireWhitespaces: true)
91 | case "'":
92 | title = self.parseMultiLine(index: &index, closing: "'", requireWhitespaces: true)
93 | case "(":
94 | title = self.parseMultiLine(index: &index, closing: ")", requireWhitespaces: true)
95 | default:
96 | guard index == self.contentStartIndex else {
97 | return .none
98 | }
99 | return .block(.referenceDef(label, destination, []))
100 | }
101 | if title.isEmpty {
102 | if onNewLine == true {
103 | return .block(.referenceDef(label, destination, []))
104 | } else {
105 | return .none
106 | }
107 | }
108 | skipWhitespace(in: self.line, from: &index, to: self.contentEndIndex)
109 | guard index >= self.contentEndIndex else {
110 | return .none
111 | }
112 | self.readNextLine()
113 | return .block(.referenceDef(label, destination, title))
114 | }
115 |
116 | public static func balanced(_ str: Substring) -> Bool {
117 | var open = 0
118 | var index = str.startIndex
119 | var prevBackslash = false
120 | while index < str.endIndex {
121 | switch str[index] {
122 | case "(":
123 | if !prevBackslash {
124 | open += 1
125 | }
126 | case ")":
127 | if !prevBackslash {
128 | open -= 1
129 | guard open >= 0 else {
130 | return false
131 | }
132 | }
133 | case "\\":
134 | prevBackslash = !prevBackslash && str[index] == "\\"
135 | default:
136 | break
137 | }
138 | index = str.index(after: index)
139 | }
140 | return open == 0
141 | }
142 |
143 | private func skipSpace(index: inout Substring.Index) -> Bool? {
144 | var newline = false
145 | skipWhitespace(in: self.line, from: &index, to: self.contentEndIndex)
146 | if index >= self.contentEndIndex {
147 | self.readNextLine()
148 | newline = true
149 | if self.finished {
150 | return nil
151 | }
152 | index = self.contentStartIndex
153 | skipWhitespace(in: self.line, from: &index, to: self.contentEndIndex)
154 | if index >= self.contentEndIndex {
155 | return nil
156 | }
157 | }
158 | return newline
159 | }
160 |
161 | private func parseLabel(index: inout Substring.Index) -> String? {
162 | let labelLines = self.parseMultiLine(index: &index, closing: "]", requireWhitespaces: false)
163 | var res = ""
164 | for line in labelLines {
165 | let components = line.components(separatedBy: .whitespaces)
166 | let newLine = components.filter { !$0.isEmpty }.joined(separator: " ")
167 | if !newLine.isEmpty {
168 | if res.isEmpty {
169 | res = newLine
170 | } else {
171 | res.append(" ")
172 | res.append(newLine)
173 | }
174 | }
175 | }
176 | let length = res.count
177 | return length > 0 && length < 1000 ? res : nil
178 | }
179 |
180 | private func parseMultiLine(index: inout Substring.Index,
181 | closing closeCh: Character,
182 | requireWhitespaces: Bool) -> Lines {
183 | let openCh = self.line[index]
184 | index = self.line.index(after: index)
185 | var start = index
186 | var prevBackslash = false
187 | var res: Lines = []
188 | while !self.finished && !self.lineEmpty {
189 | while index < self.contentEndIndex &&
190 | (prevBackslash || self.line[index] != closeCh) {
191 | if !prevBackslash && self.line[index] == openCh {
192 | return []
193 | }
194 | prevBackslash = !prevBackslash && self.line[index] == "\\"
195 | index = self.line.index(after: index)
196 | }
197 | if index >= self.contentEndIndex {
198 | res.append(self.line[start..= self.contentEndIndex else {
211 | return []
212 | }
213 | }
214 | return res
215 | }
216 | }
217 | return []
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/LinkTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LinkTransformer.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 24/06/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// An inline transformer which extracts link and image link markup and transforms it into
25 | /// `link` and `image` text fragments.
26 | ///
27 | open class LinkTransformer: InlineTransformer {
28 |
29 | public override func transform(_ text: Text) -> Text {
30 | var res = Text()
31 | var iterator = text.makeIterator()
32 | var element = iterator.next()
33 | loop: while let fragment = element {
34 | if case .delimiter("[", _, let type) = fragment {
35 | var scanner = iterator
36 | var next = scanner.next()
37 | var open = 0
38 | var inner = Text()
39 | scan: while let lookahead = next {
40 | switch lookahead {
41 | case .delimiter("]", _, _):
42 | if open == 0 {
43 | if let link = self.complete(link: type.isEmpty, inner, with: &scanner) {
44 | res.append(fragment: link)
45 | iterator = scanner
46 | element = iterator.next()
47 | continue loop
48 | }
49 | break scan
50 | }
51 | open -= 1
52 | case .delimiter("[", _, _):
53 | open += 1
54 | default:
55 | break
56 | }
57 | // if type.isEmpty {
58 | inner.append(fragment: lookahead)
59 | next = scanner.next()
60 | // } else {
61 | // next = self.transform(lookahead, from: &scanner, into: &inner)
62 | // }
63 | }
64 | res.append(fragment: fragment)
65 | element = iterator.next()
66 | } else {
67 | element = self.transform(fragment, from: &iterator, into: &res)
68 | }
69 | }
70 | return res
71 | }
72 |
73 | private func complete(link: Bool,
74 | _ text: Text,
75 | with iterator: inout Text.Iterator) -> TextFragment? {
76 | let initial = iterator
77 | let next = iterator.next()
78 | guard let element = next else {
79 | return nil
80 | }
81 | switch element {
82 | case .delimiter("(", _, _):
83 | if let res = self.completeInline(link: link, text, with: &iterator) {
84 | return res
85 | }
86 | case .delimiter("[", _, _):
87 | if let res = self.completeRef(link: link, text, with: &iterator) {
88 | return res
89 | }
90 | default:
91 | break
92 | }
93 | let components = text.description.components(separatedBy: .whitespacesAndNewlines)
94 | let label = components.filter { !$0.isEmpty }.joined(separator: " ").lowercased()
95 | if label.count < 1000,
96 | let (uri, title) = self.owner.linkRefDef[label] {
97 | let text = self.transform(text)
98 | if link && self.containsLink(text) {
99 | return nil
100 | }
101 | iterator = initial
102 | return link ? .link(text, uri, title) : .image(text, uri, title)
103 | } else {
104 | return nil
105 | }
106 | }
107 |
108 | private func completeInline(link: Bool,
109 | _ text: Text,
110 | with iterator: inout Text.Iterator) -> TextFragment? {
111 | // Skip whitespace
112 | var element = self.skipWhitespace(for: &iterator)
113 | guard let dest = element else {
114 | return nil
115 | }
116 | // Transform link description
117 | let text = self.transform(text)
118 | if link && self.containsLink(text) {
119 | return nil
120 | }
121 | // Parse destination
122 | var destination = ""
123 | choose: switch dest {
124 | // Is this a link destination surrounded by `<` and `>`
125 | case .delimiter("<", _, _):
126 | element = iterator.next()
127 | loop: while let fragment = element {
128 | switch fragment {
129 | case .delimiter(">", _, _):
130 | break loop
131 | case .delimiter("<", _, _):
132 | return nil
133 | case .hardLineBreak, .softLineBreak:
134 | return nil
135 | default:
136 | destination += fragment.rawDescription
137 | }
138 | element = iterator.next()
139 | }
140 | case .html(let str):
141 | if str.contains("\n") {
142 | return nil
143 | }
144 | destination += str
145 | case .autolink(_, let str):
146 | if str.contains("\n") {
147 | return nil
148 | }
149 | destination += str
150 | // Parsing regular destinations
151 | default:
152 | var open = 0
153 | if case .some(.text(let str)) = element,
154 | let index = str.firstIndex(where: { ch in !isAsciiWhitespaceOrControl(ch) }),
155 | index < str.endIndex {
156 | destination += str[index.. String? {
227 | var element = iterator.next()
228 | var title = ""
229 | while let fragment = element {
230 | switch fragment {
231 | case .delimiter(ch, _, _):
232 | return title
233 | default:
234 | title += fragment.description
235 | }
236 | element = iterator.next()
237 | }
238 | return nil
239 | }
240 |
241 | private func skipWhitespace(for iterator: inout Text.Iterator) -> TextFragment? {
242 | var element = iterator.next()
243 | while let fragment = element {
244 | switch fragment {
245 | case .hardLineBreak, .softLineBreak:
246 | break
247 | case .text(let str) where isWhitespaceString(str):
248 | break
249 | default:
250 | return element
251 | }
252 | element = iterator.next()
253 | }
254 | return nil
255 | }
256 |
257 | private func containsLink(_ text: Text) -> Bool {
258 | for fragment in text {
259 | switch fragment {
260 | case .emph(let inner):
261 | if self.containsLink(inner) {
262 | return true
263 | }
264 | case .strong(let inner):
265 | if self.containsLink(inner) {
266 | return true
267 | }
268 | case .link(_, _, _):
269 | return true
270 | case .autolink(_, _):
271 | return true
272 | case .image(let inner, _, _):
273 | if self.containsLink(inner) {
274 | return true
275 | }
276 | default:
277 | break
278 | }
279 | }
280 | return false
281 | }
282 |
283 | private func completeRef(link: Bool,
284 | _ text: Text,
285 | with iterator: inout Text.Iterator) -> TextFragment? {
286 | // Skip whitespace
287 | var element = self.skipWhitespace(for: &iterator)
288 | // Transform link description
289 | let text = self.transform(text)
290 | if link && self.containsLink(text) {
291 | return nil
292 | }
293 | // Parse label
294 | var label = ""
295 | while let fragment = element {
296 | switch fragment {
297 | case .delimiter("]", _, _):
298 | label = label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
299 | if let (uri, title) = self.owner.linkRefDef[label] {
300 | return link ? .link(text, uri, title) : .image(text, uri, title)
301 | } else {
302 | return nil
303 | }
304 | case .softLineBreak, .hardLineBreak:
305 | label.append(" ")
306 | default:
307 | let components = fragment.description.components(separatedBy: .whitespaces)
308 | label.append(components.filter { !$0.isEmpty }.joined(separator: " "))
309 | if label.count > 999 {
310 | return nil
311 | }
312 | }
313 | element = iterator.next()
314 | }
315 | return nil
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/ListItemParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListItemParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 05/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// A block parser for parsing list items. There are two types of list items:
25 | /// _bullet list items_ and _ordered list items_. They are represented using `listItem` blocks
26 | /// using either the `bullet` or the `ordered list type.
27 | ///
28 | open class ListItemParser: BlockParser {
29 |
30 | /// Set of supported bullet characters.
31 | private let bulletChars: Set
32 |
33 | /// Used for extending `ListItemParser`
34 | public init(docParser: DocumentParser, bulletChars: Set) {
35 | self.bulletChars = bulletChars
36 | super.init(docParser: docParser)
37 | }
38 |
39 | public required init(docParser: DocumentParser) {
40 | self.bulletChars = ["-", "+", "*"]
41 | super.init(docParser: docParser)
42 | }
43 |
44 | private class BulletListItemContainer: NestedContainer {
45 | let bullet: Character
46 | let indent: Int
47 |
48 | init(bullet: Character, tight: Bool, indent: Int, outer: Container) {
49 | self.bullet = bullet
50 | self.indent = indent
51 | super.init(outer: outer)
52 | self.density = .init(tight: tight)
53 | }
54 |
55 | public override func skipIndent(input: String,
56 | startIndex: String.Index,
57 | endIndex: String.Index) -> String.Index? {
58 | var index = startIndex
59 | var indent = 0
60 | loop: while index < endIndex && indent < self.indent {
61 | switch input[index] {
62 | case " ":
63 | indent += 1
64 | case "\t":
65 | indent += 4
66 | default:
67 | break loop
68 | }
69 | index = input.index(after: index)
70 | }
71 | guard index <= endIndex && indent >= self.indent else {
72 | return nil
73 | }
74 | return index
75 | }
76 |
77 | public override func makeBlock(_ docParser: DocumentParser) -> Block {
78 | return .listItem(.bullet(self.bullet), self.density ?? .tight, docParser.bundle(blocks: self.content))
79 | }
80 |
81 | public override var debugDescription: String {
82 | return self.outer.debugDescription + " <- bulletListItem(\(self.bullet))"
83 | }
84 | }
85 |
86 | private final class OrderedListItemContainer: BulletListItemContainer {
87 | let number: Int
88 |
89 | init(number: Int, delimiter: Character, tight: Bool, indent: Int, outer: Container) {
90 | self.number = number
91 | super.init(bullet: delimiter, tight: tight, indent: indent, outer: outer)
92 | }
93 |
94 | public override func makeBlock(_ docParser: DocumentParser) -> Block {
95 | return .listItem(.ordered(self.number, self.bullet),
96 | self.density ?? .tight,
97 | docParser.bundle(blocks: self.content))
98 | }
99 |
100 | public override var debugDescription: String {
101 | return self.outer.debugDescription + " <- orderedListItem(\(self.number), \(self.bullet))"
102 | }
103 | }
104 |
105 | public override func parse() -> ParseResult {
106 | guard self.shortLineIndent else {
107 | return .none
108 | }
109 | var i = self.contentStartIndex
110 | var listMarkerIndent = 0
111 | var marker: Character = self.line[i]
112 | var number: Int? = nil
113 | switch marker {
114 | case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
115 | var n = self.line[i].wholeNumberValue!
116 | i = self.line.index(after: i)
117 | listMarkerIndent += 1
118 | numloop: while i < self.contentEndIndex && listMarkerIndent < 8 {
119 | switch self.line[i] {
120 | case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
121 | n = n * 10 + self.line[i].wholeNumberValue!
122 | default:
123 | break numloop
124 | }
125 | i = self.line.index(after: i)
126 | listMarkerIndent += 1
127 | }
128 | guard i < self.contentEndIndex else {
129 | return .none
130 | }
131 | number = n
132 | marker = self.line[i]
133 | switch marker {
134 | case ".", ")":
135 | break
136 | default:
137 | return .none
138 | }
139 | default:
140 | if self.bulletChars.contains(marker) {
141 | break
142 | }
143 | return .none
144 | }
145 | i = self.line.index(after: i)
146 | listMarkerIndent += 1
147 | var indent = 0
148 | loop: while i < self.contentEndIndex && indent < 4 {
149 | switch self.line[i] {
150 | case " ":
151 | indent += 1
152 | case "\t":
153 | indent += 4
154 | default:
155 | break loop
156 | }
157 | i = self.line.index(after: i)
158 | }
159 | guard i >= self.contentEndIndex || indent > 0 else {
160 | return .none
161 | }
162 | if indent > 4 {
163 | indent = 1
164 | }
165 | indent += self.lineIndent + listMarkerIndent
166 | self.docParser.resetLineStart(i)
167 | let tight = !self.prevLineEmpty
168 | if let number = number {
169 | return .container { encl in
170 | OrderedListItemContainer(number: number,
171 | delimiter: marker,
172 | tight: tight,
173 | indent: indent,
174 | outer: encl)
175 | }
176 | } else {
177 | return .container { encl in
178 | BulletListItemContainer(bullet: marker, tight: tight, indent: indent, outer: encl)
179 | }
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/MarkdownParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkdownParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 03/05/2019.
6 | // Copyright © 2019-2020 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// `MarkdownParser` objects are used to parse Markdown text represented as a string.
25 | /// The `MarkdownParser` object itself defines the configuration of the parser. It is
26 | /// stateless in the sense that it can be used for parsing many input strings. This is
27 | /// done via the `parse` function. `parse` returns an abstract syntac tree representing
28 | /// the Markdown text for the given input string.
29 | ///
30 | /// The `parse` method of the `MarkdownParser` object delegates parsing of the input
31 | /// string to two types of processors: a `BlockParser` object and an `InlineTransformer`
32 | /// object. A `BlockParser` parses the Markdown block structure returning an abstract
33 | /// syntax tree ignoring inline markup. An `InlineTransformer` object is used to parse
34 | /// a particular type of inline markup within text of Markdown blocks, replacing the
35 | /// matching text with an abstract syntax tree representing the markup.
36 | ///
37 | /// The `parse` method of `MarkdownParser` operates in two phases: in the first phase,
38 | /// the block structure of an input string is identified via the `BlockParser`s. In the
39 | /// second phase, the block structure gets traversed and markup within raw text gets
40 | /// replaced with a structured representation.
41 | ///
42 | open class MarkdownParser {
43 |
44 | /// The default list of block parsers. The order of this list matters.
45 | open class var defaultBlockParsers: [BlockParser.Type] {
46 | return self.blockParsers
47 | }
48 |
49 | private static let blockParsers: [BlockParser.Type] = MarkdownParser.headingParsers + [
50 | IndentedCodeBlockParser.self,
51 | FencedCodeBlockParser.self,
52 | HtmlBlockParser.self,
53 | LinkRefDefinitionParser.self,
54 | BlockquoteParser.self,
55 | ListItemParser.self
56 | ]
57 |
58 | public static let headingParsers: [BlockParser.Type] = [
59 | AtxHeadingParser.self,
60 | SetextHeadingParser.self,
61 | ThematicBreakParser.self
62 | ]
63 |
64 | /// The default list of inline transformers. The order of this list matters.
65 | open class var defaultInlineTransformers: [InlineTransformer.Type] {
66 | return self.inlineTransformers
67 | }
68 |
69 | private static let inlineTransformers: [InlineTransformer.Type] = [
70 | DelimiterTransformer.self,
71 | CodeLinkHtmlTransformer.self,
72 | LinkTransformer.self,
73 | EmphasisTransformer.self,
74 | EscapeTransformer.self
75 | ]
76 |
77 | /// Defines a default implementation
78 | open class var standard: MarkdownParser {
79 | return self.singleton
80 | }
81 |
82 | private static let singleton: MarkdownParser = MarkdownParser()
83 |
84 | /// A custom list of block parsers; if this is provided via the constructor, it overrides
85 | /// the `defaultBlockParsers`.
86 | private let customBlockParsers: [BlockParser.Type]?
87 |
88 | /// A custom list of inline transformers; if this is provided via the constructor, it overrides
89 | /// the `defaultInlineTransformers`.
90 | private let customInlineTransformers: [InlineTransformer.Type]?
91 |
92 | /// Block parsing gets delegated to a stateful `DocumentParser` object which implements a
93 | /// protocol for invoking the `BlockParser` objects that its initializer is creating based
94 | /// on the types provided in the `blockParsers` parameter.
95 | public func documentParser(input: String) -> DocumentParser {
96 | return self.documentParser(blockParsers: self.customBlockParsers ??
97 | type(of: self).defaultBlockParsers,
98 | input: input)
99 | }
100 |
101 | /// Factory method to customize document parsing in subclasses.
102 | open func documentParser(blockParsers: [BlockParser.Type], input: String) -> DocumentParser {
103 | return DocumentParser(blockParsers: blockParsers, input: input)
104 | }
105 |
106 | /// Inline parsing is performed via a stateless `InlineParser` object which implements a
107 | /// protocol for invoking the `InlineTransformer` objects. Since the inline parser is stateless,
108 | /// a single object gets created lazily and reused for parsing all input.
109 | public func inlineParser(input: Block) -> InlineParser {
110 | return self.inlineParser(inlineTransformers: self.customInlineTransformers ??
111 | type(of: self).defaultInlineTransformers,
112 | input: input)
113 | }
114 |
115 | /// Factory method to customize inline parsing in subclasses.
116 | open func inlineParser(inlineTransformers: [InlineTransformer.Type],
117 | input: Block) -> InlineParser {
118 | return InlineParser(inlineTransformers: inlineTransformers, input: input)
119 | }
120 |
121 | /// Constructor of `MarkdownParser` objects; it takes a list of block parsers, a list of
122 | /// inline transformers as well as an input string as its parameters.
123 | public init(blockParsers: [BlockParser.Type]? = nil,
124 | inlineTransformers: [InlineTransformer.Type]? = nil) {
125 | self.customBlockParsers = blockParsers
126 | self.customInlineTransformers = inlineTransformers
127 | }
128 |
129 | /// Invokes the parser and returns an abstract syntx tree of the Markdown syntax.
130 | /// If `blockOnly` is set to `true` (default is `false`), only the block parsers are
131 | /// invoked and no inline parsing gets performed.
132 | public func parse(_ str: String, blockOnly: Bool = false) -> Block {
133 | let doc = self.documentParser(input: str).parse()
134 | if blockOnly {
135 | return doc
136 | } else {
137 | return self.inlineParser(input: doc).parse()
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/ParserUtil.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParserUtil.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 10/06/2019.
6 | // Copyright © 2019-2020 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 |
24 | public func isAsciiWhitespaceOrControl(_ ch: Character) -> Bool {
25 | return isWhitespace(ch) || isControlCharacter(ch)
26 | }
27 |
28 | public func isWhitespace(_ ch: Character) -> Bool {
29 | switch ch {
30 | case " ", "\t", "\n", "\r", "\u{b}", "\u{c}":
31 | return true
32 | default:
33 | return false
34 | }
35 | }
36 |
37 | public func isWhitespaceString(_ str: Substring) -> Bool {
38 | for ch in str {
39 | if !isWhitespace(ch) {
40 | return false
41 | }
42 | }
43 | return true
44 | }
45 |
46 | public func isUnicodeWhitespace(_ ch: Character) -> Bool {
47 | if let scalar = ch.unicodeScalars.first {
48 | return CharacterSet.whitespacesAndNewlines.contains(scalar)
49 | }
50 | return false
51 | }
52 |
53 | public func isSpace(_ ch: Character) -> Bool {
54 | return ch == " "
55 | }
56 |
57 | public func isDash(_ ch: Character) -> Bool {
58 | return ch == "-"
59 | }
60 |
61 | public func isAsciiPunctuation(_ ch: Character) -> Bool {
62 | switch ch {
63 | case "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", ":",
64 | ";", "<", "=", ">", "?", "@", "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~":
65 | return true
66 | default:
67 | return false
68 | }
69 | }
70 |
71 | public func isUppercaseAsciiLetter(_ ch: Character) -> Bool {
72 | switch ch {
73 | case "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
74 | "R", "S", "T", "U", "V", "W", "X", "Y", "Z":
75 | return true
76 | default:
77 | return false
78 | }
79 | }
80 |
81 | public func isAsciiLetter(_ ch: Character) -> Bool {
82 | switch ch {
83 | case "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
84 | "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
85 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q",
86 | "r", "s", "t", "u", "v", "w", "x", "y", "z":
87 | return true
88 | default:
89 | return false
90 | }
91 | }
92 |
93 | public func isAsciiLetterOrDigit(_ ch: Character) -> Bool {
94 | switch ch {
95 | case "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
96 | "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
97 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q",
98 | "r", "s", "t", "u", "v", "w", "x", "y", "z",
99 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
100 | return true
101 | default:
102 | return false
103 | }
104 | }
105 |
106 | public func isControlCharacter(_ ch: Character) -> Bool {
107 | if let scalar = ch.unicodeScalars.first, CharacterSet.controlCharacters.contains(scalar) {
108 | return true
109 | }
110 | return false
111 | }
112 |
113 | public func isUnicodePunctuation(_ ch: Character) -> Bool {
114 | if let scalar = ch.unicodeScalars.first, CharacterSet.punctuationCharacters.contains(scalar) {
115 | return true
116 | }
117 | return isAsciiPunctuation(ch)
118 | }
119 |
120 | public func skipWhitespace(in str: Substring,
121 | from index: inout Substring.Index,
122 | to endIndex: Substring.Index) {
123 | while index < endIndex {
124 | let ch = str[index]
125 | if ch != " " {
126 | guard let scalar = ch.unicodeScalars.first, CharacterSet.whitespaces.contains(scalar) else {
127 | return
128 | }
129 | }
130 | index = str.index(after: index)
131 | }
132 | }
133 |
134 | public func isURI(_ str: String) -> Bool {
135 | var iterator = str.makeIterator()
136 | var next = iterator.next()
137 | guard next != nil, next!.isASCII, next!.isLetter else {
138 | return false
139 | }
140 | next = iterator.next()
141 | var n = 1
142 | while let ch = next {
143 | guard ch.isASCII else {
144 | return false
145 | }
146 | if ch == ":" {
147 | if n > 1 {
148 | break
149 | } else {
150 | return false
151 | }
152 | }
153 | guard ch.isLetter || ch.isHexDigit || ch == "+" || ch == "-" || ch == "." else {
154 | return false
155 | }
156 | next = iterator.next()
157 | n += 1
158 | guard n <= 32 else {
159 | return false
160 | }
161 | }
162 | guard next != nil else {
163 | return false
164 | }
165 | while let ch = iterator.next() {
166 | guard !isWhitespace(ch), !isControlCharacter(ch), ch != "<", ch != ">" else {
167 | return false
168 | }
169 | }
170 | return true
171 | }
172 |
173 | public func isHtmlTag(_ str: String) -> Bool {
174 | var iterator = str.makeIterator()
175 | var next = iterator.next()
176 | guard let ch = next else {
177 | return false
178 | }
179 | switch ch {
180 | case "/":
181 | next = iterator.next()
182 | guard skipTagName(&next, &iterator) else {
183 | return false
184 | }
185 | _ = skipWhitespace(&next, &iterator)
186 | return next == nil
187 | case "?":
188 | return str.count > 1 && str.last! == "?"
189 | case "!":
190 | guard let fst = iterator.next() else {
191 | return false
192 | }
193 | if fst == "-" {
194 | guard let snd = iterator.next(), snd == "-" else {
195 | return false
196 | }
197 | guard str.count > 4,
198 | !str.hasPrefix("!-->"),
199 | !str.hasPrefix("!--->"),
200 | !str.hasSuffix("---") else {
201 | return false
202 | }
203 | return !str[str.index(str.startIndex, offsetBy: 3).." {
216 | next = iterator.next()
217 | }
218 | return next == nil
219 | } else {
220 | return false
221 | }
222 | default:
223 | guard skipTagName(&next, &iterator) else {
224 | return false
225 | }
226 | loop: while skipWhitespace(&next, &iterator), let ch = next, ch != "/", ch != ">" {
227 | var skipped = skipAttribute(&next, &iterator)
228 | while skipped == nil {
229 | if next == nil || next == "/" || next == ">" {
230 | break loop
231 | }
232 | skipped = skipAttribute(&next, &iterator)
233 | }
234 | guard skipped! else {
235 | return false
236 | }
237 | }
238 | if case .some("/") = next {
239 | next = iterator.next()
240 | }
241 | return next == nil
242 | }
243 | }
244 |
245 | fileprivate func skipAttribute(_ next: inout Character?,
246 | _ iterator: inout String.Iterator) -> Bool? {
247 | guard skipAttributeName(&next, &iterator) else {
248 | return false
249 | }
250 | _ = skipWhitespace(&next, &iterator)
251 | guard case .some("=") = next else {
252 | return nil
253 | }
254 | next = iterator.next()
255 | _ = skipWhitespace(&next, &iterator)
256 | guard let fst = next else {
257 | return false
258 | }
259 | next = iterator.next()
260 | switch fst {
261 | case "'":
262 | while let ch = next, ch != "'" {
263 | next = iterator.next()
264 | }
265 | guard next != nil else {
266 | return false
267 | }
268 | next = iterator.next()
269 | case "\"":
270 | while let ch = next, ch != "\"" {
271 | next = iterator.next()
272 | }
273 | guard next != nil else {
274 | return false
275 | }
276 | next = iterator.next()
277 | default:
278 | while let ch = next, !isWhitespace(ch),
279 | ch != "\"", ch != "'", ch != "=", ch != "<", ch != ">", ch != "`" {
280 | next = iterator.next()
281 | }
282 | }
283 | return true
284 | }
285 |
286 | fileprivate func skipAttributeName(_ next: inout Character?,
287 | _ iterator: inout String.Iterator) -> Bool {
288 | guard let fst = next, isAsciiLetter(fst) || fst == "_" || fst == ":" else {
289 | return false
290 | }
291 | next = iterator.next()
292 | while let ch = next {
293 | guard isAsciiLetterOrDigit(ch) || ch == "_" || ch == "-" || ch == "." || ch == ":" else {
294 | return true
295 | }
296 | next = iterator.next()
297 | }
298 | return true
299 | }
300 |
301 | fileprivate func skipTagName(_ next: inout Character?,
302 | _ iterator: inout String.Iterator) -> Bool {
303 | guard let fst = next, isAsciiLetter(fst) else {
304 | return false
305 | }
306 | next = iterator.next()
307 | while let ch = next {
308 | guard isAsciiLetterOrDigit(ch) || ch == "-" else {
309 | return true
310 | }
311 | next = iterator.next()
312 | }
313 | return true
314 | }
315 |
316 | fileprivate func skipWhitespace(_ next: inout Character?,
317 | _ iterator: inout String.Iterator) -> Bool {
318 | guard let fst = next, isWhitespace(fst) else {
319 | return false
320 | }
321 | next = iterator.next()
322 | while let ch = next {
323 | guard isWhitespace(ch) else {
324 | return true
325 | }
326 | next = iterator.next()
327 | }
328 | return true
329 | }
330 |
331 | // Detect email addresses
332 |
333 | fileprivate let emailRegExpr: NSRegularExpression =
334 | try! NSRegularExpression(pattern: #"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9]"#
335 | + #"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]"#
336 | + #"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"#)
337 |
338 | public func isEmailAddress(_ str: String) -> Bool {
339 | return emailRegExpr.firstMatch(in: str,
340 | range: NSRange(location: 0, length: str.utf16.count)) != nil
341 | }
342 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/SetextHeadingParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetextHeadingParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 12/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// A block parser which parses setext headings (headers with text underlining) returning
25 | /// `heading` blocks.
26 | ///
27 | open class SetextHeadingParser: BlockParser {
28 |
29 | public override func parse() -> ParseResult {
30 | guard self.shortLineIndent,
31 | !self.lazyContinuation,
32 | let plines = self.prevParagraphLines,
33 | !plines.isEmpty else {
34 | return .none
35 | }
36 | let ch = self.line[self.contentStartIndex]
37 | let level: Int
38 | switch ch {
39 | case "=":
40 | level = 1
41 | case "-":
42 | level = 2
43 | default:
44 | return .none
45 | }
46 | var i = self.contentStartIndex
47 | while i < self.contentEndIndex && self.line[i] == ch {
48 | i = self.line.index(after: i)
49 | }
50 | skipWhitespace(in: self.line, from: &i, to: self.contentEndIndex)
51 | guard i >= self.contentEndIndex else {
52 | return .none
53 | }
54 | self.consumeParagraphLines()
55 | self.readNextLine()
56 | return .block(.heading(level, plines.finalized()))
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Parser/TableParser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableParser.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 17/07/2020.
6 | // Copyright © 2020 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// A block parser which parses tables returning `table` blocks.
25 | ///
26 | open class TableParser: RestorableBlockParser {
27 |
28 | open override func parse() -> ParseResult {
29 | guard self.shortLineIndent else {
30 | return .none
31 | }
32 | var i = self.contentStartIndex
33 | var prev = Character(" ")
34 | while i < self.contentEndIndex {
35 | if self.line[i] == "|" && prev != "\\" {
36 | return super.parse()
37 | }
38 | prev = self.line[i]
39 | i = self.line.index(after: i)
40 | }
41 | return .none
42 | }
43 |
44 | open override func tryParse() -> ParseResult {
45 | guard let header = self.parseRow() else {
46 | return .none
47 | }
48 | self.readNextLine()
49 | guard let alignrow = self.parseRow(), alignrow.count == header.count else {
50 | return .none
51 | }
52 | var alignments = Alignments()
53 | for cell in alignrow {
54 | guard case .some(.text(let str)) = cell.first, str.count > 0 else {
55 | return .none
56 | }
57 | var check: Substring
58 | if str.first! == ":" {
59 | if str.count > 2 && str.last! == ":" {
60 | alignments.append(.center)
61 | check = str[str.index(after: str.startIndex).. header.count {
83 | row.removeLast(row.count - header.count)
84 | // Append cells if parsed row has too few
85 | } else if row.count < header.count {
86 | for _ in row.count.. Row? {
97 | var i = self.contentStartIndex
98 | skipWhitespace(in: self.line, from: &i, to: self.contentEndIndex)
99 | guard i < self.contentEndIndex else {
100 | return nil
101 | }
102 | var validRow = false
103 | if self.line[i] == "|" {
104 | validRow = true
105 | i = self.line.index(after: i)
106 | skipWhitespace(in: self.line, from: &i, to: self.contentEndIndex)
107 | }
108 | var res = Row()
109 | var text: Text? = nil
110 | while i < self.contentEndIndex {
111 | var j = i
112 | var k = i
113 | var prev = Character(" ")
114 | while j < self.contentEndIndex && (self.line[j] != "|" || prev == "\\") {
115 | prev = self.line[j]
116 | j = self.line.index(after: j)
117 | if prev != " " {
118 | k = j
119 | }
120 | }
121 | if j < self.contentEndIndex {
122 | if text == nil {
123 | res.append(Text(self.line[i.. ParseResult {
29 | guard self.shortLineIndent else {
30 | return .none
31 | }
32 | var i = self.contentStartIndex
33 | let ch = self.line[i]
34 | switch ch {
35 | case "-", "_", "*":
36 | break
37 | default:
38 | return .none
39 | }
40 | var count = 0
41 | while i < self.contentEndIndex {
42 | switch self.line[i] {
43 | case " ", "\t":
44 | break
45 | case ch:
46 | count += 1
47 | default:
48 | return .none
49 | }
50 | i = self.line.index(after: i)
51 | }
52 | guard count >= 3 else {
53 | return .none
54 | }
55 | self.readNextLine()
56 | return .block(.thematicBreak)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/Text.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Text.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 30/05/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// Struct `Text` is used to represent inline text. A `Text` struct consists of a sequence
25 | /// of `TextFragment` objects.
26 | ///
27 | public struct Text: Collection, Equatable, CustomStringConvertible, CustomDebugStringConvertible {
28 | public typealias Index = ContiguousArray.Index
29 | public typealias Iterator = ContiguousArray.Iterator
30 |
31 | private var fragments: ContiguousArray = []
32 |
33 | public init(_ str: Substring? = nil) {
34 | if let str = str {
35 | self.fragments.append(.text(str))
36 | }
37 | }
38 |
39 | public init(_ fragment: TextFragment) {
40 | self.fragments.append(fragment)
41 | }
42 |
43 | /// Returns `true` if the text is empty.
44 | public var isEmpty: Bool {
45 | return self.fragments.isEmpty
46 | }
47 |
48 | /// Returns the first text fragment if available.
49 | public var first: TextFragment? {
50 | return self.fragments.first
51 | }
52 |
53 | /// Returns the last text fragment if available.
54 | public var last: TextFragment? {
55 | return self.fragments.last
56 | }
57 |
58 | /// Appends a line of text, potentially followed by a hard line break
59 | mutating public func append(line: Substring, withHardLineBreak: Bool) {
60 | let n = self.fragments.count
61 | if n > 0, case .text(let str) = self.fragments[n - 1] {
62 | if str.last == "\\" {
63 | let newline = str[str.startIndex.. Iterator {
93 | return self.fragments.makeIterator()
94 | }
95 |
96 | /// Returns the start index.
97 | public var startIndex: Index {
98 | return self.fragments.startIndex
99 | }
100 |
101 | /// Returns the end index.
102 | public var endIndex: Index {
103 | return self.fragments.endIndex
104 | }
105 |
106 | /// Returns the text fragment at the given index.
107 | public subscript (position: Index) -> Iterator.Element {
108 | return self.fragments[position]
109 | }
110 |
111 | /// Advances the given index by one place.
112 | public func index(after i: Index) -> Index {
113 | return self.fragments.index(after: i)
114 | }
115 |
116 | /// Returns a description of this `Text` object as a string as if the text would be
117 | /// represented in Markdown.
118 | public var description: String {
119 | var res = ""
120 | for fragment in self.fragments {
121 | res = res + fragment.description
122 | }
123 | return res
124 | }
125 |
126 | /// Returns a raw description of this `Text` object as a string, i.e. as if the text
127 | /// would be represented in Markdown but ignoring all markup.
128 | public var rawDescription: String {
129 | return self.fragments.map { $0.rawDescription }.joined()
130 | }
131 |
132 | /// Returns a raw description of this `Text` object as a string for which all markup
133 | /// gets ignored.
134 | public var string: String {
135 | return self.fragments.map { $0.string }.joined()
136 | }
137 |
138 | /// Returns a debug description of this `Text` object.
139 | public var debugDescription: String {
140 | var res = ""
141 | for fragment in self.fragments {
142 | if res.isEmpty {
143 | res = fragment.debugDescription
144 | } else {
145 | res = res + ", \(fragment.debugDescription)"
146 | }
147 | }
148 | return res
149 | }
150 |
151 | /// Finalizes the `Text` object by removing trailing line breaks.
152 | public func finalized() -> Text {
153 | if let lastLine = self.fragments.last {
154 | switch lastLine {
155 | case .hardLineBreak, .softLineBreak:
156 | var plines = self
157 | plines.fragments.removeLast()
158 | return plines
159 | default:
160 | return self
161 | }
162 | } else {
163 | return self
164 | }
165 | }
166 |
167 | /// Defines an equality relationship for `Text` objects.
168 | public static func == (lhs: Text, rhs: Text) -> Bool {
169 | return lhs.fragments == rhs.fragments
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/Sources/MarkdownKit/TextFragment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextFragment.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 14/07/2019.
6 | // Copyright © 2019-2021 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | ///
24 | /// In MarkdownKit, text with markup is represented as a sequence of `TextFragment` objects.
25 | /// Each `TextFragment` enumeration variant represents one form of inline markup. Since
26 | /// markup can be arbitrarily nested, this is a recursive data structure (via struct `Text`).
27 | ///
28 | public enum TextFragment: Equatable, CustomStringConvertible, CustomDebugStringConvertible {
29 | case text(Substring)
30 | case code(Substring)
31 | case emph(Text)
32 | case strong(Text)
33 | case link(Text, String?, String?)
34 | case autolink(AutolinkType, Substring)
35 | case image(Text, String?, String?)
36 | case html(Substring)
37 | case delimiter(Character, Int, DelimiterRunType)
38 | case softLineBreak
39 | case hardLineBreak
40 | case custom(CustomTextFragment)
41 |
42 | /// Returns a description of this `TextFragment` object as a string as if the text would be
43 | /// represented in Markdown.
44 | public var description: String {
45 | switch self {
46 | case .text(let str):
47 | return str.description
48 | case .code(let str):
49 | return "`\(str.description)`"
50 | case .emph(let text):
51 | return "*\(text.description)*"
52 | case .strong(let text):
53 | return "**\(text.description)**"
54 | case .link(let text, let uri, let title):
55 | return "[\(text.description)](\(uri?.description ?? "") \(title?.description ?? ""))"
56 | case .autolink(_, let uri):
57 | return "<\(uri.description)>"
58 | case .image(let text, let uri, let title):
59 | return " \(title?.description ?? ""))"
60 | case .html(let tag):
61 | return "<\(tag.description)>"
62 | case .delimiter(let ch, let n, let type):
63 | var res = String(ch)
64 | for _ in 1.."
97 | case .delimiter(let ch, let n, let type):
98 | var res = String(ch)
99 | for _ in 1.. Bool {
163 | switch (lhs, rhs) {
164 | case (.text(let llstr), .text(let rstr)):
165 | return llstr == rstr
166 | case (.code(let lstr), .code(let rstr)):
167 | return lstr == rstr
168 | case (.emph(let ltext), .emph(let rtext)):
169 | return ltext == rtext
170 | case (.strong(let ltext), .strong(let rtext)):
171 | return ltext == rtext
172 | case (.link(let ltext, let ls1, let ls2), .link(let rtext, let rs1, let rs2)):
173 | return ltext == rtext && ls1 == rs1 && ls2 == rs2
174 | case (.autolink(let ltype, let lstr), .autolink(let rtype, let rstr)):
175 | return ltype == rtype && lstr == rstr
176 | case (.image(let ltext, let ls1, let ls2), .image(let rtext, let rs1, let rs2)):
177 | return ltext == rtext && ls1 == rs1 && ls2 == rs2
178 | case (.html(let lstr), .html(let rstr)):
179 | return lstr == rstr
180 | case (.delimiter(let lch, let ln, let ld), .delimiter(let rch, let rn, let rd)):
181 | return lch == rch && ln == rn && ld == rd
182 | case (.softLineBreak, .softLineBreak):
183 | return true
184 | case (.hardLineBreak, .hardLineBreak):
185 | return true
186 | case (.custom(let lctf), .custom(let rctf)):
187 | return lctf.equals(to: rctf)
188 | default:
189 | return false
190 | }
191 | }
192 | }
193 |
194 | ///
195 | /// Represents an autolink type.
196 | ///
197 | public enum AutolinkType: Equatable, CustomStringConvertible, CustomDebugStringConvertible {
198 | case uri
199 | case email
200 |
201 | public var description: String {
202 | switch self {
203 | case .uri:
204 | return "uri"
205 | case .email:
206 | return "email"
207 | }
208 | }
209 |
210 | public var debugDescription: String {
211 | return self.description
212 | }
213 | }
214 |
215 | ///
216 | /// Lines are arrays of substrings.
217 | ///
218 | public typealias Lines = ContiguousArray
219 |
220 | ///
221 | /// Each delimiter run is classified into a set of types which are represented via the
222 | /// `DelimiterRunType` struct.
223 | public struct DelimiterRunType: OptionSet, CustomStringConvertible {
224 | public let rawValue: UInt8
225 |
226 | public init(rawValue: UInt8) {
227 | self.rawValue = rawValue
228 | }
229 |
230 | public static let leftFlanking = DelimiterRunType(rawValue: 1 << 0)
231 | public static let rightFlanking = DelimiterRunType(rawValue: 1 << 1)
232 | public static let leftPunctuation = DelimiterRunType(rawValue: 1 << 2)
233 | public static let rightPunctuation = DelimiterRunType(rawValue: 1 << 3)
234 | public static let escaped = DelimiterRunType(rawValue: 1 << 4)
235 | public static let image = DelimiterRunType(rawValue: 1 << 5)
236 |
237 | public var description: String {
238 | var res = ""
239 | if self.rawValue & 0x1 == 0x1 {
240 | res = "\(res)\(res.isEmpty ? "" : ", ")leftFlanking"
241 | }
242 | if self.rawValue & 0x2 == 0x2 {
243 | res = "\(res)\(res.isEmpty ? "" : ", ")rightFlanking"
244 | }
245 | if self.rawValue & 0x4 == 0x4 {
246 | res = "\(res)\(res.isEmpty ? "" : ", ")leftPunctuation"
247 | }
248 | if self.rawValue & 0x8 == 0x8 {
249 | res = "\(res)\(res.isEmpty ? "" : ", ")rightPunctuation"
250 | }
251 | if self.rawValue & 0x10 == 0x10 {
252 | res = "\(res)\(res.isEmpty ? "" : ", ")escaped"
253 | }
254 | if self.rawValue & 0x20 == 0x20 {
255 | res = "\(res)\(res.isEmpty ? "" : ", ")image"
256 | }
257 | return "[\(res)]"
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/Sources/MarkdownKitProcess/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // MarkdownKitProcess
4 | //
5 | // Created by Matthias Zenger on 01/08/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 | import MarkdownKit
23 |
24 | // This is a command-line tool for converting a text file in Markdown format into
25 | // HTML. The tool also allows converting a whole folder of Markdown files into HTML.
26 |
27 | let fileManager = FileManager.default
28 |
29 | // Utility functions
30 |
31 | func markdownFiles(inDir baseUrl: URL) -> [URL] {
32 | var res: [URL] = []
33 | if let urls = try? fileManager.contentsOfDirectory(
34 | at: baseUrl,
35 | includingPropertiesForKeys: [.nameKey, .isDirectoryKey],
36 | options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) {
37 | for url in urls {
38 | let values = try? url.resourceValues(forKeys: [.isDirectoryKey])
39 | if !(values?.isDirectory ?? false) && url.lastPathComponent.hasSuffix(".md") {
40 | res.append(url)
41 | }
42 | }
43 | }
44 | return res
45 | }
46 |
47 | func baseUrl(for path: String, role: String) -> (URL, Bool) {
48 | var isDir = ObjCBool(false)
49 | guard fileManager.fileExists(atPath: path, isDirectory: &isDir) else {
50 | print("\(role) '\(path)' does not exist")
51 | exit(1)
52 | }
53 | return (URL(fileURLWithPath: path, isDirectory: isDir.boolValue), isDir.boolValue)
54 | }
55 |
56 | // Command-line argument handling
57 |
58 | guard CommandLine.arguments.count > 1 && CommandLine.arguments.count < 4 else {
59 | print("usage: mdkitprocess []")
60 | print("where: is either a Markdown file or a directory containing Markdown files")
61 | print(" is either an HTML file or a directory in which HTML files are written")
62 | exit(0)
63 | }
64 |
65 | var sourceTarget: [(URL, URL?)] = []
66 |
67 | let (sourceBaseUrl, sourceIsDir) = baseUrl(for: CommandLine.arguments[1], role: "source")
68 | if CommandLine.arguments.count == 2 {
69 | let sources = sourceIsDir ? markdownFiles(inDir: sourceBaseUrl) : [sourceBaseUrl]
70 | for source in sources {
71 | let target = source.deletingPathExtension().appendingPathExtension("html")
72 | sourceTarget.append((source, target))
73 | }
74 | } else if CommandLine.arguments[2] == "-" {
75 | guard !sourceIsDir else {
76 | print("cannot print source directory to console")
77 | exit(1)
78 | }
79 | sourceTarget.append((sourceBaseUrl, nil))
80 | } else {
81 | let (targetBaseUrl, targetIsDir) = baseUrl(for: CommandLine.arguments[2], role: "target")
82 | guard sourceIsDir == targetIsDir else {
83 | print("source and target either need to be directories or individual files")
84 | exit(1)
85 | }
86 | if sourceIsDir {
87 | let sources = markdownFiles(inDir: sourceBaseUrl)
88 | for source in sources {
89 | let target = targetBaseUrl.appendingPathComponent(source.lastPathComponent)
90 | .deletingPathExtension()
91 | .appendingPathExtension("html")
92 | sourceTarget.append((source, target))
93 | }
94 | } else {
95 | sourceTarget.append((sourceBaseUrl, targetBaseUrl))
96 | }
97 | }
98 |
99 | // Processing
100 |
101 | for (sourceUrl, optTargetUrl) in sourceTarget {
102 | if let textContent = try? String(contentsOf: sourceUrl) {
103 | let markdownContent = MarkdownParser.standard.parse(textContent)
104 | let htmlContent = HtmlGenerator.standard.generate(doc: markdownContent)
105 | if let targetUrl = optTargetUrl {
106 | if fileManager.fileExists(atPath: targetUrl.path) {
107 | print("cannot overwrite target file '\(targetUrl.path)'")
108 | } else {
109 | do {
110 | try htmlContent.write(to: targetUrl, atomically: false, encoding: .utf8)
111 | print("converted '\(sourceUrl.lastPathComponent)' into '\(targetUrl.lastPathComponent)'")
112 | } catch {
113 | print("cannot write target file '\(targetUrl.path)'")
114 | }
115 | }
116 | } else {
117 | print(htmlContent)
118 | }
119 | } else {
120 | print("cannot read source file '\(sourceUrl.path)'")
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LinuxMain.swift
3 | // MarkdownKit
4 | //
5 | // Created by Matthias Zenger on 14/02/2021.
6 | // Copyright © 2021 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #if os(Linux)
22 |
23 | import XCTest
24 | @testable import MarkdownKitTests
25 |
26 | XCTMain(
27 | [
28 | testCase(MarkdownBlockTests.allTests),
29 | testCase(ExtendedMarkdownBlockTests.allTests),
30 | testCase(MarkdownInlineTests.allTests),
31 | testCase(MarkdownHtmlTests.allTests),
32 | testCase(ExtendedMarkdownHtmlTests.allTests),
33 | testCase(MarkdownStringTests.allTests),
34 | ]
35 | )
36 |
37 | #endif
38 |
--------------------------------------------------------------------------------
/Tests/MarkdownKitTests/ExtendedMarkdownHtmlTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtendedMarkdownHtmlTests.swift
3 | // MarkdownKitTests
4 | //
5 | // Created by Matthias Zenger on 18/07/2020.
6 | // Copyright © 2020 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import MarkdownKit
23 |
24 | class ExtendedMarkdownHtmlTests: XCTestCase, MarkdownKitFactory {
25 |
26 | private func generateHtml(_ str: String) -> String {
27 | return HtmlGenerator().generate(doc: ExtendedMarkdownParser.standard.parse(str))
28 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
29 | }
30 |
31 | func testSimpleNestedLists() {
32 | XCTAssertEqual(
33 | generateHtml("- Apple\n\t- Banana"),
34 | "")
35 | }
36 |
37 | func testTables() {
38 | XCTAssertEqual(generateHtml(" Column A | Column B\n" +
39 | " -------- | --------\n"),
40 | "\n" +
41 | "Column A Column B \n" +
42 | " \n" +
43 | "
")
44 | XCTAssertEqual(generateHtml(" Column A | Column B\n" +
45 | " -------- | --------\n" +
46 | " 1 | 2 \n"),
47 | "\n" +
48 | "Column A Column B \n" +
49 | " \n" +
50 | "1 2 \n" +
51 | "
")
52 | XCTAssertEqual(generateHtml(" Column A |**Column B**\n" +
53 | " :------- | :------:\n" +
54 | " 1 | 2 \n" +
55 | " reg *it* | __bold__\n"),
56 | "\n" +
57 | "Column A " +
58 | "Column B \n" +
59 | " \n" +
60 | "1 2 \n" +
61 | "reg it " +
62 | "bold \n" +
63 | "
")
64 | }
65 |
66 | func testDescriptionLists() {
67 | XCTAssertEqual(generateHtml("Item **name**\n" +
68 | ": Description of\n" +
69 | " _item_"),
70 | "\n" +
71 | "Item name \n" +
72 | "Description of\nitem \n" +
73 | " ")
74 | XCTAssertEqual(generateHtml("Item name\n" +
75 | ": Description of\n" +
76 | "item\n" +
77 | ": Another description\n\n" +
78 | "Item two\n" +
79 | ": Description two\n" +
80 | ": Description three\n"),
81 | "\n" +
82 | "Item name \n" +
83 | "Description of\nitem \n" +
84 | "Another description \n" +
85 | "Item two \n" +
86 | "Description two \n" +
87 | "Description three \n" +
88 | " ")
89 | }
90 |
91 | static let allTests = [
92 | ("testSimpleNestedLists", testSimpleNestedLists),
93 | ("testTables", testTables),
94 | ("testDescriptionLists", testDescriptionLists),
95 | ]
96 | }
97 |
--------------------------------------------------------------------------------
/Tests/MarkdownKitTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.1.3
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tests/MarkdownKitTests/MarkdownASTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkdownASTests.swift
3 | // MarkdownKitTests
4 | //
5 | // Created by Matthias Zenger on 26/02/2022.
6 | // Copyright © 2022 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | import MarkdownKit
23 |
24 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
25 |
26 | class MarkdownASTests: XCTestCase {
27 |
28 | private func generateHtml(imageBaseUrl: URL? = nil, _ str: String) -> String {
29 | return AttributedStringGenerator(imageBaseUrl: imageBaseUrl)
30 | .htmlGenerator
31 | .generate(doc: MarkdownParser.standard.parse(str))
32 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
33 | }
34 |
35 | func testSimpleNestedLists() {
36 | XCTAssertEqual(
37 | generateHtml("- Apple\n\t- Banana"),
38 | "\n
")
39 | XCTAssertEqual(
40 | AttributedStringGenerator(options: [.tightLists])
41 | .htmlGenerator
42 | .generate(doc: MarkdownParser.standard.parse("- Apple\n\t- Banana"))
43 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines),
44 | "\n
")
45 | }
46 |
47 | func testRelativeImageUrls() {
48 | XCTAssertEqual(generateHtml(""),
49 | "
")
50 | XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"),
51 | ""),
52 | "
")
53 | XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"),
54 | ""),
55 | "
")
56 | XCTAssertEqual(generateHtml(""),
57 | "
")
58 | XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"),
59 | ""),
60 | "
")
61 | XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"),
62 | ""),
63 | "
")
64 | }
65 | }
66 |
67 | #endif
68 |
--------------------------------------------------------------------------------
/Tests/MarkdownKitTests/MarkdownExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkdownExtension.swift
3 | // MarkdownKitTests
4 | //
5 | // Created by Matthias Zenger on 11/05/2021.
6 | // Copyright © 2021 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import MarkdownKit
23 |
24 | class MarkdownExtension: XCTestCase, MarkdownKitFactory {
25 |
26 | enum LineEmphasis: CustomTextFragment {
27 | case underline(Text)
28 | case strikethrough(Text)
29 |
30 | func equals(to other: CustomTextFragment) -> Bool {
31 | guard let that = other as? LineEmphasis else {
32 | return false
33 | }
34 | switch (self, that) {
35 | case (.underline(let lhs), .underline(let rhs)):
36 | return lhs == rhs
37 | case (.strikethrough(let lhs), .strikethrough(let rhs)):
38 | return lhs == rhs
39 | default:
40 | return false
41 | }
42 | }
43 |
44 | func transform(via transformer: InlineTransformer) -> TextFragment {
45 | switch self {
46 | case .underline(let text):
47 | return .custom(LineEmphasis.underline(transformer.transform(text)))
48 | case .strikethrough(let text):
49 | return .custom(LineEmphasis.strikethrough(transformer.transform(text)))
50 | }
51 | }
52 |
53 | func generateHtml(via htmlGen: HtmlGenerator) -> String {
54 | switch self {
55 | case .underline(let text):
56 | return "" + htmlGen.generate(text: text) + " "
57 | case .strikethrough(let text):
58 | return "" + htmlGen.generate(text: text) + " "
59 | }
60 | }
61 |
62 | func generateHtml(via htmlGen: HtmlGenerator,
63 | and attrGen: AttributedStringGenerator?) -> String {
64 | return self.generateHtml(via: htmlGen)
65 | }
66 |
67 | var rawDescription: String {
68 | switch self {
69 | case .underline(let text):
70 | return text.rawDescription
71 | case .strikethrough(let text):
72 | return text.rawDescription
73 | }
74 | }
75 |
76 | var description: String {
77 | switch self {
78 | case .underline(let text):
79 | return "~\(text.description)~"
80 | case .strikethrough(let text):
81 | return "~~\(text.description)~~"
82 | }
83 | }
84 |
85 | var debugDescription: String {
86 | switch self {
87 | case .underline(let text):
88 | return "underline(\(text.debugDescription))"
89 | case .strikethrough(let text):
90 | return "strikethrough(\(text.debugDescription))"
91 | }
92 | }
93 | }
94 |
95 | final class EmphasisTestTransformer: EmphasisTransformer {
96 | override public class var supportedEmphasis: [Emphasis] {
97 | return super.supportedEmphasis + [
98 | Emphasis(ch: "~", special: false, factory: { double, text in
99 | return .custom(double ? LineEmphasis.strikethrough(text)
100 | : LineEmphasis.underline(text))
101 | })]
102 | }
103 | }
104 |
105 | final class DelimiterTestTransformer: DelimiterTransformer {
106 | override public class var emphasisChars: [Character] {
107 | return super.emphasisChars + ["~"]
108 | }
109 | }
110 |
111 | final class EmphasisTestMarkdownParser: MarkdownParser {
112 | override public class var defaultInlineTransformers: [InlineTransformer.Type] {
113 | return [DelimiterTestTransformer.self,
114 | CodeLinkHtmlTransformer.self,
115 | LinkTransformer.self,
116 | EmphasisTestTransformer.self,
117 | EscapeTransformer.self]
118 | }
119 | override public class var standard: EmphasisTestMarkdownParser {
120 | return self.singleton
121 | }
122 | private static let singleton: EmphasisTestMarkdownParser = EmphasisTestMarkdownParser()
123 | }
124 |
125 | private func parse(_ str: String) -> Block {
126 | return EmphasisTestMarkdownParser.standard.parse(str)
127 | }
128 |
129 | private func generateHtml(_ str: String) -> String {
130 | return HtmlGenerator().generate(doc: EmphasisTestMarkdownParser.standard.parse(str))
131 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
132 | }
133 |
134 | func testExtendedDelimiters() throws {
135 | XCTAssertEqual(parse("~foo bar"),
136 | document(paragraph(.delimiter("~", 1, .leftFlanking),
137 | .text("foo bar"))))
138 | XCTAssertEqual(parse("~~foo bar"),
139 | document(paragraph(.delimiter("~", 2, .leftFlanking),
140 | .text("foo bar"))))
141 | XCTAssertEqual(parse("~~foo~ bar"),
142 | document(paragraph(.delimiter("~", 1, .leftFlanking),
143 | custom(LineEmphasis.underline, .text("foo")),
144 | .text(" bar"))))
145 | XCTAssertEqual(parse("~~foo\\~ bar"),
146 | document(paragraph(.delimiter("~", 2, .leftFlanking),
147 | .text("foo~ bar"))))
148 | XCTAssertEqual(parse("~~foo~~ bar"),
149 | document(paragraph(custom(LineEmphasis.strikethrough, .text("foo")),
150 | .text(" bar"))))
151 | XCTAssertEqual(parse("ok ~~~foo~~~ bar"),
152 | document(paragraph(.text("ok "),
153 | custom(LineEmphasis.underline,
154 | custom(LineEmphasis.strikethrough, .text("foo"))),
155 | .text(" bar"))))
156 | XCTAssertEqual(parse("combined *~foo~* bar"),
157 | document(paragraph(.text("combined "),
158 | emph(custom(LineEmphasis.underline, .text("foo"))),
159 | .text(" bar"))))
160 | XCTAssertEqual(parse("combined ~*foo bar*~"),
161 | document(paragraph(.text("combined "),
162 | custom(LineEmphasis.underline, emph(.text("foo bar"))))))
163 | XCTAssertEqual(parse("combined *~foo~ bar*"),
164 | document(paragraph(.text("combined "),
165 | emph(custom(LineEmphasis.underline, .text("foo")),
166 | .text(" bar")))))
167 | }
168 |
169 | func testExtendedDelimitersHtml() {
170 | XCTAssertEqual(generateHtml("one ~two~\n~~three~~ four"),
171 | "one two \nthree four
")
172 | XCTAssertEqual(generateHtml("### Sub ~and~ heading ###\nAnd this is the text."),
173 | "Sub and heading \nAnd this is the text.
")
174 | XCTAssertEqual(generateHtml("expressive, ~~simple~, ~~and~~ ~elegant~~"),
175 | "expressive, simple , and elegant
")
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/Tests/MarkdownKitTests/MarkdownFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkdownFactory.swift
3 | // MarkdownKitTests
4 | //
5 | // Created by Matthias Zenger on 09/05/2019.
6 | // Copyright © 2019-2020 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 | @testable import MarkdownKit
23 |
24 | protocol MarkdownKitFactory {
25 | }
26 |
27 | extension MarkdownKitFactory {
28 |
29 | func document(_ bs: Block...) -> Block {
30 | return .document(ContiguousArray(bs))
31 | }
32 |
33 | func paragraph(_ strs: String?...) -> Block {
34 | var res = Text()
35 | for str in strs {
36 | if let str = str {
37 | if let last = res.last {
38 | switch last {
39 | case .softLineBreak, .hardLineBreak:
40 | break
41 | default:
42 | res.append(fragment: .softLineBreak)
43 | }
44 | }
45 |
46 | res.append(fragment: .text(Substring(str)))
47 | } else {
48 | res.append(fragment: .hardLineBreak)
49 | }
50 | }
51 | return .paragraph(res)
52 | }
53 |
54 | func paragraph(_ fragments: TextFragment...) -> Block {
55 | var res = Text()
56 | for fragment in fragments {
57 | res.append(fragment: fragment)
58 | }
59 | return .paragraph(res)
60 | }
61 |
62 | func emph(_ fragments: TextFragment...) -> TextFragment {
63 | var res = Text()
64 | for fragment in fragments {
65 | res.append(fragment: fragment)
66 | }
67 | return .emph(res)
68 | }
69 |
70 | func strong(_ fragments: TextFragment...) -> TextFragment {
71 | var res = Text()
72 | for fragment in fragments {
73 | res.append(fragment: fragment)
74 | }
75 | return .strong(res)
76 | }
77 |
78 | func link(_ dest: String?, _ title: String?, _ fragments: TextFragment...) -> TextFragment {
79 | var res = Text()
80 | for fragment in fragments {
81 | res.append(fragment: fragment)
82 | }
83 | return .link(res, dest, title)
84 | }
85 |
86 | func image(_ dest: String?, _ title: String?, _ fragments: TextFragment...) -> TextFragment {
87 | var res = Text()
88 | for fragment in fragments {
89 | res.append(fragment: fragment)
90 | }
91 | return .image(res, dest, title)
92 | }
93 |
94 | func custom(_ factory: (Text) -> CustomTextFragment,
95 | _ fragments: TextFragment...) -> TextFragment {
96 | var res = Text()
97 | for fragment in fragments {
98 | res.append(fragment: fragment)
99 | }
100 | return .custom(factory(res))
101 | }
102 |
103 | func atxHeading(_ level: Int, _ title: String) -> Block {
104 | return .heading(level, Text(Substring(title)))
105 | }
106 |
107 | func setextHeading(_ level: Int, _ strs: String?...) -> Block {
108 | var res = Text()
109 | for str in strs {
110 | if let str = str {
111 | if let last = res.last {
112 | switch last {
113 | case .softLineBreak, .hardLineBreak:
114 | break
115 | default:
116 | res.append(fragment: .softLineBreak)
117 | }
118 | }
119 | res.append(fragment: .text(Substring(str)))
120 | } else {
121 | res.append(fragment: .hardLineBreak)
122 | }
123 | }
124 | return .heading(level, res)
125 | }
126 |
127 | func blockquote(_ bs: Block...) -> Block {
128 | return .blockquote(ContiguousArray(bs))
129 | }
130 |
131 | func indentedCode(_ strs: Substring...) -> Block {
132 | return .indentedCode(ContiguousArray(strs))
133 | }
134 |
135 | func fencedCode(_ info: String?, _ strs: Substring...) -> Block {
136 | return .fencedCode(info, ContiguousArray(strs))
137 | }
138 |
139 | func list(_ num: Int, tight: Bool = true, _ bs: Block...) -> Block {
140 | return .list(num, tight, ContiguousArray(bs))
141 | }
142 |
143 | func list(tight: Bool = true, _ bs: Block...) -> Block {
144 | return .list(nil, tight, ContiguousArray(bs))
145 | }
146 |
147 | func listItem(_ num: Int,
148 | _ sep: Character,
149 | tight: Bool = false,
150 | initial: Bool = false,
151 | _ bs: Block...) -> Block {
152 | return .listItem(.ordered(num, sep),
153 | tight ? .tight : (initial ? .initial : .loose),
154 | ContiguousArray(bs))
155 | }
156 |
157 | func listItem(_ bullet: Character, tight: Bool = false, initial: Bool = false, _ bs: Block...) -> Block {
158 | return .listItem(.bullet(bullet),
159 | tight ? .tight : (initial ? .initial : .loose),
160 | ContiguousArray(bs))
161 | }
162 |
163 | func htmlBlock(_ lines: Substring...) -> Block {
164 | return .htmlBlock(ContiguousArray(lines))
165 | }
166 |
167 | func referenceDef(_ label: String, _ dest: Substring, _ title: Substring...) -> Block {
168 | return .referenceDef(label, dest, ContiguousArray(title))
169 | }
170 |
171 | func table(_ hdr: [Substring?], _ algn: [Alignment], _ rw: [Substring?]...) -> Block {
172 | func toRow(_ arr: [Substring?]) -> Row {
173 | var res = Row()
174 | for a in arr {
175 | if let str = a {
176 | let components = str.components(separatedBy: "$")
177 | if components.count <= 1 {
178 | res.append(Text(str))
179 | } else {
180 | var text = Text()
181 | for component in components {
182 | text.append(fragment: .text(Substring(component)))
183 | }
184 | res.append(text)
185 | }
186 | } else {
187 | res.append(Text())
188 | }
189 | }
190 | return res
191 | }
192 | var rows = Rows()
193 | for r in rw {
194 | rows.append(toRow(r))
195 | }
196 | return .table(toRow(hdr), ContiguousArray(algn), rows)
197 | }
198 |
199 | func definitionList(_ decls: (Substring, [(ListDensity, [Block])])...) -> Block {
200 | var defs = Definitions()
201 | for decl in decls {
202 | var res = Blocks()
203 | for (density, blocks) in decl.1 {
204 | var content = Blocks()
205 | for block in blocks {
206 | content.append(block)
207 | }
208 | res.append(.listItem(.bullet(":"), density, content))
209 | }
210 | defs.append(Definition(item: Text(decl.0), descriptions: res))
211 | }
212 | return .definitionList(defs)
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/Tests/MarkdownKitTests/MarkdownHtmlTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkdownHtmlTests.swift
3 | // MarkdownKitTests
4 | //
5 | // Created by Matthias Zenger on 20/07/2019.
6 | // Copyright © 2019 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import MarkdownKit
23 |
24 | class MarkdownHtmlTests: XCTestCase, MarkdownKitFactory {
25 |
26 | private func generateHtml(_ str: String) -> String {
27 | return HtmlGenerator().generate(doc: MarkdownParser.standard.parse(str))
28 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
29 | }
30 |
31 | func testBasics() {
32 | XCTAssertEqual(generateHtml("one *two*\n**three** four"),
33 | "one two \nthree four
")
34 | XCTAssertEqual(generateHtml("one _two_ __three__\n***\nfour"),
35 | "one two three
\n \nfour
")
36 | XCTAssertEqual(generateHtml("# Top\n## Below\nAnd this is the text."),
37 | "Top \nBelow \nAnd this is the text.
")
38 | XCTAssertEqual(generateHtml("### Sub *and* heading ###\nAnd this is the text."),
39 | "Sub and heading \nAnd this is the text.
")
40 | XCTAssertEqual(generateHtml("expressive & simple & elegant"),
41 | "expressive & simple & elegant
")
42 | XCTAssertEqual(generateHtml("This is `a & b`"),
43 | "This is a & b
")
44 | }
45 |
46 | func testLists() {
47 | XCTAssertEqual(generateHtml("""
48 | - One
49 | - Two
50 | - Three
51 | """),
52 | "")
53 | XCTAssertEqual(generateHtml("""
54 | - One
55 |
56 | Two
57 | - Three
58 | - Four
59 | """),
60 | "\nOne
\nTwo
\n \nThree
\n \n" +
61 | "Four
\n \n ")
62 | }
63 |
64 | func testSimpleNestedLists() {
65 | XCTAssertEqual(
66 | generateHtml("- Apple\n\t- Banana"),
67 | "")
68 | }
69 |
70 | func testNestedLists() {
71 | XCTAssertEqual(generateHtml("""
72 | - foo
73 | - bar
74 | * one
75 | * two
76 | * three
77 | - goo
78 | """),
79 | "\nfoo \nbar\n\none \ntwo \n" +
80 | "three \n \n \ngoo \n ")
81 | }
82 |
83 | func testImageLinks() {
84 | XCTAssertEqual(generateHtml("""
85 | This is an inline image: .
86 | """),
87 | "This is an inline image: .
")
89 | XCTAssertEqual(generateHtml("""
90 | This is an image block:
91 |
92 | 
93 | """),
94 | "This is an image block:
\n" +
95 | "
")
96 | }
97 |
98 | func testAutolinks() {
99 | XCTAssertEqual(generateHtml("Test test"),
100 | "Test <www.example.com> test
")
101 | XCTAssertEqual(generateHtml("Test test"),
102 | "Test http://www.example.com test
")
103 | }
104 |
105 | func testCodeBlocks() {
106 | XCTAssertEqual(generateHtml("Test\n\n```\nThis should not be bold .\n```\n"),
107 | "Test
\nThis should <b>not be bold</b>.\n" +
108 | "
")
109 | }
110 |
111 | static let allTests = [
112 | ("testBasics", testBasics),
113 | ("testLists", testLists),
114 | ("testNestedLists", testNestedLists),
115 | ("testImageLinks", testImageLinks),
116 | ("testAutolinks", testAutolinks),
117 | ]
118 | }
119 |
--------------------------------------------------------------------------------
/Tests/MarkdownKitTests/MarkdownStringTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkdownStringTests.swift
3 | // MarkdownKitTests
4 | //
5 | // Created by Matthias Zenger on 14/02/2021.
6 | // Copyright © 2021 Google LLC.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import MarkdownKit
23 |
24 | class MarkdownStringTests: XCTestCase {
25 |
26 | func testAmpersandEncoding() throws {
27 | XCTAssertEqual("head tail".encodingPredefinedXmlEntities(),
28 | "head tail")
29 | XCTAssertEqual("head & tail".encodingPredefinedXmlEntities(),
30 | "head & tail")
31 | XCTAssertEqual("head && tail".encodingPredefinedXmlEntities(),
32 | "head && tail")
33 | }
34 |
35 | func testPredefinedEncodings() throws {
36 | XCTAssertEqual("head \"tail\"".encodingPredefinedXmlEntities(),
37 | "head "tail"")
38 | XCTAssertEqual("head'n tail".encodingPredefinedXmlEntities(),
39 | "head'n tail")
40 | XCTAssertEqual("\"'x\" corresponds to (quote x)".encodingPredefinedXmlEntities(),
41 | ""'x" corresponds to (quote x)")
42 | }
43 |
44 | func testDecodingEntities() throws {
45 | XCTAssertEqual(""'x" corresponds to (quote x)".decodingNamedCharacters(),
46 | "\"'x\" corresponds to (quote x)")
47 | XCTAssertEqual("head&tail is not "⋔"".decodingNamedCharacters(),
48 | "head&tail\u{000A0}is not \"⋔\"")
49 | XCTAssertEqual("x≉3.141".decodingNamedCharacters(),
50 | "x≉3.141")
51 | XCTAssertEqual("⋪⋬⋫".decodingNamedCharacters(),
52 | "⋪⋬⋫")
53 | }
54 |
55 | static let allTests = [
56 | ("testAmpersandEncoding", testAmpersandEncoding),
57 | ("testPredefinedEncodings", testPredefinedEncodings),
58 | ("testDecodingEntities", testDecodingEntities),
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------