Skip to content

Commit 20ac3a1

Browse files
committed
MDS060: tighten ATX-heading guard and post-prefix indent
Two header-detection bugs surfaced by Copilot review: - `isHeader` rejected any line whose trimmed content started with `#`, but `#1 | Title` is a valid first cell, not an ATX heading. Limit the guard to actual ATX shape — one to six `#` followed by space, tab, or end of line — via a new `isATXHeading` helper. - `parseRow` checked `HasPrefix(c, "|")` against the un-trimmed row content, so a row like `> | a | b |` (extra indent after the blockquote marker) had `leading` come out false even though `logicalCells` already trimmed and treated it as a real edge. Trim the same way before edge detection. Addresses Copilot review feedback on PR #353. https://claude.ai/code/session_012X1wVbY7u9DNMpGRhNurzT
1 parent ec34d42 commit 20ac3a1

2 files changed

Lines changed: 53 additions & 4 deletions

File tree

internal/rules/tablestructure/rule.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,12 +400,32 @@ func isHeader(line []byte, prefix string) bool {
400400
if c == "" || !containsUnescapedPipe(c) {
401401
return false
402402
}
403-
if strings.HasPrefix(strings.TrimSpace(c), "#") {
404-
return false // ATX heading, not a table header
403+
if isATXHeading(c) {
404+
return false
405405
}
406406
return !isSeparatorContent(c)
407407
}
408408

409+
// isATXHeading reports whether s has the shape of a CommonMark ATX
410+
// heading: one to six `#` characters followed by a space, tab, or
411+
// end-of-line. A bare `#` at the start (e.g. `#1 | x`) is not a
412+
// heading and must not exclude a candidate from table parsing.
413+
func isATXHeading(s string) bool {
414+
s = strings.TrimSpace(s)
415+
n := 0
416+
for n < len(s) && n < 6 && s[n] == '#' {
417+
n++
418+
}
419+
if n == 0 {
420+
return false
421+
}
422+
if n == len(s) {
423+
return true // bare hashes, empty heading
424+
}
425+
c := s[n]
426+
return c == ' ' || c == '\t'
427+
}
428+
409429
// containsUnescapedPipe reports whether s contains a `|` that is a
410430
// real delimiter — that is, not escaped by a preceding `\` (with
411431
// backslash parity respected so `\\|` counts as unescaped).
@@ -463,8 +483,13 @@ func endsWithUnescapedPipe(s string) bool {
463483

464484
func parseRow(line []byte, lineNum int, prefix string) tableRow {
465485
c := rowContent(line, prefix)
466-
lead := strings.HasPrefix(c, "|")
467-
trail := endsWithUnescapedPipe(c)
486+
// Extra whitespace between the prefix and the first cell — common
487+
// inside list items and blockquotes with double-space indent —
488+
// must not hide a real edge pipe; logicalCells already trims, so
489+
// edge detection mirrors it.
490+
t := strings.TrimSpace(c)
491+
lead := strings.HasPrefix(t, "|")
492+
trail := endsWithUnescapedPipe(t)
468493
return tableRow{
469494
lineNum: lineNum,
470495
leading: lead,

internal/rules/tablestructure/rule_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,3 +485,27 @@ func TestEscapedPipeParagraphAfterTableEndsIt(t *testing.T) {
485485
assert.Equal(t, "missing blank line after table", diags[0].Message)
486486
assert.Equal(t, 5, diags[0].Line)
487487
}
488+
489+
func TestHashStartingCellNotMistakenForHeading(t *testing.T) {
490+
// `#1` (hash directly followed by a non-space) is not an ATX
491+
// heading — it's a valid first cell, so the table must still
492+
// be detected and clean.
493+
src := "# T\n\n#1 | Title\n--- | -----\nA | B\n"
494+
assert.Empty(t, check(t, StyleConsistent, src))
495+
}
496+
497+
func TestIsATXHeading(t *testing.T) {
498+
assert.True(t, isATXHeading("# Title"))
499+
assert.True(t, isATXHeading("###### Six"))
500+
assert.True(t, isATXHeading("##")) // empty heading
501+
assert.False(t, isATXHeading("#1 | Title"))
502+
assert.False(t, isATXHeading("####### Seven")) // >6 hashes
503+
assert.False(t, isATXHeading("text"))
504+
}
505+
506+
func TestParseRowIgnoresPostPrefixIndent(t *testing.T) {
507+
// Extra spaces after the blockquote marker should not break
508+
// leading-pipe detection: the table is valid and clean.
509+
src := "# T\n\n> Intro.\n>\n> | A | B |\n> | - | - |\n> | 1 | 2 |\n>\n> Outro.\n"
510+
assert.Empty(t, check(t, StyleConsistent, src))
511+
}

0 commit comments

Comments
 (0)