shithub: neatroff

Download patch

ref: c55cd099d93cd4a34a48f61bfffdba6866ea263a
parent: a964826bb75b0776bf34898855aa8193b84c4e7c
author: Ali Gholami Rudi <ali@rudi.ir>
date: Thu Apr 24 18:51:10 EDT 2014

fmt: collect words in fmt buffer for whole paragraphs

This patch prepares neatroff for filling whole paragraphs
at once.

--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@
 all: roff
 %.o: %.c roff.h
 	$(CC) -c $(CFLAGS) $<
-roff: roff.o dev.o font.o in.o cp.o tr.o ren.o out.o reg.o sbuf.o adj.o eval.o draw.o wb.o hyph.o map.o clr.o char.o
+roff: roff.o dev.o font.o in.o cp.o tr.o ren.o out.o reg.o sbuf.o fmt.o eval.o draw.o wb.o hyph.o map.o clr.o char.o
 	$(CC) -o $@ $^ $(LDFLAGS)
 clean:
 	rm -f *.o roff
--- a/adj.c
+++ /dev/null
@@ -1,243 +1,0 @@
-/* adjustment buffer for putting words into lines */
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include "roff.h"
-
-#define ADJ_LLEN(a)	MAX(0, (a)->ll - (a)->li)
-
-struct adj {
-	char *words[NWORDS];	/* words to be adjusted */
-	int wids[NWORDS];	/* the width of words */
-	int elsn[NWORDS];	/* els_neg of words */
-	int elsp[NWORDS];	/* els_pos of words */
-	int gaps[NWORDS];	/* gaps before words */
-	int nwords;
-	int wid;		/* total width of buf */
-	int swid;		/* current space width */
-	int gap;		/* space before the next word */
-	int nls;		/* newlines before the next word */
-	int li, ll;		/* current line indentation and length */
-	int filled;		/* filled all words in the last adj_fill() */
-	int eos;		/* last word ends a sentence */
-};
-
-/* .ll, .in and .ti are delayed until the partial line is output */
-static void adj_confupdate(struct adj *adj)
-{
-	adj->ll = n_l;
-	adj->li = n_ti > 0 ? n_ti : n_i;
-	n_ti = 0;
-}
-
-/* does the adjustment buffer need to be flushed without filling? */
-static int adj_fullnf(struct adj *a)
-{
-	/* blank lines; indented lines; newlines when buffer is empty */
-	return a->nls > 1 || (a->nls && a->gap) ||
-			(a->nls - a->filled > 0 && !a->nwords);
-}
-
-/* does the adjustment buffer need to be flushed? */
-int adj_full(struct adj *a, int fill)
-{
-	if (!fill)
-		return a->nls - a->filled > 0;
-	if (adj_fullnf(a))
-		return 1;
-	return a->nwords && a->wid > ADJ_LLEN(a);
-}
-
-/* is the adjustment buffer empty? */
-int adj_empty(struct adj *a, int fill)
-{
-	return !fill ? a->nls - a->filled <= 0 : !a->nwords && !adj_fullnf(a);
-}
-
-/* set space width */
-void adj_swid(struct adj *adj, int swid)
-{
-	adj->swid = swid;
-}
-
-/* move words inside an adj struct */
-static void adj_movewords(struct adj *a, int dst, int src, int len)
-{
-	memmove(a->words + dst, a->words + src, len * sizeof(a->words[0]));
-	memmove(a->wids + dst, a->wids + src, len * sizeof(a->wids[0]));
-	memmove(a->elsn + dst, a->elsn + src, len * sizeof(a->elsn[0]));
-	memmove(a->elsp + dst, a->elsp + src, len * sizeof(a->elsp[0]));
-	memmove(a->gaps + dst, a->gaps + src, len * sizeof(a->gaps[0]));
-}
-
-static char *adj_strdup(char *s)
-{
-	int l = strlen(s);
-	char *r = malloc(l + 1);
-	memcpy(r, s, l + 1);
-	return r;
-}
-
-/* copy word buffer wb in adj->words[i] */
-static void adj_word(struct adj *adj, int i, struct wb *wb, int gap)
-{
-	adj->words[i] = adj_strdup(wb_buf(wb));
-	adj->wids[i] = wb_wid(wb);
-	adj->elsn[i] = wb->els_neg;
-	adj->elsp[i] = wb->els_pos;
-	adj->gaps[i] = gap;
-}
-
-static int adj_linewid(struct adj *a, int n)
-{
-	int i, w = 0;
-	for (i = 0; i < n; i++)
-		w += a->wids[i] + a->gaps[i];
-	return w;
-}
-
-static int adj_linefit(struct adj *a, int llen)
-{
-	int i, w = 0;
-	for (i = 0; i < a->nwords; i++) {
-		w += a->wids[i] + a->gaps[i];
-		if (w > llen)
-			return i;
-	}
-	return i;
-}
-
-/* move n words from the adjustment buffer to s */
-static int adj_move(struct adj *a, int n, struct sbuf *s, int *els_neg, int *els_pos)
-{
-	int w = 0;
-	int i;
-	*els_neg = 0;
-	*els_pos = 0;
-	for (i = 0; i < n; i++) {
-		sbuf_printf(s, "%ch'%du'", c_ec, a->gaps[i]);
-		sbuf_append(s, a->words[i]);
-		w += a->wids[i] + a->gaps[i];
-		if (a->elsn[i] < *els_neg)
-			*els_neg = a->elsn[i];
-		if (a->elsp[i] > *els_pos)
-			*els_pos = a->elsp[i];
-		free(a->words[i]);
-	}
-	if (!n)
-		return 0;
-	a->nwords -= n;
-	adj_movewords(a, 0, n, a->nwords);
-	a->wid = adj_linewid(a, a->nwords);
-	if (a->nwords)		/* apply the new .l and .i */
-		adj_confupdate(a);
-	return w;
-}
-
-/* try to hyphenate the n-th word */
-static void adj_hyph(struct adj *a, int n, int w, int hyph)
-{
-	struct wb w1, w2;
-	int flg = hyph | (n ? 0 : HY_ANY);
-	wb_init(&w1);
-	wb_init(&w2);
-	if (!wb_hyph(a->words[n], w, &w1, &w2, flg)) {
-		adj_movewords(a, n + 2, n + 1, a->nwords - n);
-		free(a->words[n]);
-		adj_word(a, n, &w1, a->gaps[n]);
-		adj_word(a, n + 1, &w2, 0);
-		a->nwords++;
-		a->wid = adj_linewid(a, a->nwords);
-	}
-	wb_done(&w1);
-	wb_done(&w2);
-}
-
-/* fill and copy a line into s */
-int adj_fill(struct adj *a, int ad_b, int fill, int hyph, struct sbuf *s,
-		int *li, int *ll, int *els_neg, int *els_pos)
-{
-	int adj_div, adj_rem;
-	int w = 0;
-	int i, n;
-	int llen = ADJ_LLEN(a);
-	*ll = a->ll;
-	*li = a->li;
-	if (!fill || adj_fullnf(a)) {
-		a->filled = 0;
-		a->nls--;
-		return adj_move(a, a->nwords, s, els_neg, els_pos);
-	}
-	n = adj_linefit(a, llen);
-	if (n < a->nwords)
-		adj_hyph(a, n, llen - adj_linewid(a, n) - a->gaps[n], hyph);
-	n = adj_linefit(a, llen);
-	if (!n && a->nwords)
-		n = 1;
-	w = adj_linewid(a, n);
-	if (ad_b && n > 1) {
-		adj_div = (llen - w) / (n - 1);
-		adj_rem = (llen - w) % (n - 1);
-		for (i = 0; i < n - 1; i++)
-			a->gaps[i + 1] += adj_div + (i < adj_rem);
-	}
-	w = adj_move(a, n, s, els_neg, els_pos);
-	if (a->nwords)
-		a->wid -= a->gaps[0];
-	a->gaps[0] = 0;
-	a->filled = n && !a->nwords;
-	return w;
-}
-
-void adj_sp(struct adj *adj)
-{
-	adj->gap += adj->swid;
-}
-
-void adj_nl(struct adj *adj)
-{
-	adj->nls++;
-	adj->gap = 0;
-}
-
-/* ignore the previous newline */
-void adj_nonl(struct adj *adj)
-{
-	if (adj->nls)
-		adj->gap += adj->swid;
-	adj->nls = 0;
-}
-
-/* insert wb into the adjustment buffer */
-void adj_wb(struct adj *adj, struct wb *wb)
-{
-	if (wb_empty(wb) || adj->nwords == NWORDS)
-		return;
-	if (!adj->nwords)	/* apply the new .l and .i */
-		adj_confupdate(adj);
-	if (adj->nls && !adj->gap && adj->nwords >= 1)
-		adj->gap = (adj->nwords && adj->eos) ? adj->swid * 2 : adj->swid;
-	adj_word(adj, adj->nwords++, wb, adj->filled ? 0 : adj->gap);
-	adj->filled = 0;
-	adj->wid += adj->wids[adj->nwords - 1] + adj->gaps[adj->nwords - 1];
-	adj->nls = 0;
-	adj->gap = 0;
-	adj->eos = wb_eos(wb);
-}
-
-struct adj *adj_alloc(void)
-{
-	struct adj *adj = malloc(sizeof(*adj));
-	memset(adj, 0, sizeof(*adj));
-	return adj;
-}
-
-void adj_free(struct adj *adj)
-{
-	free(adj);
-}
-
-int adj_wid(struct adj *adj)
-{
-	return adj->wid + (adj->nls ? adj->swid : adj->gap);
-}
--- /dev/null
+++ b/fmt.c
@@ -1,0 +1,321 @@
+/*
+ * line formatting buffer for line adjustment and hyphenation
+ *
+ * The line formatting buffer does two main functions: breaking
+ * words into lines (possibly after hyphenating some of them), and, if
+ * requested, adjusting the space between words in a line.  In this
+ * file the first step is referred to as filling.
+ *
+ * Inputs are specified via these functions:
+ * + fmt_word(): for appending space-separated words.
+ * + fmt_space(): for appending spaces.
+ * + fmt_newline(): for appending new lines.
+ *
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "roff.h"
+
+#define FMT_LLEN(f)	MAX(0, (f)->ll - (f)->li)
+#define FMT_FILL(f)	(!n_ce && n_u)
+#define FMT_ADJ(f)	(n_u && !n_na && !n_ce && (n_j & AD_B) == AD_B)
+#define FMT_SWID(f)	(spacewid(n_f, n_s))
+
+struct word {
+	char *s;
+	int wid;
+	int elsn, elsp;
+	int gap;
+};
+
+struct line {
+	struct sbuf sbuf;
+	int wid, li, ll;
+	int elsn, elsp;
+};
+
+struct fmt {
+	/* queued words */
+	struct word words[NWORDS];
+	int nwords;
+	/* queued lines */
+	struct line lines[NLINES];
+	int l_head, l_tail;
+	/* current line */
+	int gap;		/* space before the next word */
+	int nls;		/* newlines before the next word */
+	int li, ll;		/* current line indentation and length */
+	int filled;		/* filled all words in the last fmt_fill() */
+	int eos;		/* last word ends a sentence */
+};
+
+/* .ll, .in and .ti are delayed until the partial line is output */
+static void fmt_confupdate(struct fmt *f)
+{
+	f->ll = n_l;
+	f->li = n_ti >= 0 ? n_ti : n_i;
+	n_ti = -1;
+}
+
+/* move words inside an fmt struct */
+static void fmt_movewords(struct fmt *a, int dst, int src, int len)
+{
+	memmove(a->words + dst, a->words + src, len * sizeof(a->words[0]));
+}
+
+static char *fmt_strdup(char *s)
+{
+	int l = strlen(s);
+	char *r = malloc(l + 1);
+	memcpy(r, s, l + 1);
+	return r;
+}
+
+/* copy word buffer wb in fmt->words[i] */
+static void fmt_insertword(struct fmt *f, int i, struct wb *wb, int gap)
+{
+	struct word *w = &f->words[i];
+	w->s = fmt_strdup(wb_buf(wb));
+	w->wid = wb_wid(wb);
+	w->elsn = wb->els_neg;
+	w->elsp = wb->els_pos;
+	w->gap = gap;
+}
+
+/* the total width of the first n words in f->words[] */
+static int fmt_wordslen(struct fmt *f, int n)
+{
+	int i, w = 0;
+	for (i = 0; i < n; i++)
+		w += f->words[i].wid + f->words[i].gap;
+	return w;
+}
+
+/* select as many words as can be fit in llen */
+static int fmt_linefit(struct fmt *f, int llen)
+{
+	int i, w = 0;
+	for (i = 0; i < f->nwords; i++) {
+		w += f->words[i].wid + f->words[i].gap;
+		if (w > llen)
+			return i;
+	}
+	return i;
+}
+
+/* move n words from the buffer to s */
+static int fmt_move(struct fmt *f, int n, struct sbuf *s, int *els_neg, int *els_pos)
+{
+	struct word *wcur;
+	int w = 0;
+	int i;
+	*els_neg = 0;
+	*els_pos = 0;
+	for (i = 0; i < n; i++) {
+		wcur = &f->words[i];
+		sbuf_printf(s, "%ch'%du'", c_ec, wcur->gap);
+		sbuf_append(s, wcur->s);
+		w += wcur->wid + wcur->gap;
+		if (wcur->elsn < *els_neg)
+			*els_neg = wcur->elsn;
+		if (wcur->elsp > *els_pos)
+			*els_pos = wcur->elsp;
+		free(wcur->s);
+	}
+	if (!n)
+		return 0;
+	f->nwords -= n;
+	fmt_movewords(f, 0, n, f->nwords);
+	if (f->nwords)		/* apply the new .l and .i */
+		fmt_confupdate(f);
+	return w;
+}
+
+/* try to hyphenate the n-th word */
+static void fmt_hyph(struct fmt *f, int n, int w, int hyph)
+{
+	struct wb w1, w2;
+	int flg = hyph | (n ? 0 : HY_ANY);
+	wb_init(&w1);
+	wb_init(&w2);
+	if (!wb_hyph(f->words[n].s, w, &w1, &w2, flg)) {
+		fmt_movewords(f, n + 2, n + 1, f->nwords - n);
+		free(f->words[n].s);
+		fmt_insertword(f, n, &w1, f->words[n].gap);
+		fmt_insertword(f, n + 1, &w2, 0);
+		f->nwords++;
+	}
+	wb_done(&w1);
+	wb_done(&w2);
+}
+
+/* estimated number of lines until traps or the end of a page */
+static int ren_safelines(void)
+{
+	return f_nexttrap() / (MAX(1, n_L) * n_v);
+}
+
+static int fmt_nlines(struct fmt *f)
+{
+	if (f->l_tail <= f->l_head)
+		return f->l_head - f->l_tail;
+	return NLINES - f->l_tail + f->l_head;
+}
+
+int fmt_fill(struct fmt *f, int all)
+{
+	int llen, fmt_div, fmt_rem;
+	int w = 0;
+	int i, n;
+	struct line *l;
+	int hyph = n_hy;
+	if (!FMT_FILL(f))
+		return 0;
+	while (f->nwords) {
+		l = &f->lines[f->l_head];
+		llen = FMT_LLEN(f);
+		if ((f->l_head + 1) % NLINES == f->l_tail)
+			return 1;
+		l->li = f->li;
+		l->ll = f->ll;
+		n = fmt_linefit(f, llen);
+		if (n == f->nwords && !all)
+			break;
+		if ((n_hy & HY_LAST) && ren_safelines() < 2 + fmt_nlines(f))
+			hyph = 0;	/* disable hyphenation for final lines */
+		if (n < f->nwords)
+			fmt_hyph(f, n, llen - fmt_wordslen(f, n) -
+					f->words[n].gap, hyph);
+		n = fmt_linefit(f, llen);
+		if (!n && f->nwords)
+			n = 1;
+		w = fmt_wordslen(f, n);
+		if (FMT_ADJ(f) && n > 1) {
+			fmt_div = (llen - w) / (n - 1);
+			fmt_rem = (llen - w) % (n - 1);
+			for (i = 0; i < n - 1; i++)
+				f->words[i + 1].gap += fmt_div + (i < fmt_rem);
+		}
+		sbuf_init(&l->sbuf);
+		l->wid = fmt_move(f, n, &l->sbuf, &l->elsn, &l->elsp);
+		f->words[0].gap = 0;
+		f->filled = n && !f->nwords;
+		f->l_head = (f->l_head + 1) % NLINES;
+	}
+	return 0;
+}
+
+/* return the next line in the buffer */
+int fmt_nextline(struct fmt *f, struct sbuf *sbuf, int *w,
+		int *li, int *ll, int *els_neg, int *els_pos)
+{
+	struct line *l;
+	l = &f->lines[f->l_tail];
+	if (f->l_head == f->l_tail)
+		return 1;
+	*li = l->li;
+	*ll = l->ll;
+	*w = l->wid;
+	*els_neg = l->elsn;
+	*els_pos = l->elsp;
+	sbuf_append(sbuf, sbuf_buf(&l->sbuf));
+	sbuf_done(&l->sbuf);
+	f->l_tail = (f->l_tail + 1) % NLINES;
+	return 0;
+}
+
+static int fmt_sp(struct fmt *f)
+{
+	struct line *l;
+	fmt_fill(f, 0);
+	if ((f->l_head + 1) % NLINES == f->l_tail)
+		return 1;
+	l = &f->lines[f->l_head];
+	f->filled = 0;
+	f->nls--;
+	l->li = f->li;
+	l->ll = f->ll;
+	sbuf_init(&l->sbuf);
+	l->wid = fmt_move(f, f->nwords, &l->sbuf, &l->elsn, &l->elsp);
+	f->l_head = (f->l_head + 1) % NLINES;
+	return 0;
+}
+
+void fmt_br(struct fmt *f)
+{
+	fmt_fill(f, 0);
+	f->filled = 0;
+	if (f->nwords)
+		fmt_sp(f);
+}
+
+void fmt_space(struct fmt *fmt)
+{
+	fmt->gap += FMT_SWID(fmt);
+}
+
+void fmt_newline(struct fmt *f)
+{
+	f->nls++;
+	f->gap = 0;
+	if (!FMT_FILL(f)) {
+		fmt_sp(f);
+		return;
+	}
+	if (f->nls == 1 && !f->filled && !f->nwords)
+		fmt_sp(f);
+	if (f->nls > 1) {
+		if (!f->filled)
+			fmt_sp(f);
+		fmt_sp(f);
+	}
+}
+
+/* insert wb into fmt */
+void fmt_word(struct fmt *f, struct wb *wb)
+{
+	if (f->nwords == NWORDS)
+		fmt_fill(f, 0);
+	if (wb_empty(wb) || f->nwords == NWORDS)
+		return;
+	if (FMT_FILL(f) && f->nls && f->gap)
+		fmt_sp(f);
+	if (!f->nwords)		/* apply the new .l and .i */
+		fmt_confupdate(f);
+	if (f->nls && !f->gap && f->nwords >= 1)
+		f->gap = (f->nwords && f->eos) ? FMT_SWID(f) * 2 : FMT_SWID(f);
+	fmt_insertword(f, f->nwords++, wb, f->filled ? 0 : f->gap);
+	f->filled = 0;
+	f->nls = 0;
+	f->gap = 0;
+	f->eos = wb_eos(wb);
+}
+
+struct fmt *fmt_alloc(void)
+{
+	struct fmt *fmt = malloc(sizeof(*fmt));
+	memset(fmt, 0, sizeof(*fmt));
+	return fmt;
+}
+
+void fmt_free(struct fmt *fmt)
+{
+	free(fmt);
+}
+
+int fmt_wid(struct fmt *fmt)
+{
+	return fmt_wordslen(fmt, fmt->nwords) +
+		(fmt->nls ? FMT_SWID(fmt) : fmt->gap);
+}
+
+int fmt_morewords(struct fmt *fmt)
+{
+	return fmt_morelines(fmt) || fmt->nwords;
+}
+
+int fmt_morelines(struct fmt *fmt)
+{
+	return fmt->l_head != fmt->l_tail;
+}
--- a/reg.c
+++ b/reg.c
@@ -13,7 +13,7 @@
 	int eregs[NENVS];	/* environment-specific number registers */
 	int tabs[NTABS];	/* tab stops */
 	char tabs_type[NTABS];	/* type of tabs: L, C, R */
