From 148ac48f7dfbd2ce3c4ef7602fa8720a1aa772bf Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Mon, 26 Aug 2024 12:37:17 -0700 Subject: [PATCH] Add rule for Markdown table Liquid syntax (#52092) --- .../contributing/content-linter-rules.md | 3 +- src/content-linter/lib/linting-rules/index.js | 2 + .../linting-rules/table-liquid-versioning.js | 83 +++++++++++++++++++ src/content-linter/style/github-docs.js | 5 ++ src/content-linter/tests/fixtures/tables.md | 59 +++++++++++++ .../tests/unit/table-liquid-versioning.js | 17 ++++ 6 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/content-linter/lib/linting-rules/table-liquid-versioning.js create mode 100644 src/content-linter/tests/fixtures/tables.md create mode 100644 src/content-linter/tests/unit/table-liquid-versioning.js diff --git a/data/reusables/contributing/content-linter-rules.md b/data/reusables/contributing/content-linter-rules.md index f6fce7f6e10c..e399af383ead 100644 --- a/data/reusables/contributing/content-linter-rules.md +++ b/data/reusables/contributing/content-linter-rules.md @@ -62,4 +62,5 @@ | GHD035 | rai-reusable-usage | RAI articles and reusables can only reference reusable content in the data/reusables/rai directory | error | feature, rai | | GHD036 | image-no-gif | Image must not be a gif, styleguide reference: contributing/style-guide-and-content-model/style-guide.md#images | error | images | | GHD038 | expired-content | Expired content must be remediated. | error | expired | -| GHD039 | expiring-soon | Content that expires soon should be proactively addressed. | warning | expired | \ No newline at end of file +| GHD039 | expiring-soon | Content that expires soon should be proactively addressed. | warning | expired | +| [GHD040](https://github.com/github/docs/blob/main/src/content-linter/README.md) | table-liquid-versioning | Tables must use the correct liquid versioning format | error | tables | \ No newline at end of file diff --git a/src/content-linter/lib/linting-rules/index.js b/src/content-linter/lib/linting-rules/index.js index 3329ab7a73e0..c03fc534b1f6 100644 --- a/src/content-linter/lib/linting-rules/index.js +++ b/src/content-linter/lib/linting-rules/index.js @@ -29,6 +29,7 @@ import { liquidIfTags, liquidIfVersionTags, liquidIfVersionVersions } from './li import { raiReusableUsage } from './rai-reusable-usage.js' import { imageNoGif } from './image-no-gif.js' import { expiredContent, expiringSoon } from './expired-content.js' +import { tableLiquidVersioning } from './table-liquid-versioning.js' const noDefaultAltText = markdownlintGitHub.find((elem) => elem.names.includes('no-default-alt-text'), @@ -73,5 +74,6 @@ export const gitHubDocsMarkdownlint = { imageNoGif, expiredContent, expiringSoon, + tableLiquidVersioning, ], } diff --git a/src/content-linter/lib/linting-rules/table-liquid-versioning.js b/src/content-linter/lib/linting-rules/table-liquid-versioning.js new file mode 100644 index 000000000000..fe32d02abcb6 --- /dev/null +++ b/src/content-linter/lib/linting-rules/table-liquid-versioning.js @@ -0,0 +1,83 @@ +import { addError, filterTokens } from 'markdownlint-rule-helpers' + +// Detects a Markdown table delimiter row +const delimiterRegexPure = /(\s)*(:)?(-+)(:)?(\s)*(\|)/ +// Detects a Markdown table delimiter row with a Liquid tag +const delimiterRegex = /(\s)*(:)?(-+)(:)?(\s)*(\|).*({%.*(ifversion|else|endif).*%})/ +// Detects a Liquid versioning tag +const liquidRegex = /^{%-?\s*(ifversion|else|endif).*-?%}/ +// Detects a Markdown table row with a Liquid versioning tag +const liquidAfterRowRegex = /(\|{1}).*(\|{1}).*{%\s*(ifversion|else|endif).*%}$/ + +export const tableLiquidVersioning = { + names: ['GHD040', 'table-liquid-versioning'], + description: 'Tables must use the correct liquid versioning format', + severity: 'error', + tags: ['tables'], + information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'), + function: function GHD040(params, onError) { + const lines = params.lines + let inTable = false + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + if (inTable && (!line || isPreviousLineIndented(lines[i], lines[i - 1]))) { + inTable = false + continue + } + + if (delimiterRegexPure.test(line)) { + // A table with rows is at least 3 lines + if (lines[i - 1] && lines[i + 1]) { + inTable = true + if (liquidAfterRowRegex.test(lines[i - 1])) { + addError( + onError, + i, + 'Liquid conditionals that version rows of data should be placed on their own line in the format `| {% ifversion enterprise %} |`.', + lines[i - 1], + null, + ) + } + if (delimiterRegex.test(line)) { + addError( + onError, + i + 1, + 'Liquid conditionals that version rows of data should be placed on their own line in the format `| {% ifversion enterprise %} |`.', + line, + null, + ) + } + continue + } + } + if (inTable) { + if (liquidRegex.test(line)) { + addError( + onError, + i + 1, + 'Liquid conditionals that version rows of data should be placed on their own line in the format `| {% ifversion enterprise %} |`. If the conditional is on its own line but is not related to the table, ensure there is one new line beween a Liquid version tag and the table.', + line, + null, + ) + } + if (liquidAfterRowRegex.test(line)) { + addError( + onError, + i + 1, + 'Liquid conditionals that version rows of data should be placed on their own line in the format `| {% ifversion enterprise %} |`.', + line, + null, + ) + } + } + } + }, +} + +function isPreviousLineIndented(line, previousLine) { + if (!line || !previousLine) return false + const numWhitespaceLine = line.length - line.trimLeft().length + const numWhitespacePrevLine = previousLine.length - previousLine.trimLeft().length + return numWhitespaceLine < numWhitespacePrevLine +} diff --git a/src/content-linter/style/github-docs.js b/src/content-linter/style/github-docs.js index 7462dec8e39f..c1e9a8ef0579 100644 --- a/src/content-linter/style/github-docs.js +++ b/src/content-linter/style/github-docs.js @@ -150,6 +150,11 @@ const githubDocsConfig = { severity: 'warning', 'partial-markdown-files': true, }, + 'table-liquid-versioning': { + // GH040 + severity: 'error', + 'partial-markdown-files': true, + }, } export const githubDocsFrontmatterConfig = { diff --git a/src/content-linter/tests/fixtures/tables.md b/src/content-linter/tests/fixtures/tables.md new file mode 100644 index 000000000000..bb0b30c2b046 --- /dev/null +++ b/src/content-linter/tests/fixtures/tables.md @@ -0,0 +1,59 @@ +--- +title: Examples of tables in Markdown +descriptions: Examples of tables in Markdown +versions: + fpt: '*' + ghes: '*' + ghec: '*' +--- + +## Good + +| Package manager | Languages | Recommended formats | All supported formats | +| --- | --- | --- | ---| +| {% ifversion volvo %} | +| Cargo | Rust | `Cargo.lock` | `Cargo.toml`, `Cargo.lock` | +| {% endif %} | + +| Package manager | Languages | Recommended formats | All supported formats | +| --- | --- | --- | ---| +| {%- ifversion volvo %} | +| Cargo | Rust | `Cargo.lock` | `Cargo.toml`, `Cargo.lock` | +| {%- endif %} | + +{% ifversion volvo %} + +1. This is a list with a table + | Package manager | Languages | Recommended formats | All supported formats | + | --- | --- | --- | ---| + | {%- ifversion volvo %} | + | Cargo | Rust | `Cargo.lock` | `Cargo.toml`, `Cargo.lock` | + | {%- endif %} | +{% endif %} + +## Bad + +| Package manager | Languages | Recommended formats | All supported formats | +| --- | --- | --- | ---| +{%- ifversion volvo %} +| Cargo | Rust | `Cargo.lock` | `Cargo.toml`, `Cargo.lock` | +{%- endif %} + +| Package manager | Languages | Recommended formats | All supported formats | +| --- | --- | --- | ---|{% ifversion volvo %} +| Cargo | Rust | `Cargo.lock` | `Cargo.toml`, `Cargo.lock` |{% endif %} + +{% ifversion volvo %} + +| Package manager | Languages | Recommended formats | All supported formats | +| --- | --- | --- | ---| +| Cargo | Rust | `Cargo.lock` | `Cargo.toml`, `Cargo.lock` | +{% endif %} + +Package manager | Languages | Recommended formats | All supported formats {% ifversion fpt %} +:- | :- | :- | :-{% endif %}{% ifversion volvo %} +Cargo | Rust | `Cargo.lock` | `Cargo.toml`, `Cargo.lock` {% endif %} + +| Package manager | Languages | Recommended formats | All supported formats | {% ifversion fpt %} +| :- | :- | :- | :-|{% endif %}{% ifversion volvo %} +|Cargo | Rust | `Cargo.lock` | `Cargo.toml`, `Cargo.lock`| {% endif %} diff --git a/src/content-linter/tests/unit/table-liquid-versioning.js b/src/content-linter/tests/unit/table-liquid-versioning.js new file mode 100644 index 000000000000..6a56534ef9d8 --- /dev/null +++ b/src/content-linter/tests/unit/table-liquid-versioning.js @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'vitest' + +import { runRule } from '../../lib/init-test.js' +import { tableLiquidVersioning } from '../../lib/linting-rules/table-liquid-versioning.js' + +const FIXTURE_FILEPATH = 'src/content-linter/tests/fixtures/tables.md' + +describe(tableLiquidVersioning.names.join(' - '), () => { + test('non-early access file with early access references fails', async () => { + const result = await runRule(tableLiquidVersioning, { files: [FIXTURE_FILEPATH] }) + const errors = result[FIXTURE_FILEPATH] + expect(errors.length).toBe(11) + const lineNumbers = errors.map((error) => error.lineNumber) + const expectedErrorLines = [38, 40, 43, 44, 51, 53, 54, 55, 57, 58, 59] + expect(JSON.stringify(lineNumbers)).toEqual(JSON.stringify(expectedErrorLines)) + }) +})