diff options
Diffstat (limited to 'src/preproc/tbl')
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 +\¢er 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: |