github-docs-arabic-enhanced / src /content-linter /lib /linting-rules /outdated-release-phase-terminology.ts
AbdulElahGwaith's picture
Upload folder using huggingface_hub
88df9e4 verified
import { addError, ellipsify } from 'markdownlint-rule-helpers'
import { getRange } from '@/content-linter/lib/helpers/utils'
import frontmatter from '@/frame/lib/read-frontmatter'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
// Mapping of outdated terms to their new replacements
// Order matters - longer phrases must come first to avoid partial matches
const TERMINOLOGY_REPLACEMENTS: [string, string][] = [
// Beta variations → public preview (longer phrases first)
['limited public beta', 'public preview'],
['public beta', 'public preview'],
['private beta', 'private preview'],
['beta', 'public preview'],
// Alpha → private preview
['alpha', 'private preview'],
// Deprecated variations → closing down
['deprecation', 'closing down'],
['deprecated', 'closing down'],
// Sunset → retired
['sunset', 'retired'],
]
// Don't lint filepaths that have legitimate uses of these terms
const EXCLUDED_PATHS: string[] = [
// Individual files
'content/actions/reference/runners/github-hosted-runners.md',
'content/actions/reference/workflows-and-actions/metadata-syntax.md',
'content/admin/administering-your-instance/administering-your-instance-from-the-command-line/command-line-utilities.md',
'content/authentication/managing-commit-signature-verification/checking-for-existing-gpg-keys.md',
'content/codespaces/setting-your-user-preferences/choosing-the-stable-or-beta-host-image.md',
'content/rest/using-the-rest-api/getting-started-with-the-rest-api.md',
'data/reusables/actions/jobs/choosing-runner-github-hosted.md',
'data/reusables/code-scanning/codeql-query-tables/cpp.md',
'data/reusables/dependabot/dependabot-updates-supported-versioning-tags.md',
'data/variables/release-phases.yml',
'data/release-notes/enterprise-server/3-17/0-rc1.yml',
// Directories
'content/site-policy/',
'data/features/',
'data/release-notes/enterprise-server/3-14/',
'data/release-notes/enterprise-server/3-15/',
]
interface CompiledRegex {
regex: RegExp
outdatedTerm: string
replacement: string
}
interface MatchInfo {
start: number
end: number
text: string
replacement: string
outdatedTerm: string
}
// Precompile RegExp objects for better performance
const COMPILED_REGEXES: CompiledRegex[] = TERMINOLOGY_REPLACEMENTS.map(
([outdatedTerm, replacement]) => ({
regex: new RegExp(`(?<!\\w|-|_)${outdatedTerm.replace(/\s+/g, '\\s+')}(?!\\w|-|_)`, 'gi'),
outdatedTerm,
replacement,
}),
)
/**
* Find all non-overlapping matches of outdated terminology in a line
* @param line - The line of text to search
* @returns Array of match objects with start, end, text, replacement, and outdatedTerm
*/
function findOutdatedTerminologyMatches(line: string): MatchInfo[] {
const foundMatches: MatchInfo[] = []
// Check each outdated term (in order - longest first)
for (const { regex, outdatedTerm, replacement } of COMPILED_REGEXES) {
// Reset regex state for each line
regex.lastIndex = 0
let match: RegExpExecArray | null
while ((match = regex.exec(line)) !== null) {
// Check if this match overlaps with any existing matches
const overlaps = foundMatches.some(
(existing) =>
(match!.index >= existing.start && match!.index < existing.end) ||
(match!.index + match![0].length > existing.start &&
match!.index + match![0].length <= existing.end) ||
(match!.index <= existing.start && match!.index + match![0].length >= existing.end),
)
if (!overlaps) {
foundMatches.push({
start: match.index,
end: match.index + match[0].length,
text: match[0],
replacement,
outdatedTerm,
})
}
}
}
// Sort matches by position for consistent ordering
return foundMatches.sort((a, b) => a.start - b.start)
}
export const outdatedReleasePhaseTerminology = {
names: ['GHD046', 'outdated-release-phase-terminology'],
description:
'Outdated release phase terminology should be replaced with current GitHub terminology',
tags: ['terminology', 'consistency', 'release-phases'],
severity: 'error',
function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip excluded files
for (const filepath of EXCLUDED_PATHS) {
if (params.name.startsWith(filepath)) {
return
}
}
// Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data
if (fm && fm.autogenerated) return
// Check all lines for outdated terminology
for (let i = 0; i < params.lines.length; i++) {
const line = params.lines[i]
const lineNumber = i + 1
// Find all matches on this line
const foundMatches = findOutdatedTerminologyMatches(line)
// Report all found matches
for (const matchInfo of foundMatches) {
const range = getRange(line, matchInfo.text)
const errorMessage = `Replace outdated terminology "${matchInfo.text}" with "${matchInfo.replacement}"`
// Provide a fix suggestion
const fixInfo = {
editColumn: matchInfo.start + 1,
deleteCount: matchInfo.text.length,
insertText: matchInfo.replacement,
}
addError(onError, lineNumber, errorMessage, ellipsify(line), range, fixInfo)
}
}
},
}