本文へスキップ

章 2.1 · 部 I · 基礎

アーキテクチャー:レイアウトのパイプライン

配置のパスと再実行:エンジンが行、フロート、注、段を置く手順と、レイアウトが落ち着くまでパスを繰り返すしくみ

更新日 2026-10-1628分所属する章: 2 アーキテクチャーenescadefritptzhjaar

かんたんな説明

このページは、Postextが紙面にものを置くしくみを知りたいプログラマー向けです。Postextは文章を最初から最後までたどり、すべての行、図、表、注、囲みをそれぞれの場所に置きます。あとの段階で前の部分が変わったとき、たとえば参照の中のページ番号が変わったときは、最初からやり直します。何も動かなくなったところで終わります。このページでは、各段階が何を決めるか、何回まで実行できるかを順に挙げます。

#レイアウトのパイプライン

配置のパスは1つだけで、レイアウトに必要な回数だけ実行します。レイアウトのパイプラインはこれですべてです。

パス(packages/postext/src/pipeline/build.tsのplaceDocumentPass)は、内容を最初のブロックから最後のブロックまでたどります。ブロックごとに、それを組むのに必要なことをすべて行います。これから入る段の幅で計測し、収まるかどうかを確かめ、改行・改段と分割禁止の規則に従って分割するか次へ送り、そのブロックが参照するフロートに枠を提示し、流れをベースライングリッドに戻します(垂直リズムの仕組みを参照)。フロート、ウィドウ、グリッドのための独立したパスはありません。どれも、各ブロックを置くときに決まります。

1回たどるだけでは決められない判断もあります。たどり終えた結果に左右されるからです。目次が出力するページ番号、ページ幅の囲みが上の段を切る高さ、短い段にまだ足りない行数などです。こうした判断のために、buildDocumentは前のパスの結果から得たヒントを付けてパスを再実行します。以下の5つのステージは、1つのブロックがパスの中でたどる順に並んでいます。再実行については、パスのドライバーと段末そろえで説明します。

#ステージ1:内容の構造化

  • 入力:未処理のPostextContent
  • 処理:Markdownをコンテンツブロックに解析し、リソースの参照をIDによってresources[]と照合して解決し、脚注の定義([^id]:)を流れから外してマーカーに引用順の番号を付け、見出しに番号を付け、フロートを計画します。どのリソースをフロートにするか、どのブロックのあとに置くかです(notes[]は読みません)
  • 出力:コンテンツブロックの平らなリスト(型と内容は持つものの、ページや段の割り当てはありません)と、フロートの計画
  • 実行:配置のパスが始まるたびに実行します。解析結果は、最初のパスのあとはメモから読み、それ以外は毎回求め直します

#ステージ2:テキストの計測

  • 入力:テキストを持つコンテンツブロックと、それがこれから入る段の幅
  • 処理:専用のmeasureモジュール(packages/postext/src/measure/)を通して、その幅でブロックの行を計測します。このモジュールはPretextの上に、ハイフネーション、両端そろえ、リッチなインラインのラン、Knuth-Plassの行分割を重ねています。結果は、そのブロックのVDTLine[]と全体の高さです
  • 要点:計測結果はキャッシュされます。cachedMeasureBlock / cachedMeasureRichBlock(measure/cache.ts内)は、テキスト、フォント、幅、そしてレイアウトに影響するすべてのオプションをキーにするので、変わっていない段落の再計測はマップの参照1回で済みます
  • 出力:正確なピクセル単位の寸法を持つ、そのブロックの行
  • 実行:前もってではなく、パスがそのブロックに達したときに実行します。幅は、ブロックがどこに入るかで決まるからです。サイド段の隣の段、回り込む図の横の行、:::columnsグループの中の段などです。別の幅の段へ送られたブロックは、その幅で計測し直します

このモジュールは役割ごとにきれいに分かれています。plain.tsはプレーンなランを、rich.tsは太字・イタリック・数式が混ざったスパンを計測し、font.tsはフォントの文字列を組み立ててキャッシュのライフサイクルを受け持ち、canvas.tsはcanvasの生のテキスト幅の基本機能を包みます。実際の運用で重要になるライフサイクルの細部が1つあります。代替フォントで計測したグリフの幅を、本来のフォントが届いた後まで残してはいけません。canvas.tsは幅をフォント文字列ごとに保持し、計測の世代を記録しています。evictFontFamilies(ビルドがあるファミリーのフェイスの変化を見つけたときに呼ばれ、watchFonts、prepareFonts、loadBundleFontsからも呼ばれます)は、そのファミリーの幅を捨ててPretextのキャッシュを消去します。古い世代で埋められたMeasurementCacheは、次の参照のときにそのファミリーで組んだブロックを捨てます。clearMeasurementCache()は、すべてのファミリーについて同じことを行います。フェイス自体はfonts/prepare.ts(prepareFonts)がレイアウトの前に読み込み、fonts/faces.tsはレイアウト済みの文書が使ったフェイスを読み戻して、fontFallbackの警告に使います。

