Sources/OrgCore/Export/TableEl.swift
53 lines · 2794 bytes
1import Foundation
2
3/// A table.el table's cells, read from its drawing: each cell's text lines and the grid rows
4/// and columns it spans, as `table-generate-source` finds them.
5enum TableEl {
6 struct Cell: Equatable {
7 var row: Int
8 var lines: [String]
9 var colspan: Int
10 var rowspan: Int
11 }
12
13 /// The cells in reading order, or nil when the drawing isn't a well-formed table.
14 static func cells(_ text: String) -> [Cell]? {
15 let raw = text.components(separatedBy: "\n").filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
16 let indent = raw.map { $0.prefix { $0 == " " || $0 == "\t" }.count }.min() ?? 0
17 let grid = raw.map { Array($0.dropFirst(indent)) }
18 guard grid.count >= 3 else { return nil }
19 func at(_ r: Int, _ c: Int) -> Character? { r >= 0 && r < grid.count && c >= 0 && c < grid[r].count ? grid[r][c] : nil }
20 func vertical(_ r: Int, _ c: Int) -> Bool { at(r, c) == "|" || at(r, c) == "+" }
21 func horizontal(_ r: Int, _ c: Int) -> Bool { at(r, c) == "-" || at(r, c) == "+" }
22 // Grid lines: rows holding a horizontal border, columns holding a corner.
23 var rows: [Int] = []
24 var columns: Set<Int> = []
25 for (r, line) in grid.enumerated() {
26 for (c, ch) in line.enumerated() where ch == "+" {
27 columns.insert(c)
28 if horizontal(r, c + 1) || horizontal(r, c - 1) { if rows.last != r { rows.append(r) } }
29 }
30 }
31 var cells: [Cell] = []
32 for r in rows {
33 for c in grid[r].indices where grid[r][c] == "+" && at(r, c + 1) == "-" && vertical(r + 1, c) {
34 // Right edge: the next corner along the top with a border below it.
35 var x2 = c + 1
36 while x2 < grid[r].count, !(grid[r][x2] == "+" && vertical(r + 1, x2)) { x2 += 1 }
37 guard x2 < grid[r].count else { return nil }
38 // Bottom edge: the next corner down the left side with a border to its right.
39 var r2 = r + 1
40 while r2 < grid.count, !(at(r2, c) == "+" && horizontal(r2, c + 1)) { r2 += 1 }
41 guard r2 < grid.count, r2 > r + 1 else { return nil }
42 let lines = (r + 1..<r2).map { line -> String in
43 let row = grid[line]
44 let end = min(x2, row.count)
45 return c + 1 < end ? String(row[(c + 1)..<end]).trimmingCharacters(in: .whitespaces) : ""
46 }
47 cells.append(Cell(row: r, lines: lines, colspan: columns.filter { $0 > c && $0 < x2 }.count + 1,
48 rowspan: rows.filter { $0 > r && $0 < r2 }.count + 1))
49 }
50 }
51 return cells.isEmpty ? nil : cells
52 }
53}