Aller au contenu principal
Recette numéro 177

Recettes · Chapitre 1 · Page et grille

La colonne que la règle des veuves laissait courte

Des colonnes en drapeau qui finissent sur la grille : fillShortColumns allonge ou raccourcit d’une ligne un paragraphe de la colonne courte.

Sur cette page

p. 1 · 4 sur 6

  • Exemple en anglais : pas encore d’édition française
  • Format 225 × 297 mm
  • 2 colonnes, Gouttière de 7 mm
  • Public Sans 9,4/13,2
  • Libre Caslon Display
  • Libre Caslon Text
  • 6 pages
  • Niveau
  • Postext 1.65.0
  • Mis en page en 149 ms
  • 162 lignes de code

En bref

Quand une colonne s’arrête une ligne au-dessus du pied parce qu’un paragraphe ne peut pas y être coupé, ce réglage recompose un paragraphe de cette colonne pour qu’elle descende jusqu’en bas.

Ce que vous allez composer

Trois pages de Littoral, un magazine de la côte : un long article sur la lentille de Fresnel, ouvert par la peinture d’une lentille allumée dans sa lanterne. Le texte court sur deux colonnes en drapeau en Public Sans, avec une ligne de grille vide entre les paragraphes, et les règles des veuves et des orphelines gardent au moins deux lignes d’un paragraphe de chaque côté d’une coupure. Le pen compose l’article deux fois. Sans le réglage, sur l’ouverture, la colonne de gauche s’arrête une ligne au-dessus de celle de droite : le paragraphe suivant ne peut pas commencer par une seule ligne en pied. Avec lui, sur la même page, le premier paragraphe passe à une ligne de plus et les deux colonnes finissent sur la dernière ligne de la grille. Page suivante, le réglage reprend aussi l’air en trop que l’équilibrage avait mis au-dessus de deux intertitres.

Cette recette répond à

  • Pourquoi une colonne s’arrête-t-elle une ligne ou deux avant le pied, et comment la remplir sans enfreindre les règles de veuves et d’orphelines ?
  • Comment obtenir des colonnes alignées en pied et une dernière page équilibrée (justification verticale) ?
  • Comment éviter veuves, orphelines et dernières lignes d’un seul mot, et garder un titre avec son texte ?

La réponse courte

script.js · lignes 37–55dans le code complet
const balancing = {
  // A ragged column has no lever of its own when the widow and orphan rules hold its last
  // line back: no heading or list end to open, no justified paragraph to set looser. This
  // one lets a paragraph of the short column run a line long, within its own word spacing
  // (or a justified one no looser than maxWordSpacing), or, when none can, set one a line
  // short so the split after it takes the line. A change is kept only when the pages end
  // with fewer short columns; otherwise the column stays as it was.
  fillShortColumns: true, // default false
  // The paragraphs go first: a crosshead keeps its one line of air unless they cannot help.
  maxLinesBeforeLoose: 0,
};
const bodyText = {
  fontFamily: 'Public Sans', fontSize: pt(9.4), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
  textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between
  hyphenation: { enabled: true }, optimalLineBreaking: true, optimalRagged: true,
  avoidWidows: true, widowMinLines: 2, avoidOrphans: true, orphanMinLines: 2, // the cause
  avoidRunts: true,
};

Ingrédients

Typographie
Public Sans, Libre Caslon Display, Libre Caslon Text (SIL OFL 1.1)
Ressources
  • lens-1400.jpg
  • A lit Fresnel lens in a lighthouse lantern at dusk (Generated With Diffusion Models, original)

Préparation

#1 · Donner à la colonne en drapeau un levier à elle

script.js · lignes 37–55dans le code complet
const balancing = {
  // A ragged column has no lever of its own when the widow and orphan rules hold its last
  // line back: no heading or list end to open, no justified paragraph to set looser. This
  // one lets a paragraph of the short column run a line long, within its own word spacing
  // (or a justified one no looser than maxWordSpacing), or, when none can, set one a line
  // short so the split after it takes the line. A change is kept only when the pages end
  // with fewer short columns; otherwise the column stays as it was.
  fillShortColumns: true, // default false
  // The paragraphs go first: a crosshead keeps its one line of air unless they cannot help.
  maxLinesBeforeLoose: 0,
};
const bodyText = {
  fontFamily: 'Public Sans', fontSize: pt(9.4), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
  textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between
  hyphenation: { enabled: true }, optimalLineBreaking: true, optimalRagged: true,
  avoidWidows: true, widowMinLines: 2, avoidOrphans: true, orphanMinLines: 2, // the cause
  avoidRunts: true,
};