中国語、日本語、韓国語の段落は、同じモジュールの中で別の経路を通ります。語間のスペースよりCJKの文字が多い段落は、CJKコンポーザー(cjkCompose.ts)に回されます。コンポーザーはテキストを単位(1文字、ラテン文字の連なり、2倍ダーシや3点リーダー、チップや注のマーカーのような分割できないボックス)に切り分け、各単位を1回だけ計測し、cjkClasses.tsの行頭・行末の規則のもとで、先頭から順に入るだけ行を埋めていきます。行頭に置けない約物は、文字を次の行へ送る前に、約物の空き(cjkPunctuation.ts)を詰めることで行内に収めます。両端そろえの行は、そのあと文字と文字の間に空きを配分します。出力は通常のVDTLineで、そのセグメントは、レンダラーが計測どおりに描くのに必要な情報を持ちます。tracking(各文字のあとのピクセル数で、すでにセグメントの幅に含まれます)、空きを詰めた約物のinkOffset、hangs、autospaceセグメントとしての漢字と欧文の間のアキ、そして中国語の注記の圏点、ルビ、割注の行です。日本語の文書では、同じコンポーザーがJLReqに従います。日本語の各レベルの禁則の分類は、改行時に各単位の端の文字から読み取ります。約物の空きはJLReqの順序で詰め、また戻します。ルビはrubyJis.tsが配置し(1:2:1の空け方、仮名への掛かり、熟語ルビ)、漢文の訓点はその文字の単位の幅を広げます。処理量は段落の長さに比例します。紅樓夢の第1回(6,949字)は1,298字の計測で組めます。異なる文字はそれぞれ1回しか計測しないからです。段落の先頭部分を計測していく方式では636,948字の計測が必要でした。中国語の組版を参照してください。

その土台で、Pretextが真価を発揮します。prepare()の呼び出しは負荷の大きい部分で、canvasのフォントエンジンでテキストを解析し、その結果をキャッシュします。一方、layout()の呼び出しは純粋な算術で、ほとんど負荷がかかりません。この分割がすべてです。テキストを一度準備すれば、エンジンはほとんど負荷なしに改行し直せます。狭い段に続く段落なら別の幅で、段末そろえがゆるくする段落なら1行長く組み直します。準備は1回、レイアウトは必要なだけ何度でも。

// Simplified: how the measure module uses pretext internally
const prepared = prepare(paragraphText, '16px/1.5 Inter');
const { height } = layout(prepared, columnWidth, 24); // 24px line-height
// => "This paragraph is 168px tall at 320px column width — that's 7 lines."

#ステージ3:ページと段への配置

  • 入力:計測済みのブロック
  • 処理:ブロックを現在の段に置きます。流れが必要とするたびにVDTPageとVDTColumnのノードを作ります。段ごとにavailableHeightを追跡します。ブロックが入らなければ、分割するか(ステージ5)、次の段または次のページへ進みます
  • 方針:先頭から順に入る場所へ置く貪欲法の配置です。改段と改ページは、有効な割り当てのうち最も単純なものに従います
  • 出力:そのブロックにpageIndex、columnIndex、bboxが割り当てられます

この時点で、VDTは本物の文書になります。このステージの前のブロックは、平らなリストの1項目にすぎず、住所がありません。配置は、それぞれをページと段に割り当てます。格子状に並んだ容器に水を注ぐようなものです。1段目をあふれるまで満たし、あふれた分を2段目に流し、ページがいっぱいになったら新しいページを始めます。

ページに内容のブロックを置く前に、配置のパスは構造上の要素のための空間を確保します。柱とノンブル(config.header / config.footerからデザインスロットとして配置されます)と、新しく開いたページで保留になっているフロートの帯です。この確保によって各段のavailableHeightが減るので、内容のブロックが流れ込み始めたとき、エンジンは使える余地がどれだけあるかを正確に知っています。

