Pandoc--embed-resources内联 SVG 与 class 属性合并机制解析:从命令测试 9652 看源码实现
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
导读
本文以 pandoc 仓库中编号 9652 的命令测试(test/command/9652.md)为切入点,深入剖析--embed-resources选项在处理inline-svg类图片时的行为:当<img>标签与所引用 SVG 文件同时携带class属性时,pandoc 会如何合并二者。读完本文,你将掌握--embed-resources的完整语义、inline-svg与<use>去重机制、class 属性合并规则,以及对应的源码实现路径,可直接复用于自己的 HTML/SVG 自包含构建与排障。
一、背景:--embed-resources与自包含 HTML
1.1 选项语义
--embed-resources[=true|false]是 pandoc 用于生成“自包含”(self-contained)HTML 的开关。按 MANUAL.txt 的定义:
- 它产生一个无外部依赖的独立 HTML 文件;
- 通过
data:URI 将链接的脚本、样式表、图片和视频内容内嵌进文档; - 生成的文件无需外部文件、也无需联网即可在浏览器中正常显示;
- 仅对 HTML 输出格式生效,包括
html4、html5、html+lhs、html5+lhs、s5、slidy、slideous、dzslides、revealjs; - 绝对 URL 指向的脚本/图片/样式表会被下载,相对 URL 资源则相对于工作目录(首个源文件为本地时)或相对于 base URL(首个源文件为远程时)查找;
- 带有
data-external="1"属性的元素会被原样保留,其链接内容不会被内嵌; - 限制:通过 JavaScript动态加载的资源无法内嵌,因此
--math-method=mathjax时字体可能缺失,离线自包含的 reveal.js 中缩放、演讲者备注等高级特性可能失效。
--self-contained是--embed-resources --standalone的已废弃同义词(MANUAL.txt)。
1.2 SVG 的两种内嵌策略
MANUAL 特别说明了对 SVG 图片的差异化处理(MANUAL.txt):
- 普通 SVG:生成
data:URI 形式的<img src="data:...">标签; - 带
inline-svg类的 SVG:直接插入内联<svg>元素。当同一 SVG 在文档中出现多次时,推荐使用这种策略,因为 pandoc 会利用<use>元素引用来减少重复。
命令测试 9652 正是围绕第二种策略构造的回归用例。
二、命令测试 9652:一个最小复现
测试文件 test/command/9652.md 完整内容如下:
% pandoc -f markdown -t html --embed-resources ```{=html} <img class="something inline-svg" src="command/9652.svg" /> ``` ^D <svg id="svg_b627f92299158b36552b" role="img" width="504.00pt" height="360.00pt" viewBox="0 0 504.00 360.00" class="something inline-svg please-do-not-delete-me"> </svg>这个命令测试文件是 pandoc 命令测试套件的标准格式:
- 第一行
% pandoc -f markdown -t html --embed-resources声明待执行的命令行; - 输入部分用 Markdown 的 fenced raw HTML 块(
```{=html})注入一个真实的<img>标签; ^D结束输入;- 之后是期望输出,pandoc 测试框架会将其与实际输出逐字节比对。
该用例对应的 SVG 源文件为 test/command/9652.svg,内容是一个空<svg>:
<?xml version='1.0' encoding='UTF-8' ?> <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='please-do-not-delete-me' width='504.00pt' height='360.00pt' viewBox='0 0 504.00 360.00'> </svg>测试意图:<img>的class是"something inline-svg",SVG 文件的class是"please-do-not-delete-me"。期望输出中,内联后的<svg>同时携带两组 class:"something inline-svg please-do-not-delete-me"——这正是 “Merge class attribute when both img and svg specify it”(当 img 与 svg 都指定 class 属性时合并二者)这一变更的回归验证,见 changelog.md。
三、源码实现:class 合并与<use>去重
3.1 整体管线
--embed-resources的核心实现在Text.Pandoc.SelfContained模块(src/Text/Pandoc/SelfContained.hs)。入口函数makeSelfContained(第 467-473 行)将输入 HTML 用 TagSoup 解析为标签流,初始化一个携带svgMap(哈希 → (id, SVG 属性))与fetchCache(资源抓取缓存)的状态,然后逐标签递归转换并重新渲染:
makeSelfContained :: PandocMonad m => T.Text -> m T.Text makeSelfContained inp = do let tags = parseTags inp let convertState = ConvertState { svgMap = mempty, fetchCache = mempty } out' <- evalStateT (convertTags tags) convertState return $ renderTags' out'3.2 判定inline-svg
在 convertTags 处理带源属性的标签时,首先判定是否为内联 SVG 场景:
convertTags (t@(TagOpen tagname as):ts) | any (isSourceAttribute tagname) as = do let inlineSvgs = tagname == "img" && case T.words <$> lookup "class" as of Nothing -> False Just cs -> "inline-svg" `elem` cs即:仅当标签是img且其 class 属性中(按空白切分后)含有inline-svg时才启用内联策略。isSourceAttribute覆盖了src、data-src、link标签的href、poster、data-background-image等源属性。
3.3 抓取与哈希
对每个源属性调用processAttribute(第 210-224 行)。当抓取结果 MIME 为image/svg+xml且当前是内联场景时,返回左值(hash, svgTags):
Fetched ("image/svg+xml", bs) | inlineSvgs -> do let hash = T.pack $ take 20 $ show $ hashWith SHA1 $ B.filter (/='\r') bs return $ Left (hash, getSvgTags (toText bs))注意两处细节:
- 哈希用SHA1对 SVG 原始字节计算并取前 20 个字符;
- 计算哈希前会过滤
\r,以保证 Windows 与非 Windows 平台上的测试结果一致。
getSvgTags(第 244-249 行)会丢弃<?xml>声明、注释以及</svg>之后的内容,只保留<svg>...</svg>片段。
3.4 首次出现:内联完整 SVG 并登记svgMap
首次遇到某哈希时(svgMap中无对应条目,第 183-208 行),将完整 SVG 内联到输出中,并计算一个稳定的id(SVG 自带 id 则沿用,否则用"svg_" <> hash),随后把(id, 合并后属性)存入svgMap:
Nothing -> case dropWhile (not . isTagOpenName "svg") tags of TagOpen "svg" svgattrs : tags' -> do let attrs' = combineSvgAttrs svgattrs svgImgAttrs let svgid = case lookup "id" attrs' of Just id' -> id' Nothing -> "svg_" <> hash ... modify $ \st -> st{ svgMap = M.insert hash (svgid, attrs'') (svgMap st) }这正是测试 9652 输出中<svg id="svg_b627f92299158b36552b" ...>的由来:该空 SVG 本身没有 id,于是 pandoc 依据内容哈希生成svg_前缀的 id。
此外,为了规避同一文档中多个不同 SVG 内部锚点(如fill:url(#gradient))的 id 冲突,代码会给内部所有id、xlink:href、href以及url(#...)引用加上svgid_前缀(addIdPrefix/fixUrl,第 193-206 行)。
3.5 重复出现:用<use>引用去重
当同一哈希再次出现(svgMap命中,第 170-181 行),不再重复内联,而是输出一个引用首次定义 id 的空壳<svg>:
Just (svgid, svgattrs) -> do let attrs' = [(k,v) | (k,v) <- combineSvgAttrs svgattrs svgImgAttrs , k /= "id"] return $ TagOpen "svg" attrs' : TagOpen "use" [("href", "#" <> svgid), ("width", "100%"), ("height", "100%")] : TagClose "use" : TagClose "svg" : rest'这就是 MANUAL 中所说“<use>元素用来减少重复”的实现:多份相同 SVG 在最终 HTML 中只有一份完整定义,其余通过<use href="#svg_...">引用。
3.6 class 属性合并:测试 9652 的核心断言
无论首次还是重复出现,最终属性都由combineSvgAttrs(第 251-288 行)计算。其合并逻辑为:
combinedAttrs = [(k, v) | (k, v) <- imgAttrs , k /= "class"] ++ [(k, v) | (k, v) <- svgAttrs , isNothing (lookup k imgAttrs) , k `notElem` ["xmlns", "xmlns:xlink", "version", "class"]] ++ mergedClasses mergedClasses = case (lookup "class" imgAttrs, lookup "class" svgAttrs) of (Just c1, Just c2) -> [("class", c1 <> " " <> c2)] _ -> []规则可归纳为:
- img 属性优先:
img上的非class属性全部保留;svg上已有的属性,若img也指定则被img覆盖; - 清理命名空间:
svg的xmlns、xmlns:xlink、version、class不直接透传; - class 合并(本次变更核心):当
img与svg都带 class 时,二者以空格连接合并为一个 class,即c1 <> " " <> c2。
套用到测试 9652:
- img class =
"something inline-svg"; - svg class =
"please-do-not-delete-me"; - 合并结果 =
"something inline-svg please-do-not-delete-me",与期望输出完全一致。
<svg id="svg_b627f92299158b36552b" role="img" width="504.00pt" height="360.00pt" viewBox="0 0 504.00 360.00" class="something inline-svg please-do-not-delete-me">这里的role="img"与aria-label由addRole/addAriaLabel(第 228-240 行)补加——内联 SVG 会丢失<img>的 alt 文本,因此用role="img"加aria-label(取自 alt)来维持可访问性。
此外combineSvgAttrs还处理viewBox/width/height的推算(第 253-261 行):若 svg 有 viewBox 而无 width/height,则从 viewBox 推导;若 img 提供了 width/height 而 svg 无 viewBox,则生成0 0 w h的 viewBox,并去掉数值末尾的.0。
四、围绕该测试的配套资源
4.1 SVG 源文件
测试引用的 test/command/9652.svg 是一个最小空 SVG,只携带 class、width、height、viewBox 四个属性。它以class='please-do-not-delete-me'命名,本身就是为了验证:即使 SVG 内部声明了 class,内联后该 class 也不会被丢弃,而是与 img 的 class 合并保留。
4.2 变更记录佐证
changelog.md 在Text.Pandoc.SelfContained条目下明确记录了这次行为变更:
Merge class attribute when both img and svg specify it (#9652, Carlos Scheidegger).
同时在 changelog 的构建/测试条目中还有 “Fix command test for #9652”(changelog.md),说明该命令测试作为回归用例被纳入测试套件。
4.3 测试套件运行方式
pandoc 的命令测试由 test/test-pandoc.hs 驱动。test/command/目录下的*.md文件即为用例,格式为“命令行 + 输入 +^D+ 期望输出”,框架执行命令后将实际输出与期望输出比对。9652 用例的输入通过 Markdown raw HTML 块(```{=html})注入原生<img>标签,因为目标场景是“HTML 中已有 img 标签”这一自包含处理阶段,而不是 Markdown 图片语法解析。
五、实战验证与注意事项
5.1 本地复现
在仓库根目录执行与测试等价的命令即可本地复现(需先构建 pandoc):
pandoc -f markdown -t html --embed-resources <<'EOF' ```{=html} <img class="something inline-svg" src="test/command/9652.svg" />EOF
预期输出即为内联后的 `<svg>`,其 class 为 `something inline-svg please-do-not-delete-me`。注意 `src` 需指向实际存在的 SVG 文件路径,相对路径按工作目录解析。 ### 5.2 实操要点 1. **启用内联 SVG 必须加 `inline-svg` 类**:普通 `<img>` 引用的 SVG 只会被转成 `data:` URI,不会内联展开; 2. **多实例去重**:同一 SVG 多次出现时,第二次起输出 `<use href="#svg_<hash>">`,节省文件体积——这是 MANUAL 推荐使用 `inline-svg` 的典型场景; 3. **class 会叠加**:img 与 svg 的 class 以空格拼接,可能超出预期,若不想保留 SVG 内部的 class,需在源 SVG 中去掉 class 属性; 4. **属性优先级**:img 上的属性(如 width/height)优先于 SVG 内部属性,可用于按引用位置微调尺寸; 5. **可访问性**:内联后 alt 文本转为 `role="img"` 与 `aria-label`,请确保为内联 SVG 的 img 提供有意义的 alt; 6. **数据 URI 兜底**:非内联场景(无 `inline-svg` 类)下,SVG 以 `data:image/svg+xml;base64,...` 形式嵌入,见 `makeDataURI`([src/Text/Pandoc/SelfContained.hs#L47-L58](https://link.gitcode.com/i/83ce8c49b035981e76ebf476eba22666#L47-L58)),文本类 MIME(`text/*`)走 URI 转义而非 base64,`+xml` 后缀还会剔除 `\r`。 --- ## 六、小结 命令测试 [test/command/9652.md](https://link.gitcode.com/i/1dbed53bdbbb9184baf8b7b74b28b085) 虽然只有短短九行,却精准锁定了 `--embed-resources` 处理 `inline-svg` 时的一个关键行为:**img 与 svg 同时携带 class 属性时,二者合并保留**。其背后是 [src/Text/Pandoc/SelfContained.hs](https://link.gitcode.com/i/83ce8c49b035981e76ebf476eba22666) 中 `combineSvgAttrs` 的 `mergedClasses` 逻辑,以及与 SHA1 内容哈希、`svgMap` 状态、`<use>` 去重、`role`/`aria-label` 补全共同构成的一整套自包含 SVG 处理管线。理解这条测试,也就理解了 pandoc 在离线 HTML 场景下对 SVG 资源的完整处理策略,可作为排查“内联后 class 丢失 / 出现多余 class / 重复 SVG 体积膨胀”等问题的直接依据。【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考