import Foundation /// A table in Markdown, as GitHub's flavour has them: a line of cells, a line of /// dashes under it with one column to each, then a line of cells to each row. struct MarkdownTable: Hashable { enum Alignment: Hashable { case leading, center, trailing } let header: [String] let alignments: [Alignment] private(set) var rows: [[String]] = [] /// None when `dashes` isn't such a line, or doesn't match `header`'s columns, /// which leaves them both as words. init?(header line: String, dashes: String) { guard line.contains("|"), dashes.contains("|") else { return nil } let columns = Self.cells(dashes) guard columns.allSatisfy(Self.isDashes) else { return nil } header = Self.cells(line) guard header.count == columns.count else { return nil } alignments = columns.map { column in switch (column.hasPrefix(":"), column.hasSuffix(":")) { case (true, true): .center case (false, true): .trailing default: .leading } } } /// Dashes, with a colon at either end or both. private static func isDashes(_ column: String) -> Bool { var dashes = Substring(column) if dashes.hasPrefix(":") { dashes.removeFirst() } if dashes.hasSuffix(":") { dashes.removeLast() } return !dashes.isEmpty && dashes.allSatisfy { $0 == "-" } } /// A row has the table's columns, whatever its line had: those it lacks are /// empty, and those it has beyond them go. mutating func add(_ line: String) { let cells = Self.cells(line).prefix(header.count) rows.append(cells + Array(repeating: "", count: header.count - cells.count)) } /// What's between a line's pipes, but for one with a backslash before it, /// which is a pipe in the cell. Those at either end hold nothing. static func cells(_ line: String) -> [String] { var cells: [String] = [] var cell = "" var escaped = false for character in line { if escaped { cell += character == "|" ? "|" : "\\\(character)" escaped = false } else if character == "\\" { escaped = true } else if character == "|" { cells.append(cell) cell = "" } else { cell.append(character) } } cells.append(escaped ? cell + "\\" : cell) let trimmed = line.trimmingCharacters(in: .whitespaces) if trimmed.hasPrefix("|") { cells.removeFirst() } if trimmed.hasSuffix("|"), !trimmed.hasSuffix("\\|"), !cells.isEmpty { cells.removeLast() } return cells.map { $0.trimmingCharacters(in: .whitespaces) } } }