-	struct adj *adj;	/* per environment line buffer */
+	struct fmt *fmt;	/* per environment line formatting buffer */
 	char tc[GNLEN];		/* tab character (.tc) */
 	char lc[GNLEN];		/* leader character (.lc) */
 	char hc[GNLEN];		/* hyphenation character (.hc) */
@@ -167,13 +167,13 @@
 {
 	struct env *env = malloc(sizeof(*env));
 	memset(env, 0, sizeof(*env));
-	env->adj = adj_alloc();
+	env->fmt = fmt_alloc();
 	return env;
 }
 
 static void env_free(struct env *env)
 {
-	adj_free(env->adj);
+	fmt_free(env->fmt);
 	free(env);
 }
 
@@ -261,9 +261,9 @@
 	env_set(id);
 }
 
-struct adj *env_adj(void)
+struct fmt *env_fmt(void)
 {
-	return env->adj;
+	return env->fmt;
 }
 
 char *env_hc(void)
--- a/ren.c
+++ b/ren.c
@@ -5,7 +5,7 @@
 #include <string.h>
 #include "roff.h"
 
-#define cadj		env_adj()		/* line buffer */
+#define cfmt		env_fmt()		/* line buffer */
 #define RENWB(wb)	((wb) == &ren_wb)	/* is ren_wb */
 
 /* diversions */
