dappconnect-sdks/apps/website/contentlayer.config.ts

262 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
defineDocumentType,
defineNestedType,
makeSource,
} from '@contentlayer/source-files'
import remarkHeadings from '@vcarl/remark-headings'
import { slug as slugify } from 'github-slugger'
import { toString } from 'mdast-util-to-string'
import * as fs from 'node:fs/promises'
import path from 'node:path'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
import { remark } from 'remark'
import remarkDirective from 'remark-directive'
import remarkGfm from 'remark-gfm'
// import remarkMessageControl from 'remark-message-control'
// import remarkAdmonitions from 'remark-github-beta-blockquote-admonitions'
// import remarkBreaks from 'remark-breaks'
import strip from 'strip-markdown'
import { visit } from 'unist-util-visit'
import type { Plugin } from 'unified'
import type { Node } from 'unist'
const CONTENT_DIR_PATH = 'docs'
export type DocHeading = {
level: 1 | 2
value: string
}
export type DocIndex = {
path: string
title: string
content: {
[key in string]: string[]
}
}
const HeroImage = defineNestedType(() => ({
name: 'HeroImage',
fields: {
src: { type: 'string', required: true },
alt: { type: 'string', required: true },
},
}))
export const Doc = defineDocumentType(() => ({
name: 'Doc',
filePathPattern: '**/*.md{,x}',
contentType: 'mdx',
fields: {
id: { type: 'number', required: false },
revision: { type: 'string', required: false },
language: { type: 'string', required: false },
title: { type: 'string', required: true },
author: { type: 'string', required: false },
image: { type: 'nested', of: HeroImage, required: false },
},
computedFields: {
slug: {
// @ts-expect-error TODO
type: 'string[]',
resolve: doc => doc._raw.flattenedPath.split('/'),
// resolve: doc => {
// return getPathSegments(doc._raw.flattenedPath).map(
// ({ pathName }) => pathName
// )
// },
},
url: {
type: 'string',
resolve: doc => `/help/${doc._raw.flattenedPath}`,
// resolve: doc => {
// const slug = getPathSegments(doc._raw.flattenedPath).map(
// ({ pathName }) => pathName
// )
// return `/help/${slug.join('/')}`
// },
},
pathSegments: {
// @ts-expect-error TODO
type: '{ order: number; pathName: string }[]',
resolve: doc => getPathSegments(doc._raw.flattenedPath),
},
titleSlug: {
type: 'string',
resolve: doc => slugify(doc.title),
},
headings: {
// @ts-expect-error TODO
type: '{ level: 1 | 2; value: string, slug: string }[]',
resolve: async doc => {
const result = await remark().use(remarkHeadings).process(doc.body.raw)
return (result.data.headings as { depth: number; value: string }[])
.filter(({ depth }) => [1, 2].includes(depth))
.map<DocHeading>(({ depth, value }) => ({
level: depth as 1 | 2,
value,
slug: slugify(value),
}))
},
},
last_edited: {
type: 'date',
resolve: async (doc): Promise<Date> => {
const stats = await fs.stat(
path.join(CONTENT_DIR_PATH, doc._raw.sourceFilePath)
)
return stats.mtime
},
},
},
}))
function getPathSegments(filePath: string) {
return (
filePath
.split('/')
// .slice(1) // skip content dir path `/docs`
.map(fileName => {
const re = /^((\d+)-)?(.*)$/
const [, , orderStr, pathName] = fileName.match(re) ?? []
const order = orderStr ? parseInt(orderStr) : 0
return { order, pathName }
})
)
}
const remarkAdmonition: Plugin = () => {
return tree => {
visit(tree, node => {
if (
node.type === 'textDirective' ||
node.type === 'leafDirective' ||
node.type === 'containerDirective'
) {
// @ts-expect-error TODO
if (!['info', 'tip', 'warn'].includes(node.name)) {
return
}
// Store node.name before overwritten with "Alert".
// @ts-expect-error TODO
const type = node.name
// const data = node.data || (node.data = {})
// const tagName = node.type === 'textDirective' ? 'span' : 'div'
node.type = 'mdxJsxFlowElement'
// @ts-expect-error TODO
node.name = 'Admonition'
// @ts-expect-error TODO
node.attributes = [
{ type: 'mdxJsxAttribute', name: 'type', value: type },
// @ts-expect-error TODO
{ type: 'mdxJsxAttribute', name: 'title', value: node.label },
]
}
})
}
}
const remarkIndexer: Plugin = () => (root, file) => {
file.data.index = indexer(root)
}
function indexer(root: Node) {
const index: DocIndex['content'] = {}
let parentHeading = ''
visit(root, ['paragraph', 'heading'], node => {
if (node.type === 'heading') {
const text = toString(node, { includeImageAlt: false })
parentHeading = text
return
}
const text = toString(node, { includeImageAlt: false })
index[parentHeading] ??= []
index[parentHeading].push(text)
})
return index
}
export default makeSource({
contentDirPath: CONTENT_DIR_PATH,
documentTypes: [Doc],
mdx: {
remarkPlugins: [
remarkGfm,
remarkDirective,
remarkAdmonition,
// [remarkMessageControl, { name: 'hello' }],
],
rehypePlugins: [
rehypeSlug,
[
rehypePrettyCode,
{
// Use one of Shiki's packaged themes
theme: 'github-light',
// Keep the background or use a custom background color?
// keepBackground: true,
// Callback hooks to add custom logic to nodes when visiting
// them.
onVisitLine(node: any) {
// Prevent lines from collapsing in `display: grid` mode, and
// allow empty lines to be copy/pasted
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }]
}
},
onVisitHighlightedLine(node: any) {
// Each line node by default has `class="line"`.
node.properties.className.push('highlighted')
},
onVisitHighlightedWord(node: any) {
// Each word node has no className by default.
node.properties.className = ['word']
},
},
],
],
},
onSuccess: async importData => {
const { allDocs } = await importData()
const basePath = '/docs'
const index: DocIndex[] = []
for (const doc of allDocs) {
const result = await remark()
.use(strip, {
keep: ['heading'],
})
.use(remarkGfm)
.use(remarkIndexer, { path: basePath + '/' + doc._raw.flattenedPath })
.process(doc.body.raw)
index.push({
title: doc.title,
path: basePath + '/' + doc._raw.flattenedPath,
content: result.data.index as DocIndex['content'],
})
}
const filePath = path.resolve('./.contentlayer/en.json')
fs.writeFile(filePath, JSON.stringify(index))
},
})