diff --git a/www/packages/remark-rehype-plugins/src/cross-project-links.ts b/www/packages/remark-rehype-plugins/src/cross-project-links.ts index 8b625489ee93f..b6bd66122e42b 100644 --- a/www/packages/remark-rehype-plugins/src/cross-project-links.ts +++ b/www/packages/remark-rehype-plugins/src/cross-project-links.ts @@ -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 = /^!(?[\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 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 - - node.properties.href = `${base}/${path}${actualUrl}` - }) + const fixProperty = (item: ExpressionJsVar) => { + if (!isExpressionJsVarObj(item)) { + return + } + + Object.entries(item).forEach(([key, value]) => { + if (key !== "href" || !isExpressionJsVarLiteral(value)) { + return + } + + 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) + } + ) } } diff --git a/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts b/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts index a56701b693332..494dca0039b76 100644 --- a/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts +++ b/www/packages/remark-rehype-plugins/src/type-list-link-fixer.ts @@ -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 = /\[(.*?)\]\((?.*?)\)/gm @@ -32,24 +38,34 @@ function matchLinks( } function traverseTypes( - types: Record[], + types: ExpressionJsVar[] | ExpressionJsVar, linkOptions: Omit ) { - 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[], + 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)?.value + typeof typesAttribute.value === "string" || + !typesAttribute.value.data?.estree ) { return } @@ -101,56 +112,15 @@ export function typeListLinkFixerPlugin( appsPath, } - let newItems: Record[] - - try { - newItems = traverseTypes( - JSON.parse( - (typesAttribute.value as Record).value as string - ) as Record[], - 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}` - ) - return - } + // let newItems: Record[] - ;( - node.attributes![typesAttributeIndex].value as Record - ).value = JSON.stringify(newItems) + const typesJsVar = estreeToJs(typesAttribute.value.data.estree) - 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] - } - ) + if (!typesJsVar) { + return } + + traverseTypes(typesJsVar, linkOptions) }) } } diff --git a/www/packages/remark-rehype-plugins/src/types/index.ts b/www/packages/remark-rehype-plugins/src/types/index.ts index ab9b8e534078e..6df171b4df48b 100644 --- a/www/packages/remark-rehype-plugins/src/types/index.ts +++ b/www/packages/remark-rehype-plugins/src/types/index.ts @@ -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 diff --git a/www/packages/remark-rehype-plugins/src/utils/estree-to-js.ts b/www/packages/remark-rehype-plugins/src/utils/estree-to-js.ts new file mode 100644 index 0000000000000..5299fdd8b1944 --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/utils/estree-to-js.ts @@ -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}` + ) + } +} diff --git a/www/packages/remark-rehype-plugins/src/utils/expression-is-utils.ts b/www/packages/remark-rehype-plugins/src/utils/expression-is-utils.ts new file mode 100644 index 0000000000000..0c833320a3fc9 --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/utils/expression-is-utils.ts @@ -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") + ) +} diff --git a/www/packages/remark-rehype-plugins/src/utils/get-attribute.ts b/www/packages/remark-rehype-plugins/src/utils/get-attribute.ts new file mode 100644 index 0000000000000..a50abe2fb9674 --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/utils/get-attribute.ts @@ -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 +}