Skip to content

Commit

Permalink
add expired content linter rules (#49684)
Browse files Browse the repository at this point in the history
Co-authored-by: hubwriter <[email protected]>
Co-authored-by: Ethan Palm <[email protected]>
Co-authored-by: Grace Park <[email protected]>
  • Loading branch information
4 people authored Mar 14, 2024
1 parent 0ee144f commit 030d659
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 1 deletion.
4 changes: 3 additions & 1 deletion data/reusables/contributing/content-linter-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@
| GHD019 | liquid-if-tags | Liquid `ifversion` tags should be used instead of `if` tags when the argument is a valid version | error | liquid, versioning |
| GHD020 | liquid-ifversion-tags | Liquid `ifversion` tags should contain valid version names as arguments | error | liquid, versioning |
| 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 |
| 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 |
86 changes: 86 additions & 0 deletions src/content-linter/lib/linting-rules/expired-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { addError } from 'markdownlint-rule-helpers'

// This rule looks for opening and closing HTML comment tags that
// contain an expiration date in the format:
//
// This is content <!-- expires yyyy-mm-dd --> that is
// expired <!-- end expires yyyy-mm-dd --> that does not expire.
//
// The `end expires` closing tag closes the content that is expired
// and must be removed.
export const expiredContent = {
names: ['GHD038', 'expired-content'],
description: 'Expired content must be remediated.',
tags: ['expired'],
function: (params, onError) => {
const tokensToCheck = params.tokens.filter(
(token) => token.type === 'inline' || token.type === 'html_block',
)

tokensToCheck.forEach((token) => {
// Looking for just opening tag with format:
// <!-- expires yyyy-mm-dd -->
const match = token.content.match(/<!--\s*expires\s(\d\d\d\d)-(\d\d)-(\d\d)\s*-->/)
if (!match) return

const expireDate = new Date(match.splice(1, 3).join(' '))
const today = new Date()
if (today < expireDate) return

addError(
onError,
token.lineNumber,
`Content marked with an expiration date has now expired. The content exists between 2 HTML comment tags in the format <!-- expires yyyy-mm-dd --> and <!-- end expires yyyy-mm-dd -->. You should remove or rewrite this content, and delete the expiration comments. Alternatively, choose a new expiration date.`,
match[0],
[token.content.indexOf(match[0]) + 1, match[0].length],
null, // No fix possible
)
})
},
}

export const DAYS_TO_WARN_BEFORE_EXPIRED = 14

// This rule looks for content that will expire in `DAYS_TO_WARN_BEFORE_EXPIRED`
// days. The rule looks for opening and closing HTML comment tags that
// contain an expiration date in the format:
//
// This is content <!-- expires yyyy-mm-dd --> that is
// expired <!-- end expires yyyy-mm-dd --> that does not expire.
//
// The `end expires` closing tag closes the content that is expired
// and must be removed.
export const expiringSoon = {
names: ['GHD039', 'expiring-soon'],
description: 'Content that expires soon should be proactively addressed.',
tags: ['expired'],
function: (params, onError) => {
const tokensToCheck = params.tokens.filter(
(token) => token.type === 'inline' || token.type === 'html_block',
)

tokensToCheck.forEach((token) => {
// Looking for just opening tag with format:
// <!-- expires yyyy-mm-dd -->
const match = token.content.match(/<!--\s*expires\s(\d\d\d\d)-(\d\d)-(\d\d)\s*-->/)
if (!match) return

const expireDate = new Date(match.splice(1, 3).join(' '))
const today = new Date()
const futureDate = new Date()
futureDate.setDate(today.getDate() + DAYS_TO_WARN_BEFORE_EXPIRED)
// Don't set warning if the content is already expired or
// if the content expires later than the DAYS_TO_WARN_BEFORE_EXPIRED
if (today > expireDate || expireDate > futureDate) return

addError(
onError,
token.lineNumber,
`Content marked with an expiration date will expire soon. The content exists between 2 HTML comment tags in the format <!-- expires yyyy-mm-dd --> and <!-- end expires yyyy-mm-dd -->. Check whether this content can be removed or rewritten before it expires.`,
match[0],
[token.content.indexOf(match[0]) + 1, match[0].length],
null, // No fix possible
)
})
},
}
3 changes: 3 additions & 0 deletions src/content-linter/lib/linting-rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { frontmatterLiquidSyntax, liquidSyntax } from './liquid-syntax.js'
import { liquidIfTags, liquidIfVersionTags } from './liquid-versioning.js'
import { raiReusableUsage } from './rai-reusable-usage.js'
import { imageNoGif } from './image-no-gif.js'
import { expiredContent, expiringSoon } from './expired-content.js'

const noDefaultAltText = markdownlintGitHub.find((elem) =>
elem.names.includes('no-default-alt-text'),
Expand Down Expand Up @@ -69,5 +70,7 @@ export const gitHubDocsMarkdownlint = {
liquidIfVersionTags,
raiReusableUsage,
imageNoGif,
expiredContent,
expiringSoon,
],
}
10 changes: 10 additions & 0 deletions src/content-linter/style/github-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ const githubDocsConfig = {
severity: 'error',
'partial-markdown-files': true,
},
'expired-content': {
// GHD038
severity: 'error',
'partial-markdown-files': true,
},
'expiring-soon': {
// GHD039
severity: 'warning',
'partial-markdown-files': true,
},
}

