requirements.txt, pipfile.lock'),
).toBeTruthy()
@@ -95,7 +95,7 @@ describe('renderContent', () => {
| user:USERNAME | [**user:defunkt ubuntu**](https://github.com/search?q=user%3Adefunkt+ubuntu&type=Issues) matches issues with the word "ubuntu" from repositories owned by @defunkt.
`)
const html = await renderContent(template)
- const $ = cheerio.load(html, { xmlMode: true })
+ const $ = load(html, { xmlMode: true })
expect($.html().includes('user:USERNAME')).toBeTruthy()
})
@@ -110,7 +110,7 @@ describe('renderContent', () => {
1. This is another list item.
`)
const html = await renderContent(template)
- const $ = cheerio.load(html, { xmlMode: true })
+ const $ = load(html, { xmlMode: true })
expect($('ol').length).toBe(1)
expect($.html().includes(' {
##### This is a level 5
`)
const html = await renderContent(template)
- const $ = cheerio.load(html, { xmlMode: true })
+ const $ = load(html, { xmlMode: true })
for (const level of [1, 2, 3, 4, 5]) {
expect(
@@ -147,7 +147,7 @@ const example = true
\`\`\`\`
`)
let html = await renderContent(template)
- let $ = cheerio.load(html, { xmlMode: true })
+ let $ = load(html, { xmlMode: true })
expect($.html().includes('')).toBeTruthy()
expect($.html().includes('const')).toBeTruthy()
@@ -157,7 +157,7 @@ const example = true
\`\`\`\`
`)
html = await renderContent(template)
- $ = cheerio.load(html, { xmlMode: true })
+ $ = load(html, { xmlMode: true })
expect($.html().includes('')).toBeTruthy()
expect($.html().includes('@articles')).toBeTruthy()
@@ -167,7 +167,7 @@ POST / HTTP/2
\`\`\`\`
`)
html = await renderContent(template)
- $ = cheerio.load(html, { xmlMode: true })
+ $ = load(html, { xmlMode: true })
expect($.html().includes('')).toBeTruthy()
expect($.html().includes('POST')).toBeTruthy()
@@ -180,7 +180,7 @@ plugins {
\`\`\`\`
`)
html = await renderContent(template)
- $ = cheerio.load(html, { xmlMode: true })
+ $ = load(html, { xmlMode: true })
expect($.html().includes('')).toBeTruthy()
expect(
$.html().includes(''maven-publish''),
@@ -192,7 +192,7 @@ FROM alpine:3.10
\`\`\`\`
`)
html = await renderContent(template)
- $ = cheerio.load(html, { xmlMode: true })
+ $ = load(html, { xmlMode: true })
expect($.html().includes('')).toBeTruthy()
expect($.html().includes('FROM')).toBeTruthy()
@@ -202,7 +202,7 @@ $resourceGroupName = "octocat-testgroup"
\`\`\`\`
`)
html = await renderContent(template)
- $ = cheerio.load(html, { xmlMode: true })
+ $ = load(html, { xmlMode: true })
expect($.html().includes('')).toBeTruthy()
expect(
$.html().includes('$resourceGroupName'),
@@ -216,7 +216,7 @@ var a = 1
\`\`\`
`)
const html = await renderContent(template)
- const $ = cheerio.load(html, { xmlMode: true })
+ const $ = load(html, { xmlMode: true })
expect($.html().includes('var a = 1')).toBeTruthy()
})
@@ -238,7 +238,7 @@ var a = 1
\`\`\`
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const el = $('button.js-btn-copy')
expect(el.data('clipboard')).toBe(2967273189)
// Generates a murmurhash based ID that matches a
@@ -250,7 +250,7 @@ var a = 1
> This is a note with a [link](https://example.com)
`)
const html = await renderContent(template, { alertTitles: { NOTE: 'Note' } })
- const $ = cheerio.load(html)
+ const $ = load(html)
const alertEl = $('.ghd-alert')
expect(alertEl.length).toBe(1)
expect(alertEl.attr('data-container')).toBe('alert')
diff --git a/src/content-render/tests/table-accessibility-labels.ts b/src/content-render/tests/table-accessibility-labels.ts
index 84dd119f4c8f..e17e246cf096 100644
--- a/src/content-render/tests/table-accessibility-labels.ts
+++ b/src/content-render/tests/table-accessibility-labels.ts
@@ -1,4 +1,4 @@
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
import { describe, expect, test } from 'vitest'
import { renderContent } from '@/content-render/index'
@@ -20,7 +20,7 @@ describe('table accessibility labels', () => {
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const table = $('table')
expect(table.length).toBe(1)
@@ -42,7 +42,7 @@ describe('table accessibility labels', () => {
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const table = $('table')
expect(table.attr('aria-labelledby')).toBe('configuration-options')
@@ -59,7 +59,7 @@ describe('table accessibility labels', () => {
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const table = $('table')
expect(table.attr('aria-label')).toBe('Pre-labeled table')
@@ -78,7 +78,7 @@ describe('table accessibility labels', () => {
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const table = $('table')
expect(table.find('caption').text()).toBe('Existing caption')
@@ -101,7 +101,7 @@ describe('table accessibility labels', () => {
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const tables = $('table')
expect(tables.length).toBe(2)
@@ -123,7 +123,7 @@ Some text here.
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const tables = $('table')
expect(tables.length).toBe(2)
@@ -145,7 +145,7 @@ Some additional context here.
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const table = $('table')
expect(table.attr('aria-labelledby')).toBe('data-table')
@@ -165,7 +165,7 @@ Some additional context here.
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const tables = $('table')
expect(tables.length).toBe(2)
@@ -185,7 +185,7 @@ Some additional context here.
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const table = $('table')
expect(table.attr('aria-labelledby')).toBe('supported-github-actions-features')
@@ -201,7 +201,7 @@ Some additional context here.
`)
const html = await renderContent(template)
- const $ = cheerio.load(html)
+ const $ = load(html)
const table = $('table')
expect(table.find('thead th').length).toBe(2)
diff --git a/src/content-render/unified/alerts.ts b/src/content-render/unified/alerts.ts
index d354a774736d..61dc11d9961d 100644
--- a/src/content-render/unified/alerts.ts
+++ b/src/content-render/unified/alerts.ts
@@ -5,7 +5,7 @@ Custom "Alerts", based on similar filter/styling in the monolith code.
import { visit } from 'unist-util-visit'
import { h } from 'hastscript'
import octicons from '@primer/octicons'
-import type { Element } from 'hast'
+import type { Element, Root, ElementContent } from 'hast'
interface AlertType {
icon: string
@@ -22,36 +22,33 @@ const alertTypes: Record = {
// Must contain one of [!NOTE], [!IMPORTANT], ...
const ALERT_REGEXP = new RegExp(`\\[!(${Object.keys(alertTypes).join('|')})\\]`, 'gi')
-
-// Using any due to conflicting unist/hast type definitions between dependencies
-const matcher = (node: any): boolean =>
- node.type === 'element' &&
- node.tagName === 'blockquote' &&
- ALERT_REGEXP.test(JSON.stringify(node.children))
+// Non-global version for .test() and .match() to avoid stateful lastIndex issues
+const ALERT_REGEXP_DETECT = new RegExp(`\\[!(${Object.keys(alertTypes).join('|')})\\]`, 'i')
export default function alerts({ alertTitles = {} }: { alertTitles?: Record }) {
- // Using any due to conflicting unist/hast type definitions between dependencies
- return (tree: any) => {
- // Using any due to conflicting unist/hast type definitions between dependencies
- visit(tree, matcher, (node: any) => {
- const key = getAlertKey(node)
+ return (tree: Root) => {
+ visit(tree, 'element', (node) => {
+ const el = node as Element
+ if (el.tagName !== 'blockquote' || !ALERT_REGEXP_DETECT.test(JSON.stringify(el.children)))
+ return
+ const key = getAlertKey(el)
if (!(key in alertTypes)) {
console.warn(
`Alert key '${key}' should be all uppercase (change it to '${key.toUpperCase()}')`,
)
}
- const alertType = alertTypes[getAlertKey(node).toUpperCase()]
- node.tagName = 'div'
- node.properties.className = `ghd-alert ghd-alert-${alertType.color}`
- node.properties.dataContainer = 'alert'
- node.children = [
+ const alertType = alertTypes[getAlertKey(el).toUpperCase()]
+ el.tagName = 'div'
+ el.properties.className = `ghd-alert ghd-alert-${alertType.color}`
+ el.properties.dataContainer = 'alert'
+ el.children = [
h(
'p',
{ className: 'ghd-alert-title' },
getOcticonSVG(alertType.icon),
alertTitles[key] || '',
),
- ...removeAlertSyntax(node.children),
+ ...removeAlertSyntax(el.children),
]
})
}
@@ -59,19 +56,22 @@ export default function alerts({ alertTitles = {} }: { alertTitles?: Record removeAlertSyntax(n))
}
- if (node.children) {
- node.children = node.children.map(removeAlertSyntax)
+ if ('children' in node) {
+ node.children = node.children.map((n) => removeAlertSyntax(n)) as typeof node.children
}
- if (node.value) {
+ if ('value' in node) {
node.value = node.value.replace(ALERT_REGEXP, '')
}
return node
diff --git a/src/content-render/unified/code-header.ts b/src/content-render/unified/code-header.ts
index f9e0c13743e3..a2ef46ff2f37 100644
--- a/src/content-render/unified/code-header.ts
+++ b/src/content-render/unified/code-header.ts
@@ -13,44 +13,40 @@ import { fromParse5 } from 'hast-util-from-parse5'
import murmur from 'imurmurhash'
import { getPrompt } from './copilot-prompt'
import { generatePromptId } from '../lib/prompt-id'
-import type { Element } from 'hast'
+import type { Element, Root } from 'hast'
interface LanguageConfig {
name: string
- // Using any for language properties that can vary (aliases, extensions, etc.)
- [key: string]: any
+ [key: string]: string | string[] | boolean | undefined
}
type Languages = Record
const languages = yaml.load(fs.readFileSync('./data/code-languages.yml', 'utf8')) as Languages
-// Using any due to conflicting unist/hast type definitions between dependencies
-const matcher = (node: any): boolean =>
- node.type === 'element' &&
- node.tagName === 'pre' &&
- // For now, limit to ones with the copy or prompt meta,
- // but we may enable for all examples later.
- (getPreMeta(node).copy || getPreMeta(node).prompt) &&
- // Don't add this header for annotated examples.
- !getPreMeta(node).annotate
-
export default function codeHeader() {
- // Using any due to conflicting unist/hast type definitions between dependencies
- return (tree: any) => {
- // Using any due to conflicting unist/hast type definitions between dependencies
- visit(tree, matcher, (node: any, index: number | undefined, parent: any) => {
- if (index !== undefined && parent) {
- parent.children[index] = wrapCodeExample(node, tree)
+ return (tree: Root) => {
+ visit(tree, 'element', (node, index, parent) => {
+ const el = node as Element
+ if (
+ el.tagName !== 'pre' ||
+ !(getPreMeta(el).copy || getPreMeta(el).prompt) ||
+ getPreMeta(el).annotate
+ )
+ return
+ if (index !== undefined && parent && 'children' in parent) {
+ ;(parent as Element).children[index] = wrapCodeExample(el, tree)
}
})
}
}
-// Using any due to conflicting unist/hast type definitions between dependencies
-function wrapCodeExample(node: any, tree: any): Element {
- const lang: string = node.children[0].properties.className?.[0].replace('language-', '')
- const code: string = node.children[0].children[0].value
+function wrapCodeExample(node: Element, tree: Root): Element {
+ const codeChild = node.children[0] as Element
+ const classNames = codeChild.properties.className as string[] | undefined
+ const lang: string = classNames?.[0]?.replace('language-', '') ?? ''
+ const textNode = codeChild.children[0] as { value: string }
+ const code: string = textNode.value
const subnav = null // getSubnav() lives in annotate.ts, not needed for normal code blocks
const hasPrompt: boolean = Boolean(getPreMeta(node).prompt)
@@ -120,16 +116,14 @@ export function header(
function btnIcon(): Element {
const btnIconHtml: string = octicons.copy.toSVG()
const btnIconAst = parse(String(btnIconHtml), { sourceCodeLocationInfo: true })
- // Using any because fromParse5 expects VFile but we only have a string
- // This is safe because parse5 only needs the string content
- const btnIconElement = fromParse5(btnIconAst, { file: btnIconHtml as any })
+ const btnIconElement = fromParse5(btnIconAst)
return btnIconElement as Element
}
-// Using any due to conflicting unist/hast type definitions between dependencies
-// node can be various mdast/hast node types, return value contains meta properties from code blocks
-export function getPreMeta(node: any): Record {
+// node can be various hast element types, return value contains meta properties from code blocks
+export function getPreMeta(node: Element): Record {
// Here's why this monstrosity works:
// https://github.com/syntax-tree/mdast-util-to-hast/blob/c87cd606731c88a27dbce4bfeaab913a9589bf83/lib/handlers/code.js#L40-L42
- return node.children[0]?.data?.meta || {}
+ const firstChild = node.children[0] as Element | undefined
+ return (firstChild?.data as Record> | undefined)?.meta || {}
}
diff --git a/src/content-render/unified/copilot-prompt.ts b/src/content-render/unified/copilot-prompt.ts
index 19d03cbf57e5..36a2554a87b8 100644
--- a/src/content-render/unified/copilot-prompt.ts
+++ b/src/content-render/unified/copilot-prompt.ts
@@ -9,14 +9,13 @@ import { parse } from 'parse5'
import { fromParse5 } from 'hast-util-from-parse5'
import { getPreMeta } from './code-header'
import { generatePromptId } from '../lib/prompt-id'
+import type { Element, Root } from 'hast'
-// node and tree are hast/unist AST nodes without proper TypeScript definitions
-// Returns an object with the prompt button element and the full prompt content
export function getPrompt(
- node: any,
- tree: any,
+ node: Element,
+ tree: Root,
code: string,
-): { element: any; promptContent: string } | null {
+): { element: Element; promptContent: string } | null {
const hasPrompt = Boolean(getPreMeta(node).prompt)
if (!hasPrompt) return null
@@ -40,11 +39,9 @@ export function getPrompt(
return { element, promptContent }
}
-// Using any because node and tree are hast/unist AST nodes without proper TypeScript definitions
-// node is the current code block element, tree is used to find referenced code blocks
function buildPromptData(
- node: any,
- tree: any,
+ node: Element,
+ tree: Root,
code: string,
): { promptContent: string; ariaLabel: string } {
// Find a ref meta in the format 'ref='
@@ -56,14 +53,18 @@ function buildPromptData(
}
// If the 'ref=' meta is found, find a matching code block to include as context in the prompt link.
- const matchingCodeEl = findMatchingCode(ref, tree)
+ const matchingCodeEl = findMatchingCode(ref as string, tree)
if (!matchingCodeEl) {
console.warn(`Can't find referenced code block with id=${ref}`)
return promptOnly(code)
}
- // Using any to access children property on untyped hast element node
// AST structure: element -> code -> text node with value property
- const matchingCode = (matchingCodeEl as any)?.children[0].children[0].value || null
+ const codeChild = matchingCodeEl.children[0] as Element | undefined
+ const textNode = codeChild?.children[0] as { value?: string } | undefined
+ const matchingCode = textNode?.value || null
+ if (!matchingCode) {
+ return promptOnly(code)
+ }
return promptAndContext(code, matchingCode)
}
@@ -84,21 +85,17 @@ function promptAndContext(
}
}
-// Using any because tree and node are hast/unist AST nodes without proper TypeScript definitions
-// Searches the AST tree for a code block with matching id in meta
-function findMatchingCode(ref: string, tree: any): any {
- return find(tree, (node: any) => {
- // Using any to access tagName property on untyped hast element node
- return node.type === 'element' && (node as any).tagName === 'pre' && getPreMeta(node).id === ref
- })
+function findMatchingCode(ref: string, tree: Root): Element | undefined {
+ return find(tree, ((node: { type: string; tagName?: string }) => {
+ return (
+ node.type === 'element' && node.tagName === 'pre' && getPreMeta(node as Element).id === ref
+ )
+ }) as Parameters[1])
}
-// Returns a hast element node for the Copilot icon
-// Using any return type because fromParse5 returns untyped hast nodes
-function copilotIcon(): any {
+function copilotIcon(): Element {
const copilotIconHtml = octicons.copilot.toSVG()
const copilotIconAst = parse(String(copilotIconHtml), { sourceCodeLocationInfo: true })
- // Using any because fromParse5 expects VFile but we only have a string
- const copilotIconElement = fromParse5(copilotIconAst, { file: copilotIconHtml as any })
- return copilotIconElement
+ const copilotIconElement = fromParse5(copilotIconAst)
+ return copilotIconElement as Element
}
diff --git a/src/content-render/unified/rewrite-empty-table-rows.ts b/src/content-render/unified/rewrite-empty-table-rows.ts
index d10e95e875c5..997fbecb963d 100644
--- a/src/content-render/unified/rewrite-empty-table-rows.ts
+++ b/src/content-render/unified/rewrite-empty-table-rows.ts
@@ -1,4 +1,5 @@
import { visit, SKIP } from 'unist-util-visit'
+import type { Element, Root } from 'hast'
/**
* Where it can mutate the AST to swap from:
@@ -49,31 +50,23 @@ import { visit, SKIP } from 'unist-util-visit'
* isn't the same all the way down. But Unified will still parse it.
* */
-// node is a hast element node without proper TypeScript definitions
-function matcher(node: any): boolean {
- return node.type === 'element' && node.tagName === 'tr'
-}
-
-// node, parent, and grandChild are hast element nodes without proper TypeScript definitions
-function visitor(
- node: any,
- index: number | undefined,
- parent: any,
-): [typeof SKIP, number] | undefined {
- if (
- node.children.every(
- (grandChild: any) =>
- grandChild.type === 'element' && grandChild.tagName === 'td' && !grandChild.children.length,
- )
- ) {
- if (index !== undefined) {
- parent.children.splice(index, 1)
- return [SKIP, index]
- }
- }
-}
-
-// tree is a hast root node without proper TypeScript definitions
export default function rewriteEmptyTableRows() {
- return (tree: any) => visit(tree, matcher, visitor)
+ return (tree: Root) =>
+ visit(tree, 'element', (node, index, parent) => {
+ const el = node as Element
+ if (el.tagName !== 'tr') return
+ if (
+ el.children.every(
+ (grandChild) =>
+ grandChild.type === 'element' &&
+ grandChild.tagName === 'td' &&
+ !grandChild.children.length,
+ )
+ ) {
+ if (index !== undefined && parent) {
+ parent.children.splice(index, 1)
+ return [SKIP, index] as const
+ }
+ }
+ })
}
diff --git a/src/content-render/unified/rewrite-for-rowheaders.ts b/src/content-render/unified/rewrite-for-rowheaders.ts
index f86e1d7162dc..c547e8c5c801 100644
--- a/src/content-render/unified/rewrite-for-rowheaders.ts
+++ b/src/content-render/unified/rewrite-for-rowheaders.ts
@@ -1,21 +1,10 @@
import { visitParents } from 'unist-util-visit-parents'
+import type { Element, Root } from 'hast'
-interface ElementNode {
- type: 'element'
- tagName: string
- properties: {
- // Properties can have any value type (strings, booleans, arrays, etc.)
- [key: string]: any
- }
+interface ScopedElement extends Element {
_scoped?: boolean
}
-interface AncestorNode {
- properties?: {
- className?: string[]
- }
-}
-
/**
* Where it can mutate the AST to swap from:
*
@@ -49,32 +38,28 @@ interface AncestorNode {
*
* */
-function matcher(node: any): node is ElementNode {
- return node.type === 'element' && node.tagName === 'td' && !('scope' in node.properties)
-}
-
-function insideRowheaders(ancestors: AncestorNode[]): boolean {
- return ancestors.some(
- (node: AncestorNode) =>
- node.properties &&
- node.properties.className &&
- node.properties.className.includes('rowheaders'),
- )
-}
+export default function rewriteForRowheaders() {
+ return (tree: Root) =>
+ visitParents(tree, 'element', (node, ancestors) => {
+ const el = node as Element
+ if (el.tagName !== 'td' || 'scope' in el.properties) return
-// ancestors is an array of hast nodes without proper TypeScript definitions
-function visitor(node: ElementNode, ancestors: any[]): void {
- if (insideRowheaders(ancestors)) {
- const tr = ancestors.at(-1) as ElementNode
- if (!tr._scoped) {
- tr._scoped = true
- node.properties.scope = 'row'
- node.tagName = 'th'
- }
- }
-}
+ const insideRowheaders = ancestors.some((ancestor) => {
+ const ancestorEl = ancestor as Partial
+ return (
+ ancestorEl.properties &&
+ Array.isArray(ancestorEl.properties.className) &&
+ ancestorEl.properties.className.includes('rowheaders')
+ )
+ })
-// tree is a hast root node without proper TypeScript definitions
-export default function rewriteForRowheaders() {
- return (tree: any) => visitParents(tree, matcher, visitor)
+ if (insideRowheaders) {
+ const tr = ancestors.at(-1) as ScopedElement
+ if (!tr._scoped) {
+ tr._scoped = true
+ el.properties.scope = 'row'
+ el.tagName = 'th'
+ }
+ }
+ })
}
diff --git a/src/content-render/unified/text-only.ts b/src/content-render/unified/text-only.ts
index 6488b1f90593..83ce950a5b0a 100644
--- a/src/content-render/unified/text-only.ts
+++ b/src/content-render/unified/text-only.ts
@@ -1,4 +1,4 @@
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
import { decode } from 'html-entities'
// Given a piece of HTML return it without HTML. E.g.
@@ -13,6 +13,6 @@ export function fastTextOnly(html: string): string {
const middle = html.slice(3, -4)
if (!middle.includes('<')) return decode(middle.trim())
}
- const $ = cheerio.load(html, { xmlMode: true })
+ const $ = load(html, { xmlMode: true })
return $.root().text().trim()
}
diff --git a/src/data-directory/lib/data-schemas/features.ts b/src/data-directory/lib/data-schemas/features.ts
index 8cf101ff16b1..1b9aa310350b 100644
--- a/src/data-directory/lib/data-schemas/features.ts
+++ b/src/data-directory/lib/data-schemas/features.ts
@@ -1,9 +1,16 @@
import { schema } from '@/frame/lib/frontmatter'
+interface FeatureVersionsProperties {
+ type?: string | string[]
+ properties?: Record
+ additionalProperties?: boolean
+ [key: string]: unknown
+}
+
interface FeatureVersionsSchema {
type: 'object'
properties: {
- versions: any
+ versions: FeatureVersionsProperties
}
additionalProperties: false
}
@@ -12,13 +19,14 @@ interface FeatureVersionsSchema {
const featureVersions: FeatureVersionsSchema = {
type: 'object',
properties: {
- versions: Object.assign({}, (schema.properties as any).versions),
+ versions: Object.assign({}, schema.properties.versions) as FeatureVersionsProperties,
},
additionalProperties: false,
}
// Remove the feature versions properties.
// We don't want to allow features within features! We just want pure versioning.
-delete featureVersions.properties.versions.properties.feature
+delete (featureVersions.properties.versions.properties as Record | undefined)
+ ?.feature
export default featureVersions
diff --git a/src/dev-toc/generate.ts b/src/dev-toc/generate.ts
index f836d8afa282..5bc80594a1ce 100644
--- a/src/dev-toc/generate.ts
+++ b/src/dev-toc/generate.ts
@@ -15,7 +15,7 @@ import path from 'path'
import { execSync } from 'child_process'
import { program } from 'commander'
import type { NextFunction, Response } from 'express'
-import type { ExtendedRequest } from '@/types'
+import type { ExtendedRequest, Context } from '@/types'
import fpt from '@/versions/lib/non-enterprise-default-version'
import { allVersionKeys } from '@/versions/lib/all-versions'
import { liquid } from '@/content-render/index'
@@ -60,10 +60,16 @@ async function main(): Promise {
get: () => '',
header: () => '',
accepts: () => false,
- context: {} as any,
+ context: {} as Context,
} as unknown as ExtendedRequest
- async function recurse(tree: any): Promise {
+ interface PageTreeNode {
+ page: { rawTitle: string }
+ renderedFullTitle?: string
+ childPages?: PageTreeNode[]
+ }
+
+ async function recurse(tree: PageTreeNode): Promise {
const { page } = tree
tree.renderedFullTitle = page.rawTitle.includes('{')
? await liquid.parseAndRender(page.rawTitle, req.context)
@@ -92,7 +98,7 @@ async function main(): Promise {
}
if (req.context && req.context.currentEnglishTree) {
- await recurse(req.context.currentEnglishTree)
+ await recurse(req.context.currentEnglishTree as PageTreeNode)
}
// Add any defaultOpenSections to the context.
diff --git a/src/fixtures/tests/annotations.ts b/src/fixtures/tests/annotations.ts
index 9a87ca2401ac..87f35190137e 100644
--- a/src/fixtures/tests/annotations.ts
+++ b/src/fixtures/tests/annotations.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest'
describe('annotations', () => {
test('code-snippet-with-hashbang', async () => {
- const $: cheerio.Root = await getDOM('/get-started/foo/code-snippet-with-hashbang')
+ const $: CheerioAPI = await getDOM('/get-started/foo/code-snippet-with-hashbang')
const annotations = $('#article-contents .annotate')
// Check http://localhost:4000/en/get-started/foo/code-snippet-with-hashbang
diff --git a/src/fixtures/tests/breadcrumbs.ts b/src/fixtures/tests/breadcrumbs.ts
index 66eb05784a80..9356ec762215 100644
--- a/src/fixtures/tests/breadcrumbs.ts
+++ b/src/fixtures/tests/breadcrumbs.ts
@@ -1,5 +1,7 @@
import { describe, expect, test } from 'vitest'
+import type { Element } from 'domhandler'
+
import { getDOM } from '@/tests/helpers/e2etest'
describe('breadcrumbs', () => {
@@ -68,7 +70,7 @@ describe('breadcrumbs', () => {
expect($breadcrumbTitles.length).toBe(0)
expect($breadcrumbLinks.length).toBe(2)
- expect(($breadcrumbLinks[0] as cheerio.TagElement).attribs.title).toBe('Deeper secrets')
- expect(($breadcrumbLinks[1] as cheerio.TagElement).attribs.title).toBe('Mariana Trench')
+ expect(($breadcrumbLinks[0] as Element).attribs.title).toBe('Deeper secrets')
+ expect(($breadcrumbLinks[1] as Element).attribs.title).toBe('Mariana Trench')
})
})
diff --git a/src/fixtures/tests/categories-and-subcategory.ts b/src/fixtures/tests/categories-and-subcategory.ts
index 3d16eb951a6a..01fb0a629124 100644
--- a/src/fixtures/tests/categories-and-subcategory.ts
+++ b/src/fixtures/tests/categories-and-subcategory.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM, head } from '@/tests/helpers/e2etest'
describe('subcategories', () => {
test('get-started/start-your-journey subcategory', async () => {
- const $: cheerio.Root = await getDOM('/get-started/start-your-journey')
+ const $: CheerioAPI = await getDOM('/get-started/start-your-journey')
const lead = $('[data-search=lead]').text()
expect(lead).toMatch('Get started using HubGit to manage Git repositories')
@@ -22,7 +22,7 @@ describe('subcategories', () => {
})
test('actions/category/subcategory subcategory has its articles intro', async () => {
- const $: cheerio.Root = await getDOM('/actions/category/subcategory')
+ const $: CheerioAPI = await getDOM('/actions/category/subcategory')
const lead = $('[data-search=lead]').text()
expect(lead).toMatch("Here's the intro for HubGit Actions.")
@@ -43,7 +43,7 @@ describe('subcategories', () => {
describe('categories', () => {
test('actions/category subcategory', async () => {
- const $: cheerio.Root = await getDOM('/actions/category')
+ const $: CheerioAPI = await getDOM('/actions/category')
const lead = $('[data-search=lead]').text()
expect(lead).toMatch('Learn how to migrate your existing CI/CD')
diff --git a/src/fixtures/tests/footer.ts b/src/fixtures/tests/footer.ts
index 3fb6e8d48344..cfb61ee35559 100644
--- a/src/fixtures/tests/footer.ts
+++ b/src/fixtures/tests/footer.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version'
@@ -7,7 +7,7 @@ import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-v
describe('footer', () => {
describe('"contact us" link', () => {
test('leads to support from articles', async () => {
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
`/en/${nonEnterpriseDefaultVersion}/get-started/start-your-journey/hello-world`,
)
expect($('a#support').attr('href')).toBe('https://support.github.com')
@@ -16,21 +16,21 @@ describe('footer', () => {
test('leads to support on 404 pages', async () => {
// Important to use the prefix /en/ on the failing URL or else
// it will render a very basic plain text 404 response.
- const $: cheerio.Root = await getDOM('/en/delicious-snacks/donuts.php', { allow404: true })
+ const $: CheerioAPI = await getDOM('/en/delicious-snacks/donuts.php', { allow404: true })
expect($('a#support').attr('href')).toBe('https://support.github.com')
})
})
describe('"support" link with nextjs', () => {
test('leads to support from articles', async () => {
- const $: cheerio.Root = await getDOM(`/en/${nonEnterpriseDefaultVersion}/get-started?nextjs=`)
+ const $: CheerioAPI = await getDOM(`/en/${nonEnterpriseDefaultVersion}/get-started?nextjs=`)
expect($('a#support').attr('href')).toBe('https://support.github.com')
})
})
describe('test redirects for product landing community links pages', () => {
test('codespaces product landing page leads to discussions page', async () => {
- const $: cheerio.Root = await getDOM('/en/get-started')
+ const $: CheerioAPI = await getDOM('/en/get-started')
expect($('a#ask-community').attr('href')).toBe(
'https://hubgit.com/orgs/community/discussions/categories/get-started',
)
@@ -39,7 +39,7 @@ describe('footer', () => {
describe('test redirects for non-product landing community links pages', () => {
test('leads to https://github.community/ when clicking on the community link', async () => {
- const $: cheerio.Root = await getDOM(`/en/get-started/start-your-journey/hello-world`)
+ const $: CheerioAPI = await getDOM(`/en/get-started/start-your-journey/hello-world`)
expect($('a#ask-community').attr('href')).toBe(
'https://github.com/orgs/community/discussions',
)
diff --git a/src/fixtures/tests/glossary.ts b/src/fixtures/tests/glossary.ts
index 70b314bc7dca..e214b7927aa4 100644
--- a/src/fixtures/tests/glossary.ts
+++ b/src/fixtures/tests/glossary.ts
@@ -1,20 +1,20 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOMCached as getDOM } from '@/tests/helpers/e2etest'
describe('glossary', () => {
test('headings are sorted alphabetically', async () => {
- const $: cheerio.Root = await getDOM('/get-started/learning-about-github/github-glossary')
+ const $: CheerioAPI = await getDOM('/get-started/learning-about-github/github-glossary')
const headings = $('#article-contents h2')
- const headingTexts = headings.map((_: number, el: any) => $(el).text()).get()
+ const headingTexts = headings.map((_, el) => $(el).text()).get()
const cloned = [...headingTexts].sort((a: string, b: string) => a.localeCompare(b))
const equalStringArray = (a: string[], b: string[]) =>
a.length === b.length && a.every((v, i) => v === b[i])
expect(equalStringArray(headingTexts, cloned)).toBe(true)
})
test('Markdown links are correct', async () => {
- const $: cheerio.Root = await getDOM('/get-started/learning-about-github/github-glossary')
+ const $: CheerioAPI = await getDOM('/get-started/learning-about-github/github-glossary')
const internalLink = $('#article-contents a[href="/en/get-started/foo"]')
expect(internalLink.length).toBe(1)
// That link used AUTOTITLE so it should be "expanded"
@@ -22,19 +22,19 @@ describe('glossary', () => {
})
test('all Liquid is evaluated', async () => {
- const $: cheerio.Root = await getDOM('/get-started/learning-about-github/github-glossary')
+ const $: CheerioAPI = await getDOM('/get-started/learning-about-github/github-glossary')
const paragraphs = $('#article-contents p')
- const paragraphTexts = paragraphs.map((_: number, el: any) => $(el).text()).get()
+ const paragraphTexts = paragraphs.map((_, el) => $(el).text()).get()
expect(paragraphTexts.find((text: string) => text.includes('{%'))).toBe(undefined)
})
test('liquid in one of the description depends on version', async () => {
// fpt
{
- const $: cheerio.Root = await getDOM('/get-started/learning-about-github/github-glossary')
+ const $: CheerioAPI = await getDOM('/get-started/learning-about-github/github-glossary')
const paragraphs = $('#article-contents p')
const paragraphTexts = paragraphs
- .map((_: number, el: any) => $(el).text())
+ .map((_, el) => $(el).text())
.get()
.join('\n')
expect(paragraphTexts).toContain('status check on HubGit.')
@@ -42,12 +42,12 @@ describe('glossary', () => {
// ghes
{
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
'/enterprise-server@latest/get-started/learning-about-github/github-glossary',
)
const paragraphs = $('#article-contents p')
const paragraphTexts = paragraphs
- .map((_: number, el: any) => $(el).text())
+ .map((_, el) => $(el).text())
.get()
.join('\n')
expect(paragraphTexts).toContain('status check on HubGit Enterprise Server.')
diff --git a/src/fixtures/tests/guides.ts b/src/fixtures/tests/guides.ts
index 1e1663303601..ff4adb7d231d 100644
--- a/src/fixtures/tests/guides.ts
+++ b/src/fixtures/tests/guides.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { get, getDOMCached as getDOM } from '@/tests/helpers/e2etest'
describe('guides', () => {
test("page's title should be document title", async () => {
- const $: cheerio.Root = await getDOM('/code-security/guides')
+ const $: CheerioAPI = await getDOM('/code-security/guides')
// This is what you'd find in src/fixtures/fixtures/content/code-security/guides.md
const title = 'Guides for cool security'
expect($('title').text()).toMatch(title)
@@ -19,7 +19,7 @@ describe('guides', () => {
describe('learning tracks', () => {
test('start the first learning track and come back via the navigation banner', async () => {
- const $: cheerio.Root = await getDOM('/code-security/guides')
+ const $: CheerioAPI = await getDOM('/code-security/guides')
const links = $('[data-testid=learning-track] a')
const link = links
.filter((_: number, el: any) => $(el).text() === 'Start learning path')
@@ -28,7 +28,7 @@ describe('learning tracks', () => {
expect(link.attr('href')).toMatch('learnProduct=code-security')
// Go the first "Start learning path" link
- const $2: cheerio.Root = await getDOM(link.attr('href')!)
+ const $2: CheerioAPI = await getDOM(link.attr('href')!)
const card2 = $2('[data-testid=learning-track-card]')
// The card has 2 links. One to go back to the guide page
// whose title is the name of the learning track
@@ -57,7 +57,7 @@ describe('learning tracks', () => {
expect(navNextLink.text()).toBe('Securing your organization')
// Go to the next (last) page
- const $3: cheerio.Root = await getDOM(nextLink.attr('href')!)
+ const $3: CheerioAPI = await getDOM(nextLink.attr('href')!)
const card3 = $3('[data-testid=learning-track-card]')
const span3 = card3.find('span').filter((_: number, el: any) => $(el).text().includes('2 of 2'))
expect(span3.text()).toBe('2 of 2 in learning path')
@@ -82,15 +82,13 @@ describe('learning tracks', () => {
test('with and without a valid ?learn= query string', async () => {
// Valid
{
- const $: cheerio.Root = await getDOM(
- '/code-security/getting-started/quickstart?learn=foo_bar',
- )
+ const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart?learn=foo_bar')
expect($('[data-testid=learning-track-card]').length).toBe(1)
expect($('[data-testid=learning-track-nav]').length).toBe(1)
}
// Invalid
{
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
'/code-security/getting-started/quickstart?learn=blablainvalid',
)
expect($('[data-testid=learning-track-card]').length).toBe(0)
@@ -98,13 +96,13 @@ describe('learning tracks', () => {
}
// Empty
{
- const $: cheerio.Root = await getDOM('/code-security/getting-started/quickstart?learn=')
+ const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart?learn=')
expect($('[data-testid=learning-track-card]').length).toBe(0)
expect($('[data-testid=learning-track-nav]').length).toBe(0)
}
// Missing
{
- const $: cheerio.Root = await getDOM('/code-security/getting-started/quickstart')
+ const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart')
expect($('[data-testid=learning-track-card]').length).toBe(0)
expect($('[data-testid=learning-track-nav]').length).toBe(0)
}
diff --git a/src/fixtures/tests/head.ts b/src/fixtures/tests/head.ts
index 99ed5c9e804a..253f43190423 100644
--- a/src/fixtures/tests/head.ts
+++ b/src/fixtures/tests/head.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest'
describe('', () => {
test('includes page intro in `description` meta tag', async () => {
- const $: cheerio.Root = await getDOM('/get-started/markdown/intro')
+ const $: CheerioAPI = await getDOM('/get-started/markdown/intro')
// The intro has Markdown syntax which becomes HTML encoded in the lead element.
const lead = $('[data-testid="lead"] p')
expect(lead.html()).toMatch('syntax')
diff --git a/src/fixtures/tests/homepage.ts b/src/fixtures/tests/homepage.ts
index 0b259e63de23..8ba9d7ece5e0 100644
--- a/src/fixtures/tests/homepage.ts
+++ b/src/fixtures/tests/homepage.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { get, getDOM } from '@/tests/helpers/e2etest'
describe('home page', () => {
test('landing area', async () => {
- const $: cheerio.Root = await getDOM('/')
+ const $: CheerioAPI = await getDOM('/')
const container = $('#landing')
expect(container.length).toBe(1)
expect(container.find('h1').text()).toBe('GitHub Docs')
@@ -13,10 +13,10 @@ describe('home page', () => {
})
test('product groups can use Liquid', async () => {
- const $: cheerio.Root = await getDOM('/')
+ const $: CheerioAPI = await getDOM('/')
const main = $('[data-testid="product"]')
const links = main.find('a[href*="/"]')
- const hrefs = links.map((i: number, link: any) => $(link)).get()
+ const hrefs = links.map((_, link) => $(link)).get()
let externalLinks = 0
for (const href of hrefs) {
if (!href.attr('href')?.startsWith('https://')) {
diff --git a/src/fixtures/tests/html-comments.ts b/src/fixtures/tests/html-comments.ts
index 62d8bcf42088..6ed1d098eede 100644
--- a/src/fixtures/tests/html-comments.ts
+++ b/src/fixtures/tests/html-comments.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOMCached as getDOM } from '@/tests/helpers/e2etest'
describe('html-comments', () => {
test('regular comments are removed', async () => {
- const $: cheerio.Root = await getDOM('/get-started/markdown/html-comments')
+ const $: CheerioAPI = await getDOM('/get-started/markdown/html-comments')
const contents = $('#article-contents')
const html = contents.html()
expect(html).not.toContain('This comment should get deleted since it mentions gooblygook')
diff --git a/src/fixtures/tests/images.ts b/src/fixtures/tests/images.ts
index 876e6902b006..229d9c188d24 100644
--- a/src/fixtures/tests/images.ts
+++ b/src/fixtures/tests/images.ts
@@ -1,13 +1,13 @@
import { describe, expect, test } from 'vitest'
import sharp from 'sharp'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { get, head, getDOM } from '@/tests/helpers/e2etest'
import { MAX_WIDTH } from '@/content-render/unified/rewrite-asset-img-tags'
describe('render Markdown image tags', () => {
test('page with a single image', async () => {
- const $: cheerio.Root = await getDOM('/get-started/images/single-image')
+ const $: CheerioAPI = await getDOM('/get-started/images/single-image')
const pictures = $('#article-contents picture')
expect(pictures.length).toBe(1)
@@ -46,7 +46,7 @@ describe('render Markdown image tags', () => {
})
test('images have density specified', async () => {
- const $: cheerio.Root = await getDOM('/get-started/images/retina-image')
+ const $: CheerioAPI = await getDOM('/get-started/images/retina-image')
const pictures = $('#article-contents picture')
expect(pictures.length).toBe(3)
@@ -60,14 +60,14 @@ describe('render Markdown image tags', () => {
})
test('image inside a list keeps its span', async () => {
- const $: cheerio.Root = await getDOM('/get-started/images/images-in-lists')
+ const $: CheerioAPI = await getDOM('/get-started/images/images-in-lists')
const imageSpan = $('#article-contents > div > ol > li > div.procedural-image-wrapper')
expect(imageSpan.length).toBe(1)
})
test("links directly to images aren't rewritten", async () => {
- const $: cheerio.Root = await getDOM('/get-started/images/link-to-image')
+ const $: CheerioAPI = await getDOM('/get-started/images/link-to-image')
// There is only 1 link inside that page
const links = $('#article-contents a[href^="/"]') // exclude header link
expect(links.length).toBe(1)
diff --git a/src/fixtures/tests/internal-links.ts b/src/fixtures/tests/internal-links.ts
index e0847dc2395e..353dc286168c 100644
--- a/src/fixtures/tests/internal-links.ts
+++ b/src/fixtures/tests/internal-links.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
+import type { Element } from 'domhandler'
import { get, getDOM } from '@/tests/helpers/e2etest'
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases'
@@ -7,9 +8,9 @@ import { allVersions } from '@/versions/lib/all-versions'
describe('autotitle', () => {
test('internal links with AUTOTITLE resolves', async () => {
- const $: cheerio.Root = await getDOM('/get-started/foo/autotitling')
+ const $: CheerioAPI = await getDOM('/get-started/foo/autotitling')
const links = $('#article-contents a[href]')
- links.each((i: number, element: cheerio.Element) => {
+ links.each((i: number, element: Element) => {
if ($(element).attr('href')?.includes('/get-started/start-your-journey/hello-world')) {
expect($(element).text()).toBe('Hello World')
}
@@ -44,19 +45,19 @@ describe('cross-version-links', () => {
'links to free-pro-team should be implicit even on %p',
async (version: string) => {
const URL = `/${version}/get-started/foo/cross-version-linking`
- const $: cheerio.Root = await getDOM(URL)
+ const $: CheerioAPI = await getDOM(URL)
const links = $('#article-contents a[href]')
// Tests that the hardcoded prefix is always removed
const firstLink = links.filter(
- (i: number, element: cheerio.Element) =>
+ (i: number, element: Element) =>
$(element).text() === 'Hello world always in free-pro-team',
)
expect(firstLink.attr('href')).toBe('/en/get-started/start-your-journey/hello-world')
// Tests that the second link always goes to enterprise-server@X.Y
const secondLink = links.filter(
- (i: number, element: cheerio.Element) =>
+ (i: number, element: Element) =>
$(element).text() === 'Autotitling page always in enterprise-server latest',
)
expect(secondLink.attr('href')).toBe(
@@ -68,12 +69,12 @@ describe('cross-version-links', () => {
describe('link-rewriting', () => {
test('/en is injected', async () => {
- const $: cheerio.Root = await getDOM('/get-started/start-your-journey/link-rewriting')
+ const $: CheerioAPI = await getDOM('/get-started/start-your-journey/link-rewriting')
const links = $('#article-contents a[href]')
{
const link = links.filter(
- (i: number, element: cheerio.Element) => $(element).text() === 'Cross Version Linking',
+ (i: number, element: Element) => $(element).text() === 'Cross Version Linking',
)
expect(link.attr('href')).toMatch('/en/get-started/')
}
@@ -81,25 +82,25 @@ describe('link-rewriting', () => {
// Some links are left untouched
{
- const link = links.filter((i: number, element: cheerio.Element) =>
+ const link = links.filter((i: number, element: Element) =>
$(element).text().includes('Enterprise 11.10'),
)
expect(link.attr('href')).toMatch('/en/enterprise/')
}
{
- const link = links.filter((i: number, element: cheerio.Element) =>
+ const link = links.filter((i: number, element: Element) =>
$(element).text().includes('peterbe'),
)
expect(link.attr('href')).toMatch(/^https:/)
}
{
- const link = links.filter((i: number, element: cheerio.Element) =>
+ const link = links.filter((i: number, element: Element) =>
$(element).text().includes('Picture'),
)
expect(link.attr('href')).toMatch(/^\/assets\//)
}
{
- const link = links.filter((i: number, element: cheerio.Element) =>
+ const link = links.filter((i: number, element: Element) =>
$(element).text().includes('GraphQL Schema'),
)
expect(link.attr('href')).toMatch(/^\/public\//)
@@ -107,26 +108,26 @@ describe('link-rewriting', () => {
})
test('/en and current version (latest) is injected', async () => {
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
'/enterprise-cloud@latest/get-started/start-your-journey/link-rewriting',
)
const links = $('#article-contents a[href]')
const link = links.filter(
- (i: number, element: cheerio.Element) => $(element).text() === 'Cross Version Linking',
+ (i: number, element: Element) => $(element).text() === 'Cross Version Linking',
)
expect(link.attr('href')).toMatch('/en/enterprise-cloud@latest/get-started/')
})
test('/en and current version number is injected', async () => {
// enterprise-server, unlike enterprise-cloud, use numbers
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
'/enterprise-server@latest/get-started/start-your-journey/link-rewriting',
)
const links = $('#article-contents a[href]')
const link = links.filter(
- (i: number, element: cheerio.Element) => $(element).text() === 'Cross Version Linking',
+ (i: number, element: Element) => $(element).text() === 'Cross Version Linking',
)
expect(link.attr('href')).toMatch(
`/en/enterprise-server@${enterpriseServerReleases.latestStable}/get-started/`,
@@ -136,16 +137,16 @@ describe('link-rewriting', () => {
describe('subcategory links', () => {
test('no free-pro-team prefix', async () => {
- const $: cheerio.Root = await getDOM('/rest/actions')
+ const $: CheerioAPI = await getDOM('/rest/actions')
const links = $('[data-testid="table-of-contents"] a[href]')
- links.each((i: number, element: cheerio.Element) => {
+ links.each((i: number, element: Element) => {
expect($(element).attr('href')).not.toContain('/free-pro-team@latest')
})
})
test('enterprise-server prefix', async () => {
- const $: cheerio.Root = await getDOM('/enterprise-server@latest/rest/actions')
+ const $: CheerioAPI = await getDOM('/enterprise-server@latest/rest/actions')
const links = $('[data-testid="table-of-contents"] a[href]')
- links.each((i: number, element: cheerio.Element) => {
+ links.each((i: number, element: Element) => {
expect($(element).attr('href')).toMatch(/\/enterprise-server@\d/)
})
})
diff --git a/src/fixtures/tests/landing-hero.ts b/src/fixtures/tests/landing-hero.ts
index 15eb263cf6e1..743b339fa793 100644
--- a/src/fixtures/tests/landing-hero.ts
+++ b/src/fixtures/tests/landing-hero.ts
@@ -1,23 +1,23 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest'
describe('product landing page', () => {
test('product landing page displays full title', async () => {
- const $: cheerio.Root = await getDOM('/get-started')
+ const $: CheerioAPI = await getDOM('/get-started')
expect($('h1').first().text()).toMatch(/Getting started with HubGit/)
})
test('product landing page lists with shortTitle heading (free-pro-team)', async () => {
- const $: cheerio.Root = await getDOM('/pages')
+ const $: CheerioAPI = await getDOM('/pages')
// Note that this particular page (in the fixtures) has Liquid
// in its shorTitle.
expect($('#all-docs a').first().text()).toMatch('All Pages (HubGit) docs')
})
test('product landing page lists with shortTitle heading (enterprise-server)', async () => {
- const $: cheerio.Root = await getDOM('/enterprise-server@latest/pages')
+ const $: CheerioAPI = await getDOM('/enterprise-server@latest/pages')
// Note that this particular page (in the fixtures) has Liquid
// in its shorTitle.
expect($('#all-docs a').first().text()).toMatch('All Pages (HubGit Enterprise Server) docs')
diff --git a/src/fixtures/tests/liquid.ts b/src/fixtures/tests/liquid.ts
index c559d56399fb..80bad84ff455 100644
--- a/src/fixtures/tests/liquid.ts
+++ b/src/fixtures/tests/liquid.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDataByLanguage } from '@/data-directory/lib/get-data'
import { getDOM } from '@/tests/helpers/e2etest'
@@ -7,28 +7,28 @@ import { supported } from '@/versions/lib/enterprise-server-releases'
describe('spotlight', () => {
test('renders styled warnings', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/warnings')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/warnings')
const nodes = $('.ghd-spotlight-attention')
expect(nodes.length).toBe(1)
expect(nodes.text().includes('This is inside the warning.')).toBe(true)
})
test('renders styled danger', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/danger')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/danger')
const nodes = $('.ghd-spotlight-danger')
expect(nodes.length).toBe(1)
expect(nodes.text().includes('Danger, Will Robinson.')).toBe(true)
})
test('renders styled tips', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/tips')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/tips')
const nodes = $('.ghd-spotlight-success')
expect(nodes.length).toBe(1)
expect(nodes.text().includes('This is inside the tip.')).toBe(true)
})
test('renders styled notes', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/notes')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/notes')
const nodes = $('.ghd-spotlight-accent')
expect(nodes.length).toBe(1)
expect(nodes.text().includes('This is inside the note.')).toBe(true)
@@ -37,7 +37,7 @@ describe('spotlight', () => {
describe('raw', () => {
test('renders raw', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/raw')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/raw')
const lead = $('[data-testid="lead"]').html()
expect(lead).toMatch('{% raw %}')
const code = $('pre code').html()
@@ -48,7 +48,7 @@ describe('raw', () => {
describe('tool', () => {
test('renders platform-specific content', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/platform-specific')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/platform-specific')
expect($('.ghd-tool.mac p').length).toBe(1)
expect($('.ghd-tool.mac p').text().includes('mac specific content')).toBe(true)
expect($('.ghd-tool.windows p').length).toBe(1)
@@ -58,7 +58,7 @@ describe('tool', () => {
})
test('renders expected mini TOC headings in platform-specific content', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/platform-specific')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/platform-specific')
expect($('h2#in-this-article').length).toBe(1)
expect($('h2#in-this-article + nav ul .ghd-tool.mac').length).toBe(1)
expect($('h2#in-this-article + nav ul .ghd-tool.windows').length).toBe(1)
@@ -68,7 +68,7 @@ describe('tool', () => {
describe('post', () => {
test('whitespace control', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/whitespace')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/whitespace')
const html = $('#article-contents').html()
expect(html).toMatch('HubGit
')
expect(html).toMatch('Text before. HubGit Text after.
')
@@ -78,7 +78,7 @@ describe('post', () => {
// Test what happens to `Cram{% ifversion fpt %}FPT{% endif %}ped.`
// when it's not free-pro-team.
{
- const $inner: cheerio.Root = await getDOM(
+ const $inner: CheerioAPI = await getDOM(
'/enterprise-server@latest/get-started/liquid/whitespace',
)
const innerHtml = $inner('#article-contents').html()
@@ -91,7 +91,7 @@ describe('post', () => {
describe('rowheaders', () => {
test('rowheaders', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/table-row-headers')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/table-row-headers')
const tables = $('#article-contents table')
expect(tables.length).toBe(2)
@@ -188,7 +188,7 @@ describe('ifversion', () => {
test.each(Object.keys(matchesPerVersion))(
'ifversion using rendered version %p',
async (version: string) => {
- const $: cheerio.Root = await getDOM(`/${version}/get-started/liquid/ifversion`)
+ const $: CheerioAPI = await getDOM(`/${version}/get-started/liquid/ifversion`)
const html = $('#article-contents').html()
const allConditions = Object.values(matchesPerVersion).flat()
@@ -215,7 +215,7 @@ describe('ifversion', () => {
describe('misc Liquid', () => {
test('links with liquid from data', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/links-with-liquid')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/links-with-liquid')
// The URL comes from variables.product.pricing_url
const url = getDataByLanguage('variables.product.pricing_url', 'en')
if (!url) throw new Error('variable could not be found')
@@ -235,7 +235,7 @@ describe('misc Liquid', () => {
// Markdown directly follows a tool tag like `{% linux %}...{% endlinux %}`.
// The next line immediately after the `{% endlinux %}` should not
// leave the Markdown unrendered
- const $: cheerio.Root = await getDOM('/get-started/liquid/tool-platform-switcher')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/tool-platform-switcher')
const innerHTML = $('#article-contents').html()
expect(innerHTML).not.toMatch('On *this* line is `Markdown` too.')
expect(innerHTML).toMatch('On this line is Markdown too.
')
@@ -244,7 +244,7 @@ describe('misc Liquid', () => {
describe('data tag', () => {
test('injects data reusables with the right whitespace', async () => {
- const $: cheerio.Root = await getDOM('/get-started/liquid/data')
+ const $: CheerioAPI = await getDOM('/get-started/liquid/data')
// This proves that the two injected reusables tables work.
// CommonMark is finicky if the indentation isn't perfect, so
diff --git a/src/fixtures/tests/markdown.ts b/src/fixtures/tests/markdown.ts
index 36b604106c38..a438412b5ffa 100644
--- a/src/fixtures/tests/markdown.ts
+++ b/src/fixtures/tests/markdown.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest'
describe('markdown rendering', () => {
test('markdown in intro', async () => {
- const $: cheerio.Root = await getDOM('/get-started/markdown/intro')
+ const $: CheerioAPI = await getDOM('/get-started/markdown/intro')
const html = $('[data-testid="lead"]').html()
expect(html).toMatch('Markdown')
expect(html).toMatch('syntax')
@@ -15,7 +15,7 @@ describe('markdown rendering', () => {
describe('alerts', () => {
test('basic rendering', async () => {
- const $: cheerio.Root = await getDOM('/get-started/markdown/alerts')
+ const $: CheerioAPI = await getDOM('/get-started/markdown/alerts')
const alerts = $('#article-contents .ghd-alert')
// See src/fixtures/fixtures/content/get-started/markdown/alerts.md
// to be this confident in the assertions.
diff --git a/src/fixtures/tests/minitoc.ts b/src/fixtures/tests/minitoc.ts
index 9d2475853acd..8ca670e7a320 100644
--- a/src/fixtures/tests/minitoc.ts
+++ b/src/fixtures/tests/minitoc.ts
@@ -1,37 +1,37 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest'
describe('minitoc', () => {
test('renders mini TOC in articles with more than one heading', async () => {
- const $: cheerio.Root = await getDOM('/en/get-started/minitocs/multiple-headings')
+ const $: CheerioAPI = await getDOM('/en/get-started/minitocs/multiple-headings')
expect($('h2#in-this-article').length).toBe(1)
expect($('h2#in-this-article + nav ul li').length).toBeGreaterThan(1)
})
test('does not render mini TOC in articles with only one heading', async () => {
- const $: cheerio.Root = await getDOM('/en/get-started/minitocs/one-heading')
+ const $: CheerioAPI = await getDOM('/en/get-started/minitocs/one-heading')
expect($('h2#in-this-article').length).toBe(0)
})
test('does not render mini TOC in articles with no headings', async () => {
- const $: cheerio.Root = await getDOM('/en/get-started/minitocs/no-heading')
+ const $: CheerioAPI = await getDOM('/en/get-started/minitocs/no-heading')
expect($('h2#in-this-article').length).toBe(0)
})
test('does not render mini TOC in non-articles', async () => {
- const $: cheerio.Root = await getDOM('/en/get-started')
+ const $: CheerioAPI = await getDOM('/en/get-started')
expect($('h2#in-this-article').length).toBe(0)
})
test('renders mini TOC with correct links when headings contain markup', async () => {
- const $: cheerio.Root = await getDOM('/en/get-started/minitocs/markup-heading')
+ const $: CheerioAPI = await getDOM('/en/get-started/minitocs/markup-heading')
expect($('h2#in-this-article + nav ul li a[href="#on"]').length).toBe(1)
})
test("max heading doesn't exceed h2", async () => {
- const $: cheerio.Root = await getDOM('/en/get-started/minitocs/multiple-headings')
+ const $: CheerioAPI = await getDOM('/en/get-started/minitocs/multiple-headings')
expect($('h2#in-this-article').length).toBe(1)
expect($('h2#in-this-article + nav ul').length).toBeGreaterThan(0) // non-indented items
expect($('h2#in-this-article + nav ul div ul div').length).toBe(0) // indented items
diff --git a/src/fixtures/tests/page-titles.ts b/src/fixtures/tests/page-titles.ts
index 2bfe53bf8cc5..3d4492ec021f 100644
--- a/src/fixtures/tests/page-titles.ts
+++ b/src/fixtures/tests/page-titles.ts
@@ -1,22 +1,22 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases'
import { getDOM } from '@/tests/helpers/e2etest'
describe('page titles', () => {
test('homepage', async () => {
- const $: cheerio.Root = await getDOM('/en')
+ const $: CheerioAPI = await getDOM('/en')
expect($('title').text()).toBe('GitHub Docs')
})
test('fpt article', async () => {
- const $: cheerio.Root = await getDOM('/get-started/start-your-journey/hello-world')
+ const $: CheerioAPI = await getDOM('/get-started/start-your-journey/hello-world')
expect($('title').text()).toBe('Hello World - GitHub Docs')
})
test('ghes article', async () => {
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
`/enterprise-server@latest/get-started/start-your-journey/hello-world`,
)
expect($('title').text()).toBe(
@@ -25,12 +25,12 @@ describe('page titles', () => {
})
test('fpt subcategory page', async () => {
- const $: cheerio.Root = await getDOM('/en/get-started/start-your-journey')
+ const $: CheerioAPI = await getDOM('/en/get-started/start-your-journey')
expect($('title').text()).toBe('Start your journey - GitHub Docs')
})
test('fpt category page', async () => {
- const $: cheerio.Root = await getDOM('/actions/category')
+ const $: CheerioAPI = await getDOM('/actions/category')
expect($('title').text()).toBe('Category page of GitHub Actions - GitHub Docs')
})
})
diff --git a/src/fixtures/tests/permissions-callout.ts b/src/fixtures/tests/permissions-callout.ts
index 6c194d9a28ae..93cc31cd9d87 100644
--- a/src/fixtures/tests/permissions-callout.ts
+++ b/src/fixtures/tests/permissions-callout.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest'
describe('permission statements', () => {
test('article page product statement', async () => {
- const $: cheerio.Root = await getDOM('/get-started/foo/page-with-callout')
+ const $: CheerioAPI = await getDOM('/get-started/foo/page-with-callout')
const callout = $('[data-testid=product-statement] div')
expect(callout.html()).toBe('Callout for HubGit Pages
')
})
@@ -16,7 +16,7 @@ describe('permission statements', () => {
// an empty string.
// This test tests that alert is not rendered if its output
// "exits" but is empty.
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
'/enterprise-server@latest/get-started/foo/page-with-callout',
)
const callout = $('[data-testid=product-statement]')
@@ -24,13 +24,13 @@ describe('permission statements', () => {
})
test('toc landing page', async () => {
- const $: cheerio.Root = await getDOM('/actions/category')
+ const $: CheerioAPI = await getDOM('/actions/category')
const callout = $('[data-testid=product-statement] div')
expect(callout.html()).toBe('This is the callout text
')
})
test('page with permission frontmatter', async () => {
- const $: cheerio.Root = await getDOM('/get-started/markdown/permissions')
+ const $: CheerioAPI = await getDOM('/get-started/markdown/permissions')
const html = $('[data-testid=permissions-statement] div').html()
// Markdown
expect(html).toMatch('admin')
@@ -39,9 +39,7 @@ describe('permission statements', () => {
})
test('page with permission frontmatter and product statement', async () => {
- const $: cheerio.Root = await getDOM(
- '/get-started/foo/page-with-permissions-and-product-callout',
- )
+ const $: CheerioAPI = await getDOM('/get-started/foo/page-with-permissions-and-product-callout')
const html = $('[data-testid=permissions-callout] div').html()
// part of the UI
expect(html).toMatch('Who can use this feature')
diff --git a/src/fixtures/tests/sidebar.ts b/src/fixtures/tests/sidebar.ts
index 68f746c4f504..568b5bb6a369 100644
--- a/src/fixtures/tests/sidebar.ts
+++ b/src/fixtures/tests/sidebar.ts
@@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOMCached as getDOM } from '@/tests/helpers/e2etest'
describe('sidebar', () => {
test('top level product mentioned at top of sidebar', async () => {
- const $: cheerio.Root = await getDOM('/get-started')
+ const $: CheerioAPI = await getDOM('/get-started')
// Desktop
const sidebarProduct = $('[data-testid="sidebar-product-xl"]')
expect(sidebarProduct.text()).toBe('Get started')
@@ -16,12 +16,12 @@ describe('sidebar', () => {
})
test('REST pages get the REST sidebar', async () => {
- const $: cheerio.Root = await getDOM('/rest')
+ const $: CheerioAPI = await getDOM('/rest')
expect($('[data-testid=rest-sidebar-reference]').length).toBe(1)
})
test('leaf-node article marked as aria-current=page', async () => {
- const $: cheerio.Root = await getDOM('/get-started/start-your-journey/hello-world')
+ const $: CheerioAPI = await getDOM('/get-started/start-your-journey/hello-world')
expect(
$(
'[data-testid=sidebar] [data-testid=product-sidebar] a[aria-current="page"] span span',
@@ -30,7 +30,7 @@ describe('sidebar', () => {
})
test('sidebar should always use the shortTitle', async () => {
- const $: cheerio.Root = await getDOM('/get-started/foo/bar')
+ const $: CheerioAPI = await getDOM('/get-started/foo/bar')
// The page /get-started/foo/bar has a short title that is different
// from its regular title.
expect(
@@ -41,7 +41,7 @@ describe('sidebar', () => {
})
test('short titles with Liquid and HTML characters', async () => {
- const $: cheerio.Root = await getDOM('/get-started/foo/html-short-title')
+ const $: CheerioAPI = await getDOM('/get-started/foo/html-short-title')
const link = $(
'[data-testid=sidebar] [data-testid=product-sidebar] a[href*="/get-started/foo/html-short-title"]',
)
@@ -51,26 +51,26 @@ describe('sidebar', () => {
test('Liquid is rendered in short title used at top of sidebar', async () => {
// Free, pro, team
{
- const $: cheerio.Root = await getDOM('/pages')
+ const $: CheerioAPI = await getDOM('/pages')
const link = $('#allproducts-menu a')
expect(link.text()).toBe('Pages (HubGit)')
}
// Enterprise Server
{
- const $: cheerio.Root = await getDOM('/enterprise-server@latest/pages')
+ const $: CheerioAPI = await getDOM('/enterprise-server@latest/pages')
const link = $('#allproducts-menu a')
expect(link.text()).toBe('Pages (HubGit Enterprise Server)')
}
// Enterprise Cloud
{
- const $: cheerio.Root = await getDOM('/enterprise-cloud@latest/pages')
+ const $: CheerioAPI = await getDOM('/enterprise-cloud@latest/pages')
const link = $('#allproducts-menu a')
expect(link.text()).toBe('Pages (HubGit Enterprise Cloud)')
}
})
test('no docset link for early-access', async () => {
- const $: cheerio.Root = await getDOM('/early-access/secrets/deeper/mariana-trench')
+ const $: CheerioAPI = await getDOM('/early-access/secrets/deeper/mariana-trench')
// Deskop
expect($('[data-testid="sidebar-product-xl"]').length).toBe(0)
// Mobile
diff --git a/src/fixtures/tests/translations.ts b/src/fixtures/tests/translations.ts
index 2fc631089f59..0682eecfcd71 100644
--- a/src/fixtures/tests/translations.ts
+++ b/src/fixtures/tests/translations.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { TRANSLATIONS_FIXTURE_ROOT } from '@/frame/lib/constants'
import { getDOM, head } from '@/tests/helpers/e2etest'
@@ -12,7 +12,7 @@ if (!TRANSLATIONS_FIXTURE_ROOT) {
describe('translations', () => {
test('home page', async () => {
- const $: cheerio.Root = await getDOM('/ja')
+ const $: CheerioAPI = await getDOM('/ja')
const h1 = $('h1').text()
// You gotta know your src/fixtures/fixtures/translations/ja-jp/data/ui.yml
expect(h1).toBe('日本 GitHub Docs')
@@ -32,13 +32,13 @@ describe('translations', () => {
})
test('hello world', async () => {
- const $: cheerio.Root = await getDOM('/ja/get-started/start-your-journey/hello-world')
+ const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/hello-world')
const h1 = $('h1').text()
expect(h1).toBe('こんにちは World')
})
test('internal links get prefixed with /ja', async () => {
- const $: cheerio.Root = await getDOM('/ja/get-started/start-your-journey/link-rewriting')
+ const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/link-rewriting')
const links = $('#article-contents a[href]')
const jaLinks = links.filter((i: number, element: any) => {
const href = $(element).attr('href')
@@ -53,7 +53,7 @@ describe('translations', () => {
})
test('internal links with AUTOTITLE resolves', async () => {
- const $: cheerio.Root = await getDOM('/ja/get-started/foo/autotitling')
+ const $: CheerioAPI = await getDOM('/ja/get-started/foo/autotitling')
const links = $('#article-contents a[href]')
links.each((i: number, element: any) => {
if ($(element).attr('href')?.includes('/ja/get-started/start-your-journey/hello-world')) {
@@ -67,7 +67,7 @@ describe('translations', () => {
test('correction of linebreaks in translations', async () => {
// free-pro-team
{
- const $: cheerio.Root = await getDOM('/ja/get-started/foo/table-with-ifversions')
+ const $: CheerioAPI = await getDOM('/ja/get-started/foo/table-with-ifversions')
const paragraph = $('#article-contents p').text()
expect(paragraph).toMatch('mention of HubGit in Liquid')
@@ -80,7 +80,7 @@ describe('translations', () => {
}
// enterprise-server
{
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
'/ja/enterprise-server@latest/get-started/foo/table-with-ifversions',
)
@@ -96,7 +96,7 @@ describe('translations', () => {
})
test('automatic correction of bad AUTOTITLE in reusables', async () => {
- const $: cheerio.Root = await getDOM('/ja/get-started/start-your-journey/hello-world')
+ const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/hello-world')
const links = $('#article-contents a[href]')
const texts = links.map((i: number, element: any) => $(element).text()).get()
// That Japanese page uses AUTOTITLE links. Both in the main `.md` file
@@ -126,7 +126,7 @@ describe('translations', () => {
// which needs to become:
//
// [Bar](バー)
- const $: cheerio.Root = await getDOM('/ja/get-started/start-your-journey/hello-world')
+ const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/hello-world')
const links = $('#article-contents a[href]')
const texts = links
.filter((i: number, element: any) => {
diff --git a/src/fixtures/tests/versioning.ts b/src/fixtures/tests/versioning.ts
index 2665fcc2e682..5fe70db14127 100644
--- a/src/fixtures/tests/versioning.ts
+++ b/src/fixtures/tests/versioning.ts
@@ -1,12 +1,12 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM, head } from '@/tests/helpers/e2etest'
import { supported } from '@/versions/lib/enterprise-server-releases'
describe('article versioning', () => {
test('only links to articles for fpt', async () => {
- const $: cheerio.Root = await getDOM('/get-started/versioning')
+ const $: CheerioAPI = await getDOM('/get-started/versioning')
const links = $('[data-testid="table-of-contents"] a')
// Only 1 link because there's only 1 article available in fpt
expect(links.length).toBe(1)
@@ -14,7 +14,7 @@ describe('article versioning', () => {
})
test('only links to articles for ghec', async () => {
- const $: cheerio.Root = await getDOM('/enterprise-cloud@latest/get-started/versioning')
+ const $: CheerioAPI = await getDOM('/enterprise-cloud@latest/get-started/versioning')
const links = $('[data-testid="table-of-contents"] a')
expect(links.length).toBe(2)
const first = links.filter((i: number) => i === 0)
diff --git a/src/fixtures/tests/video-transcripts.ts b/src/fixtures/tests/video-transcripts.ts
index 812e5fbc0c3d..36a4f6adca1c 100644
--- a/src/fixtures/tests/video-transcripts.ts
+++ b/src/fixtures/tests/video-transcripts.ts
@@ -1,12 +1,12 @@
import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
import { getDOM } from '@/tests/helpers/e2etest'
describe('transcripts', () => {
describe('product landing page', () => {
test('video link from product landing page leads to video', async () => {
- const $: cheerio.Root = await getDOM('/en/get-started')
+ const $: CheerioAPI = await getDOM('/en/get-started')
expect($('a#product-video').attr('href')).toBe(
'/en/video-transcripts/transcript--my-awesome-video',
)
@@ -15,7 +15,7 @@ describe('transcripts', () => {
describe('transcript page', () => {
test('video link from transcript leads to video', async () => {
- const $: cheerio.Root = await getDOM(
+ const $: CheerioAPI = await getDOM(
'/en/get-started/video-transcripts/transcript--my-awesome-video',
)
expect($('a#product-video').attr('href')).toBe('https://www.yourube.com/abc123')
diff --git a/src/frame/lib/get-mini-toc-items.ts b/src/frame/lib/get-mini-toc-items.ts
index a701e6bb9e98..9a370b71d98e 100644
--- a/src/frame/lib/get-mini-toc-items.ts
+++ b/src/frame/lib/get-mini-toc-items.ts
@@ -1,4 +1,5 @@
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
+import type { Element } from 'domhandler'
import { range } from 'lodash-es'
import { renderContent } from '@/content-render/index'
@@ -29,7 +30,7 @@ export default function getMiniTocItems(
maxHeadingLevel = 2,
headingScope = '',
): MiniTocItem[] {
- const $ = cheerio.load(html, { xmlMode: true })
+ const $ = load(html, { xmlMode: true })
// eg `h2, h3` or `h2, h3, h4` depending on maxHeadingLevel
const selector = range(2, maxHeadingLevel + 1)
@@ -48,9 +49,9 @@ export default function getMiniTocItems(
const flatToc = headings
.get()
.filter((item) => {
- if (!item.parent || !item.parent.attribs) return true
- // Hide any items that belong to a hidden div
- const { attribs } = item.parent
+ const parent = item.parent as Element | null
+ if (!parent || !parent.attribs) return true
+ const { attribs } = parent
return !('hidden' in attribs)
})
.map((item) => {
@@ -73,7 +74,7 @@ export default function getMiniTocItems(
$('strong', item).map((i, el) => $(el).replaceWith($(el).contents()))
const contents: MiniTocContents = { href, title: $(item).text().trim() }
- const element = $(item)[0] as cheerio.TagElement
+ const element = $(item)[0] as Element
const headingLevel = parseInt(element.name.match(/\d+/)![0], 10) || 0 // the `2` from `h2`
const platform = $(item).parent('.ghd-tool').attr('class') || ''
diff --git a/src/frame/lib/page.ts b/src/frame/lib/page.ts
index 5f3c619cbd8c..bd0079211673 100644
--- a/src/frame/lib/page.ts
+++ b/src/frame/lib/page.ts
@@ -1,7 +1,7 @@
import assert from 'assert'
import path from 'path'
import fs from 'fs/promises'
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
import getApplicableVersions from '@/versions/lib/get-applicable-versions'
import generateRedirectsForPermalinks from '@/redirects/lib/permalinks'
import getEnglishHeadings from '@/languages/lib/get-english-headings'
@@ -440,7 +440,7 @@ class Page {
if (!opts.unwrap) return html
// The unwrap option removes surrounding tags from a string, preserving any inner HTML
- const $ = cheerio.load(html, { xmlMode: true })
+ const $ = load(html, { xmlMode: true })
return $.root().contents().html() || ''
}
diff --git a/src/frame/tests/page.ts b/src/frame/tests/page.ts
index 266babf20100..591c0d8fe247 100644
--- a/src/frame/tests/page.ts
+++ b/src/frame/tests/page.ts
@@ -1,7 +1,7 @@
import { fileURLToPath } from 'url'
import path from 'path'
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
import { beforeAll, beforeEach, describe, expect, test } from 'vitest'
import Page, { FrontmatterErrorsError } from '@/frame/lib/page'
@@ -97,7 +97,7 @@ describe('Page class', () => {
}
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}`
let rendered = await page!.render(context)
- let $ = cheerio.load(rendered)
+ let $ = load(rendered)
expect(($ as any).text()).toBe(
'This text should render on any actively supported version of Enterprise Server',
)
@@ -108,7 +108,7 @@ describe('Page class', () => {
context.currentVersion = `enterprise-server@${enterpriseServerReleases.oldestSupported}`
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}`
rendered = await page!.render(context)
- $ = cheerio.load(rendered)
+ $ = load(rendered)
expect(($ as any).text()).toBe(
'This text should render on any actively supported version of Enterprise Server',
)
@@ -119,7 +119,7 @@ describe('Page class', () => {
context.currentVersion = nonEnterpriseDefaultVersion
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}`
rendered = await page!.render(context)
- $ = cheerio.load(rendered)
+ $ = load(rendered)
expect(($ as any).text()).not.toBe(
'This text should render on any actively supported version of Enterprise Server',
)
diff --git a/src/graphql/pages/breaking-changes.tsx b/src/graphql/pages/breaking-changes.tsx
index 7e8734616df2..1224d0e54e75 100644
--- a/src/graphql/pages/breaking-changes.tsx
+++ b/src/graphql/pages/breaking-changes.tsx
@@ -1,5 +1,7 @@
import { GetServerSideProps } from 'next'
import GithubSlugger from 'github-slugger'
+import type { ExtendedRequest } from '@/types'
+import type { ServerResponse } from 'http'
import { MainContextT, MainContext, getMainContext } from '@/frame/components/context/MainContext'
import { AutomatedPage } from '@/automated-pipelines/components/AutomatedPage'
@@ -40,8 +42,8 @@ export const getServerSideProps: GetServerSideProps = async (context) =>
const { getGraphqlBreakingChanges } = await import('@/graphql/lib/index')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
- const req = context.req as any
- const res = context.res as any
+ const req = context.req as unknown as ExtendedRequest
+ const res = context.res as unknown as ServerResponse
const currentVersion = context.query.versionId as string
const schema = getGraphqlBreakingChanges(currentVersion)
if (!schema) throw new Error(`No graphql breaking changes schema found for ${currentVersion}`)
@@ -65,7 +67,7 @@ export const getServerSideProps: GetServerSideProps = async (context) =>
}),
)
const titles = Object.values(headings).map((heading) => heading.title)
- const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context.context, 2)
+ const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context!, 2)
// Update the existing context to include the miniTocItems from GraphQL
automatedPageContext.miniTocItems.push(...changelogMiniTocItems)
diff --git a/src/graphql/pages/changelog.tsx b/src/graphql/pages/changelog.tsx
index b39eb0bd1d36..8dd1ce67fa7d 100644
--- a/src/graphql/pages/changelog.tsx
+++ b/src/graphql/pages/changelog.tsx
@@ -1,4 +1,6 @@
import { GetServerSideProps } from 'next'
+import type { ExtendedRequest } from '@/types'
+import type { ServerResponse } from 'http'
import { MainContextT, MainContext, getMainContext } from '@/frame/components/context/MainContext'
import { AutomatedPage } from '@/automated-pipelines/components/AutomatedPage'
@@ -31,8 +33,8 @@ export const getServerSideProps: GetServerSideProps = async (context) =>
const { getGraphqlChangelog } = await import('@/graphql/lib/index')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
- const req = context.req as any
- const res = context.res as any
+ const req = context.req as unknown as ExtendedRequest
+ const res = context.res as unknown as ServerResponse
const currentVersion = context.query.versionId as string
const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[]
if (!schema) throw new Error('No graphql free-pro-team changelog schema found.')
@@ -41,7 +43,7 @@ export const getServerSideProps: GetServerSideProps = async (context) =>
// content/graphql/reference/*
const automatedPageContext = getAutomatedPageContextFromRequest(req)
const titles = schema.map((item) => `Schema changes for ${item.date}`)
- const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context.context, 2)
+ const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context!, 2)
// Update the existing context to include the miniTocItems from GraphQL
automatedPageContext.miniTocItems.push(...changelogMiniTocItems)
diff --git a/src/graphql/pages/schema-previews.tsx b/src/graphql/pages/schema-previews.tsx
index e9799f58908d..5a1ac1beb260 100644
--- a/src/graphql/pages/schema-previews.tsx
+++ b/src/graphql/pages/schema-previews.tsx
@@ -1,4 +1,6 @@
import { GetServerSideProps } from 'next'
+import type { ExtendedRequest } from '@/types'
+import type { ServerResponse } from 'http'
import {
MainContextT,
@@ -36,8 +38,8 @@ export const getServerSideProps: GetServerSideProps = async (context) =>
const { getPreviews } = await import('@/graphql/lib/index')
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
- const req = context.req as any
- const res = context.res as any
+ const req = context.req as unknown as ExtendedRequest
+ const res = context.res as unknown as ServerResponse
const currentVersion = context.query.versionId as string
const schema = getPreviews(currentVersion) as PreviewT[]
if (!schema) throw new Error(`No graphql preview schema found for ${currentVersion}`)
@@ -47,7 +49,7 @@ export const getServerSideProps: GetServerSideProps = async (context) =>
// content/graphql/reference/*
const automatedPageContext = getAutomatedPageContextFromRequest(req)
const titles = schema.map((item) => item.title)
- const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context.context, 2)
+ const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context!, 2)
// Update the existing context to include the miniTocItems from GraphQL
automatedPageContext.miniTocItems.push(...changelogMiniTocItems)
diff --git a/src/landings/tests/curated-homepage-links.ts b/src/landings/tests/curated-homepage-links.ts
index fa94464a1792..c469743bcd3f 100644
--- a/src/landings/tests/curated-homepage-links.ts
+++ b/src/landings/tests/curated-homepage-links.ts
@@ -1,5 +1,5 @@
import { describe, expect, test, vi } from 'vitest'
-import cheerio from 'cheerio'
+import type { Element } from 'domhandler'
import { getDOM } from '@/tests/helpers/e2etest'
@@ -14,7 +14,7 @@ describe('curated homepage links', () => {
expect($links.length).toBeGreaterThanOrEqual(6)
// Check that each link is localized and includes a title and intro
- $links.each((i: number, el: cheerio.Element) => {
+ $links.each((i: number, el: Element) => {
const linkUrl = $(el).attr('href') as string
expect(linkUrl.startsWith('/en/')).toBe(true)
diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts
index db740df79bf1..1e8fb91a806c 100644
--- a/src/languages/lib/correct-translation-content.ts
+++ b/src/languages/lib/correct-translation-content.ts
@@ -27,6 +27,7 @@ export function correctTranslatedContentStrings(
content = content.replaceAll('{% datos variables', '{% data variables')
content = content.replaceAll('{% de datos variables', '{% data variables')
content = content.replaceAll('{% datos reusables', '{% data reusables')
+ content = content.replaceAll('{% data reutilizables.', '{% data reusables.')
content = content.replaceAll('{%- ifversion fpt o ghec %}', '{%- ifversion fpt or ghec %}')
content = content.replaceAll('{% ifversion fpt o ghec %}', '{% ifversion fpt or ghec %}')
}
@@ -69,9 +70,16 @@ export function correctTranslatedContentStrings(
content = content.replaceAll('{% данные variables.', '{% data variables.')
content = content.replaceAll('{% данных reusables', '{% data reusables')
content = content.replaceAll('{% данные reusables', '{% data reusables')
+ content = content.replaceAll('{% данных переменных.', '{% data variables.')
+ content = content.replaceAll('{% данных.product.', '{% data variables.product.')
+ content = content.replaceAll('{% data переменных.product.', '{% data variables.product.')
+ content = content.replaceAll('{% переменным данных.product.', '{% data variables.product.')
content = content.replaceAll('{% необработанного %}', '{% raw %}')
content = content.replaceAll('{%- ifversion fpt или ghec %}', '{%- ifversion fpt or ghec %}')
content = content.replaceAll('{% ifversion fpt или ghec %}', '{% ifversion fpt or ghec %}')
+ content = content.replaceAll('{% ifversion ghec или fpt %}', '{% ifversion ghec or fpt %}')
+ content = content.replaceAll('{% ghes или ghec %}', '{% ifversion ghes or ghec %}')
+ content = content.replaceAll('{% elsif ghec или ghes %}', '{% elsif ghec or ghes %}')
content = content.replaceAll('{% endif _%}', '{% endif %}')
content = content.replaceAll('{% конечным %}', '{% endif %}')
content = content.replaceAll('{% конец %}', '{% endif %}')
@@ -81,6 +89,7 @@ export function correctTranslatedContentStrings(
content = content.replaceAll('{% конечных головщиков %}', '{% endrowheaders %}')
content = content.replaceAll('{% данных для повторного использования.', '{% data reusables.')
content = content.replaceAll('{% еще %}', '{% else %}')
+ content = content.replaceAll('{% ещё %}', '{% else %}')
content = content.replaceAll('{% необработанные %}', '{% raw %}')
// Fix double quotes in Russian YAML files that cause parsing errors
@@ -105,6 +114,7 @@ export function correctTranslatedContentStrings(
content = content.replaceAll('{% données variables', '{% data variables')
content = content.replaceAll('{% données réutilisables.', '{% data reusables.')
content = content.replaceAll('{% variables de données.', '{% data variables.')
+ content = content.replaceAll('{% autre %}', '{% else %}')
content = content.replaceAll('{%- ifversion fpt ou ghec %}', '{%- ifversion fpt or ghec %}')
content = content.replaceAll('{% ifversion fpt ou ghec %}', '{% ifversion fpt or ghec %}')
}
@@ -125,6 +135,7 @@ export function correctTranslatedContentStrings(
content = content.replaceAll('{% Daten variables', '{% data variables')
content = content.replaceAll('{% daten variables', '{% data variables')
content = content.replaceAll('{%-Daten variables', '{%- data variables')
+ content = content.replaceAll('{%-Daten-variables', '{%- data variables')
content = content.replaceAll('{%- ifversion fpt oder ghec %}', '{%- ifversion fpt or ghec %}')
content = content.replaceAll('{% ifversion fpt oder ghec %}', '{% ifversion fpt or ghec %}')
}
diff --git a/src/languages/lib/get-alert-titles.ts b/src/languages/lib/get-alert-titles.ts
index d73e33281c78..117c646adfc0 100644
--- a/src/languages/lib/get-alert-titles.ts
+++ b/src/languages/lib/get-alert-titles.ts
@@ -3,19 +3,28 @@ import path from 'path'
import yaml from 'js-yaml'
import languages from './languages-server'
-const cache: Record = {}
+interface AlertTitles {
+ [key: string]: string
+}
+
+interface UiYaml {
+ alerts?: AlertTitles
+ [key: string]: unknown
+}
+
+const cache: Record = {}
-export async function getAlertTitles(page: Record) {
+export async function getAlertTitles(page: { languageCode: string }) {
const { languageCode } = page
if (cache[languageCode]) return cache[languageCode]
let file = ''
- let yamlFile: Record = {}
+ let yamlFile: UiYaml = {}
if (languageCode !== 'en') {
try {
const { dir } = languages[languageCode]
file = await fs.readFile(path.join(dir, `data/ui.yml`), 'utf-8')
- yamlFile = yaml.load(file) as Record
+ yamlFile = yaml.load(file) as UiYaml
} catch (e) {
console.warn(`Failed to load translated alert titles`, e)
}
@@ -23,9 +32,9 @@ export async function getAlertTitles(page: Record) {
if (!file || !yamlFile.alerts) {
const { dir } = languages.en
file = await fs.readFile(path.join(dir, `data/ui.yml`), 'utf-8')
- yamlFile = yaml.load(file) as Record
+ yamlFile = yaml.load(file) as UiYaml
}
- cache[languageCode] = yamlFile.alerts
+ cache[languageCode] = yamlFile.alerts ?? {}
return cache[languageCode]
}
diff --git a/src/languages/tests/frame.ts b/src/languages/tests/frame.ts
index fca75c4a18e7..669a12f19a49 100644
--- a/src/languages/tests/frame.ts
+++ b/src/languages/tests/frame.ts
@@ -2,6 +2,8 @@ import { describe, expect, test, vi } from 'vitest'
import { languageKeys } from '@/languages/lib/languages-server'
import { blockIndex } from '@/frame/middleware/block-robots'
+import type { Element } from 'domhandler'
+
import { get, getDOMCached as getDOM } from '@/tests/helpers/e2etest'
import Page from '@/frame/lib/page'
@@ -17,13 +19,13 @@ describe('frame', () => {
test.each(langs)('breadcrumbs link to %s pages', async (lang) => {
const $ = await getDOM(`/${lang}/get-started/learning-about-github`)
const $breadcrumbs = $('[data-testid=breadcrumbs-in-article] a')
- expect(($breadcrumbs[0] as cheerio.TagElement).attribs.href).toBe(`/${lang}/get-started`)
+ expect(($breadcrumbs[0] as Element).attribs.href).toBe(`/${lang}/get-started`)
})
test.each(langs)('homepage links go to %s pages', async (lang) => {
const $ = await getDOM(`/${lang}`)
const $links = $('[data-testid=bump-link]')
- $links.each((i: number, el: cheerio.Element) => {
+ $links.each((i: number, el: Element) => {
const linkUrl = $(el).attr('href')
expect((linkUrl || '').startsWith(`/${lang}/`)).toBe(true)
})
diff --git a/src/links/lib/excluded-links.yml b/src/links/lib/excluded-links.yml
index 42e1267592fb..f606031cd003 100644
--- a/src/links/lib/excluded-links.yml
+++ b/src/links/lib/excluded-links.yml
@@ -70,8 +70,8 @@
- is: https://moodle.org
- is: https://azure.microsoft.com
- is: https://api.octocorp.ghe.com
-- is: https://platform.openai.com/docs/guides/safety-best-practices
-- is: https://platform.openai.com/docs/guides/function-calling
+- startsWith: https://platform.openai.com/docs
+- startsWith: https://openai.com
- is: https://global.rel.tunnels.api.visualstudio.com/api/version
- is: https://www.wireguard.com/quickstart/
- is: https://docs.openstack.org/horizon/latest/
@@ -85,8 +85,6 @@
- is: https://jsonformatter.org/
- is: https://mvnrepository.com/artifact/org.xwiki.platform/xwiki-platform-oldcore
- is: https://mvnrepository.com/artifact/com.google.guava/guava
-- startsWith: https://platform.openai.com/docs/models
-- startsWith: https://openai.com/index
- is: https://github.com/github-linguist/linguist/compare/master...octocat:master
- is: https://www.servicenow.com/docs/bundle/utah-devops/page/product/enterprise-dev-ops/concept/github-integration-dev-ops.html
- startsWith: https://www.ilo.org
@@ -95,8 +93,7 @@
- is: https://www.nongnu.org/oath-toolkit/man-oathtool.html
- is: https://www.gnu.org/software/emacs/
- is: https://www.transparency.org/what-is-corruption
-- startsWith: https://platform.openai.com/docs/api-reference/
-- is: https://azuredownloads-g3ahgwb5b8bkbxhd.b01.azurefd.net/github-copilot/
+- startsWith: https://azuredownloads-g3ahgwb5b8bkbxhd.b01.azurefd.net/github-copilot/
- is: https://www.anthropic.com/claude/sonnet
- is: https://www.psiexams.com/become-psi-test-center/computer-specifications/
- is: https://www.buymeacoffee.com/
@@ -115,3 +112,21 @@
- is: https://collectd.org/documentation/manpages/collectd.conf.html#plugin-fhcount
- is: https://mywiki.wooledge.org/BashPitfalls
- startsWith: https://code.visualstudio.com/docs/configure/telemetry
+
+# npmjs.com blocks automated link checkers with 403.
+- startsWith: https://www.npmjs.com
+
+# Azure Marketplace blocks automated link checkers with 403.
+- startsWith: https://azuremarketplace.microsoft.com
+
+# Splunk docs blocks automated link checkers with 403.
+- startsWith: https://docs.splunk.com
+
+# HashiCorp rate-limits automated requests (429).
+- startsWith: https://www.hashicorp.com
+
+# ISO website blocks automated link checkers with 403.
+- startsWith: https://www.iso.org
+
+# Example domain used in style guide documentation.
+- startsWith: https://some-docs.com
diff --git a/src/links/lib/extract-links.ts b/src/links/lib/extract-links.ts
index c2e973736cea..d6a5c79f32a1 100644
--- a/src/links/lib/extract-links.ts
+++ b/src/links/lib/extract-links.ts
@@ -16,7 +16,8 @@ import type { Context, Page } from '@/types'
// Link patterns for Markdown
const INTERNAL_LINK_PATTERN = /\]\(\/[^)]+\)/g
const AUTOTITLE_LINK_PATTERN = /\[AUTOTITLE\]\(([^)]+)\)/g
-const EXTERNAL_LINK_PATTERN = /\]\((https?:\/\/[^)]+)\)/g
+// Handles one level of balanced parentheses in URLs (e.g., Wikipedia links)
+const EXTERNAL_LINK_PATTERN = /\]\((https?:\/\/(?:[^()\s]+|\([^()]*\))*)\)/g
const IMAGE_LINK_PATTERN = /!\[[^\]]*\]\(([^)]+)\)/g
// Anchor link patterns (for same-page links)
@@ -82,10 +83,19 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
const anchorLinks: ExtractedLink[] = []
const imageLinks: ExtractedLink[] = []
+ // Strip fenced code blocks to avoid checking example/placeholder URLs
+ // Replaces non-newline characters with spaces to preserve line numbers and positions
+ const strippedContent = content.replace(
+ /^ {0,3}(`{3,})[^\n]*\n[\s\S]*?^ {0,3}\1\s*$/gm,
+ (match) => {
+ return match.replace(/[^\n]/g, ' ')
+ },
+ )
+
// Extract AUTOTITLE links first (they're a special case of internal links)
let match
- while ((match = AUTOTITLE_LINK_PATTERN.exec(content)) !== null) {
- const { line, column } = getLineAndColumn(content, match.index)
+ while ((match = AUTOTITLE_LINK_PATTERN.exec(strippedContent)) !== null) {
+ const { line, column } = getLineAndColumn(strippedContent, match.index)
const href = match[1].split('#')[0] // Remove anchor if present
if (href.startsWith('/')) {
internalLinks.push({
@@ -102,17 +112,17 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
AUTOTITLE_LINK_PATTERN.lastIndex = 0
// Extract regular internal links
- while ((match = INTERNAL_LINK_PATTERN.exec(content)) !== null) {
+ while ((match = INTERNAL_LINK_PATTERN.exec(strippedContent)) !== null) {
// Skip if this is an AUTOTITLE link (already captured)
const fullMatch = match[0]
- if (content.substring(match.index - 10, match.index).includes('AUTOTITLE')) {
+ if (strippedContent.substring(match.index - 10, match.index).includes('AUTOTITLE')) {
continue
}
- const { line, column } = getLineAndColumn(content, match.index)
+ const { line, column } = getLineAndColumn(strippedContent, match.index)
// Extract href from ](/path) format
const href = fullMatch.substring(2, fullMatch.length - 1).split('#')[0]
- const text = extractLinkText(content, match.index)
+ const text = extractLinkText(strippedContent, match.index)
internalLinks.push({
href,
@@ -127,10 +137,10 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
INTERNAL_LINK_PATTERN.lastIndex = 0
// Extract external links
- while ((match = EXTERNAL_LINK_PATTERN.exec(content)) !== null) {
- const { line, column } = getLineAndColumn(content, match.index)
+ while ((match = EXTERNAL_LINK_PATTERN.exec(strippedContent)) !== null) {
+ const { line, column } = getLineAndColumn(strippedContent, match.index)
const href = match[1]
- const text = extractLinkText(content, match.index)
+ const text = extractLinkText(strippedContent, match.index)
externalLinks.push({
href,
@@ -144,8 +154,8 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
EXTERNAL_LINK_PATTERN.lastIndex = 0
// Extract anchor links
- while ((match = ANCHOR_LINK_PATTERN.exec(content)) !== null) {
- const { line, column } = getLineAndColumn(content, match.index)
+ while ((match = ANCHOR_LINK_PATTERN.exec(strippedContent)) !== null) {
+ const { line, column } = getLineAndColumn(strippedContent, match.index)
const href = match[0].substring(2, match[0].length - 1)
anchorLinks.push({
@@ -160,8 +170,8 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
ANCHOR_LINK_PATTERN.lastIndex = 0
// Extract image links
- while ((match = IMAGE_LINK_PATTERN.exec(content)) !== null) {
- const { line, column } = getLineAndColumn(content, match.index)
+ while ((match = IMAGE_LINK_PATTERN.exec(strippedContent)) !== null) {
+ const { line, column } = getLineAndColumn(strippedContent, match.index)
const href = match[1]
// Only include internal images (starting with /)
@@ -345,6 +355,19 @@ export function checkInternalLink(
}
}
+ // Strip language prefix and check redirects (which are stored without it)
+ const langPrefixMatch = resolved.match(/^\/[a-z]{2}\//)
+ if (langPrefixMatch) {
+ const withoutLang = resolved.slice(langPrefixMatch[0].length - 1)
+ if (redirects[withoutLang]) {
+ return {
+ exists: true,
+ isRedirect: true,
+ redirectTarget: redirects[withoutLang],
+ }
+ }
+ }
+
return { exists: false, isRedirect: false }
}
diff --git a/src/links/lib/validate-docs-urls.ts b/src/links/lib/validate-docs-urls.ts
index 886dd43a5b15..fbcc7e265525 100644
--- a/src/links/lib/validate-docs-urls.ts
+++ b/src/links/lib/validate-docs-urls.ts
@@ -1,5 +1,5 @@
import type { Response } from 'express'
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
import warmServer from '@/frame/lib/warm-server'
import { liquid } from '@/content-render/index'
@@ -89,7 +89,7 @@ export async function validateDocsUrl(docsUrls: DocsUrls, { checkFragments = fal
if (checkFragments && fragment) {
const permalink = (redirectedPage || page).permalinks[0]
const html = await renderInnerHTML(redirectedPage || page, permalink)
- const $ = cheerio.load(html)
+ const $ = load(html)
check.fragmentFound = $(`#${fragment}`).length > 0 || $(`a[name="${fragment}"]`).length > 0
if (!check.fragmentFound) {
const fragmentCandidates: string[] = []
diff --git a/src/links/scripts/check-links-internal.ts b/src/links/scripts/check-links-internal.ts
index 12a9c7ad98b9..a7a1b9c0da28 100644
--- a/src/links/scripts/check-links-internal.ts
+++ b/src/links/scripts/check-links-internal.ts
@@ -20,7 +20,7 @@
import { program } from 'commander'
import chalk from 'chalk'
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
import warmServer from '@/frame/lib/warm-server'
import { renderContent } from '@/content-render/index'
@@ -73,7 +73,7 @@ async function getLinksFromRenderedPage(
try {
// Render the page content
const html = await renderContent(page.markdown, context)
- const $ = cheerio.load(html)
+ const $ = load(html)
// Extract all anchor links
$('a[href]').each((_, el) => {
@@ -103,7 +103,7 @@ async function checkAnchorsOnPage(
try {
const html = await renderContent(page.markdown, context)
- const $ = cheerio.load(html)
+ const $ = load(html)
// Find all anchor links (same-page links)
$('a[href^="#"]').each((_, el) => {
diff --git a/src/links/tests/extract-links.ts b/src/links/tests/extract-links.ts
index eb75e6bde958..6465ae4c7b01 100644
--- a/src/links/tests/extract-links.ts
+++ b/src/links/tests/extract-links.ts
@@ -140,6 +140,75 @@ Also [versioned](/enterprise-server@{{ currentVersion }}/admin).
expect(result.internalLinks.length).toBeGreaterThanOrEqual(0)
})
+ test('extracts external links with parentheses in URLs', () => {
+ const content = `
+See the [shebang article](https://en.wikipedia.org/wiki/Shebang_(Unix)) for more.
+Also [Continuum](https://en.wikipedia.org/wiki/Continuum_(measurement)) is relevant.
+`
+ const result = extractLinksFromMarkdown(content)
+
+ expect(result.externalLinks).toHaveLength(2)
+ expect(result.externalLinks[0].href).toBe('https://en.wikipedia.org/wiki/Shebang_(Unix)')
+ expect(result.externalLinks[1].href).toBe(
+ 'https://en.wikipedia.org/wiki/Continuum_(measurement)',
+ )
+ })
+
+ test('skips links inside fenced code blocks', () => {
+ const content = `
+Here is [a real link](https://example.com).
+
+\`\`\`yaml
+
+[example](https://fake-example.com/not-real)
+\`\`\`
+
+And [another real link](https://real.example.com/page).
+`
+ const result = extractLinksFromMarkdown(content)
+
+ expect(result.externalLinks).toHaveLength(2)
+ expect(result.externalLinks[0].href).toBe('https://example.com')
+ expect(result.externalLinks[1].href).toBe('https://real.example.com/page')
+ })
+
+ test('preserves correct line numbers when code blocks are stripped', () => {
+ const content = `Line 1
+[Link on line 2](/path/one)
+\`\`\`
+code block on line 3
+code block on line 4
+\`\`\`
+Line 6
+[Link on line 8](/path/two)
+`
+ const result = extractLinksFromMarkdown(content)
+
+ expect(result.internalLinks).toHaveLength(2)
+ expect(result.internalLinks[0].line).toBe(2)
+ // Line numbers are preserved because code block content is replaced with spaces
+ expect(result.internalLinks[1].line).toBe(8)
+ })
+
+ test('skips links inside indented fenced code blocks', () => {
+ const content = `
+Here is [a real link](https://example.com).
+
+1. Step one:
+
+ \`\`\`yaml
+ [example](https://fake-example.com/not-real)
+ \`\`\`
+
+And [another real link](https://real.example.com/page).
+`
+ const result = extractLinksFromMarkdown(content)
+
+ expect(result.externalLinks).toHaveLength(2)
+ expect(result.externalLinks[0].href).toBe('https://example.com')
+ expect(result.externalLinks[1].href).toBe('https://real.example.com/page')
+ })
+
test('handles complex nested brackets', () => {
const content = `
Use the [\`git clone\`](/repositories/cloning) command.
@@ -186,6 +255,8 @@ describe('checkInternalLink', () => {
const redirects = {
'/en/old-path': '/en/new-path',
'/en/deprecated': '/en/current',
+ '/enterprise-server@3.19/actions/old-path': '/enterprise-server@3.19/actions/new-path',
+ '/actions/legacy-path': '/actions/current-path',
}
test('finds direct page match', () => {
@@ -233,6 +304,25 @@ describe('checkInternalLink', () => {
const result = checkInternalLink('/enterprise-server@latest/does/not/exist', pageMap, redirects)
expect(result.exists).toBe(false)
})
+
+ test('finds redirect after stripping language prefix', () => {
+ // Links from rendered HTML have /en/ prefix but redirects are stored without it
+ const result = checkInternalLink(
+ '/en/enterprise-server@3.19/actions/old-path',
+ pageMap,
+ redirects,
+ )
+ expect(result.exists).toBe(true)
+ expect(result.isRedirect).toBe(true)
+ expect(result.redirectTarget).toBe('/enterprise-server@3.19/actions/new-path')
+ })
+
+ test('finds versionless redirect after stripping language prefix', () => {
+ const result = checkInternalLink('/en/actions/legacy-path', pageMap, redirects)
+ expect(result.exists).toBe(true)
+ expect(result.isRedirect).toBe(true)
+ expect(result.redirectTarget).toBe('/actions/current-path')
+ })
})
describe('isAssetLink', () => {
diff --git a/src/search/tests/topics.ts b/src/search/tests/topics.ts
index f7a62ce02ffd..023bedff4294 100644
--- a/src/search/tests/topics.ts
+++ b/src/search/tests/topics.ts
@@ -21,7 +21,8 @@ const topics: string[] = walk(contentDir, { includeBasePath: true })
throw new Error(`More than 0 front-matter errors in file: ${filename}`)
}
- return (data as any).topics || []
+ const pageTopics = (data as Record).topics
+ return Array.isArray(pageTopics) ? (pageTopics as string[]) : []
})
.flat()
diff --git a/src/tests/helpers/e2etest.ts b/src/tests/helpers/e2etest.ts
index 69224a2ecd41..85f66ad70412 100644
--- a/src/tests/helpers/e2etest.ts
+++ b/src/tests/helpers/e2etest.ts
@@ -1,4 +1,4 @@
-import cheerio from 'cheerio'
+import { load, type CheerioAPI } from 'cheerio'
import { fetchWithRetry } from '@/frame/lib/fetch-utils'
import { omitBy, isUndefined } from 'lodash-es'
@@ -35,7 +35,7 @@ interface ResponseWithHeaders {
}
// Type alias for cached DOM results to improve maintainability
-type CachedDOMResult = cheerio.Root & { res: ResponseWithHeaders; $: cheerio.Root }
+type CachedDOMResult = CheerioAPI & { res: ResponseWithHeaders; $: CheerioAPI }
// Cache to store DOM objects
const getDOMCache = new Map()
@@ -174,7 +174,7 @@ export async function getDOM(route: string, options: GetDOMOptions = {}): Promis
throw new Error(`Page not found on ${route} (${res.statusCode})`)
}
- const $ = cheerio.load(res.body || '', { xmlMode: true })
+ const $ = load(res.body || '', { xmlMode: true })
const result = $ as CachedDOMResult
// Attach res to the cheerio object for backward compatibility
result.res = res
diff --git a/src/tests/helpers/script-data.ts b/src/tests/helpers/script-data.ts
index f28f6b7c7ad1..f3bef288faa3 100644
--- a/src/tests/helpers/script-data.ts
+++ b/src/tests/helpers/script-data.ts
@@ -1,9 +1,9 @@
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
const NEXT_DATA_QUERY = 'script#__NEXT_DATA__'
const PRIMER_DATA_QUERY = 'script#__PRIMER_DATA__'
-function getScriptData($: ReturnType, key: string): unknown {
+function getScriptData($: CheerioAPI, key: string): unknown {
const data = $(key)
if (data.length !== 1) {
throw new Error(`Not exactly 1 element match for '${key}'. Found ${data.length}`)
@@ -18,7 +18,5 @@ function getScriptData($: ReturnType, key: string): unknown
throw new Error(`Could not extract data from '${key}'`)
}
-export const getNextData = ($: ReturnType): unknown =>
- getScriptData($, NEXT_DATA_QUERY)
-export const getPrimerData = ($: ReturnType): unknown =>
- getScriptData($, PRIMER_DATA_QUERY)
+export const getNextData = ($: CheerioAPI): unknown => getScriptData($, NEXT_DATA_QUERY)
+export const getPrimerData = ($: CheerioAPI): unknown => getScriptData($, PRIMER_DATA_QUERY)
diff --git a/src/tools/components/Picker.tsx b/src/tools/components/Picker.tsx
index e125576795f6..1d4fcf7bafdf 100644
--- a/src/tools/components/Picker.tsx
+++ b/src/tools/components/Picker.tsx
@@ -24,7 +24,10 @@ export interface PickerItem {
text: string
selected: boolean
extra?: {
- [key: string]: any
+ arrow?: boolean
+ info?: boolean
+ version?: string
+ currentDate?: string
}
divider?: boolean
}
diff --git a/src/workflows/experimental/readability-report.ts b/src/workflows/experimental/readability-report.ts
index 14f51d2b1078..26dc8155fbe6 100644
--- a/src/workflows/experimental/readability-report.ts
+++ b/src/workflows/experimental/readability-report.ts
@@ -38,7 +38,7 @@
import fs from 'fs'
import path from 'path'
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
import { fetchWithRetry } from '@/frame/lib/fetch-utils'
interface ReadabilityMetrics {
@@ -219,7 +219,7 @@ async function analyzeFile(filePath: string): Promise {
// Parse HTML and extract content
const body = await response.text()
- const $ = cheerio.load(body)
+ const $ = load(body)
// Get page title
const title = $('h1').first().text().trim() || $('title').text().trim() || 'Untitled'