docs: handle Card and CardList components in cross-links plugin (#7338)

This commit is contained in:
Shahed Nasser
2024-05-16 09:40:42 +03:00
committed by GitHub
parent 490586f566
commit 1160a34f3d
6 changed files with 361 additions and 113 deletions

View File

@@ -1,45 +1,142 @@
/* eslint-disable no-case-declarations */
import type { Transformer } from "unified"
import type {
CrossProjectLinksOptions,
ExpressionJsVar,
UnistNode,
UnistNodeWithData,
UnistTree,
} from "./types/index.js"
import { estreeToJs } from "./utils/estree-to-js.js"
import getAttribute from "./utils/get-attribute.js"
import {
isExpressionJsVarLiteral,
isExpressionJsVarObj,
} from "./utils/expression-is-utils.js"
const PROJECT_REGEX = /^!(?<area>[\w-]+)!/
export function crossProjectLinksPlugin({
baseUrl,
projectUrls,
}: CrossProjectLinksOptions): Transformer {
return async (tree) => {
const { visit } = await import("unist-util-visit")
visit(tree as UnistTree, "element", (node: UnistNode) => {
if (node.tagName !== "a" || !node.properties?.href) {
function matchAndFixLinks(
link: string,
{ baseUrl, projectUrls }: CrossProjectLinksOptions
): string {
const projectArea = PROJECT_REGEX.exec(link)
if (!projectArea?.groups?.area) {
return link
}
const actualUrl = link.replace(PROJECT_REGEX, "")
const base =
projectUrls &&
Object.hasOwn(projectUrls, projectArea.groups.area) &&
projectUrls[projectArea.groups.area]?.url
? projectUrls[projectArea.groups.area].url
: baseUrl
const path =
projectUrls &&
Object.hasOwn(projectUrls, projectArea.groups.area) &&
projectUrls[projectArea.groups.area]?.path
? projectUrls[projectArea.groups.area].path
: projectArea.groups.area
return `${base}/${path}${actualUrl}`
}
function linkElmFixer(node: UnistNode, options: CrossProjectLinksOptions) {
if (!node.properties) {
return
}
node.properties.href = matchAndFixLinks(node.properties.href, options)
}
function componentFixer(
node: UnistNodeWithData,
options: CrossProjectLinksOptions
) {
if (!node.name) {
return
}
switch (node.name) {
case "CardList":
const itemsAttribute = getAttribute(node, "items")
if (
!itemsAttribute?.value ||
typeof itemsAttribute.value === "string" ||
!itemsAttribute.value.data?.estree
) {
return
}
const projectArea = PROJECT_REGEX.exec(node.properties.href)
const jsVar = estreeToJs(itemsAttribute.value.data.estree)
if (!projectArea?.groups?.area) {
if (!jsVar) {
return
}
const actualUrl = node.properties.href.replace(PROJECT_REGEX, "")
const fixProperty = (item: ExpressionJsVar) => {
if (!isExpressionJsVarObj(item)) {
return
}
const base =
projectUrls &&
Object.hasOwn(projectUrls, projectArea.groups.area) &&
projectUrls[projectArea.groups.area]?.url
? projectUrls[projectArea.groups.area].url
: baseUrl
const path =
projectUrls &&
Object.hasOwn(projectUrls, projectArea.groups.area) &&
projectUrls[projectArea.groups.area]?.path
? projectUrls[projectArea.groups.area].path
: projectArea.groups.area
Object.entries(item).forEach(([key, value]) => {
if (key !== "href" || !isExpressionJsVarLiteral(value)) {
return
}
node.properties.href = `${base}/${path}${actualUrl}`
})
value.original.value = matchAndFixLinks(
value.original.value as string,
options
)
value.original.raw = JSON.stringify(value.original.value)
})
}
if (Array.isArray(jsVar)) {
jsVar.forEach(fixProperty)
} else {
fixProperty(jsVar)
}
return
case "Card":
const hrefAttribute = getAttribute(node, "href")
if (!hrefAttribute?.value || typeof hrefAttribute.value !== "string") {
return
}
hrefAttribute.value = matchAndFixLinks(hrefAttribute.value, options)
return
}
}
export function crossProjectLinksPlugin(
options: CrossProjectLinksOptions
): Transformer {
return async (tree) => {
const { visit } = await import("unist-util-visit")
visit(
tree as UnistTree,
["element", "mdxJsxFlowElement"],
(node: UnistNode) => {
const isComponent = node.name === "Card" || node.name === "CardList"
const isLink = node.tagName === "a" && node.properties?.href
if (!isComponent && !isLink) {
return
}
if (isComponent) {
componentFixer(node as UnistNodeWithData, options)
}
linkElmFixer(node, options)
}
)
}
}

View File

@@ -1,12 +1,18 @@
import path from "path"
import { Transformer } from "unified"
import {
ExpressionJsVar,
TypeListLinkFixerOptions,
UnistNode,
UnistNodeWithData,
UnistTree,
} from "./types/index.js"
import { FixLinkOptions, fixLinkUtil } from "./index.js"
import getAttribute from "./utils/get-attribute.js"
import { estreeToJs } from "./utils/estree-to-js.js"
import {
isExpressionJsVarLiteral,
isExpressionJsVarObj,
} from "./utils/expression-is-utils.js"
const LINK_REGEX = /\[(.*?)\]\((?<link>.*?)\)/gm
@@ -32,24 +38,34 @@ function matchLinks(
}
function traverseTypes(
types: Record<string, unknown>[],
types: ExpressionJsVar[] | ExpressionJsVar,
linkOptions: Omit<FixLinkOptions, "linkedPath">
) {
return types.map((typeItem) => {
typeItem.type = matchLinks(typeItem.type as string, linkOptions)
typeItem.description = matchLinks(
typeItem.description as string,
if (Array.isArray(types)) {
types.forEach((item) => traverseTypes(item, linkOptions))
} else if (isExpressionJsVarLiteral(types)) {
types.original.value = matchLinks(
types.original.value as string,
linkOptions
)
if (typeItem.children) {
typeItem.children = traverseTypes(
typeItem.children as Record<string, unknown>[],
types.original.raw = JSON.stringify(types.original.value)
} else {
Object.values(types).forEach((value) => {
if (Array.isArray(value) || isExpressionJsVarObj(value)) {
return traverseTypes(value, linkOptions)
}
if (!isExpressionJsVarLiteral(value)) {
return
}
value.original.value = matchLinks(
value.original.value as string,
linkOptions
)
}
return typeItem
})
value.original.raw = JSON.stringify(value.original.value)
})
}
}
export function typeListLinkFixerPlugin(
@@ -76,22 +92,17 @@ export function typeListLinkFixerPlugin(
""
)
const appsPath = basePath || path.join(file.cwd, "app")
visit(tree as UnistTree, "mdxJsxFlowElement", (node: UnistNode) => {
visit(tree as UnistTree, "mdxJsxFlowElement", (node: UnistNodeWithData) => {
if (node.name !== "TypeList") {
return
}
const typesAttributeIndex = node.attributes?.findIndex(
(attribute) => attribute.name === "types"
)
if (typesAttributeIndex === undefined || typesAttributeIndex === -1) {
return
}
const typesAttribute = node.attributes![typesAttributeIndex]
const typesAttribute = getAttribute(node, "types")
if (
!typesAttribute ||
!(typesAttribute.value as Record<string, unknown>)?.value
typeof typesAttribute.value === "string" ||
!typesAttribute.value.data?.estree
) {
return
}
@@ -101,56 +112,15 @@ export function typeListLinkFixerPlugin(
appsPath,
}
let newItems: Record<string, unknown>[]
// let newItems: Record<string, unknown>[]
try {
newItems = traverseTypes(
JSON.parse(
(typesAttribute.value as Record<string, unknown>).value as string
) as Record<string, unknown>[],
linkOptions
)
} catch (e) {
// eslint-disable-next-line no-console
console.log(
`[type-list-link-fixer-plugin]: An error occurred while parsing items for page ${file.history[0]}: ${e}`
)
const typesJsVar = estreeToJs(typesAttribute.value.data.estree)
if (!typesJsVar) {
return
}
;(
node.attributes![typesAttributeIndex].value as Record<string, unknown>
).value = JSON.stringify(newItems)
if (
(node as UnistNodeWithData).attributes![typesAttributeIndex].value?.data
?.estree?.body?.length
) {
const oldItems = (node as UnistNodeWithData).attributes[
typesAttributeIndex
].value.data!.estree!.body![0].expression!.elements!
;(node as UnistNodeWithData).attributes[
typesAttributeIndex
].value.data!.estree!.body![0].expression!.elements = newItems.map(
(newItem, index) => {
oldItems[index].properties = oldItems[index].properties.map(
(property) => {
if (Object.hasOwn(newItem, property.key.value)) {
property.value.value = newItem[property.key.value]
property.value.raw = JSON.stringify(
newItem[property.key.value]
)
}
return property
}
)
return oldItems[index]
}
)
}
traverseTypes(typesJsVar, linkOptions)
})
}
}

View File

@@ -16,38 +16,70 @@ export interface UnistNode extends Node {
children?: UnistNode[]
}
export type ArrayExpression = {
type: "ArrayExpression"
elements: Expression[]
}
export type ObjectExpression = {
type: "ObjectExpression"
properties: AttributeProperty[]
}
export type LiteralExpression = {
type: "Literal"
value: unknown
raw: string
}
export type Expression =
| {
type: string
}
| ArrayExpression
| ObjectExpression
| LiteralExpression
export interface Estree {
body?: {
type?: string
expression?: Expression
}[]
}
export interface UnistNodeWithData extends UnistNode {
attributes: {
name: string
value: {
data?: {
estree?: {
body?: {
type?: string
expression?: {
type?: string
elements?: {
properties: AttributeProperty[]
}[]
}
}[]
value:
| {
data?: {
estree?: Estree
}
value?: string
}
}
value?: string
}
| string
type?: string
}[]
}
export interface AttributeProperty {
key: {
value: string
raw: string
}
value: {
value: unknown
name?: string
value?: string
raw: string
}
value:
| {
type: "Literal"
value: unknown
raw: string
}
| {
type: "JSXElement"
// TODO add correct type if necessary
openingElement: unknown
}
| ArrayExpression
}
export interface UnistTree extends Node {
@@ -94,3 +126,23 @@ export declare type LocalLinkOptions = {
filePath?: string
basePath?: string
}
export type ExpressionJsVarItem = {
original: AttributeProperty
data?: unknown
}
export type ExpressionJsVarLiteral = {
original: {
type: "Literal"
value: unknown
raw: string
}
data?: unknown
}
export type ExpressionJsVarObj = {
[k: string]: ExpressionJsVarItem | ExpressionJsVar | ExpressionJsVar[]
}
export type ExpressionJsVar = ExpressionJsVarObj | ExpressionJsVarLiteral

View File

@@ -0,0 +1,74 @@
/* eslint-disable no-case-declarations */
import {
ArrayExpression,
Estree,
Expression,
ExpressionJsVar,
ExpressionJsVarLiteral,
LiteralExpression,
ObjectExpression,
} from "../types/index.js"
export function estreeToJs(estree: Estree) {
// TODO improve on this utility. Currently it's implemented to work
// for specific use cases as we don't have a lot of info on other
// use cases.
if (
!estree.body?.length ||
estree.body[0].type !== "ExpressionStatement" ||
!estree.body[0].expression
) {
return
}
return expressionToJs(estree.body[0].expression)
}
function expressionToJs(
expression: Expression
): ExpressionJsVar | ExpressionJsVar[] | undefined {
switch (expression.type) {
case "ArrayExpression":
const arrVar: ExpressionJsVar[] = []
;(expression as ArrayExpression).elements.forEach((elm) => {
const elmJsVar = expressionToJs(elm)
if (!elmJsVar) {
return
}
if (Array.isArray(elmJsVar)) {
arrVar.push(...elmJsVar)
} else {
arrVar.push(elmJsVar)
}
})
return arrVar
case "ObjectExpression":
const objVar: ExpressionJsVar = {}
;(expression as ObjectExpression).properties.forEach((property) => {
const keyName = property.key.name ?? property.key.value
if (!keyName) {
return
}
const jsVal = expressionToJs(property.value)
if (!jsVal) {
return
}
objVar[keyName] = jsVal
})
return objVar
case "Literal":
return {
original: expression,
data: (expression as LiteralExpression).value,
} as ExpressionJsVarLiteral
case "JSXElement":
// ignore JSXElements
return
default:
console.warn(
`[expressionToJs] can't parse expression of type ${expression.type}`
)
}
}

View File

@@ -0,0 +1,35 @@
import {
ExpressionJsVarItem,
ExpressionJsVarLiteral,
ExpressionJsVarObj,
} from "../types/index.js"
export function isExpressionJsVarLiteral(
expression: unknown
): expression is ExpressionJsVarLiteral {
return (
typeof expression === "object" &&
expression !== null &&
Object.hasOwn(expression, "original")
)
}
export function isExpressionJsVarObj(
expression: unknown
): expression is ExpressionJsVarObj {
return (
typeof expression === "object" &&
expression !== null &&
!Object.hasOwn(expression, "original")
)
}
export function isExpressionJsVarItem(
expression: unknown
): expression is ExpressionJsVarItem {
return (
typeof expression === "object" &&
expression !== null &&
Object.hasOwn(expression, "original")
)
}

View File

@@ -0,0 +1,20 @@
import { UnistNodeWithData } from "../types/index.js"
export default function getAttribute(
node: UnistNodeWithData,
attrName: string
) {
const attributeIndex = node.attributes?.findIndex(
(attribute) => attribute.name === attrName
)
if (attributeIndex === undefined || attributeIndex === -1) {
return
}
const attribute = node.attributes![attributeIndex]
if (!attribute) {
return
}
return attribute
}