使用unified/remark/rehype处理Markdown时触发TypeError: attacher.call is not a function错误
unified/remark/rehype处理Markdown时触发TypeError: attacher.call is not a function
嘿,我来帮你搞定这个问题!这个错误的核心是你完全搞混了unified处理管道中remark(Markdown AST)和rehype(HTML AST)的阶段顺序,插件的调用逻辑完全不对,才会触发attacher.call is not a function这个错误。
错误原因分析
你原来的代码里犯了两个关键错误:
- 流程顺序完全颠倒:你先用
remark-html把Markdown AST(MDAST)直接转成了HTML字符串,然后又用rehype-parse去解析这个字符串成HTML AST(HAST)——这相当于把做好的蛋糕磨成粉再重新烤,完全没必要,而且unified的管道不允许在已经输出字符串的阶段再插入解析类插件,这直接导致插件的挂载逻辑崩溃,触发了attacher错误。 - 混淆了remark和rehype的职责:remark负责处理Markdown到MDAST的转换,rehype负责处理HAST到HTML的转换,两者需要通过
remark-rehype来衔接,而不是用remark-html直接跳到HTML字符串。
修正后的完整代码
首先你需要安装remark-rehype(用来把MDAST转成HAST),如果想用更简洁的包裹逻辑,还可以装rehype-wrap:
npm install remark-rehype rehype-wrap
然后修改你的getPageContent函数和相关插件逻辑:
import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkPrism from 'remark-prism'; import remarkRehype from 'remark-rehype'; import rehypeStringify from 'rehype-stringify'; import rehypeWrap from 'rehype-wrap'; // 替代自定义的rehypeWrapPreBlocks const postsDirectory = path.join(process.cwd(), 'src/app/content/pages'); export async function getPageContent(id) { const fullPath = path.join(postsDirectory, `${id}.md`); const fileContents = fs.readFileSync(fullPath, 'utf8'); const matterResult = matter(fileContents); // 正确的处理管道:Markdown → MDAST → 处理MDAST → HAST → 处理HAST → HTML const processedContent = await unified() .use(remarkParse) // 解析Markdown为MDAST .use(remarkPrism) // 在MDAST阶段添加代码高亮 .use(remarkRehype) // 把MDAST转换为HAST .use(rehypeWrap, { // 用rehype-wrap替代自定义函数,更简洁可靠 selector: 'pre', wrap: 'div.code-block-wrapper' }) .use(rehypeStringify) // 把HAST转换为HTML字符串 .process(matterResult.content); const contentHtml = processedContent.toString(); return { id, contentHtml, ...matterResult.data, }; }
如果你不想用rehype-wrap,坚持自己写包裹逻辑,那也要把自定义的rehypeWrapPreBlocks放在remark-rehype之后、rehype-stringify之前,确保它在HAST阶段运行,代码如下:
// 保留你的自定义插件 function rehypeWrapPreBlocks() { return (tree) => { visit(tree, 'element', (node, index, parent) => { if (node.tagName === 'pre') { const wrapperNode = { type: 'element', tagName: 'div', properties: { className: ['code-block-wrapper'] }, children: [node], }; parent.children[index] = wrapperNode; } }); }; } // 处理管道部分改成 const processedContent = await unified() .use(remarkParse) .use(remarkPrism) .use(remarkRehype) .use(rehypeWrapPreBlocks) // 放在HAST阶段 .use(rehypeStringify) .process(matterResult.content);
额外的改进建议
- 用rehype-prism替代remark-prism:如果你的代码高亮需要更灵活的HTML处理,
rehype-prism(作用在HAST阶段)比remark-prism更合适,它能直接操作HTML标签的class和结构,避免MDAST到HAST转换时的潜在问题。 - 错误处理:在
getPageContent里添加文件存在性检查,比如用fs.existsSync(fullPath)判断文件是否存在,提前抛出更友好的错误,而不是让fs.readFileSync直接崩溃。 - 路径简化:
path.join(process.cwd(), 'src/app/content/pages')不需要前面的/,path.join会自动处理路径分隔符,跨平台更友好。
这样修改后,你的管道逻辑就清晰了,错误也会消失,pre标签也会被正确包裹在div.code-block-wrapper里。
备注:内容来源于stack exchange,提问作者turtles




