
本文介绍一种基于 XSLT 2/3 的两阶段 FO 处理方案:先生成中间 FO 文档,再通过第二遍 XSLT 转换识别文本中的上标标记(如 ^1),并精准将对应脚注内容插入到所在页面的页脚区域,全程无需修改原始 XML 或 PDF 渲染逻辑。
本文介绍一种基于 xslt 2/3 的两阶段 fo 处理方案:先生成中间 fo 文档,再通过第二遍 xslt 转换识别文本中的上标标记(如 `^1`),并精准将对应脚注内容插入到所在页面的页脚区域,全程无需修改原始 xml 或 pdf 渲染逻辑。
在使用 Apache FOP 生成 PDF 时,若需实现「根据文本中动态标记(如 ^1)自动定位并跨页注入脚注」,标准单次 XSL-FO 转换无法满足——因为 FO 是静态排版描述语言,不具备运行时页面布局感知能力。真正的解决方案在于将逻辑拆分为两个可编程的 XSLT 阶段,利用 XSLT 2/3 的原生节点序列处理能力,而非依赖 FOP 自身扩展。
Apache Superset 是一个广泛采用的开源 BI 平台,用于 SQL 探索、图表构建和仪表板交付。当代理需要查询仓库数据、组装仪表板或使用成熟的分析界面解释指标而不是临时笔记本代码时,此技能非常有用。
✅ 核心思路:两阶段 XSLT 流水线
- 第一阶段(first-sheet.xsl):将原始 XML 转为语义清晰、结构规范的中间 FO(含可识别的标记节点,如 <fo:inline data-footnote-ref="1">);
- 第二阶段(second-pass.xsl):接收第一阶段输出的 FO 文档作为输入,扫描所有含 data-footnote-ref 属性的内联元素,提取引用编号,并结合外部脚注数据源(可通过 document() 函数加载原始 XML 中的 <footnotes>),最终在每个 <fo:page-sequence> 的 <fo:static-content flow-name="xsl-region-after"> 中动态注入对应脚注内容。
? 示例第二阶段 XSLT(XSLT 2.0+)
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- 导入第一阶段样式表(可选,用于复用模板) -->
<xsl:import href="first-sheet.xsl"/>
<!-- 加载原始 XML 中的脚注数据(假设路径为 'input.xml') -->
<xsl:variable name="footnotes" select="document('input.xml')//footnotes"/>
<!-- 主入口:应用第一阶段结果,并进入 step2 模式 -->
<xsl:template match="/">
<xsl:variable name="first-result">
<xsl:apply-imports/>
</xsl:variable>
<xsl:apply-templates select="$first-result/node()" mode="step2"/>
</xsl:template>
<!-- step2 模式:深拷贝所有节点,仅对 page-sequence 特殊处理 -->
<xsl:mode name="step2" on-no-match="shallow-copy"/>
<!-- 重写 page-sequence:注入脚注区域 -->
<xsl:template match="fo:page-sequence" mode="step2">
<xsl:copy>
<xsl:copy-of select="@*"/>
<!-- 保留原有 static-content -->
<xsl:apply-templates select="fo:static-content" mode="step2"/>
<!-- 注入脚注区域(若尚不存在) -->
<xsl:if test="not(fo:static-content[@flow-name='xsl-region-after'])">
<fo:static-content flow-name="xsl-region-after">
<fo:block font-size="8pt" text-align="left" padding-top="2pt">
<xsl:for-each select=".//fo:inline[@data-footnote-ref]">
<xsl:variable name="ref" select="@data-footnote-ref"/>
<xsl:if test="position() = 1">
<fo:leader leader-pattern="rule" rule-thickness="0.5pt" />
</xsl:if>
<fo:inline font-weight="bold"><xsl:value-of select="$ref"/>.</fo:inline>
<xsl:text> </xsl:text>
<xsl:value-of select="$footnotes/*[name() = concat('f', $ref)]"/>
<xsl:if test="position() != last()"><fo:leader leader-pattern="space" /></xsl:if>
</xsl:for-each>
</fo:block>
</fo:static-content>
</xsl:if>
<xsl:apply-templates select="fo:flow" mode="step2"/>
</xsl:copy>
</xsl:template>
<!-- 关键:将 ^N 替换为带 data-footnote-ref 的 inline 元素(应在第一阶段完成) -->
<!-- 示例(第一阶段模板片段):
<xsl:template match="text()" mode="markup">
<xsl:analyze-string select="." regex="\^(\d+)">
<xsl:matching-substring>
<fo:inline data-footnote-ref="{regex-group(1)}" font-size="8pt" vertical-align="super">^<xsl:value-of select="regex-group(1)"/></fo:inline>
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select="."/>
</xsl:non-matching-substring>
</xsl:analyze-string>
</xsl:template>
-->
</xsl:stylesheet>⚠️ 关键注意事项
- FOP + Saxon 组合:Apache FOP 本身只支持 XSLT 1.0,因此必须替换其内置 XSLT 引擎。推荐将 Saxon HE(v10+)JAR 加入 classpath,并在 Java 调用中显式指定 TransformerFactory 实现类(如 net.sf.saxon.TransformerFactoryImpl)。
- 脚注去重与分页限制:上述示例将所有脚注统一置于每页页脚。若需「每页仅显示本页出现的脚注」,需借助 xsl:for-each-group 按页面位置分组(需配合 fo:page-number-citation 或自定义 ID 机制),但 FOP 对跨页引用支持有限,更稳健的做法是预计算各页内容范围后生成分页 FO。
- 数据传递方式:第二阶段无需额外传入 XML——通过 document('input.xml') 直接读取原始文件即可;也可将脚注数据作为参数传入(<xsl:param name="footnotes-xml" as="node()"/>),提升灵活性与测试性。
- 性能权衡:两阶段转换会增加内存开销(中间 FO 树驻留),建议对大型文档启用流式处理或分块转换。
综上,该方案不依赖 FOP 扩展,完全基于标准 XSLT 2/3 能力,既保持原始排版逻辑不变,又实现了语义化、可维护的脚注动态注入,是面向复杂 PDF 排版需求的工业级实践路径。

