段の帯とspan段。page.columnsは読む順序に並んだ平らな配列ですが、ページはいつも1列に並んだ段だけでできているとは限りません。多段組みのレイアウトでは、ページ幅のインラインブロックがページを上下に積み重なる「帯」に分けます。これに当たるものは4つあります。スタイルまたはフェンスでspan: 'page'を指定した:::callout、本文中の:::columns{span="page"}グループ、配置が{ position: 'here', span: 'page' }に解決されるインラインの::resource、そして書かれた位置でページ幅に組むコミックのストリップ(:::strip{span=page})です。あとの3つは、配置の前に枠のないページ幅の囲みで包まれるので(pipeline/flowColumns.ts、pipeline/spanEmbeds.ts)、4つとも1つの仕組みで組まれます。切断のとき、現在の帯のテキストの段は切断位置で閉じられ(高さを切り詰め、availableHeightはゼロ)、ブロックはkind: 'span'を持つ専用の全幅の段に入り、その下に新しいテキストの段の帯(band + 1、同じxと幅、置き換える帯と同じ下端)が追加されます。段は追加されるだけなので、columnIndexは引き続きpage.columns[i]を指し、レンダラーに特別な描画は要りません。各段は自分のbboxで切り抜かれます。このbboxはcolumnClipRectで広げられます。グリフのインクのための2ptに加えて、段のデザインのオーバーレイ(見出しのタブや囲みのバッジなど)が段の左右にはみ出す分と、見出しのデザインが段の上に突き出す分です。段の下端は境界のまま変わりません。canvasとPDFのバックエンドで同じ矩形を使います。段間罫は帯ごとに隣り合うテキストの段の間に引かれ、ページの頭でページ幅の見出しが占める帯の下から始まります(columnRuleSegments)。スタイル付きのセクションが独自の罫を設定している場合は、ページ自身の罫で引かれます(VDTPage.columnRule、pageColumnRuleで読み取ります)。段末そろえは、span段、サイド段、高さゼロの帯を無視します。ページ幅のブロックは、帯の高さがそろっている位置(ページの頭、章扉の見出しの直後、別のページ幅のブロックや天のフロートの帯の直後)ならそのまま切ります。高さのそろっていない帯に来た場合は、代わりに「帯の上限(band cap)」を提案します(packages/postext/src/pipeline/bandCaps.ts)。ある内容のブロックで始まる帯の段をceil(Σ used / N / grid)行に縮めるものです。buildDocumentはこの上限を付けて配置のパスを再実行します(上限を付けた帯があふれたら1行ずつ広げ、これをMAX_BAND_CAP_RETRIES = 3回まで行い、それでもだめなら上限を外すので、ブロックは次のページの頭に回ります)。こうして本文はすべての配置規則を守ったまま縮めた段を埋め、切断位置で高さがそろって終わり、閉じた段に残った余りはavailableHeightとして残って段末そろえが吸収します。同じ仕組みで、章と文書の「最後の」帯もそろえます(headings.balancing.trailing)。章扉、:::part、章を閉じるplacement: 'fixed'の囲み、または文書の終わりに、現在の帯の段の高さがそろわないまま達すると、境界のブロックをキーにしたkind: 'trailing'の上限を提案します。上限は帯を開くブロックをキーにしており、そのブロックは前のページが段末そろえの追加の行を吸収するたびに動きます。そのため最後の帯の上限は、段末そろえが落ち着いたあとに、段末そろえのヒントを固定した状態で決めます。そのあと短い仕上げのラウンドで、切断が短く残した分を調整手段が埋めます。placement: 'fixed'の囲みは流れから外れます。ボックスはページの版面、仕上がりの枠、または裁ち落としの枠に固定され、それが覆うテキストの段はその領域を明け渡し(フロートの帯と同じように下または上から切られ、衝突すれば次のページへ移ります)、枠と子要素はpage.floatsに入ります。

縦組みのページ。layout.writingMode: 'vertical-rl'を指定すると、このパスは横組みのページを時計回りに90度回したものとしてレイアウトします。このようなページでは、版面、段、ブロック、行、フロート、脚注領域は流れの座標(flow coordinates)で表され、VDTPage.flowがそれを紙面に移す回転を持ちます。流れの座標の点(x, y)は、紙面の(ページの幅 − y, x)に来ます。後段の処理はこれを知る必要がありません。改行、フロート、分割禁止の規則、段末そろえは、どのページとも同じように流れの座標系で動きます。流れの1段は紙面では1段(縦組みの段)になり、各バックエンドは描画時に回転を適用します(flowToPageとpageToFlowが点を双方向に変換します)。柱、ノンブル、トンボ、背景は紙面の座標のままです。図と表は正立したブロックとしてレイアウトされ、枠の中で逆向きに回して戻されます。正立する文字は、描画時に1字ずつ回転を戻します。

#ステージ4:リソースの配置

  • 入力:ステージ1のフロートの計画と、そこまでに配置されたページ
  • 処理:参照された各リソースを、最初の参照のあとにある最初の空き枠にフロートとして配置します。参照している段の地、次の空の段の天か地、または次のページの帯です(packages/postext/src/pipeline/floatPlacement.tsがフロートを計画し、pipeline/floatSlots.tsが枠を列挙して測ります。帯は配置のパスがたどりながら確保します)
  • 実行:最初の参照を含むブロックが置かれるとすぐ、同じパスの中で実行します。そのあとに配置されるテキストは、帯を避けて流れます
  • 配置の解決:リソースごとに、エンジンはresource.placement → 種類のresourceType.defaultPlacement → 組み込みの既定値{ position: 'auto', span: 'column' }の順に解決します
