Skip to content

Commit 428aaf5

Browse files
committed
Implemented support for Markdown tables. Made it easier to extend class MarkdownParser. Included ExtendedMarkdownParser.
1 parent 1837bab commit 428aaf5

16 files changed

+658
-19
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.0 (2020-07-18)
4+
- Implemented support for Markdown tables
5+
- Made it easier to extend class `MarkdownParser`
6+
- Included extended markdown parser `ExtendedMarkdownParser`
7+
38
## 0.2.2 (2020-01-26)
49
- Fixed bug in AttributedStringGenerator.swift
510
- Migrated project to Xcode 11.3.1

MarkdownKit.xcodeproj/project.pbxproj

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
CC088C0B22DB74090059460E /* TextFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC088C0A22DB74090059460E /* TextFragment.swift */; };
1111
CC088C0F22DD2BCD0059460E /* HtmlGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC088C0E22DD2BCD0059460E /* HtmlGenerator.swift */; };
1212
CC088C2422E337BD0059460E /* MarkdownHtmlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC088C2322E337BD0059460E /* MarkdownHtmlTests.swift */; };
13+
CC24757324C1EBB600678C59 /* ExtendedMarkdownBlockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC24757224C1EBB600678C59 /* ExtendedMarkdownBlockTests.swift */; };
14+
CC24757524C1EC3600678C59 /* TableParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC24757424C1EC3600678C59 /* TableParser.swift */; };
15+
CC24757724C1ED7E00678C59 /* ExtendedMarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC24757624C1ED7E00678C59 /* ExtendedMarkdownParser.swift */; };
1316
CC2AF6F2227CD98100BEA420 /* MarkdownKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC2AF6E8227CD98100BEA420 /* MarkdownKit.framework */; };
1417
CC2AF6F7227CD98100BEA420 /* MarkdownBlockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2AF6F6227CD98100BEA420 /* MarkdownBlockTests.swift */; };
1518
CC2AF6F9227CD98100BEA420 /* MarkdownKit.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2AF6EB227CD98100BEA420 /* MarkdownKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -31,6 +34,7 @@
3134
CC8CBC21228730800084683B /* LinkRefDefinitionParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC8CBC20228730800084683B /* LinkRefDefinitionParser.swift */; };
3235
CC8CBC2322884F940084683B /* SetextHeadingParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC8CBC2222884F940084683B /* SetextHeadingParser.swift */; };
3336
CC8CBC25228898B00084683B /* HtmlBlockParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC8CBC24228898B00084683B /* HtmlBlockParser.swift */; };
37+
CC8D559D24C3560700E11F96 /* ExtendedMarkdownHtmlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC8D559C24C3560700E11F96 /* ExtendedMarkdownHtmlTests.swift */; };
3438
CCB033CF227CDB2F00E1C4DC /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB033C7227CDB2F00E1C4DC /* MarkdownParser.swift */; };
3539
CCB033D0227CDB2F00E1C4DC /* AtxHeadingParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB033C8227CDB2F00E1C4DC /* AtxHeadingParser.swift */; };
3640
CCB033D1227CDB2F00E1C4DC /* CodeBlockParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB033C9227CDB2F00E1C4DC /* CodeBlockParser.swift */; };
@@ -73,6 +77,9 @@
7377
CC0DFBE022FDC617002ACD37 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
7478
CC0DFBE222FDCCE0002ACD37 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
7579
CC0DFBE322FDCD5F002ACD37 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
80+
CC24757224C1EBB600678C59 /* ExtendedMarkdownBlockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedMarkdownBlockTests.swift; sourceTree = "<group>"; };
81+
CC24757424C1EC3600678C59 /* TableParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableParser.swift; sourceTree = "<group>"; };
82+
CC24757624C1ED7E00678C59 /* ExtendedMarkdownParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedMarkdownParser.swift; sourceTree = "<group>"; };
7683
CC2AF6E8227CD98100BEA420 /* MarkdownKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MarkdownKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7784
CC2AF6EB227CD98100BEA420 /* MarkdownKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MarkdownKit.h; sourceTree = "<group>"; };
7885
CC2AF6EC227CD98100BEA420 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -98,8 +105,9 @@
98105
CC8CBC182284B3F40084683B /* MarkdownFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownFactory.swift; sourceTree = "<group>"; };
99106
CC8CBC1E2286CED60084683B /* Blocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Blocks.swift; sourceTree = "<group>"; };
100107
CC8CBC20228730800084683B /* LinkRefDefinitionParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkRefDefinitionParser.swift; sourceTree = "<group>"; };
101-
CC8CBC2222884F940084683B /* SetextHeadingParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetextHeadingParser.swift; sourceTree = "<group>"; };
108+
CC8CBC2222884F940084683B /* SetextHeadingParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetextHeadingParser.swift; sourceTree = "<group>"; };
102109
CC8CBC24228898B00084683B /* HtmlBlockParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlBlockParser.swift; sourceTree = "<group>"; };
110+
CC8D559C24C3560700E11F96 /* ExtendedMarkdownHtmlTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedMarkdownHtmlTests.swift; sourceTree = "<group>"; };
103111
CCB033C7227CDB2F00E1C4DC /* MarkdownParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownParser.swift; sourceTree = "<group>"; };
104112
CCB033C8227CDB2F00E1C4DC /* AtxHeadingParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtxHeadingParser.swift; sourceTree = "<group>"; };
105113
CCB033C9227CDB2F00E1C4DC /* CodeBlockParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeBlockParser.swift; sourceTree = "<group>"; };
@@ -206,8 +214,10 @@
206214
children = (
207215
CC8CBC182284B3F40084683B /* MarkdownFactory.swift */,
208216
CC2AF6F6227CD98100BEA420 /* MarkdownBlockTests.swift */,
217+
CC24757224C1EBB600678C59 /* ExtendedMarkdownBlockTests.swift */,
209218
CC7B603822AC67DC0092188C /* MarkdownInlineTests.swift */,
210219
CC088C2322E337BD0059460E /* MarkdownHtmlTests.swift */,
220+
CC8D559C24C3560700E11F96 /* ExtendedMarkdownHtmlTests.swift */,
211221
CC2AF6F8227CD98100BEA420 /* Info.plist */,
212222
);
213223
name = MarkdownKitTests;
@@ -234,11 +244,13 @@
234244
isa = PBXGroup;
235245
children = (
236246
CCB033C7227CDB2F00E1C4DC /* MarkdownParser.swift */,
247+
CC24757624C1ED7E00678C59 /* ExtendedMarkdownParser.swift */,
237248
CCB033CE227CDB2F00E1C4DC /* DocumentParser.swift */,
238249
CCB033CA227CDB2F00E1C4DC /* BlockParser.swift */,
239250
CCB033CD227CDB2F00E1C4DC /* Container.swift */,
240251
CCB033C8227CDB2F00E1C4DC /* AtxHeadingParser.swift */,
241252
CC8CBC2222884F940084683B /* SetextHeadingParser.swift */,
253+
CC24757424C1EC3600678C59 /* TableParser.swift */,
242254
CCB033CB227CDB2F00E1C4DC /* ThematicBreakParser.swift */,
243255
CCB033C9227CDB2F00E1C4DC /* CodeBlockParser.swift */,
244256
CC8CBC24228898B00084683B /* HtmlBlockParser.swift */,
@@ -393,8 +405,10 @@
393405
CC8CBC17227F39C00084683B /* ListItemParser.swift in Sources */,
394406
CCB033D2227CDB2F00E1C4DC /* BlockParser.swift in Sources */,
395407
CC6D239822C03CBA00BB1302 /* LinkTransformer.swift in Sources */,
408+
CC24757524C1EC3600678C59 /* TableParser.swift in Sources */,
396409
CCB033CF227CDB2F00E1C4DC /* MarkdownParser.swift in Sources */,
397410
CCB033D3227CDB2F00E1C4DC /* ThematicBreakParser.swift in Sources */,
411+
CC24757724C1ED7E00678C59 /* ExtendedMarkdownParser.swift in Sources */,
398412
CCB033D6227CDB2F00E1C4DC /* DocumentParser.swift in Sources */,
399413
CCD07466229FDFB90053B73C /* InlineParser.swift in Sources */,
400414
CCB033D4227CDB2F00E1C4DC /* Block.swift in Sources */,
@@ -423,8 +437,10 @@
423437
buildActionMask = 2147483647;
424438
files = (
425439
CC7B603922AC67DC0092188C /* MarkdownInlineTests.swift in Sources */,
440+
CC24757324C1EBB600678C59 /* ExtendedMarkdownBlockTests.swift in Sources */,
426441
CC088C2422E337BD0059460E /* MarkdownHtmlTests.swift in Sources */,
427442
CC2AF6F7227CD98100BEA420 /* MarkdownBlockTests.swift in Sources */,
443+
CC8D559D24C3560700E11F96 /* ExtendedMarkdownHtmlTests.swift in Sources */,
428444
CC8CBC192284B3F40084683B /* MarkdownFactory.swift in Sources */,
429445
);
430446
runOnlyForDeploymentPostprocessing = 0;
@@ -591,7 +607,7 @@
591607
"@executable_path/../Frameworks",
592608
"@loader_path/Frameworks",
593609
);
594-
MARKETING_VERSION = 0.2.2;
610+
MARKETING_VERSION = 1.0;
595611
PRODUCT_BUNDLE_IDENTIFIER = net.objecthub.MarkdownKit;
596612
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
597613
SKIP_INSTALL = YES;
@@ -620,7 +636,7 @@
620636
"@executable_path/../Frameworks",
621637
"@loader_path/Frameworks",
622638
);
623-
MARKETING_VERSION = 0.2.2;
639+
MARKETING_VERSION = 1.0;
624640
PRODUCT_BUNDLE_IDENTIFIER = net.objecthub.MarkdownKit;
625641
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
626642
SKIP_INSTALL = YES;

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
_Swift MarkdownKit_ is a framework for parsing text in [Markdown](https://daringfireball.net/projects/markdown/)
1313
format. The supported syntax is based on the [CommonMark Markdown specification](https://commonmark.org).
14+
_Swift MarkdownKit_ also provides an extended version of the parser that is able to handle Markdown tables.
1415

1516
_Swift MarkdownKit_ defines an abstract syntax for Markdown, it provides a parser for parsing strings into
1617
abstract syntax trees, and comes with generators for creating HTML and
@@ -59,6 +60,20 @@ is yet another recursively defined enumeration with associated values. The examp
5960
contains a `Text` object, i.e. it encapsulates a sequence of `TextFragment` values which are
6061
"marked up strongly".
6162

63+
Class `ExtendedMarkdownParser` has the same interface like `MarkdownParser` but supports table blocks in
64+
addition to the block types defined by the [CommonMark specification](https://commonmark.org).
65+
[Tables](https://github.github.com/gfm/#tables-extension-) are based on the
66+
[GitHub Flavored Markdown specification](https://github.github.com/gfm/) with one extension: within a table
67+
block it is possible to escape newline characters to enable cell text to be written on multiple lines. Here is an example:
68+
69+
```markdown
70+
| Column 1 | Column 2 |
71+
| ------------ | -------------- |
72+
| This text \
73+
is very long | More cell text |
74+
| Last line | Last cell |
75+
```
76+
6277
### Configuring the Markdown parser
6378

6479
The Markdown dialect supported by `MarkdownParser` is defined by two parameters: a sequence of
@@ -74,6 +89,28 @@ Since `MarkdownParser` objects are stateless (beyond the configuration of block
7489
transformers), there is a predefined default `MarkdownParser` object accessible via the static property
7590
`MarkdownParser.standard`. This default parsing object is used in the example above.
7691

92+
New markdown parsers with different configurations can also be created by subclassing `MarkdownParser`
93+
and by overriding the class properties `defaultBlockParsers` and `defaultInlineTransformers`. Here is
94+
an example how class `ExtendedMarkdownParser` is derived from `MarkdownParser` simply by overriding
95+
`defaultBlockParsers` and by specializing `standard` in a covariant fashion.
96+
97+
```swift
98+
open class ExtendedMarkdownParser: MarkdownParser {
99+
override open class var defaultBlockParsers: [BlockParser.Type] {
100+
return Self.blockParsers
101+
}
102+
private static let blockParsers: [BlockParser.Type] = {
103+
var parsers = MarkdownParser.defaultBlockParsers
104+
parsers.append(TableParser.self)
105+
return parsers
106+
}()
107+
override open class var standard: ExtendedMarkdownParser {
108+
return self.singleton
109+
}
110+
private static let singleton: ExtendedMarkdownParser = ExtendedMarkdownParser()
111+
}
112+
```
113+
77114
### Processing Markdown
78115

79116
The usage of abstract syntax trees for representing Markdown text has the advantage that it is very easy to

Sources/MarkdownKit/Block.swift

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// MarkdownKit
44
//
55
// Created by Matthias Zenger on 25/04/2019.
6-
// Copyright © 2019 Google LLC.
6+
// Copyright © 2019-2020 Google LLC.
77
//
88
// Licensed under the Apache License, Version 2.0 (the "License");
99
// you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ public enum Block: Equatable, CustomStringConvertible, CustomDebugStringConverti
3737
case htmlBlock(Lines)
3838
case referenceDef(String, Substring, Lines)
3939
case thematicBreak
40+
case table(Row, Alignments, Rows)
4041

4142
/// Returns a description of the block as a string.
4243
public var description: String {
@@ -107,9 +108,18 @@ public enum Block: Equatable, CustomStringConvertible, CustomDebugStringConverti
107108
}
108109
case .thematicBreak:
109110
return "thematicBreak"
111+
case .table(let header, let align, let rows):
112+
var res = self.string(from: header) + ", "
113+
for a in align {
114+
res += a.description
115+
}
116+
for row in rows {
117+
res += ", " + self.string(from: row)
118+
}
119+
return "table(\(res))"
110120
}
111121
}
112-
122+
113123
/// Returns a debug description.
114124
public var debugDescription: String {
115125
return self.description
@@ -126,7 +136,19 @@ public enum Block: Equatable, CustomStringConvertible, CustomDebugStringConverti
126136
}
127137
return res
128138
}
129-
139+
140+
private func string(from row: Row) -> String {
141+
var res = "row("
142+
for cell in row {
143+
if res.isEmpty {
144+
res = cell.description
145+
} else {
146+
res = res + " | " + cell.description
147+
}
148+
}
149+
return res + ")"
150+
}
151+
130152
/// Defines an equality relation for two blocks.
131153
public static func == (lhs: Block, rhs: Block) -> Bool {
132154
switch (lhs, rhs) {
@@ -152,6 +174,8 @@ public enum Block: Equatable, CustomStringConvertible, CustomDebugStringConverti
152174
return llab == rlab && ldest == rdest && lt == rt
153175
case (.thematicBreak, .thematicBreak):
154176
return true
177+
case (.table(let lheader, let lalign, let lrows), .table(let rheader, let ralign, let rrows)):
178+
return lheader == rheader && lalign == ralign && lrows == rrows
155179
default:
156180
return false
157181
}
@@ -198,3 +222,38 @@ public enum ListType: Equatable, CustomStringConvertible, CustomDebugStringConve
198222
return self.description
199223
}
200224
}
225+
226+
///
227+
/// Rows are arrays of text.
228+
///
229+
public typealias Row = ContiguousArray<Text>
230+
public typealias Rows = ContiguousArray<Row>
231+
232+
///
233+
/// Column alignments are represented as arrays of `Alignment` enum values
234+
///
235+
public enum Alignment: UInt, CustomStringConvertible, CustomDebugStringConvertible {
236+
case undefined = 0
237+
case left = 1
238+
case right = 2
239+
case center = 3
240+
241+
public var description: String {
242+
switch self {
243+
case .undefined:
244+
return "-"
245+
case .left:
246+
return "L"
247+
case .right:
248+
return "R"
249+
case .center:
250+
return "C"
251+
}
252+
}
253+
254+
public var debugDescription: String {
255+
return self.description
256+
}
257+
}
258+
259+
public typealias Alignments = ContiguousArray<Alignment>

Sources/MarkdownKit/HTML/HtmlGenerator.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// MarkdownKit
44
//
55
// Created by Matthias Zenger on 15/07/2019.
6-
// Copyright © 2019 Google LLC.
6+
// Copyright © 2019-2020 Google LLC.
77
//
88
// Licensed under the Apache License, Version 2.0 (the "License");
99
// you may not use this file except in compliance with the License.
@@ -89,6 +89,38 @@ open class HtmlGenerator {
8989
return ""
9090
case .thematicBreak:
9191
return "<hr />\n"
92+
case .table(let header, let align, let rows):
93+
var tagsuffix: [String] = []
94+
for a in align {
95+
switch a {
96+
case .undefined:
97+
tagsuffix.append(">")
98+
case .left:
99+
tagsuffix.append(" align=\"left\">")
100+
case .right:
101+
tagsuffix.append(" align=\"right\">")
102+
case .center:
103+
tagsuffix.append(" align=\"center\">")
104+
}
105+
}
106+
var html = "<table><thead><tr>\n"
107+
var i = 0
108+
for head in header {
109+
html += "<th\(tagsuffix[i])\(self.generate(text: head))</th>"
110+
i += 1
111+
}
112+
html += "\n</tr></thead><tbody>\n"
113+
for row in rows {
114+
html += "<tr>"
115+
i = 0
116+
for cell in row {
117+
html += "<td\(tagsuffix[i])\(self.generate(text: cell))</td>"
118+
i += 1
119+
}
120+
html += "</tr>\n"
121+
}
122+
html += "</tbody></table>\n"
123+
return html
92124
}
93125
}
94126

Sources/MarkdownKit/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<key>CFBundlePackageType</key>
1616
<string>FMWK</string>
1717
<key>CFBundleShortVersionString</key>
18-
<string>0.2.2</string>
18+
<string>1.0</string>
1919
<key>CFBundleVersion</key>
2020
<string>$(CURRENT_PROJECT_VERSION)</string>
2121
<key>NSHumanReadableCopyright</key>

Sources/MarkdownKit/Parser/DocumentParser.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// MarkdownKit
44
//
55
// Created by Matthias Zenger on 20/04/2019.
6-
// Copyright © 2019 Google LLC.
6+
// Copyright © 2019-2020 Google LLC.
77
//
88
// Licensed under the Apache License, Version 2.0 (the "License");
99
// you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@ import Foundation
2828
public class DocumentParser {
2929

3030
/// Sequence of block parsers which implement the document parsing functionality.
31-
private var blockParsers: [BlockParser]
31+
internal private(set) var blockParsers: [BlockParser]
3232

3333
/// The input string which gets parsed.
3434
private let input: String

0 commit comments

Comments
 (0)