单一来源
当前只有一个可用来源,尚未完成多源交叉核验使用Groff创作中篇小说
blog - git - desktop - contact
Novellas with groff
2026-10-31
I recently finished my second "novella".
Don't even think about it: I won't publish this. Nor send it to you privately, which is mostly the same as publishing it.
The first story is roughly 50 A5 pages long (~15k words), the second one is about 120 pages (~40k words). (Both barely fit the definition of a "novella", apparently, with the first one being a bit too short and the second one a tiny bit too long.)
It feels pretty absurd and surreal that I have done this at all. In school, anything related to "writing" was pure horror for me -- unless it was related to maths, physics, or other "technical" stuff. But here we are.
How long did it take? Well, the first one took over seven months. But the second one, the longer one? Ten days.
Some time last year, probably around July, I started reading some of the books that were lying around in my appartment for decades. And this was much more enjoyable than I thought. No idea why. It's not like I never read a book before, but it was always a somewhat boring thing to do and it took me ages -- unless it was something technical. I'm only referring to fiction and novels here. Like, one book per year, tops. Yet, I somehow got hooked last year and now I've read about 30 novels since July 2025. Some faster (a couple of days), some slower (three months).
It wasn't that much of a stretch to try to do this myself.
This blog is still technical in nature, so I'll walk through the software that I used.
Table of contents:
Basic formatting
· Footers and page numbers
· A closer look at traps
· Inter-sentence spacing
· German hyphenation
· Markdown and other pre-processing
· Reflowing text
· demo-book.tar.gz: Putting it all together
· The writing process itself
· Where to go from here?
groff
Both of my books (they're not really "books", but I'll keep referring to them as such for the sake of simplicity) are typeset in groff. You know, the thing that usually renders manual pages in your terminal on Linux. groff/troff is a full typesetting system, though, and it can be used to produce real books, which I've already mentioned a couple of times in previous blog posts.
In my case, the appeal of groff is that it's relatively small and fast, but still able to produce high-quality output. Making that 50-page document takes about 700 ms on my 13.5 year old PC, and the 120-page one takes 1.6 seconds. This includes some pre-processing which we'll cover later. Given that most modern software these days feels like walking in mud, this is refreshingly quick.
Because it's more fun that way, I made my own little macro package. There really isn't a lot to it, though. It's tiny. Not a lot is needed for a simple novel / novella -- no table of contents, no cross-references, no diagrams, no code blocks, no lists, ... Just nicely flowing text and sometimes a word gets emphasized.
Disclaimer: You'll note some TODO items in the text below. groff is a full-blown typesetting system and these things are complicated. I'm a long way from having mastered every aspect of it.
Basic formatting
There are only three macros:
.\" Section header / chapter title .de SH . sp 1v . ps +5p . ce \f[HR]\\$1\fP . ps . sp 4v .. .\" Normal paragraph with hanging indent. .de PP . br . ti +2m .. .\" "Long" paragraph, longer empty space. .de LP . sp 2v ..
(The name SH is a spill-over from manual pages. I've written quite a few of those and this name somehow stuck. CH for "chapter heading" or CT for "chapter title" might have been better choices. Or, you know, .CHAPTER.)
You start defining a macro with .de -- anything until .. is part of that macro.
· .SH: Go down one
vee, increase type size, request centering, print the macro's first argument with the HR font (Helvetica), return to previous type size, add another four vees of space.
· .PP: Just break the current line, then request a temporary indent
for the next output line. This creates a hanging indent, which is customary in German books.
· .LP: Just some empty space between paragraphs. Used for things
like intra-chapter time jumps.
You can look up the individual commands in man 7 groff.
And that would already be enough to produce a document that somewhat resembles a book.
Put the snippet above in a file called macros.groff. I made one file per chapter. For example: chapter01.groff. You can then make a PDF using a command like this:
groff -Tpdf -dpaper=a5 -P-pa5 macros.groff chapter01.groff >book.pdf
A5 is roughly the page size of a "typical book".
Footers and page numbers
You'll note that there are no page numbers by default. You could actually just decide to not have them if all you want is a digital PDF. They're useful for printing, but you don't really need them in a file, if we're being honest.
Except: The thing gets confused about page breaks. Create the PDF using the example above and you'll see:
Looks like the baseline of the last line of output is just outside the page. Not sure what's going on here -- it's related to setting the page size to A5. In other words, I probably missed another spot where this has to be set. It'll be fixed implicitly by the following.
The whole page number thing consists of three parts: First, how large is the footer (the area where the page numbers live)? I defined this in a register (which is not really necessary but I find the "code" easier to read this way) -- the name is arbitary:
.nr _FooterSize 5v
We than plant a trap at the bottom of the page:
.wh -\n[_FooterSize]u _FooterTrap
\n[_FooterSize] "gets" the value of the register called _FooterSize. There's a minus sign in front of it, it's negative and relative to the bottom of the page. The documentation says:
A general-purpose register (one created or updated with the nr request) is implicitly dimensionless, or reckoned in basic units if interpreted in a measurement context.
I'm not 100% sure why I need to add the u. I haven't read every page of the manual, yet. Needless to say, it doesn't work without it.
Okay, so the trap gets installed at 5v before the end of the page and it'll call the _FooterTrap macro. It looks like this:
.de _FooterTrap .\" Push current environment on stack and switch to `_FooterEnv`. .\" Parent env will keep partially collected lines. . ev _FooterEnv .\" Copy settings from default environment. . evc 0 .\" Don't do this for pages 1 and 2, they're title pages in my actual .\" "book". . if \\n[%]>2 \{\ .\" Set vertical position, explained in the text below. . sp |\\n[.p]u-2v .\" Print % register centered. . ce \fR\\n[%]\fP \} .\" Go back to previous environment. . ev .\" Request page break. ' bp ..
It's much easier to read without the inline comments:
.de _FooterTrap . ev _FooterEnv . evc 0 . if \\n[%]>2 \{\ . sp |\\n[.p]u-2v . ce \fR\\n[%]\fP \} . ev ' bp ..
Why is it 'bp instead of .bp? We'll get to that.
What I'm unsure about: I set the vertical position to |\\n[.p]u-2v. That's relative to the top of the page, it's the page length minus two vee. I would have expected |-2v-1v (including the "minus one vee" mentioned in the docs) to also work, but that leads to an infinite loop (probably triggers the same trap again and again). Why?
Header
For symmetry, I also want some empty space at the top of the page:
.nr _HeaderSize 5v .de _HeaderTrap ' sp |\\n[_HeaderSize]u .. .wh 0v _HeaderTrap
And another TODO for me: This isn't 100% symmetrical. The length of the header is a bit larger than the footer. An issue with the page size again?
A closer look at traps
Let's have a closer look at traps. I'm not 100% sure on this -- this is to the best of my understanding.
Actually, let's take a step back from the example above. Let's start with this:
This is a little excerpt from Mary Shelly's Frankenstein.
These visions faded when I perused, for the first time, those poets whose effusions entranced my soul and lifted it to heaven. I also
Evidence
来源与证据
当前仅有一个可用来源,尚未完成多源交叉核验。
- 发布时间
- 内容处理
- AI 翻译 / 摘要,需结合原文核对
相关资讯
AI 翻译、摘要和解读仅用于辅助判断;涉及重要决策时,请回到上方来源核实。