Skip to content

Commit 395fbf2

Browse files
parse table cell spans in cmark and use the information (#74)
rdar://98017807
1 parent 52563fc commit 395fbf2

File tree

7 files changed

+224
-20
lines changed

7 files changed

+224
-20
lines changed

Sources/Markdown/Base/RawMarkup.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,18 @@ enum RawMarkupData: Equatable {
4949
case tableHead
5050
case tableBody
5151
case tableRow
52-
case tableCell
52+
case tableCell(colspan: UInt, rowspan: UInt)
53+
}
54+
55+
extension RawMarkupData {
56+
func isTableCell() -> Bool {
57+
switch self {
58+
case .tableCell:
59+
return true
60+
default:
61+
return false
62+
}
63+
}
5364
}
5465

5566
/// The header for the `RawMarkup` managed buffer.
@@ -297,12 +308,12 @@ final class RawMarkup: ManagedBuffer<RawMarkupHeader, RawMarkup> {
297308
}
298309

299310
static func tableRow(parsedRange: SourceRange?, _ columns: [RawMarkup]) -> RawMarkup {
300-
precondition(columns.allSatisfy { $0.header.data == .tableCell })
311+
precondition(columns.allSatisfy { $0.header.data.isTableCell() })
301312
return .create(data: .tableRow, parsedRange: parsedRange, children: columns)
302313
}
303314

304315
static func tableHead(parsedRange: SourceRange?, columns: [RawMarkup]) -> RawMarkup {
305-
precondition(columns.allSatisfy { $0.header.data == .tableCell })
316+
precondition(columns.allSatisfy { $0.header.data.isTableCell() })
306317
return .create(data: .tableHead, parsedRange: parsedRange, children: columns)
307318
}
308319

@@ -311,8 +322,8 @@ final class RawMarkup: ManagedBuffer<RawMarkupHeader, RawMarkup> {
311322
return .create(data: .tableBody, parsedRange: parsedRange, children: rows)
312323
}
313324

314-
static func tableCell(parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
315-
return .create(data: .tableCell, parsedRange: parsedRange, children: children)
325+
static func tableCell(parsedRange: SourceRange?, colspan: UInt, rowspan: UInt, _ children: [RawMarkup]) -> RawMarkup {
326+
return .create(data: .tableCell(colspan: colspan, rowspan: rowspan), parsedRange: parsedRange, children: children)
316327
}
317328
}
318329

Sources/Markdown/Block Nodes/Tables/TableCell.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,48 @@ extension Table {
3030

3131
public extension Table.Cell {
3232

33+
/// The number of columns this cell spans over.
34+
///
35+
/// A normal, non-spanning table cell has a `colspan` of 1. A value greater than one indicates
36+
/// that this cell has expanded to cover up that number of columns. A value of zero means that
37+
/// this cell is being covered up by a previous cell in the same row.
38+
var colspan: UInt {
39+
get {
40+
guard case let .tableCell(colspan, _) = _data.raw.markup.data else {
41+
fatalError("\(self) markup wrapped unexpected \(_data.raw)")
42+
}
43+
return colspan
44+
}
45+
set {
46+
_data = _data.replacingSelf(.tableCell(parsedRange: nil, colspan: newValue, rowspan: rowspan, _data.raw.markup.copyChildren()))
47+
}
48+
}
49+
50+
/// The number of rows this cell spans over.
51+
///
52+
/// A normal, non-spanning table cell has a `rowspan` of 1. A value greater than one indicates
53+
/// that this cell has expanded to cover up that number of rows. A value of zero means that
54+
/// this cell is being covered up by another cell in a row above it.
55+
var rowspan: UInt {
56+
get {
57+
guard case let .tableCell(_, rowspan) = _data.raw.markup.data else {
58+
fatalError("\(self) markup wrapped unexpected \(_data.raw)")
59+
}
60+
return rowspan
61+
}
62+
set {
63+
_data = _data.replacingSelf(.tableCell(parsedRange: nil, colspan: colspan, rowspan: newValue, _data.raw.markup.copyChildren()))
64+
}
65+
}
66+
3367
// MARK: BasicInlineContainer
3468

3569
init<Children>(_ children: Children) where Children : Sequence, Children.Element == InlineMarkup {
36-
try! self.init(RawMarkup.tableCell(parsedRange: nil, children.map { $0.raw.markup }))
70+
self.init(colspan: 1, rowspan: 1, children)
71+
}
72+
73+
init<Children>(colspan: UInt, rowspan: UInt, _ children: Children) where Children : Sequence, Children.Element == InlineMarkup {
74+
try! self.init(RawMarkup.tableCell(parsedRange: nil, colspan: colspan, rowspan: rowspan, children.map { $0.raw.markup }))
3775
}
3876

3977
// MARK: Visitation

Sources/Markdown/Parser/CommonMarkConverter.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,17 +570,22 @@ struct MarkupParser {
570570
precondition(state.nodeType == .tableCell)
571571
let parsedRange = state.range(state.node)
572572
let childConversion = convertChildren(state)
573+
let colspan = UInt(cmark_gfm_extensions_get_table_cell_colspan(state.node))
574+
let rowspan = UInt(cmark_gfm_extensions_get_table_cell_rowspan(state.node))
573575
precondition(childConversion.state.node == state.node)
574576
precondition(childConversion.state.event == CMARK_EVENT_EXIT)
575-
return MarkupConversion(state: childConversion.state.next(), result: .tableCell(parsedRange: parsedRange, childConversion.result))
577+
return MarkupConversion(state: childConversion.state.next(), result: .tableCell(parsedRange: parsedRange, colspan: colspan, rowspan: rowspan, childConversion.result))
576578
}
577579

578580
static func parseString(_ string: String, source: URL?, options: ParseOptions) -> Document {
579581
cmark_gfm_core_extensions_ensure_registered()
582+
583+
var cmarkOptions = CMARK_OPT_TABLE_SPANS
584+
if !options.contains(.disableSmartOpts) {
585+
cmarkOptions |= CMARK_OPT_SMART
586+
}
580587

581-
let parser = cmark_parser_new(options.contains(.disableSmartOpts)
582-
? CMARK_OPT_DEFAULT
583-
: CMARK_OPT_SMART)
588+
let parser = cmark_parser_new(cmarkOptions)
584589

585590
cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("table"))
586591
cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("strikethrough"))

Sources/Markdown/Walker/Walkers/MarkupFormatter.swift

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -899,15 +899,37 @@ public struct MarkupFormatter: MarkupWalker {
899899
$0.formatIndependently(options: cellFormattingOptions)
900900
}).ensuringCount(atLeast: uniformColumnCount, filler: "")
901901

902+
/// All of the column-span values from the head cells, adding cells as
903+
/// needed to meet the uniform `uniformColumnCount`.
904+
let headCellSpans = Array(table.head.cells.map {
905+
$0.colspan
906+
}).ensuringCount(atLeast: uniformColumnCount, filler: 1)
907+
902908
/// All of the independently formatted body cells' text by row, adding
903909
/// cells to each row to meet the `uniformColumnCount`.
904910
let bodyRowTexts = Array(table.body.rows.map { row -> [String] in
905911
return Array(row.cells.map {
906-
$0.formatIndependently(options: cellFormattingOptions)
912+
if $0.rowspan == 0 {
913+
// If this cell is being spanned over, replace its text
914+
// (which should be the empty string anyway) with the
915+
// rowspan marker.
916+
return "^"
917+
} else {
918+
return $0.formatIndependently(options: cellFormattingOptions)
919+
}
907920
}).ensuringCount(atLeast: uniformColumnCount,
908921
filler: "")
909922
})
910923

924+
/// All of the column- and row-span information for the body cells,
925+
/// cells to each row to meet the `uniformColumnCount`.
926+
let bodyRowSpans = Array(table.body.rows.map { row in
927+
return Array(row.cells.map {
928+
(colspan: $0.colspan, rowspan: $0.rowspan)
929+
}).ensuringCount(atLeast: uniformColumnCount,
930+
filler: (colspan: 1, rowspan: 1))
931+
})
932+
911933
// Next, calculate the maximum width of each column.
912934

913935
/// The column alignments of the table, filled out to `uniformColumnCount`.
@@ -952,15 +974,32 @@ public struct MarkupFormatter: MarkupWalker {
952974
}
953975
}
954976

