Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export default defineConfig([
| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references | yes |
| [`no-multiple-h1`](./docs/rules/no-multiple-h1.md) | Disallow multiple H1 headings in the same document | yes |
| [`require-alt-text`](./docs/rules/require-alt-text.md) | Require alternative text for images | yes |
| [`table-column-count`](./docs/rules/table-column-count.md) | Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row | yes |
<!-- Rule Table End -->

**Note:** This plugin does not provide formatting rules. We recommend using a source code formatter such as [Prettier](https://prettier.io) for that purpose.
Expand Down
67 changes: 67 additions & 0 deletions docs/rules/table-column-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# table-column-count

Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row.

## Background

In GitHub Flavored Markdown [tables](https://github.github.com/gfm/#tables-extension-), rows should maintain a consistent number of cells. While variations are sometimes tolerated, data rows having *more* cells than the header can lead to lost data or rendering issues. This rule prevents data rows from exceeding the header's column count.

## Rule Details

> [!IMPORTANT] <!-- eslint-disable-line -- This should be fixed in https://github.com/eslint/markdown/issues/294 -->
>
> This rule relies on the `table` AST node, typically available when using a GFM-compatible parser (e.g., `language: "markdown/gfm"`).

This rule is triggered if a data row in a GFM table contains more cells than the header row. It does not flag rows with fewer cells than the header.

Examples of **incorrect** code for this rule:

```markdown
<!-- eslint markdown/table-column-count: "error" -->

| Head1 | Head2 |
| ----- | ----- |
| R1C1 | R1C2 | R2C3 | <!-- This data row has 3 cells, header has 2 -->

| A |
| - |
| 1 | 2 | <!-- This data row has 2 cells, header has 1 -->
```

Examples of **correct** code for this rule:

```markdown
<!-- eslint markdown/table-column-count: "error" -->

<!-- Standard correct table -->
| Header | Header |
| ------ | ------ |
| Cell | Cell |
| Cell | Cell |

<!-- Data row with fewer cells than header (VALID for this rule) -->
<!-- Rows with fewer cells are valid because they render correctly and no data is lost -->
| Header | Header | Header |
| ------ | ------ | ------ |
| Cell | Cell | |

<!-- Table with some empty cells (VALID for this rule) -->
<!-- Missing cells are treated as empty and don't cause rendering issues -->
| Col A | Col B | Col C |
| ----- | ----- | ----- |
| 1 | | 3 |
| 4 | 5 |

<!-- Single column table -->
| Single Header |
| ------------- |
| Single Cell |
```

## When Not To Use It

If you intentionally create Markdown tables where data rows are expected to contain more cells than the header, and you have a specific (perhaps non-standard) processing or rendering pipeline that handles this scenario correctly, you might choose to disable this rule. However, adhering to this rule is recommended for typical GFM rendering and data consistency.

## Prior Art

* [MD056 - table-column-count](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md056---table-column-count)
79 changes: 79 additions & 0 deletions src/rules/table-column-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @fileoverview Rule to disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row
* @author Sweta Tanwar (@SwetaTanwar)
*/

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>} TableColumnCountRuleDefinition
*/

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------

/** @type {TableColumnCountRuleDefinition} */
export default {
meta: {
type: "problem",

docs: {
recommended: true,
description:
"Disallow data rows in a GitHub Flavored Markdown table from having more cells than the header row",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/table-column-count.md",
},

messages: {
inconsistentColumnCount:
"Table column count mismatch (Expected: {{expectedCells}}, Actual: {{actualCells}}), extra data starting here will be ignored.",
},
},

create(context) {
return {
table(node) {
if (node.children.length < 1) {
return;
}

const headerRow = node.children[0];
const expectedCellsLength = headerRow.children.length;

for (let i = 1; i < node.children.length; i++) {
const currentRow = node.children[i];
const actualCellsLength = currentRow.children.length;

if (actualCellsLength > expectedCellsLength) {
const firstExtraCellNode =
currentRow.children[expectedCellsLength];

context.report({
node: currentRow,
loc: {
start: {
line: firstExtraCellNode.position.start
.line,
column: firstExtraCellNode.position.start
.column,
},
end: {
line: currentRow.position.end.line,
column: currentRow.position.end.column,
},
},
messageId: "inconsistentColumnCount",
data: {
actualCells: String(actualCellsLength),
expectedCells: String(expectedCellsLength),
},
});
}
}
},
};
},
};
204 changes: 204 additions & 0 deletions tests/rules/table-column-count.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* @fileoverview Tests for table-column-count rule.
* @author Sweta Tanwar (@SwetaTanwar)
*/

//------------------------------------------------------------------------------
// Imports
//------------------------------------------------------------------------------

import rule from "../../src/rules/table-column-count.js";
import markdown from "../../src/index.js";
import { RuleTester } from "eslint";
import dedent from "dedent";

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester({
plugins: {
markdown,
},
language: "markdown/gfm",
});

ruleTester.run("table-column-count", rule, {
valid: [
dedent`
| Header | Header |
| ------ | ------ |
| Cell | Cell |
| Cell | Cell |
`,
dedent`
| Header | Header | Header |
| ------ | ------ | ------ |
| Cell | Cell |
| Cell | |
`,
dedent`
| A | B |
|---|---|
| | |
| C | |
`,
`Just some text. | not a table |`,
dedent`
| Header | Header |
| ------ | ------ | ----- |
| Cell | Cell |
`,
dedent`
| Header | Header |
| ------ | ------ |
`,
dedent`
Some text before.

| H1 | H2 |
|----|----|
| D1 | D2 |

Some text after.
`,
dedent`
| Valid | Table |
| ----- | ----- |
| Row | Here |
`,
dedent`
| abc | defghi |
:-: | -----------:
bar | baz
`,
dedent`
| f|oo |
| ------ |
| b \`|\` az |
| b **|** im |
`,
dedent`
| abc | def |
| --- | --- |
| bar | baz |
> bar
`,
dedent`
| abc | def |
| --- | --- |
`,
],

invalid: [
{
code: dedent`
| Head1 | Head2 |
| ----- | ----- |
| R1C1 | R1C2 | R2C3 |
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "3", expectedCells: "2" },
line: 3,
column: 17,
endLine: 3,
endColumn: 26,
},
],
},
{
code: dedent`
| Head1 | Head2 |
| ----- | ----- |
| R1C1 | R1C2 | R2C3 | R3C4 |
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "4", expectedCells: "2" },
line: 3,
column: 17,
endLine: 3,
endColumn: 33,
},
],
},
{
code: dedent`
| A |
| - |
| 1 | 2 |
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "2", expectedCells: "1" },
line: 3,
column: 5,
endLine: 3,
endColumn: 10,
},
],
},
{
code: dedent`
Some introductory text.

| Header1 | Header2 |
| ------- | ------- |
| Data1 | Data2 | Data3 |
| D4 | D5 |

Some concluding text.
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "3", expectedCells: "2" },
line: 5,
column: 21,
endLine: 5,
endColumn: 30,
},
],
},
{
code: dedent`
| abc | defghi |
:-: | -----------:
bar | baz
bar | baz | bad
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "3", expectedCells: "2" },
line: 4,
column: 11,
endLine: 4,
endColumn: 16,
},
],
},
{
code: dedent`
| abc | def |
| --- | --- |
| bar | baz | Extra |
> This is a blockquote after
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: { actualCells: "3", expectedCells: "2" },
line: 3,
column: 13,
endLine: 3,
endColumn: 22,
},
],
},
],
});