@@ -38,6 +38,7 @@
 static int bp_count;		/* number of pages so far */
 static int bp_ejected;		/* current ejected page */
 static int bp_final;		/* 1: executing em, 2: the final page, 3: the 2nd final page */
+static int ren_level;		/* the depth of render_rec() calls */
 
 static char c_fa[GNLEN];	/* field delimiter */
 static char c_fb[GNLEN];	/* field padding */
@@ -105,7 +106,7 @@
 
 int f_hpos(void)
 {
-	return adj_wid(cadj) + wb_wid(&ren_wb);
+	return fmt_wid(cfmt) + wb_wid(&ren_wb);
 }
 
 void tr_divbeg(char **args)
@@ -166,10 +167,16 @@
 	}
 }
 
+static int render_rec(int level);
 static void trap_exec(int reg)
 {
-	if (str_get(reg))
+	char cmd[16];
+	if (str_get(reg)) {
+		sprintf(cmd, "%c%s %d\n", c_cc, TR_POPREN, ren_level);
+		in_pushnl(cmd, NULL);
 		in_pushnl(str_get(reg), NULL);
+		render_rec(++ren_level);
+	}
 }
 
 static int detect_traps(int beg, int end)
@@ -221,9 +228,9 @@
 	int ljust = li;
 	int llen = ll - ljust;
 	n_n = w;
