Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,14 @@ public enum RenderBlockContent: Equatable {
}

/// A table that contains a list of row data.
public struct Table: Equatable {
public struct Table {
/// The style of header in this table.
public var header: HeaderType
/// The text alignment of each column in this table.
///
/// A `nil` value for this property is the same as all the columns being
/// ``RenderBlockContent/ColumnAlignment/unset``.
Comment on lines +248 to +249
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be inferred as this type's Equatable conformance implementing this behavior. Maybe we can mention that this is the behavior in practice or something like that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we can actually override == and implement the behavior there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a Table (with 3 rows and alignments are [.unset, .unset, .unset]) equal to a Table (with the same 3 rows but alignments are nil)?

Their actual behavior is the same but the current default Equatable implementation is false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed up a new commit that implements this behavior into the Equatable conformance, and adds some tests to ensure that the behavior holds.

public var alignments: [ColumnAlignment]?
/// The rows in this table.
public var rows: [TableRow]
/// Any extended information that describes cells in this table.
Expand All @@ -251,11 +256,23 @@ public enum RenderBlockContent: Equatable {
public var metadata: RenderContentMetadata?

/// Creates a new table with the given data.
public init(header: HeaderType, rows: [TableRow], extendedData: Set<TableCellExtendedData>, metadata: RenderContentMetadata? = nil) {
///
/// - Parameters:
/// - header: The style of header in this table.
/// - rawAlignments: The text alignment for each column in this table. If all the
/// alignments are ``RenderBlockContent/ColumnAlignment/unset``, the ``alignments``
/// property will be set to `nil`.
/// - rows: The cell data for this table.
/// - extendedData: Any extended information that describes cells in this table.
/// - metadata: Additional metadata for this table, if necessary.
public init(header: HeaderType, rawAlignments: [ColumnAlignment]? = nil, rows: [TableRow], extendedData: Set<TableCellExtendedData>, metadata: RenderContentMetadata? = nil) {
self.header = header
self.rows = rows
self.extendedData = extendedData
self.metadata = metadata
if let alignments = rawAlignments, !alignments.allSatisfy({ $0 == .unset }) {
self.alignments = alignments
}
}
}

Expand Down Expand Up @@ -374,6 +391,18 @@ public enum RenderBlockContent: Equatable {
/// The table doesn't contain headers.
case none
}

/// The methods by which a table column can have its text aligned.
public enum ColumnAlignment: String, Codable, Equatable {
/// Force text alignment to be left-justified.
case left
/// Force text alignment to be right-justified.
case right
/// Force text alignment to be centered.
case center
/// Leave text alignment to the default.
case unset
}

/// A table row that contains a list of row cells.
public struct TableRow: Codable, Equatable {
Expand Down Expand Up @@ -548,11 +577,35 @@ public enum RenderBlockContent: Equatable {
}
}

extension RenderBlockContent.Table: Equatable {
public static func == (lhs: RenderBlockContent.Table, rhs: RenderBlockContent.Table) -> Bool {
guard lhs.header == rhs.header
&& lhs.extendedData == rhs.extendedData
&& lhs.metadata == rhs.metadata
&& lhs.rows == rhs.rows
else {
return false
}

var lhsAlignments = lhs.alignments
if let align = lhsAlignments, align.allSatisfy({ $0 == .unset }) {
lhsAlignments = nil
}

var rhsAlignments = rhs.alignments
if let align = rhsAlignments, align.allSatisfy({ $0 == .unset }) {
rhsAlignments = nil
}

return lhsAlignments == rhsAlignments
}
}

// Writing a manual Codable implementation for tables because the encoding of `extendedData` does
// not follow from the struct layout.
extension RenderBlockContent.Table: Codable {
enum CodingKeys: String, CodingKey {
case header, rows, extendedData, metadata
case header, alignments, rows, extendedData, metadata
}

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

self.header = try container.decode(RenderBlockContent.HeaderType.self, forKey: .header)

let rawAlignments = try container.decodeIfPresent([RenderBlockContent.ColumnAlignment].self, forKey: .alignments)
if let alignments = rawAlignments, !alignments.allSatisfy({ $0 == .unset }) {
self.alignments = alignments
} else {
self.alignments = nil
}

self.rows = try container.decode([RenderBlockContent.TableRow].self, forKey: .rows)
self.metadata = try container.decodeIfPresent(RenderContentMetadata.self, forKey: .metadata)

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

Expand Down
24 changes: 23 additions & 1 deletion Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,30 @@ struct RenderContentCompiler: MarkupVisitor {
}
rows.append(RenderBlockContent.TableRow(cells: cells))
}

var tempAlignments = [RenderBlockContent.ColumnAlignment]()
for alignment in table.columnAlignments {
switch alignment {
case .left: tempAlignments.append(.left)
case .right: tempAlignments.append(.right)
case .center: tempAlignments.append(.center)
case nil: tempAlignments.append(.unset)
}
}
while tempAlignments.count < table.maxColumnCount {
tempAlignments.append(.unset)
}
if tempAlignments.allSatisfy({ $0 == .unset }) {
tempAlignments = []
}
let alignments: [RenderBlockContent.ColumnAlignment]?
if tempAlignments.isEmpty {
alignments = nil
} else {
alignments = tempAlignments
}

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

mutating func visitStrikethrough(_ strikethrough: Strikethrough) -> [RenderContent] {
Expand Down
12 changes: 12 additions & 0 deletions Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,18 @@
"none"
]
},
"alignments": {
"type": "array",
"items": {
"type": "string",
"enum": [
"left",
"right",
"center",
"unset"
]
}
},
"rows": {
"type": "array",
"items": {
Expand Down
117 changes: 117 additions & 0 deletions Tests/SwiftDocCTests/Model/RenderContentMetadataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class RenderContentMetadataTests: XCTestCase {
XCTAssertEqual(t.rows[0].cells.map(renderCell), ["Column 1", "Column 2"])
XCTAssertEqual(t.rows[1].cells.map(renderCell), ["Cell 1", "Cell 2"])
XCTAssertEqual(t.rows[2].cells.map(renderCell), ["Cell 3", "Cell 4"])
XCTAssertNil(t.alignments)
default: XCTFail("Unexpected element")
}
}
Expand Down Expand Up @@ -152,11 +153,127 @@ class RenderContentMetadataTests: XCTestCase {
for expectedData in expectedExtendedData {
XCTAssert(t.extendedData.contains(expectedData))
}
XCTAssertNil(t.alignments)
default: XCTFail("Unexpected element")
}

try assertRoundTripCoding(renderedTable)
}

func testRenderingTableColumnAlignments() throws {
let (bundle, context) = try testBundleAndContext(named: "TestBundle")
var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/path", fragment: nil, sourceLanguage: .swift))

let source = """
| one | two | three | four |
| :-- | --: | :---: | ---- |
| one | two | three | four |
"""
let document = Document(parsing: source)

// Verifies that a markdown table renders correctly.

let result = try XCTUnwrap(renderContentCompiler.visit(document.child(at: 0)!))
let renderedTable = try XCTUnwrap(result.first as? RenderBlockContent)

let renderCell: ([RenderBlockContent]) -> String = { cell in
return cell.reduce(into: "") { (result, element) in
switch element {
case .paragraph(let p):
guard let para = p.inlineContent.first else { return }
result.append(para.plainText)
default: XCTFail("Unexpected element"); return
}
}
}

switch renderedTable {
case .table(let t):
XCTAssertEqual(t.header, .row)
XCTAssertEqual(t.rows.count, 2)
guard t.rows.count == 2 else { return }
XCTAssertEqual(t.rows[0].cells.map(renderCell), ["one", "two", "three", "four"])
XCTAssertEqual(t.rows[1].cells.map(renderCell), ["one", "two", "three", "four"])
XCTAssertEqual(t.alignments, [.left, .right, .center, .unset])
default: XCTFail("Unexpected element")
}

try assertRoundTripCoding(renderedTable)
}

/// Verifies that a table with `nil` alignments and a table with all-unset alignments still compare as equal.
func testRenderedTableEquality() throws {
let (bundle, context) = try testBundleAndContext(named: "TestBundle")
var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/path", fragment: nil, sourceLanguage: .swift))

let source = """
| Column 1 | Column 2 |
| ------------- | ------------- |
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |
"""
let document = Document(parsing: source)

let result = try XCTUnwrap(renderContentCompiler.visit(document.child(at: 0)!))
let renderedTable = try XCTUnwrap(result.first as? RenderBlockContent)
guard case let .table(decodedTable) = renderedTable else {
XCTFail("Unexpected RenderBlockContent element")
return
}
XCTAssertNil(decodedTable.alignments)
var modifiedTable = decodedTable
modifiedTable.alignments = [.unset, .unset]

XCTAssertEqual(decodedTable, modifiedTable)
}

/// Verifies that two tables with otherwise-identical contents but different column alignments compare as unequal.
func testRenderedTableInequality() throws {
let (bundle, context) = try testBundleAndContext(named: "TestBundle")
var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/path", fragment: nil, sourceLanguage: .swift))

let decodedTableWithUnsetColumns: RenderBlockContent.Table
do {
let source = """
| Column 1 | Column 2 |
| ------------- | ------------- |
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |
"""
let document = Document(parsing: source)

let result = try XCTUnwrap(renderContentCompiler.visit(document.child(at: 0)!))
let renderedTable = try XCTUnwrap(result.first as? RenderBlockContent)
guard case let .table(decodedTable) = renderedTable else {
XCTFail("Unexpected RenderBlockContent element")
return
}
decodedTableWithUnsetColumns = decodedTable
}

let decodedTableWithLeftColumns: RenderBlockContent.Table
do {
let source = """
| Column 1 | Column 2 |
| :------------ | :------------ |
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |
"""
let document = Document(parsing: source)

// Verifies that a markdown table renders correctly.

let result = try XCTUnwrap(renderContentCompiler.visit(document.child(at: 0)!))
let renderedTable = try XCTUnwrap(result.first as? RenderBlockContent)
guard case let .table(decodedTable) = renderedTable else {
XCTFail("Unexpected RenderBlockContent element")
return
}
decodedTableWithLeftColumns = decodedTable
}

XCTAssertNotEqual(decodedTableWithUnsetColumns, decodedTableWithLeftColumns)
}

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