From b831e98523e5ed0f8919236c6b32eb636e8889a2 Mon Sep 17 00:00:00 2001 From: Assaf Inbal Date: Mon, 27 Jul 2026 13:10:53 +0300 Subject: [PATCH] fix(tables): render headerless tables (`| | |`) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A table whose header row is entirely empty cells vanished from the render. The parser took its column count from the header row, and parseTableRow trims trailing empty cells — so `| | |` collapsed to zero cells, every data row was normalized to zero columns, and the table views iterated an empty range. Only the border and header divider showed. - Parser: the separator row is authoritative for column count; the header row is padded to that width like data rows already were. - TableBlockView / PrintableTableView: skip the header band entirely when every header cell is blank, so a headerless table reads as the definition-list layout it is meant to be rather than an empty strip. Both also derive columnCount defensively and index headers safely. Co-Authored-By: Claude Opus 5 --- QuickMD/QuickMD/MarkdownBlockParser.swift | 10 ++-- QuickMD/QuickMD/MarkdownExport.swift | 40 ++++++++------ QuickMD/QuickMD/Views/TableBlockView.swift | 62 +++++++++++++--------- QuickMD/QuickMDTests/ParserTests.swift | 19 +++++++ 4 files changed, 86 insertions(+), 45 deletions(-) diff --git a/QuickMD/QuickMD/MarkdownBlockParser.swift b/QuickMD/QuickMD/MarkdownBlockParser.swift index 2226306..063b490 100644 --- a/QuickMD/QuickMD/MarkdownBlockParser.swift +++ b/QuickMD/QuickMD/MarkdownBlockParser.swift @@ -239,9 +239,12 @@ struct MarkdownBlockParser: Sendable { if !isTableSeparator(trimmed) && i + 1 < lines.count && isTableSeparator(lines[i + 1]) { flushTextBuffer(&textBuffer, to: &blocks, index: &blockIndex, using: activeRenderer) - let headers = parseTableRow(line) - let columnCount = headers.count var alignments = parseTableAlignments(lines[i + 1]) + // The separator row is authoritative for column count: a header row + // of entirely empty cells (`| | |`, a headerless table) parses to zero + // cells, which would otherwise collapse the whole table to nothing. + var headers = parseTableRow(line) + let columnCount = max(headers.count, alignments.count) var rows: [[String]] = [] i += 2 // Skip header and separator @@ -250,7 +253,8 @@ struct MarkdownBlockParser: Sendable { i += 1 } - // Normalize: ensure all rows and alignments match column count + // Normalize: ensure headers, rows, and alignments match column count + headers = normalizeArray(headers, to: columnCount, default: "") alignments = normalizeArray(alignments, to: columnCount, default: .leading) rows = rows.map { normalizeArray($0, to: columnCount, default: "") } diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index 52fccf2..e891528 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -147,36 +147,42 @@ struct PrintableTableView: View, TableAlignmentProvider { /// Stored column count - computed once on init for efficiency private let columnCount: Int + /// Headerless tables (`| | |`) skip the header band entirely + private let showsHeader: Bool + init(headers: [String], rows: [[String]], alignments: [TextAlignment]) { self.headers = headers self.rows = rows self.alignments = alignments self.renderer = MarkdownRenderer(colorScheme: .light) - self.columnCount = headers.count + self.columnCount = max(headers.count, alignments.count, rows.map(\.count).max() ?? 0) + self.showsHeader = headers.contains { !$0.trimmingCharacters(in: .whitespaces).isEmpty } } var body: some View { VStack(spacing: 0) { - // Header row - HStack(spacing: 0) { - ForEach(0.. String { + index < headers.count ? headers[index] : "" + } + // MARK: - Per-Cell Occurrence Tracking private struct CellOffsets { @@ -146,7 +158,7 @@ struct TableBlockView: View, TableAlignmentProvider { var headerOffsets: [Int] = [] for i in 0..