Skip to content

Commit ca2bc77

Browse files
emit table column alignments into Render JSON (#389)
* emit table column alignments into Render JSON rdar://77749746 * review: make alignments parameter optional * review: assert that all-unset column alignments result in nil * document Table initializer and alignments property to describe nil * filter out all-unset alignments in Table's decoder * ensure that all-unset alignments and no alignments compare equal
1 parent f5238c4 commit ca2bc77

File tree

4 files changed

+219
-4
lines changed

4 files changed

+219
-4
lines changed

Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,14 @@ public enum RenderBlockContent: Equatable {
240240
}
241241

242242
/// A table that contains a list of row data.
243-
public struct Table: Equatable {
243+
public struct Table {
244244
/// The style of header in this table.
245245
public var header: HeaderType
246+
/// The text alignment of each column in this table.
247+
///
248+
/// A `nil` value for this property is the same as all the columns being
249+
/// ``RenderBlockContent/ColumnAlignment/unset``.
250+
public var alignments: [ColumnAlignment]?
246251
/// The rows in this table.
247252
public var rows: [TableRow]
248253
/// Any extended information that describes cells in this table.
@@ -251,11 +256,23 @@ public enum RenderBlockContent: Equatable {
251256
public var metadata: RenderContentMetadata?
252257

253258
/// Creates a new table with the given data.
254-
public init(header: HeaderType, rows: [TableRow], extendedData: Set<TableCellExtendedData>, metadata: RenderContentMetadata? = nil) {
259+
///
260+
/// - Parameters:
261+
/// - header: The style of header in this table.
262+
/// - rawAlignments: The text alignment for each column in this table. If all the
263+
/// alignments are ``RenderBlockContent/ColumnAlignment/unset``, the ``alignments``
264+
/// property will be set to `nil`.
265+
/// - rows: The cell data for this table.
266+
/// - extendedData: Any extended information that describes cells in this table.
267+
/// - metadata: Additional metadata for this table, if necessary.
268+
public init(header: HeaderType, rawAlignments: [ColumnAlignment]? = nil, rows: [TableRow], extendedData: Set<TableCellExtendedData>, metadata: RenderContentMetadata? = nil) {
255269
self.header = header
256270
self.rows = rows
257271
self.extendedData = extendedData
258272
self.metadata = metadata
273+
if let alignments = rawAlignments, !alignments.allSatisfy({ $0 == .unset }) {
274+
self.alignments = alignments
275+
}
259276
}
260277
}
261278

@@ -374,6 +391,18 @@ public enum RenderBlockContent: Equatable {
374391
/// The table doesn't contain headers.
375392
case none
376393
}
394+
395+
/// The methods by which a table column can have its text aligned.
396+
public enum ColumnAlignment: String, Codable, Equatable {
397+
/// Force text alignment to be left-justified.
398+
case left
399+
/// Force text alignment to be right-justified.
400+
case right
401+
/// Force text alignment to be centered.
402+
case center
403+
/// Leave text alignment to the default.
404+
case unset
405+
}
377406

378407
/// A table row that contains a list of row cells.
379408
public struct TableRow: Codable, Equatable {
@@ -548,11 +577,35 @@ public enum RenderBlockContent: Equatable {
548577
}
549578
}
550579

580+
extension RenderBlockContent.Table: Equatable {
581+
public static func == (lhs: RenderBlockContent.Table, rhs: RenderBlockContent.Table) -> Bool {
582+
guard lhs.header == rhs.header
583+
&& lhs.extendedData == rhs.extendedData
584+
&& lhs.metadata == rhs.metadata
585+
&& lhs.rows == rhs.rows
586+
else {
587+
return false
588+
}
589+
590+
var lhsAlignments = lhs.alignments
591+
if let align = lhsAlignments, align.allSatisfy({ $0 == .unset }) {
592+
lhsAlignments = nil
593+
}
594+
595+
var rhsAlignments = rhs.alignments
596+
if let align = rhsAlignments, align.allSatisfy({ $0 == .unset }) {
597+
rhsAlignments = nil
598+
}
599+
600+
return lhsAlignments == rhsAlignments
601+
}
602+
}
603+
551604
// Writing a manual Codable implementation for tables because the encoding of `extendedData` does
552605
// not follow from the struct layout.
553606
extension RenderBlockContent.Table: Codable {
554607
enum CodingKeys: String, CodingKey {
555-
case header, rows, extendedData, metadata
608+
case header, alignments, rows, extendedData, metadata
556609
}
557610

558611
// TableCellExtendedData encodes the row and column indices as a dynamic key with the format "{row}_{column}".
@@ -589,6 +642,14 @@ extension RenderBlockContent.Table: Codable {
589642
let container = try decoder.container(keyedBy: CodingKeys.self)
590643

591644
self.header = try container.decode(RenderBlockContent.HeaderType.self, forKey: .header)
645+
646+
let rawAlignments = try container.decodeIfPresent([RenderBlockContent.ColumnAlignment].self, forKey: .alignments)
647+
if let alignments = rawAlignments, !alignments.allSatisfy({ $0 == .unset }) {
648+
self.alignments = alignments
649+
} else {
650+
self.alignments = nil
651+
}
652+
592653
self.rows = try container.decode([RenderBlockContent.TableRow].self, forKey: .rows)
593654
self.metadata = try container.decodeIfPresent(RenderContentMetadata.self, forKey: .metadata)
594655

@@ -610,6 +671,9 @@ extension RenderBlockContent.Table: Codable {
610671
public func encode(to encoder: Encoder) throws {
611672
var container = encoder.container(keyedBy: CodingKeys.self)
612673
try container.encode(header, forKey: .header)
674+
if let alignments = alignments, !alignments.isEmpty, !alignments.allSatisfy({ $0 == .unset }) {
675+
try container.encode(alignments, forKey: .alignments)
676+
}
613677
try container.encode(rows, forKey: .rows)
614678
try container.encodeIfPresent(metadata, forKey: .metadata)
615679

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,30 @@ struct RenderContentCompiler: MarkupVisitor {
262262
}
263263
rows.append(RenderBlockContent.TableRow(cells: cells))
264264
}
265+
266+
var tempAlignments = [RenderBlockContent.ColumnAlignment]()
267+
for alignment in table.columnAlignments {
268+
switch alignment {
269+
case .left: tempAlignments.append(.left)
270+
case .right: tempAlignments.append(.right)
271+
case .center: tempAlignments.append(.center)
272+
case nil: tempAlignments.append(.unset)
273+
}
274+
}
275+
while tempAlignments.count < table.maxColumnCount {
276+
tempAlignments.append(.unset)
277+
}
278+
if tempAlignments.allSatisfy({ $0 == .unset }) {
279+
tempAlignments = []
280+
}
281+
let alignments: [RenderBlockContent.ColumnAlignment]?
282+
if tempAlignments.isEmpty {
283+
alignments = nil
284+
} else {
285+
alignments = tempAlignments
286+
}
265287

266-
return [RenderBlockContent.table(.init(header: .row, rows: [RenderBlockContent.TableRow(cells: headerCells)] + rows, extendedData: extendedData, metadata: nil))]
288+
return [RenderBlockContent.table(.init(header: .row, rawAlignments: alignments, rows: [RenderBlockContent.TableRow(cells: headerCells)] + rows, extendedData: extendedData, metadata: nil))]
267289
}
268290

269291
mutating func visitStrikethrough(_ strikethrough: Strikethrough) -> [RenderContent] {

Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,18 @@
949949
"none"
950950
]
951951
},
952+
"alignments": {
953+
"type": "array",
954+
"items": {
955+
"type": "string",
956+
"enum": [
957+
"left",
958+
"right",
959+
"center",
960+
"unset"
961+
]
962+
}
963+
},
952964
"rows": {
953965
"type": "array",
954966
"items": {

Tests/SwiftDocCTests/Model/RenderContentMetadataTests.swift

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class RenderContentMetadataTests: XCTestCase {
102102
XCTAssertEqual(t.rows[0].cells.map(renderCell), ["Column 1", "Column 2"])
103103
XCTAssertEqual(t.rows[1].cells.map(renderCell), ["Cell 1", "Cell 2"])
104104
XCTAssertEqual(t.rows[2].cells.map(renderCell), ["Cell 3", "Cell 4"])
105+
XCTAssertNil(t.alignments)
105106
default: XCTFail("Unexpected element")
106107
}
107108
}
@@ -152,11 +153,127 @@ class RenderContentMetadataTests: XCTestCase {
152153
for expectedData in expectedExtendedData {
153154
XCTAssert(t.extendedData.contains(expectedData))
154155
}
156+
XCTAssertNil(t.alignments)
155157
default: XCTFail("Unexpected element")
156158
}
157159

158160
try assertRoundTripCoding(renderedTable)
159161
}
162+
163+
func testRenderingTableColumnAlignments() throws {
164+
let (bundle, context) = try testBundleAndContext(named: "TestBundle")
165+
var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/path", fragment: nil, sourceLanguage: .swift))
166+
167+
let source = """
168+
| one | two | three | four |
169+
| :-- | --: | :---: | ---- |
170+
| one | two | three | four |
171+
"""
172+
let document = Document(parsing: source)
173+
174+
// Verifies that a markdown table renders correctly.
175+
176+
let result = try XCTUnwrap(renderContentCompiler.visit(document.child(at: 0)!))
177+
let renderedTable = try XCTUnwrap(result.first as? RenderBlockContent)
178+
179+
let renderCell: ([RenderBlockContent]) -> String = { cell in
180+
return cell.reduce(into: "") { (result, element) in
181+
switch element {
182+
case .paragraph(let p):
183+
guard let para = p.inlineContent.first else { return }
184+
result.append(para.plainText)
185+
default: XCTFail("Unexpected element"); return
186+
}
187+
}
188+
}
189+
190+
switch renderedTable {
191+
case .table(let t):
192+
XCTAssertEqual(t.header, .row)
193+
XCTAssertEqual(t.rows.count, 2)
194+
guard t.rows.count == 2 else { return }
195+
XCTAssertEqual(t.rows[0].cells.map(renderCell), ["one", "two", "three", "four"])
196+
XCTAssertEqual(t.rows[1].cells.map(renderCell), ["one", "two", "three", "four"])
197+
XCTAssertEqual(t.alignments, [.left, .right, .center, .unset])
198+
default: XCTFail("Unexpected element")
199+
}
200+
201+
try assertRoundTripCoding(renderedTable)
202+
}
203+
204+
/// Verifies that a table with `nil` alignments and a table with all-unset alignments still compare as equal.
205+
func testRenderedTableEquality() throws {
206+
let (bundle, context) = try testBundleAndContext(named: "TestBundle")
207+
var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/path", fragment: nil, sourceLanguage: .swift))
208+
209+
let source = """
210+
| Column 1 | Column 2 |
211+
| ------------- | ------------- |
212+
| Cell 1 | Cell 2 |
213+
| Cell 3 | Cell 4 |
214+
"""
215+
let document = Document(parsing: source)
216+
217+
let result = try XCTUnwrap(renderContentCompiler.visit(document.child(at: 0)!))
218+
let renderedTable = try XCTUnwrap(result.first as? RenderBlockContent)
219+
guard case let .table(decodedTable) = renderedTable else {
220+
XCTFail("Unexpected RenderBlockContent element")
221+
return
222+
}
223+
XCTAssertNil(decodedTable.alignments)
224+
var modifiedTable = decodedTable
225+
modifiedTable.alignments = [.unset, .unset]
226+
227+
XCTAssertEqual(decodedTable, modifiedTable)
228+
}
229+
230+
/// Verifies that two tables with otherwise-identical contents but different column alignments compare as unequal.
231+
func testRenderedTableInequality() throws {
232+
let (bundle, context) = try testBundleAndContext(named: "TestBundle")
233+
var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/path", fragment: nil, sourceLanguage: .swift))
234+
235+
let decodedTableWithUnsetColumns: RenderBlockContent.Table
236+
do {
237+
let source = """
238+
| Column 1 | Column 2 |
239+
| ------------- | ------------- |
240+
| Cell 1 | Cell 2 |
241+
| Cell 3 | Cell 4 |
242+
"""
243+
let document = Document(parsing: source)
244+
245+
let result = try XCTUnwrap(renderContentCompiler.visit(document.child(at: 0)!))
246+
let renderedTable = try XCTUnwrap(result.first as? RenderBlockContent)
247+
guard case let .table(decodedTable) = renderedTable else {
248+
XCTFail("Unexpected RenderBlockContent element")
249+
return
250+
}
251+
decodedTableWithUnsetColumns = decodedTable
252+
}
253+
254+
let decodedTableWithLeftColumns: RenderBlockContent.Table
255+
do {
256+
let source = """
257+
| Column 1 | Column 2 |
258+
| :------------ | :------------ |
259+
| Cell 1 | Cell 2 |
260+
| Cell 3 | Cell 4 |
261+
"""
262+
let document = Document(parsing: source)
263+
264+
// Verifies that a markdown table renders correctly.
265+
266+
let result = try XCTUnwrap(renderContentCompiler.visit(document.child(at: 0)!))
267+
let renderedTable = try XCTUnwrap(result.first as? RenderBlockContent)
268+
guard case let .table(decodedTable) = renderedTable else {
269+
XCTFail("Unexpected RenderBlockContent element")
270+
return
271+
}
272+
decodedTableWithLeftColumns = decodedTable
273+
}
274+
275+
XCTAssertNotEqual(decodedTableWithUnsetColumns, decodedTableWithLeftColumns)
276+
}
160277

161278
func testStrikethrough() throws {
162279
let (bundle, context) = try testBundleAndContext(named: "TestBundle")

0 commit comments

Comments
 (0)