配置のフィールド動作
position: 'auto'リソースは参照のあとの最初の空き枠に入ります。天でも地でもかまいません。これが既定です
position: 'top'天の枠だけを使います。次の空の段またはページの天の帯に入り、段の内容をその下へ押し下げます
position: 'bottom'地の枠だけを使います。段またはページの地の帯に入り、その上の段を短くします
position: 'here'フロートしません。リソースは::resourceディレクティブの位置、つまり流れの中に現れるその場所にインラインで埋め込まれます
span: 'column'帯は1つの段だけを占めます(新しく開いたページでは、エンジンは残りの余地が最も大きい段を選びます)
span: 'page'帯はすべての段にまたがって版面の幅いっぱいに広がり、段の流れを断ち切ります。全幅の帯が先に確保されるので、1段幅のフロートは残りの空間の中に収まります
span: 'side'リソースは、サイド段をフロート専用にした1段半レイアウト(layout.sideColumnRole: 'floats')のサイド段に、最初に参照する段落の横に積んで組まれます。そのような段のないページでは'column'と同じ動作になります

ResourcePlacementには、この2つのほかにもフィールドがあります。rotate、width、align、columns、captionSide、shrink、minScale、captionMeasure、wrap、wrapGap、citingPageです。それぞれの働きは、設定 → リソースの種類と文書形式 › 配置で説明しています。

  • 配置の繰り延べ:現在のページのどの枠にも入らないフロートは、流れが次に開くページを待ちます。章の境界では、境界より前に開かれたページへ送り出されます。図版は元の大きさのままです。ただしplacement.shrink(文書全体ではlayout.floatShrink)で認めれば、枠の空きに合わせて縮小されます。新しいページに収まらない高さの表は行の境界で分割され、見出し行を繰り返しながら次のページへ続きます(tableStyle.overflow: 'split'、既定値)
  • 出力:リソースはページの帯(page.floats)に配置され、影響を受けた段の高さが減らされて、本文が帯を避けて流れます

リソースの配置はおもしろいところです。フロートは空間を占めるだけでなく、周りの空間の形を変えるからです。参照を含むブロックが置かれると、保留中のフロートには、そのあとにある現在のページの空き枠が順に提示されます。まずその段の地、次に次の空の段の天と地です(ページ幅のフロートなら、どの段にもまだ余地があるときのページの地)。どこにも入らないものは次のページを待ち、そこでは保留中のフロートが、本文が流れ込む前に自分の帯を確保します。段は帯の間に収まるように縮み、本文は狭くなった段を途切れずに流れます。読者は、図が言及された位置の近く(ただしちょうどその位置ではない)に図を見ることになります。これはプロの組版では標準的な手法で、本ではいつも行われていることです。

配置の規則。どの位置に送るかの振り分けに加えて、フロートは厳格な編集上の制約に従います。

  • 参照後の規則。フロートは、本文中の最初の参照のあとにある最初の空き枠に入り、決してその前には入りません。読者はまず参照に出会い、それからリソースを見ます。フロートが入らなければ、後ろの枠やページに繰り延べられ、前に戻ることはありません。
  • 番号の系列内での参照順。保留中のフロートは、最初の参照の順に各枠を提示されます。どこにも入らないフロートは、同じ番号の系列で後ろに並ぶフロートを足止めします。表3が表4よりあとに来ることはなく、図12が図11より前に来ることもありません。系列どうしは互いを足止めしません。待っている表があっても、あとの図は先に進めます。長い表が新しいページを待たずに済むよう、空の段の頭を提示された長い表はその段の長さで切られ、次の枠(隣の段、または次のページの帯)に続きます。見出し行は繰り返されます。
  • 章の障壁。フロートが章の外に出ることはありません。章扉(breakBeforeまたはspan: 'page'を持つ見出しレベル)、:::part、floatBarrier: trueを持つ囲みのスタイル、そして文書の終わりでは、境界自身の改ページの前に、保留中のフロートをすべて先に配置します。まずページの空き枠に、次に境界より前に開いたページに置きます(各ページは少なくとも1つのフロートを強制的に配置します)。そのようなページを開く前に、まだ保留中の図や表には、positionにかかわらず、現在のページの空き枠がもう一度提示されます。章の最後のページで参照された、天に置く指定のフロートは、専用のページを使うのではなく、そろえた段の下、そのページの地に入ります(フロートとして配置した囲みは自分の配置を保ちます)。:::pagebreakは、保留中のフロートを、そのあとに続くページへ送ります。奇偶をそろえるための白ページがあれば、そのあとです。
  • 本文の最小の余地。新しく開いたページでは、影響を受ける段に本文がまだ少なくとも3行入る場合にだけ帯を確保します。例外が1つあります。大きすぎるフロートは、まだ本文だけの帯に強制的に配置できます。これにより、ページを占めるほどの図がキューをいつまでも止めることはありません。現在のページの枠は、段の残りの高さに収まらなければなりません。そこで3行の規則が適用されるのは、別のフロートの帯の隣にある場合だけです。
  • 余白の確保。帯とその隣の本文の間には、本文の行の高さ1つ分の間隔を空けます。
  • ベースライングリッドへのそろえ。天の帯はベースライングリッドの倍数に切り上げられ(フロートの下の間隔が広がります)、押し下げられた行もすべてグリッドに乗ります。地のフロートは、キャプションの最後のベースラインがグリッドに乗るように固定されます。キャプションは隣の段の本文の最後の行とベースラインを共有し、ページはどの段でも、向かい合うページでも、同じ高さで終わります。

