Skip to content

Commit 5a28246

Browse files
authored
Merge pull request #39383 from github/repo-sync
Repo sync
2 parents a6b43f0 + 2aa5c54 commit 5a28246

File tree

7 files changed

+510
-13
lines changed

7 files changed

+510
-13
lines changed

data/reusables/contributing/content-linter-rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
| GHD049 | note-warning-formatting | Note and warning tags should be formatted according to style guide | warning | formatting, callouts, notes, warnings, style |
6969
| GHD050 | multiple-emphasis-patterns | Do not use more than one emphasis/strong, italics, or uppercase for a string | warning | formatting, emphasis, style |
7070
| GHD051 | frontmatter-versions-whitespace | Versions frontmatter should not contain unnecessary whitespace | warning | frontmatter, versions |
71+
| GHD053 | header-content-requirement | Headers must have content between them, such as an introduction | warning | headers, structure, content |
7172
| GHD054 | third-party-actions-reusable | Code examples with third-party actions must include disclaimer reusable | warning | actions, reusable, third-party |
7273
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: octicon-<icon-name> | The octicon liquid syntax used is deprecated. Use this format instead `octicon "<octicon-name>" aria-label="<Octicon aria label>"` | error | |
7374
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: site.data | Catch occurrences of deprecated liquid data syntax. | error | |

package-lock.json

Lines changed: 14 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@
265265
"cheerio": "^1.0.0-rc.12",
266266
"cheerio-to-text": "0.2.4",
267267
"classnames": "^2.5.1",
268-
"connect-timeout": "1.9.0",
268+
"connect-timeout": "1.9.1",
269269
"cookie-parser": "^1.4.7",
270270
"cuss": "2.2.0",
271271
"dayjs": "^1.11.13",
@@ -304,7 +304,7 @@
304304
"mdast-util-to-hast": "^13.2.0",
305305
"mdast-util-to-markdown": "2.1.2",
306306
"mdast-util-to-string": "^4.0.0",
307-
"morgan": "^1.10.0",
307+
"morgan": "^1.10.1",
308308
"next": "^15.3.3",
309309
"ora": "^8.0.1",
310310
"parse5": "7.1.2",
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { addError, filterTokens } from 'markdownlint-rule-helpers'
2+
3+
export const headerContentRequirement = {
4+
names: ['GHD053', 'header-content-requirement'],
5+
description: 'Headers must have content between them, such as an introduction',
6+
tags: ['headers', 'structure', 'content'],
7+
function: (params, onError) => {
8+
const headings = []
9+
10+
// Collect all heading tokens with their line numbers and levels
11+
filterTokens(params, 'heading_open', (token) => {
12+
headings.push({
13+
token,
14+
lineNumber: token.lineNumber,
15+
level: parseInt(token.tag.slice(1)), // Extract number from h1, h2, etc.
16+
line: params.lines[token.lineNumber - 1],
17+
})
18+
})
19+
20+
// Check each pair of consecutive headings
21+
for (let i = 0; i < headings.length - 1; i++) {
22+
const currentHeading = headings[i]
23+
const nextHeading = headings[i + 1]
24+
25+
// Only check if next heading is a subheading (higher level number)
26+
if (nextHeading.level > currentHeading.level) {
27+
const hasContent = checkForContentBetweenHeadings(
28+
params.lines,
29+
currentHeading.lineNumber,
30+
nextHeading.lineNumber,
31+
)
32+
33+
if (!hasContent) {
34+
addError(
35+
onError,
36+
nextHeading.lineNumber,
37+
`Header must have introductory content before subheader. Add content between "${currentHeading.line.trim()}" and "${nextHeading.line.trim()}".`,
38+
nextHeading.line,
39+
null, // No specific range within the line
40+
null, // No fix possible - requires manual content addition
41+
)
42+
}
43+
}
44+
}
45+
},
46+
}
47+
48+
/**
49+
* Check if there is meaningful content between two headings
50+
* Returns true if content exists, false if only whitespace/empty lines
51+
*/
52+
function checkForContentBetweenHeadings(lines, startLineNumber, endLineNumber) {
53+
// Convert to 0-based indexes and skip the heading lines themselves
54+
const startIndex = startLineNumber // Skip the current heading line
55+
const endIndex = endLineNumber - 2 // Stop before the next heading line
56+
57+
// Check each line between the headings
58+
for (let i = startIndex; i <= endIndex; i++) {
59+
if (i >= lines.length) break
60+
61+
const line = lines[i].trim()
62+
63+
// Skip empty lines
64+
if (line === '') continue
65+
66+
// Skip frontmatter delimiters
67+
if (line === '---') continue
68+
69+
// Skip Liquid tags that don't produce visible content
70+
if (isNonContentLiquidTag(line)) continue
71+
72+
// If we find any other content, consider it valid
73+
if (line.length > 0) {
74+
return true
75+
}
76+
}
77+
78+
return false
79+
}
80+
81+
/**
82+
* Check if a line contains only Liquid tags that don't produce visible content
83+
* This helps avoid false positives for conditional blocks
84+
*/
85+
function isNonContentLiquidTag(line) {
86+
// Match common non-content Liquid tags
87+
const nonContentTags = [
88+
/^{%\s*ifversion\s+.*%}$/,
89+
/^{%\s*elsif\s+.*%}$/,
90+
/^{%\s*else\s*%}$/,
91+
/^{%\s*endif\s*%}$/,
92+
/^{%\s*if\s+.*%}$/,
93+
/^{%\s*unless\s+.*%}$/,
94+
/^{%\s*endunless\s*%}$/,
95+
/^{%\s*comment\s*%}$/,
96+
/^{%\s*endcomment\s*%}$/,
97+
]
98+
99+
return nonContentTags.some((pattern) => pattern.test(line))
100+
}

src/content-linter/lib/linting-rules/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ import { outdatedReleasePhaseTerminology } from '@/content-linter/lib/linting-ru
5050
import { britishEnglishQuotes } from '@/content-linter/lib/linting-rules/british-english-quotes'
5151
import { multipleEmphasisPatterns } from '@/content-linter/lib/linting-rules/multiple-emphasis-patterns'
5252
import { noteWarningFormatting } from '@/content-linter/lib/linting-rules/note-warning-formatting'
53-
5453
import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rules/frontmatter-versions-whitespace'
54+
import { headerContentRequirement } from '@/content-linter/lib/linting-rules/header-content-requirement'
5555
import { thirdPartyActionsReusable } from '@/content-linter/lib/linting-rules/third-party-actions-reusable'
5656

5757
const noDefaultAltText = markdownlintGitHub.find((elem) =>
@@ -111,7 +111,7 @@ export const gitHubDocsMarkdownlint = {
111111
noteWarningFormatting, // GHD049
112112
multipleEmphasisPatterns, // GHD050
113113
frontmatterVersionsWhitespace, // GHD051
114-
114+
headerContentRequirement, // GHD053
115115
thirdPartyActionsReusable, // GHD054
116116

117117
// Search-replace rules

src/content-linter/style/github-docs.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ const githubDocsConfig = {
237237
'partial-markdown-files': true,
238238
'yml-files': true,
239239
},
240+
'header-content-requirement': {
241+
// GHD053
242+
severity: 'warning',
243+
'partial-markdown-files': true,
244+
'yml-files': true,
245+
},
240246
'third-party-actions-reusable': {
241247
// GHD054
242248
severity: 'warning',

0 commit comments

Comments
 (0)