Skip to content

Commit

Permalink
Pretty Markdown tables (#5)
Browse files Browse the repository at this point in the history
* Pretty Markdown tables

* Update README

* Improve coverage
  • Loading branch information
eneko authored Nov 29, 2018
1 parent 1fc31be commit a8a5bcf
Show file tree
Hide file tree
Showing 15 changed files with 127 additions and 35 deletions.
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,21 @@ print(table.markdown)

Generates the following output:

| | Name | Department |
| - | ---- | ---------- |
| 🍏 | Apple | Fruits |
| 🍊 | Orange | Fruits |
| 🥖 | Bread | Bakery |
| | Name | Department |
| -- | ------ | ---------- |
| 🍏 | Apple | Fruits |
| 🍊 | Orange | Fruits |
| 🥖 | Bread | Bakery |

Which renders as:

| | Name | Department |
| - | ---- | ---------- |
| 🍏 | Apple | Fruits |
| 🍊 | Orange | Fruits |
| 🥖 | Bread | Bakery |
| | Name | Department |
| -- | ------ | ---------- |
| 🍏 | Apple | Fruits |
| 🍊 | Orange | Fruits |
| 🥖 | Bread | Bakery |

Pretty tables 🎉

### Blockquotes

Expand Down Expand Up @@ -162,8 +163,8 @@ Generates the following output:

| Name | Count |
| ---- | ----- |
| Dog | 1 |
| Cat | 2 |
| Dog | 1 |
| Cat | 2 |

```swift
let foo = Bar()
Expand All @@ -182,8 +183,8 @@ Which renders as (click to expand):

| Name | Count |
| ---- | ----- |
| Dog | 1 |
| Cat | 2 |
| Dog | 1 |
| Cat | 2 |

```swift
let foo = Bar()
Expand Down
72 changes: 61 additions & 11 deletions Sources/MarkdownGenerator/MarkdownTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import Foundation

/// Render a two dimensional Markdown table.
///
/// | | Name | Department |
/// | - | ---- | ---------- |
/// | 🍏 | Apple | Fruits |
/// | 🍊 | Orange | Fruits |
/// | 🥖 | Bread | Bakery |
/// | | Name | Department |
/// | -- | ------ | ---------- |
/// | 🍏 | Apple | Fruits |
/// | 🍊 | Orange | Fruits |
/// | 🥖 | Bread | Bakery |
///
/// *Notes*:
/// - Markdown tables are not supported by all Markdown readers.
Expand All @@ -40,10 +40,14 @@ public struct MarkdownTable: MarkdownConvertible {

/// Generated Markdown output
public var markdown: String {
let headerRow = makeRow(values: headers)
let separatorRow = makeRow(values: headers.map { Array(repeating: "-", count: $0.count).joined() })
let columnWidths = computeColumnWidths()
if columnWidths.isEmpty {
return .newLine
}
let headerRow = makeRow(values: pad(values: headers, lengths: columnWidths))
let separatorRow = makeRow(values: columnWidths.map { String(repeating: "-", count: $0) })
let dataRows = data.map { columns in
return makeRow(values: columns)
return makeRow(values: pad(values: columns, lengths: columnWidths))
}
return """
\(headerRow)
Expand All @@ -52,10 +56,56 @@ public struct MarkdownTable: MarkdownConvertible {
"""
}

// Convert a String array into a markdown formatter table row.
// Table cells cannot contain multiple lines. New line characters are replaced by a space.
private func makeRow(values: [String]) -> String {
/// Return max length for each column, counting individual UTF16 characters for better emoji support.
///
/// - Returns: Array of column widths
func computeColumnWidths() -> [Int] {
let rows = [headers] + data
guard let maxColumns = rows.map({ $0.count }).max(), maxColumns > 0 else {
return []
}
let columnWidths = (0..<maxColumns).map { columnIndex -> Int in
return columnLength(values: rows.compactMap({ $0.get(at: columnIndex) }))
}
return columnWidths
}

func columnLength(values: [String]) -> Int {
return values.map({ $0.utf16.count }).max() ?? 0
}

/// Pad array of strings to a given length, counting individual UTF16 characters
///
/// - Parameters:
/// - values: array of strings to pad
/// - lengths: desired lengths
/// - Returns: array of right-padded strings
func pad(values: [String], lengths: [Int]) -> [String] {
var values = values
while values.count < lengths.count {
values.append("")
}
return zip(values, lengths).map { value, length in
value + String(repeating: " ", count: max(0, length - value.utf16.count))
}
}

/// Convert a String array into a markdown formatter table row.
/// Table cells cannot contain multiple lines. New line characters are replaced by a space.
///
/// - Parameter values: array of values
/// - Returns: Markdown formatted row
func makeRow(values: [String]) -> String {
let values = values.map { $0.replacingOccurrences(of: String.newLine, with: " ") }
return "| " + values.joined(separator: " | ") + " |"
}
}

extension Array {
func get(at index: Int) -> Element? {
guard (0..<count).contains(index) else {
return nil
}
return self[index]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// MarkdownTableTests.swift
// MarkdownGeneratorTests
//
// Created by Eneko Alonso on 11/28/18.
//

import XCTest
@testable import MarkdownGenerator

class MarkdownTableInternalTests: XCTestCase {

func testEmptyColumnLength() {
let table = MarkdownTable(headers: [], data: [])
XCTAssertEqual(table.columnLength(values: []), 0)
}

func testEmptyColumnWidths() {
let table = MarkdownTable(headers: [], data: [])
XCTAssertEqual(table.computeColumnWidths(), [])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ class MarkdownCollapsibleSectionTests: XCTestCase {
| Name | Count |
| ---- | ----- |
| Dog | 1 |
| Cat | 2 |
| Dog | 1 |
| Cat | 2 |
```swift
let foo = Bar()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ import MarkdownGenerator

class MarkdownTableTests: XCTestCase {

func testEmptyTable() {
let table = MarkdownTable(headers: [], data: [])
XCTAssertEqual(table.markdown, "\n")
}

func test1x1Table() {
let data: [[String]] = [[]]
let table = MarkdownTable(headers: ["Header"], data: data)

let output = """
| Header |
| ------ |
| |
| |
"""

XCTAssertEqual(table.markdown, output)
Expand All @@ -32,11 +37,11 @@ class MarkdownTableTests: XCTestCase {
let table = MarkdownTable(headers: ["", "Name", "Department"], data: data)

let output = """
| | Name | Department |
| - | ---- | ---------- |
| 🍏 | Apple | Fruits |
| 🍊 | Orange | Fruits |
| 🥖 | Bread | Bakery |
| | Name | Department |
| -- | ------ | ---------- |
| 🍏 | Apple | Fruits |
| 🍊 | Orange | Fruits |
| 🥖 | Bread | Bakery |
"""

XCTAssertEqual(table.markdown, output)
Expand All @@ -51,8 +56,8 @@ class MarkdownTableTests: XCTestCase {
let table = MarkdownTable(headers: ["Single-line", "Multi-line"], data: data)

let output = """
| Single-line | Multi-line |
| ----------- | ---------- |
| Single-line | Multi-line |
| ----------------- | ----------------- |
| Single-line value | Multi-line value |
| Single-line value | Multi-line value |
| Single-line value | Multi-line value |
Expand All @@ -61,6 +66,20 @@ class MarkdownTableTests: XCTestCase {
XCTAssertEqual(table.markdown, output)
}

func testMixedTable() {
let table = MarkdownTable(headers: ["Foo"], data: [["Bar"], [], ["Baz", "Bax"]])

let output = """
| Foo | |
| --- | --- |
| Bar | |
| | |
| Baz | Bax |
"""

XCTAssertEqual(table.markdown, output)
}

static var allTests = [
("test1x1Table", test1x1Table),
("test3x3Table", test3x3Table),
Expand Down

0 comments on commit a8a5bcf

Please sign in to comment.