summaryrefslogtreecommitdiffstats
path: root/src/preproc/tbl
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 19:44:05 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 19:44:05 +0000
commitd318611dd6f23fcfedd50e9b9e24620b102ba96a (patch)
tree8b9eef82ca40fdd5a8deeabf07572074c236095d /src/preproc/tbl
parentInitial commit. (diff)
downloadgroff-f22bf21391d2b916c7303c565592ae6e99efbb58.tar.xz
groff-f22bf21391d2b916c7303c565592ae6e99efbb58.zip
Adding upstream version 1.23.0.upstream/1.23.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/preproc/tbl')
-rw-r--r--src/preproc/tbl/main.cpp1692
-rw-r--r--src/preproc/tbl/table.cpp3161
-rw-r--r--src/preproc/tbl/table.h179
-rw-r--r--src/preproc/tbl/tbl.1.man2018
-rw-r--r--src/preproc/tbl/tbl.am54
-rwxr-xr-xsrc/preproc/tbl/tests/boxes-and-vertical-rules.sh174
-rwxr-xr-xsrc/preproc/tbl/tests/check-horizontal-line-length.sh78
-rwxr-xr-xsrc/preproc/tbl/tests/check-line-intersections.sh52
-rwxr-xr-xsrc/preproc/tbl/tests/check-vertical-line-length.sh49
-rwxr-xr-xsrc/preproc/tbl/tests/cooperate-with-nm-request.sh47
-rwxr-xr-xsrc/preproc/tbl/tests/count-continued-input-lines.sh43
-rwxr-xr-xsrc/preproc/tbl/tests/do-not-overdraw-page-top-in-nroff-mode.sh225
-rwxr-xr-xsrc/preproc/tbl/tests/do-not-overlap-bottom-border-in-nroff.sh62
-rwxr-xr-xsrc/preproc/tbl/tests/do-not-segv-on-invalid-vertical-span-entry.sh41
-rwxr-xr-xsrc/preproc/tbl/tests/do-not-segv-when-backslash-R-in-text-block.sh75
-rwxr-xr-xsrc/preproc/tbl/tests/expand-region-option-works.sh173
-rwxr-xr-xsrc/preproc/tbl/tests/format-time-diagnostics-work.sh268
-rwxr-xr-xsrc/preproc/tbl/tests/save-and-restore-hyphenation-parameters.sh58
-rwxr-xr-xsrc/preproc/tbl/tests/save-and-restore-inter-sentence-space.sh79
-rwxr-xr-xsrc/preproc/tbl/tests/save-and-restore-line-numbering.sh85
-rwxr-xr-xsrc/preproc/tbl/tests/save-and-restore-tab-stops.sh84
-rwxr-xr-xsrc/preproc/tbl/tests/warn-on-long-boxed-unkept-table.sh56
-rwxr-xr-xsrc/preproc/tbl/tests/x-column-modifier-works.sh172
23 files changed, 8925 insertions, 0 deletions
diff --git a/src/preproc/tbl/main.cpp b/src/preproc/tbl/main.cpp
new file mode 100644
index 0000000..db105c2
--- /dev/null
+++ b/src/preproc/tbl/main.cpp
@@ -0,0 +1,1692 @@
+/* Copyright (C) 1989-2020 Free Software Foundation, Inc.
+ Written by James Clark (jjc@jclark.com)
+
+This file is part of groff.
+
+groff is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+groff is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "table.h"
+
+#define MAX_POINT_SIZE 99
+#define MAX_VERTICAL_SPACING 72
+
+extern "C" const char *Version_string;
+
+int compatible_flag = 0;
+
+class table_input {
+ FILE *fp;
+ enum { START, MIDDLE,
+ REREAD_T, REREAD_TE, REREAD_E,
+ LEADER_1, LEADER_2, LEADER_3, LEADER_4,
+ END, ERROR } state;
+ string unget_stack;
+public:
+ table_input(FILE *);
+ int get();
+ int ended() { return unget_stack.empty() && state == END; }
+ void unget(char);
+};
+
+table_input::table_input(FILE *p)
+: fp(p), state(START)
+{
+}
+
+void table_input::unget(char c)
+{
+ assert(c != '\0');
+ unget_stack += c;
+ if (c == '\n')
+ current_lineno--;
+}
+
+int table_input::get()
+{
+ int len = unget_stack.length();
+ if (len != 0) {
+ unsigned char c = unget_stack[len - 1];
+ unget_stack.set_length(len - 1);
+ if (c == '\n')
+ current_lineno++;
+ return c;
+ }
+ int c;
+ for (;;) {
+ switch (state) {
+ case START:
+ if ((c = getc(fp)) == '.') {
+ if ((c = getc(fp)) == 'T') {
+ if ((c = getc(fp)) == 'E') {
+ if (compatible_flag) {
+ state = END;
+ return EOF;
+ }
+ else {
+ c = getc(fp);
+ if (c != EOF)
+ ungetc(c, fp);
+ if (c == EOF || c == ' ' || c == '\n') {
+ state = END;
+ return EOF;
+ }
+ state = REREAD_TE;
+ return '.';
+ }
+ }
+ else {
+ if (c != EOF)
+ ungetc(c, fp);
+ state = REREAD_T;
+ return '.';
+ }
+ }
+ else {
+ if (c != EOF)
+ ungetc(c, fp);
+ state = MIDDLE;
+ return '.';
+ }
+ }
+ else if (c == EOF) {
+ state = ERROR;
+ return EOF;
+ }
+ else {
+ if (c == '\n')
+ current_lineno++;
+ else {
+ state = MIDDLE;
+ if (c == '\0') {
+ error("invalid input character code 0");
+ break;
+ }
+ }
+ return c;
+ }
+ break;
+ case MIDDLE:
+ // handle line continuation and uninterpreted leader character
+ if ((c = getc(fp)) == '\\') {
+ c = getc(fp);
+ if (c == '\n') {
+ current_lineno++;
+ c = getc(fp);
+ }
+ else if (c == 'a' && compatible_flag) {
+ state = LEADER_1;
+ return '\\';
+ }
+ else {
+ if (c != EOF)
+ ungetc(c, fp);
+ c = '\\';
+ }
+ }
+ if (c == EOF) {
+ state = ERROR;
+ return EOF;
+ }
+ else {
+ if (c == '\n') {
+ state = START;
+ current_lineno++;
+ }
+ else if (c == '\0') {
+ error("invalid input character code 0");
+ break;
+ }
+ return c;
+ }
+ case REREAD_T:
+ state = MIDDLE;
+ return 'T';
+ case REREAD_TE:
+ state = REREAD_E;
+ return 'T';
+ case REREAD_E:
+ state = MIDDLE;
+ return 'E';
+ case LEADER_1:
+ state = LEADER_2;
+ return '*';
+ case LEADER_2:
+ state = LEADER_3;
+ return '(';
+ case LEADER_3:
+ state = LEADER_4;
+ return PREFIX_CHAR;
+ case LEADER_4:
+ state = MIDDLE;
+ return LEADER_CHAR;
+ case END:
+ case ERROR:
+ return EOF;
+ }
+ }
+}
+
+void process_input_file(FILE *);
+void process_table(table_input &in);
+
+void process_input_file(FILE *fp)
+{
+ enum { START, MIDDLE, HAD_DOT, HAD_T, HAD_TS, HAD_l, HAD_lf } state;
+ state = START;
+ int c;
+ while ((c = getc(fp)) != EOF)
+ switch (state) {
+ case START:
+ if (c == '.')
+ state = HAD_DOT;
+ else {
+ if (c == '\n')
+ current_lineno++;
+ else
+ state = MIDDLE;
+ putchar(c);
+ }
+ break;
+ case MIDDLE:
+ if (c == '\n') {
+ current_lineno++;
+ state = START;
+ }
+ putchar(c);
+ break;
+ case HAD_DOT:
+ if (c == 'T')
+ state = HAD_T;
+ else if (c == 'l')
+ state = HAD_l;
+ else {
+ putchar('.');
+ putchar(c);
+ if (c == '\n') {
+ current_lineno++;
+ state = START;
+ }
+ else
+ state = MIDDLE;
+ }
+ break;
+ case HAD_T:
+ if (c == 'S')
+ state = HAD_TS;
+ else {
+ putchar('.');
+ putchar('T');
+ putchar(c);
+ if (c == '\n') {
+ current_lineno++;
+ state = START;
+ }
+ else
+ state = MIDDLE;
+ }
+ break;
+ case HAD_TS:
+ if (c == ' ' || c == '\n' || compatible_flag) {
+ putchar('.');
+ putchar('T');
+ putchar('S');
+ while (c != '\n') {
+ if (c == EOF) {
+ error("end of file at beginning of table");
+ return;
+ }
+ putchar(c);
+ c = getc(fp);
+ }
+ putchar('\n');
+ current_lineno++;
+ {
+ table_input input(fp);
+ process_table(input);
+ set_troff_location(current_filename, current_lineno);
+ if (input.ended()) {
+ fputs(".TE", stdout);
+ while ((c = getc(fp)) != '\n') {
+ if (c == EOF) {
+ putchar('\n');
+ return;
+ }
+ putchar(c);
+ }
+ putchar('\n');
+ current_lineno++;
+ }
+ }
+ state = START;
+ }
+ else {
+ fputs(".TS", stdout);
+ putchar(c);
+ state = MIDDLE;
+ }
+ break;
+ case HAD_l:
+ if (c == 'f')
+ state = HAD_lf;
+ else {
+ putchar('.');
+ putchar('l');
+ putchar(c);
+ if (c == '\n') {
+ current_lineno++;
+ state = START;
+ }
+ else
+ state = MIDDLE;
+ }
+ break;
+ case HAD_lf:
+ if (c == ' ' || c == '\n' || compatible_flag) {
+ string line;
+ while (c != EOF) {
+ line += c;
+ if (c == '\n') {
+ current_lineno++;
+ break;
+ }
+ c = getc(fp);
+ }
+ line += '\0';
+ interpret_lf_args(line.contents());
+ printf(".lf%s", line.contents());
+ state = START;
+ }
+ else {
+ fputs(".lf", stdout);
+ putchar(c);
+ state = MIDDLE;
+ }
+ break;
+ default:
+ assert(0 == "invalid `state` in switch");
+ }
+ switch(state) {
+ case START:
+ break;
+ case MIDDLE:
+ putchar('\n');
+ break;
+ case HAD_DOT:
+ fputs(".\n", stdout);
+ break;
+ case HAD_l:
+ fputs(".l\n", stdout);
+ break;
+ case HAD_T:
+ fputs(".T\n", stdout);
+ break;
+ case HAD_lf:
+ fputs(".lf\n", stdout);
+ break;
+ case HAD_TS:
+ fputs(".TS\n", stdout);
+ break;
+ }
+ if (fp != stdin)
+ fclose(fp);
+}
+
+struct options {
+ unsigned flags;
+ int linesize;
+ char delim[2];
+ char tab_char;
+ char decimal_point_char;
+
+ options();
+};
+
+options::options()
+: flags(0), linesize(0), tab_char('\t'), decimal_point_char('.')
+{
+ delim[0] = delim[1] = '\0';
+}
+
+// Return non-zero if p and q are the same ignoring case.
+
+int strieq(const char *p, const char *q)
+{
+ for (; cmlower(*p) == cmlower(*q); p++, q++)
+ if (*p == '\0')
+ return 1;
+ return 0;
+}
+
+// Handle region options. Return a null pointer if we should give up on
+// this table.
+options *process_options(table_input &in)
+{
+ options *opt = new options;
+ string line;
+ int level = 0;
+ for (;;) {
+ int c = in.get();
+ if (c == EOF) {
+ int i = line.length();
+ while (--i >= 0)
+ in.unget(line[i]);
+ return opt;
+ }
+ if (c == '\n') {
+ in.unget(c);
+ int i = line.length();
+ while (--i >= 0)
+ in.unget(line[i]);
+ return opt;
+ }
+ else if (c == '(')
+ level++;
+ else if (c == ')')
+ level--;
+ else if (c == ';' && 0 == level) {
+ line += '\0';
+ break;
+ }
+ line += c;
+ }
+ if (line.empty())
+ return opt;
+ char *p = &line[0];
+ for (;;) {
+ while (!csalpha(*p) && *p != '\0')
+ p++;
+ if (*p == '\0')
+ break;
+ char *q = p;
+ while (csalpha(*q))
+ q++;
+ char *arg = 0;
+ if (*q != '(' && *q != '\0')
+ *q++ = '\0';
+ while (csspace(*q))
+ q++;
+ if (*q == '(') {
+ *q++ = '\0';
+ arg = q;
+ while (*q != ')' && *q != '\0')
+ q++;
+ if (*q == '\0')
+ error("'%1' region option argument missing closing parenthesis",
+ arg);
+ else
+ *q++ = '\0';
+ }
+ if (*p == '\0') {
+ if (arg)
+ error("'%1' region option argument cannot be empty", arg);
+ }
+ else if (strieq(p, "tab")) {
+ if (!arg)
+ error("'tab' region option requires argument in parentheses");
+ else {
+ if (arg[0] == '\0' || arg[1] != '\0')
+ error("'tab' region option argument must be a single"
+ " character");
+ else
+ opt->tab_char = arg[0];
+ }
+ }
+ else if (strieq(p, "linesize")) {
+ if (!arg)
+ error("'linesize' region option requires argument in"
+ " parentheses");
+ else {
+ if (sscanf(arg, "%d", &opt->linesize) != 1)
+ error("invalid argument to 'linesize' region option: '%1'",
+ arg);
+ else if (opt->linesize <= 0) {
+ error("'linesize' region option argument must be positive");
+ opt->linesize = 0;
+ }
+ }
+ }
+ else if (strieq(p, "delim")) {
+ if (!arg)
+ error("'delim' region option requires argument in parentheses");
+ else if (arg[0] == '\0' || arg[1] == '\0' || arg[2] != '\0')
+ error("argument to 'delim' option must be two characters");
+ else {
+ opt->delim[0] = arg[0];
+ opt->delim[1] = arg[1];
+ }
+ }
+ else if (strieq(p, "center") || strieq(p, "centre")) {
+ if (arg)
+ error("'center' region option does not take an argument");
+ opt->flags |= table::CENTER;
+ }
+ else if (strieq(p, "expand")) {
+ if (arg)
+ error("'expand' region option does not take an argument");
+ opt->flags |= table::EXPAND;
+ opt->flags |= table::GAP_EXPAND;
+ }
+ else if (strieq(p, "box") || strieq(p, "frame")) {
+ if (arg)
+ error("'box' region option does not take an argument");
+ opt->flags |= table::BOX;
+ }
+ else if (strieq(p, "doublebox") || strieq(p, "doubleframe")) {
+ if (arg)
+ error("'doublebox' region option does not take an argument");
+ opt->flags |= table::DOUBLEBOX;
+ }
+ else if (strieq(p, "allbox")) {
+ if (arg)
+ error("'allbox' region option does not take an argument");
+ opt->flags |= table::ALLBOX;
+ }
+ else if (strieq(p, "nokeep")) {
+ if (arg)
+ error("'nokeep' region option does not take an argument");
+ opt->flags |= table::NOKEEP;
+ }
+ else if (strieq(p, "nospaces")) {
+ if (arg)
+ error("'nospaces' region option does not take an argument");
+ opt->flags |= table::NOSPACES;
+ }
+ else if (strieq(p, "nowarn")) {
+ if (arg)
+ error("'nowarn' region option does not take an argument");
+ opt->flags |= table::NOWARN;
+ }
+ else if (strieq(p, "decimalpoint")) {
+ if (!arg)
+ error("'decimalpoint' region option requires argument in"
+ " parentheses");
+ else {
+ if (arg[0] == '\0' || arg[1] != '\0')
+ error("'decimalpoint' region option argument must be a single"
+ " character");
+ else
+ opt->decimal_point_char = arg[0];
+ }
+ }
+ else if (strieq(p, "experimental")) {
+ opt->flags |= table::EXPERIMENTAL;
+ }
+ else {
+ error("unrecognized region option '%1'", p);
+ // delete opt;
+ // return 0;
+ }
+ p = q;
+ }
+ return opt;
+}
+
+entry_modifier::entry_modifier()
+: vertical_alignment(CENTER), zero_width(0), stagger(0)
+{
+ vertical_spacing.inc = vertical_spacing.val = 0;
+ point_size.inc = point_size.val = 0;
+}
+
+entry_modifier::~entry_modifier()
+{
+}
+
+entry_format::entry_format() : type(FORMAT_LEFT)
+{
+}
+
+entry_format::entry_format(format_type t) : type(t)
+{
+}
+
+void entry_format::debug_print() const
+{
+ switch (type) {
+ case FORMAT_LEFT:
+ putc('l', stderr);
+ break;
+ case FORMAT_CENTER:
+ putc('c', stderr);
+ break;
+ case FORMAT_RIGHT:
+ putc('r', stderr);
+ break;
+ case FORMAT_NUMERIC:
+ putc('n', stderr);
+ break;
+ case FORMAT_ALPHABETIC:
+ putc('a', stderr);
+ break;
+ case FORMAT_SPAN:
+ putc('s', stderr);
+ break;
+ case FORMAT_VSPAN:
+ putc('^', stderr);
+ break;
+ case FORMAT_HLINE:
+ putc('_', stderr);
+ break;
+ case FORMAT_DOUBLE_HLINE:
+ putc('=', stderr);
+ break;
+ default:
+ assert(0 == "invalid column classifier in switch");
+ break;
+ }
+ if (point_size.val != 0) {
+ putc('p', stderr);
+ if (point_size.inc > 0)
+ putc('+', stderr);
+ else if (point_size.inc < 0)
+ putc('-', stderr);
+ fprintf(stderr, "%d ", point_size.val);
+ }
+ if (vertical_spacing.val != 0) {
+ putc('v', stderr);
+ if (vertical_spacing.inc > 0)
+ putc('+', stderr);
+ else if (vertical_spacing.inc < 0)
+ putc('-', stderr);
+ fprintf(stderr, "%d ", vertical_spacing.val);
+ }
+ if (!font.empty()) {
+ putc('f', stderr);
+ put_string(font, stderr);
+ putc(' ', stderr);
+ }
+ if (!macro.empty()) {
+ putc('m', stderr);
+ put_string(macro, stderr);
+ putc(' ', stderr);
+ }
+ switch (vertical_alignment) {
+ case entry_modifier::CENTER:
+ break;
+ case entry_modifier::TOP:
+ putc('t', stderr);
+ break;
+ case entry_modifier::BOTTOM:
+ putc('d', stderr);
+ break;
+ }
+ if (zero_width)
+ putc('z', stderr);
+ if (stagger)
+ putc('u', stderr);
+}
+
+struct format {
+ int nrows;
+ int ncolumns;
+ int *separation;
+ string *width;
+ char *equal;
+ char *expand;
+ entry_format **entry;
+ char **vline;
+
+ format(int nr, int nc);
+ ~format();
+ void add_rows(int n);
+};
+
+format::format(int nr, int nc) : nrows(nr), ncolumns(nc)
+{
+ int i;
+ separation = ncolumns > 1 ? new int[ncolumns - 1] : 0;
+ for (i = 0; i < ncolumns-1; i++)
+ separation[i] = -1;
+ width = new string[ncolumns];
+ equal = new char[ncolumns];
+ expand = new char[ncolumns];
+ for (i = 0; i < ncolumns; i++) {
+ equal[i] = 0;
+ expand[i] = 0;
+ }
+ entry = new entry_format *[nrows];
+ for (i = 0; i < nrows; i++)
+ entry[i] = new entry_format[ncolumns];
+ vline = new char*[nrows];
+ for (i = 0; i < nrows; i++) {
+ vline[i] = new char[ncolumns+1];
+ for (int j = 0; j < ncolumns+1; j++)
+ vline[i][j] = 0;
+ }
+}
+
+void format::add_rows(int n)
+{
+ int i;
+ char **old_vline = vline;
+ vline = new char*[nrows + n];
+ for (i = 0; i < nrows; i++)
+ vline[i] = old_vline[i];
+ delete[] old_vline;
+ for (i = 0; i < n; i++) {
+ vline[nrows + i] = new char[ncolumns + 1];
+ for (int j = 0; j < ncolumns + 1; j++)
+ vline[nrows + i][j] = 0;
+ }
+ entry_format **old_entry = entry;
+ entry = new entry_format *[nrows + n];
+ for (i = 0; i < nrows; i++)
+ entry[i] = old_entry[i];
+ delete[] old_entry;
+ for (i = 0; i < n; i++)
+ entry[nrows + i] = new entry_format[ncolumns];
+ nrows += n;
+}
+
+format::~format()
+{
+ delete[] separation;
+ delete[] width;
+ delete[] equal;
+ delete[] expand;
+ for (int i = 0; i < nrows; i++) {
+ delete[] vline[i];
+ delete[] entry[i];
+ }
+ delete[] vline;
+ delete[] entry;
+}
+
+struct input_entry_format : public entry_format {
+ input_entry_format *next;
+ string width;
+ int separation;
+ int vline;
+ int vline_count;
+ bool is_last_column;
+ bool is_equal_width;
+ int expand;
+ input_entry_format(format_type, input_entry_format * = 0);
+ ~input_entry_format();
+ void debug_print();
+};
+
+input_entry_format::input_entry_format(format_type t, input_entry_format *p)
+: entry_format(t), next(p)
+{
+ separation = -1;
+ is_last_column = false;
+ vline = 0;
+ vline_count = 0;
+ is_equal_width = false;
+ expand = 0;
+}
+
+input_entry_format::~input_entry_format()
+{
+}
+
+void free_input_entry_format_list(input_entry_format *list)
+{
+ while (list) {
+ input_entry_format *tem = list;
+ list = list->next;
+ delete tem;
+ }
+}
+
+void input_entry_format::debug_print()
+{
+ int i;
+ for (i = 0; i < vline_count; i++)
+ putc('|', stderr);
+ entry_format::debug_print();
+ if (!width.empty()) {
+ putc('w', stderr);
+ putc('(', stderr);
+ put_string(width, stderr);
+ putc(')', stderr);
+ }
+ if (is_equal_width)
+ putc('e', stderr);
+ if (expand)
+ putc('x', stderr);
+ if (separation >= 0)
+ fprintf(stderr, "%d", separation);
+ for (i = 0; i < vline; i++)
+ putc('|', stderr);
+ if (is_last_column)
+ putc(',', stderr);
+}
+
+// Interpret a table format specification, like "CC,LR.". Return null
+// pointer if we should give up on this table. If this is a
+// continuation format line, `current_format` will be the current format
+// line.
+format *process_format(table_input &in, options *opt,
+ format *current_format = 0)
+{
+ input_entry_format *list = 0 /* nullptr */;
+ bool have_expand = false;
+ bool is_first_row = true;
+ int c = in.get();
+ for (;;) {
+ int vline_count = 0;
+ bool got_format = false;
+ bool got_period = false;
+ format_type t = FORMAT_LEFT;
+ for (;;) {
+ if (c == EOF) {
+ error("end of input while processing table format"
+ " specification");
+ free_input_entry_format_list(list);
+ list = 0 /* nullptr */;
+ return 0 /* nullptr */;
+ }
+ switch (c) {
+ case 'n':
+ case 'N':
+ t = FORMAT_NUMERIC;
+ got_format = true;
+ break;
+ case 'a':
+ case 'A':
+ got_format = true;
+ t = FORMAT_ALPHABETIC;
+ break;
+ case 'c':
+ case 'C':
+ got_format = true;
+ t = FORMAT_CENTER;
+ break;
+ case 'l':
+ case 'L':
+ got_format = true;
+ t = FORMAT_LEFT;
+ break;
+ case 'r':
+ case 'R':
+ got_format = true;
+ t = FORMAT_RIGHT;
+ break;
+ case 's':
+ case 'S':
+ got_format = true;
+ t = FORMAT_SPAN;
+ break;
+ case '^':
+ got_format = true;
+ t = FORMAT_VSPAN;
+ break;
+ case '_':
+ case '-': // tbl also accepts this
+ got_format = true;
+ t = FORMAT_HLINE;
+ if (is_first_row)
+ opt->flags |= table::HAS_TOP_HLINE;
+ break;
+ case '=':
+ got_format = true;
+ t = FORMAT_DOUBLE_HLINE;
+ break;
+ case '.':
+ got_period = true;
+ break;
+ case '|':
+ // leading vertical line in row
+ opt->flags |= table::HAS_TOP_VLINE;
+ vline_count++;
+ // list->vline_count is updated later
+ break;
+ case ' ':
+ case '\t':
+ case '\n':
+ break;
+ default:
+ if (c == opt->tab_char)
+ break;
+ error("invalid column classifier '%1'", char(c));
+ free_input_entry_format_list(list);
+ list = 0 /* nullptr */;
+ return 0 /* nullptr */;
+ }
+ if (got_period)
+ break;
+ c = in.get();
+ if (got_format)
+ break;
+ }
+ if (got_period)
+ break;
+ list = new input_entry_format(t, list);
+ if (vline_count > 2) {
+ vline_count = 2;
+ error("more than 2 vertical lines at beginning of row description");
+ }
+ list->vline_count = vline_count;
+ // Now handle modifiers.
+ vline_count = 0;
+ bool is_valid_modifier_sequence = true;
+ do {
+ switch (c) {
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ {
+ int w = 0;
+ do {
+ w = w*10 + (c - '0');
+ c = in.get();
+ } while (c != EOF && csdigit(c));
+ list->separation = w;
+ }
+ break;
+ case 'B':
+ case 'b':
+ c = in.get();
+ list->font = "B";
+ break;
+ case 'd':
+ case 'D':
+ c = in.get();
+ list->vertical_alignment = entry_modifier::BOTTOM;
+ break;
+ case 'e':
+ case 'E':
+ c = in.get();
+ list->is_equal_width = true;
+ // 'e' and 'x' are mutually exclusive
+ list->expand = 0;
+ break;
+ case 'f':
+ case 'F':
+ do {
+ c = in.get();
+ } while (c == ' ' || c == '\t');
+ if (c == EOF) {
+ error("'f' column modifier missing font name or mounting"
+ " position");
+ break;
+ }
+ if (c == '(') {
+ for (;;) {
+ c = in.get();
+ if (c == EOF || c == ' ' || c == '\t') {
+ error("'f' column modifier missing closing parenthesis");
+ break;
+ }
+ if (c == ')') {
+ c = in.get();
+ break;
+ }
+ list->font += char(c);
+ }
+ }
+ else {
+ list->font = c;
+ char cc = c;
+ c = in.get();
+ if (!csdigit(cc)
+ && c != EOF && c != ' ' && c != '\t' && c != '.' && c != '\n') {
+ list->font += char(c);
+ c = in.get();
+ }
+ }
+ break;
+ case 'I':
+ case 'i':
+ c = in.get();
+ list->font = "I";
+ break;
+ case 'm':
+ case 'M':
+ do {
+ c = in.get();
+ } while (c == ' ' || c == '\t');
+ if (c == EOF) {
+ error("'m' column modifier missing macro name");
+ break;
+ }
+ if (c == '(') {
+ for (;;) {
+ c = in.get();
+ if (c == EOF || c == ' ' || c == '\t') {
+ error("'m' column modifier missing closing parenthesis");
+ break;
+ }
+ if (c == ')') {
+ c = in.get();
+ break;
+ }
+ list->macro += char(c);
+ }
+ }
+ else {
+ list->macro = c;
+ char cc = c;
+ c = in.get();
+ if (!csdigit(cc)
+ && c != EOF && c != ' ' && c != '\t' && c != '.' && c != '\n') {
+ list->macro += char(c);
+ c = in.get();
+ }
+ }
+ break;
+ case 'p':
+ case 'P':
+ {
+ inc_number &ps = list->point_size;
+ ps.val = 0;
+ ps.inc = 0;
+ c = in.get();
+ if (c == '+' || c == '-') {
+ ps.inc = (c == '+' ? 1 : -1);
+ c = in.get();
+ }
+ if (c == EOF || !csdigit(c)) {
+ warning("'p' column modifier must be followed by"
+ " (optionally signed) integer; ignoring");
+ ps.inc = 0;
+ }
+ else {
+ do {
+ ps.val *= 10;
+ ps.val += c - '0';
+ c = in.get();
+ } while (c != EOF && csdigit(c));
+ }
+ if (ps.val > MAX_POINT_SIZE || ps.val < -MAX_POINT_SIZE) {
+ warning("'p' column modifier argument magnitude of %1"
+ " points out of range (> %2); ignoring", ps.val,
+ MAX_POINT_SIZE);
+ ps.val = 0;
+ ps.inc = 0;
+ }
+ break;
+ }
+ case 't':
+ case 'T':
+ c = in.get();
+ list->vertical_alignment = entry_modifier::TOP;
+ break;
+ case 'u':
+ case 'U':
+ c = in.get();
+ list->stagger = 1;
+ break;
+ case 'v':
+ case 'V':
+ {
+ inc_number &vs = list->vertical_spacing;
+ vs.val = 0;
+ vs.inc = 0;
+ c = in.get();
+ if (c == '+' || c == '-') {
+ vs.inc = (c == '+' ? 1 : -1);
+ c = in.get();
+ }
+ if (c == EOF || !csdigit(c)) {
+ warning("'v' column modifier must be followed by"
+ " (optionally signed) integer; ignoring");
+ vs.inc = 0;
+ }
+ else {
+ do {
+ vs.val *= 10;
+ vs.val += c - '0';
+ c = in.get();
+ } while (c != EOF && csdigit(c));
+ }
+ if (vs.val > MAX_VERTICAL_SPACING
+ || vs.val < -MAX_VERTICAL_SPACING) {
+ warning("'v' column modifier argument magnitude of %1"
+ " points out of range (> %2); ignoring", vs.val,
+ MAX_VERTICAL_SPACING);
+ vs.val = 0;
+ vs.inc = 0;
+ }
+ break;
+ }
+ case 'w':
+ case 'W':
+ c = in.get();
+ while (c == ' ' || c == '\t')
+ c = in.get();
+ if (c == '(') {
+ list->width = "";
+ c = in.get();
+ while (c != ')') {
+ if (c == EOF || c == '\n') {
+ error("'w' column modifier missing closing parenthesis");
+ free_input_entry_format_list(list);
+ list = 0 /* nullptr */;
+ return 0 /* nullptr */;
+ }
+ list->width += c;
+ c = in.get();
+ }
+ c = in.get();
+ }
+ else {
+ if (c == '+' || c == '-') {
+ list->width = char(c);
+ c = in.get();
+ }
+ else
+ list->width = "";
+ if (c == EOF || !csdigit(c))
+ error("invalid argument to 'w' modifier");
+ else {
+ do {
+ list->width += char(c);
+ c = in.get();
+ } while (c != EOF && csdigit(c));
+ }
+ }
+ // 'w' and 'x' are mutually exclusive
+ list->expand = 0;
+ break;
+ case 'x':
+ case 'X':
+ c = in.get();
+ list->expand = 1;
+ // 'x' and 'e' are mutually exclusive
+ list->is_equal_width = false;
+ // 'x' and 'w' are mutually exclusive
+ list->width = "";
+ break;
+ case 'z':
+ case 'Z':
+ c = in.get();
+ list->zero_width = 1;
+ break;
+ case '|':
+ if (is_first_row)
+ opt->flags |= table::HAS_TOP_VLINE;
+ c = in.get();
+ vline_count++;
+ break;
+ case ' ':
+ case '\t':
+ c = in.get();
+ break;
+ default:
+ if (c == opt->tab_char)
+ c = in.get();
+ else
+ is_valid_modifier_sequence = false;
+ break;
+ }
+ } while (is_valid_modifier_sequence);
+ if (vline_count > 2) {
+ vline_count = 2;
+ error("more than 2 vertical lines after column descriptor");
+ }
+ list->vline += vline_count;
+ if (c == '\n' || c == ',') {
+ vline_count = 0;
+ is_first_row = false;
+ c = in.get();
+ list->is_last_column = true;
+ }
+ }
+ if (c == '.') {
+ do {
+ c = in.get();
+ } while (c == ' ' || c == '\t');
+ if (c != '\n') {
+ error("'.' is not the last character of the table format");
+ free_input_entry_format_list(list);
+ list = 0 /* nullptr */;
+ return 0 /* nullptr */;
+ }
+ }
+ if (!list) {
+ error("table format specification is empty");
+ free_input_entry_format_list(list);
+ list = 0 /* nullptr */;
+ return 0 /* nullptr */;
+ }
+ list->is_last_column = true;
+ // now reverse the list so that the first row is at the beginning
+ input_entry_format *rev = 0;
+ while (list != 0) {
+ input_entry_format *tem = list->next;
+ list->next = rev;
+ rev = list;
+ list = tem;
+ }
+ list = rev;
+ input_entry_format *tem;
+
+#if 0
+ for (tem = list; tem; tem = tem->next)
+ tem->debug_print();
+ putc('\n', stderr);
+#endif
+ // compute number of columns and rows
+ int ncolumns = 0;
+ int nrows = 0;
+ int col = 0;
+ for (tem = list; tem; tem = tem->next) {
+ if (tem->is_last_column) {
+ if (col >= ncolumns)
+ ncolumns = col + 1;
+ col = 0;
+ nrows++;
+ }
+ else
+ col++;
+ }
+ int row;
+ format *f;
+ if (current_format) {
+ if (ncolumns > current_format->ncolumns) {
+ error("cannot increase the number of columns in a continued format");
+ free_input_entry_format_list(list);
+ list = 0 /* nullptr */;
+ return 0 /* nullptr */;
+ }
+ f = current_format;
+ row = f->nrows;
+ f->add_rows(nrows);
+ }
+ else {
+ f = new format(nrows, ncolumns);
+ row = 0;
+ }
+ col = 0;
+ for (tem = list; tem; tem = tem->next) {
+ f->entry[row][col] = *tem;
+ if (col < ncolumns - 1) {
+ // use the greatest separation
+ if (tem->separation > f->separation[col]) {
+ if (current_format)
+ error("cannot change column separation in continued format");
+ else
+ f->separation[col] = tem->separation;
+ }
+ }
+ else if (tem->separation >= 0)
+ error("column separation specified for last column");
+ if (tem->is_equal_width && !f->equal[col]) {
+ if (current_format)
+ error("cannot change which columns are equal in continued format");
+ else
+ f->equal[col] = 1;
+ }
+ if (tem->expand && !f->expand[col]) {
+ if (current_format)
+ error("cannot change which columns are expanded in continued format");
+ else {
+ f->expand[col] = 1;
+ have_expand = true;
+ }
+ }
+ if (!tem->width.empty()) {
+ // use the last width
+ if (!f->width[col].empty() && f->width[col] != tem->width)
+ error("multiple widths for column %1", col + 1);
+ f->width[col] = tem->width;
+ }
+ if (tem->vline_count)
+ f->vline[row][col] = tem->vline_count;
+ f->vline[row][col + 1] = tem->vline;
+ if (tem->is_last_column) {
+ row++;
+ col = 0;
+ }
+ else
+ col++;
+ }
+ free_input_entry_format_list(list);
+ list = 0 /* nullptr */;
+ for (col = 0; col < ncolumns; col++) {
+ entry_format *e = f->entry[f->nrows - 1] + col;
+ if (e->type != FORMAT_HLINE
+ && e->type != FORMAT_DOUBLE_HLINE
+ && e->type != FORMAT_SPAN)
+ break;
+ }
+ if (col >= ncolumns) {
+ error("last row of format is all lines");
+ delete f;
+ return 0 /* nullptr */;
+ }
+ if (have_expand && (opt->flags & table::EXPAND)) {
+ error("'x' column modifier encountered; ignoring region option"
+ " 'expand'");
+ opt->flags &= ~table::EXPAND;
+ }
+ return f;
+}
+
+table *process_data(table_input &in, format *f, options *opt)
+{
+ char tab_char = opt->tab_char;
+ int ncolumns = f->ncolumns;
+ int current_row = 0;
+ int format_index = 0;
+ bool give_up = false;
+ enum { DATA_INPUT_LINE, TROFF_INPUT_LINE, SINGLE_HLINE, DOUBLE_HLINE } type;
+ table *tbl = new table(ncolumns, opt->flags, opt->linesize,
+ opt->decimal_point_char);
+ if (opt->delim[0] != '\0')
+ tbl->set_delim(opt->delim[0], opt->delim[1]);
+ for (;;) {
+ // first determine what type of line this is
+ int c = in.get();
+ if (c == EOF)
+ break;
+ if (c == '.') {
+ int d = in.get();
+ if (d != EOF && csdigit(d)) {
+ in.unget(d);
+ type = DATA_INPUT_LINE;
+ }
+ else {
+ in.unget(d);
+ type = TROFF_INPUT_LINE;
+ }
+ }
+ else if (c == '_' || c == '=') {
+ int d = in.get();
+ if (d == '\n') {
+ if (c == '_')
+ type = SINGLE_HLINE;
+ else
+ type = DOUBLE_HLINE;
+ if (0 == current_row)
+ tbl->flags |= table::HAS_TOP_HLINE;
+ }
+ else {
+ in.unget(d);
+ type = DATA_INPUT_LINE;
+ }
+ }
+ else {
+ type = DATA_INPUT_LINE;
+ }
+ switch (type) {
+ case DATA_INPUT_LINE:
+ {
+ string input_entry;
+ if (format_index >= f->nrows)
+ format_index = f->nrows - 1;
+ // A format row that is all lines doesn't use up a data line.
+ while (format_index < f->nrows - 1) {
+ int cnt;
+ for (cnt = 0; cnt < ncolumns; cnt++) {
+ entry_format *e = f->entry[format_index] + cnt;
+ if (e->type != FORMAT_HLINE
+ && e->type != FORMAT_DOUBLE_HLINE
+ // Unfortunately tbl treats a span as needing data.
+ // && e->type != FORMAT_SPAN
+ )
+ break;
+ }
+ if (cnt < ncolumns)
+ break;
+ for (cnt = 0; cnt < ncolumns; cnt++)
+ tbl->add_entry(current_row, cnt, input_entry,
+ f->entry[format_index] + cnt, current_filename,
+ current_lineno);
+ tbl->add_vlines(current_row, f->vline[format_index]);
+ format_index++;
+ current_row++;
+ }
+ entry_format *line_format = f->entry[format_index];
+ int col = 0;
+ bool seen_row_comment = false;
+ for (;;) {
+ if (c == tab_char || c == '\n') {
+ int ln = current_lineno;
+ if (c == '\n')
+ --ln;
+ if ((opt->flags & table::NOSPACES))
+ input_entry.remove_spaces();
+ while (col < ncolumns
+ && line_format[col].type == FORMAT_SPAN) {
+ tbl->add_entry(current_row, col, "", &line_format[col],
+ current_filename, ln);
+ col++;
+ }
+ if (c == '\n' && input_entry.length() == 2
+ && input_entry[0] == 'T' && input_entry[1] == '{') {
+ input_entry = "";
+ ln++;
+ enum {
+ START, MIDDLE, GOT_T, GOT_RIGHT_BRACE, GOT_DOT,
+ GOT_l, GOT_lf, END
+ } state = START;
+ while (state != END) {
+ c = in.get();
+ if (c == EOF)
+ break;
+ switch (state) {
+ case START:
+ if (c == 'T')
+ state = GOT_T;
+ else if (c == '.')
+ state = GOT_DOT;
+ else {
+ input_entry += c;
+ if (c != '\n')
+ state = MIDDLE;
+ }
+ break;
+ case GOT_T:
+ if (c == '}')
+ state = GOT_RIGHT_BRACE;
+ else {
+ input_entry += 'T';
+ input_entry += c;
+ state = c == '\n' ? START : MIDDLE;
+ }
+ break;
+ case GOT_DOT:
+ if (c == 'l')
+ state = GOT_l;
+ else {
+ input_entry += '.';
+ input_entry += c;
+ state = c == '\n' ? START : MIDDLE;
+ }
+ break;
+ case GOT_l:
+ if (c == 'f')
+ state = GOT_lf;
+ else {
+ input_entry += ".l";
+ input_entry += c;
+ state = c == '\n' ? START : MIDDLE;
+ }
+ break;
+ case GOT_lf:
+ if (c == ' ' || c == '\n' || compatible_flag) {
+ string args;
+ input_entry += ".lf";
+ while (c != EOF) {
+ args += c;
+ if (c == '\n')
+ break;
+ c = in.get();
+ }
+ args += '\0';
+ interpret_lf_args(args.contents());
+ // remove the '\0'
+ args.set_length(args.length() - 1);
+ input_entry += args;
+ state = START;
+ }
+ else {
+ input_entry += ".lf";
+ input_entry += c;
+ state = MIDDLE;
+ }
+ break;
+ case GOT_RIGHT_BRACE:
+ if ((opt->flags & table::NOSPACES)) {
+ while (c == ' ')
+ c = in.get();
+ if (c == EOF)
+ break;
+ }
+ if (c == '\n' || c == tab_char)
+ state = END;
+ else {
+ input_entry += 'T';
+ input_entry += '}';
+ input_entry += c;
+ state = MIDDLE;
+ }
+ break;
+ case MIDDLE:
+ if (c == '\n')
+ state = START;
+ input_entry += c;
+ break;
+ case END:
+ default:
+ assert(0 == "invalid `state` in switch");
+ }
+ }
+ if (c == EOF) {
+ error("end of data in middle of text block");
+ give_up = true;
+ break;
+ }
+ }
+ if (col >= ncolumns) {
+ if (!input_entry.empty()) {
+ if (input_entry.length() >= 2
+ && input_entry[0] == '\\'
+ && input_entry[1] == '"')
+ seen_row_comment = true;
+ else if (!seen_row_comment) {
+ if (c == '\n')
+ in.unget(c);
+ input_entry += '\0';
+ error("excess table entry '%1' discarded",
+ input_entry.contents());
+ if (c == '\n')
+ (void)in.get();
+ }
+ }
+ }
+ else
+ tbl->add_entry(current_row, col, input_entry,
+ &line_format[col], current_filename, ln);
+ col++;
+ if (c == '\n')
+ break;
+ input_entry = "";
+ }
+ else
+ input_entry += c;
+ c = in.get();
+ if (c == EOF)
+ break;
+ }
+ if (give_up)
+ break;
+ input_entry = "";
+ for (; col < ncolumns; col++)
+ tbl->add_entry(current_row, col, input_entry, &line_format[col],
+ current_filename, current_lineno - 1);
+ tbl->add_vlines(current_row, f->vline[format_index]);
+ current_row++;
+ format_index++;
+ }
+ break;
+ case TROFF_INPUT_LINE:
+ {
+ string line;
+ int ln = current_lineno;
+ for (;;) {
+ line += c;
+ if (c == '\n')
+ break;
+ c = in.get();
+ if (c == EOF) {
+ break;
+ }
+ }
+ tbl->add_text_line(current_row, line, current_filename, ln);
+ if (line.length() >= 4
+ && line[0] == '.' && line[1] == 'T' && line[2] == '&') {
+ format *newf = process_format(in, opt, f);
+ if (newf == 0)
+ give_up = true;
+ else
+ f = newf;
+ }
+ if (line.length() >= 3
+ && line[0] == '.' && line[1] == 'l' && line[2] == 'f') {
+ line += '\0';
+ interpret_lf_args(line.contents() + 3);
+ }
+ }
+ break;
+ case SINGLE_HLINE:
+ tbl->add_single_hline(current_row);
+ break;
+ case DOUBLE_HLINE:
+ tbl->add_double_hline(current_row);
+ break;
+ default:
+ assert(0 == "invalid `type` in switch");
+ }
+ if (give_up)
+ break;
+ }
+ if (!give_up && current_row == 0) {
+ error("no real data");
+ give_up = true;
+ }
+ if (give_up) {
+ delete tbl;
+ return 0;
+ }
+ // Do this here rather than at the beginning in case continued formats
+ // change it.
+ int i;
+ for (i = 0; i < ncolumns - 1; i++)
+ if (f->separation[i] >= 0)
+ tbl->set_column_separation(i, f->separation[i]);
+ for (i = 0; i < ncolumns; i++)
+ if (!f->width[i].empty())
+ tbl->set_minimum_width(i, f->width[i]);
+ for (i = 0; i < ncolumns; i++)
+ if (f->equal[i])
+ tbl->set_equal_column(i);
+ for (i = 0; i < ncolumns; i++)
+ if (f->expand[i])
+ tbl->set_expand_column(i);
+ return tbl;
+}
+
+void process_table(table_input &in)
+{
+ options *opt = 0 /* nullptr */;
+ format *fmt = 0 /* nullptr */;
+ table *tbl = 0 /* nullptr */;
+ if ((opt = process_options(in)) != 0 /* nullptr */
+ && (fmt = process_format(in, opt)) != 0 /* nullptr */
+ && (tbl = process_data(in, fmt, opt)) != 0 /* nullptr */) {
+ tbl->print();
+ delete tbl;
+ }
+ else {
+ error("giving up on this table region");
+ while (in.get() != EOF)
+ ;
+ }
+ delete opt;
+ delete fmt;
+ if (!in.ended())
+ error("premature end of file");
+}
+
+static void usage(FILE *stream)
+{
+ fprintf(stream,
+"usage: %s [-C] [file ...]\n"
+"usage: %s {-v | --version}\n"
+"usage: %s --help\n",
+ program_name, program_name, program_name);
+}
+
+int main(int argc, char **argv)
+{
+ program_name = argv[0];
+ static char stderr_buf[BUFSIZ];
+ setbuf(stderr, stderr_buf);
+ int opt;
+ static const struct option long_options[] = {
+ { "help", no_argument, 0, CHAR_MAX + 1 },
+ { "version", no_argument, 0, 'v' },
+ { NULL, 0, 0, 0 }
+ };
+ while ((opt = getopt_long(argc, argv, "vC", long_options, NULL))
+ != EOF)
+ switch (opt) {
+ case 'C':
+ compatible_flag = 1;
+ break;
+ case 'v':
+ {
+ printf("GNU tbl (groff) version %s\n", Version_string);
+ exit(EXIT_SUCCESS);
+ break;
+ }
+ case CHAR_MAX + 1: // --help
+ usage(stdout);
+ exit(EXIT_SUCCESS);
+ break;
+ case '?':
+ usage(stderr);
+ exit(EXIT_FAILURE);
+ break;
+ default:
+ assert(0 == "unhandled getopt_long return value");
+ }
+ printf(".if !\\n(.g .ab GNU tbl requires groff extensions; aborting\n"
+ ".do if !dTS .ds TS\n"
+ ".do if !dT& .ds T&\n"
+ ".do if !dTE .ds TE\n");
+ if (argc > optind) {
+ for (int i = optind; i < argc; i++)
+ if (argv[i][0] == '-' && argv[i][1] == '\0') {
+ current_filename = "-";
+ current_lineno = 1;
+ printf(".lf 1 -\n");
+ process_input_file(stdin);
+ }
+ else {
+ errno = 0;
+ FILE *fp = fopen(argv[i], "r");
+ if (fp == 0) {
+ current_filename = 0 /* nullptr */;
+ fatal("can't open '%1': %2", argv[i], strerror(errno));
+ }
+ else {
+ current_lineno = 1;
+ string fn(argv[i]);
+ fn += '\0';
+ normalize_for_lf(fn);
+ current_filename = fn.contents();
+ printf(".lf 1 %s\n", current_filename);
+ process_input_file(fp);
+ }
+ }
+ }
+ else {
+ current_filename = "-";
+ current_lineno = 1;
+ printf(".lf 1 -\n");
+ process_input_file(stdin);
+ }
+ if (ferror(stdout) || fflush(stdout) < 0)
+ fatal("output error");
+ return 0;
+}
+
+// Local Variables:
+// fill-column: 72
+// mode: C++
+// End:
+// vim: set cindent noexpandtab shiftwidth=2 textwidth=72:
diff --git a/src/preproc/tbl/table.cpp b/src/preproc/tbl/table.cpp
new file mode 100644
index 0000000..c391c90
--- /dev/null
+++ b/src/preproc/tbl/table.cpp
@@ -0,0 +1,3161 @@
+/* Copyright (C) 1989-2023 Free Software Foundation, Inc.
+ Written by James Clark (jjc@jclark.com)
+
+This file is part of groff.
+
+groff is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+groff is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "table.h"
+
+#define BAR_HEIGHT ".25m"
+#define DOUBLE_LINE_SEP "2p"
+#define HALF_DOUBLE_LINE_SEP "1p"
+#define LINE_SEP "2p"
+#define BODY_DEPTH ".25m"
+
+const int DEFAULT_COLUMN_SEPARATION = 3;
+
+#define DELIMITER_CHAR "\\[tbl]"
+#define SEPARATION_FACTOR_REG PREFIX "sep"
+#define LEFTOVER_FACTOR_REG PREFIX "leftover"
+#define BOTTOM_REG PREFIX "bot"
+#define RESET_MACRO_NAME PREFIX "init"
+#define LINESIZE_REG PREFIX "lps"
+#define TOP_REG PREFIX "top"
+#define CURRENT_ROW_REG PREFIX "crow"
+#define LAST_PASSED_ROW_REG PREFIX "passed"
+#define TRANSPARENT_STRING_NAME PREFIX "trans"
+#define QUOTE_STRING_NAME PREFIX "quote"
+#define SECTION_DIVERSION_NAME PREFIX "section"
+#define SECTION_DIVERSION_FLAG_REG PREFIX "sflag"
+#define SAVED_VERTICAL_POS_REG PREFIX "vert"
+#define NEED_BOTTOM_RULE_REG PREFIX "brule"
+#define USE_KEEPS_REG PREFIX "usekeeps"
+#define KEEP_MACRO_NAME PREFIX "keep"
+#define RELEASE_MACRO_NAME PREFIX "release"
+#define SAVED_FONT_REG PREFIX "fnt"
+#define SAVED_SIZE_REG PREFIX "sz"
+#define SAVED_FILL_REG PREFIX "fll"
+#define SAVED_INDENT_REG PREFIX "ind"
+#define SAVED_CENTER_REG PREFIX "cent"
+#define SAVED_TABS_NAME PREFIX "tabs"
+#define SAVED_INTER_WORD_SPACE_SIZE PREFIX "ss"
+#define SAVED_INTER_SENTENCE_SPACE_SIZE PREFIX "sss"
+#define TABLE_DIVERSION_NAME PREFIX "table"
+#define TABLE_DIVERSION_FLAG_REG PREFIX "tflag"
+#define TABLE_KEEP_MACRO_NAME PREFIX "tkeep"
+#define TABLE_RELEASE_MACRO_NAME PREFIX "trelease"
+#define NEEDED_REG PREFIX "needed"
+#define REPEATED_MARK_MACRO PREFIX "rmk"
+#define REPEATED_VPT_MACRO PREFIX "rvpt"
+#define SUPPRESS_BOTTOM_REG PREFIX "supbot"
+#define SAVED_DN_REG PREFIX "dn"
+#define SAVED_HYPHENATION_MODE_REG PREFIX "hyphmode"
+#define SAVED_HYPHENATION_LANG_NAME PREFIX "hyphlang"
+#define SAVED_HYPHENATION_MAX_LINES_REG PREFIX "hyphmaxlines"
+#define SAVED_HYPHENATION_MARGIN_REG PREFIX "hyphmargin"
+#define SAVED_HYPHENATION_SPACE_REG PREFIX "hyphspace"
+#define SAVED_NUMBERING_LINENO PREFIX "linenumber"
+#define SAVED_NUMBERING_SUPPRESSION_COUNT PREFIX "linenumbersuppresscnt"
+#define STARTING_PAGE_REG PREFIX "starting-page"
+#define IS_BOXED_REG PREFIX "is-boxed"
+#define PREVIOUS_PAGE_REG PREFIX "previous-page"
+
+// this must be one character
+#define COMPATIBLE_REG PREFIX "c"
+
+// for use with `ig` requests embedded inside macro definitions
+#define NOP_NAME PREFIX "nop"
+
+#define AVAILABLE_WIDTH_REG PREFIX "available-width"
+#define EXPAND_REG PREFIX "expansion-amount"
+
+#define LEADER_REG PREFIX LEADER
+
+#define BLOCK_WIDTH_PREFIX PREFIX "tbw"
+#define BLOCK_DIVERSION_PREFIX PREFIX "tbd"
+#define BLOCK_HEIGHT_PREFIX PREFIX "tbh"
+#define SPAN_WIDTH_PREFIX PREFIX "w"
+#define SPAN_LEFT_NUMERIC_WIDTH_PREFIX PREFIX "lnw"
+#define SPAN_RIGHT_NUMERIC_WIDTH_PREFIX PREFIX "rnw"
+#define SPAN_ALPHABETIC_WIDTH_PREFIX PREFIX "aw"
+#define COLUMN_SEPARATION_PREFIX PREFIX "cs"
+#define ROW_START_PREFIX PREFIX "rs"
+#define COLUMN_START_PREFIX PREFIX "cl"
+#define COLUMN_END_PREFIX PREFIX "ce"
+#define COLUMN_DIVIDE_PREFIX PREFIX "cd"
+#define ROW_TOP_PREFIX PREFIX "rt"
+
+string block_width_reg(int, int);
+string block_diversion_name(int, int);
+string block_height_reg(int, int);
+string span_width_reg(int, int);
+string span_left_numeric_width_reg(int, int);
+string span_right_numeric_width_reg(int, int);
+string span_alphabetic_width_reg(int, int);
+string column_separation_reg(int);
+string row_start_reg(int);
+string column_start_reg(int);
+string column_end_reg(int);
+string column_divide_reg(int);
+string row_top_reg(int);
+
+void set_inline_modifier(const entry_modifier *);
+void restore_inline_modifier(const entry_modifier *);
+void set_modifier(const entry_modifier *);
+int find_decimal_point(const char *, char, const char *);
+
+string an_empty_string;
+int location_force_filename = 0;
+
+void printfs(const char *,
+ const string &arg1 = an_empty_string,
+ const string &arg2 = an_empty_string,
+ const string &arg3 = an_empty_string,
+ const string &arg4 = an_empty_string,
+ const string &arg5 = an_empty_string);
+
+void prints(const string &);
+
+inline void prints(char c)
+{
+ putchar(c);
+}
+
+inline void prints(const char *s)
+{
+ fputs(s, stdout);
+}
+
+void prints(const string &s)
+{
+ if (!s.empty())
+ fwrite(s.contents(), 1, s.length(), stdout);
+}
+
+struct horizontal_span {
+ horizontal_span *next;
+ int start_col;
+ int end_col;
+ horizontal_span(int, int, horizontal_span *);
+};
+
+class single_line_entry;
+class double_line_entry;
+class simple_entry;
+
+class table_entry {
+friend class table;
+ table_entry *next;
+ int input_lineno;
+ const char *input_filename;
+protected:
+ int start_row;
+ int end_row;
+ int start_col;
+ int end_col;
+ const table *parent;
+ const entry_modifier *mod;
+public:
+ void set_location();
+ table_entry(const table *, const entry_modifier *);
+ virtual ~table_entry();
+ virtual int divert(int, const string *, int *, int);
+ virtual void do_width();
+ virtual void do_depth();
+ virtual void print() = 0;
+ virtual void position_vertically() = 0;
+ virtual single_line_entry *to_single_line_entry();
+ virtual double_line_entry *to_double_line_entry();
+ virtual simple_entry *to_simple_entry();
+ virtual int line_type();
+ virtual void note_double_vrule_on_right(int);
+ virtual void note_double_vrule_on_left(int);
+};
+
+class simple_entry : public table_entry {
+public:
+ simple_entry(const table *, const entry_modifier *);
+ void print();
+ void position_vertically();
+ simple_entry *to_simple_entry();
+ virtual void add_tab();
+ virtual void simple_print(int);
+};
+
+class empty_entry : public simple_entry {
+public:
+ empty_entry(const table *, const entry_modifier *);
+ int line_type();
+};
+
+class text_entry : public simple_entry {
+protected:
+ char *contents;
+ void print_contents();
+public:
+ text_entry(const table *, const entry_modifier *, char *);
+ ~text_entry();
+};
+
+void text_entry::print_contents()
+{
+ set_inline_modifier(mod);
+ prints(contents);
+ restore_inline_modifier(mod);
+}
+
+class repeated_char_entry : public text_entry {
+public:
+ repeated_char_entry(const table *, const entry_modifier *, char *);
+ void simple_print(int);
+};
+
+class simple_text_entry : public text_entry {
+public:
+ simple_text_entry(const table *, const entry_modifier *, char *);
+ void do_width();
+};
+
+class left_text_entry : public simple_text_entry {
+public:
+ left_text_entry(const table *, const entry_modifier *, char *);
+ void simple_print(int);
+ void add_tab();
+};
+
+class right_text_entry : public simple_text_entry {
+public:
+ right_text_entry(const table *, const entry_modifier *, char *);
+ void simple_print(int);
+ void add_tab();
+};
+
+class center_text_entry : public simple_text_entry {
+public:
+ center_text_entry(const table *, const entry_modifier *, char *);
+ void simple_print(int);
+ void add_tab();
+};
+
+class numeric_text_entry : public text_entry {
+ int dot_pos;
+public:
+ numeric_text_entry(const table *, const entry_modifier *, char *, int);
+ void do_width();
+ void simple_print(int);
+};
+
+class alphabetic_text_entry : public text_entry {
+public:
+ alphabetic_text_entry(const table *, const entry_modifier *, char *);
+ void do_width();
+ void simple_print(int);
+ void add_tab();
+};
+
+class line_entry : public simple_entry {
+protected:
+ char double_vrule_on_right;
+ char double_vrule_on_left;
+public:
+ line_entry(const table *, const entry_modifier *);
+ void note_double_vrule_on_right(int);
+ void note_double_vrule_on_left(int);
+ void simple_print(int) = 0;
+};
+
+class single_line_entry : public line_entry {
+public:
+ single_line_entry(const table *, const entry_modifier *);
+ void simple_print(int);
+ single_line_entry *to_single_line_entry();
+ int line_type();
+};
+
+class double_line_entry : public line_entry {
+public:
+ double_line_entry(const table *, const entry_modifier *);
+ void simple_print(int);
+ double_line_entry *to_double_line_entry();
+ int line_type();
+};
+
+class short_line_entry : public simple_entry {
+public:
+ short_line_entry(const table *, const entry_modifier *);
+ void simple_print(int);
+ int line_type();
+};
+
+class short_double_line_entry : public simple_entry {
+public:
+ short_double_line_entry(const table *, const entry_modifier *);
+ void simple_print(int);
+ int line_type();
+};
+
+class block_entry : public table_entry {
+ char *contents;
+protected:
+ void do_divert(int, int, const string *, int *, int);
+public:
+ block_entry(const table *, const entry_modifier *, char *);
+ ~block_entry();
+ int divert(int, const string *, int *, int);
+ void do_depth();
+ void position_vertically();
+ void print() = 0;
+};
+
+class left_block_entry : public block_entry {
+public:
+ left_block_entry(const table *, const entry_modifier *, char *);
+ void print();
+};
+
+class right_block_entry : public block_entry {
+public:
+ right_block_entry(const table *, const entry_modifier *, char *);
+ void print();
+};
+
+class center_block_entry : public block_entry {
+public:
+ center_block_entry(const table *, const entry_modifier *, char *);
+ void print();
+};
+
+class alphabetic_block_entry : public block_entry {
+public:
+ alphabetic_block_entry(const table *, const entry_modifier *, char *);
+ void print();
+ int divert(int, const string *, int *, int);
+};
+
+table_entry::table_entry(const table *p, const entry_modifier *m)
+: next(0), input_lineno(-1), input_filename(0),
+ start_row(-1), end_row(-1), start_col(-1), end_col(-1), parent(p), mod(m)
+{
+}
+
+table_entry::~table_entry()
+{
+}
+
+int table_entry::divert(int, const string *, int *, int)
+{
+ return 0;
+}
+
+void table_entry::do_width()
+{
+}
+
+single_line_entry *table_entry::to_single_line_entry()
+{
+ return 0;
+}
+
+double_line_entry *table_entry::to_double_line_entry()
+{
+ return 0;
+}
+
+simple_entry *table_entry::to_simple_entry()
+{
+ return 0;
+}
+
+void table_entry::do_depth()
+{
+}
+
+void table_entry::set_location()
+{
+ set_troff_location(input_filename, input_lineno);
+}
+
+int table_entry::line_type()
+{
+ return -1;
+}
+
+void table_entry::note_double_vrule_on_right(int)
+{
+}
+
+void table_entry::note_double_vrule_on_left(int)
+{
+}
+
+simple_entry::simple_entry(const table *p, const entry_modifier *m)
+: table_entry(p, m)
+{
+}
+
+void simple_entry::add_tab()
+{
+ // do nothing
+}
+
+void simple_entry::simple_print(int)
+{
+ // do nothing
+}
+
+void simple_entry::position_vertically()
+{
+ if (start_row != end_row)
+ switch (mod->vertical_alignment) {
+ case entry_modifier::TOP:
+ printfs(".sp |\\n[%1]u\n", row_start_reg(start_row));
+ break;
+ case entry_modifier::CENTER:
+ // Perform the motion in two stages so that the center is rounded
+ // vertically upwards even if net vertical motion is upwards.
+ printfs(".sp |\\n[%1]u\n", row_start_reg(start_row));
+ printfs(".sp \\n[" BOTTOM_REG "]u-\\n[%1]u-1v/2u\n",
+ row_start_reg(start_row));
+ break;
+ case entry_modifier::BOTTOM:
+ printfs(".sp |\\n[%1]u+\\n[" BOTTOM_REG "]u-\\n[%1]u-1v\n",
+ row_start_reg(start_row));
+ break;
+ default:
+ assert(0 == "simple entry vertical position modifier not TOP,"
+ " CENTER, or BOTTOM");
+ }
+}
+
+void simple_entry::print()
+{
+ prints(".ta");
+ add_tab();
+ prints('\n');
+ set_location();
+ prints("\\&");
+ simple_print(0);
+ prints('\n');
+}
+
+simple_entry *simple_entry::to_simple_entry()
+{
+ return this;
+}
+
+empty_entry::empty_entry(const table *p, const entry_modifier *m)
+: simple_entry(p, m)
+{
+}
+
+int empty_entry::line_type()
+{
+ return 0;
+}
+
+text_entry::text_entry(const table *p, const entry_modifier *m, char *s)
+: simple_entry(p, m), contents(s)
+{
+}
+
+text_entry::~text_entry()
+{
+ free(contents);
+}
+
+repeated_char_entry::repeated_char_entry(const table *p,
+ const entry_modifier *m, char *s)
+: text_entry(p, m, s)
+{
+}
+
+void repeated_char_entry::simple_print(int)
+{
+ printfs("\\h'|\\n[%1]u'", column_start_reg(start_col));
+ set_inline_modifier(mod);
+ printfs("\\l" DELIMITER_CHAR "\\n[%1]u\\&",
+ span_width_reg(start_col, end_col));
+ prints(contents);
+ prints(DELIMITER_CHAR);
+ restore_inline_modifier(mod);
+}
+
+simple_text_entry::simple_text_entry(const table *p,
+ const entry_modifier *m, char *s)
+: text_entry(p, m, s)
+{
+}
+
+void simple_text_entry::do_width()
+{
+ set_location();
+ printfs(".nr %1 \\n[%1]>?\\w" DELIMITER_CHAR,
+ span_width_reg(start_col, end_col));
+ print_contents();
+ prints(DELIMITER_CHAR "\n");
+}
+
+left_text_entry::left_text_entry(const table *p,
+ const entry_modifier *m, char *s)
+: simple_text_entry(p, m, s)
+{
+}
+
+void left_text_entry::simple_print(int)
+{
+ printfs("\\h'|\\n[%1]u'", column_start_reg(start_col));
+ print_contents();
+}
+
+// The only point of this is to make '\a' "work" as in Unix tbl. Grrr.
+
+void left_text_entry::add_tab()
+{
+ printfs(" \\n[%1]u", column_end_reg(end_col));
+}
+
+right_text_entry::right_text_entry(const table *p,
+ const entry_modifier *m, char *s)
+: simple_text_entry(p, m, s)
+{
+}
+
+void right_text_entry::simple_print(int)
+{
+ printfs("\\h'|\\n[%1]u'", column_start_reg(start_col));
+ prints("\002\003");
+ print_contents();
+ prints("\002");
+}
+
+void right_text_entry::add_tab()
+{
+ printfs(" \\n[%1]u", column_end_reg(end_col));
+}
+
+center_text_entry::center_text_entry(const table *p,
+ const entry_modifier *m, char *s)
+: simple_text_entry(p, m, s)
+{
+}
+
+void center_text_entry::simple_print(int)
+{
+ printfs("\\h'|\\n[%1]u'", column_start_reg(start_col));
+ prints("\002\003");
+ print_contents();
+ prints("\003\002");
+}
+
+void center_text_entry::add_tab()
+{
+ printfs(" \\n[%1]u", column_end_reg(end_col));
+}
+
+numeric_text_entry::numeric_text_entry(const table *p,
+ const entry_modifier *m,
+ char *s, int pos)
+: text_entry(p, m, s), dot_pos(pos)
+{
+}
+
+void numeric_text_entry::do_width()
+{
+ if (dot_pos != 0) {
+ set_location();
+ printfs(".nr %1 0\\w" DELIMITER_CHAR,
+ block_width_reg(start_row, start_col));
+ set_inline_modifier(mod);
+ for (int i = 0; i < dot_pos; i++)
+ prints(contents[i]);
+ restore_inline_modifier(mod);
+ prints(DELIMITER_CHAR "\n");
+ printfs(".nr %1 \\n[%1]>?\\n[%2]\n",
+ span_left_numeric_width_reg(start_col, end_col),
+ block_width_reg(start_row, start_col));
+ }
+ else
+ printfs(".nr %1 0\n", block_width_reg(start_row, start_col));
+ if (contents[dot_pos] != '\0') {
+ set_location();
+ printfs(".nr %1 \\n[%1]>?\\w" DELIMITER_CHAR,
+ span_right_numeric_width_reg(start_col, end_col));
+ set_inline_modifier(mod);
+ prints(contents + dot_pos);
+ restore_inline_modifier(mod);
+ prints(DELIMITER_CHAR "\n");
+ }
+}
+
+void numeric_text_entry::simple_print(int)
+{
+ printfs("\\h'|(\\n[%1]u-\\n[%2]u-\\n[%3]u/2u+\\n[%2]u+\\n[%4]u-\\n[%5]u)'",
+ span_width_reg(start_col, end_col),
+ span_left_numeric_width_reg(start_col, end_col),
+ span_right_numeric_width_reg(start_col, end_col),
+ column_start_reg(start_col),
+ block_width_reg(start_row, start_col));
+ print_contents();
+}
+
+alphabetic_text_entry::alphabetic_text_entry(const table *p,
+ const entry_modifier *m,
+ char *s)
+: text_entry(p, m, s)
+{
+}
+
+void alphabetic_text_entry::do_width()
+{
+ set_location();
+ printfs(".nr %1 \\n[%1]>?\\w" DELIMITER_CHAR,
+ span_alphabetic_width_reg(start_col, end_col));
+ print_contents();
+ prints(DELIMITER_CHAR "\n");
+}
+
+void alphabetic_text_entry::simple_print(int)
+{
+ printfs("\\h'|\\n[%1]u'", column_start_reg(start_col));
+ printfs("\\h'\\n[%1]u-\\n[%2]u/2u'",
+ span_width_reg(start_col, end_col),
+ span_alphabetic_width_reg(start_col, end_col));
+ print_contents();
+}
+
+// The only point of this is to make '\a' "work" as in Unix tbl. Grrr.
+
+void alphabetic_text_entry::add_tab()
+{
+ printfs(" \\n[%1]u", column_end_reg(end_col));
+}
+
+block_entry::block_entry(const table *p, const entry_modifier *m, char *s)
+: table_entry(p, m), contents(s)
+{
+}
+
+block_entry::~block_entry()
+{
+ delete[] contents;
+}
+
+void block_entry::position_vertically()
+{
+ if (start_row != end_row)
+ switch(mod->vertical_alignment) {
+ case entry_modifier::TOP:
+ printfs(".sp |\\n[%1]u\n", row_start_reg(start_row));
+ break;
+ case entry_modifier::CENTER:
+ // Perform the motion in two stages so that the center is rounded
+ // vertically upwards even if net vertical motion is upwards.
+ printfs(".sp |\\n[%1]u\n", row_start_reg(start_row));
+ printfs(".sp \\n[" BOTTOM_REG "]u-\\n[%1]u-\\n[%2]u/2u\n",
+ row_start_reg(start_row),
+ block_height_reg(start_row, start_col));
+ break;
+ case entry_modifier::BOTTOM:
+ printfs(".sp |\\n[%1]u+\\n[" BOTTOM_REG "]u-\\n[%1]u-\\n[%2]u\n",
+ row_start_reg(start_row),
+ block_height_reg(start_row, start_col));
+ break;
+ default:
+ assert(0 == "block entry vertical position modifier not TOP,"
+ " CENTER, or BOTTOM");
+ }
+ if (mod->stagger)
+ prints(".sp -.5v\n");
+}
+
+int block_entry::divert(int ncols, const string *mw, int *sep, int do_expand)
+{
+ do_divert(0, ncols, mw, sep, do_expand);
+ return 1;
+}
+
+void block_entry::do_divert(int alphabetic, int ncols, const string *mw,
+ int *sep, int do_expand)
+{
+ int i;
+ for (i = start_col; i <= end_col; i++)
+ if (parent->expand[i])
+ break;
+ if (i > end_col) {
+ if (do_expand)
+ return;
+ }
+ else {
+ if (!do_expand)
+ return;
+ }
+ printfs(".di %1\n", block_diversion_name(start_row, start_col));
+ prints(".if \\n[" SAVED_FILL_REG "] .fi\n"
+ ".in 0\n");
+ prints(".ll ");
+ for (i = start_col; i <= end_col; i++)
+ if (mw[i].empty() && !parent->expand[i])
+ break;
+ if (i > end_col) {
+ // Every column spanned by this entry has a minimum width.
+ for (int j = start_col; j <= end_col; j++) {
+ if (j > start_col) {
+ if (sep)
+ printfs("+%1n", as_string(sep[j - 1]));
+ prints('+');
+ }
+ if (parent->expand[j])
+ prints("\\n[" EXPAND_REG "]u");
+ else
+ printfs("(n;%1)", mw[j]);
+ }
+ printfs(">?\\n[%1]u", span_width_reg(start_col, end_col));
+ }
+ else
+ // Assign each column with a block entry 1/(n+1) of the line
+ // width, where n is the column count.
+ printfs("(u;\\n[%1]>?(\\n[.l]*%2/%3))",
+ span_width_reg(start_col, end_col),
+ as_string(end_col - start_col + 1),
+ as_string(ncols + 1));
+ if (alphabetic)
+ prints("-2n");
+ prints("\n");
+ prints(".ss \\n[" SAVED_INTER_WORD_SPACE_SIZE "]"
+ " \\n[" SAVED_INTER_SENTENCE_SPACE_SIZE "]\n");
+ prints(".cp \\n(" COMPATIBLE_REG "\n");
+ set_modifier(mod);
+ set_location();
+ prints(contents);
+ prints(".br\n.di\n.cp 0\n");
+ if (!mod->zero_width) {
+ if (alphabetic) {
+ printfs(".nr %1 \\n[%1]>?(\\n[dl]+2n)\n",
+ span_width_reg(start_col, end_col));
+ printfs(".nr %1 \\n[%1]>?\\n[dl]\n",
+ span_alphabetic_width_reg(start_col, end_col));
+ }
+ else
+ printfs(".nr %1 \\n[%1]>?\\n[dl]\n",
+ span_width_reg(start_col, end_col));
+ }
+ printfs(".nr %1 \\n[dn]\n", block_height_reg(start_row, start_col));
+ printfs(".nr %1 \\n[dl]\n", block_width_reg(start_row, start_col));
+ prints("." RESET_MACRO_NAME "\n"
+ ".in \\n[" SAVED_INDENT_REG "]u\n"
+ ".nf\n");
+ // the block might have contained .lf commands
+ location_force_filename = 1;
+}
+
+void block_entry::do_depth()
+{
+ printfs(".nr " BOTTOM_REG " \\n[" BOTTOM_REG "]>?(\\n[%1]+\\n[%2])\n",
+ row_start_reg(start_row),
+ block_height_reg(start_row, start_col));
+}
+
+left_block_entry::left_block_entry(const table *p,
+ const entry_modifier *m, char *s)
+: block_entry(p, m, s)
+{
+}
+
+void left_block_entry::print()
+{
+ printfs(".in +\\n[%1]u\n", column_start_reg(start_col));
+ printfs(".%1\n", block_diversion_name(start_row, start_col));
+ prints(".in\n");
+}
+
+right_block_entry::right_block_entry(const table *p,
+ const entry_modifier *m, char *s)
+: block_entry(p, m, s)
+{
+}
+
+void right_block_entry::print()
+{
+ printfs(".in +\\n[%1]u+\\n[%2]u-\\n[%3]u\n",
+ column_start_reg(start_col),
+ span_width_reg(start_col, end_col),
+ block_width_reg(start_row, start_col));
+ printfs(".%1\n", block_diversion_name(start_row, start_col));
+ prints(".in\n");
+}
+
+center_block_entry::center_block_entry(const table *p,
+ const entry_modifier *m, char *s)
+: block_entry(p, m, s)
+{
+}
+
+void center_block_entry::print()
+{
+ printfs(".in +\\n[%1]u+(\\n[%2]u-\\n[%3]u/2u)\n",
+ column_start_reg(start_col),
+ span_width_reg(start_col, end_col),
+ block_width_reg(start_row, start_col));
+ printfs(".%1\n", block_diversion_name(start_row, start_col));
+ prints(".in\n");
+}
+
+alphabetic_block_entry::alphabetic_block_entry(const table *p,
+ const entry_modifier *m,
+ char *s)
+: block_entry(p, m, s)
+{
+}
+
+int alphabetic_block_entry::divert(int ncols, const string *mw, int *sep,
+ int do_expand)
+{
+ do_divert(1, ncols, mw, sep, do_expand);
+ return 1;
+}
+
+void alphabetic_block_entry::print()
+{
+ printfs(".in +\\n[%1]u+(\\n[%2]u-\\n[%3]u/2u)\n",
+ column_start_reg(start_col),
+ span_width_reg(start_col, end_col),
+ span_alphabetic_width_reg(start_col, end_col));
+ printfs(".%1\n", block_diversion_name(start_row, start_col));
+ prints(".in\n");
+}
+
+line_entry::line_entry(const table *p, const entry_modifier *m)
+: simple_entry(p, m), double_vrule_on_right(0), double_vrule_on_left(0)
+{
+}
+
+void line_entry::note_double_vrule_on_right(int is_corner)
+{
+ double_vrule_on_right = is_corner ? 1 : 2;
+}
+
+void line_entry::note_double_vrule_on_left(int is_corner)
+{
+ double_vrule_on_left = is_corner ? 1 : 2;
+}
+
+single_line_entry::single_line_entry(const table *p, const entry_modifier *m)
+: line_entry(p, m)
+{
+}
+
+int single_line_entry::line_type()
+{
+ return 1;
+}
+
+void single_line_entry::simple_print(int dont_move)
+{
+ printfs("\\h'|\\n[%1]u",
+ column_divide_reg(start_col));
+ if (double_vrule_on_left) {
+ prints(double_vrule_on_left == 1 ? "-" : "+");
+ prints(HALF_DOUBLE_LINE_SEP);
+ }
+ prints("'");
+ if (!dont_move)
+ prints("\\v'-" BAR_HEIGHT "'");
+ printfs("\\s[\\n[" LINESIZE_REG "]]" "\\D'l |\\n[%1]u",
+ column_divide_reg(end_col+1));
+ if (double_vrule_on_right) {
+ prints(double_vrule_on_left == 1 ? "+" : "-");
+ prints(HALF_DOUBLE_LINE_SEP);
+ }
+ prints("0'\\s0");
+ if (!dont_move)
+ prints("\\v'" BAR_HEIGHT "'");
+}
+
+single_line_entry *single_line_entry::to_single_line_entry()
+{
+ return this;
+}
+
+double_line_entry::double_line_entry(const table *p,
+ const entry_modifier *m)
+: line_entry(p, m)
+{
+}
+
+int double_line_entry::line_type()
+{
+ return 2;
+}
+
+void double_line_entry::simple_print(int dont_move)
+{
+ if (!dont_move)
+ prints("\\v'-" BAR_HEIGHT "'");
+ printfs("\\h'|\\n[%1]u",
+ column_divide_reg(start_col));
+ if (double_vrule_on_left) {
+ prints(double_vrule_on_left == 1 ? "-" : "+");
+ prints(HALF_DOUBLE_LINE_SEP);
+ }
+ prints("'");
+ printfs("\\v'-" HALF_DOUBLE_LINE_SEP "'"
+ "\\s[\\n[" LINESIZE_REG "]]"
+ "\\D'l |\\n[%1]u",
+ column_divide_reg(end_col+1));
+ if (double_vrule_on_right)
+ prints("-" HALF_DOUBLE_LINE_SEP);
+ prints(" 0'");
+ printfs("\\v'" DOUBLE_LINE_SEP "'"
+ "\\D'l |\\n[%1]u",
+ column_divide_reg(start_col));
+ if (double_vrule_on_right) {
+ prints(double_vrule_on_left == 1 ? "+" : "-");
+ prints(HALF_DOUBLE_LINE_SEP);
+ }
+ prints(" 0'");
+ prints("\\s0"
+ "\\v'-" HALF_DOUBLE_LINE_SEP "'");
+ if (!dont_move)
+ prints("\\v'" BAR_HEIGHT "'");
+}
+
+double_line_entry *double_line_entry::to_double_line_entry()
+{
+ return this;
+}
+
+short_line_entry::short_line_entry(const table *p, const entry_modifier *m)
+: simple_entry(p, m)
+{
+}
+
+int short_line_entry::line_type()
+{
+ return 1;
+}
+
+void short_line_entry::simple_print(int dont_move)
+{
+ if (mod->stagger)
+ prints("\\v'-.5v'");
+ if (!dont_move)
+ prints("\\v'-" BAR_HEIGHT "'");
+ printfs("\\h'|\\n[%1]u'", column_start_reg(start_col));
+ printfs("\\s[\\n[" LINESIZE_REG "]]"
+ "\\D'l \\n[%1]u 0'"
+ "\\s0",
+ span_width_reg(start_col, end_col));
+ if (!dont_move)
+ prints("\\v'" BAR_HEIGHT "'");
+ if (mod->stagger)
+ prints("\\v'.5v'");
+}
+
+short_double_line_entry::short_double_line_entry(const table *p,
+ const entry_modifier *m)
+: simple_entry(p, m)
+{
+}
+
+int short_double_line_entry::line_type()
+{
+ return 2;
+}
+
+void short_double_line_entry::simple_print(int dont_move)
+{
+ if (mod->stagger)
+ prints("\\v'-.5v'");
+ if (!dont_move)
+ prints("\\v'-" BAR_HEIGHT "'");
+ printfs("\\h'|\\n[%2]u'"
+ "\\v'-" HALF_DOUBLE_LINE_SEP "'"
+ "\\s[\\n[" LINESIZE_REG "]]"
+ "\\D'l \\n[%1]u 0'"
+ "\\v'" DOUBLE_LINE_SEP "'"
+ "\\D'l |\\n[%2]u 0'"
+ "\\s0"
+ "\\v'-" HALF_DOUBLE_LINE_SEP "'",
+ span_width_reg(start_col, end_col),
+ column_start_reg(start_col));
+ if (!dont_move)
+ prints("\\v'" BAR_HEIGHT "'");
+ if (mod->stagger)
+ prints("\\v'.5v'");
+}
+
+void set_modifier(const entry_modifier *m)
+{
+ if (!m->font.empty())
+ printfs(".ft %1\n", m->font);
+ if (m->point_size.val != 0) {
+ prints(".ps ");
+ if (m->point_size.inc > 0)
+ prints('+');
+ else if (m->point_size.inc < 0)
+ prints('-');
+ printfs("%1\n", as_string(m->point_size.val));
+ }
+ if (m->vertical_spacing.val != 0) {
+ prints(".vs ");
+ if (m->vertical_spacing.inc > 0)
+ prints('+');
+ else if (m->vertical_spacing.inc < 0)
+ prints('-');
+ printfs("%1\n", as_string(m->vertical_spacing.val));
+ }
+ if (!m->macro.empty())
+ printfs(".%1\n", m->macro);
+}
+
+void set_inline_modifier(const entry_modifier *m)
+{
+ if (!m->font.empty())
+ printfs("\\f[%1]", m->font);
+ if (m->point_size.val != 0) {
+ prints("\\s[");
+ if (m->point_size.inc > 0)
+ prints('+');
+ else if (m->point_size.inc < 0)
+ prints('-');
+ printfs("%1]", as_string(m->point_size.val));
+ }
+ if (m->stagger)
+ prints("\\v'-.5v'");
+}
+
+void restore_inline_modifier(const entry_modifier *m)
+{
+ if (!m->font.empty())
+ prints("\\f[\\n[" SAVED_FONT_REG "]]");
+ if (m->point_size.val != 0)
+ prints("\\s[\\n[" SAVED_SIZE_REG "]]");
+ if (m->stagger)
+ prints("\\v'.5v'");
+}
+
+struct stuff {
+ stuff *next;
+ int row; // occurs before row 'row'
+ char printed; // has it been printed?
+
+ stuff(int);
+ virtual void print(table *) = 0;
+ virtual ~stuff();
+ virtual int is_single_line() { return 0; };
+ virtual int is_double_line() { return 0; };
+};
+
+stuff::stuff(int r) : next(0), row(r), printed(0)
+{
+}
+
+stuff::~stuff()
+{
+}
+
+struct text_stuff : public stuff {
+ string contents;
+ const char *filename;
+ int lineno;
+
+ text_stuff(const string &, int, const char *, int);
+ ~text_stuff();
+ void print(table *);
+};
+
+text_stuff::text_stuff(const string &s, int r, const char *fn, int ln)
+: stuff(r), contents(s), filename(fn), lineno(ln)
+{
+}
+
+text_stuff::~text_stuff()
+{
+}
+
+void text_stuff::print(table *)
+{
+ printed = 1;
+ prints(".cp \\n(" COMPATIBLE_REG "\n");
+ set_troff_location(filename, lineno);
+ prints(contents);
+ prints(".cp 0\n");
+ location_force_filename = 1; // it might have been a .lf command
+}
+
+struct single_hline_stuff : public stuff {
+ single_hline_stuff(int);
+ void print(table *);
+ int is_single_line();
+};
+
+single_hline_stuff::single_hline_stuff(int r) : stuff(r)
+{
+}
+
+void single_hline_stuff::print(table *tbl)
+{
+ printed = 1;
+ tbl->print_single_hline(row);
+}
+
+int single_hline_stuff::is_single_line()
+{
+ return 1;
+}
+
+struct double_hline_stuff : stuff {
+ double_hline_stuff(int);
+ void print(table *);
+ int is_double_line();
+};
+
+double_hline_stuff::double_hline_stuff(int r) : stuff(r)
+{
+}
+
+void double_hline_stuff::print(table *tbl)
+{
+ printed = 1;
+ tbl->print_double_hline(row);
+}
+
+int double_hline_stuff::is_double_line()
+{
+ return 1;
+}
+
+struct vertical_rule {
+ vertical_rule *next;
+ int start_row;
+ int end_row;
+ int col;
+ char is_double;
+ string top_adjust;
+ string bot_adjust;
+
+ vertical_rule(int, int, int, int, vertical_rule *);
+ ~vertical_rule();
+ void contribute_to_bottom_macro(table *);
+ void print();
+};
+
+vertical_rule::vertical_rule(int sr, int er, int c, int dbl,
+ vertical_rule *p)
+: next(p), start_row(sr), end_row(er), col(c), is_double(dbl)
+{
+}
+
+vertical_rule::~vertical_rule()
+{
+}
+
+void vertical_rule::contribute_to_bottom_macro(table *tbl)
+{
+ printfs(".if \\n[" CURRENT_ROW_REG "]>=%1",
+ as_string(start_row));
+ if (end_row != tbl->get_nrows() - 1)
+ printfs("&(\\n[" CURRENT_ROW_REG "]<%1)",
+ as_string(end_row));
+ prints(" \\{\\\n");
+ printfs(". if %1<=\\n[" LAST_PASSED_ROW_REG "] .nr %2 \\n[#T]\n",
+ as_string(start_row),
+ row_top_reg(start_row));
+ const char *offset_table[3];
+ if (is_double) {
+ offset_table[0] = "-" HALF_DOUBLE_LINE_SEP;
+ offset_table[1] = "+" HALF_DOUBLE_LINE_SEP;
+ offset_table[2] = 0;
+ }
+ else {
+ offset_table[0] = "";
+ offset_table[1] = 0;
+ }
+ for (const char **offsetp = offset_table; *offsetp; offsetp++) {
+ prints(". sp -1\n"
+ "\\v'" BODY_DEPTH);
+ if (!bot_adjust.empty())
+ printfs("+%1", bot_adjust);
+ prints("'");
+ printfs("\\h'\\n[%1]u%3'\\s[\\n[" LINESIZE_REG "]]\\D'l 0 |\\n[%2]u-1v",
+ column_divide_reg(col),
+ row_top_reg(start_row),
+ *offsetp);
+ if (!bot_adjust.empty())
+ printfs("-(%1)", bot_adjust);
+ // don't perform the top adjustment if the top is actually #T
+ if (!top_adjust.empty())
+ printfs("+((%1)*(%2>\\n[" LAST_PASSED_ROW_REG "]))",
+ top_adjust,
+ as_string(start_row));
+ prints("'\\s0\n");
+ }
+ prints(".\\}\n");
+}
+
+void vertical_rule::print()
+{
+ printfs("\\*[" TRANSPARENT_STRING_NAME "]"
+ ".if %1<=\\*[" QUOTE_STRING_NAME "]\\n[" LAST_PASSED_ROW_REG "] "
+ ".nr %2 \\*[" QUOTE_STRING_NAME "]\\n[#T]\n",
+ as_string(start_row),
+ row_top_reg(start_row));
+ const char *offset_table[3];
+ if (is_double) {
+ offset_table[0] = "-" HALF_DOUBLE_LINE_SEP;
+ offset_table[1] = "+" HALF_DOUBLE_LINE_SEP;
+ offset_table[2] = 0;
+ }
+ else {
+ offset_table[0] = "";
+ offset_table[1] = 0;
+ }
+ for (const char **offsetp = offset_table; *offsetp; offsetp++) {
+ prints("\\*[" TRANSPARENT_STRING_NAME "].sp -1\n"
+ "\\*[" TRANSPARENT_STRING_NAME "]\\v'" BODY_DEPTH);
+ if (!bot_adjust.empty())
+ printfs("+%1", bot_adjust);
+ prints("'");
+ printfs("\\h'\\n[%1]u%3'"
+ "\\s[\\n[" LINESIZE_REG "]]"
+ "\\D'l 0 |\\*[" QUOTE_STRING_NAME "]\\n[%2]u-1v",
+ column_divide_reg(col),
+ row_top_reg(start_row),
+ *offsetp);
+ if (!bot_adjust.empty())
+ printfs("-(%1)", bot_adjust);
+ // don't perform the top adjustment if the top is actually #T
+ if (!top_adjust.empty())
+ printfs("+((%1)*(%2>\\*[" QUOTE_STRING_NAME "]\\n["
+ LAST_PASSED_ROW_REG "]))",
+ top_adjust,
+ as_string(start_row));
+ prints("'"
+ "\\s0\n");
+ }
+}
+
+table::table(int nc, unsigned f, int ls, char dpc)
+: nrows(0), ncolumns(nc), linesize(ls), decimal_point_char(dpc),
+ vrule_list(0), stuff_list(0), span_list(0),
+ entry_list(0), entry_list_tailp(&entry_list), entry(0),
+ vline(0), row_is_all_lines(0), left_separation(0),
+ right_separation(0), total_separation(0), allocated_rows(0), flags(f)
+{
+ minimum_width = new string[ncolumns];
+ column_separation = ncolumns > 1 ? new int[ncolumns - 1] : 0;
+ equal = new char[ncolumns];
+ expand = new char[ncolumns];
+ int i;
+ for (i = 0; i < ncolumns; i++) {
+ equal[i] = 0;
+ expand[i] = 0;
+ }
+ for (i = 0; i < ncolumns - 1; i++)
+ column_separation[i] = DEFAULT_COLUMN_SEPARATION;
+ delim[0] = delim[1] = '\0';
+}
+
+table::~table()
+{
+ for (int i = 0; i < nrows; i++) {
+ delete[] entry[i];
+ delete[] vline[i];
+ }
+ delete[] entry;
+ delete[] vline;
+ while (entry_list) {
+ table_entry *tem = entry_list;
+ entry_list = entry_list->next;
+ delete tem;
+ }
+ delete[] minimum_width;
+ delete[] column_separation;
+ delete[] equal;
+ delete[] expand;
+ while (stuff_list) {
+ stuff *tem = stuff_list;
+ stuff_list = stuff_list->next;
+ delete tem;
+ }
+ while (vrule_list) {
+ vertical_rule *tem = vrule_list;
+ vrule_list = vrule_list->next;
+ delete tem;
+ }
+ delete[] row_is_all_lines;
+ while (span_list) {
+ horizontal_span *tem = span_list;
+ span_list = span_list->next;
+ delete tem;
+ }
+}
+
+void table::set_delim(char c1, char c2)
+{
+ delim[0] = c1;
+ delim[1] = c2;
+}
+
+void table::set_minimum_width(int c, const string &w)
+{
+ assert(c >= 0 && c < ncolumns);
+ minimum_width[c] = w;
+}
+
+void table::set_column_separation(int c, int n)
+{
+ assert(c >= 0 && c < ncolumns - 1);
+ column_separation[c] = n;
+}
+
+void table::set_equal_column(int c)
+{
+ assert(c >= 0 && c < ncolumns);
+ equal[c] = 1;
+}
+
+void table::set_expand_column(int c)
+{
+ assert(c >= 0 && c < ncolumns);
+ expand[c] = 1;
+}
+
+void table::add_stuff(stuff *p)
+{
+ stuff **pp;
+ for (pp = &stuff_list; *pp; pp = &(*pp)->next)
+ ;
+ *pp = p;
+}
+
+void table::add_text_line(int r, const string &s, const char *filename,
+ int lineno)
+{
+ add_stuff(new text_stuff(s, r, filename, lineno));
+}
+
+void table::add_single_hline(int r)
+{
+ add_stuff(new single_hline_stuff(r));
+}
+
+void table::add_double_hline(int r)
+{
+ add_stuff(new double_hline_stuff(r));
+}
+
+void table::allocate(int r)
+{
+ if (r >= nrows) {
+ typedef table_entry **PPtable_entry; // work around g++ 1.36.1 bug
+ if (r >= allocated_rows) {
+ if (allocated_rows == 0) {
+ allocated_rows = 16;
+ if (allocated_rows <= r)
+ allocated_rows = r + 1;
+ entry = new PPtable_entry[allocated_rows];
+ vline = new char*[allocated_rows];
+ }
+ else {
+ table_entry ***old_entry = entry;
+ int old_allocated_rows = allocated_rows;
+ allocated_rows *= 2;
+ if (allocated_rows <= r)
+ allocated_rows = r + 1;
+ entry = new PPtable_entry[allocated_rows];
+ memcpy(entry, old_entry, sizeof(table_entry**)*old_allocated_rows);
+ delete[] old_entry;
+ char **old_vline = vline;
+ vline = new char*[allocated_rows];
+ memcpy(vline, old_vline, sizeof(char*)*old_allocated_rows);
+ delete[] old_vline;
+ }
+ }
+ assert(allocated_rows > r);
+ while (nrows <= r) {
+ entry[nrows] = new table_entry*[ncolumns];
+ int i;
+ for (i = 0; i < ncolumns; i++)
+ entry[nrows][i] = 0;
+ vline[nrows] = new char[ncolumns+1];
+ for (i = 0; i < ncolumns+1; i++)
+ vline[nrows][i] = 0;
+ nrows++;
+ }
+ }
+}
+
+void table::do_hspan(int r, int c)
+{
+ assert(r >= 0 && c >= 0 && r < nrows && c < ncolumns);
+ if (c == 0) {
+ error("first column cannot be horizontally spanned");
+ return;
+ }
+ table_entry *e = entry[r][c];
+ if (e) {
+ assert(e->start_row <= r && r <= e->end_row
+ && e->start_col <= c && c <= e->end_col
+ && e->end_row - e->start_row > 0
+ && e->end_col - e->start_col > 0);
+ return;
+ }
+ e = entry[r][c-1];
+ // e can be 0 if we had an empty entry or an error
+ if (e == 0)
+ return;
+ if (e->start_row != r) {
+ /*
+ l l
+ ^ s */
+ error("impossible horizontal span at row %1, column %2", r + 1,
+ c + 1);
+ }
+ else {
+ e->end_col = c;
+ entry[r][c] = e;
+ }
+}
+
+void table::do_vspan(int r, int c)
+{
+ assert(r >= 0 && c >= 0 && r < nrows && c < ncolumns);
+ if (0 == r) {
+ error("first row cannot be vertically spanned");
+ return;
+ }
+ table_entry *e = entry[r][c];
+ if (e) {
+ assert(e->start_row <= r);
+ assert(r <= e->end_row);
+ assert(e->start_col <= c);
+ assert(c <= e->end_col);
+ assert((e->end_row - e->start_row) > 0);
+ assert((e->end_col - e->start_col) > 0);
+ return;
+ }
+ e = entry[r-1][c];
+ // e can be a null pointer if we had an empty entry or an error
+ if (0 == e)
+ return;
+ if (e->start_col != c) {
+ /* l s
+ l ^ */
+ error("impossible vertical span at row %1, column %2", r + 1,
+ c + 1);
+ }
+ else {
+ for (int i = c; i <= e->end_col; i++) {
+ assert(entry[r][i] == 0);
+ entry[r][i] = e;
+ }
+ e->end_row = r;
+ }
+}
+
+int find_decimal_point(const char *s, char decimal_point_char,
+ const char *delim)
+{
+ if (s == 0 || *s == '\0')
+ return -1;
+ const char *p;
+ int in_delim = 0; // is p within eqn delimiters?
+ // tbl recognises \& even within eqn delimiters; I don't
+ for (p = s; *p; p++)
+ if (in_delim) {
+ if (*p == delim[1])
+ in_delim = 0;
+ }
+ else if (*p == delim[0])
+ in_delim = 1;
+ else if (p[0] == '\\' && p[1] == '&')
+ return p - s;
+ int possible_pos = -1;
+ in_delim = 0;
+ for (p = s; *p; p++)
+ if (in_delim) {
+ if (*p == delim[1])
+ in_delim = 0;
+ }
+ else if (*p == delim[0])
+ in_delim = 1;
+ else if (p[0] == decimal_point_char && csdigit(p[1]))
+ possible_pos = p - s;
+ if (possible_pos >= 0)
+ return possible_pos;
+ in_delim = 0;
+ for (p = s; *p; p++)
+ if (in_delim) {
+ if (*p == delim[1])
+ in_delim = 0;
+ }
+ else if (*p == delim[0])
+ in_delim = 1;
+ else if (csdigit(*p))
+ possible_pos = p + 1 - s;
+ return possible_pos;
+}
+
+void table::add_entry(int r, int c, const string &str,
+ const entry_format *f, const char *fn, int ln)
+{
+ allocate(r);
+ table_entry *e = 0;
+ char *s = str.extract();
+ if (str.search('\n') >= 0) {
+ bool was_changed = false;
+ for (int i = 0; s[i] != '\0'; i++)
+ if ((i > 0) && (s[(i - 1)] == '\\') && (s[i] == 'R')) {
+ s[i] = '&';
+ was_changed = true;
+ }
+ if (was_changed)
+ error_with_file_and_line(fn, ln, "repeating a glyph with '\\R'"
+ " is not allowed in a text block");
+ }
+ if (str == "\\_") {
+ e = new short_line_entry(this, f);
+ }
+ else if (str == "\\=") {
+ e = new short_double_line_entry(this, f);
+ }
+ else if (str == "_") {
+ single_line_entry *lefte;
+ if (c > 0 && entry[r][c-1] != 0 &&
+ (lefte = entry[r][c-1]->to_single_line_entry()) != 0
+ && lefte->start_row == r
+ && lefte->mod->stagger == f->stagger) {
+ lefte->end_col = c;
+ entry[r][c] = lefte;
+ }
+ else
+ e = new single_line_entry(this, f);
+ }
+ else if (str == "=") {
+ double_line_entry *lefte;
+ if (c > 0 && entry[r][c-1] != 0 &&
+ (lefte = entry[r][c-1]->to_double_line_entry()) != 0
+ && lefte->start_row == r
+ && lefte->mod->stagger == f->stagger) {
+ lefte->end_col = c;
+ entry[r][c] = lefte;
+ }
+ else
+ e = new double_line_entry(this, f);
+ }
+ else if (str == "\\^") {
+ if (r == 0) {
+ error("first row cannot contain a vertical span entry '\\^'");
+ e = new empty_entry(this, f);
+ }
+ else
+ do_vspan(r, c);
+ }
+ else {
+ int is_block = str.search('\n') >= 0;
+ switch (f->type) {
+ case FORMAT_SPAN:
+ assert(str.empty());
+ do_hspan(r, c);
+ break;
+ case FORMAT_LEFT:
+ if (!str.empty()) {
+ if (is_block)
+ e = new left_block_entry(this, f, s);
+ else
+ e = new left_text_entry(this, f, s);
+ }
+ else
+ e = new empty_entry(this, f);
+ break;
+ case FORMAT_CENTER:
+ if (!str.empty()) {
+ if (is_block)
+ e = new center_block_entry(this, f, s);
+ else
+ e = new center_text_entry(this, f, s);
+ }
+ else
+ e = new empty_entry(this, f);
+ break;
+ case FORMAT_RIGHT:
+ if (!str.empty()) {
+ if (is_block)
+ e = new right_block_entry(this, f, s);
+ else
+ e = new right_text_entry(this, f, s);
+ }
+ else
+ e = new empty_entry(this, f);
+ break;
+ case FORMAT_NUMERIC:
+ if (!str.empty()) {
+ if (is_block) {
+ error_with_file_and_line(fn, ln, "can't have numeric text block");
+ e = new left_block_entry(this, f, s);
+ }
+ else {
+ int pos = find_decimal_point(s, decimal_point_char, delim);
+ if (pos < 0)
+ e = new center_text_entry(this, f, s);
+ else
+ e = new numeric_text_entry(this, f, s, pos);
+ }
+ }
+ else
+ e = new empty_entry(this, f);
+ break;
+ case FORMAT_ALPHABETIC:
+ if (!str.empty()) {
+ if (is_block)
+ e = new alphabetic_block_entry(this, f, s);
+ else
+ e = new alphabetic_text_entry(this, f, s);
+ }
+ else
+ e = new empty_entry(this, f);
+ break;
+ case FORMAT_VSPAN:
+ do_vspan(r, c);
+ break;
+ case FORMAT_HLINE:
+ if ((str.length() != 0) && (str != "\\&"))
+ error_with_file_and_line(fn, ln,
+ "ignoring non-empty data entry using"
+ " '_' column classifier");
+ e = new single_line_entry(this, f);
+ break;
+ case FORMAT_DOUBLE_HLINE:
+ if ((str.length() != 0) && (str != "\\&"))
+ error_with_file_and_line(fn, ln,
+ "ignoring non-empty data entry using"
+ " '=' column classifier");
+ e = new double_line_entry(this, f);
+ break;
+ default:
+ assert(0 == "table column format not in FORMAT_{SPAN,LEFT,CENTER,"
+ "RIGHT,NUMERIC,ALPHABETIC,VSPAN,HLINE,DOUBLE_HLINE}");
+ }
+ }
+ if (e) {
+ table_entry *preve = entry[r][c];
+ if (preve) {
+ /* c s
+ ^ l */
+ error_with_file_and_line(fn, ln, "row %1, column %2 already"
+ " spanned",
+ r + 1, c + 1);
+ delete e;
+ }
+ else {
+ e->input_lineno = ln;
+ e->input_filename = fn;
+ e->start_row = e->end_row = r;
+ e->start_col = e->end_col = c;
+ *entry_list_tailp = e;
+ entry_list_tailp = &e->next;
+ entry[r][c] = e;
+ }
+ }
+}
+
+// add vertical lines for row r
+
+void table::add_vlines(int r, const char *v)
+{
+ allocate(r);
+ bool lwarned = false;
+ bool twarned = false;
+ for (int i = 0; i < ncolumns+1; i++) {
+ assert(v[i] < 3);
+ if (v[i] && (flags & (BOX | ALLBOX | DOUBLEBOX)) && (i == 0)
+ && (!lwarned)) {
+ error("ignoring vertical line at leading edge of boxed table");
+ lwarned = true;
+ }
+ else if (v[i] && (flags & (BOX | ALLBOX | DOUBLEBOX))
+ && (i == ncolumns) && (!twarned)) {
+ error("ignoring vertical line at trailing edge of boxed table");
+ twarned = true;
+ }
+ else
+ vline[r][i] = v[i];
+ }
+}
+
+void table::check()
+{
+ table_entry *p = entry_list;
+ int i, j;
+ while (p) {
+ for (i = p->start_row; i <= p->end_row; i++)
+ for (j = p->start_col; j <= p->end_col; j++)
+ assert(entry[i][j] == p);
+ p = p->next;
+ }
+}
+
+void table::print()
+{
+ location_force_filename = 1;
+ check();
+ init_output();
+ determine_row_type();
+ compute_widths();
+ if (!(flags & CENTER))
+ prints(".if \\n[" SAVED_CENTER_REG "] \\{\\\n");
+ prints(". in +(u;\\n[.l]-\\n[.i]-\\n[TW]/2>?-\\n[.i])\n"
+ ". nr " SAVED_INDENT_REG " \\n[.i]\n");
+ if (!(flags & CENTER))
+ prints(".\\}\n");
+ build_vrule_list();
+ define_bottom_macro();
+ do_top();
+ for (int i = 0; i < nrows; i++)
+ do_row(i);
+ do_bottom();
+}
+
+void table::determine_row_type()
+{
+ row_is_all_lines = new char[nrows];
+ for (int i = 0; i < nrows; i++) {
+ bool had_single = false;
+ bool had_double = false;
+ bool had_non_line = false;
+ for (int c = 0; c < ncolumns; c++) {
+ table_entry *e = entry[i][c];
+ if (e != 0) {
+ if (e->start_row == e->end_row) {
+ int t = e->line_type();
+ switch (t) {
+ case -1:
+ had_non_line = true;
+ break;
+ case 0:
+ // empty
+ break;
+ case 1:
+ had_single = true;
+ break;
+ case 2:
+ had_double = true;
+ break;
+ default:
+ assert(0 == "table entry line type not in {-1, 0, 1, 2}");
+ }
+ if (had_non_line)
+ break;
+ }
+ c = e->end_col;
+ }
+ }
+ if (had_non_line)
+ row_is_all_lines[i] = 0;
+ else if (had_double)
+ row_is_all_lines[i] = 2;
+ else if (had_single)
+ row_is_all_lines[i] = 1;
+ else
+ row_is_all_lines[i] = 0;
+ }
+}
+
+int table::count_expand_columns()
+{
+ int count = 0;
+ for (int i = 0; i < ncolumns; i++)
+ if (expand[i])
+ count++;
+ return count;
+}
+
+void table::init_output()
+{
+ prints(".\\\" initialize output\n");
+ prints(".nr " COMPATIBLE_REG " \\n(.C\n"
+ ".cp 0\n");
+ if (linesize > 0)
+ printfs(".nr " LINESIZE_REG " %1\n", as_string(linesize));
+ else
+ prints(".nr " LINESIZE_REG " \\n[.s]\n");
+ if (!(flags & CENTER))
+ prints(".nr " SAVED_CENTER_REG " \\n[.ce]\n");
+ if (compatible_flag)
+ prints(".ds " LEADER_REG " \\a\n");
+ if (!(flags & NOKEEP))
+ prints(".if !r " USE_KEEPS_REG " .nr " USE_KEEPS_REG " 1\n");
+ prints(".de " RESET_MACRO_NAME "\n"
+ ". ft \\n[.f]\n"
+ ". ps \\n[.s]\n"
+ ". vs \\n[.v]u\n"
+ ". in \\n[.i]u\n"
+ ". ll \\n[.l]u\n"
+ ". ls \\n[.L]\n"
+ ". hy \\\\n[" SAVED_HYPHENATION_MODE_REG "]\n"
+ ". hla \\\\*[" SAVED_HYPHENATION_LANG_NAME "]\n"
+ ". hlm \\\\n[" SAVED_HYPHENATION_MAX_LINES_REG "]\n"
+ ". hym \\\\n[" SAVED_HYPHENATION_MARGIN_REG "]\n"
+ ". hys \\\\n[" SAVED_HYPHENATION_SPACE_REG "]\n"
+ ". ad \\n[.j]\n"
+ ". ie \\n[.u] .fi\n"
+ ". el .nf\n"
+ ". ce \\n[.ce]\n"
+ ". ta \\\\*[" SAVED_TABS_NAME "]\n"
+ ". ss \\\\n[" SAVED_INTER_WORD_SPACE_SIZE "]"
+ " \\\\n[" SAVED_INTER_SENTENCE_SPACE_SIZE "]\n"
+ "..\n"
+ ".nr " SAVED_INDENT_REG " \\n[.i]\n"
+ ".nr " SAVED_FONT_REG " \\n[.f]\n"
+ ".nr " SAVED_SIZE_REG " \\n[.s]\n"
+ ".nr " SAVED_FILL_REG " \\n[.u]\n"
+ ".ds " SAVED_TABS_NAME " \\n[.tabs]\n"
+ ".nr " SAVED_INTER_WORD_SPACE_SIZE " \\n[.ss]\n"
+ ".nr " SAVED_INTER_SENTENCE_SPACE_SIZE " \\n[.sss]\n"
+ ".nr " SAVED_HYPHENATION_MODE_REG " \\n[.hy]\n"
+ ".ds " SAVED_HYPHENATION_LANG_NAME " \\n[.hla]\n"
+ ".nr " SAVED_HYPHENATION_MAX_LINES_REG " (\\n[.hlm])\n"
+ ".nr " SAVED_HYPHENATION_MARGIN_REG " \\n[.hym]\n"
+ ".nr " SAVED_HYPHENATION_SPACE_REG " \\n[.hys]\n"
+ ".nr T. 0\n"
+ ".nr " CURRENT_ROW_REG " 0-1\n"
+ ".nr " LAST_PASSED_ROW_REG " 0-1\n"
+ ".nr " SECTION_DIVERSION_FLAG_REG " 0\n"
+ ".ds " TRANSPARENT_STRING_NAME "\n"
+ ".ds " QUOTE_STRING_NAME "\n"
+ ".nr " NEED_BOTTOM_RULE_REG " 1\n"
+ ".nr " SUPPRESS_BOTTOM_REG " 0\n"
+ ".eo\n"
+ ".de " REPEATED_MARK_MACRO "\n"
+ ". mk \\$1\n"
+ ". if !'\\n(.z'' \\!." REPEATED_MARK_MACRO " \"\\$1\"\n"
+ "..\n"
+ ".de " REPEATED_VPT_MACRO "\n"
+ ". vpt \\$1\n"
+ ". if !'\\n(.z'' \\!." REPEATED_VPT_MACRO " \"\\$1\"\n"
+ "..\n");
+ if (!(flags & NOKEEP)) {
+ prints(".de " KEEP_MACRO_NAME "\n"
+ ". if '\\n[.z]'' \\{\\\n"
+ ". ds " QUOTE_STRING_NAME " \\\\\n"
+ ". ds " TRANSPARENT_STRING_NAME " \\!\n"
+ ". di " SECTION_DIVERSION_NAME "\n"
+ ". nr " SECTION_DIVERSION_FLAG_REG " 1\n"
+ ". in 0\n"
+ ". \\}\n"
+ "..\n"
+ // Protect '#' in macro name from being interpreted by eqn.
+ ".ig\n"
+ ".EQ\n"
+ "delim off\n"
+ ".EN\n"
+ "..\n"
+ ".de " RELEASE_MACRO_NAME "\n"
+ ". if \\n[" SECTION_DIVERSION_FLAG_REG "] \\{\\\n"
+ ". di\n"
+ ". in \\n[" SAVED_INDENT_REG "]u\n"
+ ". nr " SAVED_DN_REG " \\n[dn]\n"
+ ". ds " QUOTE_STRING_NAME "\n"
+ ". ds " TRANSPARENT_STRING_NAME "\n"
+ ". nr " SECTION_DIVERSION_FLAG_REG " 0\n"
+ ". if \\n[.t]<=\\n[dn] \\{\\\n"
+ ". nr T. 1\n"
+ ". T#\n"
+ ". nr " SUPPRESS_BOTTOM_REG " 1\n"
+ ". sp \\n[.t]u\n"
+ ". nr " SUPPRESS_BOTTOM_REG " 0\n"
+ ". mk #T\n"
+ ". \\}\n");
+ if (!(flags & NOWARN)) {
+ prints(". if \\n[.t]<=\\n[" SAVED_DN_REG "] \\{\\\n");
+ // eqn(1) delimiters have already been switched off.
+ entry_list->set_location();
+ // Since we turn off traps, troff won't go into an infinite loop
+ // when we output the table row; it will just flow off the bottom
+ // of the page.
+ prints(". tmc \\n[.F]:\\n[.c]: warning:\n"
+ ". tm1 \" table row does not fit on page \\n%\n");
+ prints(". \\}\n");
+ }
+ prints(". nf\n"
+ ". ls 1\n"
+ ". " SECTION_DIVERSION_NAME "\n"
+ ". ls\n"
+ ". rm " SECTION_DIVERSION_NAME "\n"
+ ". \\}\n"
+ "..\n"
+ ".ig\n"
+ ".EQ\n"
+ "delim on\n"
+ ".EN\n"
+ "..\n"
+ ".nr " TABLE_DIVERSION_FLAG_REG " 0\n"
+ ".de " TABLE_KEEP_MACRO_NAME "\n"
+ ". if '\\n[.z]'' \\{\\\n"
+ ". di " TABLE_DIVERSION_NAME "\n"
+ ". nr " TABLE_DIVERSION_FLAG_REG " 1\n"
+ ". \\}\n"
+ "..\n"
+ ".de " TABLE_RELEASE_MACRO_NAME "\n"
+ ". if \\n[" TABLE_DIVERSION_FLAG_REG "] \\{\\\n"
+ ". br\n"
+ ". di\n"
+ ". nr " SAVED_DN_REG " \\n[dn]\n"
+ ". ne \\n[dn]u+\\n[.V]u\n"
+ ". ie \\n[.t]<=\\n[" SAVED_DN_REG "] \\{\\\n");
+ // Protect characters in diagnostic message (especially :, [, ])
+ // from being interpreted by eqn.
+ prints(". ds " NOP_NAME " \\\" empty\n");
+ prints(". ig " NOP_NAME "\n"
+ ".EQ\n"
+ "delim off\n"
+ ".EN\n"
+ ". " NOP_NAME "\n");
+ entry_list->set_location();
+ prints(". nr " PREVIOUS_PAGE_REG " (\\n% - 1)\n"
+ ". tmc \\n[.F]:\\n[.c]: error:\n"
+ ". tmc \" boxed table does not fit on page"
+ " \\n[" PREVIOUS_PAGE_REG "];\n"
+ ". tm1 \" use .TS H/.TH with a supporting macro package"
+ "\n"
+ ". rr " PREVIOUS_PAGE_REG "\n");
+ prints(". ig " NOP_NAME "\n"
+ ".EQ\n"
+ "delim on\n"
+ ".EN\n"
+ ". " NOP_NAME "\n");
+ prints(". \\}\n"
+ ". el \\{\\\n"
+ ". in 0\n"
+ ". ls 1\n"
+ ". nf\n"
+ ". " TABLE_DIVERSION_NAME "\n"
+ ". \\}\n"
+ ". rm " TABLE_DIVERSION_NAME "\n"
+ ". \\}\n"
+ "..\n");
+ }
+ prints(".ec\n"
+ ".ce 0\n");
+ prints(".nr " SAVED_NUMBERING_LINENO " \\n[ln]\n"
+ ".nr ln 0\n"
+ ".nr " SAVED_NUMBERING_SUPPRESSION_COUNT " \\n[.nn]\n"
+ ".nn 2147483647\n"); // 2^31-1; inelegant but effective
+ prints(".nf\n");
+}
+
+string block_width_reg(int r, int c)
+{
+ static char name[sizeof(BLOCK_WIDTH_PREFIX)+INT_DIGITS+1+INT_DIGITS];
+ sprintf(name, BLOCK_WIDTH_PREFIX "%d,%d", r, c);
+ return string(name);
+}
+
+string block_diversion_name(int r, int c)
+{
+ static char name[sizeof(BLOCK_DIVERSION_PREFIX)+INT_DIGITS+1+INT_DIGITS];
+ sprintf(name, BLOCK_DIVERSION_PREFIX "%d,%d", r, c);
+ return string(name);
+}
+
+string block_height_reg(int r, int c)
+{
+ static char name[sizeof(BLOCK_HEIGHT_PREFIX)+INT_DIGITS+1+INT_DIGITS];
+ sprintf(name, BLOCK_HEIGHT_PREFIX "%d,%d", r, c);
+ return string(name);
+}
+
+string span_width_reg(int start_col, int end_col)
+{
+ static char name[sizeof(SPAN_WIDTH_PREFIX)+INT_DIGITS+1+INT_DIGITS];
+ sprintf(name, SPAN_WIDTH_PREFIX "%d", start_col);
+ if (end_col != start_col)
+ sprintf(strchr(name, '\0'), ",%d", end_col);
+ return string(name);
+}
+
+string span_left_numeric_width_reg(int start_col, int end_col)
+{
+ static char name[sizeof(SPAN_LEFT_NUMERIC_WIDTH_PREFIX)+INT_DIGITS+1+INT_DIGITS];
+ sprintf(name, SPAN_LEFT_NUMERIC_WIDTH_PREFIX "%d", start_col);
+ if (end_col != start_col)
+ sprintf(strchr(name, '\0'), ",%d", end_col);
+ return string(name);
+}
+
+string span_right_numeric_width_reg(int start_col, int end_col)
+{
+ static char name[sizeof(SPAN_RIGHT_NUMERIC_WIDTH_PREFIX)+INT_DIGITS+1+INT_DIGITS];
+ sprintf(name, SPAN_RIGHT_NUMERIC_WIDTH_PREFIX "%d", start_col);
+ if (end_col != start_col)
+ sprintf(strchr(name, '\0'), ",%d", end_col);
+ return string(name);
+}
+
+string span_alphabetic_width_reg(int start_col, int end_col)
+{
+ static char name[sizeof(SPAN_ALPHABETIC_WIDTH_PREFIX)+INT_DIGITS+1+INT_DIGITS];
+ sprintf(name, SPAN_ALPHABETIC_WIDTH_PREFIX "%d", start_col);
+ if (end_col != start_col)
+ sprintf(strchr(name, '\0'), ",%d", end_col);
+ return string(name);
+}
+
+string column_separation_reg(int col)
+{
+ static char name[sizeof(COLUMN_SEPARATION_PREFIX)+INT_DIGITS];
+ sprintf(name, COLUMN_SEPARATION_PREFIX "%d", col);
+ return string(name);
+}
+
+string row_start_reg(int row)
+{
+ static char name[sizeof(ROW_START_PREFIX)+INT_DIGITS];
+ sprintf(name, ROW_START_PREFIX "%d", row);
+ return string(name);
+}
+
+string column_start_reg(int col)
+{
+ static char name[sizeof(COLUMN_START_PREFIX)+INT_DIGITS];
+ sprintf(name, COLUMN_START_PREFIX "%d", col);
+ return string(name);
+}
+
+string column_end_reg(int col)
+{
+ static char name[sizeof(COLUMN_END_PREFIX)+INT_DIGITS];
+ sprintf(name, COLUMN_END_PREFIX "%d", col);
+ return string(name);
+}
+
+string column_divide_reg(int col)
+{
+ static char name[sizeof(COLUMN_DIVIDE_PREFIX)+INT_DIGITS];
+ sprintf(name, COLUMN_DIVIDE_PREFIX "%d", col);
+ return string(name);
+}
+
+string row_top_reg(int row)
+{
+ static char name[sizeof(ROW_TOP_PREFIX)+INT_DIGITS];
+ sprintf(name, ROW_TOP_PREFIX "%d", row);
+ return string(name);
+}
+
+void init_span_reg(int start_col, int end_col)
+{
+ printfs(".nr %1 \\n(.H\n.nr %2 0\n.nr %3 0\n.nr %4 0\n",
+ span_width_reg(start_col, end_col),
+ span_alphabetic_width_reg(start_col, end_col),
+ span_left_numeric_width_reg(start_col, end_col),
+ span_right_numeric_width_reg(start_col, end_col));
+}
+
+void compute_span_width(int start_col, int end_col)
+{
+ printfs(".nr %1 \\n[%1]>?(\\n[%2]+\\n[%3])\n"
+ ".if \\n[%4] .nr %1 \\n[%1]>?(\\n[%4]+2n)\n",
+ span_width_reg(start_col, end_col),
+ span_left_numeric_width_reg(start_col, end_col),
+ span_right_numeric_width_reg(start_col, end_col),
+ span_alphabetic_width_reg(start_col, end_col));
+}
+
+// Increase the widths of columns so that the width of any spanning
+// entry is not greater than the sum of the widths of the columns that
+// it spans. Ensure that the widths of columns remain equal.
+
+void table::divide_span(int start_col, int end_col)
+{
+ assert(end_col > start_col);
+ printfs(".nr " NEEDED_REG " \\n[%1]-(\\n[%2]",
+ span_width_reg(start_col, end_col),
+ span_width_reg(start_col, start_col));
+ int i;
+ for (i = start_col + 1; i <= end_col; i++) {
+ // The column separation may shrink with the expand option.
+ if (!(flags & EXPAND))
+ printfs("+%1n", as_string(column_separation[i - 1]));
+ printfs("+\\n[%1]", span_width_reg(i, i));
+ }
+ prints(")\n");
+ printfs(".nr " NEEDED_REG " \\n[" NEEDED_REG "]/%1\n",
+ as_string(end_col - start_col + 1));
+ prints(".if \\n[" NEEDED_REG "] \\{\\\n");
+ for (i = start_col; i <= end_col; i++)
+ printfs(". nr %1 +\\n[" NEEDED_REG "]\n",
+ span_width_reg(i, i));
+ int equal_flag = 0;
+ for (i = start_col; i <= end_col && !equal_flag; i++)
+ if (equal[i] || expand[i])
+ equal_flag = 1;
+ if (equal_flag) {
+ for (i = 0; i < ncolumns; i++)
+ if (i < start_col || i > end_col)
+ printfs(". nr %1 +\\n[" NEEDED_REG "]\n",
+ span_width_reg(i, i));
+ }
+ prints(".\\}\n");
+}
+
+void table::sum_columns(int start_col, int end_col, int do_expand)
+{
+ assert(end_col > start_col);
+ int i;
+ for (i = start_col; i <= end_col; i++)
+ if (expand[i])
+ break;
+ if (i > end_col) {
+ if (do_expand)
+ return;
+ }
+ else {
+ if (!do_expand)
+ return;
+ }
+ printfs(".nr %1 \\n[%2]",
+ span_width_reg(start_col, end_col),
+ span_width_reg(start_col, start_col));
+ for (i = start_col + 1; i <= end_col; i++)
+ printfs("+(%1*\\n[" SEPARATION_FACTOR_REG "])+\\n[%2]",
+ as_string(column_separation[i - 1]),
+ span_width_reg(i, i));
+ prints('\n');
+}
+
+horizontal_span::horizontal_span(int sc, int ec, horizontal_span *p)
+: next(p), start_col(sc), end_col(ec)
+{
+}
+
+void table::build_span_list()
+{
+ span_list = 0;
+ table_entry *p = entry_list;
+ while (p) {
+ if (p->end_col != p->start_col) {
+ horizontal_span *q;
+ for (q = span_list; q; q = q->next)
+ if (q->start_col == p->start_col
+ && q->end_col == p->end_col)
+ break;
+ if (!q)
+ span_list = new horizontal_span(p->start_col, p->end_col, span_list);
+ }
+ p = p->next;
+ }
+ // Now sort span_list primarily by order of end_row, and secondarily
+ // by reverse order of start_row. This ensures that if we divide
+ // spans using the order in span_list, we will get reasonable results.
+ horizontal_span *unsorted = span_list;
+ span_list = 0;
+ while (unsorted) {
+ horizontal_span **pp;
+ for (pp = &span_list; *pp; pp = &(*pp)->next)
+ if (unsorted->end_col < (*pp)->end_col
+ || (unsorted->end_col == (*pp)->end_col
+ && (unsorted->start_col > (*pp)->start_col)))
+ break;
+ horizontal_span *tem = unsorted->next;
+ unsorted->next = *pp;
+ *pp = unsorted;
+ unsorted = tem;
+ }
+}
+
+void table::compute_overall_width()
+{
+ prints(".\\\" compute overall width\n");
+ if (!(flags & GAP_EXPAND)) {
+ if (left_separation)
+ printfs(".if n .ll -%1n\n", as_string(left_separation));
+ if (right_separation)
+ printfs(".if n .ll -%1n\n", as_string(right_separation));
+ }
+ // Compute the amount of horizontal space available for expansion,
+ // measuring every column _including_ those eligible for expansion.
+ // This is the minimum required to set the table without compression.
+ prints(".nr " EXPAND_REG " 0\n");
+ prints(".nr " AVAILABLE_WIDTH_REG " \\n[.l]-\\n[.i]");
+ for (int i = 0; i < ncolumns; i++)
+ printfs("-\\n[%1]", span_width_reg(i, i));
+ if (total_separation)
+ printfs("-%1n", as_string(total_separation));
+ prints("\n");
+ // If the "expand" region option was given, a different warning will
+ // be issued later (if "nowarn" was not also specified).
+ if ((!(flags & NOWARN)) && (!(flags & EXPAND))) {
+ prints(".if \\n[" AVAILABLE_WIDTH_REG "]<0 \\{\\\n");
+ // Protect characters in diagnostic message (especially :, [, ])
+ // from being interpreted by eqn.
+ prints(". ig\n"
+ ".EQ\n"
+ "delim off\n"
+ ".EN\n"
+ ". .\n");
+ entry_list->set_location();
+ prints(". tmc \\n[.F]:\\n[.c]: warning:\n"
+ ". tm1 \" table wider than line length minus indentation"
+ "\n");
+ prints(". ig\n"
+ ".EQ\n"
+ "delim on\n"
+ ".EN\n"
+ ". .\n");
+ prints(". nr " AVAILABLE_WIDTH_REG " 0\n");
+ prints(".\\}\n");
+ }
+ // Now do a similar computation, this time omitting columns that
+ // _aren't_ undergoing expansion. The difference is the amount of
+ // space we have to distribute among the expanded columns.
+ bool do_expansion = false;
+ for (int i = 0; i < ncolumns; i++)
+ if (expand[i]) {
+ do_expansion = true;
+ break;
+ }
+ if (do_expansion) {
+ prints(".if \\n[" AVAILABLE_WIDTH_REG "] \\\n");
+ prints(". nr " EXPAND_REG " \\n[.l]-\\n[.i]");
+ for (int i = 0; i < ncolumns; i++)
+ if (!expand[i])
+ printfs("-\\n[%1]", span_width_reg(i, i));
+ if (total_separation)
+ printfs("-%1n", as_string(total_separation));
+ prints("\n");
+ int colcount = count_expand_columns();
+ if (colcount > 1)
+ printfs(".nr " EXPAND_REG " \\n[" EXPAND_REG "]/%1\n",
+ as_string(colcount));
+ for (int i = 0; i < ncolumns; i++)
+ if (expand[i])
+ printfs(".nr %1 \\n[%1]>?\\n[" EXPAND_REG "]\n",
+ span_width_reg(i, i));
+ }
+}
+
+void table::compute_total_separation()
+{
+ if (flags & (ALLBOX | BOX | DOUBLEBOX))
+ left_separation = right_separation = 1;
+ else {
+ for (int r = 0; r < nrows; r++) {
+ if (vline[r][0] > 0)
+ left_separation = 1;
+ if (vline[r][ncolumns] > 0)
+ right_separation = 1;
+ }
+ }
+ total_separation = left_separation + right_separation;
+ for (int c = 0; c < ncolumns - 1; c++)
+ total_separation += column_separation[c];
+}
+
+void table::compute_separation_factor()
+{
+ prints(".\\\" compute column separation factor\n");
+ // Don't let the separation factor be negative.
+ prints(".nr " SEPARATION_FACTOR_REG " \\n[.l]-\\n[.i]");
+ for (int i = 0; i < ncolumns; i++)
+ printfs("-\\n[%1]", span_width_reg(i, i));
+ printfs("/%1\n", as_string(total_separation));
+ // Store the remainder for use in compute_column_positions().
+ if (flags & GAP_EXPAND) {
+ prints(".if n \\\n");
+ prints(". nr " LEFTOVER_FACTOR_REG " \\n[.l]-\\n[.i]");
+ for (int i = 0; i < ncolumns; i++)
+ printfs("-\\n[%1]", span_width_reg(i, i));
+ printfs("%%%1\n", as_string(total_separation));
+ }
+ prints(".ie \\n[" SEPARATION_FACTOR_REG "]<=0 \\{\\\n");
+ if (!(flags & NOWARN)) {
+ // Protect characters in diagnostic message (especially :, [, ])
+ // from being interpreted by eqn.
+ prints(".ig\n"
+ ".EQ\n"
+ "delim off\n"
+ ".EN\n"
+ "..\n");
+ entry_list->set_location();
+ prints(".tmc \\n[.F]:\\n[.c]: warning:\n"
+ ".tm1 \" table column separation reduced to zero\n"
+ ".nr " SEPARATION_FACTOR_REG " 0\n");
+ }
+ prints(".\\}\n"
+ ".el .if \\n[" SEPARATION_FACTOR_REG "]<1n \\{\\\n");
+ if (!(flags & NOWARN)) {
+ entry_list->set_location();
+ prints(".tmc \\n[.F]:\\n[.c]: warning:\n"
+ ".tm1 \" table column separation reduced to fit line"
+ " length\n");
+ prints(".ig\n"
+ ".EQ\n"
+ "delim on\n"
+ ".EN\n"
+ "..\n");
+ }
+ prints(".\\}\n");
+}
+
+void table::compute_column_positions()
+{
+ prints(".\\\" compute column positions\n");
+ printfs(".nr %1 0\n", column_divide_reg(0));
+ printfs(".nr %1 %2n\n", column_start_reg(0),
+ as_string(left_separation));
+ // In nroff mode, compensate for width of vertical rule.
+ if (left_separation)
+ printfs(".if n .nr %1 +1n\n", column_start_reg(0));
+ int i;
+ for (i = 1;; i++) {
+ printfs(".nr %1 \\n[%2]+\\n[%3]\n",
+ column_end_reg(i-1),
+ column_start_reg(i-1),
+ span_width_reg(i-1, i-1));
+ if (i >= ncolumns)
+ break;
+ printfs(".nr %1 \\n[%2]+(%3*\\n[" SEPARATION_FACTOR_REG "])\n",
+ column_start_reg(i),
+ column_end_reg(i-1),
+ as_string(column_separation[i-1]));
+ // If we have leftover expansion room in a table using the "expand"
+ // region option, put it prior to the last column so that the table
+ // looks as if expanded to the available line length.
+ if ((ncolumns > 2) && (flags & GAP_EXPAND) && (i == (ncolumns - 1)))
+ printfs(".if n .if \\n[" LEFTOVER_FACTOR_REG "] .nr %1 +(1n>?\\n["
+ LEFTOVER_FACTOR_REG "])\n",
+ column_start_reg(i));
+ printfs(".nr %1 \\n[%2]+\\n[%3]/2\n",
+ column_divide_reg(i),
+ column_end_reg(i-1),
+ column_start_reg(i));
+ }
+ printfs(".nr %1 \\n[%2]+%3n\n",
+ column_divide_reg(ncolumns),
+ column_end_reg(i-1),
+ as_string(right_separation));
+ printfs(".nr TW \\n[%1]\n",
+ column_divide_reg(ncolumns));
+ if (flags & DOUBLEBOX) {
+ printfs(".nr %1 +" DOUBLE_LINE_SEP "\n", column_divide_reg(0));
+ printfs(".nr %1 -" DOUBLE_LINE_SEP "\n", column_divide_reg(ncolumns));
+ }
+}
+
+void table::make_columns_equal()
+{
+ int first = -1; // index of first equal column
+ int i;
+ for (i = 0; i < ncolumns; i++)
+ if (equal[i]) {
+ if (first < 0) {
+ printfs(".nr %1 \\n[%1]", span_width_reg(i, i));
+ first = i;
+ }
+ else
+ printfs(">?\\n[%1]", span_width_reg(i, i));
+ }
+ if (first >= 0) {
+ prints('\n');
+ for (i = first + 1; i < ncolumns; i++)
+ if (equal[i])
+ printfs(".nr %1 \\n[%2]\n",
+ span_width_reg(i, i),
+ span_width_reg(first, first));
+ }
+}
+
+void table::compute_widths()
+{
+ prints(".\\\" compute column widths\n");
+ build_span_list();
+ int i;
+ horizontal_span *p;
+ // These values get refined later.
+ prints(".nr " SEPARATION_FACTOR_REG " 1n\n");
+ for (i = 0; i < ncolumns; i++) {
+ init_span_reg(i, i);
+ if (!minimum_width[i].empty())
+ printfs(".nr %1 (n;%2)\n", span_width_reg(i, i), minimum_width[i]);
+ }
+ for (p = span_list; p; p = p->next)
+ init_span_reg(p->start_col, p->end_col);
+ // Compute all field widths except for blocks.
+ table_entry *q;
+ for (q = entry_list; q; q = q->next)
+ if (!q->mod->zero_width)
+ q->do_width();
+ // Compute all span widths, not handling blocks yet.
+ for (i = 0; i < ncolumns; i++)
+ compute_span_width(i, i);
+ for (p = span_list; p; p = p->next)
+ compute_span_width(p->start_col, p->end_col);
+ // Making columns equal normally increases the width of some columns.
+ make_columns_equal();
+ // Note that divide_span keeps equal width columns equal.
+ // This function might increase the width of some columns, too.
+ for (p = span_list; p; p = p->next)
+ divide_span(p->start_col, p->end_col);
+ compute_total_separation();
+ for (p = span_list; p; p = p->next)
+ sum_columns(p->start_col, p->end_col, 0);
+ // Now handle unexpanded blocks.
+ bool had_spanning_block = false;
+ bool had_equal_block = false;
+ for (q = entry_list; q; q = q->next)
+ if (q->divert(ncolumns, minimum_width,
+ (flags & EXPAND) ? column_separation : 0, 0)) {
+ if (q->end_col > q->start_col)
+ had_spanning_block = true;
+ for (i = q->start_col; i <= q->end_col && !had_equal_block; i++)
+ if (equal[i])
+ had_equal_block = true;
+ }
+ // Adjust widths.
+ if (had_equal_block)
+ make_columns_equal();
+ if (had_spanning_block)
+ for (p = span_list; p; p = p->next)
+ divide_span(p->start_col, p->end_col);
+ compute_overall_width();
+ if ((flags & EXPAND) && total_separation != 0) {
+ compute_separation_factor();
+ for (p = span_list; p; p = p->next)
+ sum_columns(p->start_col, p->end_col, 0);
+ }
+ else {
+ // Handle expanded blocks.
+ for (p = span_list; p; p = p->next)
+ sum_columns(p->start_col, p->end_col, 1);
+ for (q = entry_list; q; q = q->next)
+ if (q->divert(ncolumns, minimum_width, 0, 1)) {
+ if (q->end_col > q->start_col)
+ had_spanning_block = true;
+ }
+ // Adjust widths again.
+ if (had_spanning_block)
+ for (p = span_list; p; p = p->next)
+ divide_span(p->start_col, p->end_col);
+ }
+ compute_column_positions();
+}
+
+void table::print_single_hline(int r)
+{
+ prints(".vs " LINE_SEP ">?\\n[.V]u\n"
+ ".ls 1\n"
+ "\\v'" BODY_DEPTH "'"
+ "\\s[\\n[" LINESIZE_REG "]]");
+ if (r > nrows - 1)
+ prints("\\D'l |\\n[TW]u 0'");
+ else {
+ int start_col = 0;
+ for (;;) {
+ while (start_col < ncolumns
+ && entry[r][start_col] != 0
+ && entry[r][start_col]->start_row != r)
+ start_col++;
+ int end_col;
+ for (end_col = start_col;
+ end_col < ncolumns
+ && (entry[r][end_col] == 0
+ || entry[r][end_col]->start_row == r);
+ end_col++)
+ ;
+ if (end_col <= start_col)
+ break;
+ printfs("\\h'|\\n[%1]u",
+ column_divide_reg(start_col));
+ if ((r > 0 && vline[r-1][start_col] == 2)
+ || (r < nrows && vline[r][start_col] == 2))
+ prints("-" HALF_DOUBLE_LINE_SEP);
+ prints("'");
+ printfs("\\D'l |\\n[%1]u",
+ column_divide_reg(end_col));
+ if ((r > 0 && vline[r-1][end_col] == 2)
+ || (r < nrows && vline[r][end_col] == 2))
+ prints("+" HALF_DOUBLE_LINE_SEP);
+ prints(" 0'");
+ start_col = end_col;
+ }
+ }
+ prints("\\s0\n");
+ prints(".ls\n"
+ ".vs\n");
+}
+
+void table::print_double_hline(int r)
+{
+ prints(".vs " LINE_SEP "+" DOUBLE_LINE_SEP
+ ">?\\n[.V]u\n"
+ ".ls 1\n"
+ "\\v'" BODY_DEPTH "'"
+ "\\s[\\n[" LINESIZE_REG "]]");
+ if (r > nrows - 1)
+ prints("\\v'-" DOUBLE_LINE_SEP "'"
+ "\\D'l |\\n[TW]u 0'"
+ "\\v'" DOUBLE_LINE_SEP "'"
+ "\\h'|0'"
+ "\\D'l |\\n[TW]u 0'");
+ else {
+ int start_col = 0;
+ for (;;) {
+ while (start_col < ncolumns
+ && entry[r][start_col] != 0
+ && entry[r][start_col]->start_row != r)
+ start_col++;
+ int end_col;
+ for (end_col = start_col;
+ end_col < ncolumns
+ && (entry[r][end_col] == 0
+ || entry[r][end_col]->start_row == r);
+ end_col++)
+ ;
+ if (end_col <= start_col)
+ break;
+ const char *left_adjust = 0;
+ if ((r > 0 && vline[r-1][start_col] == 2)
+ || (r < nrows && vline[r][start_col] == 2))
+ left_adjust = "-" HALF_DOUBLE_LINE_SEP;
+ const char *right_adjust = 0;
+ if ((r > 0 && vline[r-1][end_col] == 2)
+ || (r < nrows && vline[r][end_col] == 2))
+ right_adjust = "+" HALF_DOUBLE_LINE_SEP;
+ printfs("\\v'-" DOUBLE_LINE_SEP "'"
+ "\\h'|\\n[%1]u",
+ column_divide_reg(start_col));
+ if (left_adjust)
+ prints(left_adjust);
+ prints("'");
+ printfs("\\D'l |\\n[%1]u",
+ column_divide_reg(end_col));
+ if (right_adjust)
+ prints(right_adjust);
+ prints(" 0'");
+ printfs("\\v'" DOUBLE_LINE_SEP "'"
+ "\\h'|\\n[%1]u",
+ column_divide_reg(start_col));
+ if (left_adjust)
+ prints(left_adjust);
+ prints("'");
+ printfs("\\D'l |\\n[%1]u",
+ column_divide_reg(end_col));
+ if (right_adjust)
+ prints(right_adjust);
+ prints(" 0'");
+ start_col = end_col;
+ }
+ }
+ prints("\\s0\n"
+ ".ls\n"
+ ".vs\n");
+}
+
+void table::compute_vrule_top_adjust(int start_row, int col, string &result)
+{
+ if (row_is_all_lines[start_row] && start_row < nrows - 1) {
+ if (row_is_all_lines[start_row] == 2)
+ result = LINE_SEP ">?\\n[.V]u" "+" DOUBLE_LINE_SEP;
+ else
+ result = LINE_SEP ">?\\n[.V]u";
+ start_row++;
+ }
+ else {
+ result = "";
+ if (start_row == 0)
+ return;
+ for (stuff *p = stuff_list; p && p->row <= start_row; p = p->next)
+ if (p->row == start_row
+ && (p->is_single_line() || p->is_double_line()))
+ return;
+ }
+ int left = 0;
+ if (col > 0) {
+ table_entry *e = entry[start_row-1][col-1];
+ if (e && e->start_row == e->end_row) {
+ if (e->to_double_line_entry() != 0)
+ left = 2;
+ else if (e->to_single_line_entry() != 0)
+ left = 1;
+ }
+ }
+ int right = 0;
+ if (col < ncolumns) {
+ table_entry *e = entry[start_row-1][col];
+ if (e && e->start_row == e->end_row) {
+ if (e->to_double_line_entry() != 0)
+ right = 2;
+ else if (e->to_single_line_entry() != 0)
+ right = 1;
+ }
+ }
+ if (row_is_all_lines[start_row-1] == 0) {
+ if (left > 0 || right > 0) {
+ result += "-" BODY_DEPTH "-" BAR_HEIGHT;
+ if ((left == 2 && right != 2) || (right == 2 && left != 2))
+ result += "-" HALF_DOUBLE_LINE_SEP;
+ else if (left == 2 && right == 2)
+ result += "+" HALF_DOUBLE_LINE_SEP;
+ }
+ }
+ else if (row_is_all_lines[start_row-1] == 2) {
+ if ((left == 2 && right != 2) || (right == 2 && left != 2))
+ result += "-" DOUBLE_LINE_SEP;
+ else if (left == 1 || right == 1)
+ result += "-" HALF_DOUBLE_LINE_SEP;
+ }
+}
+
+void table::compute_vrule_bot_adjust(int end_row, int col, string &result)
+{
+ if (row_is_all_lines[end_row] && end_row > 0) {
+ end_row--;
+ result = "";
+ }
+ else {
+ stuff *p;
+ for (p = stuff_list; p && p->row < end_row + 1; p = p->next)
+ ;
+ if (p && p->row == end_row + 1 && p->is_double_line()) {
+ result = "-" DOUBLE_LINE_SEP;
+ return;
+ }
+ if ((p != 0 && p->row == end_row + 1)
+ || end_row == nrows - 1) {
+ result = "";
+ return;
+ }
+ if (row_is_all_lines[end_row+1] == 1)
+ result = LINE_SEP;
+ else if (row_is_all_lines[end_row+1] == 2)
+ result = LINE_SEP "+" DOUBLE_LINE_SEP;
+ else
+ result = "";
+ }
+ int left = 0;
+ if (col > 0) {
+ table_entry *e = entry[end_row+1][col-1];
+ if (e && e->start_row == e->end_row) {
+ if (e->to_double_line_entry() != 0)
+ left = 2;
+ else if (e->to_single_line_entry() != 0)
+ left = 1;
+ }
+ }
+ int right = 0;
+ if (col < ncolumns) {
+ table_entry *e = entry[end_row+1][col];
+ if (e && e->start_row == e->end_row) {
+ if (e->to_double_line_entry() != 0)
+ right = 2;
+ else if (e->to_single_line_entry() != 0)
+ right = 1;
+ }
+ }
+ if (row_is_all_lines[end_row+1] == 0) {
+ if (left > 0 || right > 0) {
+ result = "1v-" BODY_DEPTH "-" BAR_HEIGHT;
+ if ((left == 2 && right != 2) || (right == 2 && left != 2))
+ result += "+" HALF_DOUBLE_LINE_SEP;
+ else if (left == 2 && right == 2)
+ result += "-" HALF_DOUBLE_LINE_SEP;
+ }
+ }
+ else if (row_is_all_lines[end_row+1] == 2) {
+ if (left == 2 && right == 2)
+ result += "-" DOUBLE_LINE_SEP;
+ else if (left != 2 && right != 2 && (left == 1 || right == 1))
+ result += "-" HALF_DOUBLE_LINE_SEP;
+ }
+}
+
+void table::add_vertical_rule(int start_row, int end_row,
+ int col, int is_double)
+{
+ vrule_list = new vertical_rule(start_row, end_row, col, is_double,
+ vrule_list);
+ compute_vrule_top_adjust(start_row, col, vrule_list->top_adjust);
+ compute_vrule_bot_adjust(end_row, col, vrule_list->bot_adjust);
+}
+
+void table::build_vrule_list()
+{
+ int col;
+ if (flags & ALLBOX) {
+ for (col = 1; col < ncolumns; col++) {
+ int start_row = 0;
+ for (;;) {
+ while (start_row < nrows && vline_spanned(start_row, col))
+ start_row++;
+ if (start_row >= nrows)
+ break;
+ int end_row = start_row;
+ while (end_row < nrows && !vline_spanned(end_row, col))
+ end_row++;
+ end_row--;
+ add_vertical_rule(start_row, end_row, col, 0);
+ start_row = end_row + 1;
+ }
+ }
+ }
+ if (flags & (BOX | ALLBOX | DOUBLEBOX)) {
+ add_vertical_rule(0, nrows - 1, 0, 0);
+ add_vertical_rule(0, nrows - 1, ncolumns, 0);
+ }
+ for (int end_row = 0; end_row < nrows; end_row++)
+ for (col = 0; col < ncolumns+1; col++)
+ if (vline[end_row][col] > 0
+ && !vline_spanned(end_row, col)
+ && (end_row == nrows - 1
+ || vline[end_row+1][col] != vline[end_row][col]
+ || vline_spanned(end_row+1, col))) {
+ int start_row;
+ for (start_row = end_row - 1;
+ start_row >= 0
+ && vline[start_row][col] == vline[end_row][col]
+ && !vline_spanned(start_row, col);
+ start_row--)
+ ;
+ start_row++;
+ add_vertical_rule(start_row, end_row, col, vline[end_row][col] > 1);
+ }
+ for (vertical_rule *p = vrule_list; p; p = p->next)
+ if (p->is_double)
+ for (int r = p->start_row; r <= p->end_row; r++) {
+ if (p->col > 0 && entry[r][p->col-1] != 0
+ && entry[r][p->col-1]->end_col == p->col-1) {
+ int is_corner = r == p->start_row || r == p->end_row;
+ entry[r][p->col-1]->note_double_vrule_on_right(is_corner);
+ }
+ if (p->col < ncolumns && entry[r][p->col] != 0
+ && entry[r][p->col]->start_col == p->col) {
+ int is_corner = r == p->start_row || r == p->end_row;
+ entry[r][p->col]->note_double_vrule_on_left(is_corner);
+ }
+ }
+}
+
+void table::define_bottom_macro()
+{
+ prints(".\\\" define bottom macro\n");
+ prints(".eo\n"
+ // protect # in macro name against eqn
+ ".ig\n"
+ ".EQ\n"
+ "delim off\n"
+ ".EN\n"
+ "..\n"
+ ".de T#\n"
+ ". if !\\n[" SUPPRESS_BOTTOM_REG "] \\{\\\n"
+ ". " REPEATED_VPT_MACRO " 0\n"
+ ". mk " SAVED_VERTICAL_POS_REG "\n");
+ if (flags & (BOX | ALLBOX | DOUBLEBOX)) {
+ prints(". if \\n[T.]&\\n[" NEED_BOTTOM_RULE_REG "] \\{\\\n");
+ print_single_hline(0);
+ prints(". \\}\n");
+ }
+ prints(". ls 1\n");
+ for (vertical_rule *p = vrule_list; p; p = p->next)
+ p->contribute_to_bottom_macro(this);
+ if (flags & DOUBLEBOX)
+ prints(". if \\n[T.] \\{\\\n"
+ ". vs " DOUBLE_LINE_SEP ">?\\n[.V]u\n"
+ "\\v'" BODY_DEPTH "'\\s[\\n[" LINESIZE_REG "]]"
+ "\\D'l \\n[TW]u 0'\\s0\n"
+ ". vs\n"
+ ". \\}\n"
+ ". if \\n[" LAST_PASSED_ROW_REG "]>=0 "
+ ".nr " TOP_REG " \\n[#T]-" DOUBLE_LINE_SEP "\n"
+ ". sp -1\n"
+ "\\v'" BODY_DEPTH "'\\s[\\n[" LINESIZE_REG "]]"
+ "\\D'l 0 |\\n[" TOP_REG "]u-1v'\\s0\n"
+ ". sp -1\n"
+ "\\v'" BODY_DEPTH "'\\h'|\\n[TW]u'\\s[\\n[" LINESIZE_REG "]]"
+ "\\D'l 0 |\\n[" TOP_REG "]u-1v'\\s0\n");
+ prints(". ls\n");
+ prints(". nr " LAST_PASSED_ROW_REG " \\n[" CURRENT_ROW_REG "]\n"
+ ". sp |\\n[" SAVED_VERTICAL_POS_REG "]u\n"
+ ". " REPEATED_VPT_MACRO " 1\n");
+ if ((flags & NOKEEP) && (flags & (BOX | DOUBLEBOX | ALLBOX)))
+ prints(". if (\\n% > \\n[" STARTING_PAGE_REG "]) \\{\\\n"
+ ". tmc \\n[.F]:\\n[.c]: warning:\n"
+ ". tmc \" boxed, unkept table does not fit on page\n"
+ ". tm1 \" \\n[" STARTING_PAGE_REG "]\n"
+ ". \\}\n");
+ prints(". \\}\n"
+ "..\n"
+ ".ig\n"
+ ".EQ\n"
+ "delim on\n"
+ ".EN\n"
+ "..\n"
+ ".ec\n");
+}
+
+// is the vertical line before column c in row r horizontally spanned?
+
+int table::vline_spanned(int r, int c)
+{
+ assert(r >= 0 && r < nrows && c >= 0 && c < ncolumns + 1);
+ return (c != 0 && c != ncolumns && entry[r][c] != 0
+ && entry[r][c]->start_col != c
+ // horizontally spanning lines don't count
+ && entry[r][c]->to_double_line_entry() == 0
+ && entry[r][c]->to_single_line_entry() == 0);
+}
+
+int table::row_begins_section(int r)
+{
+ assert(r >= 0 && r < nrows);
+ for (int i = 0; i < ncolumns; i++)
+ if (entry[r][i] && entry[r][i]->start_row != r)
+ return 0;
+ return 1;
+}
+
+int table::row_ends_section(int r)
+{
+ assert(r >= 0 && r < nrows);
+ for (int i = 0; i < ncolumns; i++)
+ if (entry[r][i] && entry[r][i]->end_row != r)
+ return 0;
+ return 1;
+}
+
+void table::do_row(int r)
+{
+ printfs(".\\\" do row %1\n", i_to_a(r));
+ if (!(flags & NOKEEP) && row_begins_section(r))
+ prints(".if \\n[" USE_KEEPS_REG "] ." KEEP_MACRO_NAME "\n");
+ bool had_line = false;
+ stuff *p;
+ for (p = stuff_list; p && p->row < r; p = p->next)
+ ;
+ for (stuff *p1 = p; p1 && p1->row == r; p1 = p1->next)
+ if (!p1->printed && (p1->is_single_line() || p1->is_double_line())) {
+ had_line = true;
+ break;
+ }
+ if (!had_line && !row_is_all_lines[r])
+ printfs("." REPEATED_MARK_MACRO " %1\n", row_top_reg(r));
+ had_line = false;
+ for (; p && p->row == r; p = p->next)
+ if (!p->printed) {
+ p->print(this);
+ if (!had_line && (p->is_single_line() || p->is_double_line())) {
+ printfs("." REPEATED_MARK_MACRO " %1\n", row_top_reg(r));
+ had_line = true;
+ }
+ }
+ // change the row *after* printing the stuff list (which might contain .TH)
+ printfs("\\*[" TRANSPARENT_STRING_NAME "].nr " CURRENT_ROW_REG " %1\n",
+ as_string(r));
+ if (!had_line && row_is_all_lines[r])
+ printfs("." REPEATED_MARK_MACRO " %1\n", row_top_reg(r));
+ // we might have had a .TH, for example, since we last tried
+ if (!(flags & NOKEEP) && row_begins_section(r))
+ prints(".if \\n[" USE_KEEPS_REG "] ." KEEP_MACRO_NAME "\n");
+ printfs(".mk %1\n", row_start_reg(r));
+ prints(".mk " BOTTOM_REG "\n"
+ "." REPEATED_VPT_MACRO " 0\n");
+ int c;
+ int row_is_blank = 1;
+ int first_start_row = r;
+ for (c = 0; c < ncolumns; c++) {
+ table_entry *e = entry[r][c];
+ if (e) {
+ if (e->end_row == r) {
+ e->do_depth();
+ if (e->start_row < first_start_row)
+ first_start_row = e->start_row;
+ row_is_blank = 0;
+ }
+ c = e->end_col;
+ }
+ }
+ if (row_is_blank)
+ prints(".nr " BOTTOM_REG " +1v\n");
+ if (row_is_all_lines[r]) {
+ prints(".vs " LINE_SEP);
+ if (row_is_all_lines[r] == 2)
+ prints("+" DOUBLE_LINE_SEP);
+ prints(">?\\n[.V]u\n.ls 1\n");
+ prints("\\&");
+ prints("\\v'" BODY_DEPTH);
+ if (row_is_all_lines[r] == 2)
+ prints("-" HALF_DOUBLE_LINE_SEP);
+ prints("'");
+ for (c = 0; c < ncolumns; c++) {
+ table_entry *e = entry[r][c];
+ if (e) {
+ if (e->end_row == e->start_row)
+ e->to_simple_entry()->simple_print(1);
+ c = e->end_col;
+ }
+ }
+ prints("\n");
+ prints(".ls\n"
+ ".vs\n");
+ prints(".nr " BOTTOM_REG " \\n[" BOTTOM_REG "]>?\\n[.d]\n");
+ printfs(".sp |\\n[%1]u\n", row_start_reg(r));
+ }
+ for (int i = row_is_all_lines[r] ? r - 1 : r;
+ i >= first_start_row;
+ i--) {
+ simple_entry *first = 0;
+ for (c = 0; c < ncolumns; c++) {
+ table_entry *e = entry[r][c];
+ if (e) {
+ if (e->end_row == r && e->start_row == i) {
+ simple_entry *simple = e->to_simple_entry();
+ if (simple) {
+ if (!first) {
+ prints(".ta");
+ first = simple;
+ }
+ simple->add_tab();
+ }
+ }
+ c = e->end_col;
+ }
+ }
+ if (first) {
+ prints('\n');
+ first->position_vertically();
+ first->set_location();
+ prints("\\&");
+ first->simple_print(0);
+ for (c = first->end_col + 1; c < ncolumns; c++) {
+ table_entry *e = entry[r][c];
+ if (e) {
+ if (e->end_row == r && e->start_row == i) {
+ simple_entry *simple = e->to_simple_entry();
+ if (simple) {
+ if (e->end_row != e->start_row) {
+ prints('\n');
+ simple->position_vertically();
+ prints("\\&");
+ }
+ simple->simple_print(0);
+ }
+ }
+ c = e->end_col;
+ }
+ }
+ prints('\n');
+ prints(".nr " BOTTOM_REG " \\n[" BOTTOM_REG "]>?\\n[.d]\n");
+ printfs(".sp |\\n[%1]u\n", row_start_reg(r));
+ }
+ }
+ for (c = 0; c < ncolumns; c++) {
+ table_entry *e = entry[r][c];
+ if (e) {
+ if (e->end_row == r && e->to_simple_entry() == 0) {
+ e->position_vertically();
+ e->print();
+ prints(".nr " BOTTOM_REG " \\n[" BOTTOM_REG "]>?\\n[.d]\n");
+ printfs(".sp |\\n[%1]u\n", row_start_reg(r));
+ }
+ c = e->end_col;
+ }
+ }
+ prints("." REPEATED_VPT_MACRO " 1\n"
+ ".sp |\\n[" BOTTOM_REG "]u\n"
+ "\\*[" TRANSPARENT_STRING_NAME "].nr " NEED_BOTTOM_RULE_REG " 1\n");
+ if (r != nrows - 1 && (flags & ALLBOX)) {
+ print_single_hline(r + 1);
+ prints("\\*[" TRANSPARENT_STRING_NAME "].nr " NEED_BOTTOM_RULE_REG " 0\n");
+ }
+ if (r != nrows - 1) {
+ if (p && p->row == r + 1
+ && (p->is_single_line() || p->is_double_line())) {
+ p->print(this);
+ prints("\\*[" TRANSPARENT_STRING_NAME "].nr " NEED_BOTTOM_RULE_REG
+ " 0\n");
+ }
+ int printed_one = 0;
+ for (vertical_rule *vr = vrule_list; vr; vr = vr->next)
+ if (vr->end_row == r) {
+ if (!printed_one) {
+ prints("." REPEATED_VPT_MACRO " 0\n");
+ printed_one = 1;
+ }
+ vr->print();
+ }
+ if (printed_one)
+ prints("." REPEATED_VPT_MACRO " 1\n");
+ if (!(flags & NOKEEP) && row_ends_section(r))
+ prints(".if \\n[" USE_KEEPS_REG "] ." RELEASE_MACRO_NAME "\n");
+ }
+}
+
+void table::do_top()
+{
+ prints(".\\\" do top\n");
+ prints(".ss \\n[" SAVED_INTER_WORD_SPACE_SIZE "]\n");
+ prints(".fc \002\003\n");
+ if (flags & (BOX | DOUBLEBOX | ALLBOX))
+ prints(".nr " IS_BOXED_REG " 1\n");
+ else
+ prints(".nr " IS_BOXED_REG " 0\n");
+ if (!(flags & NOKEEP) && (flags & (BOX | DOUBLEBOX | ALLBOX)))
+ prints("." TABLE_KEEP_MACRO_NAME "\n");
+ if (flags & DOUBLEBOX) {
+ prints(".ls 1\n"
+ ".vs " LINE_SEP ">?\\n[.V]u\n"
+ "\\v'" BODY_DEPTH "'\\s[\\n[" LINESIZE_REG "]]\\D'l \\n[TW]u 0'\\s0\n"
+ ".vs\n"
+ "." REPEATED_MARK_MACRO " " TOP_REG "\n"
+ ".vs " DOUBLE_LINE_SEP ">?\\n[.V]u\n");
+ printfs("\\v'" BODY_DEPTH "'"
+ "\\s[\\n[" LINESIZE_REG "]]"
+ "\\h'\\n[%1]u'"
+ "\\D'l |\\n[%2]u 0'"
+ "\\s0"
+ "\n",
+ column_divide_reg(0),
+ column_divide_reg(ncolumns));
+ prints(".ls\n"
+ ".vs\n");
+ }
+ else if (flags & (ALLBOX | BOX))
+ print_single_hline(0);
+ // On terminal devices, a vertical rule on the first row of the table
+ // will stick out 1v above it if it the table is unboxed or lacks a
+ // horizontal rule on the first row. This is necessary for grotty's
+ // rule intersection detection. We must make room for it so that the
+ // vertical rule is not drawn above the top of the page.
+ else if ((flags & HAS_TOP_VLINE) && !(flags & HAS_TOP_HLINE))
+ prints(".if n .sp\n");
+ prints(".nr " STARTING_PAGE_REG " \\n%\n");
+ //printfs(".mk %1\n", row_top_reg(0));
+}
+
+void table::do_bottom()
+{
+ prints(".\\\" do bottom\n");
+ // print stuff after last row
+ for (stuff *p = stuff_list; p; p = p->next)
+ if (p->row > nrows - 1)
+ p->print(this);
+ if (!(flags & NOKEEP))
+ prints(".if \\n[" USE_KEEPS_REG "] ." RELEASE_MACRO_NAME "\n");
+ printfs(".mk %1\n", row_top_reg(nrows));
+ prints(".nr " NEED_BOTTOM_RULE_REG " 1\n"
+ ".nr T. 1\n"
+ // protect # in macro name against eqn
+ ".ig\n"
+ ".EQ\n"
+ "delim off\n"
+ ".EN\n"
+ "..\n"
+ ".T#\n"
+ ".ig\n"
+ ".EQ\n"
+ "delim on\n"
+ ".EN\n"
+ "..\n");
+ if (!(flags & NOKEEP) && (flags & (BOX | DOUBLEBOX | ALLBOX)))
+ prints("." TABLE_RELEASE_MACRO_NAME "\n");
+ if (flags & DOUBLEBOX)
+ prints(".sp " DOUBLE_LINE_SEP "\n");
+ // Horizontal box lines take up an entire row on nroff devices (maybe
+ // a half-row if we ever support [emulators of] devices like the
+ // Teletype Model 37 with half-line motions).
+ if (flags & (BOX | DOUBLEBOX | ALLBOX))
+ prints(".if n .sp\n");
+ // Space again for the doublebox option, until we can draw that more
+ // attractively; see Savannah #43637.
+ if (flags & DOUBLEBOX)
+ prints(".if n .sp\n");
+ prints("." RESET_MACRO_NAME "\n"
+ ".nn \\n[" SAVED_NUMBERING_SUPPRESSION_COUNT "]\n"
+ ".ie \\n[" SAVED_NUMBERING_LINENO "] "
+ ".nm \\n[" SAVED_NUMBERING_LINENO "]\n"
+ ".el .nm\n"
+ ".fc\n"
+ ".cp \\n(" COMPATIBLE_REG "\n");
+}
+
+int table::get_nrows()
+{
+ return nrows;
+}
+
+const char *last_filename = 0;
+
+void set_troff_location(const char *fn, int ln)
+{
+ if (!location_force_filename && last_filename != 0
+ && strcmp(fn, last_filename) == 0)
+ printfs(".lf %1\n", as_string(ln));
+ else {
+ string filename(fn);
+ filename += '\0';
+ normalize_for_lf(filename);
+ printfs(".lf %1 %2\n", as_string(ln), filename.contents());
+ last_filename = fn;
+ location_force_filename = 0;
+ }
+}
+
+void printfs(const char *s, const string &arg1, const string &arg2,
+ const string &arg3, const string &arg4, const string &arg5)
+{
+ if (s) {
+ char c;
+ while ((c = *s++) != '\0') {
+ if (c == '%') {
+ switch (*s++) {
+ case '1':
+ prints(arg1);
+ break;
+ case '2':
+ prints(arg2);
+ break;
+ case '3':
+ prints(arg3);
+ break;
+ case '4':
+ prints(arg4);
+ break;
+ case '5':
+ prints(arg5);
+ break;
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ break;
+ case '%':
+ prints('%');
+ break;
+ default:
+ assert(0 == "printfs format character not in [1-9%]");
+ }
+ }
+ else
+ prints(c);
+ }
+ }
+}
+
+// Local Variables:
+// fill-column: 72
+// mode: C++
+// End:
+// vim: set cindent noexpandtab shiftwidth=2 textwidth=72:
diff --git a/src/preproc/tbl/table.h b/src/preproc/tbl/table.h
new file mode 100644
index 0000000..62346fa
--- /dev/null
+++ b/src/preproc/tbl/table.h
@@ -0,0 +1,179 @@
+/* Copyright (C) 1989-2020 Free Software Foundation, Inc.
+ Written by James Clark (jjc@jclark.com)
+
+This file is part of groff.
+
+groff is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+groff is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "lib.h"
+
+#include <stdlib.h>
+#include <ctype.h>
+#include <errno.h>
+
+#include "cset.h"
+#include "cmap.h"
+#include "stringclass.h"
+#include "errarg.h"
+#include "error.h"
+#include "lf.h"
+
+// PREFIX and PREFIX_CHAR must be the same.
+#define PREFIX "3"
+#define PREFIX_CHAR '3'
+
+// LEADER and LEADER_CHAR must be the same.
+#define LEADER "a"
+#define LEADER_CHAR 'a'
+
+struct inc_number {
+ short inc;
+ short val;
+};
+
+struct entry_modifier {
+ inc_number point_size;
+ inc_number vertical_spacing;
+ string font;
+ string macro;
+ enum { CENTER, TOP, BOTTOM } vertical_alignment;
+ char zero_width;
+ char stagger;
+
+ entry_modifier();
+ ~entry_modifier();
+};
+
+enum format_type {
+ FORMAT_LEFT,
+ FORMAT_CENTER,
+ FORMAT_RIGHT,
+ FORMAT_NUMERIC,
+ FORMAT_ALPHABETIC,
+ FORMAT_SPAN,
+ FORMAT_VSPAN,
+ FORMAT_HLINE,
+ FORMAT_DOUBLE_HLINE
+};
+
+struct entry_format : public entry_modifier {
+ format_type type;
+
+ entry_format(format_type);
+ entry_format();
+ void debug_print() const;
+};
+
+class table_entry;
+struct horizontal_span;
+struct stuff;
+struct vertical_rule;
+
+class table {
+ int nrows;
+ int ncolumns;
+ int linesize;
+ char delim[2];
+ char decimal_point_char;
+ vertical_rule *vrule_list;
+ stuff *stuff_list;
+ horizontal_span *span_list;
+ table_entry *entry_list;
+ table_entry **entry_list_tailp;
+ table_entry ***entry;
+ char **vline;
+ char *row_is_all_lines;
+ string *minimum_width;
+ int *column_separation;
+ char *equal;
+ int left_separation; // from a vertical rule or box border, in ens
+ int right_separation; // from a vertical rule or box border, in ens
+ int total_separation;
+ int allocated_rows;
+ void build_span_list();
+ void compute_overall_width();
+ void do_hspan(int r, int c);
+ void do_vspan(int r, int c);
+ void allocate(int r);
+ void compute_widths();
+ void divide_span(int, int);
+ void sum_columns(int, int, int);
+ void compute_total_separation();
+ void compute_separation_factor();
+ void compute_column_positions();
+ void do_row(int);
+ void init_output();
+ void add_stuff(stuff *);
+ void do_top();
+ void do_bottom();
+ void do_vertical_rules();
+ void build_vrule_list();
+ void add_vertical_rule(int, int, int, int);
+ void define_bottom_macro();
+ int vline_spanned(int r, int c);
+ int row_begins_section(int);
+ int row_ends_section(int);
+ void make_columns_equal();
+ void compute_vrule_top_adjust(int, int, string &);
+ void compute_vrule_bot_adjust(int, int, string &);
+ void determine_row_type();
+ int count_expand_columns();
+public:
+ unsigned flags;
+ enum {
+ CENTER = 0x00000001,
+ EXPAND = 0x00000002,
+ BOX = 0x00000004,
+ ALLBOX = 0x00000008,
+ DOUBLEBOX = 0x00000010,
+ NOKEEP = 0x00000020,
+ NOSPACES = 0x00000040,
+ NOWARN = 0x00000080,
+ // The next few properties help manage nroff mode output.
+ HAS_TOP_VLINE = 0x00000100,
+ HAS_TOP_HLINE = 0x00000200,
+ GAP_EXPAND = 0x00000400,
+ EXPERIMENTAL = 0x80000000 // undocumented
+ };
+ char *expand;
+ table(int nc, unsigned flags, int linesize, char decimal_point_char);
+ ~table();
+
+ void add_text_line(int r, const string &, const char *, int);
+ void add_single_hline(int r);
+ void add_double_hline(int r);
+ void add_entry(int r, int c, const string &, const entry_format *,
+ const char *, int lineno);
+ void add_vlines(int r, const char *);
+ void check();
+ void print();
+ void set_minimum_width(int c, const string &w);
+ void set_column_separation(int c, int n);
+ void set_equal_column(int c);
+ void set_expand_column(int c);
+ void set_delim(char c1, char c2);
+ void print_single_hline(int r);
+ void print_double_hline(int r);
+ int get_nrows();
+};
+
+void set_troff_location(const char *, int);
+
+extern int compatible_flag;
+
+// Local Variables:
+// fill-column: 72
+// mode: C++
+// End:
+// vim: set cindent noexpandtab shiftwidth=2 textwidth=72:
diff --git a/src/preproc/tbl/tbl.1.man b/src/preproc/tbl/tbl.1.man
new file mode 100644
index 0000000..4c9a98a
--- /dev/null
+++ b/src/preproc/tbl/tbl.1.man
@@ -0,0 +1,2018 @@
+'\" t
+.TH @g@tbl @MAN1EXT@ "@MDATE@" "groff @VERSION@"
+.SH Name
+@g@tbl \- prepare tables for
+.I groff
+documents
+.
+.
+.\" ====================================================================
+.\" Legal Terms
+.\" ====================================================================
+.\"
+.\" Copyright (C) 1989-2023 Free Software Foundation, Inc.
+.\"
+.\" Permission is granted to make and distribute verbatim copies of this
+.\" manual provided the copyright notice and this permission notice are
+.\" preserved on all copies.
+.\"
+.\" Permission is granted to copy and distribute modified versions of
+.\" this manual under the conditions for verbatim copying, provided that
+.\" the entire resulting derived work is distributed under the terms of
+.\" a permission notice identical to this one.
+.\"
+.\" Permission is granted to copy and distribute translations of this
+.\" manual into another language, under the above conditions for
+.\" modified versions, except that this permission notice may be
+.\" included in translations approved by the Free Software Foundation
+.\" instead of in the original English.
+.
+.
+.\" Save and disable compatibility mode (for, e.g., Solaris 10/11).
+.do nr *groff_tbl_1_man_C \n[.cp]
+.cp 0
+.
+.\" Define fallback for groff 1.23's MR macro if the system lacks it.
+.nr do-fallback 0
+.if !\n(.f .nr do-fallback 1 \" mandoc
+.if \n(.g .if !d MR .nr do-fallback 1 \" older groff
+.if !\n(.g .nr do-fallback 1 \" non-groff *roff
+.if \n[do-fallback] \{\
+. de MR
+. ie \\n(.$=1 \
+. I \%\\$1
+. el \
+. IR \%\\$1 (\\$2)\\$3
+. .
+.\}
+.rr do-fallback
+.
+.
+.\" ====================================================================
+.SH Synopsis
+.\" ====================================================================
+.
+.SY @g@tbl
+.RB [ \-C ]
+.RI [ file\~ .\|.\|.]
+.YS
+.
+.
+.SY @g@tbl
+.B \-\-help
+.YS
+.
+.
+.SY @g@tbl
+.B \-v
+.
+.SY @g@tbl
+.B \-\-version
+.YS
+.
+.
+.\" ====================================================================
+.SH Description
+.\" ====================================================================
+.
+The GNU implementation of
+.I tbl \" generic
+is part of the
+.MR groff @MAN1EXT@
+document formatting system.
+.
+.I @g@tbl
+is a
+.MR @g@troff @MAN1EXT@
+preprocessor that translates descriptions of tables embedded in
+.MR roff @MAN7EXT@
+input files into the language understood by
+.IR @g@troff .
+.
+It copies the contents of each
+.I file
+to the standard output stream,
+except that lines between
+.B .TS
+and
+.B .TE
+are interpreted as table descriptions.
+.
+While GNU
+.IR tbl 's \" GNU
+input syntax is highly compatible with AT&T
+.IR tbl , \" AT&T
+the output GNU
+.I tbl \" GNU
+produces cannot be processed by AT&T
+.IR troff ; \" AT&T
+GNU
+.I troff \" GNU
+(or a
+.I troff \" generic
+implementing any GNU extensions employed)
+must be used.
+.
+Normally,
+.I @g@tbl
+is not executed directly by the user,
+but invoked by specifying the
+.B \-t
+option to
+.MR groff @MAN1EXT@ .
+.
+If no
+.I file
+operands are given on the command line,
+or if
+.I file
+is
+.RB \[lq] \- \[rq],
+.I @g@tbl
+reads the standard input stream.
+.
+.
+.\" ====================================================================
+.SS Overview
+.\" ====================================================================
+.
+.I @g@tbl
+expects to find table descriptions between input lines that begin with
+.B .TS
+(table start)
+and
+.B .TE
+(table end).
+.
+Each such
+.I table region
+encloses one or more table descriptions.
+.
+Within a table region,
+table descriptions beyond the first must each be preceded
+by an input line beginning with
+.BR .T& .
+.
+This mechanism does not start a new table region;
+all table descriptions are treated as part of their
+.BR .TS / .TE
+enclosure,
+even if they are boxed or have column headings that repeat on subsequent
+pages
+(see below).
+.
+.
+.P
+(Experienced
+.I roff
+users should observe that
+.I @g@tbl
+is not a
+.I roff
+language interpreter:
+the default control character must be used,
+and no spaces or tabs are permitted between the control character and
+the macro name.
+.
+These
+.I @g@tbl
+input tokens remain as-is in the output,
+where they become ordinary macro calls.
+.
+Macro packages often define
+.BR TS ,
+.BR T& ,
+and
+.B TE
+macros to handle issues of table placement on the page.
+.
+.I @g@tbl
+produces
+.I groff
+code to define these macros as empty if their definitions do not exist
+when the formatter encounters a table region.)
+.
+.
+.P
+Each table region may begin with
+.I region options,
+and must contain one or more
+.I table definitions;
+each table definition contains a
+.I format specification
+followed by one or more input lines (rows) of
+.I entries.
+.
+These entries comprise the
+.I table data.
+.
+.
+.
+.\" ====================================================================
+.SS "Region options"
+.\" ====================================================================
+.
+The line immediately following the
+.B .TS
+token may specify region options,
+keywords that influence the interpretation or rendering of the region as
+a whole or all table entries within it indiscriminately.
+.
+They must be separated by commas,
+spaces,
+or tabs.
+.
+Those that require a parenthesized argument permit spaces and tabs
+between the option's name and the opening parenthesis.
+.
+Options accumulate and cannot be unset within a region once declared;
+if an option that takes a parameter is repeated,
+the last occurrence controls.
+.
+If present,
+the set of region options must be terminated with a semicolon
+.RB ( ; ).
+.
+.
+.P
+Any of the
+.BR allbox ,
+.BR box ,
+.BR doublebox ,
+.BR frame ,
+and
+.B doubleframe
+region options makes a table \[lq]boxed\[rq] for the purpose of later
+discussion.
+.
+.
+.TP
+.B allbox
+Enclose each table entry in a box;
+implies
+.BR box .
+.
+.
+.TP
+.B box
+Enclose the entire table region in a box.
+.
+As a GNU extension,
+the alternative option name
+.B frame
+is also recognized.
+.
+.
+.TP
+.B center
+Center the table region with respect to the current indentation and line
+length;
+the default is to left-align it.
+.
+As a GNU extension,
+the alternative option name
+.B centre
+is also recognized.
+.
+.
+.TP
+.BI decimalpoint( c )
+Recognize character
+.I c
+as the decimal separator in columns using the
+.B N
+(numeric) classifier
+(see subsection \[lq]Column classifiers\[rq] below).
+.
+This is a GNU extension.
+.
+.
+.TP
+.BI delim( xy )
+Recognize characters
+.I x
+.RI and\~ y
+as start and end delimiters,
+respectively,
+for
+.MR @g@eqn @MAN1EXT@
+input,
+and ignore input between them.
+.
+.I x
+.RI and\~ y
+need not be distinct.
+.
+.
+.TP
+.B doublebox
+Enclose the entire table region in a double box;
+implies
+.BR box .
+.
+As a GNU extension,
+the alternative option name
+.B \%doubleframe
+is also recognized.
+.
+.
+.TP
+.B expand
+Spread the table horizontally to fill the available space
+(line length minus indentation)
+by increasing column separation.
+.
+Ordinarily,
+a table is made only as wide as necessary to accommodate the widths of
+its entries and its column separations
+(whether specified or default).
+.
+When
+.B expand
+applies to a table that exceeds the available horizontal space,
+column separation is reduced as far as necessary
+(even to zero).
+.
+.I @g@tbl
+produces
+.I groff
+input that issues a diagnostic if such compression occurs.
+.
+The column modifier
+.B x
+(see below)
+overrides this option.
+.
+.
+.TP
+.BI linesize( n )
+Draw lines or rules
+(e.g.,
+from
+.BR box )
+with a thickness of
+.IR n \~points.
+.
+The default is the current type size when the region begins.
+.
+This option is ignored on terminal devices.
+.
+.
+.TP
+.B nokeep
+Don't use
+.I roff
+diversions to manage page breaks.
+.
+Normally,
+.I @g@tbl
+employs them to avoid breaking a page within a table row.
+.
+This usage can sometimes interact badly with macro packages' own use of
+diversions\[em]when footnotes,
+for example,
+are employed.
+.
+This is a GNU extension.
+.
+.
+.TP
+.B nospaces
+Ignore leading and trailing spaces in table entries.
+.
+This is a GNU extension.
+.
+.
+.TP
+.B nowarn
+Suppress diagnostic messages produced at document formatting time when
+the line or page lengths are inadequate to contain a table row.
+.
+This is a GNU extension.
+.
+.
+.\" TODO: How about "right"? (and "left" for symmetry)
+.TP
+.BI tab( c )
+Use the character
+.I c
+instead of a tab to separate entries in a row of table data.
+.
+.
+.\" ====================================================================
+.SS "Table format specification"
+.\" ====================================================================
+.
+The table format specification is mandatory:
+it determines the number of columns in the table and directs how the
+entries within it are to be typeset.
+.
+The format specification is a series of column
+.I descriptors.
+.
+Each descriptor encodes a
+.I classifier
+followed by zero or more
+.I modifiers.
+.
+Classifiers are letters
+(recognized case-insensitively)
+or punctuation symbols;
+modifiers consist of or begin with letters or numerals.
+.
+Spaces,
+tabs,
+newlines,
+and commas separate descriptors.
+.
+Newlines and commas are special;
+they apply the descriptors following them to a subsequent row of the
+table.
+.
+(This enables column headings to be centered or emboldened while the
+table entries for the data are not,
+for instance.)
+.
+We term the resulting group of column descriptors a
+.I row definition.
+.
+Within a row definition,
+separation between column descriptors
+(by spaces or tabs)
+is often optional;
+only some modifiers,
+described below,
+make separation necessary.
+.
+.
+.P
+Each column descriptor begins with a mandatory
+.I classifier,
+a character that selects from one of several arrangements.
+.
+Some determine the positioning of table entries within a rectangular
+cell:
+centered,
+left-aligned,
+numeric
+(aligned to a configurable decimal separator),
+and so on.
+.
+Others perform special operations like drawing lines or spanning entries
+from adjacent cells in the table.
+.
+Except for
+.RB \[lq] | \[rq],
+any classifier can be followed by one or more
+.I modifiers;
+some of these accept an argument,
+which in GNU
+.I tbl \" GNU
+can be parenthesized.
+.\" AT&T tbl allowed parentheses only after 'w'.
+.\" TODO: Accept parentheses after 'p' and 'v'.
+.
+Modifiers select fonts,
+set the type size,
+.\"define the column width,
+.\"adjust inter-column spacing, \" slack text for window/orphan control
+and perform other tasks described below.
+.
+.
+.P
+The format specification can occupy multiple input lines,
+but must conclude with a dot
+.RB \[lq] .\& \[rq]
+followed by a newline.
+.
+Each row definition is applied in turn to one row of the table.
+.
+The last row definition is applied to rows of table data in excess of
+the row definitions.
+.
+.
+.P
+For clarity in this document's examples,
+we shall write classifiers in uppercase and modifiers in lowercase.
+.
+Thus,
+.RB \[lq] CbCb,LR.\& \[rq]
+defines two rows of two columns.
+.
+The first row's entries are centered and boldfaced;
+the second and any further rows' first and second columns are left- and
+right-aligned,
+respectively.
+.
+.\" slack text for window/orphan control
+.\"If more rows of entries are added to the table data,
+.\"they reuse the row definition
+.\".RB \[lq] LR \[rq].
+.
+.
+.P
+The row definition with the most column descriptors determines the
+number of columns in the table;
+any row definition with fewer is implicitly extended on the right-hand
+side with
+.B L
+classifiers as many times as necessary to make the table rectangular.
+.
+.
+.\" ====================================================================
+.SS "Column classifiers"
+.\" ====================================================================
+.
+The
+.BR L ,
+.BR R ,
+and
+.B C
+classifiers are the easiest to understand and use.
+.
+.
+.TP
+.BR A ,\~ a
+Center longest entry in this column,
+left-align remaining entries in the column with respect to the centered
+entry,
+then indent all entries by one en.
+.
+Such \[lq]alphabetic\[rq] entries
+(hence the name of the classifier)
+can be used in the same column as
+.BR L -classified
+entries,
+as in
+.RB \[lq] LL,AR.\& \[rq].
+.
+The
+.B A
+entries are often termed \[lq]sub-columns\[rq] due to their indentation.
+.
+.
+.TP
+.BR C ,\~ c
+Center entry within the column.
+.
+.
+.TP
+.BR L ,\~ l
+Left-align entry within the column.
+.
+.
+.TP
+.BR N ,\~ n
+Numerically align entry in the column.
+.
+.I @g@tbl
+aligns columns of numbers vertically at the units place.
+.
+If multiple decimal separators are adjacent to a digit,
+it uses the rightmost one for vertical alignment.
+.
+If there is no decimal separator,
+the rightmost digit is used for vertical alignment;
+otherwise,
+.I @g@tbl
+centers the entry within the column.
+.
+The
+.I roff
+dummy character
+.B \[rs]&
+in an entry marks the glyph preceding it
+(if any)
+as the units place;
+if multiple instances occur in the data,
+the leftmost is used for alignment.
+.
+.
+.IP
+If
+.BR N -classified
+entries share a column with
+.B L
+or
+.BR R \~entries,
+.I @g@tbl
+centers the widest
+.BR N \~entry
+with respect to the widest
+.B L
+or
+.BR R \~entry,
+preserving the alignment of
+.BR N \~entries
+with respect to each other.
+.
+.
+.IP
+The appearance of
+.I @g@eqn
+equations
+within
+.BR N -classified
+columns
+can be troublesome due to the foregoing textual scan for a decimal
+separator.
+.
+Use the
+.B \%delim
+region option to make
+.I @g@tbl
+ignore the data within
+.I eqn
+delimiters for that purpose.
+.
+.
+.TP
+.BR R ,\~ r
+Right-align entry within the column.
+.
+.
+.TP
+.BR S ,\~ s
+Span previous entry on the left into this column.
+.
+.
+.TP
+.B \[ha]
+Span entry in the same column from the previous row into this row.
+.
+.
+.TP
+.BR _ ,\~ \-
+Replace table entry with a horizontal rule.
+.
+An empty table entry is expected to correspond to this classifier;
+if data are found there,
+.I @g@tbl
+issues a diagnostic message.
+.
+.
+.TP
+.B =
+Replace table entry with a double horizontal rule.
+.
+An empty table entry is expected to correspond to this classifier;
+if data are found there,
+.I @g@tbl
+issues a diagnostic message.
+.
+.
+.TP
+.B |
+Place a vertical rule (line) on the corresponding row of the table
+(if two of these are adjacent,
+a double vertical rule).
+.
+This classifier does not contribute to the column count and no table
+entries correspond to it.
+.
+A
+.B |
+to the left of the first column descriptor or to the right of the last
+one produces a vertical rule at the edge of the table;
+these are redundant
+(and ignored)
+in boxed tables.
+.
+.
+.P
+To change the table format within a
+.I @g@tbl
+region,
+use the
+.B .T&
+token at the start of a line.
+.
+It is followed by a format specification and table data,
+but
+.I not
+region options.
+.
+The quantity of columns in a new table format thus introduced cannot
+increase relative to the previous table format;
+in that case,
+you must end the table region and start another.
+.
+If that will not serve because the region uses box options or the
+columns align in an undesirable manner,
+you must design the initial table format specification to include the
+maximum quantity of columns required,
+and use the
+.B S
+horizontal spanning classifier where necessary to achieve the desired
+columnar alignment.
+.
+.
+.P
+Attempting to horizontally span in the first column or vertically span
+on the first row is an error.
+.
+Non-rectangular span areas are also not supported.
+.
+.
+.\" ====================================================================
+.SS "Column modifiers"
+.\" ====================================================================
+.
+Any number of modifiers can follow a column classifier.
+.
+Arguments to modifiers,
+where accepted,
+are case-sensitive.
+.
+If the same modifier is applied to a column specifier more than once,
+or if conflicting modifiers are applied,
+only the last occurrence has effect.
+.
+The
+.RB modifier\~ x
+is mutually exclusive with
+.B e
+.RB and\~ w ,
+but
+.B e
+is not mutually exclusive
+.RB with\~ w ;
+if these are used in combination,
+.BR x \~unsets
+both
+.B e
+.RB and\~ w ,
+while either
+.B e
+or
+.B w
+.RB overrides\~ x .
+.
+.
+.br
+.ne 4v \" Keep next two tagged paragraphs together.
+.TP
+.BR b ,\~ B
+Typeset entry in boldface,
+abbreviating
+.BR f(B) .
+.
+.
+.TP
+.BR d ,\~ D
+Align a vertically spanned table entry to the bottom
+(\[lq]down\[rq]),
+instead of the center,
+of its range.
+.
+This is a GNU extension.
+.
+.
+.TP
+.BR e ,\~ E
+Equalize the widths of columns with this modifier.
+.
+The column with the largest width controls.
+.
+This modifier sets the default line length used in a text block.
+.
+.
+.TP
+.BR f ,\~ F
+Select the typeface for the table entry.
+.
+This modifier must be followed by a font or style name
+(one or two characters not starting with a digit),
+font mounting position
+(a single digit),
+or a name or mounting position of any length in parentheses.
+.
+The last form is a GNU extension.
+.
+(The parameter corresponds to that accepted by the
+.I troff \" generic
+.B ft
+request.)
+.
+A one-character argument not in parentheses must be separated by one or
+more spaces or tabs from what follows.
+.
+.
+.TP
+.BR i ,\~ I
+Typeset entry in an oblique or italic face,
+abbreviating
+.BR f(I) .
+.
+.
+.TP
+.BR m ,\~ M
+Call a
+.I groff
+macro before typesetting a text block
+(see subsection \[lq]Text blocks\[rq] below).
+.
+This is a GNU extension.
+.
+This modifier must be followed by a macro name of one or two characters
+or a name of any length in parentheses.
+.
+A one-character macro name not in parentheses must be separated by one
+or more spaces or tabs from what follows.
+.
+The named macro must be defined before the table region containing this
+column modifier is encountered.
+.
+The macro should contain only simple
+.I groff
+requests to change text formatting,
+like adjustment or hyphenation.
+.
+The macro is called
+.I after
+the column modifiers
+.BR b ,
+.BR f ,
+.BR i ,
+.BR p ,
+and
+.B v
+take effect;
+it can thus override other column modifiers.
+.
+.
+.TP
+.BR p ,\~ P
+Set the type size for the table entry.
+.
+This modifier must be followed by an
+.RI integer\~ n
+with an optional leading sign.
+.
+If unsigned,
+the type size is set to
+.IR n \~scaled
+points.
+.
+Otherwise,
+the type size is incremented or decremented per the sign by
+.IR n \~scaled
+points.
+.
+The use of a signed multi-digit number is a GNU extension.
+.
+(The parameter corresponds to that accepted by the
+.I troff \" generic
+.B ps
+request.)
+.
+If a type size modifier is followed by a column separation modifier
+(see below),
+they must be separated by at least one space or tab.
+.\" TODO: Allow parentheses so scaling units and fractional values can
+.\" be used?
+.
+.
+.TP
+.BR t ,\~ T
+Align a vertically spanned table entry to the top,
+instead of the center,
+of its range.
+.
+.
+.TP
+.BR u ,\~ U
+Move the column up one half-line,
+\[lq]staggering\[rq] the rows.
+.
+This is a GNU extension.
+.
+.
+.TP
+.BR v ,\~ V
+Set the vertical spacing to be used in a text block.
+.
+This modifier must be followed by an
+.RI integer\~ n
+with an optional leading sign.
+.
+If unsigned,
+the vertical spacing is set to
+.IR n\~ points.
+.
+Otherwise,
+the vertical spacing is incremented or decremented per the sign by
+.IR n\~ points.
+.
+The use of a signed multi-digit number is a GNU extension.
+.
+(This parameter corresponds to that accepted by the
+.I troff \" generic
+.B vs
+request.)
+.
+If a vertical spacing modifier is followed by a column separation
+modifier
+(see below),
+they must be separated by at least one space or tab.
+.\" TODO: Allow parentheses so scaling units and fractional values can
+.\" be used?
+.
+.
+.TP
+.BR w ,\~ W
+Set the column's minimum width.
+.
+This modifier must be followed by a number,
+which is either a unitless integer,
+or a
+.I roff
+horizontal measurement in parentheses.
+.
+Parentheses are required if the width is to be followed immediately by
+an explicit column separation
+(alternatively,
+follow the width with one or more spaces or tabs).
+.
+If no unit is specified,
+ens are assumed.
+.
+This modifier sets the default line length used in a text block.
+.
+.
+.TP
+.BR x ,\~ X
+Expand the column.
+.
+After computing the column widths,
+distribute any remaining line length evenly over all columns bearing
+this modifier.
+.
+Applying the
+.BR x \~modifier
+to more than one column is a GNU extension.
+.\" 'x' wasn't documented at all in Lesk 1979.
+.
+This modifier sets the default line length used in a text block.
+.
+.
+.TP
+.BR z ,\~ Z
+Ignore the table entries corresponding to this column for width
+calculation purposes;
+that is,
+compute the column's width using only the information in its descriptor.
+.
+.
+.TP
+.I n
+A numeric suffix on a column descriptor sets the separation distance
+(in ens)
+from the succeeding column;
+the default separation is
+.BR 3n .
+.
+This separation is
+proportionally multiplied if the
+.B expand
+region option is in effect;
+in the case of tables wider than the output line length,
+this separation might be zero.
+.
+A negative separation cannot be specified.
+.
+A separation amount after the last column in a row is nonsensical and
+provokes a diagnostic from
+.IR @g@tbl .
+.
+.
+.\" ====================================================================
+.SS "Table data"
+.\" ====================================================================
+.
+The table data come after the format specification.
+.
+Each input line corresponds to a table row,
+except that a backslash at the end of a line of table data continues an
+entry on the next input line.
+.
+(Text blocks,
+discussed below,
+also spread table entries across multiple input lines.)
+.
+Table entries within a row are separated in the input by a tab character
+by default;
+see the
+.B tab
+region option above.
+.
+Excess entries in a row of table data
+(those that have no corresponding column descriptor,
+not even an implicit one arising from rectangularization of the table)
+are discarded with a diagnostic message.
+.
+.I roff
+control lines are accepted between rows of table data and within text
+blocks.
+.
+If you wish to visibly mark an empty table entry in the document source,
+populate it with the
+.B \[rs]&
+.I roff
+dummy character.
+.
+The table data are interrupted by a line consisting of the
+.B .T&
+input token,
+and conclude with the line
+.BR .TE .
+.
+.
+.P
+Ordinarily,
+a table entry is typeset rigidly.
+.
+It is not filled,
+broken,
+hyphenated,
+adjusted,
+or populated with additional inter-sentence space.
+.
+.I @g@tbl
+instructs the formatter to measure each table entry as it occurs in the
+input,
+updating the width required by its corresponding column.
+.
+If the
+.B z
+modifier applies to the column,
+this measurement is ignored;
+if
+.B w
+applies and its argument is larger than this width,
+that argument is used instead.
+.
+In contrast to conventional
+.I roff
+input
+(within a paragraph,
+say),
+changes to text formatting,
+such as font selection or vertical spacing,
+do not persist between entries.
+.
+.
+.P
+Several forms of table entry are interpreted specially.
+.
+.
+.IP \[bu] 2n
+If a table row contains only an underscore or equals sign
+.RB ( _
+or
+.BR = ),
+a single or double horizontal rule (line),
+respectively,
+is drawn across the table at that point.
+.
+.
+.IP \[bu] 2n
+A table entry containing only
+.B _
+or
+.B =
+on an otherwise populated row is replaced by a single or double
+horizontal rule,
+respectively,
+joining its
+neighbors.
+.
+.
+.IP \[bu] 2n
+Prefixing a lone underscore or equals sign with a backslash also has
+meaning.
+.
+If a table entry consists only of
+.B \[rs]_
+or
+.B \[rs]=
+on an otherwise populated row,
+it is replaced by a single or double horizontal rule,
+respectively,
+that does
+.I not
+(quite) join its neighbors.
+.
+.
+.IP \[bu]
+A table entry consisting of
+.BI \[rs]R x\c
+,
+where
+.IR x \~is
+any
+.I roff
+ordinary or special character,
+is replaced by enough repetitions of the glyph corresponding
+.RI to\~ x
+to fill the column,
+albeit without joining its neighbors.
+.\" TODO: Bad things happen if there's garbage in the entry after 'x',
+.\" which can be a *roff special character escape sequence, so
+.\" validation is not trivial.
+.
+.
+.IP \[bu]
+On any row but the first,
+a table entry of
+.B \[rs]\[ha]
+causes the entry above it to span down into the current one.
+.
+.
+.P
+On occasion,
+these special tokens may be required as literal table data.
+.
+To use either
+.B _
+or
+.B =
+literally and alone in an entry,
+prefix or suffix it with the
+.I roff
+dummy character
+.BR \[rs]& .
+.
+To express
+.BR \[rs]_ ,
+.BR \[rs]= ,
+or
+.BR \[rs]R ,
+use a
+.I roff
+escape sequence to interpolate the backslash
+.RB ( \[rs]e
+or
+.BR \[rs][rs] ).
+.
+A reliable way to emplace the
+.B \[rs]\[ha]
+glyph sequence within a table entry is to use a pair of
+.I groff
+special character escape sequences
+.RB ( \[rs][rs]\[rs][ha] ).
+.
+.
+.P
+Rows of table entries can be interleaved with
+.I groff
+control lines;
+these do not count as table data.
+.
+On such lines the default control character
+.RB ( .\& )
+must be used
+(and not changed);
+the no-break control character is not recognized.
+.
+To start the first table entry in a row with a dot,
+precede it with the
+.I roff
+dummy character
+.BR \[rs]& .
+.
+.
+.\" ====================================================================
+.SS "Text blocks"
+.\" ====================================================================
+.
+An ordinary table entry's contents can make a column,
+and therefore the table,
+excessively wide;
+the table then exceeds the line length of the page,
+and becomes ugly or is exposed to truncation by the output device.
+.
+When a table entry requires more conventional typesetting,
+breaking across more than one output line
+(and thereby increasing the height of its row),
+it can be placed within a
+.I text block.
+.
+.
+.P
+.I @g@tbl
+interprets a table entry beginning with
+.RB \[lq] T{ \[rq]
+at the end of an input line not as table data,
+but as a token starting a text block.
+.
+Similarly,
+.RB \[lq] T} \[rq]
+at the start of an input line ends a text block;
+it must also end the table entry.
+.
+Text block tokens can share an input line with other table data
+(preceding
+.B T{
+and following
+.BR T} ).
+.
+Input lines between these tokens are formatted in a diversion by
+.IR troff . \" generic
+.
+Text blocks cannot be nested.
+.
+Multiple text blocks can occur in a table row.
+.
+.
+.P
+Text blocks are formatted as was the text prior to the table,
+modified by applicable column descriptors.
+.
+Specifically,
+the classifiers
+.BR A ,
+.BR C ,
+.BR L ,
+.BR N ,
+.BR R ,
+and
+.B S
+determine a text block's
+.I alignment
+within its cell,
+but not its
+.I adjustment.
+.
+Add
+.B na
+or
+.B ad
+requests to the beginning of a text block to alter its adjustment
+distinctly from other text in the document.
+.
+As with other table entries,
+when a text block ends,
+any alterations to formatting parameters are discarded.
+.
+They do not affect subsequent table entries,
+not even other text blocks.
+.
+.
+.P
+.ne 2v
+If
+.B w
+or
+.B x
+modifiers are not specified for
+.I all
+columns of a text block's span,
+the default length of the text block
+(more precisely,
+the line length used to process the text block diversion)
+is computed as
+.IR L \[tmu] C /( N +1),
+.\" ...and rounded to the horizontal motion quantum of the output device
+where
+.I L
+is the current line length,
+.I C
+the number of columns spanned by the text block,
+and
+.I N
+the number of columns in the table.
+.
+If necessary,
+you can also control a text block's width by including an
+.B ll
+(line length)
+request in it prior to any text to be formatted.
+.
+Because a diversion is used to format the text block,
+its height and width are subsequently available in the registers
+.B dn
+and
+.BR dl ,
+respectively.
+.
+.
+.\" ====================================================================
+.SS \f[I]roff\f[] interface
+.\" ====================================================================
+.
+The register
+.B TW
+stores the width of the table region in basic units;
+it can't be used within the region itself,
+but is defined before the
+.B .TE
+token is output so that a
+.I groff
+macro named
+.B TE
+can make use of it.
+.
+.B T.\&
+is a Boolean-valued register indicating whether the bottom of the table
+is being processed.
+.
+The
+.B #T
+register marks the top of the table.
+.
+Avoid using these names for any other purpose.
+.
+.
+.P
+.I @g@tbl
+also defines a macro
+.B T#
+to produce the bottom and side lines of a boxed table.
+.
+While
+.I @g@tbl
+itself arranges for the output to include a call of this macro at the
+end of such a table,
+it can also be used by macro packages to create boxes for multi-page
+tables by calling it from a page footer macro that is itself called by
+a trap planted near the bottom of the page.
+.
+See section \[lq]Limitations\[rq] below for more on multi-page tables.
+.
+.
+.P
+GNU
+.I tbl \" GNU
+.\" AT&T tbl used all kinds of registers; many began with "3".
+internally employs register,
+string,
+macro,
+and diversion names beginning with the
+.RB numeral\~ 3 .
+.
+A document to be preprocessed with GNU
+.I tbl \" GNU
+should not use any such identifiers.
+.\" XXX: Why are they not named starting with "gtbl*" or something? GNU
+.\" tbl turns AT&T troff compatibility mode off anyway.
+.
+.
+.\" ====================================================================
+.SS "Interaction with \f[I]@g@eqn\f[]"
+.\" ====================================================================
+.
+.I @g@tbl
+should always be called before
+.MR @g@eqn @MAN1EXT@ .
+.
+(\c
+.MR groff @MAN1EXT@
+automatically arranges preprocessors in the correct order.)
+.
+Don't call the
+.B EQ
+and
+.B EN
+macros within tables;
+instead,
+set up delimiters in your
+.I eqn \" generic
+input and use the
+.B \%delim
+region option so that
+.I @g@tbl
+will recognize them.
+.
+.
+.br
+.ne 5v \" Keep enough space for heading, intro sentence, and first item.
+.\" ====================================================================
+.SS "GNU \f[I]tbl\f[] enhancements"
+.\" ====================================================================
+.
+In addition to extensions noted above,
+GNU
+.I tbl \" GNU
+removes constraints endured by users of AT&T
+.IR tbl .\" AT&T
+.
+.
+.IP \[bu] 2n
+Region options can be specified in any lettercase.
+.
+.
+.IP \[bu]
+There is no limit on the number of columns in a table,
+regardless of their classification,
+nor any limit on the number of text blocks.
+.
+.
+.IP \[bu]
+All table rows are considered when deciding column widths,
+not just those occurring in the first 200 input lines of a region.
+.
+Similarly,
+table continuation
+.RB ( .T& )
+tokens are recognized outside a region's first 200 input lines.
+.
+.
+.IP \[bu]
+Numeric and alphabetic entries may appear in the same column.
+.
+.
+.IP \[bu]
+Numeric and alphabetic entries may span horizontally.
+.
+.
+.\" ====================================================================
+.SS "Using GNU \f[I]tbl\f[] within macros"
+.\" ====================================================================
+.
+You can embed a table region inside a macro definition.
+.
+However,
+since
+.I @g@tbl
+writes its own macro definitions at the beginning of each table region,
+it is necessary to call end macros instead of ending macro definitions
+with
+.RB \[lq] ..\& \[rq].
+.\" XXX: Why don't we fix that by ending all of tbl's own macro
+.\" definitions with a call to a macro in its own reserved name space?
+.
+Additionally,
+the escape character must be disabled. \" XXX: Why?
+.
+.
+.P
+Not all
+.I @g@tbl
+features can be exercised from such macros because
+.I @g@tbl
+is a
+.I roff
+preprocessor:
+it sees the input earlier than
+.I @g@troff
+does.
+.
+For example,
+vertically aligning decimal separators fails if the numbers containing
+them occur as macro or string parameters;
+the alignment is performed by
+.I @g@tbl
+itself,
+which sees only
+.BR \[rs]$1 ,
+.BR \[rs]$2 ,
+and so on,
+and therefore can't recognize a decimal separator that only appears
+later when
+.I @g@troff
+interpolates a macro or string definition.
+.
+.
+.\" XXX: The following is a general caveat about preprocessors; move it.
+.P
+Using
+.I @g@tbl
+macros within conditional input
+(that is,
+contingent upon an
+.BR if ,
+.BR ie ,
+.BR el ,
+or
+.B while
+request)
+can result in misleading line numbers in subsequent diagnostics.
+.
+.I @g@tbl
+unconditionally injects its output into the source document,
+but the conditional branch containing it may not be taken,
+and if it is not,
+the
+.B lf
+requests that
+.I @g@tbl
+injects to restore the source line number cannot take effect.
+.
+Consider copying the input line counter register
+.B c.\&
+and restoring its value at a convenient location after applicable
+arithmetic.
+.
+.
+.br
+.ne 5v
+.\" ====================================================================
+.SH Options
+.\" ====================================================================
+.
+.B \-\-help
+displays a usage message,
+while
+.B \-v
+and
+.B \-\-version
+show version information;
+all exit afterward.
+.
+.
+.TP
+.B \-C
+Enable AT&T compatibility mode:
+recognize
+.B .TS
+and
+.B .TE
+even when followed by a character other than space or newline.
+.
+Furthermore,
+interpret the uninterpreted leader escape sequence
+.BR \[rs]a .
+.
+.
+.\" ====================================================================
+.SH Limitations
+.\" ====================================================================
+.
+Multi-page tables,
+if boxed and/or if you want their column headings repeated after page
+breaks,
+require support at the time the document is formatted.
+.
+A convention for such support has arisen in macro packages such as
+.IR ms ,
+.IR mm ,
+and
+.IR me .
+.
+To use it,
+follow the
+.B .TS
+token with a space and then
+.RB \[lq] H \[rq];
+this will be interpreted by the formatter
+as a
+.B TS
+macro call with an
+.B H
+argument.
+.
+Then,
+within the table data,
+call the
+.B TH
+macro;
+this informs the macro package where the headings end.
+.
+If your table has no such heading rows,
+or you do not desire their repetition,
+call
+.B TH
+immediately after the table format specification.
+.
+If a multi-page table is boxed or has repeating column headings,
+do not enclose it with keep/release macros,
+or divert it in any other way.
+.
+Further,
+the
+.B bp
+request will not cause a page break in a
+.RB \[lq] "TS H" \[rq]
+table.
+.
+Define a macro to wrap
+.BR bp :
+invoke it normally if there is no current diversion.
+.
+Otherwise,
+pass the macro call to the enclosing diversion using the transparent
+line escape sequence
+.BR \[rs]!\& ;
+this will \[lq]bubble up\[rq] the page break to the output device.
+.
+See section \[lq]Examples\[rq] below for a demonstration.
+.
+.
+.P
+Double horizontal rules are not supported by
+.MR grotty @MAN1EXT@ ;
+single rules are used instead.
+.
+.I \%grotty
+also ignores half-line motions,
+so the
+.B u
+column modifier has no effect.
+.
+On terminal devices
+.RI (\[lq] nroff\~ mode\[rq]),
+horizontal rules and box borders occupy a full vee of space;
+this amount is doubled for
+.B doublebox
+tables.
+.
+Tables using these features thus require more vertical space in
+.I nroff
+mode than in
+.I troff
+mode:
+write
+.B ne
+requests accordingly.
+.
+Vertical rules between columns are drawn in the space between columns in
+.I nroff
+mode;
+using double vertical rules and/or reducing the column separation below
+the default can make them ugly or overstrike them with table data.
+.
+.
+.P
+A text block within a table must be able to fit on one page.
+.
+.
+.P
+Using
+.B \[rs]a
+to put leaders in table entries does not work
+in GNU
+.IR tbl , \" GNU
+except in compatibility mode.
+.
+This is correct behavior:
+.B \[rs]a
+is an
+.I uninterpreted
+leader.
+.
+You can still use the
+.I roff
+leader character (Control+A) or define a string to use
+.B \[rs]a
+as it was designed:
+to be interpreted only in copy mode.
+.
+.
+.RS
+.P
+.EX
+\&.ds a \[rs]a
+\&.TS
+\&box center tab(;);
+\&Lw(2i)0 L.
+\&Population\[rs]*a;6,327,119
+\&.TE
+.EE
+.RE
+.
+.
+.\" We use a real leader to avoid defining a string in a man page.
+.P
+.TS
+box center tab(;);
+Lw(2i)0 L.
+Population;6,327,119
+.TE
+.
+.
+.P
+A leading and/or trailing
+.B |
+in a format specification,
+such as
+.RB \[lq] |LCR|.\& \[rq],
+produces an en space between the vertical rules and the content of the
+adjacent columns.
+.
+If no such space is desired
+(so that the rule abuts the content),
+you can introduce \[lq]dummy\[rq] columns with zero separation and empty
+corresponding table entries before and/or after.
+.
+.
+.RS
+.P
+.EX
+\&.TS
+\&center tab(#);
+\&R0|L C R0|L.
+_
+\&#levulose#glucose#dextrose#
+_
+\&.TE
+.EE
+.RE
+.
+.
+.P
+These dummy columns have zero width and are therefore invisible;
+unfortunately they usually don't work as intended on terminal devices.
+.
+.
+.if t \{\
+.TS
+center tab(#);
+R0|L C R0|L.
+_
+#levulose#glucose#dextrose#
+_
+.TE
+.\}
+.
+.
+.\" ====================================================================
+.SH Examples
+.\" ====================================================================
+.
+It can be easier to acquire the language of
+.I tbl \" generic
+through examples than formal description,
+especially at first.
+.
+.
+.\" Note: This example is nearly at the column limit (78n) for nroff
+.\" output. Recast with care.
+.RS
+.P
+.EX
+\&.TS
+box center tab(#);
+Cb Cb
+L L.
+Ability#Application
+Strength#crushes a tomato
+Dexterity#dodges a thrown tomato
+Constitution#eats a month-old tomato without becoming ill
+Intelligence#knows that a tomato is a fruit
+Wisdom#chooses \[rs]f[I]not\[rs]f[] to put tomato in a fruit salad
+Charisma#sells obligate carnivores tomato-based fruit salads
+\&.TE
+.EE
+.RE
+.
+.
+.P
+.TS
+box center tab(#);
+Cb Cb
+L L.
+Ability#Application
+Strength#crushes a tomato
+Dexterity#dodges a thrown tomato
+Constitution#eats a month-old tomato without becoming ill
+Intelligence#knows that a tomato is a fruit
+Wisdom#chooses \f[I]not\f[] to put tomato in a fruit salad
+Charisma#sells obligate carnivores tomato-based fruit salads
+.TE
+.
+.
+.P
+The
+.B A
+and
+.B N
+column classifiers can be easier to grasp in visual rendering than in
+description.
+.
+.
+.RS
+.P
+.EX
+\&.TS
+center tab(;);
+CbS,LN,AN.
+Daily energy intake (in MJ)
+Macronutrients
+\&.\[rs]" assume 3 significant figures of precision
+Carbohydrates;4.5
+Fats;2.25
+Protein;3
+\&.T&
+LN,AN.
+Mineral
+Pu\-239;14.6
+_
+\&.T&
+LN.
+Total;\[rs][ti]24.4
+\&.TE
+.EE
+.RE
+.
+.
+.RS
+.P
+.TS
+center tab(;);
+CbS,LN,AN.
+Daily energy intake (in MJ)
+.\" assume 3 significant figures of precision
+Macronutrients
+Carbohydrates;4.5
+Fats;2.25
+Protein;3
+.T&
+LN,AN.
+Mineral
+Pu-239;14.6
+_
+.T&
+LN.
+Total;\[ti]24.4
+.TE
+.RE
+.
+.
+.br
+.ne 12v
+.P
+Next,
+we'll lightly adapt a compact presentation of spanning,
+vertical alignment,
+and zero-width column modifiers from the
+.I mandoc
+reference for its
+.I tbl \" generic
+interpreter.
+.
+It rewards close study.
+.
+.
+.RS
+.P
+.EX
+\&.TS
+box center tab(:);
+Lz S | Rt
+Ld| Cb| \[ha]
+\[ha] | Rz S.
+left:r
+l:center:
+:right
+\&.TE
+.EE
+.RE
+.
+.
+.RS
+.P
+.TS
+box center tab(:);
+Lz S | Rt
+Ld| Cb| ^
+^ | Rz S.
+left:r
+l:center:
+:right
+.TE
+.RE
+.
+.
+.P
+.ne 2v
+Row staggering is not visually achievable on terminal devices,
+but a table using it can remain comprehensible nonetheless.
+.
+.
+.RS
+.P
+.EX
+\&.TS
+center tab(|);
+Cf(BI) Cf(BI) Cf(B), C C Cu.
+n|n\[rs]f[B]\[rs][tmu]\[rs]f[]n|difference
+1|1
+2|4|3
+3|9|5
+4|16|7
+5|25|9
+6|36|11
+\&.TE
+.EE
+.RE
+.
+.
+.RS
+.P
+.TS
+center tab(|);
+Cf(BI) Cf(BI) Cf(B), C C Cu.
+n|n\f[B]\[tmu]\f[]n|difference
+1|1
+2|4|3
+3|9|5
+4|16|7
+5|25|9
+6|36|11
+.TE
+.RE
+.
+.
+.P
+Some
+.I @g@tbl
+features cannot be illustrated in the limited environment of a portable
+man page.
+.
+.
+.\" TODO: Find a better example than this.
+.\".P
+.\"As noted above,
+.\"we can embed a table region in a
+.\".I groff
+.\"macro definition.
+.\".
+.\".IR @g@tbl ,
+.\"however,
+.\"cannot know what will result from any macro argument interpolations,
+.\"so we might confine such interpolations to one column of the table and
+.\"apply the
+.\".B x
+.\"modifier to it.
+.\".
+.\".
+.\".RS
+.\".P
+.\".EX
+.\"\&.de END
+.\"\&..
+.\"\&.eo
+.\"\&.de MYTABLE END
+.\"\&.TS
+.\"\&allbox tab(;);
+.\"\&C Lx.
+.\"\&This is table \[rs]$1.;\[rs]$2
+.\"\&.TE
+.\"\&.END
+.\"\&.ec
+.\"\&.MYTABLE 1 alpha
+.\"\&.MYTABLE 2 beta
+.\"\&.MYTABLE 3 "gamma delta"
+.\".EE
+.\".RE
+.\"
+.\"
+.P
+We can define a macro outside of a
+.I tbl \" generic
+region that we can call from within it to cause a page break inside a
+multi-page boxed table.
+.
+You can choose a different name;
+be sure to change both occurrences of \[lq]BP\[rq].
+.
+.
+.RS
+.P
+.ne 4v
+.EX
+\&.de BP
+\&.\& ie \[aq]\[rs]\[rs]n(.z\[aq]\[aq] \&.bp \[rs]\[rs]$1
+\&.\& el \[rs]!.BP \[rs]\[rs]$1
+\&..
+.EE
+.RE
+.
+.
+.\" ====================================================================
+.SH "See also"
+.\" ====================================================================
+.
+\[lq]Tbl\[em]A Program to Format Tables\[rq],
+by M.\& E.\& Lesk,
+1976
+(revised 16 January 1979),
+AT&T Bell Laboratories Computing Science Technical Report No.\& 49.
+.
+.
+.P
+The spanning example above was taken from
+.UR https://man.openbsd.org/tbl.7
+.IR mandoc 's
+man page for its
+.I tbl \" mandoc
+implementation
+.UE .
+.
+.
+.P
+.MR groff @MAN1EXT@ ,
+.MR @g@troff @MAN1EXT@
+.
+.
+.\" Restore compatibility mode (for, e.g., Solaris 10/11).
+.cp \n[*groff_tbl_1_man_C]
+.do rr *groff_tbl_1_man_C
+.
+.
+.\" Local Variables:
+.\" fill-column: 72
+.\" mode: nroff
+.\" End:
+.\" vim: set filetype=groff textwidth=72:
diff --git a/src/preproc/tbl/tbl.am b/src/preproc/tbl/tbl.am
new file mode 100644
index 0000000..d4b0edb
--- /dev/null
+++ b/src/preproc/tbl/tbl.am
@@ -0,0 +1,54 @@
+# Copyright (C) 2014-2020 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+prefixexecbin_PROGRAMS += tbl
+tbl_LDADD = libgroff.a $(LIBM) lib/libgnu.a
+tbl_SOURCES = \
+ src/preproc/tbl/main.cpp \
+ src/preproc/tbl/table.cpp \
+ src/preproc/tbl/table.h
+PREFIXMAN1 += src/preproc/tbl/tbl.1
+EXTRA_DIST += src/preproc/tbl/tbl.1.man
+
+tbl_TESTS = \
+ src/preproc/tbl/tests/boxes-and-vertical-rules.sh \
+ src/preproc/tbl/tests/check-horizontal-line-length.sh \
+ src/preproc/tbl/tests/check-line-intersections.sh \
+ src/preproc/tbl/tests/check-vertical-line-length.sh \
+ src/preproc/tbl/tests/cooperate-with-nm-request.sh \
+ src/preproc/tbl/tests/count-continued-input-lines.sh \
+ src/preproc/tbl/tests/do-not-overdraw-page-top-in-nroff-mode.sh \
+ src/preproc/tbl/tests/do-not-overlap-bottom-border-in-nroff.sh \
+ src/preproc/tbl/tests/do-not-segv-on-invalid-vertical-span-entry.sh \
+ src/preproc/tbl/tests/do-not-segv-when-backslash-R-in-text-block.sh \
+ src/preproc/tbl/tests/expand-region-option-works.sh \
+ src/preproc/tbl/tests/format-time-diagnostics-work.sh \
+ src/preproc/tbl/tests/save-and-restore-hyphenation-parameters.sh \
+ src/preproc/tbl/tests/save-and-restore-inter-sentence-space.sh \
+ src/preproc/tbl/tests/save-and-restore-line-numbering.sh \
+ src/preproc/tbl/tests/save-and-restore-tab-stops.sh \
+ src/preproc/tbl/tests/warn-on-long-boxed-unkept-table.sh \
+ src/preproc/tbl/tests/x-column-modifier-works.sh
+TESTS += $(tbl_TESTS)
+EXTRA_DIST += $(tbl_TESTS)
+
+
+# Local Variables:
+# fill-column: 72
+# mode: makefile-automake
+# End:
+# vim: set autoindent filetype=automake textwidth=72:
diff --git a/src/preproc/tbl/tests/boxes-and-vertical-rules.sh b/src/preproc/tbl/tests/boxes-and-vertical-rules.sh
new file mode 100755
index 0000000..0471188
--- /dev/null
+++ b/src/preproc/tbl/tests/boxes-and-vertical-rules.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+#
+# Copyright (C) 2023 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+fail=
+
+wail () {
+ echo ...FAILED >&2
+ fail=YES
+}
+
+# Test behavior of unexpanded tables using boxes and/or vertical rules.
+
+# Case 1: "naked" table
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+L L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table without vertical rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+# 3 spaces between table entries
+echo "$output" | sed -n '2p' \
+ | grep -qx 'abcdef abcdef abcdef abcdef abcdef' || wail
+
+# Case 2: left-hand vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+| L L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with left-hand rule" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+# 3 spaces between table entries
+echo "$output" | sed -n '3p' \
+ | grep -qx '| abcdef abcdef abcdef abcdef abcdef' || wail
+
+# Case 3: right-hand vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+L L L L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with right-hand rule" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+# 3 spaces between table entries
+echo "$output" | sed -n '3p' \
+ | grep -qx 'abcdef abcdef abcdef abcdef abcdef |' || wail
+
+# Case 4: vertical rule on both ends
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+| L L L L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with both rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+# 3 spaces between table entries
+echo "$output" | sed -n '3p' \
+ | grep -qx '| abcdef abcdef abcdef abcdef abcdef |' || wail
+
+# Case 5: vertical rule on both ends and interior rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+| L L L | L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with both edge and interior rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+# 3 spaces between table entries
+echo "$output" | sed -n '3p' \
+ | grep -qx '| abcdef abcdef abcdef | abcdef abcdef |' || wail
+
+# Case 6: boxed table
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+box tab(@);
+L L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking boxed table without interior rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+# 3 spaces between table entries
+echo "$output" | sed -n '3p' \
+ | grep -qx '| abcdef abcdef abcdef abcdef abcdef |' || wail
+
+# Case 7: boxed table with interior vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+box tab(@);
+L L L | L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking boxed table with interior rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+# 3 spaces between table entries
+echo "$output" | sed -n '3p' \
+ | grep -qx '| abcdef abcdef abcdef | abcdef abcdef |' || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/check-horizontal-line-length.sh b/src/preproc/tbl/tests/check-horizontal-line-length.sh
new file mode 100755
index 0000000..3d5e2a2
--- /dev/null
+++ b/src/preproc/tbl/tests/check-horizontal-line-length.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+fail=
+
+wail () {
+ echo "...FAILED" >&2
+ echo "$output"
+ fail=yes
+}
+
+# GNU tbl draws horizontal lines 1n wider than they need to be on nroff
+# devices to enable them to cross a vertical line on the right-hand
+# side.
+
+input='.ll 10n
+.TS
+L.
+_
+1234567890
+.TE
+.pl \n(nlu
+'
+
+echo "checking length of plain horizontal rule" >&2
+output=$(printf "%s" "$input" | "$groff" -Tascii -t)
+echo "$output" | grep -Eqx -- '-{11}' || wail
+
+input='.ll 12n
+.TS
+| L |.
+_
+1234567890
+_
+.TE
+.pl \n(nlu
+'
+
+echo "checking intersection of vertical and horizontal rules" >&2
+output=$(printf "%s" "$input" | "$groff" -Tascii -t)
+echo "$output" | sed -n '1p' | grep -Eqx '\+-{12}\+' || wail
+echo "$output" | sed -n '3p' | grep -Eqx '\+-{12}\+' || wail
+
+input='.ll 12n
+.TS
+box;
+L.
+1234567890
+.TE
+.pl \n(nlu
+'
+
+echo "checking width of boxed table" >&2
+output=$(printf "%s" "$input" | "$groff" -Tascii -t)
+echo "$output" | sed -n '1p' | grep -Eqx '\+-{12}\+' || wail
+echo "$output" | sed -n '3p' | grep -Eqx '\+-{12}\+' || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/check-line-intersections.sh b/src/preproc/tbl/tests/check-line-intersections.sh
new file mode 100755
index 0000000..2012beb
--- /dev/null
+++ b/src/preproc/tbl/tests/check-line-intersections.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+fail=
+
+wail () {
+ echo "...FAILED" >&2
+ echo "$output"
+ fail=yes
+}
+
+input='.TS
+allbox tab(@);
+L L L.
+a@b@c
+d@e@f
+g@h@i
+.TE
+'
+
+output=$(printf "%s" "$input" | "$groff" -Tascii -t)
+echo "$output"
+
+for l in 1 3 5 7
+do
+ echo "checking intersections on line $l"
+ echo "$output" | sed -n ${l}p | grep -Fqx '+---+---+---+' || wail
+done
+
+# TODO: Check `-Tutf8` output for correct crossing glyph identities.
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/check-vertical-line-length.sh b/src/preproc/tbl/tests/check-vertical-line-length.sh
new file mode 100755
index 0000000..1aafd09
--- /dev/null
+++ b/src/preproc/tbl/tests/check-vertical-line-length.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+fail=
+
+wail () {
+ echo "...FAILED" >&2
+ echo "$output"
+ fail=yes
+}
+
+# GNU tbl draws vertical lines 1v taller than they need to be on nroff
+# devices to enable them to cross a potential horizontal line in the
+# table.
+
+input='.ll 12n
+.TS
+| L |.
+_
+1234567890
+.TE
+.pl \n(nlu
+'
+
+echo "checking length of plain vertical rule" >&2
+output=$(printf "%s" "$input" | "$groff" -Tascii -t)
+echo "$output" | sed -n '2p' | grep -Fqx -- '| 1234567890 |' || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/cooperate-with-nm-request.sh b/src/preproc/tbl/tests/cooperate-with-nm-request.sh
new file mode 100755
index 0000000..cfbd750
--- /dev/null
+++ b/src/preproc/tbl/tests/cooperate-with-nm-request.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+#
+# Copyright (C) 2021 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+set -e
+
+# Regression-test Savannah #59812.
+#
+# A nonzero value of \n[ln] should not cause spurious numbering of table
+# rows.
+
+DOC='\
+.nf
+foo
+.nm 1
+bar
+.nm
+baz
+.TS
+l.
+qux
+.TE
+'
+
+OUTPUT=$(printf "%s" "$DOC" | "$groff" -Tascii -t)
+
+echo "$OUTPUT" | grep -Fqx qux
+
+# vim:set ai noet sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/count-continued-input-lines.sh b/src/preproc/tbl/tests/count-continued-input-lines.sh
new file mode 100755
index 0000000..4682788
--- /dev/null
+++ b/src/preproc/tbl/tests/count-continued-input-lines.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+set -e
+
+# Regression-test Savannah #62191.
+#
+# Line continuation in a row of table data should not make the input
+# line counter inaccurate.
+
+input='.this-is-line-1
+.TS
+L.
+foo\
+bar\
+baz\
+qux
+.TE
+.this-is-line-9'
+
+output=$(printf "%s\n" "$input" | "$groff" -Tascii -t -ww -z 2>&1)
+echo "$output" | grep -q '^troff.*:9:.*this-is-line-9'
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/do-not-overdraw-page-top-in-nroff-mode.sh b/src/preproc/tbl/tests/do-not-overdraw-page-top-in-nroff-mode.sh
new file mode 100755
index 0000000..c4698f1
--- /dev/null
+++ b/src/preproc/tbl/tests/do-not-overdraw-page-top-in-nroff-mode.sh
@@ -0,0 +1,225 @@
+#!/bin/sh
+#
+# Copyright (C) 2022-2023 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+grotty="${abs_top_builddir:-.}/grotty"
+
+fail=
+
+wail () {
+ echo ...FAILED >&2
+ fail=YES
+}
+
+# Regression-test Savannah #63449.
+#
+# In nroff mode, a table at the top of the page (i.e., one starting at
+# the first possible output line, with no vertical margin) that has
+# vertical rules should not overdraw the page top and provoke a warning
+# from grotty about "character(s) above [the] first line [being]
+# discarded".
+
+# Case 1: No horizontal rules; vertical rule at leading column.
+input='.TS
+| L.
+foo
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (1)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that a lone vertical rule starts the first output line"
+echo "$output" | sed -n '1p' | grep -Fqx '|' || wail
+
+# Case 2: No horizontal rules; vertical rule between columns.
+input='.TS
+tab(@);
+L | L.
+foo@bar
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (2)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that a lone vertical rule ends the first output line"
+echo "$output" | sed -n '1p' | grep -Eqx ' +\|' || wail
+
+# Case 3: No horizontal rules; vertical rule at trailing column.
+input='.TS
+L |.
+foo
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (3)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that a lone vertical rule ends the first output line"
+echo "$output" | sed -n '1p' | grep -Eqx ' +\|' || wail
+
+# Case 4: Vertical rule with horizontal rule in row description.
+input='.TS
+_
+L |.
+foo
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (4)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that intersection is placed on the first output line"
+echo "$output" | sed -n '1p' | grep -q '+' || wail
+
+# Case 5: Vertical rule with horizontal rule as first table datum.
+input='.TS
+L |.
+_
+foo
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (5)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that intersection is placed on the first output line"
+echo "$output" | sed -n '1p' | grep -q '+' || wail
+
+# Case 6: Horizontal rule as non-first row description with vertical
+# rule.
+input='.TS
+L,_,L |.
+foo
+bar
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (6)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that table data begin on first output line"
+echo "$output" | sed -n '1p' | grep -q 'foo' || wail
+
+# Also ensure that no collateral damage arises in related cases.
+
+# Case 7: Horizontal rule as first table datum with no vertical rule.
+input='.TS
+L.
+_
+foo
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (7)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that horizontal rule is placed on the first output line"
+echo "$output" | sed -n '1p' | grep -q '^---' || wail
+
+# Case 8: Horizontal rule as last table datum with no vertical rule.
+input='.TS
+L.
+foo
+_
+.TE
+.ec @
+.pl @n(nlu'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (8)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that horizontal rule is placed on the last output line"
+echo "$output" | sed -n '$p' | grep -q '^---' || wail
+
+# Case 9: Horizontal rule in row description with no vertical rule.
+input='.TS
+_
+L.
+foo
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (9)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that intersection is placed on the first output line"
+echo "$output" | sed -n '1p' | grep -q '^---' || wail
+
+# Case 10: Horizontal rule as non-first row description with no vertical
+# rule.
+input='.TS
+L,_,L.
+foo
+bar
+.TE'
+
+tmp=$(printf "%s\n" "$input" | "$groff" -t -Z -Tascii -P-cbou)
+output=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>/dev/null)
+error=$(printf "%s" "$tmp" | "$grotty" -F ./font 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that no diagnostic messages are produced by grotty (10)"
+echo "$error" | grep -q 'grotty:' && wail
+
+echo "checking that table data begin on first output line"
+echo "$output" | sed -n '1p' | grep -q 'foo' || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/do-not-overlap-bottom-border-in-nroff.sh b/src/preproc/tbl/tests/do-not-overlap-bottom-border-in-nroff.sh
new file mode 100755
index 0000000..4f63eed
--- /dev/null
+++ b/src/preproc/tbl/tests/do-not-overlap-bottom-border-in-nroff.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+fail=
+
+wail () {
+ echo "...FAILED" >&2
+ fail=yes
+}
+
+# Regression-test Savannah #49390.
+
+input='foo
+.TS
+box;
+L.
+bar
+.TE
+baz
+.pl \n(nlu
+'
+
+echo "checking for post-table text non-overlap of (single) box border"
+output=$(printf "%s" "$input" | "$groff" -t -Tascii)
+echo "$output" | grep -q baz || wail
+
+input='foo
+.TS
+doublebox;
+L.
+bar
+.TE
+baz
+.pl \n(nlu
+'
+
+echo "checking for post-table text non-overlap of double box border"
+output=$(printf "%s" "$input" | "$groff" -t -Tascii)
+echo "$output" | grep -q baz || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/do-not-segv-on-invalid-vertical-span-entry.sh b/src/preproc/tbl/tests/do-not-segv-on-invalid-vertical-span-entry.sh
new file mode 100755
index 0000000..1d672ec
--- /dev/null
+++ b/src/preproc/tbl/tests/do-not-segv-on-invalid-vertical-span-entry.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+#
+# Copyright (C) 2021 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+tbl="${abs_top_builddir:-.}/tbl"
+
+# Regression-test Savannah #61417.
+#
+# Don't segfault because we tried to span down from an invalid span that
+# tbl neglected to replace with an empty table entry.
+
+test -f core && exit 77 # skip
+
+input=$(cat <<EOF
+.TS
+l.
+\^
+\^
+.TE
+EOF
+)
+output=$(printf "%s" "$input" | "$tbl")
+! test -f core
+
+# vim:set ai noet sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/do-not-segv-when-backslash-R-in-text-block.sh b/src/preproc/tbl/tests/do-not-segv-when-backslash-R-in-text-block.sh
new file mode 100755
index 0000000..30bd9f6
--- /dev/null
+++ b/src/preproc/tbl/tests/do-not-segv-when-backslash-R-in-text-block.sh
@@ -0,0 +1,75 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+fail=
+
+wail () {
+ echo "...FAILED" >&2
+ fail=yes
+}
+
+# Regression-test Savannah #62366.
+#
+# Do not SEGV when a text block begins with a repeating glyph token, and
+# do not malformat the output if it ends with one.
+
+test -f core && exit 77 # skip
+
+input='.TS
+L.
+T{
+\Ra
+T}
+.TE
+.TS
+L.
+T{
+foo
+\Ra
+T}
+.TE
+.TS
+L.
+T{
+foo
+\Ra
+bar
+T}
+.TE'
+
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii -P-cbou)
+
+echo "checking that tbl doesn't segfault" >&2
+test -f core && wail
+
+echo "checking text block starting with repeating glyph" >&2
+echo "$output" | sed -n 1p | grep -qx 'a' || wail
+
+echo "checking text block ending with repeating glyph" >&2
+echo "$output" | sed -n 2p | grep -qx 'foo a' || wail
+
+echo "checking text block containing repeating glyph" >&2
+echo "$output" | sed -n 3p | grep -qx 'foo a bar' || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/expand-region-option-works.sh b/src/preproc/tbl/tests/expand-region-option-works.sh
new file mode 100755
index 0000000..97da39d
--- /dev/null
+++ b/src/preproc/tbl/tests/expand-region-option-works.sh
@@ -0,0 +1,173 @@
+#!/bin/sh
+#
+# Copyright (C) 2023 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+fail=
+
+wail () {
+ echo ...FAILED >&2
+ fail=YES
+}
+
+# Ensure that the "expand" region option expands to the line length.
+
+# Case 1: "naked" table
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+expand tab(@);
+L L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table without vertical rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '2p' \
+ | grep -Eqx '(abcdef {8,9}){4}abcdef' || wail
+
+# Case 2: left-hand vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+expand tab(@);
+| L L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with left-hand rule" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx '\| abcdef {8}abcdef {7}abcdef {8}abcdef {9}abcdef' \
+ || wail
+
+# Case 3: right-hand vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+expand tab(@);
+L L L L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with right-hand rule" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx 'abcdef {8}abcdef {7}abcdef {8}abcdef {9}abcdef \|' \
+ || wail
+
+# Case 4: vertical rule on both ends
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+expand tab(@);
+| L L L L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with both rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx '\| abcdef {7}abcdef {7}abcdef {8}abcdef {8}abcdef \|' \
+ || wail
+
+# Case 5: vertical rule on both ends and interior rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+expand tab(@);
+| L L L | L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with both edge and interior rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx \
+ '\| abcdef {7}abcdef {7}abcdef {4}\| {3}abcdef {8}abcdef \|' || wail
+
+# Case 6: boxed table
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+box expand tab(@);
+L L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking boxed table without interior rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx '\| abcdef {7}abcdef {7}abcdef {8}abcdef {8}abcdef \|' \
+ || wail
+
+# Case 7: boxed table with interior vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+box expand tab(@);
+L L L | L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking boxed table with interior rule" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx \
+ '\| abcdef {7}abcdef {7}abcdef {4}\| {3}abcdef {8}abcdef \|' || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/format-time-diagnostics-work.sh b/src/preproc/tbl/tests/format-time-diagnostics-work.sh
new file mode 100755
index 0000000..9d422bd
--- /dev/null
+++ b/src/preproc/tbl/tests/format-time-diagnostics-work.sh
@@ -0,0 +1,268 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+set -e
+
+# Ensure we get diagnostics when we expect to, and not when we don't.
+#
+# Do NOT pattern-match the text of the diagnostic messages; those should
+# be left flexible. (Some day they might even be localized.)
+
+# As of this writing, there are 5 distinct format-time diagnostic
+# messages that tbl writes roff code to generate, one of which can be
+# produced two different ways.
+
+# Diagnostic #1: a row overruns the page bottom
+input='.pl 2v
+.TS
+;
+L.
+T{
+.nf
+1
+2
+3
+T}
+.TE
+'
+
+echo "checking for diagnostic when row with text box overruns page" \
+ "bottom"
+output=$(printf "%s" "$input" | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+echo "checking 'nowarn' suppression of diagnostic when row with text" \
+ "box overruns page bottom"
+input_nowarn=$(printf "%s" "$input" | sed 's/;/nowarn;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 0
+
+echo "checking 'nokeep' suppression of diagnostic when row with text" \
+ "box overruns page bottom"
+input_nowarn=$(printf "%s" "$input" | sed 's/;/nokeep;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 0
+
+# The other way to get "diagnostic #1" is to have a row that is too
+# tall _without_ involving a text block, for instance by having a font
+# or vertical spacing that is too high.
+input='.pl 2v
+.vs 3v
+.TS
+;
+L.
+1
+.TE
+'
+
+echo "checking for diagnostic when row with large vertical spacing" \
+ "overruns page bottom"
+output=$(printf "%s" "$input" | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+echo "checking 'nowarn' suppression of diagnostic when row with large" \
+ "vertical spacing overruns page bottom"
+input_nowarn=$(printf "%s" "$input" | sed 's/;/nowarn;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 0
+
+echo "checking 'nokeep' suppression of diagnostic when row with large" \
+ "vertical spacing overruns page bottom"
+input_nowarn=$(printf "%s" "$input" | sed 's/;/nokeep;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 0
+
+# Diagnostic #2: a boxed table won't fit on a page
+
+input='.pl 2v
+.vs 3v
+.TS
+box;
+L.
+1
+.TE
+'
+
+echo "checking for diagnostic when boxed table won't fit on page"
+output=$(printf "%s" "$input" | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+# The above is an error, so the "nowarn" region option won't shut it up.
+#
+# However, "nokeep" does--but arguably shouldn't. See
+# <https://savannah.gnu.org/bugs/?61878>. If that gets fixed, we should
+# test that we still get a diagnostic even with the option given.
+
+# Diagnostic #3: unexpanded columns overrun the line length
+#
+# Case 1: no 'x' column modifiers used
+
+input='.pl 2v
+.ll 10n
+.TS
+;
+L.
+12345678901
+.TE
+'
+
+echo "checking for diagnostic when unexpanded columns overrun line" \
+ "length (1)"
+output=$(printf "%s" "$input" | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+echo "checking 'nowarn' suppression of diagnostic when unexpanded" \
+ "columns overrun line length (1)"
+input_nowarn=$(printf "%s" "$input" | sed 's/;/nowarn;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 0
+
+# Avoiding keeps won't get you out of this one.
+echo "checking 'nokeep' NON-suppression of diagnostic when unexpanded" \
+ "columns overrun line length (1)"
+input_nowarn=$(printf "%s" "$input" | sed 's/;/nokeep;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+# Case 2: 'x' column modifier used
+#
+# This worked as a "get out of jail (warning) free" card in groff 1.22.4
+# and earlier; i.e., it incorrectly suppressed the warning. See
+# Savannah #61854.
+
+input='.pl 2v
+.ll 10n
+.TS
+;
+Lx.
+12345678901
+.TE
+'
+
+echo "checking for diagnostic when unexpanded columns overrun line" \
+ "length (2)"
+output=$(printf "%s" "$input" | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+echo "checking 'nowarn' suppression of diagnostic when unexpanded" \
+ "columns overrun line length (2)"
+input_nowarn=$(printf "%s" "$input" | sed 's/;/nowarn;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 0
+
+# Avoiding keeps won't get you out of this one.
+echo "checking 'nokeep' NON-suppression of diagnostic when unexpanded" \
+ "columns overrun line length (2)"
+input_nowarn=$(printf "%s" "$input" | sed 's/;/nokeep;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+# Diagnostic #4: expanded table gets all column separation squashed out
+
+input='.pl 3v
+.ll 10n
+.TS
+tab(;) expand;
+L L.
+abcde;fghij
+.TE
+'
+
+echo "checking for diagnostic when region-expanded table has column" \
+ "separation eliminated"
+output=$(printf "%s" "$input" | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+echo "checking 'nowarn' suppression of diagnostic when" \
+ "region-expanded table has column separation eliminated"
+input_nowarn=$(printf "%s" "$input" | sed 's/;$/ nowarn;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 0
+
+# Avoiding keeps won't get you out of this one.
+echo "checking 'nokeep' NON-suppression of diagnostic when" \
+ "region-expanded table has column separation eliminated"
+input_nowarn=$(printf "%s" "$input" | sed 's/;$/ nokeep;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+# Diagnostic #5: expanded table gets column separation reduced
+
+input='.pl 3v
+.ll 10n
+.TS
+tab(;) expand;
+L L.
+abcd;efgh
+.TE
+'
+
+echo "checking for diagnostic when region-expanded table has column" \
+ "separation reduced"
+output=$(printf "%s" "$input" | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+echo "checking 'nowarn' suppression of diagnostic when" \
+ "region-expanded table has column separation reduced"
+input_nowarn=$(printf "%s" "$input" | sed 's/;$/ nowarn;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 0
+
+# Avoiding keeps won't get you out of this one.
+echo "checking 'nokeep' NON-suppression of diagnostic when" \
+ "region-expanded table has column separation reduced"
+input_nowarn=$(printf "%s" "$input" | sed 's/;$/ nokeep;/')
+output=$(printf "%s" "$input_nowarn" \
+ | "$groff" -Tascii -t 2>&1 >/dev/null)
+nlines=$(echo "$output" | grep . | wc -l)
+test $nlines -eq 1
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/save-and-restore-hyphenation-parameters.sh b/src/preproc/tbl/tests/save-and-restore-hyphenation-parameters.sh
new file mode 100755
index 0000000..e368b31
--- /dev/null
+++ b/src/preproc/tbl/tests/save-and-restore-hyphenation-parameters.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+#
+# Copyright (C) 2021 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+set -e
+
+# Regression-test Savannah #59971.
+#
+# Hyphenation needs to be restored between (and after) text blocks just
+# as adjustment is.
+
+EXAMPLE='.nr LL 78n
+.hw a-bc-def-ghij-klmno-pqrstu-vwxyz
+.LP
+Here is a table with hyphenation disabled in its text block.
+.
+.TS
+l lx.
+foo T{
+.nh
+abcdefghijklmnopqrstuvwxyz
+abcdefghijklmnopqrstuvwxyz
+abcdefghijklmnopqrstuvwxyz
+T}
+.TE
+.
+Let us see if hyphenation is enabled again as it should be.
+abcdefghijklmnopqrstuvwxyz'
+
+OUTPUT=$(printf "%s\n" "$EXAMPLE" | "$groff" -Tascii -P-cbou -t -ms)
+
+echo "$OUTPUT"
+
+echo "testing whether hyphenation disabled in table text block" >&2
+! echo "$OUTPUT" | grep '^foo' | grep -- '-$'
+
+echo "testing whether hyphenation enabled after table" >&2
+echo "$OUTPUT" | grep -qx 'Let us see.*lmno-'
+
+# vim:set ai noet sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/save-and-restore-inter-sentence-space.sh b/src/preproc/tbl/tests/save-and-restore-inter-sentence-space.sh
new file mode 100755
index 0000000..e9a06d8
--- /dev/null
+++ b/src/preproc/tbl/tests/save-and-restore-inter-sentence-space.sh
@@ -0,0 +1,79 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+fail=
+
+wail () {
+ echo ...FAILED >&2
+ fail=YES
+}
+
+# Regression-test Savannah #61909.
+#
+# Inter-sentence space should not be applied to the content of ordinary
+# table entries. They are set "rigidly" (tbl(1)), also without filling,
+# adjustment, hyphenation or breaking. If you want those things, use a
+# text block.
+
+input='.ss 12 120
+Before one.
+Before two.
+.TS
+L.
+.\" two spaces
+Foo. Bar.
+.\" four spaces
+Baz. Qux.
+.\" two spaces
+T{
+Ack. Nak.
+T}
+.TE
+After one.
+After two.
+'
+
+output=$(printf "%s\n" "$input" | "$groff" -Tascii -P-cbou -t)
+echo "$output"
+
+echo "checking that inter-sentence space is altered too early"
+echo "$output" \
+ | grep -Fqx 'Before one. Before two.' || wail # 11 spaces
+
+echo "checking that inter-sentence space is not applied to ordinary" \
+ "table entries (1)"
+echo "$output" | grep -Fqx 'Foo. Bar.' || wail # 2 spaces
+
+echo "checking that inter-sentence space is not applied to ordinary" \
+ "table entries (2)"
+echo "$output" | grep -Fqx 'Baz. Qux.' || wail # 4 spaces
+
+echo "checking that inter-sentence space is applied to text blocks"
+echo "$output" | grep -Fqx 'Ack. Nak.' || wail # 11 spaces
+
+echo "checking that inter-sentence space is restored after table"
+echo "$output" \
+ | grep -Fqx 'After one. After two.' || wail # 11 spaces
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/save-and-restore-line-numbering.sh b/src/preproc/tbl/tests/save-and-restore-line-numbering.sh
new file mode 100755
index 0000000..592b43a
--- /dev/null
+++ b/src/preproc/tbl/tests/save-and-restore-line-numbering.sh
@@ -0,0 +1,85 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+fail=
+
+wail () {
+ echo ...FAILED >&2
+ fail=YES
+}
+
+# Regression-test Savannah #60140.
+#
+# Line numbering needs to be suspended within a table and restored
+# afterward. Historical implementations handled line numbering in
+# tables badly when text blocks were used.
+
+input='.nm 1
+Here is a line of output.
+Sic transit adispicing meatballs.
+We pad it out with more content to ensure that the line breaks.
+.TS
+L.
+This is my table.
+There are many like it but this one is mine.
+T{
+Ut enim ad minima veniam,
+quis nostrum exercitationem ullam corporis suscipitlaboriosam,
+nisi ut aliquid ex ea commodi consequatur?
+T}
+.TE
+What is the line number now?'
+
+output=$(printf "%s\n" "$input" | "$groff" -Tascii -P-cbou -t)
+echo "$output"
+
+echo "testing that line numbering is suppressed in table" >&2
+echo "$output" | grep -Fqx 'This is my table.' || wail
+
+echo "testing that line numbering is restored after table" >&2
+echo "$output" | grep -Eq '3 +What is the line number now\?' || wail
+
+input='.nf
+.nm 1
+test of line numbering suppression
+five
+four
+.nn 3
+three
+.TS
+L.
+I am a table.
+I have two rows.
+.TE
+two
+one
+numbering returns here'
+
+output=$(printf "%s\n" "$input" | "$groff" -Tascii -P-cbou -t)
+echo "$output"
+
+echo "testing that suppressed numbering is restored correctly" >&2
+echo "$output" | grep -Eq '4 +numbering returns here' || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/save-and-restore-tab-stops.sh b/src/preproc/tbl/tests/save-and-restore-tab-stops.sh
new file mode 100755
index 0000000..b98922a
--- /dev/null
+++ b/src/preproc/tbl/tests/save-and-restore-tab-stops.sh
@@ -0,0 +1,84 @@
+#!/bin/sh
+#
+# Copyright (C) 2020 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+# Regression-test Savannah #42978.
+#
+# When tbl changes the tab stops, it needs to restore them.
+#
+# Based on an example by Bjarni Igni Gislason.
+
+EXAMPLE='.TH tbl\-tabs\-test 1 2020-10-20 "groff test suite"
+.SH Name
+tbl\-tabs\-test \- see if tbl messes up the tab stops
+.SH Description
+Do not use tabs in man pages outside of
+.BR .TS / .TE
+regions.
+.PP
+But if you do.\|.\|.
+.PP
+.TS
+l l l.
+table entries long enough to change the tab stops
+.TE
+.PP
+.EX
+#!/bin/sh
+case $#
+1)
+ if foo
+ then
+ bar
+ else
+ if baz
+ then
+ qux
+ fi
+ fi
+;;
+esac
+.EE'
+
+OUTPUT=$(printf "%s\n" "$EXAMPLE" | "$groff" -Tascii -P-cbou -t -man)
+FAIL=
+
+if ! echo "$OUTPUT" | grep -Eq '^ {12}if foo$'
+then
+ FAIL=yes
+ echo "first tab stop is wrong" >&2
+fi
+
+if ! echo "$OUTPUT" | grep -Eq '^ {17}bar$'
+then
+ FAIL=yes
+ echo "second tab stop is wrong" >&2
+fi
+
+if ! echo "$OUTPUT" | grep -Eq '^ {22}qux$'
+then
+ FAIL=yes
+ echo "third tab stop is wrong" >&2
+fi
+
+test -z "$FAIL"
+
+# vim:set ai noet sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/warn-on-long-boxed-unkept-table.sh b/src/preproc/tbl/tests/warn-on-long-boxed-unkept-table.sh
new file mode 100755
index 0000000..621a752
--- /dev/null
+++ b/src/preproc/tbl/tests/warn-on-long-boxed-unkept-table.sh
@@ -0,0 +1,56 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+fail=
+
+wail () {
+ echo ...FAILED >&2
+ fail=YES
+}
+
+# Regression-test Savannah #61878.
+#
+# A boxed, unkept table that overruns the page bottom will produce ugly
+# output; it looks especially bizarre in nroff mode.
+#
+# We set the page length to 2v to force a problem (any boxed table in
+# nroff mode needs 3 vees minimum), and put a page break at the start to
+# catch an incorrectly initialized starting page number for the table.
+
+input='.pl 2v
+.bp
+.TS
+box nokeep;
+L.
+Z
+.TE'
+
+output=$(printf "%s" "$input" | "$groff" -t -Tascii 2>/dev/null)
+error=$(printf "%s" "$input" | "$groff" -t -Tascii 2>&1 >/dev/null)
+echo "$output"
+
+echo "checking that a diagnostic message is produced"
+echo "$error" | grep -q 'warning: boxed.*page 2$' || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72:
diff --git a/src/preproc/tbl/tests/x-column-modifier-works.sh b/src/preproc/tbl/tests/x-column-modifier-works.sh
new file mode 100755
index 0000000..da9b890
--- /dev/null
+++ b/src/preproc/tbl/tests/x-column-modifier-works.sh
@@ -0,0 +1,172 @@
+#!/bin/sh
+#
+# Copyright (C) 2023 Free Software Foundation, Inc.
+#
+# This file is part of groff.
+#
+# groff is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# groff is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+groff="${abs_top_builddir:-.}/test-groff"
+
+fail=
+
+wail () {
+ echo ...FAILED >&2
+ fail=YES
+}
+
+# Ensure that the "x" column modifier causes table expansion to the line
+# length.
+
+# Case 1: "naked" table
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+Lx L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table without vertical rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '2p' \
+ | grep -Eqx 'abcdef {25}(abcdef {3}){3}abcdef' || wail
+
+# Case 2: left-hand vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+| Lx L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with left-hand rule" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx '\| abcdef {23}(abcdef {3}){3}abcdef' || wail
+
+# Case 3: right-hand vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+Lx L L L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with right-hand rule" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx 'abcdef {23}(abcdef {3}){3}abcdef \|' || wail
+
+# Case 4: vertical rule on both ends
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+| Lx L L L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with both rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx '\| abcdef {21}(abcdef {3}){3}abcdef \|' || wail
+
+# Case 5: vertical rule on both ends and interior rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+tab(@);
+| Lx L L | L L |.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking unboxed table with both edge and interior rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx \
+ '\| abcdef {21}abcdef {3}abcdef \| abcdef {3}abcdef \|' \
+ || wail
+
+# Case 6: boxed table
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+box tab(@);
+Lx L L L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking boxed table without interior rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx '\| abcdef {21}(abcdef {3}){3}abcdef \|' || wail
+
+# Case 7: boxed table with interior vertical rule
+
+input='.ll 64n
+.nf
+1234567890123456789012345678901234567890123456789012345678901234
+.fi
+.TS
+box tab(@);
+Lx L L | L L.
+abcdef@abcdef@abcdef@abcdef@abcdef
+.TE
+.pl \n(nlu'
+
+echo "checking boxed table with interior rules" >&2
+output=$(printf "%s\n" "$input" | "$groff" -t -Tascii)
+echo "$output"
+echo "$output" | sed -n '3p' \
+ | grep -Eqx \
+ '\| abcdef {21}abcdef {3}abcdef \| abcdef {3}abcdef \|' \
+ || wail
+
+test -z "$fail"
+
+# vim:set ai et sw=4 ts=4 tw=72: