Skip to content

Commit

Permalink
Content Linter Rule - Whitespace (#52622)
Browse files Browse the repository at this point in the history
Co-authored-by: Rachael Sewell <[email protected]>
  • Loading branch information
Saturn226 and rachmari authored Nov 7, 2024
1 parent 630efbd commit 97f2feb
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 1 deletion.
3 changes: 2 additions & 1 deletion data/reusables/contributing/content-linter-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@
| GHD038 | expired-content | Expired content must be remediated. | error | expired |
| 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 |
| GHD041 | third-party-action-pinning | Code examples that use third-party actions must always pin to a full length commit SHA | error | feature, actions |
| GHD041 | third-party-action-pinning | Code examples that use third-party actions must always pin to a full length commit SHA | error | feature, actions |
| GHD042 | liquid-tag-whitespace | Liquid tags should start and end with one whitespace. Liquid tag arguments should be separated by only one whitespace. | error | liquid, format |
2 changes: 2 additions & 0 deletions src/content-linter/lib/linting-rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { imageNoGif } from './image-no-gif.js'
import { expiredContent, expiringSoon } from './expired-content.js'
import { tableLiquidVersioning } from './table-liquid-versioning.js'
import { thirdPartyActionPinning } from './third-party-action-pinning.js'
import { liquidTagWhitespace } from './liquid-tag-whitespace.js'

const noDefaultAltText = markdownlintGitHub.find((elem) =>
elem.names.includes('no-default-alt-text'),
Expand Down Expand Up @@ -77,5 +78,6 @@ export const gitHubDocsMarkdownlint = {
expiringSoon,
tableLiquidVersioning,
thirdPartyActionPinning,
liquidTagWhitespace,
],
}
64 changes: 64 additions & 0 deletions src/content-linter/lib/linting-rules/liquid-tag-whitespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { TokenKind } from 'liquidjs'

import { getLiquidTokens, getPositionData } from '../helpers/liquid-utils.js'
import { addFixErrorDetail } from '../helpers/utils.js'

/*
Liquid tags should start and end with one whitespace. For example:
DO use a single whitespace character
{% data <args> %}
DON'T use 0 or more than 1 whitespace
{%data <args> %}
DON'T use more than 1 whitespace between args
{%data arg1 arg2 %}
*/

export const liquidTagWhitespace = {
names: ['GHD042', 'liquid-tag-whitespace'],
description:
'Liquid tags should start and end with one whitespace. Liquid tag arguments should be separated by only one whitespace.',
tags: ['liquid', 'format'],
function: (params, onError) => {
const content = params.lines.join('\n')
const tokens = getLiquidTokens(content).filter((token) => token.kind === TokenKind.Tag)
for (const token of tokens) {
const { lineNumber, column, length } = getPositionData(token, params.lines)

const range = [column, length]
const tag = params.lines[lineNumber - 1].slice(column - 1, column - 1 + length)

// Get just the opening and closing tags, which includes any whitespace
// added before the tag name or any arguments
const openTag = tag.slice(0, token.contentRange[0] - token.begin)
const closeTag = tag.slice(-(token.end - token.contentRange[1]))

const isOpenTagOneSpace = openTag !== openTag.trim() + ' '
const isCloseTagOneSpace = closeTag !== ' ' + closeTag.trim()

const moreThanOneSpace = /\s{2,}/
const isArgOneSpace = moreThanOneSpace.test(tag)

const fixedContent =
openTag.trim() + ' ' + token.content.replace(moreThanOneSpace, ' ') + ' ' + closeTag.trim()

if (isOpenTagOneSpace || isCloseTagOneSpace || isArgOneSpace) {
addFixErrorDetail(
onError,
lineNumber,
fixedContent,
params.lines[lineNumber - 1].slice(column - 1, column - 1 + length),
range,
{
lineNumber,
editColumn: column,
deleteCount: length,
insertText: fixedContent,
},
)
}
}
},
}
6 changes: 6 additions & 0 deletions src/content-linter/style/github-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ const githubDocsConfig = {
'partial-markdown-files': true,
'yml-files': true,
},
'liquid-tag-whitespace': {
// GHD042
severity: 'error',
'partial-markdown-files': true,
'yml-files': true,
},
}

export const githubDocsFrontmatterConfig = {
Expand Down
71 changes: 71 additions & 0 deletions src/content-linter/tests/unit/liquid-tag-whitespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, test } from 'vitest'

import { runRule } from '../../lib/init-test.js'
import { liquidTagWhitespace } from '../../lib/linting-rules/liquid-tag-whitespace.js'

describe(liquidTagWhitespace.names.join(' - '), () => {
test('liquid tags with correct whitespace pass', async () => {
const markdown = [
'{% data variables.location.product_location %}',
'{% assign my_variable = "value" %}',
'{% if user %}Hello, {{ user.name }}{% endif %}',
].join('\n')

const result = await runRule(liquidTagWhitespace, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(0)
})

test('liquid tags with incorrect whitespace fail', async () => {
const markdown = [
'{%data variables.location.product_location %}',
'{% assign my_variable = "value"%}',
'{% if user %}Hello, {{ user.name }} {%endif %}',
'{% data variables.location.product_location %}',
'{%-data variables.location.product_location -%}',
'{%- assign my_variable = "value"-%}',
'{%- if user -%}Hello, {{ user.name }} {%endif %}',
'{%- data variables.location.product_location -%}',
].join('\n')

const result = await runRule(liquidTagWhitespace, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(8)
expect(errors[2].lineNumber).toBe(3)
expect(errors[2].fixInfo).toEqual({
deleteCount: 10,
editColumn: 37,
lineNumber: 3,
insertText: '{% endif %}',
})
})

test('liquid tags with multiple spaces between arguments fail', async () => {
const markdown = [
'{% assign my_variable = "value" %}',
'{% if user %}Hello, {{ user.name }}{% endif %}',
].join('\n')

const result = await runRule(liquidTagWhitespace, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(2)
expect(errors[1].lineNumber).toBe(2)
expect(errors[0].fixInfo).toEqual({
deleteCount: 35,
editColumn: 1,
lineNumber: 1,
insertText: '{% assign my_variable = "value" %}',
})
})

test('liquid tags with single spaces between arguments pass', async () => {
const markdown = [
'{% assign my_variable = "value" %}',
'{% if user %}Hello, {{ user.name }}{% endif %}',
].join('\n')

const result = await runRule(liquidTagWhitespace, { strings: { markdown } })
const errors = result.markdown
expect(errors.length).toBe(0)
})
})

0 comments on commit 97f2feb

Please sign in to comment.