Files
medusa-store/www/packages/docs-utils/src/utils/parsers.ts
Shahed Nasser f81eb51b67 docs: add copy subscriber button (#12405)
* docs: add copy subscriber button

* re-generate

* fixes + update copy button
2025-05-08 12:48:44 +03:00

1041 lines
23 KiB
TypeScript

import { ExpressionJsVar, UnistNode, UnistNodeWithData, UnistTree } from "types"
import { SKIP, VisitorResult } from "unist-util-visit"
import { estreeToJs } from "../estree-to-js.js"
import {
isExpressionJsVarLiteral,
isExpressionJsVarObj,
} from "../expression-is-utils.js"
import path from "path"
import { readFileSync } from "fs"
import type { Documentation } from "react-docgen"
export type ComponentParser<TOptions = any> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options?: TOptions
) => VisitorResult
export const parseCard: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
let title: string | undefined,
text: string | undefined,
href: string | undefined
node.attributes?.some((attr) => {
if (title && text && href) {
return true
}
if (attr.name === "title") {
title = attr.value as string
} else if (attr.name === "text") {
text = attr.value as string
} else if (attr.name === "href") {
href = attr.value as string
}
return false
})
if (!title || !href) {
return
}
parent?.children.splice(index, 1, {
type: "paragraph",
children: [
{
type: "link",
url: href,
children: [
{
type: "text",
value: title,
},
],
},
{
type: "text",
value: `: ${text}`,
},
],
})
return [SKIP, index]
}
export const parseCardList: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const items = node.attributes?.find((attr) => attr.name === "items")
if (!items || typeof items.value === "string" || !items.value.data?.estree) {
return
}
const itemsJsVar = estreeToJs(items.value.data.estree)
if (!itemsJsVar || !Array.isArray(itemsJsVar)) {
return
}
const listItems = itemsJsVar
.map((item) => {
if (
!isExpressionJsVarObj(item) ||
!("title" in item) ||
!("href" in item) ||
!isExpressionJsVarLiteral(item.title) ||
!isExpressionJsVarLiteral(item.href)
) {
return null
}
const description = isExpressionJsVarLiteral(item.text)
? (item.text.data as string)
: ""
const children: UnistNode[] = [
{
type: "link",
url: `${item.href.data}`,
children: [
{
type: "text",
value: item.title.data as string,
},
],
},
]
if (description.length) {
children.push({
type: "text",
value: `: ${description}`,
})
}
return {
type: "listItem",
children: [
{
type: "paragraph",
children,
},
],
}
})
.filter(Boolean) as UnistNode[]
parent?.children.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: listItems,
})
return [SKIP, index]
}
export const parseCodeTabs: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const tabs = node.children?.filter((child) => child.name === "CodeTab")
if (!tabs) {
return
}
const children: UnistNode[] = []
tabs.forEach((tab) => {
const label = tab.attributes?.find((attr) => attr.name === "label")
const code = tab.children?.find((child) => child.type === "code")
if (!label || !code) {
return
}
children.push(
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: label.value as string,
},
],
},
code
)
})
parent?.children.splice(index, 1, ...children)
return [SKIP, index]
}
export const parseDetails: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const summary = node.attributes?.find(
(attr) => attr.name === "summaryContent"
)
const children: UnistNode[] = []
if (summary?.value) {
children.push({
type: "heading",
depth: 3,
children: [
{
type: "text",
value: (summary?.value as string) || "Details",
},
],
})
}
children.push(...(node.children || []))
parent?.children.splice(index, 1, ...children)
return [SKIP, index]
}
export const parseNote: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
parent.children?.splice(index, 1, ...(node.children || []))
return [SKIP, index]
}
export const parsePrerequisites: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const items = node.attributes?.find((attr) => attr.name === "items")
if (!items || typeof items.value === "string" || !items.value.data?.estree) {
return
}
const itemsJsVar = estreeToJs(items.value.data.estree)
if (!itemsJsVar || !Array.isArray(itemsJsVar)) {
return
}
const listItems = itemsJsVar
.map((item) => {
if (
!isExpressionJsVarObj(item) ||
!("text" in item) ||
!("link" in item) ||
!isExpressionJsVarLiteral(item.text) ||
!isExpressionJsVarLiteral(item.link)
) {
return null
}
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "link",
url: `${item.link.data}`,
children: [
{
type: "text",
value: item.text.data,
},
],
},
],
},
],
}
})
.filter(Boolean) as UnistNode[]
parent?.children.splice(
index,
1,
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: "Prerequisites",
},
],
},
{
type: "list",
ordered: false,
spread: false,
children: listItems,
}
)
return [SKIP, index]
}
export const parseSourceCodeLink: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const link = node.attributes?.find((attr) => attr.name === "link")
if (!link) {
return
}
parent?.children.splice(index, 1, {
type: "paragraph",
children: [
{
type: "link",
url: link?.value as string,
children: [
{
type: "text",
value: "Source code",
},
],
},
],
})
return [SKIP, index]
}
export const parseTable: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const headerNode = node.children?.find(
(child) => child.name === "Table.Header"
)
const bodyNode = node.children?.find((child) => child.name === "Table.Body")
let nodeText = ``
let headerCellsCount = 0
headerNode?.children?.forEach((headerRow, rowIndex) => {
if (rowIndex > 0) {
nodeText += `\n`
}
const childCells =
headerRow.children?.length === 1 &&
headerRow.children[0].type === "paragraph"
? headerRow.children[0].children
: headerRow.children
headerCellsCount = childCells?.length || 0
childCells?.forEach((headerCell) => {
if (headerCell.name !== "Table.HeaderCell") {
return
}
nodeText += `|`
nodeText += formatNodeText(getTextNode(headerCell))
})
nodeText += `|`
})
nodeText += `\n|${new Array(headerCellsCount).fill(`---`).join(`|`)}|`
bodyNode?.children?.forEach((bodyRow) => {
nodeText += `\n`
bodyRow.children?.forEach((bodyCell) => {
nodeText += `|`
nodeText += formatNodeText(getTextNode(bodyCell))
})
nodeText += `|`
})
parent.children?.splice(index, 1, {
type: "paragraph",
children: [
{
type: "text",
value: nodeText,
},
],
})
}
export const parseTabs: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
if ((node.children?.length || 0) < 2) {
return
}
const tabs: UnistNode[] = []
node.children![0].children?.forEach((tabList) => {
tabList.children?.forEach((tabTrigger, index) => {
const tabContentNode = node.children?.[1].children?.[index]
const tabLabel = formatNodeText(getTextNode(tabTrigger))
const tabContent = tabContentNode?.children || []
if (!tabLabel || !tabContent) {
return
}
tabs.push(
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: tabLabel,
},
],
},
...tabContent
)
})
})
parent.children?.splice(index, 1, ...tabs)
return [SKIP, index]
}
export const parseTypeList: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const types = node.attributes?.find((attr) => attr.name === "types")
if (!types || typeof types.value === "string" || !types.value.data?.estree) {
return
}
const typesJsVar = estreeToJs(types.value.data.estree)
if (!typesJsVar || !Array.isArray(typesJsVar)) {
return
}
const generateTypeListItems = (
typesJsVar: ExpressionJsVar[]
): UnistNode[] => {
const listItems: UnistNode[] = []
typesJsVar.forEach((item) => {
if (!isExpressionJsVarObj(item)) {
return
}
const typeName = isExpressionJsVarLiteral(item.name) ? item.name.data : ""
const itemType = isExpressionJsVarLiteral(item.type) ? item.type.data : ""
const itemDescription = isExpressionJsVarLiteral(item.description)
? item.description.data
: ""
if (!typeName || !itemType) {
return
}
const itemListChildren = item.children || []
const children: UnistNode[] = [
{
type: "paragraph",
children: [
{
type: "text",
value: `${typeName}: (${itemType}) ${itemDescription}`.trim(),
},
],
},
]
if (Array.isArray(itemListChildren) && itemListChildren.length) {
children.push(...generateTypeListItems(itemListChildren))
}
listItems.push({
type: "listItem",
children,
})
})
return listItems
}
const listItems = generateTypeListItems(typesJsVar)
parent?.children.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: listItems,
})
return [SKIP, index]
}
export const parseWorkflowDiagram: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const worflowItems = node.attributes?.find((attr) => attr.name === "workflow")
if (
!worflowItems ||
typeof worflowItems.value === "string" ||
!worflowItems.value.data?.estree
) {
return
}
const workflowJsVar = estreeToJs(worflowItems.value.data.estree)
if (
!isExpressionJsVarObj(workflowJsVar) ||
!("steps" in workflowJsVar) ||
!Array.isArray(workflowJsVar.steps)
) {
return
}
const generateWorkflowItems = (
jsVarItems: ExpressionJsVar[]
): UnistNode[] => {
const listItems: UnistNode[] = []
jsVarItems.forEach((item) => {
if (!isExpressionJsVarObj(item)) {
return
}
const stepName = isExpressionJsVarLiteral(item.name) ? item.name.data : ""
const stepDescription = isExpressionJsVarLiteral(item.description)
? item.description.data
: ""
const stepLink = isExpressionJsVarLiteral(item.link)
? (item.link.data as string)
: `#${stepName}`
if (!stepName) {
return
}
const stepChildren = item.steps || []
const children: UnistNode[] = [
{
type: "paragraph",
children: [
{
type: "link",
url: stepLink,
children: [
{
type: "text",
value: `${stepName}`,
},
],
},
{
type: "text",
value: `: ${stepDescription}`,
},
],
},
]
if (Array.isArray(stepChildren) && stepChildren.length) {
children.push(...generateWorkflowItems(stepChildren))
}
listItems.push({
type: "listItem",
children,
})
})
return listItems
}
const listItems = generateWorkflowItems(workflowJsVar.steps)
parent?.children.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: listItems,
})
return [SKIP, index]
}
export const parseComponentExample: ComponentParser<{
examplesBasePath: string
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.examplesBasePath) {
return
}
const exampleName = node.attributes?.find((attr) => attr.name === "name")
if (!exampleName) {
return
}
const fileContent = readFileSync(
path.join(options.examplesBasePath, `${exampleName.value as string}.tsx`),
"utf-8"
)
parent.children?.splice(index, 1, {
type: "code",
lang: "tsx",
value: fileContent,
})
return [SKIP, index]
}
export const parseComponentReference: ComponentParser<{ specsPath: string }> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.specsPath) {
return
}
const mainComponent = node.attributes?.find(
(attr) => attr.name === "mainComponent"
)?.value as string
if (!mainComponent) {
return
}
const componentNames: string[] = []
const componentsToShowAttr = node.attributes?.find(
(attr) => attr.name === "componentsToShow"
)
if (
componentsToShowAttr &&
typeof componentsToShowAttr.value !== "string" &&
componentsToShowAttr.value.data?.estree
) {
const componentsToShowJsVar = estreeToJs(
componentsToShowAttr.value.data.estree
)
if (componentsToShowAttr && Array.isArray(componentsToShowJsVar)) {
componentNames.push(
...componentsToShowJsVar
.map((item) => {
return isExpressionJsVarLiteral(item) ? (item.data as string) : ""
})
.filter((name) => name.length > 0)
)
}
}
if (!componentNames.length) {
componentNames.push(mainComponent)
}
const getComponentNodes = (componentName: string): UnistNode[] => {
const componentSpecsFile = path.join(
options.specsPath,
mainComponent,
`${componentName}.json`
)
const componentSpecs: Documentation = JSON.parse(
readFileSync(componentSpecsFile, "utf-8")
)
const componentNodes: UnistNode[] = [
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: `${componentName} Props`,
},
],
},
]
if (componentSpecs.description) {
componentNodes.push({
type: "paragraph",
children: [
{
type: "text",
value: componentSpecs.description,
},
],
})
}
if (componentSpecs.props) {
const listNode: UnistNode = {
type: "list",
ordered: false,
spread: false,
children: [],
}
Object.entries(componentSpecs.props).forEach(([propName, propData]) => {
listNode.children?.push({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value:
`${propName}: (${propData.type?.name || propData.tsType?.name}) ${propData.description || ""}${propData.defaultValue ? ` Default: ${propData.defaultValue.value}` : ""}`.trim(),
},
],
},
],
})
})
componentNodes.push(listNode)
}
return componentNodes
}
parent.children?.splice(
index,
1,
...componentNames.flatMap(getComponentNodes)
)
}
export const parsePackageInstall: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const packageName = node.attributes?.find(
(attr) => attr.name === "packageName"
)
if (!packageName) {
return
}
parent.children?.splice(index, 1, {
type: "code",
lang: "bash",
value: `npm install ${packageName.value}`,
})
return [SKIP, index]
}
export const parseIconSearch: ComponentParser<{ iconNames: string[] }> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.iconNames) {
return
}
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: options.iconNames.map((iconName) => ({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value: iconName,
},
],
},
],
})),
})
return [SKIP, index]
}
export const parseHookValues: ComponentParser<{
hooksData: {
[k: string]: {
value: string
type?: {
type: string
}
description?: string
}[]
}
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.hooksData) {
return
}
const hookName = node.attributes?.find((attr) => attr.name === "hook")
if (
!hookName ||
!hookName.value ||
typeof hookName.value !== "string" ||
!options.hooksData[hookName.value]
) {
return
}
const hookData = options.hooksData[hookName.value]
const listItems = hookData.map((item) => {
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value:
`${item.value}: (${item.type?.type}) ${item.description || ""}`.trim(),
},
],
},
],
}
})
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: listItems,
})
return [SKIP, index]
}
export const parseColors: ComponentParser<{
colors: {
[k: string]: Record<string, string>
}
}> = (
node: UnistNodeWithData,
index: number,
parent: UnistTree,
options
): VisitorResult => {
if (!options?.colors) {
return
}
parent.children?.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: Object.entries(options.colors).flatMap(([section, colors]) => [
{
type: "heading",
depth: 3,
children: [
{
type: "text",
value: section,
},
],
},
...Object.entries(colors).map(([name, value]) => ({
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value: name,
},
{
type: "text",
value: `: ${value}`,
},
],
},
],
})),
]),
})
}
export const parseSplitList: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const items = node.attributes?.find((attr) => attr.name === "items")
if (!items || typeof items.value === "string" || !items.value.data?.estree) {
return
}
const itemsJsVar = estreeToJs(items.value.data.estree)
if (!itemsJsVar || !Array.isArray(itemsJsVar)) {
return
}
const listItems = itemsJsVar
.map((item) => {
if (
!isExpressionJsVarObj(item) ||
!("title" in item) ||
!("link" in item) ||
!isExpressionJsVarLiteral(item.title) ||
!isExpressionJsVarLiteral(item.link)
) {
return null
}
const description = isExpressionJsVarLiteral(item.description)
? (item.description.data as string)
: ""
return {
type: "listItem",
children: [
{
type: "paragraph",
children: [
{
type: "link",
url: `${item.link.data}`,
children: [
{
type: "text",
value: `${item.title.data}${description ? `: ${description}` : ""}`,
},
],
},
],
},
],
}
})
.filter(Boolean) as UnistNode[]
if (!listItems.length) {
return
}
parent?.children.splice(index, 1, {
type: "list",
ordered: false,
spread: false,
children: listItems,
})
return [SKIP, index]
}
export const parseEventHeader: ComponentParser = (
node: UnistNodeWithData,
index: number,
parent: UnistTree
): VisitorResult => {
const headerContent = node.attributes?.find(
(attr) => attr.name === "headerProps"
)
const headerLvl = node.attributes?.find((attr) => attr.name === "headerLvl")
if (
!headerContent ||
typeof headerContent.value === "string" ||
!headerContent.value.data?.estree ||
!headerLvl ||
typeof headerLvl.value !== "string" ||
!headerLvl.value
) {
return
}
const headerPropsJsVar = estreeToJs(headerContent.value.data.estree)
if (
!isExpressionJsVarObj(headerPropsJsVar) ||
!("children" in headerPropsJsVar) ||
!isExpressionJsVarLiteral(headerPropsJsVar.children)
) {
return
}
const headerLevel = parseInt(headerLvl.value, 10)
const headerChildren = headerPropsJsVar.children.data as string
const headerChildrenNode = {
type: "text",
value: headerChildren,
}
const headerNode = {
type: "heading",
depth: headerLevel,
children: [headerChildrenNode],
}
parent?.children.splice(index, 1, headerNode)
return [SKIP, index]
}
/**
* Helpers
*/
const getTextNode = (node: UnistNode): UnistNode | undefined => {
let textNode: UnistNode | undefined
node.children?.some((child) => {
if (textNode) {
return true
}
if (child.type === "text") {
textNode = child
} else if (child.type === "paragraph") {
textNode = getTextNode(child)
} else if (child.type === "link") {
textNode = getTextNode(child)
} else if (child.value) {
textNode = child
}
return textNode !== undefined
})
return textNode
}
const formatNodeText = (node?: UnistNode): string => {
if (!node) {
return ""
}
if (node.type === "inlineCode") {
return `\`${node.value}\``
} else if (node.type === "code") {
return `\`\`\`${"lang" in node ? node.lang : "ts"}\n${node.value}\n\`\`\``
} else if (node.type === "link") {
return `[${node.children?.[0].value}](${node.url})`
}
return node.value || ""
}