種類別・初出順の番号付け。リソースの番号はMarkdownには書きません。pipeline/resourceNumbering.tsが、読む順序で最初に参照された時点で、リソースのResourceTypeを使って各リソースに番号を割り当てます。numberingTemplateは、種類ごとのカウンター{n}と、参照の時点で有効な見出しのカウンター{h1}〜{h6}を組み合わせます(たとえば'{h1}.{n}' → 「1.7」)。resetOnはカウンターをいつリセットするか('never'、または任意の見出しレベル)を決め、counterFormatは10進数、ローマ数字、アルファベットのいずれの数字を使うかを選びます。組み込みの「図」と「表」の種類はdefaultResourceTypes(locale)から得られ、文書の言語に合わせてローカライズされます。番号付けは最初の参照の順に従うので、文書の途中に新しい図を挿入すると、それ以降の番号はすべて自動的に振り直されます。原稿を編集する必要はありません。

#ステージ5:改行・改段と分割禁止の規則

このステージが、組版エンジンと、ただテキストを流し込むだけのものとを分けます。プロの組版者が何世紀にもわたって手作業で適用してきた編集上の品質の規則を強制します。素朴なテキストの描画では、まったく無視される規則です。

3つの仕組みが仕事を分け合い、それぞれが、必要な情報のある場所で働きます。行分割の中でのラントのペナルティー、段落が段の終わりにかかるところでのスコアによる分割、そしてブロックの間での構造上の分割禁止の規則です。

ウィドウ、オーファン、ラント

ウィドウとオーファンは、素人の組版であることを最もはっきり示す兆候です。

  • ウィドウとは、段の下端にひとりで残された段落の1行です。段落は次の段に続きますが、その1行は取り残されたように見えます(段が途中で終わってしまったかのようです)。
  • オーファンとは、段の上端に取り残された段落の1行です。段落の大部分は前の段にあり、1行だけがはみ出しています(前後の文脈から切り離されたように見えます)。
  • ラントとは、最後の行が短い1語(または2語)だけになった段落です。視覚的には、まともな1行と感じるにはあまりにも短すぎます。構造上はウィドウほど深刻ではありませんが、注意深い読者にとっては同じくらい目障りです。

ラントは行分割の中で決まります。ラントは段落だけで決まる性質です。段落がどこに置かれても、最後の行の短さは変わりません。そのため3つの規則のうち、Knuth-Plassの行分割が見るのはこれだけです。ある改行位置の組で、段落の最後の行の内容の幅がruntMinCharacters × normalSpaceWidthを下回る場合(既定のruntMinCharactersは20。語間のスペース20個分の内容で、およそ8〜12文字です)、行分割はデメリットの2乗の式の中で、その行のバッドネスにruntPenalty(既定は1000)を加えます。これにより、ラントのペナルティーは行のバッドネス(10000で頭打ち)と同じ尺度で競い合い、バッドネスに埋もれてしまうことはありません。このペナルティーは、通常のデメリット(バッドネス、ハイフネーションのコスト、適合クラスの不一致)と並んで1つの全体最適化の中に置かれ、行分割はほとんどの場合、ラントのない改行位置の組を見つけます。見つからないときは、語間と、わずかな負のトラッキングで差を吸収できる場合に、bodyText.tightenRuntsが段落を1行短く組みます。ペナルティーはmeasureモジュールが行分割に渡し(measure/plain.ts、measure/rich.ts)、knuthPlass/breakpoints.tsが適用します。

ウィドウとオーファンは段の終わりで決まります。行分割は、ある幅に対して段落を改行します。段落がページのどこに来るかを知らないので、どの行が段を閉じることになるかもわかりません。配置のパスにはそれがわかります。段落の行が段の残りにすべては収まらないとき、chooseParagraphSplit(pipeline/orphanWidow.ts内)は、すでに改行済みの行について、0から収まる行数までのすべての分割kを試します。k行が残り、あとの行が次の段を開きます。候補ごとに、次のスコアを付けます。

  • 段の下端に空いたまま残す行について、slackWeight · slack²。slackは、収まる行数からkを引いた値です(既定のslackWeightは10)。
  • 段の下端に残る行数がwidowMinLinesより少なければ、widowPenalty(既定は1000)を加えます。
  • 次の段の頭へ送る行数がorphanMinLinesより少なければ、orphanPenalty(既定は1000)を加えます。

スコアが最も低い分割が選ばれ、k = 0なら段落はまるごと次へ送られます。規則はゆるやかなもので、どの程度ゆるやかかは数値が示します。1000のペナルティーは、空の行10行分のスラックのコストに当たります(10 · 10² = 1000)。したがって既定値では、両方の規則を守る分割は、段に空く行が10行未満である限り必ず選ばれます。それを超えると、ウィドウやオーファンのほうがコストの低い選択になることがあります。分割は、行分割がすでに作った行の中から選ぶだけで、段落を改行し直すことはありません。リストの項目は、avoidOrphansInListsとavoidWidowsInListsで同じ分割を通り、avoidRuntsInListsでラントのペナルティーを受けます(いずれも既定はtrue)。

