Convert 21 JavaScript files to TypeScript (#57896)
This commit is contained in:
@@ -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)
|
||||
@@ -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
|
||||
@@ -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+.*%}$/,
|
||||
@@ -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'),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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')
|
||||
@@ -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'
|
||||
@@ -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 = [
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user