mdx.ts•6.61 kB
import { readdir, readFile } from 'fs/promises'
import { join, relative } from 'path'
import { parseStringPromise } from 'xml2js'
export const AGORA_NEW_DOCS_SITE = 'https://doc.shengwang.cn/'
interface MDXFile {
path: string
content: string
}
export const DOC_ROUTE_BASE_PATH_MAPPING = {
shared: {
routeBasePath: '/global-shared',
},
default: {
routeBasePath: '/doc',
},
['api-reference']: {
routeBasePath: '/api-ref',
},
faq: {
routeBasePath: '/faq',
},
basics: {
routeBasePath: '/basics',
},
}
export async function getAllMDXFiles(dir: string): Promise<MDXFile[]> {
const files: MDXFile[] = []
async function scan(directory: string) {
const entries = await readdir(directory, { withFileTypes: true })
for (const entry of entries) {
const fullPath = join(directory, entry.name)
if (entry.isDirectory()) {
await scan(fullPath)
} else if (entry.name.endsWith('.mdx')) {
const content = await readFile(fullPath, 'utf-8')
const relativePath = relative(dir, fullPath)
files.push({
path: relativePath,
content,
})
}
}
}
await scan(dir)
return files
}
export async function checkFileExists(
filePath: string
): Promise<string | false> {
try {
const content = await readFile(filePath, 'utf-8')
return content
} catch (error: unknown) {
if ((error as any)?.code === 'ENOENT') {
return false
}
throw error
}
}
export async function getMDXFileContent(filePath: string): Promise<string> {
try {
const isMdFileExist = await checkFileExists(filePath + '.md')
const isMdxFileExist = await checkFileExists(filePath + '.mdx')
const mdFileContent = isMdFileExist || isMdxFileExist
if (!mdFileContent) {
throw new Error(`File ${filePath} not found`)
}
return mdFileContent
} catch (error) {
console.error(`Error reading file ${filePath}:`, error)
throw error
}
}
export type TSiteMap = {
urlset: {
url: {
loc: string[]
[x: string]: string[]
}[]
}
}
export const url2LocalPath = (url: string): string | undefined => {
const urlPath = url.replace(AGORA_NEW_DOCS_SITE, '')
const routeBasePath = urlPath.split('/').shift()
if (!routeBasePath) return undefined
const localFolder = Object.keys(DOC_ROUTE_BASE_PATH_MAPPING).find((i) => {
const targetItem =
DOC_ROUTE_BASE_PATH_MAPPING[i as keyof typeof DOC_ROUTE_BASE_PATH_MAPPING]
return targetItem.routeBasePath.replace('/', '') === routeBasePath
})
if (!localFolder) return undefined
const localPath = urlPath.replace(routeBasePath, localFolder)
return localPath
}
export async function getSiteMap(filePath: string): Promise<
{
loc: string
folder: string
category?: string
}[]
> {
const siteMapFile = await readFile(filePath, 'utf-8')
try {
const result: TSiteMap = await parseStringPromise(siteMapFile)
// console.log('Parsed sitemap XML:', result)
let urls: {
loc: string
folder: string
category?: string
}[] = []
const allowedPrefixList = Object.values(DOC_ROUTE_BASE_PATH_MAPPING).map(
(item) => item.routeBasePath.replace('/', '')
)
if (result.urlset && Array.isArray(result.urlset.url)) {
urls = result.urlset.url.reduce((prev, curr) => {
const targetLoc = curr?.loc?.[0]
const parsedLoc = targetLoc?.replace(AGORA_NEW_DOCS_SITE, '')
const allowedPrefix = allowedPrefixList.find((prefix) =>
parsedLoc?.startsWith(prefix)
)
const category =
Object.entries(DOC_ROUTE_BASE_PATH_MAPPING).find(
([_, value]) =>
value.routeBasePath.replace('/', '') === allowedPrefix
)?.[0] || allowedPrefix
if (allowedPrefix && parsedLoc) {
prev.push({
loc: parsedLoc,
folder: allowedPrefix,
category,
})
}
return prev
}, [] as typeof urls)
}
return urls
} catch (error) {
console.error('Failed to parse sitemap XML:', error)
return []
}
}
export async function getDocContentByUri(uri: string): Promise<string> {
try {
// 解析URI
if (!uri.startsWith('docs://')) {
throw new Error('Invalid URI format, must start with docs://')
}
// const encodedPath = uri.replace('docs://', '')
// const decodedPath = decodeURIComponent(encodedPath)
// const localPath = url2LocalPath(decodedPath)
const encodedPath = uri.replace('docs://', '')
const localPath = encodedPath
if (!localPath) {
throw new Error(`Missing local path: ${uri}`)
}
// 使用DOCS_MD_DIR获取完整路径
const DOCS_MD_DIR = process.env.DOCS_MD_DIR || join(process.cwd(), '.docs')
// retrieve the file content
const mdxContent = await getMDXFileContent(join(DOCS_MD_DIR, localPath))
return mdxContent
} catch (error) {
console.error(`Error: ${error}`)
throw error
}
}
const getMdxContentAndFrontmatter = (content: string) => {
const frontmatterRegex = /---([\s\S]*?)---([\s\S]*)/
const match = content.match(frontmatterRegex)
if (match) {
const frontmatter = match[1]
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'))
.join('\n')
const restContent = match[2].trim()
return {
frontmatter,
content: restContent,
}
}
return {
frontmatter: '',
content: content.trim(),
}
}
const getMdxFrontmatterVars = (frontmatter: string) => {
if (!frontmatter.trim()) return {}
return frontmatter.split('\n').reduce((acc, line) => {
const [key, value] = line.split(':')
acc[key.trim()] = value.trim()
return acc
}, {} as Record<string, string>)
}
export const handleMdxContent = (content: string) => {
const { frontmatter, content: restContent } =
getMdxContentAndFrontmatter(content)
const frontmatterVars = getMdxFrontmatterVars(frontmatter)
let handledRestContent = restContent
for (const key in frontmatterVars) {
const keyName = key.trim()
const keyValue = frontmatterVars[keyName]
const regex1 = new RegExp(`\\$\\{frontMatter\\.${keyName}\\}`, 'g')
handledRestContent = handledRestContent.replace(regex1, keyValue)
const regex2 = new RegExp(`frontMatter\\.${keyName}`, 'g')
handledRestContent = handledRestContent.replace(regex2, keyValue)
}
return {
metadata: {
frontmatter,
originContent: restContent,
frontmatterVars: frontmatterVars,
},
content: handledRestContent,
}
}