1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Convert 21 JavaScript files to TypeScript (#57896)

This commit is contained in:
Kevin Heis
2025-10-10 12:38:08 -07:00
committed by GitHub
parent 58f8bdb7ae
commit 37a2754e32
21 changed files with 222 additions and 98 deletions

View File

@@ -1,20 +1,38 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError, filterTokens } from 'markdownlint-rule-helpers' import { addError, filterTokens } from 'markdownlint-rule-helpers'
import matter from '@gr2m/gray-matter' import matter from '@gr2m/gray-matter'
import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types'
// Adds an error object with details conditionally via the onError callback // Adds an error object with details conditionally via the onError callback
export function addFixErrorDetail(onError, lineNumber, expected, actual, range, fixInfo) { export function addFixErrorDetail(
onError: RuleErrorCallback,
lineNumber: number,
expected: string,
actual: string,
// Using flexible type to accommodate different range formats from various linting rules
range: [number, number] | number[] | null,
// Using any for fixInfo as markdownlint-rule-helpers accepts various fix info structures
fixInfo: any,
): void {
addError(onError, lineNumber, `Expected: ${expected}`, ` Actual: ${actual}`, range, fixInfo) addError(onError, lineNumber, `Expected: ${expected}`, ` Actual: ${actual}`, range, fixInfo)
} }
export function forEachInlineChild(params, type, handler) { export function forEachInlineChild(
filterTokens(params, 'inline', (token) => { params: RuleParams,
for (const child of token.children.filter((c) => c.type === type)) { type: string,
// Using any for child and token types because different linting rules pass tokens with varying structures
// beyond the base MarkdownToken interface (e.g., ImageToken with additional properties)
handler: (child: any, token: any) => void,
): void {
filterTokens(params, 'inline', (token: MarkdownToken) => {
for (const child of token.children!.filter((c) => c.type === type)) {
handler(child, token) handler(child, token)
} }
}) })
} }
export function getRange(line, content) { export function getRange(line: string, content: string): [number, number] | null {
if (content.length === 0) { if (content.length === 0) {
// This function assumes that the content is something. If it's an // This function assumes that the content is something. If it's an
// empty string it can never produce a valid range. // empty string it can never produce a valid range.
@@ -24,7 +42,7 @@ export function getRange(line, content) {
return startColumnIndex !== -1 ? [startColumnIndex + 1, content.length] : null return startColumnIndex !== -1 ? [startColumnIndex + 1, content.length] : null
} }
export function isStringQuoted(text) { export function isStringQuoted(text: string): boolean {
// String starts with either a single or double quote // String starts with either a single or double quote
// ends with either a single or double quote // ends with either a single or double quote
// and optionally ends with a question mark or exclamation point // and optionally ends with a question mark or exclamation point
@@ -32,7 +50,7 @@ export function isStringQuoted(text) {
return /^['"].*['"][?!]?$/.test(text) return /^['"].*['"][?!]?$/.test(text)
} }
export function isStringPunctuated(text) { export function isStringPunctuated(text: string): boolean {
// String ends with punctuation of either // String ends with punctuation of either
// . ? ! and optionally ends with single // . ? ! and optionally ends with single
// or double quotes. This also allows // or double quotes. This also allows
@@ -41,7 +59,7 @@ export function isStringPunctuated(text) {
return /^.*[.?!]['"]?$/.test(text) return /^.*[.?!]['"]?$/.test(text)
} }
export function doesStringEndWithPeriod(text) { export function doesStringEndWithPeriod(text: string): boolean {
// String ends with punctuation of either // String ends with punctuation of either
// . ? ! and optionally ends with single // . ? ! and optionally ends with single
// or double quotes. This also allows // or double quotes. This also allows
@@ -50,7 +68,7 @@ export function doesStringEndWithPeriod(text) {
return /^.*\.['"]?$/.test(text) return /^.*\.['"]?$/.test(text)
} }
export function quotePrecedesLinkOpen(text) { export function quotePrecedesLinkOpen(text: string | undefined): boolean {
if (!text) return false if (!text) return false
return text.endsWith('"') || text.endsWith("'") return text.endsWith('"') || text.endsWith("'")
} }
@@ -87,12 +105,15 @@ export function quotePrecedesLinkOpen(text) {
// { type: 'paragraph_close'}, <-- Index 5 - NOT INCLUDED // { type: 'paragraph_close'}, <-- Index 5 - NOT INCLUDED
// ] // ]
// //
export function filterTokensByOrder(tokens, tokenOrder) { export function filterTokensByOrder(
const matches = [] tokens: MarkdownToken[],
tokenOrder: string[],
): MarkdownToken[] {
const matches: MarkdownToken[] = []
// Get a list of token indexes that match the // Get a list of token indexes that match the
// first token (root) in the tokenOrder array // first token (root) in the tokenOrder array
const tokenRootIndexes = [] const tokenRootIndexes: number[] = []
const firstTokenOrderType = tokenOrder[0] const firstTokenOrderType = tokenOrder[0]
tokens.forEach((token, index) => { tokens.forEach((token, index) => {
if (token.type === firstTokenOrderType) { if (token.type === firstTokenOrderType) {
@@ -125,7 +146,8 @@ export const docsDomains = ['docs.github.com', 'help.github.com', 'developer.git
// This is the format we get from Markdownlint. // This is the format we get from Markdownlint.
// Returns null if the lines do not contain // Returns null if the lines do not contain
// frontmatter properties. // frontmatter properties.
export function getFrontmatter(lines) { // Returns frontmatter as a Record with any values since YAML can contain various types
export function getFrontmatter(lines: string[]): Record<string, any> | null {
const fmString = lines.join('\n') const fmString = lines.join('\n')
const { data } = matter(fmString) const { data } = matter(fmString)
// If there is no frontmatter or the frontmatter contains // If there is no frontmatter or the frontmatter contains
@@ -134,7 +156,7 @@ export function getFrontmatter(lines) {
return data return data
} }
export function getFrontmatterLines(lines) { export function getFrontmatterLines(lines: string[]): string[] {
const indexStart = lines.indexOf('---') const indexStart = lines.indexOf('---')
if (indexStart === -1) return [] if (indexStart === -1) return []
const indexEnd = lines.indexOf('---', indexStart + 1) const indexEnd = lines.indexOf('---', indexStart + 1)

View File

@@ -1,14 +1,17 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers' import { addError } from 'markdownlint-rule-helpers'
import { getRange } from '../helpers/utils' import { getRange } from '../helpers/utils'
import frontmatter from '@/frame/lib/read-frontmatter' import frontmatter from '@/frame/lib/read-frontmatter'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
export const britishEnglishQuotes = { export const britishEnglishQuotes = {
names: ['GHD048', 'british-english-quotes'], names: ['GHD048', 'british-english-quotes'],
description: description:
'Periods and commas should be placed inside quotation marks (American English style)', 'Periods and commas should be placed inside quotation marks (American English style)',
tags: ['punctuation', 'quotes', 'style', 'consistency'], tags: ['punctuation', 'quotes', 'style', 'consistency'],
severity: 'warning', // Non-blocking as requested in the issue severity: 'warning', // Non-blocking as requested in the issue
function: (params, onError) => { function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip autogenerated files // Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n') const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data const fm = frontmatter(frontmatterString).data
@@ -33,7 +36,7 @@ export const britishEnglishQuotes = {
/** /**
* Check if the current position is within a code context (code blocks, inline code, URLs) * Check if the current position is within a code context (code blocks, inline code, URLs)
*/ */
function isInCodeContext(line, allLines, lineIndex) { function isInCodeContext(line: string, allLines: string[], lineIndex: number): boolean {
// Skip if line contains code fences // Skip if line contains code fences
if (line.includes('```') || line.includes('~~~')) { if (line.includes('```') || line.includes('~~~')) {
return true return true
@@ -67,18 +70,22 @@ function isInCodeContext(line, allLines, lineIndex) {
/** /**
* Find and report British English quote patterns in a line * Find and report British English quote patterns in a line
*/ */
function findAndReportBritishQuotes(line, lineNumber, onError) { function findAndReportBritishQuotes(
line: string,
lineNumber: number,
onError: RuleErrorCallback,
): void {
// Pattern to find quote followed by punctuation outside // Pattern to find quote followed by punctuation outside
// Matches: "text". or 'text', or "text", etc. // Matches: "text". or 'text', or "text", etc.
const britishPattern = /(["'])([^"']*?)\1\s*([.,])/g const britishPattern = /(["'])([^"']*?)\1\s*([.,])/g
let match let match: RegExpMatchArray | null
while ((match = britishPattern.exec(line)) !== null) { while ((match = britishPattern.exec(line)) !== null) {
const quoteChar = match[1] const quoteChar = match[1]
const quotedText = match[2] const quotedText = match[2]
const punctuation = match[3] const punctuation = match[3]
const fullMatch = match[0] const fullMatch = match[0]
const startIndex = match.index const startIndex = match.index ?? 0
// Create the corrected version (punctuation inside quotes) // Create the corrected version (punctuation inside quotes)
const correctedText = quoteChar + quotedText + punctuation + quoteChar const correctedText = quoteChar + quotedText + punctuation + quoteChar

View File

@@ -1,18 +1,28 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError, filterTokens } from 'markdownlint-rule-helpers' import { addError, filterTokens } from 'markdownlint-rule-helpers'
import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types'
interface HeadingInfo {
token: MarkdownToken
lineNumber: number
level: number
line: string
}
export const headerContentRequirement = { export const headerContentRequirement = {
names: ['GHD053', 'header-content-requirement'], names: ['GHD053', 'header-content-requirement'],
description: 'Headers must have content between them, such as an introduction', description: 'Headers must have content between them, such as an introduction',
tags: ['headers', 'structure', 'content'], tags: ['headers', 'structure', 'content'],
function: (params, onError) => { function: (params: RuleParams, onError: RuleErrorCallback) => {
const headings = [] const headings: HeadingInfo[] = []
// Collect all heading tokens with their line numbers and levels // Collect all heading tokens with their line numbers and levels
filterTokens(params, 'heading_open', (token) => { filterTokens(params, 'heading_open', (token: MarkdownToken) => {
headings.push({ headings.push({
token, token,
lineNumber: token.lineNumber, lineNumber: token.lineNumber,
level: parseInt(token.tag.slice(1)), // Extract number from h1, h2, etc. level: parseInt(token.tag!.slice(1)), // Extract number from h1, h2, etc.
line: params.lines[token.lineNumber - 1], line: params.lines[token.lineNumber - 1],
}) })
}) })
@@ -49,7 +59,11 @@ export const headerContentRequirement = {
* Check if there is meaningful content between two headings * Check if there is meaningful content between two headings
* Returns true if content exists, false if only whitespace/empty lines * Returns true if content exists, false if only whitespace/empty lines
*/ */
function checkForContentBetweenHeadings(lines, startLineNumber, endLineNumber) { function checkForContentBetweenHeadings(
lines: string[],
startLineNumber: number,
endLineNumber: number,
): boolean {
// Convert to 0-based indexes and skip the heading lines themselves // Convert to 0-based indexes and skip the heading lines themselves
const startIndex = startLineNumber // Skip the current heading line const startIndex = startLineNumber // Skip the current heading line
const endIndex = endLineNumber - 2 // Stop before the next heading line const endIndex = endLineNumber - 2 // Stop before the next heading line
@@ -82,7 +96,7 @@ function checkForContentBetweenHeadings(lines, startLineNumber, endLineNumber) {
* Check if a line contains only Liquid tags that don't produce visible content * Check if a line contains only Liquid tags that don't produce visible content
* This helps avoid false positives for conditional blocks * This helps avoid false positives for conditional blocks
*/ */
function isNonContentLiquidTag(line) { function isNonContentLiquidTag(line: string): boolean {
// Match common non-content Liquid tags // Match common non-content Liquid tags
const nonContentTags = [ const nonContentTags = [
/^{%\s*ifversion\s+.*%}$/, /^{%\s*ifversion\s+.*%}$/,

View File

@@ -1,4 +1,6 @@
// @ts-ignore - markdownlint-rule-search-replace doesn't provide TypeScript declarations
import searchReplace from 'markdownlint-rule-search-replace' import searchReplace from 'markdownlint-rule-search-replace'
// @ts-ignore - @github/markdownlint-github doesn't provide TypeScript declarations
import markdownlintGitHub from '@github/markdownlint-github' import markdownlintGitHub from '@github/markdownlint-github'
import { codeFenceLineLength } from '@/content-linter/lib/linting-rules/code-fence-line-length' import { codeFenceLineLength } from '@/content-linter/lib/linting-rules/code-fence-line-length'
@@ -57,10 +59,13 @@ import { thirdPartyActionsReusable } from '@/content-linter/lib/linting-rules/th
import { frontmatterLandingRecommended } from '@/content-linter/lib/linting-rules/frontmatter-landing-recommended' import { frontmatterLandingRecommended } from '@/content-linter/lib/linting-rules/frontmatter-landing-recommended'
import { ctasSchema } from '@/content-linter/lib/linting-rules/ctas-schema' import { ctasSchema } from '@/content-linter/lib/linting-rules/ctas-schema'
const noDefaultAltText = markdownlintGitHub.find((elem) => // Using any type because @github/markdownlint-github doesn't provide TypeScript declarations
// The elements in the array have a 'names' property that contains rule identifiers
const noDefaultAltText = markdownlintGitHub.find((elem: any) =>
elem.names.includes('no-default-alt-text'), elem.names.includes('no-default-alt-text'),
) )
const noGenericLinkText = markdownlintGitHub.find((elem) => // Using any type because @github/markdownlint-github doesn't provide TypeScript declarations
const noGenericLinkText = markdownlintGitHub.find((elem: any) =>
elem.names.includes('no-generic-link-text'), elem.names.includes('no-generic-link-text'),
) )

View File

@@ -1,5 +1,6 @@
import { TokenKind } from 'liquidjs' // @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers' import { addError } from 'markdownlint-rule-helpers'
import { TokenKind } from 'liquidjs'
import { getDataByLanguage } from '@/data-directory/lib/get-data' import { getDataByLanguage } from '@/data-directory/lib/get-data'
import { import {
@@ -9,6 +10,8 @@ import {
OUTPUT_CLOSE, OUTPUT_CLOSE,
} from '../helpers/liquid-utils' } from '../helpers/liquid-utils'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
/* /*
Checks for instances where a Liquid data or indented_data_reference Checks for instances where a Liquid data or indented_data_reference
tag is used but is not defined. tag is used but is not defined.
@@ -19,11 +22,12 @@ export const liquidDataReferencesDefined = {
'Liquid data or indented data references were found in content that have no value or do not exist in the data directory', 'Liquid data or indented data references were found in content that have no value or do not exist in the data directory',
tags: ['liquid'], tags: ['liquid'],
parser: 'markdownit', parser: 'markdownit',
function: (params, onError) => { function: (params: RuleParams, onError: RuleErrorCallback) => {
const content = params.lines.join('\n') const content = params.lines.join('\n')
// Using any type because getLiquidTokens returns tokens from liquidjs library without complete type definitions
const tokens = getLiquidTokens(content) const tokens = getLiquidTokens(content)
.filter((token) => token.kind === TokenKind.Tag) .filter((token: any) => token.kind === TokenKind.Tag)
.filter((token) => token.name === 'data' || token.name === 'indented_data_reference') .filter((token: any) => token.name === 'data' || token.name === 'indented_data_reference')
if (!tokens.length) return if (!tokens.length) return
@@ -54,12 +58,16 @@ export const liquidDataTagFormat = {
description: description:
'Liquid data or indented data references tags must be correctly formatted and have the correct number of arguments and spacing', 'Liquid data or indented data references tags must be correctly formatted and have the correct number of arguments and spacing',
tags: ['liquid', 'format'], tags: ['liquid', 'format'],
function: (params, onError) => { function: (params: RuleParams, onError: RuleErrorCallback) => {
const CHECK_LIQUID_TAGS = [OUTPUT_OPEN, OUTPUT_CLOSE, '{', '}'] const CHECK_LIQUID_TAGS = [OUTPUT_OPEN, OUTPUT_CLOSE, '{', '}']
const content = params.lines.join('\n') const content = params.lines.join('\n')
const tokenTags = getLiquidTokens(content).filter((token) => token.kind === TokenKind.Tag) // Using any type because getLiquidTokens returns tokens from liquidjs library without complete type definitions
const dataTags = tokenTags.filter((token) => token.name === 'data') // Tokens have properties like 'kind', 'name', 'args', and 'content' that aren't fully typed
const indentedDataTags = tokenTags.filter((token) => token.name === 'indented_data_reference') const tokenTags = getLiquidTokens(content).filter((token: any) => token.kind === TokenKind.Tag)
const dataTags = tokenTags.filter((token: any) => token.name === 'data')
const indentedDataTags = tokenTags.filter(
(token: any) => token.name === 'indented_data_reference',
)
for (const token of dataTags) { for (const token of dataTags) {
// A data tag has only one argument, the data directory path. // A data tag has only one argument, the data directory path.
@@ -125,9 +133,9 @@ export const liquidDataTagFormat = {
} }
// Convenient wrapper because linting is always about English content // Convenient wrapper because linting is always about English content
const getData = (liquidRef) => getDataByLanguage(liquidRef, 'en') const getData = (liquidRef: string) => getDataByLanguage(liquidRef, 'en')
const hasData = (liquidRef) => { const hasData = (liquidRef: string): boolean => {
try { try {
// If a reusable contains a nonexistent data reference, it will // If a reusable contains a nonexistent data reference, it will
// return undefined. If the data reference is inherently broken // return undefined. If the data reference is inherently broken

View File

@@ -1,9 +1,18 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers' import { addError } from 'markdownlint-rule-helpers'
import { getFrontmatter } from '../helpers/utils' import { getFrontmatter } from '../helpers/utils'
import { liquid } from '@/content-render/index' import { liquid } from '@/content-render/index'
import { isLiquidError } from '@/languages/lib/render-with-fallback' import { isLiquidError } from '@/languages/lib/render-with-fallback'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
interface ErrorMessageInfo {
errorDescription: string
lineNumber: number
columnNumber: number
}
/* /*
Attempts to parse all liquid in the frontmatter of a file Attempts to parse all liquid in the frontmatter of a file
to verify the syntax is correct. to verify the syntax is correct.
@@ -12,7 +21,7 @@ export const frontmatterLiquidSyntax = {
names: ['GHD017', 'frontmatter-liquid-syntax'], names: ['GHD017', 'frontmatter-liquid-syntax'],
description: 'Frontmatter properties must use valid Liquid', description: 'Frontmatter properties must use valid Liquid',
tags: ['liquid', 'frontmatter'], tags: ['liquid', 'frontmatter'],
function: (params, onError) => { function: (params: RuleParams, onError: RuleErrorCallback) => {
const fm = getFrontmatter(params.lines) const fm = getFrontmatter(params.lines)
if (!fm) return if (!fm) return
@@ -31,7 +40,7 @@ export const frontmatterLiquidSyntax = {
// If the error source is not a Liquid error but rather a // If the error source is not a Liquid error but rather a
// ReferenceError or bad type we should allow that error to be thrown // ReferenceError or bad type we should allow that error to be thrown
if (!isLiquidError(error)) throw error if (!isLiquidError(error)) throw error
const { errorDescription, columnNumber } = getErrorMessageInfo(error.message) const { errorDescription, columnNumber } = getErrorMessageInfo((error as Error).message)
const lineNumber = params.lines.findIndex((line) => line.trim().startsWith(`${key}:`)) + 1 const lineNumber = params.lines.findIndex((line) => line.trim().startsWith(`${key}:`)) + 1
// Add the key length plus 3 to the column number to account colon and // Add the key length plus 3 to the column number to account colon and
// for the space after the key and column number starting at 1. // for the space after the key and column number starting at 1.
@@ -42,7 +51,7 @@ export const frontmatterLiquidSyntax = {
startRange + value.length - 1 > params.lines[lineNumber - 1].length startRange + value.length - 1 > params.lines[lineNumber - 1].length
? params.lines[lineNumber - 1].length - startRange + 1 ? params.lines[lineNumber - 1].length - startRange + 1
: value.length : value.length
const range = [startRange, endRange] const range: [number, number] = [startRange, endRange]
addError( addError(
onError, onError,
lineNumber, lineNumber,
@@ -64,20 +73,22 @@ export const liquidSyntax = {
names: ['GHD018', 'liquid-syntax'], names: ['GHD018', 'liquid-syntax'],
description: 'Markdown content must use valid Liquid', description: 'Markdown content must use valid Liquid',
tags: ['liquid'], tags: ['liquid'],
function: function GHD018(params, onError) { function: function GHD018(params: RuleParams, onError: RuleErrorCallback) {
try { try {
liquid.parse(params.lines.join('\n')) liquid.parse(params.lines.join('\n'))
} catch (error) { } catch (error) {
// If the error source is not a Liquid error but rather a // If the error source is not a Liquid error but rather a
// ReferenceError or bad type we should allow that error to be thrown // ReferenceError or bad type we should allow that error to be thrown
if (!isLiquidError(error)) throw error if (!isLiquidError(error)) throw error
const { errorDescription, lineNumber, columnNumber } = getErrorMessageInfo(error.message) const { errorDescription, lineNumber, columnNumber } = getErrorMessageInfo(
(error as Error).message,
)
const line = params.lines[lineNumber - 1] const line = params.lines[lineNumber - 1]
// We don't have enough information to know the length of the full // We don't have enough information to know the length of the full
// liquid tag without doing some regex testing and making assumptions // liquid tag without doing some regex testing and making assumptions
// about if the end tag is correctly formed, so we just give a // about if the end tag is correctly formed, so we just give a
// range from the start of the tag to the end of the line. // range from the start of the tag to the end of the line.
const range = [columnNumber, line.slice(columnNumber - 1).length] const range: [number, number] = [columnNumber, line.slice(columnNumber - 1).length]
addError( addError(
onError, onError,
lineNumber, lineNumber,
@@ -90,7 +101,7 @@ export const liquidSyntax = {
}, },
} }
function getErrorMessageInfo(message) { function getErrorMessageInfo(message: string): ErrorMessageInfo {
const [errorDescription, lineString, columnString] = message.split(',') const [errorDescription, lineString, columnString] = message.split(',')
// There has to be a line number so we'll default to line 1 if the message // There has to be a line number so we'll default to line 1 if the message
// doesn't contain a line number. // doesn't contain a line number.

View File

@@ -1,11 +1,14 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError, ellipsify } from 'markdownlint-rule-helpers' import { addError, ellipsify } from 'markdownlint-rule-helpers'
import { getRange } from '@/content-linter/lib/helpers/utils' import { getRange } from '@/content-linter/lib/helpers/utils'
import frontmatter from '@/frame/lib/read-frontmatter' import frontmatter from '@/frame/lib/read-frontmatter'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
// Mapping of outdated terms to their new replacements // Mapping of outdated terms to their new replacements
// Order matters - longer phrases must come first to avoid partial matches // Order matters - longer phrases must come first to avoid partial matches
const TERMINOLOGY_REPLACEMENTS = [ const TERMINOLOGY_REPLACEMENTS: [string, string][] = [
// Beta variations → public preview (longer phrases first) // Beta variations → public preview (longer phrases first)
['limited public beta', 'public preview'], ['limited public beta', 'public preview'],
['public beta', 'public preview'], ['public beta', 'public preview'],
@@ -23,35 +26,51 @@ const TERMINOLOGY_REPLACEMENTS = [
['sunset', 'retired'], ['sunset', 'retired'],
] ]
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 // Precompile RegExp objects for better performance
const COMPILED_REGEXES = TERMINOLOGY_REPLACEMENTS.map(([outdatedTerm, replacement]) => ({ const COMPILED_REGEXES: CompiledRegex[] = TERMINOLOGY_REPLACEMENTS.map(
regex: new RegExp(`(?<!\\w|-|_)${outdatedTerm.replace(/\s+/g, '\\s+')}(?!\\w|-|_)`, 'gi'), ([outdatedTerm, replacement]) => ({
outdatedTerm, regex: new RegExp(`(?<!\\w|-|_)${outdatedTerm.replace(/\s+/g, '\\s+')}(?!\\w|-|_)`, 'gi'),
replacement, outdatedTerm,
})) replacement,
}),
)
/** /**
* Find all non-overlapping matches of outdated terminology in a line * Find all non-overlapping matches of outdated terminology in a line
* @param {string} line - The line of text to search * @param line - The line of text to search
* @returns {Array} Array of match objects with start, end, text, replacement, and outdatedTerm * @returns Array of match objects with start, end, text, replacement, and outdatedTerm
*/ */
function findOutdatedTerminologyMatches(line) { function findOutdatedTerminologyMatches(line: string): MatchInfo[] {
const foundMatches = [] const foundMatches: MatchInfo[] = []
// Check each outdated term (in order - longest first) // Check each outdated term (in order - longest first)
for (const { regex, outdatedTerm, replacement } of COMPILED_REGEXES) { for (const { regex, outdatedTerm, replacement } of COMPILED_REGEXES) {
// Reset regex state for each line // Reset regex state for each line
regex.lastIndex = 0 regex.lastIndex = 0
let match let match: RegExpExecArray | null
while ((match = regex.exec(line)) !== null) { while ((match = regex.exec(line)) !== null) {
// Check if this match overlaps with any existing matches // Check if this match overlaps with any existing matches
const overlaps = foundMatches.some( const overlaps = foundMatches.some(
(existing) => (existing) =>
(match.index >= existing.start && match.index < existing.end) || (match!.index >= existing.start && match!.index < existing.end) ||
(match.index + match[0].length > existing.start && (match!.index + match![0].length > existing.start &&
match.index + match[0].length <= existing.end) || match!.index + match![0].length <= existing.end) ||
(match.index <= existing.start && match.index + match[0].length >= existing.end), (match!.index <= existing.start && match!.index + match![0].length >= existing.end),
) )
if (!overlaps) { if (!overlaps) {
@@ -76,7 +95,7 @@ export const outdatedReleasePhaseTerminology = {
'Outdated release phase terminology should be replaced with current GitHub terminology', 'Outdated release phase terminology should be replaced with current GitHub terminology',
tags: ['terminology', 'consistency', 'release-phases'], tags: ['terminology', 'consistency', 'release-phases'],
severity: 'error', severity: 'error',
function: (params, onError) => { function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip autogenerated files // Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n') const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data const fm = frontmatter(frontmatterString).data

View File

@@ -1,7 +1,10 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers' import { addError } from 'markdownlint-rule-helpers'
import { getRange } from '../helpers/utils' import { getRange } from '../helpers/utils'
import frontmatter from '@/frame/lib/read-frontmatter' import frontmatter from '@/frame/lib/read-frontmatter'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
// Regex to detect table rows (must start with |, contain at least one more |, and end with optional whitespace) // Regex to detect table rows (must start with |, contain at least one more |, and end with optional whitespace)
const TABLE_ROW_REGEX = /^\s*\|.*\|\s*$/ const TABLE_ROW_REGEX = /^\s*\|.*\|\s*$/
// Regex to detect table separator rows (contains only |, :, -, and whitespace) // Regex to detect table separator rows (contains only |, :, -, and whitespace)
@@ -12,7 +15,7 @@ const LIQUID_ONLY_CELL_REGEX = /^\s*{%\s*(ifversion|else|endif|elsif).*%}\s*$/
/** /**
* Counts the number of columns in a table row by splitting on | and handling edge cases * Counts the number of columns in a table row by splitting on | and handling edge cases
*/ */
function countColumns(row) { function countColumns(row: string): number {
// Remove leading and trailing whitespace // Remove leading and trailing whitespace
const trimmed = row.trim() const trimmed = row.trim()
@@ -38,7 +41,7 @@ function countColumns(row) {
/** /**
* Checks if a table row contains only Liquid conditionals * Checks if a table row contains only Liquid conditionals
*/ */
function isLiquidOnlyRow(row) { function isLiquidOnlyRow(row: string): boolean {
const trimmed = row.trim() const trimmed = row.trim()
if (!trimmed.includes('|')) return false if (!trimmed.includes('|')) return false
@@ -61,7 +64,7 @@ export const tableColumnIntegrity = {
description: 'Tables must have consistent column counts across all rows', description: 'Tables must have consistent column counts across all rows',
tags: ['tables', 'accessibility', 'formatting'], tags: ['tables', 'accessibility', 'formatting'],
severity: 'error', severity: 'error',
function: (params, onError) => { function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip autogenerated files // Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n') const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data const fm = frontmatter(frontmatterString).data
@@ -69,9 +72,7 @@ export const tableColumnIntegrity = {
const lines = params.lines const lines = params.lines
let inTable = false let inTable = false
let expectedColumnCount = null let expectedColumnCount: number | null = null
let tableStartLine = null
let headerRow = null
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i] const line = lines[i]
@@ -84,8 +85,6 @@ export const tableColumnIntegrity = {
const nextLine = lines[i + 1] const nextLine = lines[i + 1]
if (nextLine && TABLE_SEPARATOR_REGEX.test(nextLine)) { if (nextLine && TABLE_SEPARATOR_REGEX.test(nextLine)) {
inTable = true inTable = true
tableStartLine = i + 1
headerRow = line
expectedColumnCount = countColumns(line) expectedColumnCount = countColumns(line)
continue continue
} }
@@ -95,8 +94,6 @@ export const tableColumnIntegrity = {
if (inTable && !isTableRow) { if (inTable && !isTableRow) {
inTable = false inTable = false
expectedColumnCount = null expectedColumnCount = null
tableStartLine = null
headerRow = null
continue continue
} }
@@ -113,10 +110,10 @@ export const tableColumnIntegrity = {
const range = getRange(line, line.trim()) const range = getRange(line, line.trim())
let errorMessage let errorMessage
if (actualColumnCount > expectedColumnCount) { if (actualColumnCount > expectedColumnCount!) {
errorMessage = `Table row has ${actualColumnCount} columns but header has ${expectedColumnCount}. Add ${actualColumnCount - expectedColumnCount} more column(s) to the header row to match this row.` errorMessage = `Table row has ${actualColumnCount} columns but header has ${expectedColumnCount}. Add ${actualColumnCount - expectedColumnCount!} more column(s) to the header row to match this row.`
} else { } else {
errorMessage = `Table row has ${actualColumnCount} columns but header has ${expectedColumnCount}. Add ${expectedColumnCount - actualColumnCount} missing column(s) to this row.` errorMessage = `Table row has ${actualColumnCount} columns but header has ${expectedColumnCount}. Add ${expectedColumnCount! - actualColumnCount} missing column(s) to this row.`
} }
addError( addError(

View File

@@ -9,10 +9,12 @@ const pages = await loadPageMap(pageList)
const redirects = await loadRedirects(pageList) const redirects = await loadRedirects(pageList)
const liquidElsif = /{%\s*elsif/ const liquidElsif = /{%\s*elsif/
const containsLiquidElseIf = (text) => liquidElsif.test(text) const containsLiquidElseIf = (text: string) => liquidElsif.test(text)
describe('front matter', () => { describe('front matter', () => {
function makeCustomErrorMessage(page, trouble, key) { // Using any type for page because it comes from loadPages which returns dynamic page objects with varying properties
// Using any[] for trouble because the error objects have different shapes depending on the validation that failed
function makeCustomErrorMessage(page: any, trouble: any[], key: string) {
let customErrorMessage = `In the front matter of ${page.relativePath} ` let customErrorMessage = `In the front matter of ${page.relativePath} `
if (trouble.length > 0) { if (trouble.length > 0) {
if (trouble.length === 1) { if (trouble.length === 1) {
@@ -20,7 +22,8 @@ describe('front matter', () => {
} else { } else {
customErrorMessage += `there are ${trouble.length} .${key} front matter entries that are not correct.` customErrorMessage += `there are ${trouble.length} .${key} front matter entries that are not correct.`
} }
const nonWarnings = trouble.filter((t) => !t.warning) // Using any type because trouble array contains objects with varying error properties
const nonWarnings = trouble.filter((t: any) => !t.warning)
for (const { uri, index, redirects } of nonWarnings) { for (const { uri, index, redirects } of nonWarnings) {
customErrorMessage += `\nindex: ${index} URI: ${uri}` customErrorMessage += `\nindex: ${index} URI: ${uri}`
if (redirects) { if (redirects) {
@@ -29,7 +32,8 @@ describe('front matter', () => {
customErrorMessage += '\tPage not found' customErrorMessage += '\tPage not found'
} }
} }
if (trouble.find((t) => t.redirects)) { // Using any type because trouble array contains objects with varying error properties
if (trouble.find((t: any) => t.redirects)) {
customErrorMessage += `\n\nNOTE! To automatically fix the redirects run this command:\n` customErrorMessage += `\n\nNOTE! To automatically fix the redirects run this command:\n`
customErrorMessage += `\n\t./src/links/scripts/update-internal-links.js content/${page.relativePath}\n\n` customErrorMessage += `\n\t./src/links/scripts/update-internal-links.js content/${page.relativePath}\n\n`
} }
@@ -46,7 +50,8 @@ describe('front matter', () => {
const redirectsContext = { redirects, pages } const redirectsContext = { redirects, pages }
const trouble = page.includeGuides const trouble = page.includeGuides
.map((uri, i) => checkURL(uri, i, redirectsContext)) // Using any type for uri because includeGuides can contain various URI formats
.map((uri: any, i: number) => checkURL(uri, i, redirectsContext))
.filter(Boolean) .filter(Boolean)
const customErrorMessage = makeCustomErrorMessage(page, trouble, 'includeGuides') const customErrorMessage = makeCustomErrorMessage(page, trouble, 'includeGuides')

View File

@@ -1,5 +1,6 @@
import { describe, expect, test } from 'vitest' import { describe, expect, test } from 'vitest'
import markdownlint from 'markdownlint' import markdownlint from 'markdownlint'
// @ts-ignore - markdownlint-rule-search-replace doesn't provide TypeScript declarations
import searchReplace from 'markdownlint-rule-search-replace' import searchReplace from 'markdownlint-rule-search-replace'
import { searchReplaceConfig } from '../../style/github-docs' import { searchReplaceConfig } from '../../style/github-docs'

View File

@@ -42,7 +42,9 @@ describe(liquidQuotedConditionalArg.names.join(' - '), () => {
const errors = result.markdown const errors = result.markdown
expect(errors.length).toBe(3) expect(errors.length).toBe(3)
expect(errors.map((error) => error.lineNumber)).toEqual([6, 7, 8]) expect(errors.map((error) => error.lineNumber)).toEqual([6, 7, 8])
expect(errors[0].errorRange).toEqual([1, 22], [1, 29], [1, 23]) expect(errors[0].errorRange).toEqual([1, 22])
expect(errors[1].errorRange).toEqual([1, 29])
expect(errors[2].errorRange).toEqual([1, 30])
}) })
test('unless conditional with quote args fails', async () => { test('unless conditional with quote args fails', async () => {
const markdown = [ const markdown = [

View File

@@ -1,4 +1,6 @@
import { visit } from 'unist-util-visit' import { visit } from 'unist-util-visit'
import type { Node, Parent } from 'unist'
import type { Element } from 'hast'
/** /**
* A rehype plugin that automatically adds aria-labelledby attributes to tables * A rehype plugin that automatically adds aria-labelledby attributes to tables
@@ -30,16 +32,24 @@ import { visit } from 'unist-util-visit'
* 4. Skipping tables that already have accessibility attributes * 4. Skipping tables that already have accessibility attributes
*/ */
function isTableElement(node) { interface HeadingInfo {
return node.type === 'element' && node.tagName === 'table' id: string
text: string
} }
function isHeadingElement(node) { function isTableElement(node: Node): node is Element {
return node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) return node.type === 'element' && (node as Element).tagName === 'table'
} }
function hasExistingAccessibilityAttributes(tableNode) { function isHeadingElement(node: Node): node is Element {
return ( return (
node.type === 'element' &&
['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes((node as Element).tagName)
)
}
function hasExistingAccessibilityAttributes(tableNode: Element): boolean {
return !!(
tableNode.properties && tableNode.properties &&
(tableNode.properties.ariaLabel || (tableNode.properties.ariaLabel ||
tableNode.properties.ariaLabelledBy || tableNode.properties.ariaLabelledBy ||
@@ -48,13 +58,13 @@ function hasExistingAccessibilityAttributes(tableNode) {
) )
} }
function hasExistingCaption(tableNode) { function hasExistingCaption(tableNode: Element): boolean {
return tableNode.children?.some( return tableNode.children?.some(
(child) => child.type === 'element' && child.tagName === 'caption', (child) => child.type === 'element' && (child as Element).tagName === 'caption',
) )
} }
function findPrecedingHeading(parent, tableIndex) { function findPrecedingHeading(parent: Parent, tableIndex: number): HeadingInfo | null {
if (!parent.children || tableIndex === 0) return null if (!parent.children || tableIndex === 0) return null
// Look backwards from the table position for the nearest heading // Look backwards from the table position for the nearest heading
@@ -66,7 +76,7 @@ function findPrecedingHeading(parent, tableIndex) {
const headingId = node.properties?.id const headingId = node.properties?.id
if (headingId) { if (headingId) {
return { return {
id: headingId, id: headingId as string,
text: extractTextFromNode(node), text: extractTextFromNode(node),
} }
} }
@@ -75,7 +85,7 @@ function findPrecedingHeading(parent, tableIndex) {
// Stop searching if we hit another table or significant content block // Stop searching if we hit another table or significant content block
if ( if (
isTableElement(node) || isTableElement(node) ||
(node.type === 'element' && ['section', 'article', 'div'].includes(node.tagName)) (node.type === 'element' && ['section', 'article', 'div'].includes((node as Element).tagName))
) { ) {
break break
} }
@@ -84,13 +94,13 @@ function findPrecedingHeading(parent, tableIndex) {
return null return null
} }
function extractTextFromNode(node) { function extractTextFromNode(node: Node): string {
if (node.type === 'text') { if (node.type === 'text') {
return node.value return (node as any).value
} }
if (node.type === 'element' && node.children) { if (node.type === 'element' && (node as Element).children) {
return node.children return (node as Element).children
.map((child) => extractTextFromNode(child)) .map((child) => extractTextFromNode(child))
.filter(Boolean) .filter(Boolean)
.join('') .join('')
@@ -101,8 +111,8 @@ function extractTextFromNode(node) {
} }
export default function addTableAccessibilityLabels() { export default function addTableAccessibilityLabels() {
return (tree) => { return (tree: Node) => {
visit(tree, (node, index, parent) => { visit(tree, (node: Node, index: number | undefined, parent: Parent | undefined) => {
if (!isTableElement(node) || !parent || typeof index !== 'number') { if (!isTableElement(node) || !parent || typeof index !== 'number') {
return return
} }

View File

@@ -7,10 +7,21 @@ import statsd from '@/observability/lib/statsd'
// The only reason this is exported is for the sake of the unit tests' // The only reason this is exported is for the sake of the unit tests'
// ability to test in-memory miss after purging this with a mutation // ability to test in-memory miss after purging this with a mutation
export const cache = new Map() // Using any type for cache values because this function can fetch any JSON structure
// The returned JSON content structure is unknown until runtime
export const cache = new Map<string, any>()
const inProd = process.env.NODE_ENV === 'production' const inProd = process.env.NODE_ENV === 'production'
interface GetRemoteJSONConfig {
retry?: {
limit?: number
}
timeout?: {
response?: number
}
}
// Wrapper on `got()` that is able to both cache in memory and on disk. // Wrapper on `got()` that is able to both cache in memory and on disk.
// The on-disk caching is in `.remotejson/`. // The on-disk caching is in `.remotejson/`.
// We use this for downloading `redirects.json` files from one of the // We use this for downloading `redirects.json` files from one of the
@@ -21,7 +32,12 @@ const inProd = process.env.NODE_ENV === 'production'
// 1. Is it in memory cache? // 1. Is it in memory cache?
// 2. No, is it on disk? // 2. No, is it on disk?
// 3. No, download from the internet then store responses in memory and disk // 3. No, download from the internet then store responses in memory and disk
export default async function getRemoteJSON(url, config) { // Using any return type because this function fetches arbitrary JSON from remote URLs
// The JSON structure varies depending on the URL and cannot be known at compile time
export default async function getRemoteJSON(
url: string,
config?: GetRemoteJSONConfig,
): Promise<any> {
// We could get fancy and make the cache key depend on the `config` too // We could get fancy and make the cache key depend on the `config` too
// given that this is A) only used for archived enterprise stuff, // given that this is A) only used for archived enterprise stuff,
// and B) the config is only applicable on cache miss when doing the `got()`. // and B) the config is only applicable on cache miss when doing the `got()`.
@@ -59,7 +75,14 @@ export default async function getRemoteJSON(url, config) {
} }
} }
} catch (error) { } catch (error) {
if (!(error instanceof SyntaxError || (error instanceof Error && error.code === 'ENOENT'))) { if (
!(
error instanceof SyntaxError ||
(error instanceof Error &&
'code' in error &&
(error as NodeJS.ErrnoException).code === 'ENOENT')
)
) {
throw error throw error
} }
} }