これらはすべてBodyTextConfigで調整できます。設定 → オーファン、ウィドウ、ラント、分割禁止の規則を参照してください。いずれかの*Penaltyを0にすると、その規則は事実上無効になります。

構造上の分割禁止の規則

1つの段落より大きなまとまりもあります。隣り合うブロックにまたがるもので、行分割だけでは扱えません。配置のパスは、各ブロックを置くときにこれを強制し、改段や改ページで分かれてしまうまとまりを、まるごと前へ送ります。

  • 見出しとその最初の段落。見出しが導く段落が次の段で始まる場合、見出しを段の下端に置いてはなりません。headings.keepWithNext(既定はtrue)で強制します。見出しに加えて、次のブロックの本文のウィドウの最小行数(bodyText.widowMinLines、既定は2)を入れる余地がない場合(avoidWidowsがオフのときは1行だけ)、見出しは本文と一緒に移るよう前へ送られます。
  • 連続する見出し。複数の見出しが続く場合(たとえばh2のあとにh3、そのあとに段落)、まとまり全体が一緒にいなければなりません。どの見出しも、それが導く内容なしに段の下端に取り残されてはなりません。
  • コロンで導かれるリスト。段落がリストを直接導くコロンで終わる場合、コロンのある行はリストの始まりと一緒にいなければなりません。bodyText.keepColonWithList(既定はtrue)で強制します。段落を置くと最初のリスト項目を始める余地がなくなる場合(リストのオーファンとウィドウの規則で項目がひとまとまりに保たれるときは項目全体、bodyText.colonListRoom: 'line'のときはpostext 1.4までと同じく1行)、コロンのある最後の行(段落が1行ならその段落全体)がリストと一緒に前へ移ります。この規則が段落全体を送らなければならず、しかもその段で段落の直前に見出しが続いている場合は、その見出しも一緒に前へ引き寄せ、keepWithNextが黙って破られないようにします。唯一の例外は、それより前の移動ですでに前へ送られた見出しだけがその段にある場合です。このときエンジンは段落を見出しと一緒に保ち、ループを避けるために、コロンとリストが離れるという軽いほうの違反を受け入れます。
  • 図とそのキャプション。図とキャプションは切り離せない単位です。必ず一緒に移ります。

分割禁止の違反を検出すると、エンジンはまとまり全体を次の段またはページへ送ります。すると、まとまりが抜けた段は、そのまとまりが占めるはずだった行数だけ短く終わります。それを埋めるのは段末そろえの仕事です。

うまく行えば、これらの修正は目に見えません(読者が気づくことはないはずです)。しかし、修正がないことは、注意深く読む人には一目でわかります。段の頭にぽつんと置かれた1行や、エンジンがテキストを収めるのをあきらめた不ぞろいなすき間です。プロの出版社は、まさにこうした問題を防ぐためのスタイルガイドをまるごと持っています。Postextはそれを自動化します。

#パスのドライバー

配置のパスとその再実行1回の配置のパスが、内容を5つのステージに通す。構造化、計測、配置、フロート、そして改行・改段と分割禁止の規則である。buildDocumentは、前のパスから得たヒントを付けて、パス全体を再実行する。ヒントは、帯の上限、段末そろえの調整手段、最後の帯の切断、確定したページのラベルや注の番号である。1回の配置のパスステージ1構造化ステージ2計測ステージ3配置ステージ4フロートステージ5改行・改段、分割禁止ヒントを付けて再実行:帯の上限、段末そろえの調整手段、最後の帯の切断
1回のパスで文書全体を配置する。buildDocumentは、各ラウンドが落ち着くか上限に達するまで、ヒントを付けてパスを再実行する。

buildDocumentは、配置のパスを回すドライバーです(pipeline/build.tsのbuildDocumentGenとbuildDocumentBalanced)。まずパスを1回実行します。次に、パスが報告した内容を読み、見直しの必要な判断があれば、ヒント(PassHints)を付けてパスをまるごと再実行します。ヒントは、次のパスが適用する上限、追加の行、番号です。下の表のどの条件も生じなければ、最初のパスだけでビルドは終わります。

再実行はラウンドに分かれます。1つのラウンドが1種類の判断を受け持ち、次の順に実行されます。

