Quarto docx 表题与图题出框
拆掉 1×1 浮层表,用 Word「表题」「图题」居中独立段落
经验来自《中国旱区农业技术发展报告2026》第5章。复用入口:
../lua/docx-table-caption.lua(拆壳)与../lua/docx-table-layout.lua(列宽/表头)。前提与挂载见 harness 长文。图号空格是另一件事,见 quarto-crossref-tight-num.qmd。
书稿 docx 是出版社交付件。表题须在表外、用 Word 段落样式「表题」、居中;图题须在图下、用「图题」、居中。Quarto 交叉引用默认把题与主体塞进同一张 1×1 Word 表,样式还走 Image Caption 且左对齐。format.docx 的 YAML 调不出这个结构。
发生情形
章文件按书稿协议写 kable / ggplot,并给 chunk 加交叉引用:
#| label: tbl-ai-projects
#| tbl-cap: !expr tbl_cap#| label: fig-scatter-mtcars-probe
#| fig-cap: !expr fig_captbl_cap / fig_cap 仍是中文 + <br> + Table 5-N / Figure 5-N。html 预览看起来正常。打开 docx 则:
| 位置 | 未修时 | 期望 |
|---|---|---|
| 表题 | 在 1×1 外层表单元格里,与表体同属一张表 | 表外独立段落 |
| 图题 | 同上,与图同属一张表 | 图外独立段落,位于图下 |
| Word 样式 | Image Caption(图、表都是) |
「表题」/「图题」 |
| 对齐 | 段落里写死 jc=left,看起来靠左 |
居中 |
| 书签 | 包住整块外壳单元格 | 锚在题段落上 |
第2章同一套 #| label: tbl- / fig- 会同样中招。只改 reference-doc 的 Table 边框会让题更像表的一部分。
根因
三层叠在一起。源文件的 glue / kable() / ggplot 没有写错。
1×1 浮层表(主因)
#| label: tbl-... / fig-... 加 #| tbl-cap / fig-cap 会生成 Quarto FloatRefTarget。docx 渲染器(floatreftarget.lua)为了对齐,用 pandoc.SimpleTable 包一层:
- 外层
w:tbl(样式Table,一列一行) - 单元格里是一个带 identifier 的
Div - Div 里先(或后)放题,再放表或图
这是已知行为,不是本仓写错。相关讨论:quarto-cli #11756、#10587、#4004、#9655。
没有交叉引用的裸 Pandoc 表,题会写成表外段落、样式 Table Caption。加上 #| label: tbl- 后走 float,这条路径被换掉。
题用错样式
Pandoc 默认表题是 Table Caption。Quarto 交叉引用一律走 docxDivCaption(),往段落里塞一段 OpenXML,写死 w:pStyle=ImageCaption。图、表都是 Image Caption。模板里即使有 Table Caption,成品也用不上。
左对齐盖住样式
同一段 OpenXML 还写死 w:jc w:val="left"(渲染器把 divCaption 的 align 固定成 "left")。模板里「表题」「图题」即使设了居中,段落级 jc=left 仍会盖过去。只改 reference-doc 的样式定义,Word 里看起来还是靠左。
模板起初没有中文样式名
template/create-template-tiny.docx 在本包中对应 ../reference-doc.docx。原有的是 Pandoc 英文化名 Table Caption / Image Caption。出版社要的是样式名「表题」「图题」。custom-style="表题" 而模板没有该样式时,Word 会静默回落正文。
修复方案
Lua 滤镜拆壳 + reference-doc 备齐样式。不要改成 officedown,也不要去掉 #| label:。
滤镜做什么
../lua/docx-table-caption.lua,仅 docx,at: post-render(须排在 crossref-tight-num.lua 后面,此时「表5-1」「图5-1」已经写好)。
- 认出 1 列 1 行的外壳
Table。 - 摊平单元格里 Quarto 放的
Div,取出tbl-/fig-identifier。 - 把题与主体分开:主体是数据
Table或含图的Para/Image;题是「表N-」/「图N-」或带ImageCaptionOpenXML 的段落。 - 表:先题后表。图:先图后题。
- 题包进
Div,custom-style为「表题」或「图题」;书签放在题上。 - 把 OpenXML 里的
ImageCaption改成BiaoTi/TuTi,并把jc=left改成center。
不要给 Para / Table 赋 .attr:Quarto 1.8 的 walker 会报 Trying to set unavailable property attr。题用带 custom-style 的 Div。
html 不跑这套拆壳(quarto.doc.is_format("docx") 或 FORMAT 匹配 docx)。专栏/callout 也用表画框,但没有「表N-」题 + 内层数据表,不会被拆。
模板里要有两个样式
在 ../reference-doc.docx 的 word/styles.xml:
| styleId | w:name |
克隆来源 | 用途 |
|---|---|---|---|
BiaoTi |
表题 | Table Caption | 表外题 |
TuTi |
图题 | Image Caption | 图下题 |
二者都 basedOn caption(ab),jc=center,等线、加粗、小五(sz=21),并加 qFormat 以便 Word 样式廊看得见。滤镜的 custom-style 对的是 样式名「表题」「图题」,不是 styleId。
linebreak.lua 仍负责把 fig_cap / tbl_cap 里的 <br> 变成 Word 换行。单章 YAML 若不挂它,中英双行会挤在一段里。
挂载位置(三处都要)
被渲的章若不在 _quarto.yml 的 project.render 列表里,只写根配置时单独 quarto render path/to/chapter.qmd 可能吃不到滤镜。
| 文件 | 路径 |
|---|---|
项目 _quarto.yml |
lua/docx-table-caption.lua、lua/docx-table-layout.lua |
子目录 _metadata.yaml |
../lua/… |
| 章 YAML | 与该文件相对 lua/ 的路径 |
都带 at: post-render。章 YAML 建议写成:
filters:
- ../lua/linebreak.lua
- path: ../lua/crossref-tight-num.lua
at: post-render
- path: ../lua/docx-table-caption.lua
at: post-render
- path: ../lua/docx-table-layout.lua
at: post-render从仓库根目录渲染。章内有行内 r 时不要用 --no-execute。
源文件怎么写
表、图的 qmd 协议不变:
num_tbl/num_fig加一。tbl_cap/fig_cap:中文 +<br>+Table 5-N/Figure 5-N。- 制表/绘图 chunk:
#| label: tbl-...或fig-...,tbl-cap/fig-cap: !expr ...。 add_footer_asis(cap_note, cap_source)。- 正文
见 @tbl-.../见 @fig-...(@两侧留空格)。
不要为了躲浮层而去掉 #| label:,正文交叉引用会断。不要把题改成手工 Markdown 段落来「骗」Pandoc。
第5章曾用 mtcars 散点图(fig-scatter-mtcars-probe)验收图题出框;定稿前可删。表题用章内三张 kable 表即可验收。
表列宽与表头
拆壳之后,内层数据表的列宽问题会更明显:Pandoc 按 Markdown 字符数估列宽(CJK 与 ASCII 都算 1),再写 tblLayout=fixed。第5章「序号」「年份」只有两三个字,列宽落到约 346 twips;Table 样式左右边距各 108,内容区只剩约 130 twips,小五字号下「2024」会折成三四行,整张表被撑高。
kable(align="c") 和拆壳滤镜都不是主因:前者只管对齐,后者把内层 colspecs 原样带出。模板 Table 样式原先只有 band1Horz,没有 tblStylePr type="firstRow",所以 Word 即使写了 tblHeader / firstRow=1,表头看起来仍与表体一样(Compact、不加粗)。
../lua/docx-table-layout.lua 排在拆壳之后、仅 docx:
- 按显示宽度量列(CJK=2,ASCII=1),短列(显示宽度 ≤ 6)保底约 11%,再归一化到 100%。
- 表头 inlines 包进
Strong,单元格仍用Plain(Pandoc 才会写成 Compact / 五号)。不要改成Para,否则丢掉 Compact,回落到文档默认小四(sz=24)。 - 跳过不足 2 列的表(callout 外壳)。
模板 Table 样式补 firstRow 加粗,并钉 sz=21 / szCs=21(五号,与 Compact 表体一致)。不要把每张表手写 tbl-colwidths 当默认方案,也不要为此改成 flextable / officedown。
不要做的事
- 指望
_metadata.yaml的format.docx再加几个键拆壳。 - 只用
reference-doc改Table边框来「修」题(题会跟着吃边框)。 - 去掉
#| label: tbl-/fig-或改成 officedown / flextable 另写一套交叉引用。 - 渲完用 Python 改
document.xml(书签和升级都难收;只当滤镜失败的后备)。 - 滤镜里给
Para/Table写.attr。 - 见 1 列表就拆:会拆掉 callout。
- 同一滤镜对 html 拆壳。
- 承诺 Word「插入题注」那种
SEQ域。Quarto 的「表5-1」是静态字,更新域不会重编号。 - 靠每张表手写
tbl-colwidths或改成 flextable / officedown 来修列宽。
验收
渲一章 --to docx(来源仓曾用 report/chapter5-special.qmd)。抽查 OOXML 或 Word:
- 表题在表外,
pStyle为BiaoTi,jc=center,下一元素是数据表。 - 图题在图下,
pStyle为TuTi,jc=center;图与题都不在 1×1 外壳里。 - 无
ImageCaption落在题上。 - 书签
tbl-.../fig-...在题段落上;正文「见表5-1」「见图5-1」仍能跳转。 - 表下/图下的
add_footer_asis注仍紧跟主体。 - 中英双行中间有
w:br。 - html 预览结构无变化。
- 数据表「年份」「序号」列宽足够「2024」单行;表头加粗,字号与表体同为五号(Compact /
sz=21),不要回落小四。
可用 Python 抽 word/document.xml:搜 BiaoTi / TuTi,确认题段落的 inside_tbl 为假;抽 tblGrid 看短列不再是 346 twips。
复用清单
新章若仍交付 docx:
- 模板:确认有「表题」「图题」,居中;
reference-doc指向该模板。 - 三处挂
docx-table-caption.lua与docx-table-layout.lua,at: post-render;同时挂linebreak.lua与crossref-tight-num.lua。布局滤镜须排在拆壳之后。 - 源文件继续用
#| label:+tbl-cap/fig-cap,不要改协议。不要靠每张表手写tbl-colwidths当默认方案。 - 先渲一章 docx,抽查表外/图下、样式名、居中、书签、短列(年份)不折成多行、表头加粗。