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 { 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)
|
||||||
@@ -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
|
||||||
@@ -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+.*%}$/,
|
||||||
@@ -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'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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(
|
||||||
@@ -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')
|
||||||
@@ -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'
|
||||||
@@ -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 = [
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user