Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -85,6 +85,7 @@ export default defineConfig([
| [`no-missing-atx-heading-space`](./docs/rules/no-missing-atx-heading-space.md) | Disallow headings without a space after the hash characters | yes |
| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references | 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
65 changes: 65 additions & 0 deletions docs/rules/table-column-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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 focuses on preventing 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 (i.e., will be flagged):

```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 (i.e., will NOT be flagged):

```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) -->
| Header | Header | Header |
| ------ | ------ | ------ |
| Cell | Cell | |

<!-- Table with some empty cells (VALID for this rule) -->
| Col A | Col B | Col C |
| ----- | ----- | ----- |
| 1 | | 3 |
| 4 | 5 | |

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

## When Not To Use It

If you are intentionally creating 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, for typical GFM rendering and data consistency, adhering to this rule is recommended.

## Prior Art

* [table-column-count](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md056---table-column-count)
68 changes: 68 additions & 0 deletions src/rules/table-column-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @fileoverview Rule to ensure GitHub Flavored Markdown tables have a consistent number of cells in each 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",
},

fixable: null,
schema: [],

messages: {
inconsistentColumnCount:
"Data row {{dataRowIndex}} (1-indexed) has {{actualCells}} cells, but header has {{expectedCells}} cells (should not exceed header count).",
},
},

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

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

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

if (actualCells > expectedCells) {
context.report({
node: currentRow,
messageId: "inconsistentColumnCount",
data: {
dataRowIndex: String(i),
actualCells: String(actualCells),
expectedCells: String(expectedCells),
},
});
}
}
},
};
},
};
142 changes: 142 additions & 0 deletions tests/rules/table-column-count.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* @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 |
`,
],

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

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

Some concluding text.
`,
errors: [
{
messageId: "inconsistentColumnCount",
data: {
dataRowIndex: "1",
actualCells: "3",
expectedCells: "2",
},
line: 5,
column: 1,
endLine: 5,
endColumn: 30,
},
],
},
],
});