diff --git a/src/content-linter/lib/helpers.js b/src/content-linter/lib/helpers.js new file mode 100644 index 000000000000..879b36462baa --- /dev/null +++ b/src/content-linter/lib/helpers.js @@ -0,0 +1,14 @@ +import { addError, newLineRe } from 'markdownlint-rule-helpers' + +// Adds an error object with details conditionally via the onError callback +export function addFixErrorDetail(onError, lineNumber, expected, actual, range, fixInfo) { + addError(onError, lineNumber, `Expected: ${expected}`, ` Actual: ${actual}`, range, fixInfo) +} + +export function getCodeFenceTokens(params) { + return params.tokens.filter((t) => t.type === 'fence') +} + +export function getCodeFenceLines(token) { + return token.content.split(newLineRe) +} diff --git a/src/content-linter/lib/linting-rules/code-fence-line-length.js b/src/content-linter/lib/linting-rules/code-fence-line-length.js index e883ae3fe765..6ecdaa1d1705 100644 --- a/src/content-linter/lib/linting-rules/code-fence-line-length.js +++ b/src/content-linter/lib/linting-rules/code-fence-line-length.js @@ -1,12 +1,13 @@ -import { addError } from 'markdownlint-rule-helpers' +import { addError, ellipsify } from 'markdownlint-rule-helpers' -import { getCodeFenceTokens, getCodeFenceLines } from '../markdownlint-helpers.js' +import { getCodeFenceTokens, getCodeFenceLines } from '../helpers.js' export const codeFenceLineLength = { names: ['GHD001', 'code-fence-line-length'], description: 'Code fence lines should not exceed a maximum length', tags: ['code'], severity: 'warning', + information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'), function: function GHD001(params, onError) { const MAX_LINE_LENGTH = String(params.config.maxLength || 60) const codeFenceTokens = getCodeFenceTokens(params) @@ -22,8 +23,9 @@ export const codeFenceLineLength = { onError, lineNumber, `Code fence line exceeds ${MAX_LINE_LENGTH} characters.`, - undefined, // N/A - undefined, // N/A + ellipsify(line), + [1, line.length], + null, // No fix possible ) } }) diff --git a/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js b/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js index 4d106740b3d0..c0cf6000322d 100644 --- a/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js +++ b/src/content-linter/lib/linting-rules/image-alt-text-end-punctuation.js @@ -1,32 +1,30 @@ -import { addError, forEachInlineChild } from 'markdownlint-rule-helpers' +import { forEachInlineChild } from 'markdownlint-rule-helpers' + +import { addFixErrorDetail } from '../helpers.js' export const imageAltTextEndPunctuation = { names: ['GHD002', 'image-alt-text-end-punctuation'], - description: 'Images alternate text should end with a punctuation.', + description: 'Alternate text for images should end with a punctuation.', severity: 'error', tags: ['accessibility', 'images'], + information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'), function: function GHD003(params, onError) { forEachInlineChild(params, 'image', function forToken(token) { const quoteRegex = /[.?!]['"]$/ const endRegex = /[.?!]$/ const imageAltText = token.content.trim() + const startColumnIndex = token.line.indexOf(imageAltText) + const range = startColumnIndex ? [startColumnIndex + 1, imageAltText.length] : null if ( (!imageAltText.endsWith('"') && !imageAltText.slice(-1).match(endRegex)) || (imageAltText.endsWith('"') && !imageAltText.slice(-2).match(quoteRegex)) ) { - addError( - onError, - token.lineNumber, - `On line ${token.lineNumber}, the image alt text '${imageAltText}' must have punctuation at the end of the sentence.`, - undefined, - undefined, - { - lineNumber: token.lineNumber, - editColumn: token.line.indexOf(']') + 1, - deleteCount: 0, - insertText: '.', - }, - ) + addFixErrorDetail(onError, token.lineNumber, imageAltText + '.', imageAltText, range, { + lineNumber: token.lineNumber, + editColumn: token.line.indexOf(']') + 1, + deleteCount: 0, + insertText: '.', + }) } }) }, diff --git a/src/content-linter/lib/linting-rules/image-alt-text-length.js b/src/content-linter/lib/linting-rules/image-alt-text-length.js index fbfef3f877ab..c1823f5c64eb 100644 --- a/src/content-linter/lib/linting-rules/image-alt-text-length.js +++ b/src/content-linter/lib/linting-rules/image-alt-text-length.js @@ -6,6 +6,7 @@ export const incorrectAltTextLength = { severity: 'warning', description: 'Images alternate text should be between 40-150 characters', tags: ['accessibility', 'images'], + information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'), function: function GHD004(params, onError) { forEachInlineChild(params, 'image', async function forToken(token) { let renderedString = token.content @@ -18,7 +19,10 @@ export const incorrectAltTextLength = { addError( onError, token.lineNumber, - `The alt text: ${renderedString}, is ${renderedString.length} characters long`, + `Image alternate text is ${renderedString.length} characters long.`, + renderedString, + null, // No range + null, // No fix possible ) } }) diff --git a/src/content-linter/lib/linting-rules/image-file-kebab.js b/src/content-linter/lib/linting-rules/image-file-kebab.js index 112d3d52fff6..58198f41ef67 100644 --- a/src/content-linter/lib/linting-rules/image-file-kebab.js +++ b/src/content-linter/lib/linting-rules/image-file-kebab.js @@ -1,19 +1,26 @@ -import { addError, forEachInlineChild } from 'markdownlint-rule-helpers' +import { forEachInlineChild } from 'markdownlint-rule-helpers' + +import { addFixErrorDetail } from '../helpers.js' export const imageFileKebab = { names: ['GHD004', 'image-file-kebab'], description: 'Image file names should always be lowercase kebab case', severity: 'warning', tags: ['accessibility', 'images'], + information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'), function: function GHD005(params, onError) { forEachInlineChild(params, 'image', async function forToken(token) { const imageFileName = token.attrs[0][1].split('/').pop().split('.')[0] const nonKebabRegex = /([A-Z]|_)/ + const suggestedFileName = imageFileName.toLowerCase().replace(/_/g, '-') if (imageFileName.match(nonKebabRegex)) { - addError( + addFixErrorDetail( onError, token.lineNumber, - `The image file name: ${token.attrs[0][1]}, is not lowercase kebab case.`, + imageFileName, + suggestedFileName, + [token.line.indexOf(imageFileName) + 1, imageFileName.length], + null, // Todo add fix ) } }) diff --git a/src/content-linter/lib/linting-rules/internal-links-lang.js b/src/content-linter/lib/linting-rules/internal-links-lang.js index 9a42337eb3aa..e2301f7ad34a 100644 --- a/src/content-linter/lib/linting-rules/internal-links-lang.js +++ b/src/content-linter/lib/linting-rules/internal-links-lang.js @@ -1,5 +1,6 @@ -import { addError, filterTokens } from 'markdownlint-rule-helpers' +import { filterTokens } from 'markdownlint-rule-helpers' +import { addFixErrorDetail } from '../helpers.js' import { languageKeys } from '../../../../lib/languages.js' export const internalLinksLang = { @@ -7,6 +8,7 @@ export const internalLinksLang = { description: 'Internal links must not have a hardcoded language code', severity: 'error', tags: ['links', 'url'], + information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'), function: function GHD006(params, onError) { filterTokens(params, 'inline', (token) => { let linkHref = '' @@ -16,8 +18,9 @@ export const internalLinksLang = { linkHref = '' for (const attr of child.attrs) { if ( - languageKeys.includes(attr[1].split('/')[1]) || - languageKeys.includes(attr[1].split('/')[0]) + languageKeys.some( + (lang) => attr[1].startsWith(`/${lang}`) || attr[1].startsWith(lang), + ) ) { internalLinkHasLang = true linkHref = attr[1] @@ -25,12 +28,12 @@ export const internalLinksLang = { } } else if (child.type === 'link_close') { if (internalLinkHasLang) { - addError( + addFixErrorDetail( onError, child.lineNumber, - `This internal link: ${linkHref} must not start with a hardcoded language code.`, - undefined, - undefined, + linkHref.replace(/(\/)?[a-z]{2}/, ''), + linkHref, + undefined, // Todo add range { lineNumber: child.lineNumber, editColumn: token.line.indexOf('(') + 2, diff --git a/src/content-linter/lib/linting-rules/internal-links-slash.js b/src/content-linter/lib/linting-rules/internal-links-slash.js index 81e2799be86b..077a5ea74059 100644 --- a/src/content-linter/lib/linting-rules/internal-links-slash.js +++ b/src/content-linter/lib/linting-rules/internal-links-slash.js @@ -1,10 +1,13 @@ -import { addError, filterTokens } from 'markdownlint-rule-helpers' +import { filterTokens } from 'markdownlint-rule-helpers' + +import { addFixErrorDetail } from '../helpers.js' export const internalLinksSlash = { names: ['GHD006', 'internal-links-slash'], description: 'Internal links must start with a /', severity: 'error', tags: ['links', 'url'], + information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'), function: function GHD007(params, onError) { filterTokens(params, 'inline', (token) => { let linkHref = '' @@ -26,19 +29,12 @@ export const internalLinksSlash = { } } else if (child.type === 'link_close') { if (!internalLinkHasSlash) { - addError( - onError, - child.lineNumber, - `This relative link: ${linkHref} on line ${child.lineNumber} must start with a /`, - undefined, - undefined, - { - lineNumber: child.lineNumber, - editColumn: token.line.indexOf('(') + 2, - deleteCount: 0, - insertText: '/', - }, - ) + addFixErrorDetail(onError, child.lineNumber, `/${linkHref}`, linkHref, undefined, { + lineNumber: child.lineNumber, + editColumn: token.line.indexOf('(') + 2, + deleteCount: 0, + insertText: '/', + }) internalLinkHasSlash = true } } diff --git a/src/content-linter/lib/markdownlint-helpers.js b/src/content-linter/lib/markdownlint-helpers.js deleted file mode 100644 index 26825b087696..000000000000 --- a/src/content-linter/lib/markdownlint-helpers.js +++ /dev/null @@ -1,9 +0,0 @@ -import { newLineRe } from 'markdownlint-rule-helpers' - -export function getCodeFenceTokens(params) { - return params.tokens.filter((t) => t.type === 'fence') -} - -export function getCodeFenceLines(token) { - return token.content.split(newLineRe) -} diff --git a/src/content-linter/scripts/markdownlint.js b/src/content-linter/scripts/markdownlint.js index 220b61ead18e..bf421db0532d 100755 --- a/src/content-linter/scripts/markdownlint.js +++ b/src/content-linter/scripts/markdownlint.js @@ -28,9 +28,10 @@ program ) .option('--fix', 'Fix linting errors.') .option('--summary-by-rule', 'Summarize the number of errors for each rule.') + .option('--verbose', 'Output full format for all errors.') .parse(process.argv) -const { files, fix, paths, error, rules, summaryByRule } = program.opts() +const { files, fix, paths, error, rules, summaryByRule, verbose } = program.opts() const DEFAULT_LINT_DIRS = ['content', 'data'] main() @@ -77,7 +78,7 @@ async function main() { } } - // Used for a temporary way to allow us to see how many errors currently + // Used for a temparary way to allow us to see how many errors currently // exist for each rule in the content directory. if (summaryByRule) { reportSummaryByRule(result, config) @@ -129,12 +130,38 @@ function reportSummaryByRule(result, config) { function reportResults(results) { Object.entries(results) // each result key always has an array value, but it may be empty - .filter(([key, result]) => result.length) + .filter(([, result]) => result.length) .forEach(([key, result]) => { - console.log(key, result) + console.log(key) + if (!verbose) { + result.forEach((flaw) => { + const simplifiedResult = formatResult(flaw) + console.log(simplifiedResult) + }) + } else { + console.log(result) + } }) } function getErrorCountByFile(result) { return Object.values(result).filter((value) => value.length).length } + +function formatResult(object) { + return Object.entries(object).reduce((acc, [key, value]) => { + if (key === 'fixInfo') { + acc.fixable = !!value + delete acc.fixInfo + return acc + } + if (!value) return acc + if (key === 'errorRange') { + acc.columnNumber = value[0] + delete acc.range + return acc + } + acc[key] = value + return acc + }, {}) +}