Breaking text into lines

Greedy line breaking, why not the optimal kind, and measuring text without a font.

In the code: foundation/geometry.py

Every label in every diagram goes through this, and it starts from an embarrassing position: there is no font engine.

Measuring text you cannot measure

To size a box around a label you must know how wide the label is, which properly means asking a font for the advance width of each glyph, applying kerning, and handling ligatures. Sauvignon is stdlib-only and emits SVG for someone else's renderer to draw, so it has none of that.

So it approximates. Glyph advance is taken as a fixed fraction of the font size — about 0.58 for common sans-serif faces — and a string's width is its length times that. It is wrong for every individual string, and close enough on average that boxes look right.

The honest consequence: Illlli and WWWWWW measure the same, so a label of all-narrow or all-wide characters gets a box that is too roomy or slightly tight. The fix is a real metrics table per font, which is a bounded amount of work and a lot of data. The approximation buys the whole library being dependency-free.

<diagram padding="16">
  <row gap="14" align="start">
    <contains padding="10">
      <text width="150">A label long enough that it has to wrap onto several lines</text>
    </contains>
    <contains padding="10">
      <text width="150">Il lililli illi lilli illil lilil illi lillil ill lilli il</text>
    </contains>
  </row>
</diagram>

Both boxes are the same width and hold exactly the same number of characters — fifty-eight each. Character count is the only input the model has, so it breaks both into four lines and reserves both the same height. The second is visibly emptier: every one of its glyphs is narrower than the 0.58 average, and the error compounds across the whole string. That is the approximation showing its edge.

(The text is inside a <contains> because a shape's own label does not wrap — only the <text> mark does. That asymmetry is itself a limitation worth knowing about: a long label on a <rect> runs past its box rather than breaking.)

Greedy is the right kind of wrong

Given widths, breaking into lines is the classic problem. Fill the current line with words until the next one would not fit, then start a new line. It is greedy (or first-fit): each break is decided locally, in one pass, without looking ahead.

The famous alternative is Knuth and Plass's total-fit algorithm, from TeX: score each possible set of breakpoints by total "badness" — how stretched or squeezed the lines are — and use dynamic programming to find the arrangement minimising it over the whole paragraph. It produces noticeably better-looking text, because a slightly worse early break can save a terrible later one.

Sauvignon uses greedy, deliberately:

This is the same trade as everywhere else in the engine: the good-enough algorithm, chosen knowingly, with the better one named so the choice can be revisited.

The two intrinsic widths fall out of it

Wrapping is what gives the sizing pass its two numbers (see measure, then place):

Neither needs the wrapping algorithm to run; both are computed directly. But they bound it: no useful line is wider than max-content, and a box narrower than min-content cannot hold the text without breaking a word.

When a word is wider than the line

Which happens. A URL, an identifier, a German compound. The word cannot fit, and greedy has nothing to give.

The last resort is to break the word itself, at character boundaries, into chunks that do fit — CSS calls this overflow-wrap: break-word. It is deliberately a separate pass that runs before the normal wrapping, chopping over-long words into pieces, so the main algorithm never has to cope with an unbreakable item. Otherwise every line-breaking loop needs an "and what if this single word doesn't fit" branch, which is how such code becomes unreadable.

Proper hyphenation — breaking at syllable boundaries with a dictionary or Liang's pattern algorithm, again from TeX — would be nicer and is a great deal more machinery for a diagram label.

Further reading. Knuth & Plass, Breaking Paragraphs into Lines (1981), which is worth reading even if you never implement it; Liang's thesis on hyphenation patterns (1983) for the other half of TeX's typesetting.