docs: add line highlight validation (#14380)
* docs: add line highlight validation * add to all projects * fixes * fixes * fix * fixes
This commit is contained in:
1 parent
6380c1fdf4
commit
cb33388202
59 files changed
+514
-243
No files matched your search
@@ -175,7 +175,7 @@ function componentChecker({
|
||||
|
||||
const itemJsVar = estreeToJs(attribute.value.data.estree)
|
||||
|
||||
if (!itemJsVar) {
|
||||
if (!itemJsVar || "name" in itemJsVar) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ function checkLink({
|
||||
currentPageFilePath: string
|
||||
options: BrokenLinkCheckerOptions
|
||||
}) {
|
||||
if (!link || typeof link !== "string") {
|
||||
if (!link || typeof link !== "string" || link === "/" || link === "#") {
|
||||
return
|
||||
}
|
||||
// try to remove hash
|
||||
|
||||
@@ -88,7 +88,7 @@ function componentFixer(
|
||||
|
||||
const itemJsVar = estreeToJs(attribute.value.data.estree)
|
||||
|
||||
if (!itemJsVar) {
|
||||
if (!itemJsVar || "name" in itemJsVar) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from "./prerequisites-link-fixer.js"
|
||||
export * from "./resolve-admonitions.js"
|
||||
export * from "./type-list-link-fixer.js"
|
||||
export * from "./ui-rehype-plugin.js"
|
||||
export * from "./validate-highlights.js"
|
||||
export * from "./workflow-diagram-link-fixer.js"
|
||||
|
||||
export * from "./utils/fix-link.js"
|
||||
@@ -105,7 +105,7 @@ export function componentLinkFixer(
|
||||
|
||||
const itemJsVar = estreeToJs(attribute.value.data.estree)
|
||||
|
||||
if (!itemJsVar) {
|
||||
if (!itemJsVar || "name" in itemJsVar) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import {
|
||||
ArrayExpression,
|
||||
Estree,
|
||||
ExpressionJsVar,
|
||||
Identifier,
|
||||
JSXElementExpression,
|
||||
JSXExpressionContainer,
|
||||
LiteralExpression,
|
||||
UnistNode,
|
||||
UnistTree,
|
||||
} from "types"
|
||||
import type { Transformer } from "unified"
|
||||
import { estreeToJs, expressionToJs } from "docs-utils"
|
||||
|
||||
type Options = {
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export function validateHighlightsPlugin(options?: Options): Transformer {
|
||||
const { verbose } = options || {}
|
||||
return async (tree) => {
|
||||
const { visit } = await import("unist-util-visit")
|
||||
const highlightConsts = new Map<string, string[][]>()
|
||||
|
||||
visit(tree as UnistTree, ["mdxjsEsm", "element"], (node: UnistNode) => {
|
||||
if (node.type === "mdxjsEsm") {
|
||||
if (node.data && "estree" in node.data && node.data.estree) {
|
||||
const highlightsJsVar = estreeToJs(node.data.estree)
|
||||
|
||||
if (
|
||||
!highlightsJsVar ||
|
||||
!("name" in highlightsJsVar) ||
|
||||
typeof highlightsJsVar.name !== "string" ||
|
||||
!Array.isArray(highlightsJsVar.value)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const highlights = getHighlightsFromExpressionJsVar(
|
||||
highlightsJsVar.value
|
||||
)
|
||||
if (highlights) {
|
||||
highlightConsts.set(highlightsJsVar.name, highlights)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
node.tagName !== "pre" ||
|
||||
!node.children?.length ||
|
||||
!node.children[0].data ||
|
||||
!("estree" in node.children[0].data) ||
|
||||
!node.children[0].data.estree
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const body = (node.children[0].data.estree as Estree).body?.[0]
|
||||
|
||||
if (
|
||||
!body ||
|
||||
body.type !== "ExpressionStatement" ||
|
||||
!body.expression ||
|
||||
body.expression.type !== "JSXElement"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const bodyExpression = body.expression as JSXElementExpression
|
||||
|
||||
if (
|
||||
!bodyExpression.children.length ||
|
||||
bodyExpression.children[0].type !== "JSXExpressionContainer"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const codeExpression = (
|
||||
bodyExpression.children[0] as JSXExpressionContainer
|
||||
).expression
|
||||
|
||||
if (codeExpression.type !== "Literal") {
|
||||
return
|
||||
}
|
||||
|
||||
const highlightsAttr = bodyExpression.openingElement?.attributes.find(
|
||||
(attr) => attr.name.name === "highlights"
|
||||
)
|
||||
|
||||
if (!highlightsAttr) {
|
||||
return
|
||||
}
|
||||
|
||||
let highlights: string[][] | undefined
|
||||
if (
|
||||
highlightsAttr.value.expression.type !== "Identifier" &&
|
||||
highlightsAttr.value.expression.type === "ArrayExpression"
|
||||
) {
|
||||
const highlightsJs = getExpressionJsVarFromArrExpression(
|
||||
highlightsAttr.value.expression as ArrayExpression
|
||||
)
|
||||
if (!highlightsJs || !Array.isArray(highlightsJs)) {
|
||||
return
|
||||
}
|
||||
highlights = getHighlightsFromExpressionJsVar(highlightsJs)
|
||||
|
||||
if (!highlights) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (highlightsAttr.value.expression.type !== "Identifier") {
|
||||
return
|
||||
}
|
||||
const name = (highlightsAttr.value.expression as Identifier).name
|
||||
highlights = highlightConsts.get(name)
|
||||
|
||||
if (!highlights) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const code = (codeExpression as LiteralExpression).value as string
|
||||
|
||||
validateCodeHighlights({
|
||||
highlights,
|
||||
code,
|
||||
verbose,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getExpressionJsVarFromArrExpression = (
|
||||
expression: ArrayExpression
|
||||
): ExpressionJsVar[][] => {
|
||||
if (!Array.isArray(expression.elements)) {
|
||||
return []
|
||||
}
|
||||
return expression.elements.map((elm) => {
|
||||
const elmJsVar = expressionToJs(elm)
|
||||
if (!elmJsVar || !Array.isArray(elmJsVar)) {
|
||||
return []
|
||||
}
|
||||
return elmJsVar
|
||||
})
|
||||
}
|
||||
|
||||
const getHighlightsFromExpressionJsVar = (
|
||||
expression: ExpressionJsVar[] | ExpressionJsVar[][]
|
||||
): string[][] | undefined => {
|
||||
const highlights: string[][] = []
|
||||
const shouldExtractHighlights = !Array.isArray(expression[0])
|
||||
// the expression is an array where each three items are a highlight
|
||||
// so we need to split the expression into groups of three items first
|
||||
const highlightExpressions = shouldExtractHighlights
|
||||
? (expression as ExpressionJsVar[]).reduce((acc, item, index) => {
|
||||
if (index % 3 === 0) {
|
||||
acc.push([])
|
||||
}
|
||||
acc[acc.length - 1].push(item)
|
||||
return acc
|
||||
}, [] as ExpressionJsVar[][])
|
||||
: (expression as ExpressionJsVar[][])
|
||||
|
||||
if (!highlightExpressions.length) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
for (const highlightExpression of highlightExpressions) {
|
||||
if (highlightExpression.length !== 3) {
|
||||
continue
|
||||
}
|
||||
|
||||
const highlight = []
|
||||
for (const item of highlightExpression) {
|
||||
if (typeof item.data !== "string") {
|
||||
continue
|
||||
}
|
||||
highlight.push(item.data)
|
||||
}
|
||||
highlights.push(highlight)
|
||||
}
|
||||
|
||||
return !highlights.length ? undefined : highlights
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the highlights of each code block are correct. It validates:
|
||||
*
|
||||
* 1. The highlight's first parameter is a string number of a valid line number in the code block.
|
||||
* 2. The highlight's second parameter is a string that is a valid text in the line of the code block.
|
||||
*/
|
||||
const validateCodeHighlights = ({
|
||||
highlights,
|
||||
code,
|
||||
verbose,
|
||||
}: {
|
||||
highlights: string[][]
|
||||
code: string
|
||||
verbose?: boolean
|
||||
}): void => {
|
||||
try {
|
||||
const lines = code.trim().split("\n")
|
||||
for (const highlight of highlights) {
|
||||
if (highlight.length < 2) {
|
||||
throw new Error(`Highlight array must have at least 2 elements`)
|
||||
}
|
||||
|
||||
const lineNumber = highlight[0]
|
||||
const text = highlight[1]
|
||||
|
||||
if (typeof lineNumber !== "string" || typeof text !== "string") {
|
||||
throw new Error(`Highlight array must have at least 2 elements`)
|
||||
}
|
||||
|
||||
if (isNaN(Number(lineNumber))) {
|
||||
throw new Error(`Highlight line number must be a number`)
|
||||
}
|
||||
|
||||
const line = lines[Number(lineNumber) - 1]?.trim()
|
||||
if (line === undefined) {
|
||||
throw new Error(`Highlight line number ${lineNumber} not found in code`)
|
||||
}
|
||||
|
||||
if (text.length && !line.includes(text)) {
|
||||
throw new Error(
|
||||
`Highlight text ${text} not found in line ${lineNumber}`
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
formatError({
|
||||
message: (err as Error).message,
|
||||
code,
|
||||
verbose,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const formatError = ({
|
||||
message,
|
||||
code,
|
||||
verbose,
|
||||
}: {
|
||||
message: string
|
||||
code: string
|
||||
verbose?: boolean
|
||||
}): string => {
|
||||
return `${message}${verbose ? `:\n${code}` : ""}`
|
||||
}
|
||||
Reference in new issue
Block a user