-	if (ad == AD_C)
+	if ((ad & AD_B) == AD_C)
 		ljust += llen > w ? (llen - w) / 2 : 0;
-	if (ad == AD_R)
+	if ((ad & AD_B) == AD_R)
 		ljust += llen - w;
 	if (ljust)
 		sbuf_printf(spre, "%ch'%du'", c_ec, ljust);
@@ -300,13 +307,6 @@
 	wb_done(&wb);
 }
 
-/* return one if the next line causes a trap or new page */
-static int ren_lastline(void)
-{
-	int lspc = MAX(1, n_L) * n_v;
-	return detect_traps(n_d, n_d + lspc) || detect_pagelimit(lspc);
-}
-
 /* process a line and print it with ren_out() */
 static int ren_line(char *line, int w, int ad, int body,
 		int li, int ll, int els_neg, int els_pos)
@@ -349,21 +349,21 @@
 	return 0;
 }
 
-/* output current line; returns 1 if triggered a trap */
-static int ren_bradj(struct adj *adj, int fill, int ad)
+/* read a line from fmt and send it to ren_line() */
+static int ren_passline(struct fmt *fmt)
 {
 	struct sbuf sbuf;
-	int ll, li, els_neg, els_pos;
-	int w, hyph, ret;
+	int ll, li, els_neg, els_pos, w, ret;
+	int ad = n_j;
 	ren_first();
-	if (adj_empty(adj, fill))
+	if (!fmt_morewords(fmt))
 		return 0;
 	sbuf_init(&sbuf);
-	hyph = n_hy;
-	if ((n_hy & HY_LAST) && ren_lastline())
-		hyph = 0;	/* disable hyphenation final lines */
-	w = adj_fill(adj, ad == AD_B, fill, hyph, &sbuf,
-			&li, &ll, &els_neg, &els_pos);
+	fmt_nextline(fmt, &sbuf, &w, &li, &ll, &els_neg, &els_pos);
+	if (!n_u || n_na)
+		ad = AD_L;
+	if (n_ce)
+		ad = AD_C;
 	ret = ren_line(sbuf_buf(&sbuf), w, ad, 1, li, ll, els_neg, els_pos);
 	sbuf_done(&sbuf);
 	return ret;
@@ -370,20 +370,19 @@
 }
 
 /* output current line; returns 1 if triggered a trap */
