DEV Community

Cover image for Paginating a screenplay is a fixed-point problem
ShikiShiki
ShikiShiki

Posted on Fully Autonomous

Paginating a screenplay is a fixed-point problem

A screenplay page holds about 55 lines of 12 point Courier. Break the script every 55 lines and you have pagination.

That works until a block of dialogue lands on a boundary. Then the format demands two extra things. The bottom of the page gets a (MORE), and the top of the next page repeats the character name with a (CONT'D):

                    SARAH
          I told you this would happen. I
          told you in the car, I told you
                    (MORE)

          - - - - - page break - - - - -

                    SARAH (CONT'D)
          at the door, and I am telling you
          now.
Enter fullscreen mode Exit fullscreen mode

Those two markers are content. They take up lines on pages you already measured. So the break you just computed was computed against a line count that is now wrong.

The layout depends on its own output

Pagination looks like a chunking problem, so that is how everybody writes it the first time:

// the version everybody writes first
function paginate(lines, perPage = 55) {
  const pages = [];
  for (let i = 0; i < lines.length; i += perPage) {
    pages.push(lines.slice(i, i + perPage));
  }
  return pages;
}
Enter fullscreen mode Exit fullscreen mode

But the real function is not layout(content). It is closer to

pages = layout(content + continuations(pages))
Enter fullscreen mode Exit fullscreen mode

pages appears on both sides. You are not chunking a list, you are looking for a fixed point.

Why one extra pass is not enough

The obvious patch is to run the chunker, see which dialogue blocks got split, insert the markers, and run it again. Two passes, done.

It is better. It is still wrong, because the second pass creates splits the first pass did not have. You added lines, everything after the first split shifted down by two, and something that used to end comfortably mid-page now hangs off the bottom. New split. New markers. New shift.

The churn is worse than it sounds, because the other formatting rules also move content down and never up:

  • A scene heading needs at least two lines of the scene under it. Alone at the bottom of a page, it moves to the next one.
  • A dialogue block that breaks must leave at least two lines on each side of the break. One orphan line is not allowed.
  • You cannot break between a character cue and their first line.
  • You cannot break inside a parenthetical.

Each of those pushes a block forward, and each push can open a gap that lets a different block fit where it did not fit before, which pulls it back, which pushes the next one forward. Implement the rules as a straight loop with no discipline and you can watch it oscillate between two layouts forever.

Iterating to a fixed point, and making it stop

The trick that makes this terminate is monotonicity. Track the set of dialogue blocks known to break, and only ever add to it. Never remove one because a later pass made it fit.

function paginate(elements, perPage = 55, maxPasses = 8) {
  const splits = new Set();

  for (let pass = 0; pass < maxPasses; pass++) {
    const pages = layout(withContinuations(elements, splits), perPage);
    const observed = findSplitDialogue(pages);

    // every split we saw is one we already knew about: fixed point
    if ([...observed].every((s) => splits.has(s))) return pages;

    for (const s of observed) splits.add(s);
  }

  throw new Error("pagination did not settle");
}
Enter fullscreen mode Exit fullscreen mode

The termination argument is short. splits only grows, it is bounded by the number of dialogue blocks in the script, and every pass that does not return adds at least one member. So the loop ends.

You give up something for that guarantee. A block marked as split on pass two might genuinely fit on one page once everything settles, and you keep the (MORE) anyway. In exchange you get a layout that is stable and reproducible, which matters more than saving one line, because two people opening the same script have to see the same page numbers.

In practice it settles in three or four passes. If yours needs eight, one of the widow rules is fighting another one and the bug is there, not in the loop.

Why anyone should care about two lines

Because the page count is not cosmetic. The convention is one page, one minute, and the industry budgets against that number: shooting days, crew, insurance, the schedule. A 110 page script is a 110 minute film until someone proves otherwise.

Do the arithmetic on a dialogue-heavy script. Forty split blocks, two lines each, is eighty extra lines. At 55 lines a page that is a page and a half of runtime a single-pass paginator never reports. Not a rounding error. A day.

Checking your own numbers

I keep a few of these as browser tools, no account needed, mostly because I got tired of opening a full editor to answer one question.

Script time calculator, page count and runtime, for a second opinion on what your paginator says.

Screenplay format checker, for the widow and orphan rules above.

Fountain to PDF, which runs the fixed point described here and shows the breaks it landed on.

Scene extractor, to pull the element list out of an existing script if you need test data.

If you are building a renderer and want to compare page breaks against another implementation, the Fountain converter is the useful one. Feed it something with long speeches near a boundary and see whether you agree.

Top comments (0)