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 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
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)
}
export function forEachInlineChild(params, type, handler) {
filterTokens(params, 'inline', (token) => {
for (const child of token.children.filter((c) => c.type === type)) {
export function forEachInlineChild(
params: RuleParams,
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)
}
})
}
export function getRange(line, content) {
export function getRange(line: string, content: string): [number, number] | null {
if (content.length === 0) {
// This function assumes that the content is something. If it's an
// 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
}
export function isStringQuoted(text) {
export function isStringQuoted(text: string): boolean {
// String starts 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
@@ -32,7 +50,7 @@ export function isStringQuoted(text) {
return /^['"].*['"][?!]?$/.test(text)
}
export function isStringPunctuated(text) {
export function isStringPunctuated(text: string): boolean {
// String ends with punctuation of either
// . ? ! and optionally ends with single
// or double quotes. This also allows
@@ -41,7 +59,7 @@ export function isStringPunctuated(text) {
return /^.*[.?!]['"]?$/.test(text)
}
export function doesStringEndWithPeriod(text) {
export function doesStringEndWithPeriod(text: string): boolean {
// String ends with punctuation of either
// . ? ! and optionally ends with single
// or double quotes. This also allows
@@ -50,7 +68,7 @@ export function doesStringEndWithPeriod(text) {
return /^.*\.['"]?$/.test(text)
}
export function quotePrecedesLinkOpen(text) {
export function quotePrecedesLinkOpen(text: string | undefined): boolean {
if (!text) return false
return text.endsWith('"') || text.endsWith("'")
}
@@ -87,12 +105,15 @@ export function quotePrecedesLinkOpen(text) {
// { type: 'paragraph_close'}, <-- Index 5 - NOT INCLUDED
// ]
//
export function filterTokensByOrder(tokens, tokenOrder) {
const matches = []
export function filterTokensByOrder(
tokens: MarkdownToken[],
tokenOrder: string[],
): MarkdownToken[] {
const matches: MarkdownToken[] = []
// Get a list of token indexes that match the
// first token (root) in the tokenOrder array
const tokenRootIndexes = []
const tokenRootIndexes: number[] = []
const firstTokenOrderType = tokenOrder[0]
tokens.forEach((token, index) => {
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.
// Returns null if the lines do not contain
// 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 { data } = matter(fmString)
// If there is no frontmatter or the frontmatter contains
@@ -134,7 +156,7 @@ export function getFrontmatter(lines) {
return data
}
export function getFrontmatterLines(lines) {
export function getFrontmatterLines(lines: string[]): string[] {
const indexStart = lines.indexOf('---')
if (indexStart === -1) return []
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 { getRange } from '../helpers/utils'
import frontmatter from '@/frame/lib/read-frontmatter'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
export const britishEnglishQuotes = {
names: ['GHD048', 'british-english-quotes'],
description:
'Periods and commas should be placed inside quotation marks (American English style)',
tags: ['punctuation', 'quotes', 'style', 'consistency'],
severity: 'warning', // Non-blocking as requested in the issue
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n')
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)
*/
function isInCodeContext(line, allLines, lineIndex) {
function isInCodeContext(line: string, allLines: string[], lineIndex: number): boolean {
// Skip if line contains code fences
if (line.includes('```') || line.includes('~~~')) {
return true
@@ -67,18 +70,22 @@ function isInCodeContext(line, allLines, lineIndex) {
/**
* 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
// Matches: "text". or 'text', or "text", etc.
const britishPattern = /(["'])([^"']*?)\1\s*([.,])/g
let match
let match: RegExpMatchArray | null
while ((match = britishPattern.exec(line)) !== null) {
const quoteChar = match[1]
const quotedText = match[2]
const punctuation = match[3]
const fullMatch = match[0]
const startIndex = match.index
const startIndex = match.index ?? 0
// Create the corrected version (punctuation inside quotes)
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 type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-linter/types'
interface HeadingInfo {
token: MarkdownToken
lineNumber: number
level: number
line: string
}
export const headerContentRequirement = {
names: ['GHD053', 'header-content-requirement'],
description: 'Headers must have content between them, such as an introduction',
tags: ['headers', 'structure', 'content'],
function: (params, onError) => {
const headings = []
function: (params: RuleParams, onError: RuleErrorCallback) => {
const headings: HeadingInfo[] = []
// Collect all heading tokens with their line numbers and levels
filterTokens(params, 'heading_open', (token) => {
filterTokens(params, 'heading_open', (token: MarkdownToken) => {
headings.push({
token,
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],
})
})
@@ -49,7 +59,11 @@ export const headerContentRequirement = {
* Check if there is meaningful content between two headings
* 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
const startIndex = startLineNumber // Skip the current 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
* This helps avoid false positives for conditional blocks
*/
function isNonContentLiquidTag(line) {
function isNonContentLiquidTag(line: string): boolean {
// Match common non-content Liquid tags
const nonContentTags = [
/^{%\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'
// @ts-ignore - @github/markdownlint-github doesn't provide TypeScript declarations
import markdownlintGitHub from '@github/markdownlint-github'
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 { 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'),
)
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'),
)

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 { TokenKind } from 'liquidjs'
import { getDataByLanguage } from '@/data-directory/lib/get-data'
import {
@@ -9,6 +10,8 @@ import {
OUTPUT_CLOSE,
} from '../helpers/liquid-utils'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
/*
Checks for instances where a Liquid data or indented_data_reference
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',
tags: ['liquid'],
parser: 'markdownit',
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const content = params.lines.join('\n')
// Using any type because getLiquidTokens returns tokens from liquidjs library without complete type definitions
const tokens = getLiquidTokens(content)
.filter((token) => token.kind === TokenKind.Tag)
.filter((token) => token.name === 'data' || token.name === 'indented_data_reference')
.filter((token: any) => token.kind === TokenKind.Tag)
.filter((token: any) => token.name === 'data' || token.name === 'indented_data_reference')
if (!tokens.length) return
@@ -54,12 +58,16 @@ export const liquidDataTagFormat = {
description:
'Liquid data or indented data references tags must be correctly formatted and have the correct number of arguments and spacing',
tags: ['liquid', 'format'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const CHECK_LIQUID_TAGS = [OUTPUT_OPEN, OUTPUT_CLOSE, '{', '}']
const content = params.lines.join('\n')
const tokenTags = getLiquidTokens(content).filter((token) => token.kind === TokenKind.Tag)
const dataTags = tokenTags.filter((token) => token.name === 'data')
const indentedDataTags = tokenTags.filter((token) => token.name === 'indented_data_reference')
// Using any type because getLiquidTokens returns tokens from liquidjs library without complete type definitions
// Tokens have properties like 'kind', 'name', 'args', and 'content' that aren't fully typed
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) {
// 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
const getData = (liquidRef) => getDataByLanguage(liquidRef, 'en')
const getData = (liquidRef: string) => getDataByLanguage(liquidRef, 'en')
const hasData = (liquidRef) => {
const hasData = (liquidRef: string): boolean => {
try {
// If a reusable contains a nonexistent data reference, it will
// 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 { getFrontmatter } from '../helpers/utils'
import { liquid } from '@/content-render/index'
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
to verify the syntax is correct.
@@ -12,7 +21,7 @@ export const frontmatterLiquidSyntax = {
names: ['GHD017', 'frontmatter-liquid-syntax'],
description: 'Frontmatter properties must use valid Liquid',
tags: ['liquid', 'frontmatter'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const fm = getFrontmatter(params.lines)
if (!fm) return
@@ -31,7 +40,7 @@ export const frontmatterLiquidSyntax = {
// If the error source is not a Liquid error but rather a
// ReferenceError or bad type we should allow that error to be thrown
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
// 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.
@@ -42,7 +51,7 @@ export const frontmatterLiquidSyntax = {
startRange + value.length - 1 > params.lines[lineNumber - 1].length
? params.lines[lineNumber - 1].length - startRange + 1
: value.length
const range = [startRange, endRange]
const range: [number, number] = [startRange, endRange]
addError(
onError,
lineNumber,
@@ -64,20 +73,22 @@ export const liquidSyntax = {
names: ['GHD018', 'liquid-syntax'],
description: 'Markdown content must use valid Liquid',
tags: ['liquid'],
function: function GHD018(params, onError) {
function: function GHD018(params: RuleParams, onError: RuleErrorCallback) {
try {
liquid.parse(params.lines.join('\n'))
} catch (error) {
// If the error source is not a Liquid error but rather a
// ReferenceError or bad type we should allow that error to be thrown
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]
// We don't have enough information to know the length of the full
// liquid tag without doing some regex testing and making assumptions
// 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.
const range = [columnNumber, line.slice(columnNumber - 1).length]
const range: [number, number] = [columnNumber, line.slice(columnNumber - 1).length]
addError(
onError,
lineNumber,
@@ -90,7 +101,7 @@ export const liquidSyntax = {
},
}
function getErrorMessageInfo(message) {
function getErrorMessageInfo(message: string): ErrorMessageInfo {
const [errorDescription, lineString, columnString] = message.split(',')
// There has to be a line number so we'll default to line 1 if the message
// 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 { 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 = [
const TERMINOLOGY_REPLACEMENTS: [string, string][] = [
// Beta variations → public preview (longer phrases first)
['limited public beta', 'public preview'],
['public beta', 'public preview'],
@@ -23,35 +26,51 @@ const TERMINOLOGY_REPLACEMENTS = [
['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
const COMPILED_REGEXES = TERMINOLOGY_REPLACEMENTS.map(([outdatedTerm, replacement]) => ({
regex: new RegExp(`(?<!\\w|-|_)${outdatedTerm.replace(/\s+/g, '\\s+')}(?!\\w|-|_)`, 'gi'),
outdatedTerm,
replacement,
}))
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 {string} line - The line of text to search
* @returns {Array} Array of match objects with start, end, text, replacement, and outdatedTerm
* @param line - The line of text to search
* @returns Array of match objects with start, end, text, replacement, and outdatedTerm
*/
function findOutdatedTerminologyMatches(line) {
const foundMatches = []
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
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),
(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) {
@@ -76,7 +95,7 @@ export const outdatedReleasePhaseTerminology = {
'Outdated release phase terminology should be replaced with current GitHub terminology',
tags: ['terminology', 'consistency', 'release-phases'],
severity: 'error',
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n')
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 { getRange } from '../helpers/utils'
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)
const TABLE_ROW_REGEX = /^\s*\|.*\|\s*$/
// 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
*/
function countColumns(row) {
function countColumns(row: string): number {
// Remove leading and trailing whitespace
const trimmed = row.trim()
@@ -38,7 +41,7 @@ function countColumns(row) {
/**
* Checks if a table row contains only Liquid conditionals
*/
function isLiquidOnlyRow(row) {
function isLiquidOnlyRow(row: string): boolean {
const trimmed = row.trim()
if (!trimmed.includes('|')) return false
@@ -61,7 +64,7 @@ export const tableColumnIntegrity = {
description: 'Tables must have consistent column counts across all rows',
tags: ['tables', 'accessibility', 'formatting'],
severity: 'error',
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data
@@ -69,9 +72,7 @@ export const tableColumnIntegrity = {
const lines = params.lines
let inTable = false
let expectedColumnCount = null
let tableStartLine = null
let headerRow = null
let expectedColumnCount: number | null = null
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
@@ -84,8 +85,6 @@ export const tableColumnIntegrity = {
const nextLine = lines[i + 1]
if (nextLine && TABLE_SEPARATOR_REGEX.test(nextLine)) {
inTable = true
tableStartLine = i + 1
headerRow = line
expectedColumnCount = countColumns(line)
continue
}
@@ -95,8 +94,6 @@ export const tableColumnIntegrity = {
if (inTable && !isTableRow) {
inTable = false
expectedColumnCount = null
tableStartLine = null
headerRow = null
continue
}
@@ -113,10 +110,10 @@ export const tableColumnIntegrity = {
const range = getRange(line, line.trim())
let errorMessage
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.`
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.`
} 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(

View File

@@ -9,10 +9,12 @@ const pages = await loadPageMap(pageList)
const redirects = await loadRedirects(pageList)
const liquidElsif = /{%\s*elsif/
const containsLiquidElseIf = (text) => liquidElsif.test(text)
const containsLiquidElseIf = (text: string) => liquidElsif.test(text)
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} `
if (trouble.length > 0) {
if (trouble.length === 1) {
@@ -20,7 +22,8 @@ describe('front matter', () => {
} else {
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) {
customErrorMessage += `\nindex: ${index} URI: ${uri}`
if (redirects) {
@@ -29,7 +32,8 @@ describe('front matter', () => {
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\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 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)
const customErrorMessage = makeCustomErrorMessage(page, trouble, 'includeGuides')

View File

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

View File

@@ -42,7 +42,9 @@ describe(liquidQuotedConditionalArg.names.join(' - '), () => {
const errors = result.markdown
expect(errors.length).toBe(3)
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 () => {
const markdown = [

View File

@@ -1,4 +1,6 @@
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
@@ -30,16 +32,24 @@ import { visit } from 'unist-util-visit'
* 4. Skipping tables that already have accessibility attributes
*/
function isTableElement(node) {
return node.type === 'element' && node.tagName === 'table'
interface HeadingInfo {
id: string
text: string
}
function isHeadingElement(node) {
return node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName)
function isTableElement(node: Node): node is Element {
return node.type === 'element' && (node as Element).tagName === 'table'
}
function hasExistingAccessibilityAttributes(tableNode) {
function isHeadingElement(node: Node): node is Element {
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.ariaLabel ||
tableNode.properties.ariaLabelledBy ||
@@ -48,13 +58,13 @@ function hasExistingAccessibilityAttributes(tableNode) {
)
}
function hasExistingCaption(tableNode) {
function hasExistingCaption(tableNode: Element): boolean {
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
// Look backwards from the table position for the nearest heading
@@ -66,7 +76,7 @@ function findPrecedingHeading(parent, tableIndex) {
const headingId = node.properties?.id
if (headingId) {
return {
id: headingId,
id: headingId as string,
text: extractTextFromNode(node),
}
}
@@ -75,7 +85,7 @@ function findPrecedingHeading(parent, tableIndex) {
// Stop searching if we hit another table or significant content block
if (
isTableElement(node) ||
(node.type === 'element' && ['section', 'article', 'div'].includes(node.tagName))
(node.type === 'element' && ['section', 'article', 'div'].includes((node as Element).tagName))
) {
break
}
@@ -84,13 +94,13 @@ function findPrecedingHeading(parent, tableIndex) {
return null
}
function extractTextFromNode(node) {
function extractTextFromNode(node: Node): string {
if (node.type === 'text') {
return node.value
return (node as any).value
}
if (node.type === 'element' && node.children) {
return node.children
if (node.type === 'element' && (node as Element).children) {
return (node as Element).children
.map((child) => extractTextFromNode(child))
.filter(Boolean)
.join('')
@@ -101,8 +111,8 @@ function extractTextFromNode(node) {
}
export default function addTableAccessibilityLabels() {
return (tree) => {
visit(tree, (node, index, parent) => {
return (tree: Node) => {
visit(tree, (node: Node, index: number | undefined, parent: Parent | undefined) => {
if (!isTableElement(node) || !parent || typeof index !== 'number') {
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'
// 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'
interface GetRemoteJSONConfig {
retry?: {
limit?: number
}
timeout?: {
response?: number
}
}
// Wrapper on `got()` that is able to both cache in memory and on disk.
// The on-disk caching is in `.remotejson/`.
// 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?
// 2. No, is it on 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
// 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()`.
@@ -59,7 +75,14 @@ export default async function getRemoteJSON(url, config) {
}
}
} 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
}
}