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
+}