ラウンド実行の条件次のパスに伝えること上限
1. 目次ホストが与えていないページ番号を文書が出力する場合。コンテンツにoutlineがないときの:::tocや:::index、またはアンカーのページを出力する参照です前のビルドのページのラベルを入れたアウトラインMAX_TOC_ROUNDS:最初のビルドのあとに3回まで
2. 注の番号footnotes.numberingが'page'、'column'、'spread'のいずれかで、注が段または見開きの下端に置かれる場合。注の番号は、レイアウトがその注をどこに組むかで決まります前のビルドで各注に付いた番号(footnoteNumbers)MAX_FOOTNOTE_ROUNDS:最初のビルドのあとに3回まで
3. 図の横のキャプションサイドの囲みまたはサイドフロートが、図がサイド段に組んだキャプション(placement.captionSide)の上にかかった場合そのキャプションを図の下に置くこと(captionUnder)3パス
4. 参照ページのフロートplacement.citingPageを指定したフロートが、それを参照する行よりあとのページまたは段に入った場合そのページまたは段を、頭にそのフロートを置いて始めること(citingPageFloats)MAX_CITING_PAGE_ROUNDS:提案のラウンドは3回まで。そのあと、パスがそこに保てなかったフロートのまとまりごとに1パス
5. 帯の上限ページ幅のブロックが、段の高さがそろっていない帯に来た場合(ステージ3を参照)その帯を切る高さ(bandCaps)MAX_BAND_PASSES:提案と再試行で4パスまで。上限は1行ずつ3回まで広げ(MAX_BAND_CAP_RETRIES)、それでもだめなら外します
6. 段末そろえheadings.balancing.enabledがオンで、そろえの対象になる段が短く終わった場合ブロックごとの追加のグリッド行と、1行長く組む段落(balanceExtraPx、balanceLooseness)MAX_BALANCING_PASSES:セグメントごとに10回の試行。MAX_BALANCING_PASSES_PER_DOCUMENT:文書ごとに30パス
7. 最後の帯段末そろえがheadings.balancing.trailingとともにオンで、章または文書が、段の高さがそろっていない帯で終わった場合その帯に付ける最後の帯の上限。段末そろえのヒントは固定したままですMAX_BAND_PASSES:提案と再試行で4パスまで。上限ごとに再試行は3回
8. 仕上げラウンド7が最後の帯の上限を残した場合上限を付けたレイアウトの上で、新しいセグメントに分けて、段末そろえをもう一度行うことセグメントごとに10回の試行と30パス。数え直します

ラウンド3〜8は、1回のビルド(buildDocumentBalanced)の中で順に実行されます。ラウンド1と2はその外側にあります。目次のラウンドや注の番号のラウンドは、それぞれがビルドまるごと1回で、その中に自分のラウンド3〜8があります。ビルド全体に対する上限はなく、あるのはこれらの上限だけです。

再実行はすべて、完全な配置です。ヒントが前のパスのツリーに手を加えることはありません。パスはヒントを有効にして文書を配置し直すので、上に2行多く空いた見出しも、6行上で切られた帯も、ほかのすべてと同じ改行・改段と分割禁止の規則を通ります。そのあとドライバーは結果を確かめます。囲みが切断位置に来なかった帯の上限は、広げるか外します。セグメントをよくしなかった段末そろえのパスは、そのセグメントについては捨てます。ドライバーが返すものは、いずれかのパスが配置したページでできています。1つのパスの全体か、段末そろえのあとなら、セグメントごとに、そのセグメントにとって最良だったパスのページです。

文書が報告するもの。doc.iterationCountは、返されたレイアウトに至るまでの配置のパスを数えます。最初のパスと、帯の上限、段末そろえ、最後の帯の各ラウンドのパスです。図の横のキャプションと参照ページのフロートのパスは含まれず、目次や注の番号のラウンドで先に行ったビルドも含まれません。BuildDocumentOptions.onPassは、すべてのパスについて、その経過時間とともに呼ばれます。doc.convergedは段末そろえだけに関する値です。すべてのセグメントが、そろえの対象になる段に空の行を残さずに終わったか、提案できる調整手段がなくなって終わったときにtrueになります。あるセグメントが、改善しなかったパスで止まったか、試行回数を使い切ったかして、すき間が残ったままのときはfalseです。段末そろえがオフならtrueです。converged: falseの文書も完成しています。ドライバーが見つけた最良のレイアウトを持っていて、そのどこかの段が短く終わっているということです。

#段末そろえ

  • 実行:ドライバーのラウンド6と8として実行します(既定で有効です。headings.balancing.enabled: falseで無効にできます。縦組みと文字グリッドでは既定でオフです。設定 → 段末そろえを参照)
  • 入力:それまでで最良のレイアウトと、そろえの対象になる各段の下端にある空のグリッド行(pipeline/columnBalancing.tsのcollectColumnGaps)
  • 処理:短い段をその場で埋める調整手段を提案し、それを付けて配置のパスを実行し、すき間のスコアが下がったセグメントについて、その結果を採用します
  • 出力:ページの最後のグリッド位置で終わる段。適用した調整手段はすべて、適用先のブロックに記録されます(VDTBlock.balancing)

段の高さがそろっていないと、すぐに目につきます。改行・改段の規則は、そうした段を絶えず生みます。本文と一緒にいるために次へ送られた見出し、ウィドウを避けるためにまるごと送られた段落、収まらなかった図。隣の段より2行上で止まった段は、未完成に見えます。レイアウトが途中で投げ出されたかのようです。