977+
/// Calculate the width of the given column and colspan.
978+
///
979+
/// This adds up the appropriate column widths based on the given column span, including
980+
/// the default span of 1, where it will only return the `finalColumnWidths` value for the
981+
/// given `column`.
982+
func columnWidth(column: Int, colspan: Int) -> Int {
983+
let lastColumn = column + colspan
984+
return (column..<lastColumn).map({ finalColumnWidths[$0] }).reduce(0, { $0 + $1 })
985+
}
986+
955987
// We now know the width that each printed column will be.
956988

957989
/// Each of the header cells expanded to the right dimensions by
958990
/// extending each line with spaces to fit the uniform column width.
959991
let expandedHeaderCellTexts = (0..<uniformColumnCount)
960992
.map { column -> String in
961-
let minLineLength = finalColumnWidths[column]
962-
return headCellTexts[column]
963-
.ensuringCount(atLeast: minLineLength, filler: " ")
993+
let colspan = headCellSpans[column]
994+
if colspan == 0 {
995+
// If this cell is being spanned over, collapse it so it
996+
// can be filled with the spanning cell.
997+
return ""
998+
} else {
999+
let minLineLength = columnWidth(column: column, colspan: Int(colspan))
1000+
return headCellTexts[column]
1001+
.ensuringCount(atLeast: minLineLength, filler: " ")
1002+
}
9641003
}
9651004

