docs: handle Card and CardList components in cross-links plugin (#7338)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
74
www/packages/remark-rehype-plugins/src/utils/estree-to-js.ts
Normal file
74
www/packages/remark-rehype-plugins/src/utils/estree-to-js.ts
Normal 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}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user