authentik llms.txt 生成器源码解析:MDX 转义导入路径(\_esc-note.mdx)的识别、解转义与 partial 内联
【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik
导读
authentik 官方文档站点使用自研的 Docusaurus 插件(ak-llms-txt-plugin)将大量 MDX 文档转换为符合 llmstxt.org 约定的纯 Markdown 载荷(llms.txt、llms-full.txt、单页.md),供搜索引擎、Agent 与 LLM 直接检索。本篇文章以该插件解析阶段的测试夹具 escaped-import.md 为主线,完整拆解一条极易踩坑的边界场景:MDX 中带反斜杠转义的 partial 导入路径(./\_esc-note.mdx)如何被正确识别、解转义、读取并内联进最终 Markdown。读完你可以掌握该插件从 fixture 到llms.txt输出的完整数据流,以及其源码级实现细节。
一、定位:这条 fixture 在 llms.txt 插件中扮演的角色
在 website/docusaurus-theme/llms-txt/ 目录下,整个插件由四个核心模块组成:
- markdown.mjs:MDX → 干净 Markdown 的转换核心(partial 内联、指令剥离、admonition 围栏清理);
- node.mjs:文档发现(glob)、解析、分组与路由解析;
- generate.mjs:拼装
llms.txt/llms-full.txt/ 分组索引 / 单页载荷四种输出字符串; - plugin.mjs:Docusaurus 插件外壳,串联以上模块。
其中__fixtures__/parse/目录存放的是解析阶段的测试输入样本,与同目录下的cve.md、draft.md、heading-only.md、linky.md、prereq-list.md、quoted.md共同构成一组覆盖不同解析边界的夹具集。而escaped-import.md正是专门用来验证“导入路径带 Markdown 转义符”这一种情况的标本。其完整内容仅七行:
import EscNote from "./\_esc-note.mdx"; # Escaped Import <EscNote /> Trailing body.它导入的同目录 partial 文件 _esc-note.mdx 内容同样极简:
Escaped partial body.这个 fixture 虽小,却精准地覆盖了插件内联机制中的关键分支:import语句、被\转义的下划线路径、JSX 组件引用<EscNote />、以及尾随正文。所有断言围绕这四个要素展开。
二、问题本质:为什么\_esc-note.mdx需要反斜杠转义
要理解这条 fixture,先要理解 authentik 文档中的partial 约定。在 node.mjs 的collectDocFiles中,文档扫描使用 FastGlob 收集**/*.{md,mdx},但明确排除了以下模式:
ignore: [ "**/_*.{md,mdx}", // 下划线前缀 = partial,不单独成页 "**/_*/**", "**/*.test.{md,mdx}", "**/__tests__/**", "**/__fixtures__/**", // 夹具目录同样不进生产索引 "**/node_modules/**", ...ignoreFiles, ],也就是说,下划线前缀是 partial(片段)文件的命名约定——它们不会被当作独立页面收录进llms.txt,而是作为“素材”被其他页面通过import引入。正因为 partial 本身不出现在最终索引中,内联(inline)机制就是 partial 内容能进入最终.md载荷的唯一通道。
那么导入路径里的反斜杠从何而来?从 markdown.mjs 的源码注释可以明确看到设计意图:
// Markdown escapes leading underscores etc. in .md import paths (`\_partial.mdx`); // unescape before resolving so the partial file is actually found. const cleanImportPath = importPath.replace(/\\(?=[_*[\]()#-])/g, "");即:MDX 文档本身会被 Markdown 解析器处理,而下划线(_)是 Markdown 的强调(emphasis)标记符;当 import 路径以_开头时,为避免被当作强调语法解析破坏路径,作者在路径中用反斜杠转义下划线等 Markdown 特殊字符(_ * [ ] ( ) # -均在转义字符类中)。插件在解析路径时必须先解除转义,才能拿到真实的文件路径。
同时,从实现看,inlinePartials的导入匹配正则将路径中是否含下划线作为命中条件之一:
const importRe = /^\s*import\s+(?:(\w+)|{\s*(\w+)\s*})\s+from\s+'"['"];?\s*$/gm;路径段[^'"]+_[^'"]+要求导入路径中必然出现一个_,这与“partial 文件以下划线命名”的约定互相印证——只有 partial 导入才需要被内联。
三、源码机制:inlinePartials的解转义与内联全流程
核心函数inlinePartials位于 markdown.mjs,按顺序执行四个步骤:
1. 正则匹配 import 语句并解转义路径
对每一处import X from "./\_xxx.mdx"匹配,通过importPath.replace(/\\(?=[_*[\]()#-])/g, "")将\_还原为_。注意这里的正则只移除那些后面紧跟 Markdown 特殊字符的反斜杠,不会误伤路径中的普通字符。
2. 基于源文件目录解析真实路径
const partialPath = resolve(dirname(filePath), cleanImportPath); bodies.set(name, loadPartial(partialPath, new Set([filePath])));以 fixture 为例,filePath是__fixtures__/parse/escaped-import.md,cleanImportPath是./_esc-note.mdx,最终解析到__fixtures__/parse/_esc-note.mdx。
3. 读取 partial 内容(含循环导入防护)
loadPartial(markdown.mjs)实现如下:
function loadPartial(partialPath, chain) { if (chain.has(partialPath)) return ""; // 循环导入防护 const raw = readFileSync(partialPath, "utf-8"); const { content } = parseFileContentFrontMatter(raw); return content.trim(); }它使用Set记录已加载路径链,防止 partial 反向导入其导入者时造成无限递归;同时通过 Docusaurus 的parseFileContentFrontMatter剥离 partial 自身的 frontmatter,只保留正文。
4. 删除 import 行并替换 JSX 引用
let out = content.replace(importRe, ""); for (const [name, body] of bodies) { const jsxRe = new RegExp(`<${name}\\s*(?:[^>]*?)(?:/>|>[\\s\\S]*?</${name}>)`, "g"); out = out.replace(jsxRe, body); }正则同时匹配自闭合<EscNote />与带子内容的<EscNote>...</EscNote>两种形式。执行完毕后,fixture 中的import行被移除,<EscNote />被替换为 partial 正文Escaped partial body.。
四、测试验证:markdown.test.mjs的三个断言
该机制由 markdown.test.mjs 中专门的用例兜底:
test("cleanMdxToMarkdown inlines a partial whose import path has a Markdown-escaped underscore", async () => { const file = resolve(FIXTURE_PARSE, "escaped-import.md"); const raw = readFileSync(file, "utf-8"); const out = await cleanMdxToMarkdown(raw, file); assert.ok(out.includes("Escaped partial body."), "escaped-path partial is inlined"); assert.ok(!/^import\s/m.test(out), "import line removed"); assert.ok(!out.includes("\\_esc-note"), "no escaped path leaks"); });三个断言分别对应三个必须同时成立的正确性要求:
- 内容确实被内联:输出必须包含 partial 正文
Escaped partial body.,否则说明解转义或路径解析失败; - import 语句被移除:转换后的纯 Markdown 不允许残留 ESM 语法;
- 转义符不泄漏:输出中不能出现
\_esc-note这样的转义残迹,保证最终载荷是干净、可被直接消费的 Markdown。
这套断言同时也保护了重构安全——任何破坏解转义逻辑的改动都会在node --test阶段被拦截。
五、完整数据流:从 fixture 到llms.txt与单页.md
内联只是第一步。cleanMdxToMarkdown(markdown.mjs)在 partial 内联之后,还会串联完整的 remark 流水线:
const file = await unified() .use(remarkParse) .use(remarkMdx) .use(remarkGfm) .use(remarkDirective) .use(stripNodesPlugin) .use(remarkStringify, { bullet: "-", fences: true }) .process(inlined); return stripAdmonitionFences(String(file)).trim();stripNodesPlugin(markdown.mjs)在 AST 层面删除mdxjsEsm、mdxJsxFlowElement等 MDX/JSX 节点,并将其文本子节点上移保留;stripAdmonitionFences(markdown.mjs)移除 Docusaurus 的:::note等 admonition 围栏标记,同时感知代码围栏,不会误删代码块内的:::;- 若 MDX 解析抛错(如 frontmatter 格式非法),则降级到
regexClean正则兜底(markdown.mjs),保证“解析失败不崩溃、正文仍保留”。
而在插件层 plugin.mjs 的buildLLMSOutputs中,每个文档依次经历:
parseDocFile(file, absDir) // frontmatter + 标题 + 描述提取(node.mjs) → resolveDocumentUrl(...) // 路由解析,frontmatter slug 优先 → assignGroup / groupLabel // 分组(topic 或 category) → cleanMdxToMarkdown(...) // 本篇文章的主角:内联 + 清洗 → generateIndex / generateFullText / generatePerGroupIndexes / renderPagePayload最终产出四类文件(文件名常量见 common.mjs):
llms.txt:分组链接索引(generateIndex,generate.mjs);llms-full.txt:全部页面全文拼接(generateFullText);<group>/llms.txt:每组的独立索引(generatePerGroupIndexes);- 每个页面对应的
.md载荷(renderPagePayload,generate.mjs),格式为# 标题+ 描述引用 + 清洗后正文。
以 fixture 为例,若它是一条真实文档,则内联后单页载荷正文将是:
# Escaped Import Escaped partial body. Trailing body.——import行与 JSX 标签消失,partial 内容成为正文的一部分,这正是 Agent/LLM 检索时看到的样子。
六、设计要点与可借鉴的边界处理
从这条 fixture 延伸开去,整个插件在处理“非标准 Markdown”时表现出几个值得借鉴的设计决策:
- 命名约定驱动解析:partial 用下划线前缀命名,导入正则也以下划线为命中锚点,两处约定相互印证,避免了对任意文件做内联猜测;
- 解转义是路径解析的前置步骤:将“文本层转义”与“文件系统路径”解耦,先用白名单字符类(
_*[\]()#-)精确解转义,再交给resolve解析,兼顾正确性与安全性; - 失败降级而非崩溃:MDX 解析失败时走
regexClean兜底,并只在汇总日志中统计(plugin.mjs中的mdxFallbacks计数),保证文档站点构建不被单个坏文件阻断; - 循环导入防护:
loadPartial的chain集合从机制上杜绝 partial 互相导入导致的递归; - 代码围栏感知:
stripAdmonitionFences通过维护围栏字符栈,确保:::info这类标记在代码块内(包括~~~块内嵌套```的极端情况)不被误删,相关边界在 markdown.test.mjs 中有专项用例覆盖。
此外,插件还处理了部署场景的链接正确性问题:resolveSiteUrl(plugin.mjs)在 Netlify 的deploy-preview/branch-deploy上下文中优先使用DEPLOY_PRIME_URL作为链接基址,避免预览部署链接错误指向生产域名;loadContent阶段无routesPaths时则用resolveDocumentUrlFromSource从源码路径推导路由,保证开发服务器下文件也能即时生成。
七、小结
escaped-import.md虽是一份 7 行的测试夹具,却是理解 authentik 文档管线中“MDX → 干净 Markdown”转换机制的绝佳切片:它同时检验了 partial 命名约定、Markdown 转义语义、路径解转义、文件内联与输出洁净度。从它的处理流程可以完整看到inlinePartials→cleanMdxToMarkdown→buildLLMSOutputs→llms.txt/单页.md的整条链路。对于任何需要从 MDX 文档体系生成 LLM 可读文本索引的开发者,这一实现提供了可直接复用的设计范式:用命名约定区分素材与页面、用白名单精确解转义、用 AST 而非字符串替换做内容清洗、用测试夹具锁定每一个边界行为。
【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考