これを直すために、ブロックが別の段へ移ることはありません。改段は、収まらなかった要素に結び付いています。次の段を開くブロックは、残っていた余地に対して高すぎたのであり、余地がさらに小さくなっても高すぎることに変わりはありません。そのため短い段は内側から、最大でもそのすき間の分だけ埋めることができ、次の段へ越えていくものはありません。調整は局所的で、組版者が手作業で行うことと同じです。目が受け入れる場所に、1行分の空きを足します。段がそろえの対象になるのは、内容がその段を越えて同じページのあとの段へ流れた場合です。ページの最後の段については、そのページが次のページへ続く場合です。章の最後のページは短く終わってかまいません。その最後の帯は、代わりに最後の帯の上限によって、高さをそろえて切られます(ステージ3を参照)。

すき間ごとに、調整手段を編集上の優先順位に従って提案します。設定 → 段末そろえに挙げたのと同じ順序です。

  1. 段を閉じる囲み:下端の下の空きを自分の上の空きとして取り、下端をページの最後のグリッド位置、つまり隣の段の最終行と同じ高さにそろえます。trailingCalloutとして記録されます。
  2. セーフエリアのある画像(Resource.safeArea):段にインラインで置かれているか、その段だけにかかるフロートとして段の頭または末尾にある画像が、セーフエリアの範囲内で切り抜きながら、グリッドの行単位で大きくなります。画像が高くなってもページに穴は開かないので、空きを足すどの手段よりも先に働きます。flexFigureとして記録されます。
  3. 見出し:上にグリッドの行を、1行ずつ順番に受け取ります。最も重要な見出し(レベルの数字が最も小さいもの)が先なので、最も多く受け取ります。段の一番上にある見出しは受け取らないので、段はページの上端から始まったままです。見出しがこうして受け取るのは、まず最初の数行だけです。そのあと下の調整手段を試し、それらで埋められなくなったときに、上限まで行を足します。headingとして記録されます。
  4. リストの終わり:リストが終わる位置に、グリッドの行を1行受け取ります。listEndとして記録されます。
  5. 数式・囲み・図の下:その下にグリッドの行を1行受け取ります。別行立ての数式や囲みの下の、本文が再開する位置(afterDisplay)と、段の頭にある図や表とその下の本文との間(afterFloat)です。
  6. ゆるい段落:最後の手段です。段の中の段落を、長いものから順に、それぞれ1行長く組み直します(TeXの\looseness=+1。Knuth-Plassの行分割のloosenessオプションを使います)。長くした組み方は、すべての行がbodyText.maxWordSpacingの範囲内にとどまる場合にだけ採用します。語間だけでは1行を稼げないとき、段落は、それを稼げる最小の正のトラッキングを、上限の範囲内で受けることもあります。looseParagraphとして記録されます。

上限、切り替え、例外(文字グリッド、最後のページ、複数の段にまたがるフロート)は、headings.balancingのキーです。設定のページがそれらを一覧にし、最新の状態に保っています。このページでは繰り返しません。

提案は、実際に配置して確かめます。調整手段を適用するパスは完全な配置であり、ときには調整手段が結局、内容を改段の向こうへ動かしてしまうことがあります。頭の部分が収まらなくなった分割済みの段落や、枠を失ったフロートです。その地点よりあとのページは流れ方が変わり、別の場所にすき間が開きます。ドライバーはスコアでそれに気づき、そのセグメントについてパスを捨て、変化のあった最初の段で新たに適用された調整手段をブラックリストに入れます。こうして次の提案は、うまく働いた調整手段を残し、連鎖を起こしたものを外します。

段末そろえはセグメントごとに収束します。明示的な区切り(章扉、:::pagebreak)の間にあるページは、互いに独立してレイアウトされます。そうした区切りを越えて流れるものは何もないので、ある一続きのページの中の段末そろえの調整手段が、別の一続きのページの行を動かすことはありません。そのため段末そろえのループは、こうしたセグメントをそれぞれ単独で判定します。セグメントのすき間のスコアは、そろえの対象になる段の下端にある空のグリッド行の数です。1回のパスでは文書全体を配置しますが、各セグメントは、自分の分の調整手段を自分のすき間のスコアで採用または却下し、自分の連鎖をブラックリストに入れ、自分で頭打ちを判断し、自分の試行回数の予算を使います。再試行で悪くなったセグメントは、自分にとって最良だったパスのページに戻され、ほかのセグメントはそのまま先へ進みます。こうして30章の本は、各章を1つずつ処理したときとまったく同じように段末そろえされます。本全体のPDFと、同じ章だけのPDFは同一になります。どこか1か所の連鎖のために、本のすべてのページがパスを1回余分に費やすことはありません。

上限は安全弁です。閉じられない段もあります。短い段落しかない段には使える調整手段がなく、どの調整手段もブロックを改段の向こうへ押し出してしまう段には、採用できるものがありません。エンジンは見つけた最良のレイアウトを残し、その段は短いままにします。調整手段はブロックに記録されているので、校正刷りやテストで、何によっても閉じられなかった段を一覧にできます(設定 → どの調整手段が働いたかを参照)。