9661005
/// Rendered delimter row cells with the correct width.
@@ -988,10 +1027,18 @@ public struct MarkupFormatter: MarkupWalker {
9881027
/// appropriately for their row and column.
9891028
let expandedBodyRowTexts = bodyRowTexts.enumerated()
9901029
.map { (row, rowCellTexts) -> [String] in
1030+
let rowSpans = bodyRowSpans[row]
9911031
return (0..<uniformColumnCount).map { column -> String in
992-
let minLineLength = finalColumnWidths[column]
993-
return rowCellTexts[column]
994-
.ensuringCount(atLeast: minLineLength, filler: " ")
1032+
let colspan = rowSpans[column].colspan
1033+
if colspan == 0 {
1034+
// If this cell is being spanned over, collapse it so it
1035+
// can be filled with the spanning cell.
1036+
return ""
1037+
} else {
1038+
let minLineLength = columnWidth(column: column, colspan: Int(colspan))
1039+
return rowCellTexts[column]
1040+
.ensuringCount(atLeast: minLineLength, filler: " ")
1041+
}
9951042
}
9961043
}
9971044

Sources/Markdown/Walker/Walkers/MarkupTreeDumper.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,20 @@ struct MarkupTreeDumper: MarkupWalker {
258258
mutating func visitSymbolLink(_ symbolLink: SymbolLink) {
259259
dump(symbolLink, customDescription: symbolLink.destination.map { "destination: \($0)" })
260260
}
261+
262+
mutating func visitTableCell(_ tableCell: Table.Cell) {
263+
var desc = ""
264+
if tableCell.colspan != 1 {
265+
desc += " colspan: \(tableCell.colspan)"
266+
}
267+
if tableCell.rowspan != 1 {
268+
desc += " rowspan: \(tableCell.rowspan)"
269+
}
270+
desc = desc.trimmingCharacters(in: .whitespaces)
271+
if !desc.isEmpty {
272+
dump(tableCell, customDescription: desc)
273+
} else {
274+
dump(tableCell)
275+
}
276+
}
261277
}

Tests/MarkdownTests/Block Nodes/TableTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,40 @@ class TableTests: XCTestCase {
205205
"""
206206
XCTAssertEqual(expectedDump, document.debugDescription(options: .printSourceLocations))
207207
}
208+
209+
func testParseCellSpans() {
210+
let source = """
211+
| one | two | three |
212+
| --- | --- | ----- |
213+
| big || small |
214+
| ^ || small |
215+
"""
216+
217+
let document = Document(parsing: source)
218+
219+
let expectedDump = """
220+
Document @1:1-4:22
221+
└─ Table @1:1-4:22 alignments: |-|-|-|
222+
├─ Head @1:1-1:22
223+
│ ├─ Cell @1:2-1:7
224+
│ │ └─ Text @1:3-1:6 "one"
225+
│ ├─ Cell @1:8-1:13
226+
│ │ └─ Text @1:9-1:12 "two"
227+
│ └─ Cell @1:14-1:21
228+
│ └─ Text @1:15-1:20 "three"
229+
└─ Body @3:1-4:22
230+
├─ Row @3:1-3:22
231+
│ ├─ Cell @3:2-3:12 colspan: 2 rowspan: 2
232+
│ │ └─ Text @3:3-3:6 "big"
233+
│ ├─ Cell @3:13-3:14 colspan: 0
234+
│ └─ Cell @3:14-3:21
235+
│ └─ Text @3:15-3:20 "small"
236+
└─ Row @4:1-4:22
237+
├─ Cell @4:2-4:12 colspan: 2 rowspan: 0
238+
├─ Cell @4:13-4:14 colspan: 0
239+
└─ Cell @4:14-4:21
240+
└─ Text @4:15-4:20 "small"
241+
"""
242+
XCTAssertEqual(expectedDump, document.debugDescription(options: .printSourceLocations))
243+
}
208244
}

Tests/MarkdownTests/Visitors/MarkupFormatterTests.swift

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,9 +1265,9 @@ class MarkupFormatterTableTests: XCTestCase {
12651265
│ └─ Link destination: "https://swift.org"
12661266
│ └─ Text "https://swift.org"
12671267
└─ Row
1268-
├─ Cell
1268+
├─ Cell colspan: 2
12691269
│ └─ InlineHTML <br/>
1270-
├─ Cell
1270+
├─ Cell colspan: 0
12711271
└─ Cell
12721272
"""
12731273
XCTAssertEqual(expectedDump, document.debugDescription())
@@ -1277,7 +1277,58 @@ class MarkupFormatterTableTests: XCTestCase {
12771277
|*A* |**B** |~C~ |
12781278
|:-------------------------|:--------------------:|------------------:|
12791279
|[Apple](https://apple.com)|![image](image.png "")|<https://swift.org>|
1280-
|<br/> | | |
1280+
|<br/> || |
1281+
"""
1282+
1283+
XCTAssertEqual(expected, formatted)
1284+
print(formatted)
1285+
1286+
let reparsed = Document(parsing: formatted)
1287+
print(reparsed.debugDescription())
1288+
XCTAssertTrue(document.hasSameStructure(as: reparsed))
1289+
}
1290+
1291+
func testRoundTripRowspan() {
1292+
let source = """
1293+
| one | two | three |
1294+
| --- | --- | ----- |
1295+
| big || small |
1296+
| ^ || small |
1297+
"""
1298+
1299+
let document = Document(parsing: source)
1300+
1301+
let expectedDump = """
1302+
Document
1303+
└─ Table alignments: |-|-|-|
1304+
├─ Head
1305+
│ ├─ Cell
1306+
│ │ └─ Text "one"
1307+
│ ├─ Cell
1308+
│ │ └─ Text "two"
1309+
│ └─ Cell
1310+
│ └─ Text "three"
1311+
└─ Body
1312+
├─ Row
1313+
│ ├─ Cell colspan: 2 rowspan: 2
1314+
│ │ └─ Text "big"
1315+
│ ├─ Cell colspan: 0
1316+
│ └─ Cell
1317+
│ └─ Text "small"
1318+
└─ Row
1319+
├─ Cell colspan: 2 rowspan: 0
1320+
├─ Cell colspan: 0
1321+
└─ Cell
1322+
└─ Text "small"
1323+
"""
1324+
XCTAssertEqual(expectedDump, document.debugDescription())
1325+
1326+
let formatted = document.format()
1327+
let expected = """
1328+
|one|two|three|
1329+
|---|---|-----|
1330+
|big ||small|
1331+
|^ ||small|
12811332
"""
12821333

12831334
XCTAssertEqual(expected, formatted)

0 commit comments

Comments
 (0)