L’équilibrage des colonnes remplit une colonne courte en ajoutant des lignes de grille au-dessus d’un titre, après une liste, sous un encadré ou une figure et, en dernier recours, en composant un paragraphe justifié une ligne plus lâche. Une colonne de texte courant en drapeau n’a rien de tout cela entre ses intertitres, et la ligne que retiennent les règles des veuves et des orphelines reste vide. fillShortColumns laisse un paragraphe en drapeau de cette colonne prendre une ligne de plus avec ses propres espaces ; quand aucun ne le peut, il en compose un avec une ligne de moins pour que le paragraphe qui suit la coupure remonte et remplisse la ligne. Le moteur ne garde le changement que si moins de colonnes finissent courtes : l’activer ne dégrade jamais une page. maxLinesBeforeLoose: 0 essaie les paragraphes avant les titres, si bien que chaque intertitre garde une seule ligne d’air au-dessus de lui.

#2 · Composer l’article deux fois et comparer

script.js · lignes 204–208dans le code complet
const withFill = (fill) => ({ ...config, headings: { ...config.headings,
  balancing: { ...balancing, fillShortColumns: fill } } }); // a copy: config keeps true
const build = (fill) => buildDocumentWithFonts({ markdown, resources }, withFill(fill),
  kitFonts(FONTS));
const docs = { off: await build(false), on: await build(true) };

Les deux documents viennent d’une même configuration et d’une même chaîne Markdown. La seconde composition passe la configuration telle quelle ; la première, une copie avec le réglage désactivé, de sorte que le paquet qui s’ouvre dans le Sandbox porte fillShortColumns: true. Les deux boutons au-dessus du bureau passent d’une composition à l’autre. Seules les pages qui continuent sont remplies jusqu’au pied : la page 3 clôt l’article, et ses colonnes sont coupées à la même hauteur dans les deux compositions.

#3 · Ouvrir sur la peinture

script.js · lignes 59–85dans le code complet
const DEPTH = 150; // mm: how far down the page the painting bleeds
const sans = { fontFamily: 'Public Sans', fontWeight: 700, textTransform: 'uppercase' };
const at = (to, edge, x, y, width) => ({ anchor: { to, edge },
  offset: { x: mm(x), y: mm(y) }, ...(width && { size: { width: mm(width) } }) });
const air = { padding: { bottom: mm(6) } }; // counts: the text starts 6 mm under the byline
const opener = {
  enabled: true,
  slot: { elements: [
    { kind: 'image', id: 'art', resourceId: 'lens', placement: { anchor: { to: 'bleed',
      edge: 'top-left' }, size: { width: 'fill', height: mm(DEPTH) } } },
    { kind: 'text', id: 'credit', content: '{attr.credit}', ...sans, fontWeight: 500,
      fontSize: pt(6.5), letterSpacing: pt(0.6), color: col('muted'), align: 'right',
      placement: at('container', 'top-right', 0, DEPTH - TOP + 2) },
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...sans, fontSize: pt(8.5),
      letterSpacing: pt(1.7), color: col('brass'), align: 'left',
      placement: at('container', 'top-left', 0, DEPTH - TOP + 9) },
    { kind: 'text', id: 'headline', content: '{titleText}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(46), lineHeight: 1.02, color: col('ink'), align: 'left',
      placement: at('#kicker', 'below', 0, 2.5, 112) },
    { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: 'Libre Caslon Text',
      italic: true, fontSize: pt(12), lineHeight: 1.3, color: col('sea'), align: 'left',
      placement: at('#headline', 'right-of', 8, 2.5) },
    { kind: 'text', id: 'byline', content: '{attr.byline}', ...sans, fontSize: pt(7.5),
      letterSpacing: pt(1.3), color: col('ink'), align: 'left', box: air,
      placement: at('#standfirst', 'below', 0, 3.5) },
  ] },
};

La peinture est un élément image ancré au fond perdu, haut de 150 mm, qui atteint donc la coupe sur trois côtés, ce qu’aucun flottant ne fait. Le surtitre, le titre, le chapeau et la signature viennent des attributs de la ligne #. La signature se termine par une marge intérieure de 6 mm qui compte dans la hauteur de l’ouverture : les colonnes commencent sur la première ligne de grille au-dessous.

#4 · Des titres courants hors de l’ouverture

script.js · lignes 89–106dans le code complet
const label = { ...sans, fontWeight: 600, fontSize: pt(7.5), letterSpacing: pt(1.3),
  color: col('muted') };
