# 配置：孤行与各栏齐底

> 让行与块在换栏处保持在一起的规则，以及让每一栏底部齐平的调整手段

- HTML版本: https://postext.dev/zh/docs/configuration-page-flow
- 最后更新: 2026-10-16
- 阅读时间: 15分钟
- 其他语言: [en](https://postext.dev/en/docs/configuration-page-flow.md), [es](https://postext.dev/es/docs/configuration-page-flow.md), [ca](https://postext.dev/ca/docs/configuration-page-flow.md), [de](https://postext.dev/de/docs/configuration-page-flow.md), [fr](https://postext.dev/fr/docs/configuration-page-flow.md), [it](https://postext.dev/it/docs/configuration-page-flow.md), [pt](https://postext.dev/pt/docs/configuration-page-flow.md), [ja](https://postext.dev/ja/docs/configuration-page-flow.md), [ar](https://postext.dev/ar/docs/configuration-page-flow.md)

## 简单来说

本页讲Postext怎样决定一栏或一页在哪里结束。一个段落不应只留一行在栏顶或栏底，标题也不应和它下面的正文分开。这些规则可能让某一栏比其他栏短。这时Postext会按固定的顺序做一些小调整，让每一栏在同一高度结束。本页列出每条规则和每种调整，以及怎样查看用了哪一种。

## 段首孤行、段末孤行、孤字与不拆分规则

这些缺陷值背后的机制见[断词与两端对齐](https://postext.dev/zh/docs/justification.md)。本节是驱动它们的`bodyText`配置键的参考。

除了断词和间距上下限，正文配置还提供一组软规则，用来防止在结构上别扭的地方拆开段落。它们全部作为缺陷值（demerits）输入Knuth-Plass断行算法：让版面倾向于干净的断点，但从不强制执行硬性规则。把某个`*Penalty`值设为`0`，就相当于关闭对应的规则。

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `avoidOrphans` | `boolean` | `true` | 避免段落在下一栏顶部只剩不足`orphanMinLines`行就结束。在`japan`地区的字符网格（`cjk.grid`）上默认为`false`。 |
| `orphanMinLines` | `number` | `2` | 段落被拆开时，下一栏顶部至少要有的行数。仅在`avoidOrphans`为`true`时起作用。 |
| `orphanPenalty` | `number` | `1000` | 违反段末孤行（orphan）约束时加上的缺陷值。值越高，算法越强烈地避免段末孤行；`0`关闭该惩罚。 |
| `avoidOrphansInLists` | `boolean` | `true` | 为`true`时，列表项也受段末孤行保护（不只是段落）。仅在`avoidOrphans`为`true`时有效。 |
| `avoidWidows` | `boolean` | `true` | 避免段落在当前栏底部只有不足`widowMinLines`行就开始。在字符网格（`cjk.grid`）上默认为`false`：页面排满网格所数的行数。 |
| `widowMinLines` | `number` | `2` | 段落被拆开时，当前栏底部至少要有的行数。仅在`avoidWidows`为`true`时起作用。 |
| `widowPenalty` | `number` | `1000` | 违反段首孤行（widow）约束时加上的缺陷值。`0`关闭该惩罚。 |
| `avoidWidowsInLists` | `boolean` | `true` | 为`true`时，列表项也受段首孤行保护。仅在`avoidWidows`为`true`时有效。 |
| `shortListSplit` | `'allow' \| 'warn' \| 'avoid'` | `'allow'` | 短列表被拆在两栏或两页之间：正文中不超过`shortListMaxItems`项的列表，例如三项的列表排成1 + 2。`avoidOrphansInLists`和`avoidWidowsInLists`让同一列表项的各行保持在一起，这条规则让同一列表的各项保持在一起。`'allow'`：这类列表和其他列表一样，在栏结束处拆开。`'warn'`：排法相同，并报告每个被拆开的列表（一条`shortListSplit`警告，见[文档中的警告](https://postext.dev/zh/docs/configuration-programmatic-usage.md#文档中的警告)）。`'avoid'`：列表整体排出；栏中已有内容的下方放不下时，它移到下一栏，紧挨在它上面的标题和引出它的冒号行（`keepColonWithList`）随它一起移走。比空栏还高的列表和其他列表一样拆开，并报告。`:::callout`内的列表遵循框自己的拆分规则，`:::toc`和`:::index`的条目不算列表。postext 1.28起。 |
| `shortListMaxItems` | `number` | `3` | 对`shortListSplit`而言，列表最多有多少项才算短列表，嵌套列表的项也计算在内。取整数，至少为1。postext 1.28起。 |
| `avoidRunts` | `boolean` | `true` | 避免段落以很短的末行结束，即*孤字*，例如单独一个短词。配合`optimalRagged`，不齐行文字也适用。中文、日文或韩文段落不会以只有一个字符（单独或连同其后的收尾标点）的行结束（孤字）：只要上一行在字距上限之内仍能两端对齐，就把最后一个字符让给这一行。左对齐、右对齐或居中的行同样让出这个字，因此少一个字；设置`cjk.grid.fullLines`（稿纸）后，这样的行每个格子都填满，段落可以以一个字结尾。 |
| `runtMinCharacters` | `number` | `20` | 段落末行的阈值，以词间空格而不是字母计数：末行宽度小于`runtMinCharacters × normalSpaceWidth`像素时即为孤字。在大多数正文字体中，一个词间空格是四分之一到三分之一em，约为半个小写字母宽，所以默认值20捕捉的是短于4到7 em的末行，大约8到12个字母。如果想捕捉短于约N个字母的末行，把它设为2 × N左右。 |
| `runtPenalty` | `number` | `1000` | 注入Knuth–Plass平方缺陷值公式中的等效劣度（与行的*劣度*同一尺度，劣度在10000处饱和）。`0`关闭该惩罚。 |
| `gradedRuntPenalty` | `boolean` | `false` | 按末行短了多少来缩放孤字惩罚：宽度为*w*、低于阈值*t*的末行，代价是`runtPenalty × (1 − w / t)`，而不是整个惩罚值。这样两个词的结尾比一个词的代价低，当上面某一行能匀出一个词时，断行器会把它拉下来（'…sallies of' / 'our minds.'，而不是'…sallies of our' / 'minds.'）。默认关闭：每个孤字代价相同，断行器保留上面较紧的行。 |
| `avoidRuntsInLists` | `boolean` | `true` | 为`true`时，列表项也计入孤字惩罚。仅在`avoidRunts`为`true`时有效。 |
| `tightenRunts` | `boolean` | `true` | 惩罚无法避免孤字时，改为把段落排短一行：词间距收紧（不低于`minWordSpacing`），如果单靠这一点还挤不下，再加上少量负字距。以下情况拒绝排短，孤字保留：会把一行两端对齐的文字拉伸到超过`maxWordSpacing`，或段落中已有更松的两端对齐行时超过那一行；或者会让齐左排出的行（超过正常空格3倍）比原来更多。不齐行文字的词间距保持原宽，所以那里只有字距参与调整。需要开启`optimalLineBreaking`和`avoidRunts`（不齐行文字还需要`optimalRagged`）。 |
| `maxRuntTracking` | `number` | `10` | 修正孤字时最多可以使用的字距，单位为千分之一em（InDesign的单位：10 = 每个字符0.01 em），以收紧的方式施加。`0`表示只靠词间距来修正。 |
| `slackWeight` | `number` | `10` | 施加在“未用栏空间”平方代价上的权重。值越高，版面越倾向于把栏填满；`0`完全取消空白压力。 |
| `keepColonWithList` | `boolean` | `true` | 段落以直接引出列表的冒号结尾时，让带冒号的末行与列表保持在一起：如果放下这个段落后，第一个列表项没有空间在同一栏或同一页开始，就把末行（段落只有一行时则是整个段落）连同列表移到下一栏。多少空间才算够由`colonListRoom`决定。当这条规则要把整个段落推走，而栏中紧挨在它前面的是一串标题时，这些标题也会一起移走，以便`headings.keepWithNext`继续成立。 |
| `colonListRoom` | `'item' \| 'line'` | `'item'` | `keepColonWithList`要求在冒号行下面留出的空间。`'item'`：列表的段首孤行和段末孤行规则会在栏底留下的第一个列表项的部分，允许在那里拆开时为一行，规则要求保持完整时为整个列表项（比如一个两行的列表项）。`'line'`：一行，与postext 1.4及更早版本相同；这时被那些规则保持完整的第一个列表项会单独移到下一栏，把冒号行留在栏底。在`configVersion` 6之前保存、且书中有以冒号引出的列表的配置，读取时采用`'line'`（见[postext 1.4及更早版本写出的文件包](https://postext.dev/zh/docs/configuration-bundles.md#postext-14及更早版本写出的文件包)）。其他任何值都按`'item'`读取。 |
| `hyphenateAcrossColumns` | `boolean \| 'withinPage'` | `true` | 允许一栏或一页以断词结束（InDesign的*Hyphenate Across Column*）。设为`false`时，跨越分栏处的段落会重新断行，使它在本栏的最后一行以完整的词结束；差额由上面各行的词间距在`maxWordSpacing`和`minWordSpacing`范围内吸收。这是一种偏好：如果在这些范围内找不到能避免断词的断点，连字符就保留。它会尝试段落的每一个分栏点：第一个分栏点，以及之后落在整栏结束处的分栏点，在一次重新断行中处理；之后落在其他位置的分栏点（遵守段首孤行规则、在截齐的分栏带处）则从那一栏开始另做一次重新断行，前面各栏已排好的行保持原有断点，只重排其余部分。`'withinPage'`只守护结束一页的那些行，别的行不管：一个词可以断在同一页的两栏之间，而一页的最后一行只要范围允许就以完整的词结束。它改动的行比`false`少，这在三栏及以上的版式里有分量，因为每三个栏尾中有两个在页内；单栏时两个值排出的行相同。如果一页中某栏之后再没有接收文字的栏，这一栏就算结束它所在的页：专放浮动体的边栏不接收文字，被表格或图片占满的栏也不接收。框的正文不受影响。开启`optimalRagged`时，不齐行段落也同样重新断行：它的词间距保持原宽，所以只有行尾移动。每次重新断行都要再测量一遍段落，因此栏尾断词很多的书排版会稍慢一些。需要开启`optimalLineBreaking`，不齐行文字还需要`optimalRagged`。 |
| `paragraphContainerSpacing` | `'collapse' \| 'add'` | `'collapse'` | 以段落收尾的`:::paragraphs`容器下方、即该段落与其后块之间的间距（见[`:::paragraphs`容器](https://postext.dev/zh/docs/configuration-styles.md#paragraphs容器)）。`'collapse'`：取样式的`spaceBetween`和`marginBottom`以及容器周围文字的段间距（开启`paragraphSpacing`时为一行；在框内则取框的段间距）中的较大者，再与下一个块自己在上方保留的间距合并，与连续正文中两个段落之间的处理相同：参考文献下面的标题与它相距自己的`marginTop`，而不是这个外边距再加上条目的间距。`'add'`：与postext 1.4及更早版本相同，在网格对齐前只在最后一行下面放样式自身的间距，再加上下一个块自己的上方间距，不计段间距，所以容器之后的段落可能比其他任何段落都更贴近它。在`configVersion` 8之前保存、声明了段落样式、且书中含有这种容器的配置，读取时采用`'add'`（见[postext 1.4及更早版本写出的文件包](https://postext.dev/zh/docs/configuration-bundles.md#postext-14及更早版本写出的文件包)）。无论哪种方式，负的`marginBottom`都会把下一个块往上拉。段落样式可以为自己的容器另定规则（`containerSpacing`，自postext 1.28起）。 |

**关于孤字**。孤字是指段落的末行太短，不像一行正常的文字，通常是一两个短词孤零零地落在段落末尾。由于判断依据是这一行的像素长度与正常空格宽度之比，`runtMinCharacters`会自动适应当前字号。视觉宽度超过`runtMinCharacters × spaceWidth`的短词没有问题；比这窄（或真正单独一个）的词会招来孤字惩罚。阈值以词间空格计数，而词间空格约为字母宽度的一半：默认值20相当于大约8到12个字母的末行。每个孤字都承担完整的惩罚值，所以在两个都低于阈值的结尾之间，断行器会保留上面较紧的行；`gradedRuntPenalty`则按短缺程度给每个结尾定价，较长的结尾胜出。给对数学感兴趣的读者：在默认的`runtPenalty`为1000时，避免孤字的优先级高于任何需要大约r≈2.15以内词间距拉伸的其他断点组合。

**软规则，不是硬规则。**这些规则都不能*阻止*某个断点，引擎总会给出一个版面。它们是缺陷值：算法在一次全局优化中权衡劣度、断词代价、适配等级的平滑度和这些结构性惩罚，选出总代价最低的断点组合。如果需要更硬的保证，就提高惩罚值；如果某篇文档放宽惩罚后读起来更好，就降低它。

## 各栏齐底

出版方要求每一栏都从页面顶部开始、在页面底部平齐结束。分页规则（段首孤行和段末孤行保护、标题与正文不分离、不可拆分的图）自然会留下短栏，即栏底空着一个或多个基线网格行。启用齐底后，引擎会像排版工人那样处理，按编辑上的优先顺序使用以下手段：

1. **收尾的框**：结束一个短栏的标注框整体下移，下移量正好等于其底部下方的空余，使它的下边缘落在页面最后一个网格位置上，与相邻栏的最后一行平齐。即使空余不足一行，只要没有内容移入另一栏，它也会占满这段空余。默认情况下这一手段先于其他所有手段执行，并占用整段空余，所以一个注释上方段落的框可能最后落在该段落下方好几行处；`closingBox: 'last'`让标题、列表结尾和其他间距手段先占用整行，框只占它们剩下的部分；`closingBox: 'off'`则从不移动它。
2. **有安全区的图片**：设了安全区（`Resource.safeArea`，见[文档格式 › 安全区](https://postext.dev/zh/docs/document-format-resources.md#安全区)）的图片，如果以行内方式排在短栏里，或者作为只占这一栏的浮动体排在栏顶或栏底，就按整数个网格行增高，在安全区以内裁切，直到这一栏排满，或者裁切到了安全区为止。图片变高不会在页面上留下空洞，所以这一手段在增加任何空白之前执行；在栏首要保持平齐的页面或区段上（见下文），位于栏顶的浮动体不会增高。记录为`flexFigure`。这一手段没有单独的设置：只有资源标出了安全区的图片才会增高。
3. **标题**：在短栏内各标题的上外边距中增加整数个网格行。需要多行且栏内有多个标题时，这些行分配给各个标题，最重要的标题总是分得最多（`h2`比`h3`多）。位于栏顶的标题从不加空，因此各栏始终从页面顶部开始。唯一的例外是：在文字流继续到下一页的页面上，紧接在栏首图或表下方的标题，空间会加在这个标题上方、图的下方。一个标题这样最多先加`maxLinesBeforeLoose`行（默认两行），然后轮到后面的手段；只有它们再也补不了时，标题才继续加行，直到`maxLinesPerHeading`。
4. **列表结尾**：标题无法吸收全部空余时，在列表或编号列表结束处增加一个网格行（列表后的空白读起来很自然），每个列表结尾有上限。
5. **公式、框和图的下方**：接着，在独立公式或标注框之下、文字接续之处增加一个网格行（`stretchAfterDisplays`；读起来像该元素自己的边距），再在栏首的图或表之下增加（`stretchAfterFloats`，上限为`maxLinesAfterFloat`）。
6. **放松段落**：作为最后手段，把栏内某一段重新断行，使其多出一行（即TeX的`\looseness=+1`），并选最长的段落，让增加的词间距分散得看不出来。只有每一行都保持在**`bodyText.maxWordSpacing`以下**时，放松后的方案才会被采用，文字灰度绝不超出你已配置的限度。需要启用`bodyText.optimalLineBreaking`。

在字符网格上（`cjk.grid`，见[中文排版 › 字符网格](https://postext.dev/zh/docs/chinese-layout.md#字符网格)），每个字占一个一em宽的格子，每一行都落在网格的行距上，这个行距同时也是基线网格。和竖排一样，网格上默认不做齐底：在网格上逐行排的页面，就像GB/T 9704按每页22行、每行28字计数那样，短栏就保持短栏。配置中设了`enabled: true`时，使用的是保持网格的手段：标题、列表结尾、行间公式和图都按整数个网格行加空，其下的文字整行下移；对于没有空行可让的标准，`gridLines: 'off'`把这些手段排除在外。中文或日文段落只有在任意两个字之间的间距都不超过`maxTracking`时才多排一行，也不对其中的字符另加字距：少一个字的行要把一个em分摊到各个字间，远超默认的0.01 em，所以在由整格组成的网格上，放松段落的手段通常找不到可用的段落。结束一栏的框和有安全区的图片本来就不受网格约束，在这里与在别处一样起作用。

只有当页面自然延续到下一页时，才会对该页最后一栏做齐底；一章的末页本来就可以短一截。这样的页面，以及由`trailing`截平的收尾区段，还会让各栏的栏首保持平齐：栏首为图或表时，不会在其下方加行（浮动体后手段）；在这样的图下方开栏的标题或框，无论`stretchAfterFloats`如何设置，都留在栏首；紧接在`span: 'page'`章首标题之下、与它同处一栏的标题也是如此。这样就不会为了让栏底对齐而让某一栏比旁边一栏起得更低。

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | 是否让各栏底部平齐。在竖排（`layout.writingMode: 'vertical-rl'`）和字符网格（`cjk.grid.enabled`，postext 1.25 起）上默认关闭；设为`true`时在那里也会开启。 |
| `maxLinesPerHeading` | `number` | `4` | 单个标题上方最多可增加的网格行数。 |
| `maxLinesBeforeLoose` | `number` | `2` | 在尝试后面的手段（列表结尾、公式和框、图、放松段落）之前，一个标题上方最多先加的网格行数。只有这些手段再也补不了时，标题才继续加行，直到`maxLinesPerHeading`；这样，只有一个标题的页面不会在段落还能多排一行时就在标题上方空出四行。取值不小于`maxLinesPerHeading`时，标题先加满全部行，与postext 1.27及之前相同；`0`表示先试其他手段，再在标题上方加行。自postext 1.28起；此前保存的文档保持原来的顺序（`pinLegacyLooseOrder`）。 |
| `stretchAfterLists` | `boolean` | `true` | 标题无法吸收全部空余时，允许在列表结束处增加网格行。 |
| `maxLinesAfterList` | `number` | `1` | 单个列表结尾之后最多增加的网格行数。 |
| `stretchAfterDisplays` | `boolean` | `true` | 在列表结尾手段之后，允许在短栏内的独立公式之下、标注框之下各增加一个网格行：其后的块下移一行。设为`false`时，公式和框下方的空白只由它们自己的边距决定；空余交给后面的手段，若仍补不满，该栏就短一截结束。自postext 1.28起。 |
| `stretchAfterFloats` | `boolean` | `true` | 在列表结尾手段之后，允许在短栏栏首的图或表（顶部浮动体）下方增加网格行，让其下的文字下移，而不是让该栏短一截结束。在不延续到下一页的页面（一章的末页）上，以及由`trailing`截平的收尾区段中，从不这样做：那里各栏栏首保持平齐，最后一栏可以短一行结束。图下方的第一个块可能是前一页开始的段落的余下部分：它同样会下移，下移量是分页规则在栏底留出的那一行（段首孤行（widow）规则留空的一行，或者一个段落间距，其后已放不下文字）。设为`false`可让文字紧贴在图下方。浮动体跨多栏时，其下各栏要么一起下移，要么都不动，见`stretchAfterWideFloats`。 |
| `maxLinesAfterFloat` | `number` | `1` | 单个顶部浮动体下方最多增加的网格行数。 |
| `stretchAfterWideFloats` | `'flush' \| 'always'` | `'flush'` | 用于跨两个及以上文字栏的图或表之下。取`'flush'`时，只有该浮动体下方的每个文字栏都加上同样的行数（各栏栏首因此保持平齐），或者所加的行正好补满该栏时，才让一栏下移；会让某一栏栏首下移、而该栏仍然短一截的行不会加上，空余交给放松段落。`'always'`把这样的栏当作任何浮动体下方的栏来处理，与postext 1.27及之前相同。其他取值都按`'flush'`处理。自postext 1.28起；此前保存的文档保持`'always'`（`pinLegacyWideFloatStretch`）。 |
| `looseParagraphs` | `boolean` | `true` | 最后手段：在`bodyText.maxWordSpacing`范围内，把短栏中的段落重新断行，放松一行（每段多一行）。 |
| `maxLooseParagraphs` | `number` | `2` | 同一短栏中最多可以有几个段落多排一行，从最长的段落开始。 |
| `trackParagraphs` | `boolean` | `true` | 单靠词间距无法多出一行时，放松的段落还可以采用恰好能多出这一行的最小正字距。 |
| `maxTracking` | `number` | `10` | 该字距的上限，单位是每个字符千分之一em（10 = 0.01 em）。 |
| `trailing` | `boolean` | `true` | 截平一章和整个文档的收尾区段：当文字流在页面排满之前结束（遇到章首页、`:::part`、结束一章的`placement: 'fixed'`框或文档结尾），且各栏长短不齐时，把它们截成一样高。区段上限为`ceil(Σ used / N / grid)`行，在上述手段处理完前面各页之后才计算。这样，一份较短的参考文献在每一栏都结束于同一高度，而不是排满第一栏、让最后一栏空一半。截平时遵守未截平区段所遵守的规则：如果一个不能跨越截线拆分的块（段首孤行和段末孤行最小行数要求保持完整的段落尾部、不可拆分的框）会越过截线（而渲染器会按栏框裁切，于是丢掉最后几行），或者某个标题会落在栏末而其正文却在下一栏开头（启用`headings.keepWithNext`时，即默认情况），截线就下移一行，最多三次，否则放弃截平。如果某个块无法在栏首图下方截线留出的行中开始（段落在那里需要满足段末孤行（orphan）最小行数），它会转入该区段的下一栏，与未截平的栏一样，图则单独占一栏。栏底相差不超过一个网格行的各栏保持原样：最后一栏短一行结束是常见的收尾，截平只会挪动一行。截平作用于测量时所在的区段，页中由通栏框要求的截平也一样。postext 1.4及以前，如果末页的第一段文字曾先被分配给一个放不下它的区段（被浮动体整页占据的页面，或通栏框在前一页底部留下的窄区段），截平就耗在那个区段上，末页的所有行都留在第一栏。宽度不同的栏（两栏都有文字的一栏半版式）按面积截平，每栏的高度按其宽度加权，因为窄栏的一行容纳的文字更少。仅在`enabled`为true时生效。 |
| `trailingStep` | `'lines' \| 'grid'` | `'lines'` | `trailing`截平收尾区段时所用的单位（`beforeSpan`在移到下一页的框后面截平时也用它）。`'lines'`：文字采用自身行距的区段（索引、参考文献表、小字号的容器）按该行距的整行截平和重试；这个行距取区段内多数行共有的行高，不计标题和框。这样各栏最多相差一行，与正文区段一样。正文区段，以及两种行距各占一半的区段，仍像以前一样截在基线网格上。行高相同、行与行却相隔不足整行的区段（条目之间留空的参考文献、手工设了边距的索引分组）也是如此：每栏要有五分之四的行彼此相隔整数行。`'grid'`：每次截平都是整数个基线网格行，与postext 1.27及之前相同；那时截在网格上会让不在网格上的区段某一栏多进一行，截平失败后下移的距离又超过该区段的一行（各栏最多相差三行）。其他取值都按`'lines'`处理。自postext 1.28起；此前保存的文档保持`'grid'`（`pinLegacyTrailingStep`）。 |
| `beforeSpan` | `boolean` | `true` | 截平通栏块留在身后的区段：当`span: 'page'`标注框即使经过截平仍放不进当前各栏下方，必须移到下一页或拆分（`calloutStyles[].keepTogether: false`）时，被它打断的各栏会被截成一样高（与收尾区段采用相同的截平上限），而不是第一栏排满页面、最后一栏短一截。该页作为显式分页处理，以免上述手段把最后一栏重新拉伸到页面底部；框或其放得下的部分随后位于截平后的各栏下方。仅在`enabled`为true时生效。 |
| `closingBox` | `'first' \| 'last' \| 'off'` | `'first'` | 结束短栏的框何时占用其底部下方的空余（手段1，记录为`trailingCallout`）。`'first'`：先于其他所有手段，与postext 1.4及以前相同。框占用整段空余，即使有好几行，其上方的标题一行也分不到。`'last'`：在标题、列表结尾、行间公式和浮动体手段之后执行，它们先占用整行；框随上方的文字一起下移，然后只占用它们剩下的部分，通常不足一行，这样框底仍对齐最后一个网格位置，框也仍靠近它所注释的文字。`'off'`：从不执行；框保留其底部下方的空余，但其他手段在框上方加空时，仍可以把它整行下移，框下剩余的不足一行的空间保持不变。在末页上，框是唯一能让其底部与相邻栏对齐的手段，`'off'`让它留在文字流放置的位置。其他任何值都按`'first'`处理。区段中唯一一栏文字以框结尾时（单栏页面提前结束、下一个块另起下一页），该框不会移动：旁边没有可对齐的栏（postext 1.19 起）。 |
| `gridLines` | `'allow' \| 'off'` | `'allow'` | 用于字符网格：是否允许按整数个网格行加空的手段（标题上方、列表结束处、行间公式或框下方、栏首图下方）运行。这些手段让每个字都留在自己的格子里；`'off'`适合按页计算行数的标准，让该栏短一截结束。不在网格上时不起作用。其他任何值都按`'allow'`处理。postext 1.25 起。 |

### 哪个手段起了作用

排版会把它使用的每个手段记录在所作用的块上，即`buildDocument`返回的VDT中的`block.balancing`。校样、测试或报告可以据此说明某一栏为何平齐，以及哪些栏没有任何手段能补齐。齐底没有处理的块不带`balancing`，用`enabled: false`构建的文档则完全没有。

在字符网格上，文档会说明某一栏为何可能短一截：页面在网格上、配置又没有开启齐底，因而齐底关闭时，`doc.gridBalancing`为`{ off: true }`；齐底运行但不带整行手段时，它为`{ gridLines: 'off' }`。仍然偏短的栏带有`column.gridRefused`，即网格不允许它使用的手段（段落只有把字匀开到格子之外才能多排一行时为`'looseParagraph'`，在`gridLines: 'off'`下则是各个整行手段）。不在网格上时两者都不存在。

```ts
interface VDTBalancing {
  levers: BalanceLever[]; // 通常只有一个：列表后的段落可能既占用列表结尾那一行，又多排一行
  spaceAbove: number;     // 间距手段在块上方增加的px（只有looseParagraph或flexFigure起作用时为0）
  bodyGrowth?: number;    // flexFigure：图片增高的px，在安全区以内裁切
  extraLines?: number;    // looseParagraph：段落多出的行数
  tracking?: number;      // looseParagraph：多出这些行所用的字距，单位为千分之一em（0 = 仅靠词间距）
}
type BalanceLever = 'trailingCallout' | 'flexFigure' | 'heading' | 'listEnd' | 'afterDisplay' | 'afterFloat' | 'looseParagraph';
```

| 手段 | 记录位置 | 作用 |
| --- | --- | --- |
| `trailingCallout` | 标注框的外框块 | 结束该栏的框下移，下移量正好等于其底部下方的空余（`spaceAbove`；不足一行也可以）。 |
| `flexFigure` | 图片的块（行内）或浮动体 | 设了安全区的图片按整数个网格行增高了`bodyGrowth` px，在安全区以内裁切。资源块的`bodySource`是显示出来的部分，`bodyFlex.delta`是同一增量。 |
| `heading` | 标题 | 在标题上方增加整数个网格行，最多`maxLinesPerHeading`行。 |
| `listEnd` | 列表之后的第一个块 | 在列表结束处增加一个网格行，最多`maxLinesAfterList`行。 |
| `afterDisplay` | 公式或框之后的块 | 在行间公式或标注框下方增加一个网格行。 |
| `afterFloat` | 该栏的第一个块 | 在栏首的浮动体区段与其下文字之间增加一个网格行，最多`maxLinesAfterFloat`行。 |
| `looseParagraph` | 段落 | 段落重新断行后多出`extraLines`行，所用的是能做到这一点的最小字距（`tracking`；`block.letterSpacing`是以px表示的同一数值）。 |

截平记录在被截的栏上：每个被截平的栏（通栏框留下的区段、收尾区段）上`column.bandCapped`都为true，`column.trailingCap`还额外标出一章或整个文档的收尾区段（`trailing`）。

```ts
import { buildDocument } from 'postext';

const doc = buildDocument(content, config);
for (const page of doc.pages) {
  page.columns.forEach((column, i) => {
    const levers: string[] = column.blocks.flatMap((block) => block.balancing?.levers ?? []);
    if (column.trailingCap) levers.push('closing band cut level');
    else if (column.bandCapped) levers.push('band cut level');
    if (levers.length > 0) console.log(`page ${page.index + 1}, column ${i + 1}: ${levers.join(', ')}`);
  });
}
// page 3, column 1: heading, heading
// page 3, column 2: listEnd, looseParagraph
```