-static int ren_br(int force)
+static int ren_br(void)
 {
-	int ad = n_j;
-	if (!n_u || n_na || (n_j == AD_B && force))
-		ad = AD_L;
-	if (n_ce)
-		ad = AD_C;
-	return ren_bradj(cadj, !force && !n_ce && n_u, ad);
+	fmt_fill(cfmt, 0);
+	while (fmt_morelines(cfmt))
+		ren_passline(cfmt);
+	fmt_br(cfmt);
+	return ren_passline(cfmt);
 }
 
 void tr_br(char **args)
 {
 	if (args[0][0] == c_cc)
-		ren_br(1);
+		ren_br();
 }
 
 void tr_sp(char **args)
@@ -391,7 +390,7 @@
 	int traps = 0;
 	int n = args[1] ? eval(args[1], 'v') : n_v;
 	if (args[0][0] == c_cc)
-		traps = ren_br(1);
+		traps = ren_br();
 	if (n && !n_ns && !traps)
 		down(n);
 }
@@ -445,25 +444,13 @@
 		ren_pagelimit(n);
 }
 
-static void push_eject(void)
+static void ren_ejectpage(int br)
 {
-	char buf[32];
 	bp_ejected = bp_count;
-	sprintf(buf, "%c%s %d\n", c_cc, TR_EJECT, bp_ejected);
-	in_pushnl(buf, NULL);
-}
-
-static void push_br(void)
-{
-	char br[8] = {c_cc, 'b', 'r', '\n'};
-	in_pushnl(br, NULL);
-}
-
-static void ren_eject(int id)
-{
-	if (id == bp_ejected && id == bp_count && !cdiv) {
+	if (br)
+		ren_br();
+	while (bp_count == bp_ejected && !cdiv) {
 		if (detect_traps(n_d, n_p)) {
-			push_eject();
 			ren_traps(n_d, n_p, 1);
 		} else {
 			bp_ejected = 0;
@@ -472,20 +459,12 @@
 	}
 }
 
-void tr_eject(char **args)
-{
-	ren_eject(atoi(args[1]));
-}
-
 void tr_bp(char **args)
 {
 	if (!cdiv && (args[1] || !n_ns)) {
-		if (bp_ejected != bp_count)
-			push_eject();
-		if (args[0][0] == c_cc)
-			push_br();
 		if (args[1])
 			bp_next = eval_re(args[1], n_pg, 0);
+		ren_ejectpage(args[0][0] == c_cc);
 	}
 }
 
@@ -518,16 +497,16 @@
 {
 	int in = args[1] ? eval_re(args[1], n_i, 'm') : n_i0;
 	if (args[0][0] == c_cc)
-		ren_br(1);
+		ren_br();
 	n_i0 = n_i;
 	n_i = MAX(0, in);
-	n_ti = 0;
+	n_ti = -1;
 }
 
 void tr_ti(char **args)
 {
 	if (args[0][0] == c_cc)
-		ren_br(1);
+		ren_br();
 	if (args[1])
 		n_ti = eval_re(args[1], n_i, 'm');
 }
@@ -559,7 +538,7 @@
 void tr_nf(char **args)
 {
 	if (args[0][0] == c_cc)
-		ren_br(1);
+		ren_br();
 	n_u = 0;
 }
 
@@ -566,7 +545,7 @@
 void tr_fi(char **args)
 {
 	if (args[0][0] == c_cc)
-		ren_br(1);
+		ren_br();
 	n_u = 1;
 }
 
@@ -573,7 +552,7 @@
 void tr_ce(char **args)
 {
 	if (args[0][0] == c_cc)
-		ren_br(1);
+		ren_br();
 	n_ce = args[1] ? atoi(args[1]) : 1;
 }
 
@@ -928,56 +907,61 @@
 	return 0;
 }
 
-/* read characters from in.c and pass rendered lines to out.c */
-int render(void)
+/* cause nested render_rec() to exit */
+void tr_popren(char **args)
 {
+	ren_level = args[1] ? atoi(args[1]) : 0;
+}
+
+/* read characters from tr.c and pass the rendered lines to out.c */
+static int render_rec(int level)
+{
 	struct wb *wb = &ren_wb;
 	int c;
-	n_nl = -1;
-	wb_init(wb);
-	tr_first();
-	ren_first();			/* transition to the first page */
-	c = ren_next();
-	while (1) {
+	while (ren_level >= level) {
+		while (!tr_nextreq())
+			if (ren_level < level)
+				break;
+		if (ren_level < level)
+			break;
 		if (ren_aborted)
 			return 1;
+		c = ren_next();
 		if (c < 0) {
 			if (bp_final >= 2)
 				break;
-			if (bp_final == 0 && trap_em >= 0) {
-				trap_exec(trap_em);
+			if (bp_final == 0) {
 				bp_final = 1;
+				fmt_fill(cfmt, 0);
+				if (trap_em >= 0)
+					trap_exec(trap_em);
 			} else {
 				bp_final = 2;
-				push_eject();
-				push_br();
+				ren_ejectpage(1);
 			}
-			c = ren_next();
-			continue;
 		}
-		ren_cnl = c == '\n';
-		/* add wb (the current word) to cadj */
+		if (c >= 0)
+			ren_cnl = c == '\n';
+		/* add wb (the current word) to cfmt */
 		if (c == ' ' || c == '\n') {
-			adj_swid(cadj, spacewid(n_f, n_s));
 			if (!wb_part(wb)) {	/* not after a \c */
-				adj_wb(cadj, wb);
-				wb_reset(wb);
-				/* wb contains only commands like \f */
-				if (!ren_nl && wb_empty(wb))
-					adj_nonl(cadj);
+				fmt_word(cfmt, wb);
 				if (c == '\n')
-					adj_nl(cadj);
+					fmt_newline(cfmt);
 				else
-					adj_sp(cadj);
+					fmt_space(cfmt);
+				if (!(n_j & AD_P))
+					fmt_fill(cfmt, 0);
+				wb_reset(wb);
 			}
 		}
 		/* flush the line if necessary */
-		if (c == ' ' || c == '\n') {
-			while ((ren_fillreq && !wb_part(wb) && !n_ce && n_u) ||
-						adj_full(cadj, !n_ce && n_u)) {
-				ren_br(0);
-				ren_fillreq = 0;
-			}
+		if (c == ' ' || c == '\n' || c < 0) {
+			if (ren_fillreq && !wb_part(wb))
+				fmt_fill(cfmt, 1);
+			while (fmt_morelines(cfmt))
+				ren_passline(cfmt);
+			ren_fillreq = 0;
 		}
 		if (c == '\n' || ren_nl)	/* end or start of input line */
 			n_lb = f_hpos();
@@ -985,17 +969,30 @@
 			trap_exec(n_it);
 		if (c == '\n' && !wb_part(wb))
 			n_ce = MAX(0, n_ce - 1);
-		if (c != ' ') {
+		if (c != ' ' && c >= 0) {
 			ren_back(c);
 			ren_char(wb, ren_next, ren_back);
 		}
-		ren_nl = c == '\n';
-		c = ren_next();
+		if (c >= 0)
+			ren_nl = c == '\n';
 	}
+	return 0;
+}
+
+/* render input words */
+int render(void)
+{
+	struct wb *wb = &ren_wb;
+	n_nl = -1;
+	wb_init(wb);
+	while (!tr_nextreq())
+		;
+	ren_first();			/* transition to the first page */
+	render_rec(0);
 	bp_final = 3;
-	if (!adj_empty(cadj, 0))
+	if (fmt_morewords(cfmt))
 		ren_page(bp_next, 1);
-	ren_br(1);
+	ren_br();
 	wb_done(wb);
 	return 0;
 }
--- a/roff.h
+++ b/roff.h
@@ -15,7 +15,7 @@
  * + font_xyz: fonts (font.c)
  * + sbuf_xyz: variable length string buffers (sbuf.c)
  * + wb_xyz: word buffers (wb.c)
- * + adj_xyz: line adjustment buffers (adj.c)
+ * + fmt_xyz: line formatting buffers (fmt.c)
  * + n_xyz: builtin number register xyz
  * + c_xyz: characters for requests like hc and mc
  *
@@ -34,7 +34,8 @@
 #define RNLEN		NMLEN	/* register/macro name */
 #define ILNLEN		1000	/* line limit of input files */
 #define LNLEN		4000	/* line buffer length (ren.c/out.c) */
-#define NWORDS		256	/* number of words in line buffer */
+#define NWORDS		512	/* number of queued words in formatting buffer */
+#define NLINES		32	/* number of queued lines in formatting buffer */
 #define NARGS		16	/* number of macro arguments */
 #define NPREV		16	/* environment stack depth */
 #define NTRAPS		1024	/* number of traps per page */
@@ -99,7 +100,7 @@
 /* enviroments */
 void env_init(void);
 void env_done(void);
-struct adj *env_adj(void);
+struct fmt *env_fmt(void);
 char *env_hc(void);
 char *env_mc(void);
 char *env_tc(void);
@@ -205,7 +206,7 @@
 void cp_blk(int skip);		/* skip or read the next line or block */
 void cp_wid(int enable);	/* control inlining \w requests */
 #define cp_back		in_back	/* cp.c is stateless */
-void tr_first(void);		/* read until the first non-command line */
+int tr_nextreq(void);		/* read the next troff request */
 
 /* variable length string buffer */
 struct sbuf {
@@ -291,24 +292,26 @@
 
 void hyphenate(char *hyphs, char *word, int flg);
 
-/* adjustment */
-#define AD_L		0
-#define AD_B		1
-#define AD_C		3
-#define AD_R		5
+/* adjustment types */
+#define AD_C		0	/* center */
+#define AD_L		1	/* adjust left margin (flag) */
+#define AD_R		2	/* adjust right margin (flag) */
+#define AD_B		3	/* adjust both margin (mask) */
+#define AD_P		4	/* paragraph-at-once adjustment (flag) */
 
-struct adj *adj_alloc(void);
-void adj_free(struct adj *adj);
-int adj_fill(struct adj *adj, int ad_b, int fill, int hyph, struct sbuf *dst,
+/* line formatting */
+struct fmt *fmt_alloc(void);
+void fmt_free(struct fmt *fmt);
+int fmt_wid(struct fmt *fmt);
+void fmt_word(struct fmt *fmt, struct wb *wb);
+void fmt_newline(struct fmt *fmt);
+void fmt_space(struct fmt *fmt);
+void fmt_br(struct fmt *fmt);
+int fmt_fill(struct fmt *fmt, int all);
+int fmt_morelines(struct fmt *fmt);
+int fmt_morewords(struct fmt *fmt);
+int fmt_nextline(struct fmt *fmt, struct sbuf *sbuf, int *w,
 		int *li, int *ll, int *els_neg, int *els_pos);
-int adj_full(struct adj *adj, int fill);
-int adj_empty(struct adj *adj, int fill);
-int adj_wid(struct adj *adj);
-void adj_swid(struct adj *adj, int swid);
-void adj_wb(struct adj *adj, struct wb *wb);
-void adj_nl(struct adj *adj);
-void adj_sp(struct adj *adj);
-void adj_nonl(struct adj *adj);
 
 /* rendering */
 int render(void);				/* the main loop */
@@ -362,7 +365,7 @@
 void tr_ta(char **args);
 void tr_ti(char **args);
 void tr_wh(char **args);
-void tr_eject(char **args);
+void tr_popren(char **args);
 
 void tr_init(void);
 
@@ -389,7 +392,7 @@
 /* internal commands */
 #define TR_DIVBEG	"\07<"	/* diversion begins */
 #define TR_DIVEND	"\07>"	/* diversion ends */
-#define TR_EJECT	"\07P"	/* page eject */
+#define TR_POPREN	"\07P"	/* exit render_rec() */
 
 /* mapping register, macro and environment names to indices */
 #define NREGS		4096	/* maximum number of mapped names */
--- a/tr.c
+++ b/tr.c
@@ -269,32 +269,34 @@
 	n_na = 1;
 }
 
-static void tr_ad(char **args)
+static int adjmode(int c, int def)
 {
-	n_na = 0;
-	if (!args[1])
-		return;
-	switch (args[1][0]) {
-	case '0' + AD_L:
+	switch (c) {
 	case 'l':
-		n_j = AD_L;
-		break;
-	case '0' + AD_R:
+		return AD_L;
 	case 'r':
-		n_j = AD_R;
-		break;
-	case '0' + AD_C:
+		return AD_R;
 	case 'c':
-		n_j = AD_C;
-		break;
-	case '0' + AD_B:
+		return AD_C;
 	case 'b':
 	case 'n':
-		n_j = AD_B;
-		break;
+		return AD_B;
 	}
+	return def;
 }
 
+static void tr_ad(char **args)
+{
+	char *s = args[1];
+	n_na = 0;
+	if (!s)
+		return;
+	if (isdigit(s[0]))
+		n_j = atoi(s) & 15;
+	else
+		n_j = s[0] == 'p' ? AD_P | adjmode(s[1], AD_B) : adjmode(s[0], n_j);
+}
+
 static void tr_tm(char **args)
 {
 	fprintf(stderr, "%s\n", args[1]);
@@ -817,7 +819,7 @@
 } cmds[] = {
 	{TR_DIVBEG, tr_divbeg},
 	{TR_DIVEND, tr_divend},
-	{TR_EJECT, tr_eject},
+	{TR_POPREN, tr_popren},
 	{"ab", tr_ab, mkargs_eol},
 	{"ad", tr_ad},
 	{"af", tr_af},
@@ -906,39 +908,50 @@
 	{"wh", tr_wh},
 };
 
-int tr_next(void)
+/* read the next troff request; return zero if a request was executed. */
+int tr_nextreq(void)
 {
-	int c = cp_next();
-	int nl = c == '\n';
 	char *args[NARGS + 3] = {NULL};
 	char cmd[RNLEN];
 	char buf[LNLEN];
 	struct cmd *req;
-	while (tr_nl && c >= 0 && (c == c_cc || c == c_c2)) {
-		nl = 1;
-		memset(args, 0, sizeof(args));
-		args[0] = cmd;
-		cmd[0] = c;
-		req = NULL;
-		arg_regname(cmd + 1, sizeof(cmd) - 1);
-		req = str_dget(map(cmd + 1));
-		if (req) {
-			if (req->args)
-				req->args(args + 1, buf, sizeof(buf));
-			else
-				mkargs_req(args + 1, buf, sizeof(buf));
-			req->f(args);
-		} else {
-			cp_wid(0);
-			mkargs(args + 1, buf, sizeof(buf));
-			cp_wid(1);
-			if (str_get(map(cmd + 1)))
-				in_push(str_get(map(cmd + 1)), args + 1);
-		}
-		c = cp_next();
-		nl = c == '\n';
+	int c;
+	if (!tr_nl)
+		return 1;
+	c = cp_next();
+	if (c < 0 || (c != c_cc && c != c_c2)) {
+		cp_back(c);
+		return 1;
 	}
-	tr_nl = c < 0 || nl;
+	memset(args, 0, sizeof(args));
+	args[0] = cmd;
+	cmd[0] = c;
+	req = NULL;
+	arg_regname(cmd + 1, sizeof(cmd) - 1);
+	req = str_dget(map(cmd + 1));
+	if (req) {
+		if (req->args)
+			req->args(args + 1, buf, sizeof(buf));
+		else
+			mkargs_req(args + 1, buf, sizeof(buf));
+		req->f(args);
+	} else {
+		cp_wid(0);
+		mkargs(args + 1, buf, sizeof(buf));
+		cp_wid(1);
+		if (str_get(map(cmd + 1)))
+			in_push(str_get(map(cmd + 1)), args + 1);
+	}
+	return 0;
+}
+
+int tr_next(void)
+{
+	int c;
+	while (!tr_nextreq())
+		;
+	c = cp_next();
+	tr_nl = c == '\n' || c < 0;
 	return c;
 }
 
@@ -947,10 +960,4 @@
 	int i;
 	for (i = 0; i < LEN(cmds); i++)
 		str_dset(map(cmds[i].id), &cmds[i]);
-}
-
-void tr_first(void)
-{
-	cp_back(tr_next());
-	tr_nl = 1;
 }