const folio = { fontFamily: 'Public Sans', fontWeight: 700, fontSize: pt(8.5), color: col('ink') };
const pin = (edge, x, y) => ({ anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(y) } });
const sides = [['even', 'left', 1], ['odd', 'right', -1]]; // 1: the outer edge is on the left
const header = { elements: sides.flatMap(([parity, edge, s]) => [
  { kind: 'text', id: `folio-${parity}`, content: '{pageNumber}', ...folio,
    placement: pin(`top-${edge}`, s * OUTER, 12) },
  { kind: 'text', id: `head-${parity}`, ...label,
    content: s > 0 ? '{title} · {subtitle}' : '{chapterTitle}',
    placement: pin(`top-${edge}`, s * (OUTER + 9), 12.35) }, // the smaller size's baseline
  { kind: 'rule', id: `rule-${parity}`, direction: 'horizontal', thickness: pt(0.5),
    color: col('rule'), placement: { ...pin(`top-${edge}`, s * OUTER, 17),
      size: { width: mm(197) } } },
].map((element) => ({ ...element, parity, pages: 'body' }))) }; // none on the opener
const footer = { elements: sides.map(([parity, edge, s]) => ({ kind: 'text', parity,
  id: `drop-folio-${parity}`, ...folio, content: '{pageNumber}', pages: 'opener', align: edge,
  placement: pin(`bottom-${edge}`, s * OUTER, -11) })) }; // the opener's only folio

La page paire nomme le magazine et le numéro depuis le frontmatter, la page impaire l’article, et pages: 'body' les écarte de l’ouverture, qui ne porte qu’un folio en pied. L’étiquette descend de 0,35 mm par rapport au folio, pour que les deux corps partagent une ligne de base. Le filet placé dessous mesure 197 mm, la largeur du bloc de texte.

La recette complète

Sandbox
// ═══ Postext Cookbook · Nº 177 · Filling the column the widow rule left short ═════
// https://postext.dev/en/cookbook/filling-short-columns
// Code: MIT · Text: original (CC BY 4.0) · Painting: diffusion model
// Fonts: Public Sans, Libre Caslon Display, Libre Caslon Text (OFL) · Needs postext ≥ 1.65.0
//
// A magazine long read in two ragged columns, set twice. Without fillShortColumns a column
// whose paragraph would split into a widow ends a line above the foot; with it, a paragraph
// of that column is set a line longer or shorter, and the column ends on the last grid line.
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'filling-short-columns';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// Every colour below is linked to this palette by id.
const palette = {
  ink: '#1b2026', // text: a slate near-black
  brass: '#875a14', // the accent: kicker and crossheads (6:1 on paper)
  sea: '#2c4a5e', // the standfirst
  rule: '#c9ccd0', // hairlines
  muted: '#5d656e', // running heads, credit, colophon
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // The engine's defaults link to 'main-color': point it at the accent, so nothing prints blue.
  { id: 'main-color', name: 'brass (defaults)', value: { hex: palette.brass, model: 'hex' } },
];
const TOP = 22; // top margin in mm: the opener's container starts here
const OUTER = 14; // outer margin in mm: folios and running heads align to it
const LEAD = 13.2; // body leading in pt: the baseline grid every column ends on

// #region answer: ragged columns that end on the grid: fillShortColumns
const balancing = {
  // A ragged column has no lever of its own when the widow and orphan rules hold its last
  // line back: no heading or list end to open, no justified paragraph to set looser. This
  // one lets a paragraph of the short column run a line long, within its own word spacing
  // (or a justified one no looser than maxWordSpacing), or, when none can, set one a line
  // short so the split after it takes the line. A change is kept only when the pages end
  // with fewer short columns; otherwise the column stays as it was.
  fillShortColumns: true, // default false
  // The paragraphs go first: a crosshead keeps its one line of air unless they cannot help.
  maxLinesBeforeLoose: 0,
};
const bodyText = {

Le script.js composé s’exécute tel quel : collez-le dans le script de module de n’importe quelle page, ou ouvrez la recette sur CodePen. Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)

Variantes

#Laisser les intertitres prendre le blanc d’abord

Avec l’ordre par défaut, un titre prend jusqu’à deux lignes de grille avant qu’un paragraphe ne s’allonge, et les intertitres de la page suivante s’ouvrent de nouveau.

-  maxLinesBeforeLoose: 0,
+  maxLinesBeforeLoose: 2,

#Justifier le texte

Un paragraphe justifié pouvait déjà prendre une ligne de plus sans le réglage, dans la limite de maxWordSpacing. Avec fillShortColumns, il peut aussi en prendre une de moins, et il n’est jamais composé plus lâche que sa ligne la plus lâche.

-  textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between
+  textAlign: 'justify', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between

Erreurs fréquentes

  • Le réglage comble un blanc d’une ligne. Une colonne à laquelle il en manque deux, par exemple celle qui finit après l’espace entre paragraphes avec la place d’une seule ligne du suivant, peut rester courte : ajustez le texte d’une phrase.
  • Un paragraphe dont la nouvelle composition finirait sur une ligne trop courte n’est pas utilisé, si bien qu’une colonne peut rester courte quand elle n’a que des paragraphes brefs.

Crédits

Texte
Texte original, CC BY 4.0
Images
  • A lit Fresnel lens in a lighthouse lantern at dusk · Generated With Diffusion Models · original
Polices
Public Sans (SIL OFL 1.1) · Libre Caslon Display (SIL OFL 1.1) · Libre Caslon Text (SIL OFL 1.1)
Sandbox