You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用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这个错误。

错误原因分析

你原来的代码里犯了两个关键错误:

  1. 流程顺序完全颠倒:你先用remark-html把Markdown AST(MDAST)直接转成了HTML字符串,然后又用rehype-parse去解析这个字符串成HTML AST(HAST)——这相当于把做好的蛋糕磨成粉再重新烤,完全没必要,而且unified的管道不允许在已经输出字符串的阶段再插入解析类插件,这直接导致插件的挂载逻辑崩溃,触发了attacher错误。
  2. 混淆了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);

额外的改进建议

  1. 用rehype-prism替代remark-prism:如果你的代码高亮需要更灵活的HTML处理,rehype-prism(作用在HAST阶段)比remark-prism更合适,它能直接操作HTML标签的class和结构,避免MDAST到HAST转换时的潜在问题。
  2. 错误处理:在getPageContent里添加文件存在性检查,比如用fs.existsSync(fullPath)判断文件是否存在,提前抛出更友好的错误,而不是让fs.readFileSync直接崩溃。
  3. 路径简化path.join(process.cwd(), 'src/app/content/pages')不需要前面的/,path.join会自动处理路径分隔符,跨平台更友好。

这样修改后,你的管道逻辑就清晰了,错误也会消失,pre标签也会被正确包裹在div.code-block-wrapper里。

备注:内容来源于stack exchange,提问作者turtles

火山引擎 最新活动