export const githubDocsFrontmatterConfig = {
Expand Down
149 changes: 149 additions & 0 deletions src/content-linter/tests/unit/expired-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { runRule } from '../../lib/init-test.js'
import {
expiredContent,
expiringSoon,
DAYS_TO_WARN_BEFORE_EXPIRED,
} from '../../lib/linting-rules/expired-content.js'

describe(expiredContent.names.join(' - '), () => {
test('Date in the past triggers error', async () => {
const markdown = [
'',
'This is some content that <!-- expires 2024-03-01 -->is',
'expiring soon<!-- end expires 2024-03-01 --> never expires.',
].join('\n')
const result = await runRule(expiredContent, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
expect(errors[0].errorRange).toEqual([27, 27])
expect(errors[0].fixInfo).toBeNull()
})

test('Date in the future does not trigger error', async () => {
const markdown = [
'',
'This is some content that <!-- expires 2099-03-01 -->is',
'expiring soon<!-- end expires 2024-03-01 --> never expires.',
].join('\n')
const result = await runRule(expiredContent, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(0)
})

test('multiple spaces in HTML comment triggers error', async () => {
const markdown = [
'',
'This is some content that <!-- expires 2024-03-01 -->is',
'expiring soon<!-- end expires 2024-03-01 --> never expires.',
].join('\n')
const result = await runRule(expiredContent, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
expect(errors[0].errorRange).toEqual([27, 34])
expect(errors[0].fixInfo).toBeNull()
})

test('no surrounding spaces in HTML comment triggers error', async () => {
const markdown = [
'',
'This is some content that <!--expires 2024-03-01-->is',
'expiring soon<!-- end expires 2024-03-01 --> never expires.',
].join('\n')
const result = await runRule(expiredContent, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
expect(errors[0].errorRange).toEqual([27, 25])
expect(errors[0].fixInfo).toBeNull()
})

test('HTML tag on its own line triggers error', async () => {
const markdown = [
'',
'<!-- expires 2024-03-01 -->',
`This is expiring soon.`,
'<!-- end expires 2024-03-01 -->',
].join('\n')
const result = await runRule(expiredContent, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
expect(errors[0].errorRange).toEqual([1, 27])
expect(errors[0].fixInfo).toBeNull()
})
})

describe(expiringSoon.names.join(' - '), () => {
test('Date more than number of days to warn does not trigger warning', async () => {
const formattedDate = getFormattedDate(1)
const markdown = [
'',
`This is some content that <!-- expires ${formattedDate} -->is`,
`expiring soon<!-- end expires ${formattedDate} --> never expires.`,
].join('\n')
const result = await runRule(expiringSoon, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(0)
})

test('Date equivalent to number of days to warn does trigger warning', async () => {
const formattedDate = getFormattedDate()
const markdown = [
'',
`This is some content that <!-- expires ${formattedDate} -->is`,
`expiring soon<!-- end expires ${formattedDate} --> never expires.`,
].join('\n')
const result = await runRule(expiringSoon, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
expect(errors[0].errorRange).toEqual([27, 27])
expect(errors[0].fixInfo).toBeNull()
})

test('Date less than number of days to warn does trigger warning', async () => {
const formattedDate = getFormattedDate(-1)
const markdown = [
'',
`This is some content that <!-- expires ${formattedDate} -->is`,
`expiring soon<!-- end expires ${formattedDate} --> never expires.`,
].join('\n')
const result = await runRule(expiringSoon, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
expect(errors[0].errorRange).toEqual([27, 27])
expect(errors[0].fixInfo).toBeNull()
})
test('HTML tag on its own line triggeres warning', async () => {
const formattedDate = getFormattedDate(-1)
const markdown = [
'',
`<!-- expires ${formattedDate} -->`,
`This is expiring soon.`,
`<!-- end expires ${formattedDate}`,
].join('\n')
const result = await runRule(expiringSoon, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(1)
expect(errors[0].lineNumber).toBe(2)
expect(errors[0].errorRange).toEqual([1, 27])
expect(errors[0].fixInfo).toBeNull()
})
})

function getFormattedDate(additionalDays = 0) {
const today = new Date()
today.setDate(today.getDate() + DAYS_TO_WARN_BEFORE_EXPIRED + additionalDays)
return today
.toLocaleDateString('en-GB', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
.split('/')
.reverse()
.join('-')
}

0 comments on commit 030d659

Please sign in to comment.